Skip to content

Commit

Permalink
Use optional or required literal instead of optional flag
Browse files Browse the repository at this point in the history
  • Loading branch information
konowrockis committed Jul 3, 2021
1 parent 4ed4ffb commit 8feb706
Show file tree
Hide file tree
Showing 20 changed files with 146 additions and 107 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ yarn add ts-routes

## Quick start

```js
```ts
import { createRouting, number, query, segment, uuid } from 'ts-routes';

const routes = createRouting({
Expand Down
12 changes: 6 additions & 6 deletions __tests__/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ describe("query", () => {
product: {
...segment`/product`,
query: {
productId: query(true),
productId: query("optional"),
},
},
} as const);
Expand All @@ -22,7 +22,7 @@ describe("query", () => {
product: {
...segment`/product`,
query: {
productId: query(false),
productId: query("required"),
},
},
} as const);
Expand All @@ -37,8 +37,8 @@ describe("query", () => {
product: {
...segment`/product`,
query: {
productId: query(false),
details: query(true),
productId: query("required"),
details: query("optional"),
},
},
} as const);
Expand All @@ -58,7 +58,7 @@ describe("query", () => {
product: {
...segment`/product`,
query: {
filter: query(false),
filter: query("required"),
},
children: {
details: segment`/${number("productId")}`,
Expand All @@ -76,7 +76,7 @@ describe("query", () => {
product: {
...segment`/product`,
query: {
productId: query(true),
productId: query("optional"),
},
},
} as const);
Expand Down
46 changes: 1 addition & 45 deletions __tests__/segments/arg.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,6 @@
import { string, createRouting, segment, arg } from "../../src";
import { createRouting, segment, arg } from "../../src";

describe("arg segment", () => {
it("creates route with an arg segment", () => {
const routes = createRouting({
product: segment`/product/${string("productId")}`,
} as const);

const route = routes.product({ productId: "id" });

expect(route).toEqual("/product/id");
});

it("creates route with an optional arg segment", () => {
const routes = createRouting({
product: segment`/product/${string("productId", {
optional: true,
})}`,
} as const);

const route = routes.product();

expect(route).toEqual("/product");
});

it("creates route with a custom pattern param", () => {
const routes = createRouting({
product: segment`/product/${arg("productId", {
Expand All @@ -45,28 +23,6 @@ describe("arg segment", () => {
expect(() => routes.product({ productId: "123" })).toThrow();
});

it("returns the correct path pattern when required", () => {
const routes = createRouting({
product: segment`/product/${string("productId")}`,
} as const);

const pattern = routes.product.pattern;

expect(pattern).toEqual("/product/:productId");
});

it("returns the correct path pattern when optional", () => {
const routes = createRouting({
product: segment`/product/${string("productId", {
optional: true,
})}`,
} as const);

const pattern = routes.product.pattern;

expect(pattern).toEqual("/product/:productId?");
});

it("returns the correct path pattern for custom regexes", () => {
const routes = createRouting({
product: segment`/product/${arg("productId", {
Expand Down
4 changes: 2 additions & 2 deletions __tests__/segments/number.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createRouting, number, segment } from "../../src";
describe("number segment", () => {
it("creates route with an optional number param", () => {
const routes = createRouting({
product: segment`/product/${number("productId", { optional: true })}`,
product: segment`/product/${number("productId", "optional")}`,
} as const);

const route = routes.product();
Expand Down Expand Up @@ -41,7 +41,7 @@ describe("number segment", () => {

it("returns the correct pattern when optional", () => {
const routes = createRouting({
product: segment`/product/${number("productId", { optional: true })}`,
product: segment`/product/${number("productId", "optional")}`,
} as const);

const pattern = routes.product.pattern;
Expand Down
43 changes: 43 additions & 0 deletions __tests__/segments/string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { string, createRouting, segment } from "../../src";

describe("arg segment", () => {
it("creates route with an arg segment", () => {
const routes = createRouting({
product: segment`/product/${string("productId")}`,
} as const);

const route = routes.product({ productId: "id" });

expect(route).toEqual("/product/id");
});

it("creates route with an optional arg segment", () => {
const routes = createRouting({
product: segment`/product/${string("productId", "optional")}`,
} as const);

const route = routes.product();

expect(route).toEqual("/product");
});

it("returns the correct path pattern when required", () => {
const routes = createRouting({
product: segment`/product/${string("productId")}`,
} as const);

const pattern = routes.product.pattern;

expect(pattern).toEqual("/product/:productId");
});

it("returns the correct path pattern when optional", () => {
const routes = createRouting({
product: segment`/product/${string("productId", "optional")}`,
} as const);

const pattern = routes.product.pattern;

expect(pattern).toEqual("/product/:productId?");
});
});
2 changes: 1 addition & 1 deletion __tests__/segments/uuid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe("uuid segment", () => {

it("creates route with an optional uuid param", () => {
const routes = createRouting({
product: segment`/product/${uuid("productId", { optional: true })}`,
product: segment`/product/${uuid("productId", "optional")}`,
} as const);

const route = routes.product();
Expand Down
8 changes: 4 additions & 4 deletions __tests__/types/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ const routes = createRouting({
products: {
...segment`/products`,
query: {
filter: query(false),
optionalFilter: query(true),
object: query<{ test: string }, true>(true),
filter: query("required"),
optionalFilter: query("optional"),
object: query<{ test: string }, "optional">("optional"),
},
children: {
product: segment`/${number("productId")}`,
},
},
order: segment`/orders/${number("orderId", { optional: true })}`,
order: segment`/orders/${number("orderId", "optional")}`,
});

// Should not allow creating routing from raw strings
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ts-routes",
"version": "2.0.0",
"version": "2.0.0-alpha.0",
"description": "Strongly typed routes management",
"main": "lib/index.cjs.js",
"module": "lib/index.esm.js",
Expand Down
15 changes: 10 additions & 5 deletions src/PathParamDescription.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
export default class PathParamDescription<TName extends string = string, TOptional extends boolean = true> {
import { Optionality } from "./helpers";

export default class PathParamDescription<
TName extends string = string,
TOptionality extends Optionality = "optional",
> {
public readonly pattern: string;
public readonly name: TName;
public readonly optional: TOptional;
public readonly optionality: TOptionality;

constructor({ name, optional, pattern }: { name: TName; optional: TOptional; pattern?: string }) {
constructor({ name, optionality, pattern }: { name: TName; optionality: TOptionality; pattern?: string }) {
const patternPart = pattern ? `(${pattern})` : "";
const requirementPart = optional ? "?" : "";
const requirementPart = optionality === "optional" ? "?" : "";

this.name = name;
this.optional = optional ?? (false as any);
this.optionality = optionality;
this.pattern = `:${name}${patternPart}${requirementPart}`;
}
}
6 changes: 4 additions & 2 deletions src/QueryParamDescription.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export default class QueryParamDescription<TReturnType = string, TOptional extends boolean = false> {
import { Optionality } from "./helpers";

export default class QueryParamDescription<TReturnType = string, TOptionality extends Optionality = "required"> {
private readonly _?: TReturnType;

constructor(public readonly optional: TOptional) {}
constructor(public readonly optionality: TOptionality) {}
}
5 changes: 3 additions & 2 deletions src/RouteDescription.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import SegmentPattern from "./SegmentPattern";
import QueryParamDescription from "./QueryParamDescription";
import PathParamDescription from "./PathParamDescription";
import { Optionality } from "./helpers";

export default interface RouteDescription<
TPathParams extends PathParamDescription<string, boolean>[] = [],
TQueryParams extends Record<string, QueryParamDescription<any, boolean>> = {},
TPathParams extends PathParamDescription<string, Optionality>[] = [],
TQueryParams extends Record<string, QueryParamDescription<any, Optionality>> = {},
TChildren extends { readonly [name: string]: RouteDescription<any, any> } = {},
> {
readonly pattern: SegmentPattern<TPathParams>;
Expand Down
3 changes: 2 additions & 1 deletion src/SegmentPattern.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Optionality } from "./helpers";
import PathParamDescription from "./PathParamDescription";

export default class SegmentPattern<TPathParamsDescription extends PathParamDescription<string, boolean>[]> {
export default class SegmentPattern<TPathParamsDescription extends PathParamDescription<string, Optionality>[]> {
constructor(public readonly pattern: string, public readonly params: TPathParamsDescription) {}
}
15 changes: 8 additions & 7 deletions src/createRouting.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { compile } from "path-to-regexp";
import { IParseOptions, IStringifyOptions, parse, stringify } from "qs";
import { Optionality } from "./helpers";
import PathParamDescription from "./PathParamDescription";
import QueryParamDescription from "./QueryParamDescription";
import RouteDescription from "./RouteDescription";
Expand Down Expand Up @@ -53,30 +54,30 @@ type GetQueryParams<TRouteDescription extends RouteDescription<any, any, any>> =

type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;

type MapPathParams<TPathParams extends PathParamDescription<string, boolean>[]> = UnionToIntersection<
type MapPathParams<TPathParams extends PathParamDescription<string, Optionality>[]> = UnionToIntersection<
{
[T in keyof TPathParams]: GetParam<TPathParams[T]>;
}[number]
>;

type SingleOrArray<T> = T | T[];

type MapQueryParams<TQueryParams extends Record<string, QueryParamDescription<any, boolean>>> = {
[TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription<any, true>
type MapQueryParams<TQueryParams extends Record<string, QueryParamDescription<any, Optionality>>> = {
[TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription<any, "optional">
? TName
: never]?: SingleOrArray<GetQueryResultType<TQueryParams[TName]>>;
} &
{
[TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription<any, false>
[TName in keyof TQueryParams as TQueryParams[TName] extends QueryParamDescription<any, "required">
? TName
: never]: SingleOrArray<GetQueryResultType<TQueryParams[TName]>>;
};

type GetQueryResultType<TQueryParam extends QueryParamDescription<any, boolean>> =
TQueryParam extends QueryParamDescription<infer TReturnType, boolean> ? TReturnType : never;
type GetQueryResultType<TQueryParam extends QueryParamDescription<any, Optionality>> =
TQueryParam extends QueryParamDescription<infer TReturnType, Optionality> ? TReturnType : never;

type GetParam<TPathParam> = TPathParam extends PathParamDescription<infer TName, infer TOptional>
? TOptional extends true
? TOptional extends "optional"
? {
[T in TName]?: string;
}
Expand Down
2 changes: 2 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export type PathParamsFor<T extends (...args: any[]) => string> = Parameters<T>[
: NonNullable<Parameters<T>[0]>;

export type QueryParamsFor<T extends (...args: any[]) => string> = Parameters<T>[1];

export type Optionality = "required" | "optional";
9 changes: 5 additions & 4 deletions src/parameters/arg.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Optionality } from "../helpers";
import PathParamDescription from "../PathParamDescription";

export default function arg<TName extends string = string, TOptional extends boolean = false>(
export default function arg<TName extends string = string, TOptionality extends Optionality = "required">(
name: TName,
{
pattern,
optional = false as TOptional,
optionality = "required" as TOptionality,
}: {
pattern?: string;
optional?: TOptional;
optionality?: TOptionality;
} = {},
) {
return new PathParamDescription({ name, optional, pattern });
return new PathParamDescription({ name, optionality, pattern });
}
22 changes: 15 additions & 7 deletions src/parameters/number.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Optionality } from "../helpers";
import PathParamDescription from "../PathParamDescription";

export default function uuid<TName extends string = string, TOptional extends boolean = false>(
export default function uuid<TName extends string = string, TOptionality extends Optionality = "required">(
name: TName,
{
optional = false as TOptional,
}: {
optional?: TOptional;
} = {},
optsOrOptionality?:
| {
optionality?: TOptionality;
}
| TOptionality,
) {
let optionality: TOptionality;
if (typeof optsOrOptionality === "string") {
optionality = optsOrOptionality;
} else {
optionality = optsOrOptionality?.optionality ?? ("required" as TOptionality);
}

const number = "[0-9]+";

return new PathParamDescription({ name, optional, pattern: number });
return new PathParamDescription({ name, optionality, pattern: number });
}
22 changes: 15 additions & 7 deletions src/parameters/string.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { Optionality } from "../helpers";
import PathParamDescription from "../PathParamDescription";

export default function string<TName extends string = string, TOptional extends boolean = false>(
export default function string<TName extends string = string, TOptionality extends Optionality = "required">(
name: TName,
{
optional = false as TOptional,
}: {
optional?: TOptional;
} = {},
optsOrOptionality?:
| {
optionality?: TOptionality;
}
| TOptionality,
) {
return new PathParamDescription({ name, optional });
let optionality: TOptionality;
if (typeof optsOrOptionality === "string") {
optionality = optsOrOptionality;
} else {
optionality = optsOrOptionality?.optionality ?? ("required" as TOptionality);
}

return new PathParamDescription({ name, optionality });
}
Loading

0 comments on commit 8feb706

Please sign in to comment.