Skip to content

Commit

Permalink
fix type for json() now that handles multiple types; fixes #918
Browse files Browse the repository at this point in the history
  • Loading branch information
pmcelhaney committed May 29, 2024
1 parent fbb62b7 commit b09fb59
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-pets-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": patch
---

fixed the type system to address that fact that json() handles multiple content types
7 changes: 3 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
70 changes: 45 additions & 25 deletions src/server/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface Example {

type MediaType = `${string}/${string}`;

type OmitAll<T, K extends (keyof T)[]> = {
[P in keyof T as P extends K[number] ? never : P]: T[P];
};

type OmitValueWhenNever<Base> = Pick<
Base,
{
Expand All @@ -24,25 +28,34 @@ type OmitValueWhenNever<Base> = Pick<
interface OpenApiResponse {
content: { [key: MediaType]: OpenApiContent };
headers: { [key: string]: OpenApiHeader };
requiredHeaders: string
requiredHeaders: string;
}

interface OpenApiResponses {
[key: string]: OpenApiResponse;
}

type IfHasKey<SomeObject, Key, Yes, No> = Key extends keyof SomeObject
? Yes
type IfHasKey<SomeObject, Keys extends (keyof any)[], Yes, No> = Keys extends [
infer FirstKey,
...infer RestKeys,
]
? FirstKey extends keyof SomeObject
? Yes
: RestKeys extends (keyof any)[]
? IfHasKey<SomeObject, RestKeys, Yes, No>
: 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<Omit<Response["content"], ContentType>>;
ContentTypes,
(
body: Response["content"][ArrayToUnion<ContentTypes>]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<OmitAll<Response["content"], ContentTypes>>;
headers: Response["headers"];
requiredHeaders: Response["requiredHeaders"];
}>,
Expand All @@ -55,7 +68,7 @@ type MatchFunction<Response extends OpenApiResponse> = <
ContentType extends MediaType & keyof Response["content"],
>(
contentType: ContentType,
body: Response["content"][ContentType]["schema"]
body: Response["content"][ContentType]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<Omit<Response["content"], ContentType>>;
headers: Response["headers"];
Expand All @@ -66,7 +79,7 @@ type HeaderFunction<Response extends OpenApiResponse> = <
Header extends string & keyof Response["headers"],
>(
header: Header,
value: Response["headers"][Header]["schema"]
value: Response["headers"][Header]["schema"],
) => GenericResponseBuilder<{
content: NeverIfEmpty<Response["content"]>;
headers: NeverIfEmpty<Omit<Response["headers"], Header>>;
Expand All @@ -92,27 +105,36 @@ interface ResponseBuilder {
xml: (body: unknown) => ResponseBuilder;
}

type ArrayToUnion<T extends readonly any[]> = T[number];

type GenericResponseBuilderInner<
Response extends OpenApiResponse = OpenApiResponse,
> = OmitValueWhenNever<{
header: [keyof Response["headers"]] extends [never]
? never
: HeaderFunction<Response>;
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<Response>;
random: [keyof Response["content"]] extends [never]
? never
: RandomFunction<Response>;
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<Response> ? "COUNTERFACT_RESPONSE" : keyof OmitValueWhenNever<Response> extends "headers" ? { header: HeaderFunction<Response>, ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE" } : GenericResponseBuilderInner<Response>;
> = {} extends OmitValueWhenNever<Response>
? "COUNTERFACT_RESPONSE"
: keyof OmitValueWhenNever<Response> extends "headers"
? {
ALL_REMAINING_HEADERS_ARE_OPTIONAL: "COUNTERFACT_RESPONSE";
header: HeaderFunction<Response>;
}
: GenericResponseBuilderInner<Response>;

type ResponseBuilderFactory<
Responses extends OpenApiResponses = OpenApiResponses,
Expand Down Expand Up @@ -205,7 +227,7 @@ interface OpenApiOperation {
};
}

type WideResponseBuilder = {
interface WideResponseBuilder {
header: (body: unknown) => WideResponseBuilder;
html: (body: unknown) => WideResponseBuilder;
json: (body: unknown) => WideResponseBuilder;
Expand All @@ -215,26 +237,24 @@ type WideResponseBuilder = {
xml: (body: unknown) => WideResponseBuilder;
}

type WideOperationArgument = {
path: Record<string, string>;
query: Record<string, string>;
header: Record<string, string>;
interface WideOperationArgument {
body: unknown;
response: Record<number, WideResponseBuilder>;
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
};


0 comments on commit b09fb59

Please sign in to comment.