UNPKG

@typed/fp

Version:

Data Structures and Resources for fp-ts

514 lines (455 loc) 13.8 kB
/** * @typed/fp/Path is a collection of helpers for constructing paths that follow * the path-to-regexp syntax and type-level combinators for parsing that syntax and * for interpolating values. * @since 0.13.0 */ import { A, N } from 'ts-toolbelt' /* Start Region: Parameter DSL */ /** * Template for parameters * @category Model * @since 0.13.0 */ export type Param<A extends string> = `:${A}` /** * @category Constructor * @since 0.13.0 */ export const param = <A extends string>(param: A): Param<A> => `:${param}` as Param<A> /** * Template for optional path parts * @category Model * @since 0.13.0 */ export type Optional<A extends string> = `${A}?` /** * @category Constructor * @since 0.13.0 */ export const optional = <A extends string>(param: A): Optional<A> => `${param}?` as Optional<A> /** * Construct a custom prefix * @category Model * @since 0.13.0 */ export type Prefix<P extends string, A extends string> = `{${P}${A}}` /** * @category Constructor * @since 0.13.0 */ export const prefix = <P extends string, A extends Param<string> | Unnamed>(prefix: P, param: A) => `{${prefix}${param}}` as Prefix<P, A> /** * Construct query params * @category Model * @since 0.13.0 */ export type QueryParam<K extends string, V extends string> = `` extends V ? K : `${K}=${V}` /** * Construct query params * @category Constructor * @since 0.13.0 */ export const queryParam = <K extends string, V extends string>(key: K, value: V) => `${key}=${value}` as QueryParam<K, V> /** * zero or more path parts will be matched to this param * @category Model * @since 0.13.0 */ export type ZeroOrMore<A extends string> = `${Param<A>}*` /** * @category Constructor * @since 0.13.0 */ export const zeroOrMore = <A extends string>(param: A): ZeroOrMore<A> => `:${param}*` as ZeroOrMore<A> /** * @category Model * @since 0.13.0 */ export type OneOrMore<A extends string> = `${Param<A>}+` /** * one or more path parts will be matched to this param * @category Constructor * @since 0.13.0 */ export const oneOrMore = <A extends string>(param: A): OneOrMore<A> => `:${param}+` as OneOrMore<A> /** * Creates the path-to-regexp syntax for query parameters * @category Model * @since 0.13.0 */ export type QueryParams< Q extends readonly QueryParam<any, any>[], R extends string = ``, > = Q extends readonly [infer Head, ...infer Tail] ? QueryParams< A.Cast<Tail, readonly QueryParam<any, any>[]>, `` extends R ? `\\?${A.Cast<Head, string>}` : `${R}&${A.Cast<Head, string>}` > : R /** * @category Constructor * @since 0.13.0 */ export const queryParams = <P extends readonly [QueryParam<any, any>, ...QueryParam<any, any>]>( ...params: P ): QueryParams<P> => `\\?${params.join('&')}` as QueryParams<P> /** * @category Constructor * @since 0.13.0 */ export const unnamed = `(.*)` as const /** * @category Model * @since 0.13.0 */ export type Unnamed = typeof unnamed /** * Composes other path parts into a single path * @category Type-level * @since 0.13.0 */ export type PathJoin<A extends ReadonlyArray<string>> = A extends readonly [ infer Head, ...infer Tail ] ? `${FormatPart<A.Cast<Head, string>>}${PathJoin<A.Cast<Tail, ReadonlyArray<string>>>}` : `` /** * @category Combinator * @since 0.13.0 */ export const pathJoin = <P extends ReadonlyArray<string>>(...parts: P): PathJoin<P> => { if (parts.length === 0) { return `` as PathJoin<P> } const [head, ...tail] = parts return `${formatPart(head)}${pathJoin(...tail)}` as PathJoin<P> } /** * Formats a piece of a path * @category Combinator * @since 0.13.0 */ export const formatPart = (part: string) => { part = removeLeadingSlash(part) if (part.startsWith('{')) { return part } if (part.startsWith('\\?')) { return part } return part === '' ? '' : `/${part}` } /** * @category Type-level * @since 0.13.0 */ export type FormatPart<P extends string> = `` extends P ? P : RemoveLeadingSlash<P> extends `\\?${infer _}` ? RemoveLeadingSlash<P> : RemoveLeadingSlash<P> extends `{${infer _}` ? RemoveLeadingSlash<P> : P extends QueryParam<infer _, infer _> | QueryParams<infer _, infer _> ? P : `/${RemoveLeadingSlash<P>}` /** * Remove forward slashes prefixes recursively * @category Type-level * @since 0.13.0 */ export type RemoveLeadingSlash<A> = A extends `/${infer R}` ? RemoveLeadingSlash<R> : A /** * @category Combinator * @since 0.13.0 */ export const removeLeadingSlash = <A extends string>(a: A): RemoveLeadingSlash<A> => { let s = a.slice() while (s.startsWith('/')) { s = s.slice(1) } return s as RemoveLeadingSlash<A> } /* End Region: Parameter DSL */ /* Start Region: Extract Parameters from Path */ // Extract the parameters out of a path /** * @category Type-level * @since 0.13.0 */ export type ParamsOf<A extends string> = Compact<PartsToParams<PathToParts<A>>> // Extract the Query parameters out of a path /** * @category Type-level * @since 0.13.0 */ export type QueryParamsOf<P extends string> = Compact<QueryToParams<PathToQuery<P>>> // Convert a path back into a tuple of path parts /** * @category Type-level * @since 0.13.0 */ export type PathToParts<P> = P extends `${infer Head}\\?${infer Tail}` ? readonly [...PathToParts<Head>, `\\?${Tail}`] : P extends `${infer Head}/${infer Tail}` ? readonly [...PathToParts<Head>, ...PathToParts<Tail>] : P extends `${infer Head}{${infer Q}}?${infer Tail}` ? readonly [...PathToParts<Head>, `{${Q}}?`, ...PathToParts<`${Tail}`>] : P extends `${infer Head}{${infer Q}}${infer Tail}` ? readonly [...PathToParts<Head>, `{${Q}}`, ...PathToParts<`${Tail}`>] : `` extends P ? readonly [] : readonly [P] /** * @category Type-level * @since 0.13.0 */ export type PartsToParams<A extends ReadonlyArray<string>, AST = {}> = A extends readonly [ infer Head, ...infer Tail ] ? PartsToParams<A.Cast<Tail, readonly string[]>, AST & PartToParam<A.Cast<Head, string>, AST>> : AST /** * @category Type-level * @since 0.13.0 */ export type PartToParam<A extends string, AST> = A extends `\\${infer R}` ? PartsToParams<QueryParamsToParts<R, []>, AST> : A extends Unnamed ? { readonly [K in FindNextIndex<AST> extends number ? FindNextIndex<AST> : never]: string } : A extends `${infer _}${Unnamed}}?` ? { readonly [K in FindNextIndex<AST> extends number ? FindNextIndex<AST> : never]?: string } : A extends `${infer _}${Unnamed}}` ? { readonly [K in FindNextIndex<AST> extends number ? FindNextIndex<AST> : never]: string } : A extends `${infer _}${Param<infer R>}}?` ? { readonly [K in R]?: string } : A extends `${infer _}${Param<infer R>}}` ? { readonly [K in R]: string } : A extends `${infer _}${Param<infer R>}?` ? { readonly [K in R]?: string } : A extends `${infer _}${Param<infer R>}+` ? { readonly [K in R]: readonly [string, ...string[]] } : A extends `${infer _}${Param<infer R>}*` ? { readonly [K in R]: readonly string[] } : A extends `${infer _}${Param<infer R>}` ? { readonly [K in R]: string } : {} /** * @category Type-level * @since 0.13.0 */ export type QueryParamsToParts< Q extends string, R extends ReadonlyArray<string>, > = Q extends `\\?${infer Q}` ? QueryParamsToParts<Q, R> : Q extends `?${infer Q}` ? QueryParamsToParts<Q, R> : Q extends `${infer Head}&${infer Tail}` ? QueryParamsToParts<Tail, QueryParamsToParts<Head, R>> : readonly [...R, QueryParamValue<Q>] /** * @category Type-level * @since 0.13.0 */ export type QueryToParams<Q extends string, AST = {}> = Q extends `${infer Head}&${infer Tail}` ? QueryToParams<Tail, QueryToParams<Head, AST>> : Q extends `?${infer K}` ? QueryToParams<K, AST> : Q extends `${infer K}?` ? AST & Partial<QueryParamAst<K>> : Q extends `${infer K}` ? AST & QueryParamAst<K> : AST // Increments up from I until it finds an index that is not taken /** * @category Type-level * @since 0.13.0 */ export type FindNextIndex<AST, I extends number = 0> = I extends keyof AST ? FindNextIndex<AST, N.Add<I, 1>> : I /** * @category Type-level * @since 0.13.0 */ export type PathToQuery<P extends string> = P extends `${infer _}\\${infer Q}` ? Q : `` type QueryParamValue<K extends string> = K extends `${infer _}=${infer R}` ? R : string type QueryParamAst<K extends string> = Readonly<Record<QueryParamAstKey<K>, QueryParamAstValue<K>>> type QueryParamAstKey<K extends string> = K extends `${infer K}=${infer _}` ? K : K type QueryParamAstValue<K extends string> = K extends `${infer _}=${infer R}` ? R extends Param<infer _> | Unnamed ? string : R : string type Compact<A> = { readonly [K in keyof A]: A[K] } /* End Region: Extract Parameters from Path */ /* Start Region: Interpolations */ /** * @category Type-level * @since 0.13.0 */ export type Interpolate< P extends string, Params extends ParamsOf<P>, > = P extends `${infer Head}\\?${infer Tail}` ? PathJoin< InterpolateWithQueryParams< SplitQueryParams<Tail>, Params, InpterpolateParts<PathToParts<Head>, Params> >[0] > : PathJoin<InpterpolateParts<PathToParts<P>, Params>[0]> /** * @category Type-level * @since 0.13.0 */ export type InpterpolateParts< Parts extends readonly any[], Params extends {}, R extends readonly any[] = [], AST = {}, > = Parts extends readonly [infer H, ...infer T] ? H extends Optional<Prefix<infer Pre, Unnamed>> ? FindNextIndex<AST> extends keyof Params ? InpterpolateParts< T, Params, AppendPrefix<R, Pre, `${A.Cast<Params[FindNextIndex<AST>], string | number>}`>, AST & Record<H, Params[FindNextIndex<AST>]> > : InpterpolateParts<T, Params, R, AST> : H extends Prefix<infer Pre, Unnamed> ? InpterpolateParts< T, Params, AppendPrefix< R, Pre, `${A.Cast<Params[A.Cast<FindNextIndex<AST>, keyof Params>], string | number>}` >, AST & Record<H, Params[A.Cast<FindNextIndex<AST>, keyof Params>]> > : H extends Optional<Prefix<infer Pre, Param<infer P>>> ? P extends keyof Params ? InpterpolateParts< T, Params, AppendPrefix<R, Pre, A.Cast<Params[A.Cast<P, keyof Params>], string>>, AST & Record<H, Params[A.Cast<P, keyof Params>]> > : InpterpolateParts<T, Params, R, AST> : H extends Prefix<infer Pre, Param<infer P>> ? InpterpolateParts< T, Params, AppendPrefix<R, Pre, A.Cast<Params[A.Cast<P, keyof Params>], string>>, AST & Record<H, Params[A.Cast<P, keyof Params>]> > : InterpolatePart<H, Params, AST> extends readonly [infer A, infer B] ? InpterpolatePartsWithNext<T, Params, R, readonly [A, B]> : InpterpolateParts<T, Params, R, AST> : readonly [R, AST] /** * @category Type-level * @since 0.13.0 */ export type AppendPrefix< R extends readonly any[], Pre extends string, P extends string, > = R extends readonly [...infer Init, infer L] ? readonly [...Init, `${A.Cast<L, string>}${Pre}${P}`] : R /** * @category Type-level * @since 0.13.0 */ export type InpterpolatePartsWithNext< Parts extends readonly any[], Params extends {}, R extends readonly any[], Next extends readonly [any, any], > = InpterpolateParts<Parts, Params, readonly [...R, Next[0]], Next[1]> /** * @category Type-level * @since 0.13.0 */ export type InterpolatePart<P, Params, AST> = P extends Optional<Param<infer R>> ? R extends keyof Params ? readonly [Params[R], AST & Record<R, Params[R]>] : readonly ['', AST] : P extends Param<infer R> ? R extends keyof Params ? readonly [Params[R], AST & Record<R, Params[R]>] : readonly [P, AST] : P extends Unnamed ? FindNextIndex<AST> extends keyof Params ? InterpolateUnnamedPart<Params, FindNextIndex<AST>, AST> : readonly [P, AST] : P extends Prefix<infer Pre, Param<infer R>> ? R extends keyof Params ? [`${Pre}${A.Cast<Params[R], string>}`, AST & Record<R, Params[R]>] : [] : P extends Optional<Prefix<infer Pre, Param<infer R>>> ? R extends keyof Params ? [`${Pre}${A.Cast<Params[R], string>}`, AST & Partial<Record<R, Params[R]>>] : [] : readonly [P, AST] /** * @category Type-level * @since 0.13.0 */ export type InterpolateUnnamedPart<Params, K extends keyof Params, AST> = readonly [ Params[K], AST & Record<K, Params[K]>, ] type InterpolateWithQueryParams< Q extends readonly string[], Params, Previous extends readonly [any, any], First extends boolean = true, > = Q extends readonly [infer Head, ...infer Tail] ? InterpolateWithQueryParams< A.Cast<Tail, readonly string[]>, Params, InterpolateQueryParamPart<Head, Params, Previous, First>[0], InterpolateQueryParamPart<Head, Params, Previous, First>[1] > : Previous /** * @category Type-level * @since 0.13.0 */ export type InterpolateQueryParamPart< Part, Params, Previous extends readonly [readonly string[], any], First extends boolean, > = Part extends QueryParam<infer K, infer V> ? InterpolateQueryParamPartWithKey< First extends true ? `?${K}` : `&${K}`, Previous[0], InterpolatePart<V, Params, Previous[1]>, First > : readonly [[[...Previous[0], Part], Previous[1]], false] type InterpolateQueryParamPartWithKey< K extends string, Parts extends readonly string[], Previous extends readonly [string, any], First extends boolean, > = '' extends Previous[0] ? [[Parts, Previous[1]], First] : [[[...Parts, `${K}=${Previous[0]}`], Previous[1]], false] type SplitQueryParams<P extends string> = P extends `${infer Head}&${infer Tail}` ? readonly [Head, ...SplitQueryParams<Tail>] : readonly [P]