Skip to content

Commit

Permalink
update decoder interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ChrisShank committed Sep 13, 2024
1 parent a4d9e16 commit dd9ce09
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 122 deletions.
125 changes: 53 additions & 72 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,12 @@
import test from 'node:test';
import assert from 'node:assert';
import {
RouteData,
RouteParser,
boolean,
date,
datetime,
float,
int,
route,
string,
} from './index.js';
import { RouteData, RouteParser, boolean, date, datetime, num, route, string } from './index.js';

type DecodeFixture =
| {
route: () => RouteParser;
input: string;
expectedMatch: ReturnType<RouteParser['decode']>;
error?: undefined;
}
| {
route: () => RouteParser;
input: string;
expectedMatch?: undefined;
error: true;
};
interface DecodeFixture {
route: () => RouteParser;
input: string;
expectedMatch: ReturnType<RouteParser['decode']>;
}

// Adapted from the URLPattern test suite: https://github.com/kenchris/urlpattern-polyfill/blob/main/test/urlpatterntestdata.json
const URLPatternDecoderFixtures: DecodeFixture[] = [
Expand Down Expand Up @@ -53,7 +35,7 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
expectedMatch: null,
},
{
route: () => route`/foo/${['bar', string]}`,
route: () => route`/foo/${string('bar')}`,
input: '/foo/bar',
expectedMatch: {
params: { bar: 'bar' },
Expand All @@ -62,7 +44,7 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/foo/${['bar', string]}`,
route: () => route`/foo/${string('bar')}`,
input: '/foo/index.html',
expectedMatch: {
params: { bar: 'index.html' },
Expand All @@ -71,17 +53,17 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/foo/${['bar', string]}`,
route: () => route`/foo/${string('bar')}`,
input: '/foo/',
expectedMatch: null,
},
{
route: () => route`/foo/${['bar', string]}`,
route: () => route`/foo/${string('bar')}`,
input: '/foo/bar/',
expectedMatch: null,
},
{
route: () => route`/${['café', string]}`,
route: () => route`/${string('café')}`,
input: '/foo',
expectedMatch: {
params: { café: 'foo' },
Expand All @@ -90,7 +72,7 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['\u2118', string]}`,
route: () => route`/${string('\u2118')}`,
input: '/foo',
expectedMatch: {
params: { '\u2118': 'foo' },
Expand All @@ -99,7 +81,7 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['\u3400', string]}`,
route: () => route`/${string('\u3400')}`,
input: '/foo',
expectedMatch: {
params: { '\u3400': 'foo' },
Expand Down Expand Up @@ -134,6 +116,16 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
hash: '',
},
},
/* should we parse this TTL
{
route: () => route`/café`,
input: '/café',
expectedMatch: {
params: {},
search: {},
hash: '',
},
}, */
{
route: () => route`/caf%c3%a9`,
input: '/café',
Expand Down Expand Up @@ -180,7 +172,7 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`${['name', string]}.html`,
route: () => route`${string('name')}.html`,
input: 'foo.html',
expectedMatch: {
params: { name: 'foo' },
Expand All @@ -197,25 +189,20 @@ const URLPatternDecoderFixtures: DecodeFixture[] = [
hash: '',
},
},
{
route: () => route`/${['id', string]}/${['id', string]}`,
input: 'Throw error',
error: true,
},
];

const decoderFixtures: DecodeFixture[] = [
{
route: () => route`/${['int', int]}`,
route: () => route`/${num('num')}`,
input: '/1',
expectedMatch: {
params: { int: 1 },
params: { num: 1 },
search: {},
hash: '',
},
},
{
route: () => route`/${['float', float]}`,
route: () => route`/${num('float')}`,
input: '/1.01',
expectedMatch: {
params: { float: 1.01 },
Expand All @@ -224,7 +211,7 @@ const decoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['boolean', boolean]}`,
route: () => route`/${boolean('boolean')}`,
input: '/true',
expectedMatch: {
params: { boolean: true },
Expand All @@ -233,7 +220,7 @@ const decoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['boolean', boolean]}`,
route: () => route`/${boolean('boolean')}`,
input: '/false',
expectedMatch: {
params: { boolean: false },
Expand All @@ -242,7 +229,7 @@ const decoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['date', date]}`,
route: () => route`/${date('date')}`,
input: '/01-01-2000',
expectedMatch: {
params: { date: new Date('01-01-2000') },
Expand All @@ -251,7 +238,7 @@ const decoderFixtures: DecodeFixture[] = [
},
},
{
route: () => route`/${['datetime', datetime]}`,
route: () => route`/${datetime('datetime')}`,
input: '/2000-01-01T07%3A07%3A06.664Z',
expectedMatch: {
params: { datetime: new Date('2000-01-01T07:07:06.664Z') },
Expand All @@ -263,29 +250,17 @@ const decoderFixtures: DecodeFixture[] = [

const decodeFixtures = [...URLPatternDecoderFixtures, ...decoderFixtures];

for (const { route, input, expectedMatch, error } of decodeFixtures) {
for (const { route, input, expectedMatch } of decodeFixtures) {
test(`Decode: '${input}' for ${route}`, () => {
if (error) {
assert.throws(() => route());
} else {
assert.deepStrictEqual(route().decode(input), expectedMatch);
}
assert.deepStrictEqual(route().decode(input), expectedMatch);
});
}

type EncodeFixture =
| {
route: () => RouteParser;
input: RouteData<Record<string, any>>;
expectedMatch: string;
error?: undefined;
}
| {
route: () => RouteParser;
input: RouteData<Record<string, any>>;
expectedMatch?: undefined;
error: true;
};
interface EncodeFixture {
route: () => RouteParser;
input: RouteData<Record<string, any>>;
expectedMatch: string;
}

// Adapted from the URLPattern test suite: https://github.com/kenchris/urlpattern-polyfill/blob/main/test/urlpatterntestdata.json
const URLPatternEncoderFixtures: EncodeFixture[] = [
Expand Down Expand Up @@ -380,14 +355,14 @@ const URLPatternEncoderFixtures: EncodeFixture[] = [

const encoderFixtures: EncodeFixture[] = [
{
route: () => route`/${['int', int]}`,
route: () => route`/${['num', num]}`,
input: {
params: { int: 1 },
params: { num: 1 },
},
expectedMatch: '/1',
},
{
route: () => route`/${['float', float]}`,
route: () => route`/${['float', num]}`,
input: {
params: { float: 1.01 },
},
Expand Down Expand Up @@ -425,12 +400,18 @@ const encoderFixtures: EncodeFixture[] = [

const encodeFixtures = [...URLPatternEncoderFixtures, ...encoderFixtures];

for (const { route, input, expectedMatch, error } of encodeFixtures) {
for (const { route, input, expectedMatch } of encodeFixtures) {
test(`Encode: '${expectedMatch}' for ${route}`, () => {
if (error) {
assert.throws(() => route());
} else {
assert.deepStrictEqual(route().encode(input), expectedMatch);
}
assert.deepStrictEqual(route().encode(input), expectedMatch);
});
}

type ErrorFixture = () => RouteParser;

const errorFixtures: ErrorFixture[] = [() => route`/${string('id')}/${string('id')}`];

for (const route of errorFixtures) {
test(`Throws: '${route}'`, () => {
assert.throws(() => route());
});
}
84 changes: 34 additions & 50 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,49 @@
export type NamedRouteParameter<Name extends string = string, Data = any> = [
name: Name,
decoder: Decoder<Data>
];

export interface Decoder<Data> {
readonly matcher: string;
/** Parse a string in data. */
<Name extends string>(name: Name): NamedRouteParameter<Name, Data>;
decode: (blob: string) => Data;
/** Convert data into a string */
encode: (data: Data) => string;
}

export const string: Decoder<string> = {
matcher: '([^/]+)',
decode: (blob) => blob,
encode: (data) => data,
};
export const string: Decoder<string> = <Name extends string>(name: Name) => [name, string];
string.decode = (blob) => blob;
string.encode = (data) => data;

export const boolean: Decoder<boolean> = {
matcher: '([^/]+)',
decode: (blob) => blob === 'true',
encode: (data) => data.toString(),
};
export const boolean: Decoder<boolean> = <Name extends string>(name: Name) => [name, boolean];
boolean.decode = (blob) => blob === 'true';
boolean.encode = (data) => data.toString();

export const int: Decoder<number> = {
matcher: '([^/]+)',
decode: (blob) => +blob,
encode: (data) => data.toString(),
};
export const num: Decoder<number> = <Name extends string>(name: Name) => [name, num];
num.decode = (blob) => +blob;
num.encode = (data) => data.toString();

export const float: Decoder<number> = {
matcher: '([^/]+)',
decode: (blob) => parseFloat(blob),
encode: (data) => data.toString(),
};
export const date: Decoder<Date> = <Name extends string>(name: Name) => [name, date];
date.decode = (blob) => new Date(blob);
date.encode = (data) => data.toISOString().split('T')[0];

export const date: Decoder<Date> = {
matcher: '([^/]+)',
decode: (blob) => new Date(blob),
encode: (data) => data.toISOString().split('T')[0],
};

export const datetime: Decoder<Date> = {
matcher: '([^/]+)',
decode: (blob) => new Date(blob),
encode: (data) => data.toISOString(),
};
export const datetime: Decoder<Date> = <Name extends string>(name: Name) => [name, datetime];
datetime.decode = (blob) => new Date(blob);
datetime.encode = (data) => data.toISOString();

export function array<Data>(decoder: Decoder<Data>): Decoder<Data[]> {
return {
matcher: '([^/]+)',
decode: (blob) => {
const arr = JSON.parse(blob);
if (!(arr instanceof Array)) {
throw new Error('[routtl]: `array` decoder failed to parse array.');
}
return arr.map((value) => decoder.decode(value));
},
encode: (data) => JSON.stringify(data.map((value) => decoder.encode(value))),
const arrayDecoder: Decoder<Data[]> = <Name extends string>(name: Name) => [name, arrayDecoder];

arrayDecoder.decode = (blob) => {
const arr = JSON.parse(blob);
if (!(arr instanceof Array)) {
throw new Error('[routtl]: `array` decoder failed to parse array.');
}
return arr.map((value) => decoder.decode(value));
};
}

export type NamedRouteParameter<Name extends string = string, Data = any> = [
name: Name,
decoder: Decoder<Data>
];
arrayDecoder.encode = (data) => JSON.stringify(data.map((value) => decoder.encode(value)));

return arrayDecoder;
}

export type RouteParameter<Data = any> = Decoder<Data> & {
<Name extends string>(name: Name): NamedRouteParameter<Name, Data>;
Expand Down Expand Up @@ -170,7 +154,7 @@ export class RouteParser<
'^' +
this.#tokens
.map((token) =>
typeof token === 'string' ? token.replace(escapeRegex, '\\$&') : token[1].matcher
typeof token === 'string' ? token.replace(escapeRegex, '\\$&') : '([^/]+)'
)
.join('') +
'$'
Expand Down

0 comments on commit dd9ce09

Please sign in to comment.