Skip to content

Commit

Permalink
Merge pull request #27 from JakeSidSmith/splat
Browse files Browse the repository at this point in the history
Splat
  • Loading branch information
JakeSidSmith authored Sep 8, 2022
2 parents 2cadd91 + e916d80 commit 6240969
Show file tree
Hide file tree
Showing 10 changed files with 216 additions and 39 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,30 @@ const CLIENT_URLS = {
};
```

## Example using splat

If we want to match any trailing part of a URL/path we can do so using a `splat` URL part.

```ts
const url = createTSURL(['user', requiredString('userId'), splat('splat')], {
baseUrl: 'https://server.com',
basePath: '/api',
trailingSlash: false,
});

url.constructPath({ userId: 'abc' }, {});
// returns '/api/user/abc'

url.constructPath({ userId: 'abc', splat: ['posts', '123'] }, {});
// returns '/api/user/abc/posts/123'

url.deconstruct('https://server.com/user/abc');
// returns { urlParams: { userId: 'abc', splat: undefined }, queryParams: {} }

url.deconstruct('https://server.com/user/123/posts/123');
// returns { urlParams: { userId: '123', splat: ['posts', '123'] }, queryParams: {} }
```

## API

### createTSURL
Expand Down Expand Up @@ -291,9 +315,16 @@ The URL schema supports the following:
- `optionalString`
- `optionalNumber`
- `optionalBoolean`
- `splat`

In addition to the ones listed above, the query params schema also supports the following:
The query params schema supports the following:

- `requiredString`
- `requiredNumber`
- `requiredBoolean`
- `optionalString`
- `optionalNumber`
- `optionalBoolean`
- `requiredStringArray`
- `requiredNumberArray`
- `requiredBooleanArray`
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@jakesidsmith/tsurl",
"version": "2.1.0",
"version": "2.2.0",
"description": "Type safe URL construction and deconstruction",
"main": "dist/index.js",
"scripts": {
Expand Down
13 changes: 13 additions & 0 deletions src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export enum PartType {
OPTIONAL_STRING_ARRAY = 'OPTIONAL_STRING_ARRAY',
OPTIONAL_NUMBER_ARRAY = 'OPTIONAL_NUMBER_ARRAY',
OPTIONAL_BOOLEAN_ARRAY = 'OPTIONAL_BOOLEAN_ARRAY',
SPLAT = 'SPLAT',
}

export class RequiredString<T extends string> {
Expand Down Expand Up @@ -133,6 +134,16 @@ export class OptionalBooleanArray<T extends string> {
}
}

export class Splat<T extends string> {
public readonly type = PartType.SPLAT as const;
public readonly required = false as const;
public name: T;

public constructor(name: T) {
this.name = name;
}
}

export const requiredString = <T extends string>(name: T) =>
new RequiredString(name);

Expand Down Expand Up @@ -168,3 +179,5 @@ export const optionalNumberArray = <T extends string>(name: T) =>

export const optionalBooleanArray = <T extends string>(name: T) =>
new OptionalBooleanArray(name);

export const splat = <T extends string>(name: T) => new Splat(name);
28 changes: 13 additions & 15 deletions src/tsurl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import queryString from 'query-string';
import urlParse from 'url-parse';

import { DEFAULT_OPTIONS } from './constants';
import { PartType } from './params';
import {
InferQueryParams,
InferURLParams,
Expand Down Expand Up @@ -115,29 +116,26 @@ export class TSURL<
};
};

public getPathTemplate = () => {
private getURLParams = () => {
const urlParams: Record<string, string> = {};

this.schema.forEach((part) => {
if (typeof part === 'object') {
const optional = part.required ? '' : '?';
urlParams[part.name] = `:${part.name}${optional}`;
if (part.type === PartType.SPLAT) {
urlParams[part.name] = `:${part.name}*`;
} else {
const optional = part.required ? '' : '?';
urlParams[part.name] = `:${part.name}${optional}`;
}
}
});

return constructPathAndMaybeEncode(urlParams, this.schema, this.options);
return urlParams;
};

public getURLTemplate = () => {
const urlParams: Record<string, string> = {};
public getPathTemplate = () =>
constructPathAndMaybeEncode(this.getURLParams(), this.schema, this.options);

this.schema.forEach((part) => {
if (typeof part === 'object') {
const optional = part.required ? '' : '?';
urlParams[part.name] = `:${part.name}${optional}`;
}
});

return constructURLAndMaybeEncode(urlParams, this.schema, this.options);
};
public getURLTemplate = () =>
constructURLAndMaybeEncode(this.getURLParams(), this.schema, this.options);
}
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
RequiredNumberArray,
RequiredString,
RequiredStringArray,
Splat,
} from './params';

export type RequiredPart<T extends string> =
Expand All @@ -30,7 +31,10 @@ export type OptionalPart<T extends string> =
| OptionalNumberArray<T>
| OptionalBooleanArray<T>;

export type AnyPart<T extends string> = RequiredPart<T> | OptionalPart<T>;
export type AnyPart<T extends string> =
| RequiredPart<T>
| OptionalPart<T>
| Splat<T>;

export type URLParamsSchema = ReadonlyArray<
| string
Expand All @@ -40,6 +44,7 @@ export type URLParamsSchema = ReadonlyArray<
| OptionalString<string>
| OptionalNumber<string>
| OptionalBoolean<string>
| Splat<string>
>;

export type QueryParamsSchema = ReadonlyArray<
Expand Down Expand Up @@ -83,6 +88,8 @@ export type InferURLParams<S extends URLParamsSchema = readonly never[]> =
[P in V extends OptionalNumber<infer Name> ? Name : never]?: number;
} & {
[P in V extends OptionalBoolean<infer Name> ? Name : never]?: boolean;
} & {
[P in V extends Splat<infer Name> ? Name : never]?: readonly string[];
}
: never;

Expand Down
18 changes: 17 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
OptionalNumberArray,
OptionalString,
OptionalStringArray,
PartType,
RequiredBoolean,
RequiredBooleanArray,
RequiredNumber,
RequiredNumberArray,
RequiredString,
RequiredStringArray,
Splat,
} from './params';
import {
AnyPart,
Expand Down Expand Up @@ -158,6 +160,16 @@ export const serializeValue = <T extends string>(
}
}

if (part instanceof Splat) {
if (typeof value === 'string' || Array.isArray(value)) {
return ([] as readonly string[]).concat(value);
}

if (typeof value === 'undefined') {
return value;
}
}

throw new Error(`Invalid value for part "${part.name}" - ${value}`);
};

Expand Down Expand Up @@ -232,7 +244,7 @@ export const serializeQueryParams = <
};

export const constructPath = <S extends URLParamsSchema = readonly never[]>(
urlParams: Record<string, string | boolean | number>,
urlParams: Record<string, string | boolean | number | readonly string[]>,
urlParamsSchema: S,
options: Omit<TSURLOptions<readonly never[]>, 'queryParams'>
) => {
Expand All @@ -254,6 +266,10 @@ export const constructPath = <S extends URLParamsSchema = readonly never[]>(
return '';
}

if (part.type === PartType.SPLAT && Array.isArray(value)) {
return value.join('/');
}

return value.toString();
})
.join('/');
Expand Down
Loading

0 comments on commit 6240969

Please sign in to comment.