Skip to content

Commit

Permalink
Merge pull request #31 from JakeSidSmith/enum-params
Browse files Browse the repository at this point in the history
Enum params
  • Loading branch information
osilviotti authored Sep 29, 2022
2 parents 27db47d + b1f7b9e commit 419fb9c
Show file tree
Hide file tree
Showing 11 changed files with 759 additions and 35 deletions.
15 changes: 11 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ url.deconstruct('https://server.com/api/user/123/posts/123');
// returns { urlParams: { userId: '123', splat: ['posts', '123'] }, queryParams: {} }
```

## Allow sub-paths when you don't want to match a splat
## Ignore sub-paths when you don't want to match a splat

In some cases you want your URL templates to match a specific template, but when deconstructing you don't mind if the URL/path contains additional sub-paths. In these cases you can pass the `allowSubPaths` option to the deconstruct method.
In some cases you want your URL templates to match a specific template, but when deconstructing you don't mind if the URL/path contains additional sub-paths. In these cases you can pass the `ignoreSubPaths` option to the deconstruct method.

```ts
const url = createTSURL(['user', requiredString('userId')], {
Expand All @@ -208,7 +208,7 @@ url.deconstruct('https://server.com/api/user/123/posts/123');
// throws an error

url.deconstruct('https://server.com/api/user/123/posts/123', {
allowSubPaths: true,
ignoreSubPaths: true,
});
// returns { urlParams: { userId: '123' }, queryParams: {} }
```
Expand Down Expand Up @@ -330,7 +330,8 @@ This is the second argument to the `deconstruct` function.

Options include:

- `allowSubPaths` - `boolean` - whether the deconstruction will allow sub-paths (stuff that appears after your defined template) in the provided URL/path
- `ignoreSubPaths` - `boolean` - whether the deconstruction will allow sub-paths (stuff that appears after your defined template) in the provided URL/path
- `ignoreInvalidEnums` - `boolean` - whether the deconstruction will error or omit invalid values if the URL contains values for enum restricted fields that do not adhere to the enum (does not apply to `requiredEnum` fields)

### Parameters

Expand All @@ -341,25 +342,31 @@ The URL schema supports the following:
- `requiredString`
- `requiredNumber`
- `requiredBoolean`
- `requiredEnum`
- `optionalString`
- `optionalNumber`
- `optionalBoolean`
- `optionalEnum`
- `splat`

The query params schema supports the following:

- `requiredString`
- `requiredNumber`
- `requiredBoolean`
- `requiredEnum`
- `optionalString`
- `optionalNumber`
- `optionalBoolean`
- `optionalEnum`
- `requiredStringArray`
- `requiredNumberArray`
- `requiredBooleanArray`
- `requiredEnumArray`
- `optionalStringArray`
- `optionalNumberArray`
- `optionalBooleanArray`
- `optionalEnumArray`

## Contributing

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.3.2",
"version": "3.0.0",
"description": "Type safe URL construction and deconstruction",
"main": "dist/index.js",
"scripts": {
Expand Down
177 changes: 177 additions & 0 deletions src/params.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,52 @@
import { EnumValue, EnumLike } from './types';

export enum PartType {
REQUIRED_STRING = 'REQUIRED_STRING',
REQUIRED_NUMBER = 'REQUIRED_NUMBER',
REQUIRED_BOOLEAN = 'REQUIRED_BOOLEAN',
REQUIRED_ENUM = 'REQUIRED_ENUM',
OPTIONAL_STRING = 'OPTIONAL_STRING',
OPTIONAL_NUMBER = 'OPTIONAL_NUMBER',
OPTIONAL_BOOLEAN = 'OPTIONAL_BOOLEAN',
OPTIONAL_ENUM = 'OPTIONAL_ENUM',
REQUIRED_STRING_ARRAY = 'REQUIRED_STRING_ARRAY',
REQUIRED_NUMBER_ARRAY = 'REQUIRED_NUMBER_ARRAY',
REQUIRED_BOOLEAN_ARRAY = 'REQUIRED_BOOLEAN_ARRAY',
REQUIRED_ENUM_ARRAY = 'REQUIRED_ENUM_ARRAY',
OPTIONAL_STRING_ARRAY = 'OPTIONAL_STRING_ARRAY',
OPTIONAL_NUMBER_ARRAY = 'OPTIONAL_NUMBER_ARRAY',
OPTIONAL_BOOLEAN_ARRAY = 'OPTIONAL_BOOLEAN_ARRAY',
OPTIONAL_ENUM_ARRAY = 'OPTIONAL_ENUM_ARRAY',
SPLAT = 'SPLAT',
}

const extractValidEnumValues = <V extends EnumValue, K extends string>(
valid: readonly V[] | EnumLike<V, K>
) => {
if (Array.isArray(valid)) {
return valid;
}

const values = Object.values(valid);

return values.reduce<readonly V[]>((acc, value) => {
// Numbers are always values
if (typeof value === 'number') {
return [...acc, value as V];
}

// String values are not stored as keys
if (
typeof value === 'string' &&
typeof (valid as EnumLike<V, K>)[value as unknown as K] !== 'number'
) {
return [...acc, value as V];
}

return acc;
}, []);
};

export class RequiredString<T extends string> {
public readonly type = PartType.REQUIRED_STRING as const;
public readonly required = true as const;
Expand Down Expand Up @@ -44,6 +77,24 @@ export class RequiredBoolean<T extends string> {
}
}

export class RequiredEnum<
T extends string,
V extends EnumValue,
K extends string = never
> {
public readonly type = PartType.REQUIRED_ENUM as const;
public readonly required = true as const;
public name: T;
public valid: readonly V[];

public constructor(name: T, valid: readonly V[]);
public constructor(name: T, valid: EnumLike<V, K>);
public constructor(name: T, valid: readonly V[] | EnumLike<V, K>) {
this.name = name;
this.valid = extractValidEnumValues(valid);
}
}

export class RequiredStringArray<T extends string> {
public readonly type = PartType.REQUIRED_STRING_ARRAY as const;
public readonly required = true as const;
Expand Down Expand Up @@ -74,6 +125,24 @@ export class RequiredBooleanArray<T extends string> {
}
}

export class RequiredEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
> {
public readonly type = PartType.REQUIRED_ENUM_ARRAY as const;
public readonly required = true as const;
public name: T;
public valid: readonly V[];

public constructor(name: T, valid: readonly V[]);
public constructor(name: T, valid: EnumLike<V, K>);
public constructor(name: T, valid: readonly V[] | EnumLike<V, K>) {
this.name = name;
this.valid = extractValidEnumValues(valid);
}
}

export class OptionalString<T extends string> {
public readonly type = PartType.OPTIONAL_STRING as const;
public readonly required = false as const;
Expand Down Expand Up @@ -104,6 +173,24 @@ export class OptionalBoolean<T extends string> {
}
}

export class OptionalEnum<
T extends string,
V extends EnumValue,
K extends string = never
> {
public readonly type = PartType.OPTIONAL_ENUM as const;
public readonly required = false as const;
public name: T;
public valid: readonly V[];

public constructor(name: T, valid: readonly V[]);
public constructor(name: T, valid: EnumLike<V, K>);
public constructor(name: T, valid: readonly V[] | EnumLike<V, K>) {
this.name = name;
this.valid = extractValidEnumValues(valid);
}
}

export class OptionalStringArray<T extends string> {
public readonly type = PartType.OPTIONAL_STRING_ARRAY as const;
public readonly required = false as const;
Expand Down Expand Up @@ -134,6 +221,24 @@ export class OptionalBooleanArray<T extends string> {
}
}

export class OptionalEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
> {
public readonly type = PartType.OPTIONAL_ENUM_ARRAY as const;
public readonly required = false as const;
public name: T;
public valid: readonly V[];

public constructor(name: T, valid: readonly V[]);
public constructor(name: T, valid: EnumLike<V, K>);
public constructor(name: T, valid: readonly V[] | EnumLike<V, K>) {
this.name = name;
this.valid = extractValidEnumValues(valid);
}
}

export class Splat<T extends string> {
public readonly type = PartType.SPLAT as const;
public readonly required = false as const;
Expand All @@ -153,6 +258,24 @@ export const requiredNumber = <T extends string>(name: T) =>
export const requiredBoolean = <T extends string>(name: T) =>
new RequiredBoolean(name);

export function requiredEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[]): RequiredEnum<T, V, K>;
export function requiredEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: EnumLike<V, K>): RequiredEnum<T, V, K>;
export function requiredEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[] | EnumLike<V, K>) {
return new RequiredEnum(name, valid as EnumLike<V, K>);
}

export const requiredStringArray = <T extends string>(name: T) =>
new RequiredStringArray(name);

Expand All @@ -162,6 +285,24 @@ export const requiredNumberArray = <T extends string>(name: T) =>
export const requiredBooleanArray = <T extends string>(name: T) =>
new RequiredBooleanArray(name);

export function requiredEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[]): RequiredEnumArray<T, V, K>;
export function requiredEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: EnumLike<V, K>): RequiredEnumArray<T, V, K>;
export function requiredEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[] | EnumLike<V, K>) {
return new RequiredEnumArray(name, valid as EnumLike<V, K>);
}

export const optionalString = <T extends string>(name: T) =>
new OptionalString(name);

Expand All @@ -171,6 +312,24 @@ export const optionalNumber = <T extends string>(name: T) =>
export const optionalBoolean = <T extends string>(name: T) =>
new OptionalBoolean(name);

export function optionalEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[]): OptionalEnum<T, V, K>;
export function optionalEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: EnumLike<V, K>): OptionalEnum<T, V, K>;
export function optionalEnum<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[] | EnumLike<V, K>) {
return new OptionalEnum(name, valid as EnumLike<V, K>);
}

export const optionalStringArray = <T extends string>(name: T) =>
new OptionalStringArray(name);

Expand All @@ -180,4 +339,22 @@ export const optionalNumberArray = <T extends string>(name: T) =>
export const optionalBooleanArray = <T extends string>(name: T) =>
new OptionalBooleanArray(name);

export function optionalEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[]): OptionalEnumArray<T, V, K>;
export function optionalEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: EnumLike<V, K>): OptionalEnumArray<T, V, K>;
export function optionalEnumArray<
T extends string,
V extends EnumValue,
K extends string = never
>(name: T, valid: readonly V[] | EnumLike<V, K>) {
return new OptionalEnumArray(name, valid as EnumLike<V, K>);
}

export const splat = <T extends string>(name: T) => new Splat(name);
19 changes: 16 additions & 3 deletions src/tsurl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
InferQueryParams,
InferURLParams,
QueryParamsSchema,
SerializeValueOptions,
TSURLOptions,
URLParamsSchema,
} from './types';
Expand Down Expand Up @@ -105,7 +106,7 @@ export class TSURL<
const urlMatch = match<
Record<string, string | undefined | null | readonly string[]>
>(pathTemplate, {
end: !deconstructOptions?.allowSubPaths,
end: !deconstructOptions?.ignoreSubPaths,
})(parsed.pathname);

if (!urlMatch) {
Expand All @@ -120,9 +121,21 @@ export class TSURL<
arrayFormatSeparator: this.options.queryArrayFormatSeparator,
}).query;

const serializeValueOptions: SerializeValueOptions = {
ignoreInvalidEnums: deconstructOptions?.ignoreInvalidEnums,
};

return {
urlParams: serializeURLParams(urlMatch.params, this.schema),
queryParams: serializeQueryParams(queryParams, this.options.queryParams),
urlParams: serializeURLParams(
urlMatch.params,
this.schema,
serializeValueOptions
),
queryParams: serializeQueryParams(
queryParams,
this.options.queryParams,
serializeValueOptions
),
};
};

Expand Down
Loading

0 comments on commit 419fb9c

Please sign in to comment.