From b09fb59b178185c6695e8d4ab9239496b193ae1e Mon Sep 17 00:00:00 2001 From: Patrick McElhaney Date: Wed, 29 May 2024 14:13:29 -0400 Subject: [PATCH] fix type for json() now that handles multiple types; fixes #918 --- .changeset/rare-pets-work.md | 5 +++ docs/usage.md | 7 ++-- src/server/types.d.ts | 70 +++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 .changeset/rare-pets-work.md diff --git a/.changeset/rare-pets-work.md b/.changeset/rare-pets-work.md new file mode 100644 index 00000000..284d4997 --- /dev/null +++ b/.changeset/rare-pets-work.md @@ -0,0 +1,5 @@ +--- +"counterfact": patch +--- + +fixed the type system to address that fact that json() handles multiple content types diff --git a/docs/usage.md b/docs/usage.md index 2102ee7c..dc32d638 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -111,10 +111,9 @@ The `$.response` object is used to build a valid response for the URL and reques - `.random()` returns random data, using `examples` and other metadata from the OpenAPI document. - `.header(name, value)` adds a response header. It will only show up when a response header is expected and you haven't already provided it. - `.match(contentType, content)` is used to return content which matches the content type. If the API is intended to serve one of multiple content types, depending on the client's `Accepts:` header, you can chain multiple `match()` calls. -- `.json(content)` is shorthand for `.match("application/json", content)` -- `.text(content)` is shorthand for `.match("text/plain", content)` -- `.html(content)` is shorthand for `.match("text/html", content)` -- `.xml(content)` is shorthand for `.match("application/xml", content)` + - `.json(content)`, `.text(content)`, `.html(content)`, and `.xml(content)` are shorthands for the `match()` function, e.g. `.text(content)` is shorthand for `.match("text/plain", content)`. + - if the content type is XML, you can pass a JSON object, and Counterfact will automatically convert it to XML for you + - The `.json()` shortcut handles both JSON and XML. You can build a response by chaining one or more of these functions, e.g. diff --git a/src/server/types.d.ts b/src/server/types.d.ts index cc613b34..14166cb8 100644 --- a/src/server/types.d.ts +++ b/src/server/types.d.ts @@ -14,6 +14,10 @@ interface Example { type MediaType = `${string}/${string}`; +type OmitAll = { + [P in keyof T as P extends K[number] ? never : P]: T[P]; +}; + type OmitValueWhenNever = Pick< Base, { @@ -24,25 +28,34 @@ type OmitValueWhenNever = Pick< interface OpenApiResponse { content: { [key: MediaType]: OpenApiContent }; headers: { [key: string]: OpenApiHeader }; - requiredHeaders: string + requiredHeaders: string; } interface OpenApiResponses { [key: string]: OpenApiResponse; } -type IfHasKey = Key extends keyof SomeObject - ? Yes +type IfHasKey = Keys extends [ + infer FirstKey, + ...infer RestKeys, +] + ? FirstKey extends keyof SomeObject + ? Yes + : RestKeys extends (keyof any)[] + ? IfHasKey + : No : No; type MaybeShortcut< - ContentType extends MediaType, + ContentTypes extends MediaType[], Response extends OpenApiResponse, > = IfHasKey< Response["content"], - ContentType, - (body: Response["content"][ContentType]["schema"]) => GenericResponseBuilder<{ - content: NeverIfEmpty>; + ContentTypes, + ( + body: Response["content"][ArrayToUnion]["schema"], + ) => GenericResponseBuilder<{ + content: NeverIfEmpty>; headers: Response["headers"]; requiredHeaders: Response["requiredHeaders"]; }>, @@ -55,7 +68,7 @@ type MatchFunction = < ContentType extends MediaType & keyof Response["content"], >( contentType: ContentType, - body: Response["content"][ContentType]["schema"] + body: Response["content"][ContentType]["schema"], ) => GenericResponseBuilder<{ content: NeverIfEmpty>; headers: Response["headers"]; @@ -66,7 +79,7 @@ type HeaderFunction = < Header extends string & keyof Response["headers"], >( header: Header, - value: Response["headers"][Header]["schema"] + value: Response["headers"][Header]["schema"], ) => GenericResponseBuilder<{ content: NeverIfEmpty; headers: NeverIfEmpty>; @@ -92,27 +105,36 @@ interface ResponseBuilder { xml: (body: unknown) => ResponseBuilder; } +type ArrayToUnion = T[number]; + type GenericResponseBuilderInner< Response extends OpenApiResponse = OpenApiResponse, > = OmitValueWhenNever<{ header: [keyof Response["headers"]] extends [never] ? never : HeaderFunction; - html: MaybeShortcut<"text/html", Response>; - json: MaybeShortcut<"application/json", Response>; + html: MaybeShortcut<["text/html"], Response>; + json: MaybeShortcut<["application/json", "text/json", "text/x-json", "application/xml", "text/xml"], Response>; match: [keyof Response["content"]] extends [never] ? never : MatchFunction; random: [keyof Response["content"]] extends [never] ? never : RandomFunction; - text: MaybeShortcut<"text/plain", Response>; - xml: MaybeShortcut<"application/xml", Response>; + text: MaybeShortcut<["text/plain"], Response>; + xml: MaybeShortcut<["application/xml", "text/xml"], Response>; }>; type GenericResponseBuilder< Response extends OpenApiResponse = OpenApiResponse, -> = {} extends OmitValueWhenNever ? "COUNTERFACT_RESPONSE" : keyof OmitValueWhenNever extends "headers" ? { header: HeaderFunction, ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE" } : GenericResponseBuilderInner; +> = {} extends OmitValueWhenNever + ? "COUNTERFACT_RESPONSE" + : keyof OmitValueWhenNever extends "headers" + ? { + ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE"; + header: HeaderFunction; + } + : GenericResponseBuilderInner; type ResponseBuilderFactory< Responses extends OpenApiResponses = OpenApiResponses, @@ -205,7 +227,7 @@ interface OpenApiOperation { }; } -type WideResponseBuilder = { +interface WideResponseBuilder { header: (body: unknown) => WideResponseBuilder; html: (body: unknown) => WideResponseBuilder; json: (body: unknown) => WideResponseBuilder; @@ -215,26 +237,24 @@ type WideResponseBuilder = { xml: (body: unknown) => WideResponseBuilder; } -type WideOperationArgument = { - path: Record; - query: Record; - header: Record; +interface WideOperationArgument { body: unknown; - response: Record; + context: unknown; + header: { [key: string]: string }; + path: { [key: string]: string }; proxy: (url: string) => { proxyUrl: string }; - context: unknown -}; + query: { [key: string]: string }; + response: { [key: number]: WideResponseBuilder }; +} export type { HttpStatusCode, MediaType, + OmitValueWhenNever, OpenApiOperation, OpenApiParameters, OpenApiResponse, ResponseBuilder, ResponseBuilderFactory, WideOperationArgument, - OmitValueWhenNever }; - -