@furystack/rest
Version:
Generic REST package
197 lines • 9.05 kB
TypeScript
import type { OpenApiDocument } from './openapi-document.js';
import type { RestApi } from './rest-api.js';
/**
* Converts an OpenAPI `{param}` path to FuryStack `:param` format at the type level.
*/
export type ConvertOpenApiPath<P extends string> = P extends `${infer Before}{${infer Param}}${infer After}` ? `${Before}:${Param}${ConvertOpenApiPath<After>}` : P;
type ResolveRef<Doc extends OpenApiDocument, Ref extends string> = Ref extends `#/components/schemas/${infer Name}` ? Doc['components'] extends {
schemas: infer S;
} ? Name extends keyof S ? S[Name] : unknown : unknown : Ref extends `#/components/parameters/${infer Name}` ? Doc['components'] extends {
parameters: infer S;
} ? Name extends keyof S ? S[Name] : unknown : unknown : Ref extends `#/components/responses/${infer Name}` ? Doc['components'] extends {
responses: infer S;
} ? Name extends keyof S ? S[Name] : unknown : unknown : Ref extends `#/components/requestBodies/${infer Name}` ? Doc['components'] extends {
requestBodies: infer S;
} ? Name extends keyof S ? S[Name] : unknown : unknown : unknown;
type ResolveSchemaOrRef<Doc extends OpenApiDocument, S> = S extends {
$ref: infer Ref extends string;
} ? ResolveRef<Doc, Ref> : S;
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
/**
* Maps a JSON Schema type keyword to its TypeScript equivalent.
* Supports primitives, arrays, objects, enum, const, oneOf/anyOf/allOf, nullable, and $ref.
*
* The `Doc` parameter is threaded through for `$ref` resolution within schemas.
*/
export type JsonSchemaToType<S, Doc extends OpenApiDocument = OpenApiDocument> = S extends {
$ref: infer Ref extends string;
} ? JsonSchemaToType<ResolveRef<Doc, Ref>, Doc> : S extends {
const: infer C;
} ? C : S extends {
type: 'string';
enum: ReadonlyArray<infer E>;
} ? E : S extends {
type: 'string';
} ? string : S extends {
type: 'number' | 'integer';
} ? number : S extends {
type: 'boolean';
} ? boolean : S extends {
type: 'null';
} ? null : S extends {
type: readonly [infer T, 'null'];
} ? JsonSchemaToType<{
type: T;
}, Doc> | null : S extends {
type: readonly ['null', infer T];
} ? JsonSchemaToType<{
type: T;
}, Doc> | null : S extends {
type: 'array';
items: infer Items;
} ? Array<JsonSchemaToType<Items, Doc>> : S extends {
type: 'object';
properties: infer Props extends Record<string, unknown>;
} ? S extends {
required: ReadonlyArray<infer R extends string>;
} ? {
[K in keyof Props & R]: JsonSchemaToType<Props[K], Doc>;
} & {
[K in Exclude<keyof Props, R>]?: JsonSchemaToType<Props[K], Doc>;
} : {
[K in keyof Props]?: JsonSchemaToType<Props[K], Doc>;
} : S extends {
type: 'object';
} ? Record<string, unknown> : S extends {
allOf: ReadonlyArray<infer Items>;
} ? UnionToIntersection<JsonSchemaToType<Items, Doc>> : S extends {
oneOf: ReadonlyArray<infer Items>;
} ? JsonSchemaToType<Items, Doc> : S extends {
anyOf: ReadonlyArray<infer Items>;
} ? JsonSchemaToType<Items, Doc> : unknown;
type LowercaseHttpMethod = 'get' | 'put' | 'post' | 'delete' | 'patch' | 'head' | 'options' | 'trace';
type MethodMap = {
get: 'GET';
put: 'PUT';
post: 'POST';
delete: 'DELETE';
patch: 'PATCH';
head: 'HEAD';
options: 'OPTIONS';
trace: 'TRACE';
};
type UppercaseMethod<M extends string> = M extends keyof MethodMap ? MethodMap[M] : never;
type PathsWithMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> = {
[P in keyof NonNullable<T['paths']> & string]: NonNullable<T['paths']>[P] extends infer PathItem ? M extends keyof PathItem ? PathItem[M] extends object ? P : never : never : never;
}[keyof NonNullable<T['paths']> & string];
type GetOperation<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = NonNullable<T['paths']>[P] extends infer PathItem ? M extends keyof PathItem ? PathItem[M] : never : never;
type ExtractResponseSchema<Doc extends OpenApiDocument, Op> = Op extends {
responses: infer R;
} ? R extends {
'200': infer Resp200;
} ? ResolveSchemaOrRef<Doc, Resp200> extends {
content: {
'application/json': {
schema: infer S;
};
};
} ? JsonSchemaToType<S, Doc> : unknown : R extends {
'201': infer Resp201;
} ? ResolveSchemaOrRef<Doc, Resp201> extends {
content: {
'application/json': {
schema: infer S;
};
};
} ? JsonSchemaToType<S, Doc> : unknown : unknown : unknown;
type ExtractRequestBodySchema<Doc extends OpenApiDocument, Op> = Op extends {
requestBody: infer RB;
} ? ResolveSchemaOrRef<Doc, RB> extends {
content: {
'application/json': {
schema: infer S;
};
};
} ? JsonSchemaToType<S, Doc> : never : never;
type ExtractPathParamsFromPath<P extends string> = P extends `${string}{${infer Param}}${infer Rest}` ? {
[K in Param | keyof ExtractPathParamsFromPath<Rest>]: string;
} : never;
type HasPathParams<P extends string> = P extends `${string}{${string}}${string}` ? true : false;
type ResolvedParam<Doc extends OpenApiDocument, P> = P extends {
$ref: infer Ref extends string;
} ? ResolveRef<Doc, Ref> : P;
type ExtractQueryParamEntry<Doc extends OpenApiDocument, P> = ResolvedParam<Doc, P> extends {
in: 'query';
name: infer N extends string;
schema: infer S;
} ? {
[K in N]: JsonSchemaToType<S, Doc>;
} : ResolvedParam<Doc, P> extends {
in: 'query';
name: infer N extends string;
} ? {
[K in N]: string;
} : never;
type BuildQueryParamsFromTuple<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail] ? [ExtractQueryParamEntry<Doc, Head>] extends [never] ? BuildQueryParamsFromTuple<Doc, Tail> : ExtractQueryParamEntry<Doc, Head> & BuildQueryParamsFromTuple<Doc, Tail> : unknown;
type HasQueryParams<Doc extends OpenApiDocument, T> = T extends readonly [infer Head, ...infer Tail] ? [ExtractQueryParamEntry<Doc, Head>] extends [never] ? HasQueryParams<Doc, Tail> : true : false;
type BuildQueryParams<Doc extends OpenApiDocument, Op> = Op extends {
parameters: infer Params extends readonly unknown[];
} ? HasQueryParams<Doc, Params> extends true ? BuildQueryParamsFromTuple<Doc, Params> : never : never;
type ExtractTags<Op> = Op extends {
tags: infer T;
} ? T : never;
type ExtractDeprecated<Op> = Op extends {
deprecated: true;
} ? true : never;
type ExtractSummary<Op> = Op extends {
summary: infer S extends string;
} ? S : never;
type ExtractDescription<Op> = Op extends {
description: infer S extends string;
} ? S : never;
type BuildEndpoint<T extends OpenApiDocument, P extends string, M extends LowercaseHttpMethod> = {
result: ExtractResponseSchema<T, GetOperation<T, P, M>>;
} & ([ExtractRequestBodySchema<T, GetOperation<T, P, M>>] extends [never] ? unknown : {
body: ExtractRequestBodySchema<T, GetOperation<T, P, M>>;
}) & (HasPathParams<P> extends true ? {
url: ExtractPathParamsFromPath<P>;
} : unknown) & ([BuildQueryParams<T, GetOperation<T, P, M>>] extends [never] ? unknown : {
query: BuildQueryParams<T, GetOperation<T, P, M>>;
}) & ([ExtractTags<GetOperation<T, P, M>>] extends [never] ? unknown : {
tags: ExtractTags<GetOperation<T, P, M>>;
}) & ([ExtractDeprecated<GetOperation<T, P, M>>] extends [never] ? unknown : {
deprecated: true;
}) & ([ExtractSummary<GetOperation<T, P, M>>] extends [never] ? unknown : {
summary: ExtractSummary<GetOperation<T, P, M>>;
}) & ([ExtractDescription<GetOperation<T, P, M>>] extends [never] ? unknown : {
description: ExtractDescription<GetOperation<T, P, M>>;
});
type EndpointsForMethod<T extends OpenApiDocument, M extends LowercaseHttpMethod> = string extends PathsWithMethod<T, M> ? never : [PathsWithMethod<T, M>] extends [never] ? never : {
[P in PathsWithMethod<T, M> as ConvertOpenApiPath<P>]: BuildEndpoint<T, P, M>;
};
type CleanObject<T> = {
[K in keyof T as [T[K]] extends [never] ? never : K]: T[K];
};
/**
* Extracts a strongly-typed `RestApi` from an OpenAPI document type.
*
* Supports `$ref` resolution, `allOf`/`oneOf`/`anyOf` composition, `nullable`, `const`,
* and metadata extraction (`tags`, `deprecated`, `summary`, `description`).
*
* Use with `as const satisfies OpenApiDocument` to get full type inference:
*
* @example
* ```typescript
* import type { OpenApiDocument, OpenApiToRestApi } from '@furystack/rest'
* import { createClient } from '@furystack/rest-client-fetch'
*
* const apiDoc = { ... } as const satisfies OpenApiDocument
* type MyApi = OpenApiToRestApi<typeof apiDoc>
* const client = createClient<MyApi>({ endpointUrl: 'https://api.example.com' })
* ```
*/
export type OpenApiToRestApi<T extends OpenApiDocument> = CleanObject<{
[M in LowercaseHttpMethod as [EndpointsForMethod<T, M>] extends [never] ? never : UppercaseMethod<M>]: EndpointsForMethod<T, M>;
}> extends infer R extends RestApi ? R : never;
export {};
//# sourceMappingURL=openapi-to-rest-api.d.ts.map