Skip to content

Commit

Permalink
refactor: rewrite ResponseBody to extract from a ResponseMap with no-…
Browse files Browse the repository at this point in the history
…content support
  • Loading branch information
christoph-fricke committed Feb 19, 2024
1 parent 757e83f commit ff4ad10
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 15 deletions.
43 changes: 28 additions & 15 deletions src/api-spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type {
OperationRequestBodyContent,
PathsWithMethod,
ResponseContent,
ResponseObjectMap,
} from "openapi-typescript-helpers";
import type {
ConvertToStringified,
Expand Down Expand Up @@ -68,8 +66,11 @@ export type RequestBody<
? OperationRequestBodyContent<ApiSpec[Path][Method]>
: never;

/** Extract the response body of a given path and method from an api spec. */
export type ResponseBody<
/**
* Extract a response map of a given path and method from an api spec.
* A response map has the shape of (status -> media-type -> body).
*/
export type ResponseMap<
ApiSpec extends AnyApiSpec,
Path extends keyof ApiSpec,
Method extends HttpMethod,
Expand All @@ -78,21 +79,33 @@ export type ResponseBody<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
responses: any;
}
? ConvertNoContent<
MapToValues<
ResolvedObjectUnion<
ResponseContent<
MapToValues<ResponseObjectMap<ApiSpec[Path][Method]>>
>
>
>
>
? {
[Status in keyof ApiSpec[Path][Method]["responses"]]: ConvertContent<
ApiSpec[Path][Method]["responses"][Status]
>["content"];
}
: never
: never;

/** Extract the response body of a given path and method from an api spec. */
export type ResponseBody<
ApiSpec extends AnyApiSpec,
Path extends keyof ApiSpec,
Method extends HttpMethod,
> = MapToValues<
ResolvedObjectUnion<MapToValues<ResponseMap<ApiSpec, Path, Method>>>
>;

/**
* OpenAPI-TS generates "no content" with `content?: never`.
* However, `new Response().body` is `null` and strictly typing no-content in
* MSW requires `null`. Therefore, this helper maps no-content to `null`.
* MSW requires `null`. Therefore, this helper ensures that "no-content"
* can be mapped to null when typing the response body.
*/
type ConvertNoContent<Content> = [Content] extends [never] ? null : Content;
type ConvertContent<Content> =
Required<Content> extends { content: never }
? { content: { "no-content": null } }
: // eslint-disable-next-line @typescript-eslint/no-explicit-any
Content extends { content: any }
? Content
: never;
28 changes: 28 additions & 0 deletions test/fixtures/no-content.api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,31 @@ paths:
responses:
204:
description: No Content
/no-content-resource:
get:
summary: Get Resource With No Content
operationId: getResourceNoContent
responses:
200:
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Resource"
204:
description: NoContent
components:
schemas:
Resource:
type: object
required:
- id
- name
- value
properties:
id:
type: string
name:
type: string
value:
type: integer
11 changes: 11 additions & 0 deletions test/no-content.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,15 @@ describe("Given an OpenAPI schema endpoint with no-content", () => {
response.not.toEqualTypeOf<Response>();
response.toEqualTypeOf<StrictResponse<null>>();
});

test("When a endpoint with NoContent response is mocked, Then the no-content option is included in the response union", async () => {
type Endpoint = typeof http.get<"/no-content-resource">;
const resolver = expectTypeOf<Endpoint>().parameter(1);
const response = resolver.returns.extract<Response>();

response.not.toEqualTypeOf<Response>();
response.toEqualTypeOf<
StrictResponse<{ id: string; name: string; value: number } | null>
>();
});
});

0 comments on commit ff4ad10

Please sign in to comment.