From 6a5d60bbf46bdfae023cc88f5f75daa62196aefb Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Wed, 22 Nov 2023 09:38:37 -0700 Subject: [PATCH] Add examples docs --- docs/.vitepress/config.ts | 1 + docs/advanced.md | 404 +++++++++++--------------------------- docs/examples.md | 238 ++++++++++++++++++++++ pnpm-lock.yaml | 112 +++++------ 4 files changed, 410 insertions(+), 345 deletions(-) create mode 100644 docs/examples.md diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 4310c2804..3e7afefb5 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -52,6 +52,7 @@ export default defineConfig({ { text: "Introduction", link: "/introduction" }, { text: "CLI", link: "/cli" }, { text: "Node.js API", link: "/node" }, + { text: "Examples", link: "/examples" }, { text: "Migrating from 6.x", link: "/migration-guide" }, { text: "Advanced", link: "/advanced" }, { text: "About", link: "/about" }, diff --git a/docs/advanced.md b/docs/advanced.md index 24684fa17..2bd17a1aa 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -5,218 +5,79 @@ description: Advanced usage as well as tips, tricks, and best practices # Advanced usage -Advanced usage and various topics. +Advanced usage and various topics. Interpret this as loose recommendations for _most_ people, and feel free to disregard if it doesn’t work for your setup! -## Data fetching - -Fetching data can be done simply and safely using an **automatically-typed fetch wrapper**: - -- [openapi-fetch](/openapi-fetch/) (recommended) -- [openapi-typescript-fetch](https://www.npmjs.com/package/openapi-typescript-fetch) by [@ajaishankar](https://github.com/ajaishankar) - -::: tip - -A good fetch wrapper should **never use generics.** Generics require more typing and can hide errors! - -::: - -## Integrating with Mock-Service-Worker (MSW) - -Using `openapi-typescript` and a wrapper around fetch, such as `openapi-fetch`, ensures that our application's API client does not have conflicts with your OpenAPI specification. -However, while you can address issues with the API client easily, you have to "manually" remember to adjust API mocks since there is no mechanism that warns you about conflicts. - -If you are using [Mock Service Worker (MSW)](https://mswjs.io) to define your API mocks, you can use a **small, automatically-typed wrapper** around MSW, which enables you to address conflicts in your API mocks easily when your OpenAPI specification changes. Ultimately, you can have the same level of confidence in your application's API client **and** API mocks. - -We recommend the following wrapper, which works flawlessly with `openapi-typescript`: - -- [openapi-msw](https://www.npmjs.com/package/openapi-msw) by [@christoph-fricke](https://github.com/christoph-fricke) - -## Testing - -One of the most common causes of false positive tests is when mocks are out-of-date with the actual API responses. - -`openapi-typescript` offers a fantastic way to guard against this with minimal effort. Here’s one example how you could write your own helper function to typecheck all mocks to match your OpenAPI schema (we’ll use [vitest](https://vitest.dev/)/[vitest-fetch-mock](https://www.npmjs.com/package/vitest-fetch-mock) but the same principle could work for any setup): +## Debugging -Let’s say we want to write our mocks in the following object structure, so we can mock multiple endpoints at once: +To enable debugging, set `DEBUG=openapi-ts:*` as an env var like so: -```ts -{ - [pathname]: { - [HTTP method]: { status: [status], body: { …[some mock data] } }; - } -} +```sh +$ DEBUG=openapi-ts:* npx openapi-typescript schema.yaml -o my-types.ts ``` -Using our generated types we can then infer **the correct data shape** for any given path + HTTP method + status code. An example test would look like this: - -::: code-group [my-test.test.ts] - -```ts -import { mockResponses } from "../test/utils"; - -describe("My API test", () => { - it("mocks correctly", async () => { - mockResponses({ - "/users/{user_id}": { - // ✅ Correct 200 response - get: { status: 200, body: { id: "user-id", name: "User Name" } }, - // ✅ Correct 403 response - delete: { status: 403, body: { code: "403", message: "Unauthorized" } }, - }, - "/users": { - // ✅ Correct 201 response - put: { 201: { status: "success" } }, - }, - }); - - // test 1: GET /users/{user_id}: 200 - await fetch("/users/user-123"); - - // test 2: DELETE /users/{user_id}: 403 - await fetch("/users/user-123", { method: "DELETE" }); - - // test 3: PUT /users: 200 - await fetch("/users", { - method: "PUT", - body: JSON.stringify({ id: "new-user", name: "New User" }), - }); - - // test cleanup - fetchMock.resetMocks(); - }); -}); -``` +To only see certain types of debug messages, you can set `DEBUG=openapi-ts:[scope]` instead. Valid scopes are `redoc`, `lint`, `bundle`, and `ts`. -::: +Note that debug messages will be suppressed if the output is `stdout`. -_Note: this example uses a vanilla `fetch()` function, but any fetch wrapper—including [openapi-fetch](/openapi-fetch/)—could be dropped in instead without any changes._ +In no particular order, here are a few best practices to make life easier when working with OpenAPI-derived types. -And the magic that produces this would live in a `test/utils.ts` file that can be copy + pasted where desired (hidden for simplicity): +## Enum extensions -
-📄 test/utils.ts +`x-enum-varnames` can be used to have another enum name for the corresponding value. This is used to define names of the enum items. -::: code-group [test/utils.ts] +`x-enum-descriptions` can be used to provide an individual description for each value. This is used for comments in the code (like javadoc if the target language is java). -```ts -import { paths } from "./api/v1/my-schema"; // generated by openapi-typescript - -// Settings -// ⚠️ Important: change this! This prefixes all URLs -const BASE_URL = "https://myapi.com/v1"; -// End Settings - -// type helpers — ignore these; these just make TS lookups better -type FilterKeys = { - [K in keyof Obj]: K extends Matchers ? Obj[K] : never; -}[keyof Obj]; -type PathResponses = T extends { responses: any } ? T["responses"] : unknown; -type OperationContent = T extends { content: any } ? T["content"] : unknown; -type MediaType = `${string}/${string}`; -type MockedResponse = FilterKeys< - OperationContent, - MediaType -> extends never - ? { status: Status; body?: never } - : { - status: Status; - body: FilterKeys, MediaType>; - }; +`x-enum-descriptions` and `x-enum-varnames` are each expected to be list of items containing the same number of items as enum. The order of the items in the list matters: their position is used to group them together. -/** - * Mock fetch() calls and type against OpenAPI schema - */ -export function mockResponses(responses: { - [Path in keyof Partial]: { - [Method in keyof Partial]: MockedResponse< - PathResponses - >; - }; -}) { - fetchMock.mockResponse((req) => { - const mockedPath = findPath( - req.url.replace(BASE_URL, ""), - Object.keys(responses), - )!; - // note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature. - if (!mockedPath || (!responses as any)[mockedPath]) - throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior) - const method = req.method.toLowerCase(); - if (!(responses as any)[mockedPath][method]) - throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked - if (!(responses as any)[mockedPath][method]) { - throw new Error(`${req.method} called but not mocked on ${mockedPath}`); - } - const { status, body } = (responses as any)[mockedPath][method]; - return { status, body: JSON.stringify(body) }; - }); -} +Example: -// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id} -export function findPath( - actual: string, - testPaths: string[], -): string | undefined { - const url = new URL( - actual, - actual.startsWith("http") ? undefined : "http://testapi.com", - ); - const actualParts = url.pathname.split("/"); - for (const p of testPaths) { - let matched = true; - const testParts = p.split("/"); - if (actualParts.length !== testParts.length) continue; // automatically not a match if lengths differ - for (let i = 0; i < testParts.length; i++) { - if (testParts[i]!.startsWith("{")) continue; // path params ({user_id}) always count as a match - if (actualParts[i] !== testParts[i]) { - matched = false; - break; - } - } - if (matched) return p; - } -} +```yaml +ErrorCode: + type: integer + format: int32 + enum: + - 100 + - 200 + - 300 + x-enum-varnames: + - Unauthorized + - AccessDenied + - Unknown + x-enum-descriptions: + - "User is not authorized" + - "User has no access to this resource" + - "Something went wrong" ``` -::: - -::: info Additional Explanation - -That code is quite above is quite a doozy! For the most part, it’s a lot of implementation detail you can ignore. The `mockResponses(…)` function signature is where all the important magic happens—you’ll notice a direct link between this structure and our design. From there, the rest of the code is just making the runtime work as expected. - -::: +Will result in: ```ts -export function mockResponses(responses: { - [Path in keyof Partial]: { - [Method in keyof Partial]: MockedResponse< - PathResponses - >; - }; -}); +enum ErrorCode { + // User is not authorized + Unauthorized = 100 + // User has no access to this resource + AccessDenied = 200 + // Something went wrong + Unknown = 300 +} ``` -
+## Styleguide -Now, whenever your schema updates, **all your mock data will be typechecked correctly** 🎉. This is a huge step in ensuring resilient, accurate tests. +Loose recommendations to improve type generation. -## Debugging +### Redocly rules -To enable debugging, set `DEBUG=openapi-ts:*` as an env var like so: +To reduce errors in TypeScript generation, the following built-in rules are recommended to enforce in your [Redocly config](https://redocly.com/docs/cli/rules/built-in-rules/): -```sh -$ DEBUG=openapi-ts:* npx openapi-typescript schema.yaml -o my-types.ts -``` - -To only see certain types of debug messages, you can set `DEBUG=openapi-ts:[scope]` instead. Valid scopes are `redoc`, `lint`, `bundle`, and `ts`. - -Note that debug messages will be suppressed if the output is `stdout`. - -## Tips - -In no particular order, here are a few best practices to make life easier when working with OpenAPI-derived types. +| Rule | Setting | Reason | +| :-------------------------------------------------------------------------------------------- | :------------: | :----------------------------- | +| [operation-operationId-unique](https://redocly.com/docs/cli/rules/built-in-rules/#operations) | `error` | Prevents invalid TS generation | +| [operation-parameters-unique](https://redocly.com/docs/cli/rules/built-in-rules/#parameters) | `error` | Prevents missing params | +| [path-not-include-query](https://redocly.com/docs/cli/rules/built-in-rules/#parameters) | `error` | Prevents missing params | +| [spec](https://redocly.com/docs/cli/rules/built-in-rules/#special-rules) | `3.0` or `3.1` | Enables better schema checks | -### Embrace `snake_case` +### Embrace `snake_case` in JS Different languages have different preferred syntax styles. To name a few: @@ -226,20 +87,20 @@ Different languages have different preferred syntax styles. To name a few: - `PascalCase` - `kebab-case` -TypeScript, which this library is optimized for, uses mostly `camelCase` with some sprinkles of `PascalCase`(classes) and `SCREAMING_SNAKE_CASE` (constants). - -However, APIs are language-agnostic, and may contain a different syntax style from TypeScript (usually indiciative of the language of the backend). It’s not uncommon to encounter `snake_case` in object properties. And so it’s tempting for most JS/TS developers to want to enforce `camelCase` on everything for the sake of consistency. But it’s better to **resist that urge** because in addition to being a timesink, it introduces the following maintenance issues: +It’s tempting to want to rename API responses into `camelCase` that most JS styleguides encourage. However, **avoid renaming** because in addition to being a timesink, it introduces the following maintenance issues: - ❌ generated types (like the ones produced by openapi-typescript) now have to be manually typed again - ❌ renaming has to happen at runtime, which means you’re slowing down your application for an invisible change - ❌ name transformation utilities have to be built & maintained (and tested!) - ❌ the API probably needs `snake_case` for requestBodies anyway, so all that work now has to be undone for every API request -Instead, treat “consistency” in a more holistic sense, recognizing that preserving the API schema as-written is better than adhering to language-specific style conventions. +Instead, treat “consistency” in a more holistic sense, recognizing that preserving the API schema as-written is better than adhering to JS style conventions. -### Enable `noUncheckedIndexedAccess` +### Enable `noUncheckedIndexedAccess` in TSConfig -[Additional Properties](https://swagger.io/docs/specification/data-models/dictionaries/) (a.k.a. dictionaries) generate a type of `Record` in TypeScript. TypeScript’s default behavior is a bit dangerous because it will confidently assert a key is there even if you haven’t checked for it. For that reason it’s **highly recommended** to enable `compilerOptions.noUncheckedIndexedAccess` ([docs](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess)) so any `additionalProperties` key will be typed as `T | undefined`. +Enable `compilerOptions.noUncheckedIndexedAccess` in TSConfig ([docs](https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAccess)) so any `additionalProperties` key will be typed as `T | undefined`. + +The default behavior of [Additional Properties](https://swagger.io/docs/specification/data-models/dictionaries/) (dictionaries) will generate a type of `Record`, which can very easily produce null reference errors. TypeScript lets you access any arbitrary key without checking it exists first, so it won’t save you from typos or the event a key is just missing. ### Be specific in your schema @@ -407,73 +268,6 @@ prefixItems: -### Use `$defs` only in object types - -[JSONSchema $defs](https://json-schema.org/understanding-json-schema/structuring.html#defs) can be used to provide sub-schema definitions anywhere. However, these won’t always convert cleanly to TypeScript. For example, this works: - -```yaml -components: - schemas: - DefType: - type: object # ✅ `type: "object"` is OK to define $defs on - $defs: - myDefType: - type: string - MyType: - type: object - properties: - myType: - $ref: "#/components/schemas/DefType/$defs/myDefType" -``` - -This will transform into the following TypeScript: - -```ts -export interface components { - schemas: { - DefType: { - $defs: { - myDefType: string; - }; - }; - MyType: { - myType?: components["schemas"]["DefType"]["$defs"]["myDefType"]; // ✅ Works - }; - }; -} -``` - -However, this won’t: - -```yaml -components: - schemas: - DefType: - type: string # ❌ this won’t keep its $defs - $defs: - myDefType: - type: string - MyType: - properties: - myType: - $ref: "#/components/schemas/DefType/$defs/myDefType" -``` - -Because it will transform into: - -```ts -export interface components { - schemas: { - DefType: string; - MyType: { - myType?: components["schemas"]["DefType"]["$defs"]["myDefType"]; // ❌ Property '$defs' does not exist on type 'String'. - }; - }; -} -``` - -So be wary about where you define `$defs` as they may go missing in your final generated types. When in doubt, you can always define `$defs` at the root schema level. - ### Use `oneOf` by itself OpenAPI’s composition tools (`oneOf`/`anyOf`/`allOf`) are powerful tools for reducing the amount of code in your schema while maximizing flexibility. TypeScript unions, however, don’t provide [XOR behavior](https://en.wikipedia.org/wiki/Exclusive_or), which means they don’t map directly to `oneOf`. For that reason, it’s recommended to use `oneOf` by itself, and not combined with other composition methods or other properties. e.g.: @@ -547,43 +341,75 @@ _Note: you optionally could provide `discriminator.propertyName: "type"` on `Pet While the schema permits you to use composition in any way you like, it’s good to always take a look at the generated types and see if there’s a simpler way to express your unions & intersections. Limiting the use of `oneOf` is not the only way to do that, but often yields the greatest benefits. -### Enum with custom names and descriptions +## JSONSchema $defs caveats -`x-enum-varnames` can be used to have another enum name for the corresponding value. This is used to define names of the enum items. +[JSONSchema $defs](https://json-schema.org/understanding-json-schema/structuring.html#defs) can be used to provide sub-schema definitions anywhere. However, these won’t always convert cleanly to TypeScript. For example, this works: -`x-enum-descriptions` can be used to provide an individual description for each value. This is used for comments in the code (like javadoc if the target language is java). +```yaml +components: + schemas: + DefType: + type: object # ✅ `type: "object"` is OK to define $defs on + $defs: + myDefType: + type: string + MyType: + type: object + properties: + myType: + $ref: "#/components/schemas/DefType/$defs/myDefType" +``` -`x-enum-descriptions` and `x-enum-varnames` are each expected to be list of items containing the same number of items as enum. The order of the items in the list matters: their position is used to group them together. +This will transform into the following TypeScript: -Example: +```ts +export interface components { + schemas: { + DefType: { + $defs: { + myDefType: string; + }; + }; + MyType: { + myType?: components["schemas"]["DefType"]["$defs"]["myDefType"]; // ✅ Works + }; + }; +} +``` + +However, this won’t: ```yaml -ErrorCode: - type: integer - format: int32 - enum: - - 100 - - 200 - - 300 - x-enum-varnames: - - Unauthorized - - AccessDenied - - Unknown - x-enum-descriptions: - - "User is not authorized" - - "User has no access to this resource" - - "Something went wrong" +components: + schemas: + DefType: + type: string # ❌ this won’t keep its $defs + $defs: + myDefType: + type: string + MyType: + properties: + myType: + $ref: "#/components/schemas/DefType/$defs/myDefType" ``` -Will result in: +Because it will transform into: ```ts -enum ErrorCode { - // User is not authorized - Unauthorized = 100 - // User has no access to this resource - AccessDenied = 200 - // Something went wrong - Unknown = 300 +export interface components { + schemas: { + DefType: string; + MyType: { + myType?: components["schemas"]["DefType"]["$defs"]["myDefType"]; // ❌ Property '$defs' does not exist on type 'String'. + }; + }; } ``` + +So be wary about where you define `$defs` as they may go missing in your final generated types. + +::: tip + +When in doubt, you can always define `$defs` at the root schema level. + +::: diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 000000000..93bf9673d --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,238 @@ +--- +title: Examples +description: Using openapi-typescript in real-world applications +--- + +# Examples + +The types generated by openapi-typescript are universal, and can be used in a variety of ways. While these examples are not comprehensive, hopefully they’ll spark ideas about how to use these in your app. + +## Data fetching + +Fetching data can be done simply and safely using an **automatically-typed fetch wrapper**: + +- [openapi-fetch](/openapi-fetch/) (recommended) +- [openapi-typescript-fetch](https://www.npmjs.com/package/openapi-typescript-fetch) by [@ajaishankar](https://github.com/ajaishankar) + +::: tip + +A good fetch wrapper should **never use generics.** Generics require more typing and can hide errors! + +::: + +## Hono + +[Hono](https://hono.dev/) is a modern server framework for Node.js that can be deployed to the edge (e.g. [Cloudflare Workers](https://developers.cloudflare.com/workers/)) just as easily as a standard container. It also has TypeScript baked-in, so it’s a great fit for generated types. + +After [generating your types using the CLI](/introduction), pass in the proper `paths` response for each endpoint: + +```ts +import { Hono } from "hono"; +import { components, paths } from "./path/to/my/types"; + +const app = new Hono(); + +/** /users */ +app.get("/users", async (ctx) => { + try { + const users = db.get("SELECT * from users"); + return ctx.json< + paths["/users"]["responses"][200]["content"]["application/json"] + >(users); + } catch (err) { + return ctx.json({ + status: 500, + message: err ?? "An error occurred", + }); + } +}); + +export default app; +``` + +::: tip + +TypeChecking in server environments can be tricky, as you’re often querying databases and talking to other endpoints that TypeScript can’t introspect. But using generics will alert you of the obvious errors that TypeScript _can_ catch (and more things in your stack may have types than you realize!). + +::: + +## Mock-Service-Worker (MSW) + +If you are using [Mock Service Worker (MSW)](https://mswjs.io) to define your API mocks, you can use a **small, automatically-typed wrapper** around MSW, which enables you to address conflicts in your API mocks easily when your OpenAPI specification changes. Ultimately, you can have the same level of confidence in your application's API client **and** API mocks. + +Using `openapi-typescript` and a wrapper around fetch, such as `openapi-fetch`, ensures that our application's API client does not have conflicts with your OpenAPI specification. + +However, while you can address issues with the API client easily, you have to "manually" remember to adjust API mocks since there is no mechanism that warns you about conflicts. + +We recommend the following wrapper, which works flawlessly with `openapi-typescript`: + +- [openapi-msw](https://www.npmjs.com/package/openapi-msw) by [@christoph-fricke](https://github.com/christoph-fricke) + +## Test Mocks + +One of the most common causes of false positive tests is when mocks are out-of-date with the actual API responses. + +`openapi-typescript` offers a fantastic way to guard against this with minimal effort. Here’s one example how you could write your own helper function to typecheck all mocks to match your OpenAPI schema (we’ll use [vitest](https://vitest.dev/)/[vitest-fetch-mock](https://www.npmjs.com/package/vitest-fetch-mock) but the same principle could work for any setup): + +Let’s say we want to write our mocks in the following object structure, so we can mock multiple endpoints at once: + +```ts +{ + [pathname]: { + [HTTP method]: { status: [status], body: { …[some mock data] } }; + } +} +``` + +Using our generated types we can then infer **the correct data shape** for any given path + HTTP method + status code. An example test would look like this: + +::: code-group [my-test.test.ts] + +```ts +import { mockResponses } from "../test/utils"; + +describe("My API test", () => { + it("mocks correctly", async () => { + mockResponses({ + "/users/{user_id}": { + // ✅ Correct 200 response + get: { status: 200, body: { id: "user-id", name: "User Name" } }, + // ✅ Correct 403 response + delete: { status: 403, body: { code: "403", message: "Unauthorized" } }, + }, + "/users": { + // ✅ Correct 201 response + put: { 201: { status: "success" } }, + }, + }); + + // test 1: GET /users/{user_id}: 200 + await fetch("/users/user-123"); + + // test 2: DELETE /users/{user_id}: 403 + await fetch("/users/user-123", { method: "DELETE" }); + + // test 3: PUT /users: 200 + await fetch("/users", { + method: "PUT", + body: JSON.stringify({ id: "new-user", name: "New User" }), + }); + + // test cleanup + fetchMock.resetMocks(); + }); +}); +``` + +::: + +_Note: this example uses a vanilla `fetch()` function, but any fetch wrapper—including [openapi-fetch](/openapi-fetch/)—could be dropped in instead without any changes._ + +And the magic that produces this would live in a `test/utils.ts` file that can be copy + pasted where desired (hidden for simplicity): + +
+📄 test/utils.ts + +::: code-group [test/utils.ts] + +```ts +import { paths } from "./api/v1/my-schema"; // generated by openapi-typescript + +// Settings +// ⚠️ Important: change this! This prefixes all URLs +const BASE_URL = "https://myapi.com/v1"; +// End Settings + +// type helpers — ignore these; these just make TS lookups better +type FilterKeys = { + [K in keyof Obj]: K extends Matchers ? Obj[K] : never; +}[keyof Obj]; +type PathResponses = T extends { responses: any } ? T["responses"] : unknown; +type OperationContent = T extends { content: any } ? T["content"] : unknown; +type MediaType = `${string}/${string}`; +type MockedResponse = FilterKeys< + OperationContent, + MediaType +> extends never + ? { status: Status; body?: never } + : { + status: Status; + body: FilterKeys, MediaType>; + }; + +/** + * Mock fetch() calls and type against OpenAPI schema + */ +export function mockResponses(responses: { + [Path in keyof Partial]: { + [Method in keyof Partial]: MockedResponse< + PathResponses + >; + }; +}) { + fetchMock.mockResponse((req) => { + const mockedPath = findPath( + req.url.replace(BASE_URL, ""), + Object.keys(responses), + )!; + // note: we get lazy with the types here, because the inference is bad anyway and this has a `void` return signature. The important bit is the parameter signature. + if (!mockedPath || (!responses as any)[mockedPath]) + throw new Error(`No mocked response for ${req.url}`); // throw error if response not mocked (remove or modify if you’d like different behavior) + const method = req.method.toLowerCase(); + if (!(responses as any)[mockedPath][method]) + throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // likewise throw error if other parts of response aren’t mocked + if (!(responses as any)[mockedPath][method]) { + throw new Error(`${req.method} called but not mocked on ${mockedPath}`); + } + const { status, body } = (responses as any)[mockedPath][method]; + return { status, body: JSON.stringify(body) }; + }); +} + +// helper function that matches a realistic URL (/users/123) to an OpenAPI path (/users/{user_id} +export function findPath( + actual: string, + testPaths: string[], +): string | undefined { + const url = new URL( + actual, + actual.startsWith("http") ? undefined : "http://testapi.com", + ); + const actualParts = url.pathname.split("/"); + for (const p of testPaths) { + let matched = true; + const testParts = p.split("/"); + if (actualParts.length !== testParts.length) continue; // automatically not a match if lengths differ + for (let i = 0; i < testParts.length; i++) { + if (testParts[i]!.startsWith("{")) continue; // path params ({user_id}) always count as a match + if (actualParts[i] !== testParts[i]) { + matched = false; + break; + } + } + if (matched) return p; + } +} +``` + +::: + +::: info Additional Explanation + +That code is quite above is quite a doozy! For the most part, it’s a lot of implementation detail you can ignore. The `mockResponses(…)` function signature is where all the important magic happens—you’ll notice a direct link between this structure and our design. From there, the rest of the code is just making the runtime work as expected. + +::: + +```ts +export function mockResponses(responses: { + [Path in keyof Partial]: { + [Method in keyof Partial]: MockedResponse< + PathResponses + >; + }; +}); +``` + +
+ +Now, whenever your schema updates, **all your mock data will be typechecked correctly** 🎉. This is a huge step in ensuring resilient, accurate tests. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f2ddc3842..e89dd45c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -405,8 +405,8 @@ packages: chalk: 2.4.2 dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + /@babel/helper-string-parser@7.23.4: + resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} engines: {node: '>=6.9.0'} dev: true @@ -429,12 +429,12 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/parser@7.23.3: - resolution: {integrity: sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==} + /@babel/parser@7.23.4: + resolution: {integrity: sha512-vf3Xna6UEprW+7t6EtOmFpHNAuxw3xqPZghy+brsnusscJRW5BMUzzHZc5ICjULee81WeUV2jjakG09MDglJXQ==} engines: {node: '>=6.0.0'} hasBin: true dependencies: - '@babel/types': 7.23.3 + '@babel/types': 7.23.4 dev: true /@babel/runtime@7.23.4: @@ -444,11 +444,11 @@ packages: regenerator-runtime: 0.14.0 dev: true - /@babel/types@7.23.3: - resolution: {integrity: sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==} + /@babel/types@7.23.4: + resolution: {integrity: sha512-7uIFwVYpoplT5jp/kVv6EF93VaJ8H+Yn5IczYiaAi98ajzjfoZfslet/e0sLh+wVBjb2qqIut1b0S26VSafsSQ==} engines: {node: '>=6.9.0'} dependencies: - '@babel/helper-string-parser': 7.22.5 + '@babel/helper-string-parser': 7.23.4 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 dev: true @@ -1565,96 +1565,96 @@ packages: - encoding dev: false - /@rollup/rollup-android-arm-eabi@4.5.0: - resolution: {integrity: sha512-OINaBGY+Wc++U0rdr7BLuFClxcoWaVW3vQYqmQq6B3bqQ/2olkaoz+K8+af/Mmka/C2yN5j+L9scBkv4BtKsDA==} + /@rollup/rollup-android-arm-eabi@4.5.1: + resolution: {integrity: sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.5.0: - resolution: {integrity: sha512-UdMf1pOQc4ZmUA/NTmKhgJTBimbSKnhPS2zJqucqFyBRFPnPDtwA8MzrGNTjDeQbIAWfpJVAlxejw+/lQyBK/w==} + /@rollup/rollup-android-arm64@4.5.1: + resolution: {integrity: sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.5.0: - resolution: {integrity: sha512-L0/CA5p/idVKI+c9PcAPGorH6CwXn6+J0Ys7Gg1axCbTPgI8MeMlhA6fLM9fK+ssFhqogMHFC8HDvZuetOii7w==} + /@rollup/rollup-darwin-arm64@4.5.1: + resolution: {integrity: sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.5.0: - resolution: {integrity: sha512-QZCbVqU26mNlLn8zi/XDDquNmvcr4ON5FYAHQQsyhrHx8q+sQi/6xduoznYXwk/KmKIXG5dLfR0CvY+NAWpFYQ==} + /@rollup/rollup-darwin-x64@4.5.1: + resolution: {integrity: sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.5.0: - resolution: {integrity: sha512-VpSQ+xm93AeV33QbYslgf44wc5eJGYfYitlQzAi3OObu9iwrGXEnmu5S3ilkqE3Pr/FkgOiJKV/2p0ewf4Hrtg==} + /@rollup/rollup-linux-arm-gnueabihf@4.5.1: + resolution: {integrity: sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.5.0: - resolution: {integrity: sha512-OrEyIfpxSsMal44JpEVx9AEcGpdBQG1ZuWISAanaQTSMeStBW+oHWwOkoqR54bw3x8heP8gBOyoJiGg+fLY8qQ==} + /@rollup/rollup-linux-arm64-gnu@4.5.1: + resolution: {integrity: sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.5.0: - resolution: {integrity: sha512-1H7wBbQuE6igQdxMSTjtFfD+DGAudcYWhp106z/9zBA8OQhsJRnemO4XGavdzHpGhRtRxbgmUGdO3YQgrWf2RA==} + /@rollup/rollup-linux-arm64-musl@4.5.1: + resolution: {integrity: sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.5.0: - resolution: {integrity: sha512-FVyFI13tXw5aE65sZdBpNjPVIi4Q5mARnL/39UIkxvSgRAIqCo5sCpCELk0JtXHGee2owZz5aNLbWNfBHzr71Q==} + /@rollup/rollup-linux-x64-gnu@4.5.1: + resolution: {integrity: sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.5.0: - resolution: {integrity: sha512-eBPYl2sLpH/o8qbSz6vPwWlDyThnQjJfcDOGFbNjmjb44XKC1F5dQfakOsADRVrXCNzM6ZsSIPDG5dc6HHLNFg==} + /@rollup/rollup-linux-x64-musl@4.5.1: + resolution: {integrity: sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.5.0: - resolution: {integrity: sha512-xaOHIfLOZypoQ5U2I6rEaugS4IYtTgP030xzvrBf5js7p9WI9wik07iHmsKaej8Z83ZDxN5GyypfoyKV5O5TJA==} + /@rollup/rollup-win32-arm64-msvc@4.5.1: + resolution: {integrity: sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.5.0: - resolution: {integrity: sha512-Al6quztQUrHwcOoU2TuFblUQ5L+/AmPBXFR6dUvyo4nRj2yQRK0WIUaGMF/uwKulvRcXkpHe3k9A8Vf93VDktA==} + /@rollup/rollup-win32-ia32-msvc@4.5.1: + resolution: {integrity: sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.5.0: - resolution: {integrity: sha512-8kdW+brNhI/NzJ4fxDufuJUjepzINqJKLGHuxyAtpPG9bMbn8P5mtaCcbOm0EzLJ+atg+kF9dwg8jpclkVqx5w==} + /@rollup/rollup-win32-x64-msvc@4.5.1: + resolution: {integrity: sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==} cpu: [x64] os: [win32] requiresBuild: true @@ -1932,8 +1932,8 @@ packages: resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} dev: true - /@types/markdown-it@13.0.6: - resolution: {integrity: sha512-0VqpvusJn1/lwRegCxcHVdmLfF+wIsprsKMC9xW8UPcTxhFcQtoN/fBU1zMe8pH7D/RuueMh2CaBaNv+GrLqTw==} + /@types/markdown-it@13.0.7: + resolution: {integrity: sha512-U/CBi2YUUcTHBt5tjO2r5QV/x0Po6nsYwQU4Y04fBS6vfoImaiZ6f8bi3CjTCxBPQSO1LMyUqkByzi8AidyxfA==} dependencies: '@types/linkify-it': 3.0.5 '@types/mdurl': 1.0.5 @@ -2205,7 +2205,7 @@ packages: /@vue/compiler-core@3.3.8: resolution: {integrity: sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==} dependencies: - '@babel/parser': 7.23.3 + '@babel/parser': 7.23.4 '@vue/shared': 3.3.8 estree-walker: 2.0.2 source-map-js: 1.0.2 @@ -2221,7 +2221,7 @@ packages: /@vue/compiler-sfc@3.3.8: resolution: {integrity: sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==} dependencies: - '@babel/parser': 7.23.3 + '@babel/parser': 7.23.4 '@vue/compiler-core': 3.3.8 '@vue/compiler-dom': 3.3.8 '@vue/compiler-ssr': 3.3.8 @@ -2247,7 +2247,7 @@ packages: /@vue/reactivity-transform@3.3.8: resolution: {integrity: sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==} dependencies: - '@babel/parser': 7.23.3 + '@babel/parser': 7.23.4 '@vue/compiler-core': 3.3.8 '@vue/shared': 3.3.8 estree-walker: 2.0.2 @@ -4693,8 +4693,8 @@ packages: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} dev: true - /minisearch@6.2.0: - resolution: {integrity: sha512-BECkorDF1TY2rGKt9XHdSeP9TP29yUbrAaCh/C03wpyf1vx3uYcP/+8XlMcpTkgoU0rBVnHMAOaP83Rc9Tm+TQ==} + /minisearch@6.3.0: + resolution: {integrity: sha512-ihFnidEeU8iXzcVHy74dhkxh/dn8Dc08ERl0xwoMMGqp4+LvRSCgicb+zGqWthVokQKvCSxITlh3P08OzdTYCQ==} dev: true /mixme@0.5.10: @@ -5438,23 +5438,23 @@ packages: fsevents: 2.3.3 dev: true - /rollup@4.5.0: - resolution: {integrity: sha512-41xsWhzxqjMDASCxH5ibw1mXk+3c4TNI2UjKbLxe6iEzrSQnqOzmmK8/3mufCPbzHNJ2e04Fc1ddI35hHy+8zg==} + /rollup@4.5.1: + resolution: {integrity: sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.5.0 - '@rollup/rollup-android-arm64': 4.5.0 - '@rollup/rollup-darwin-arm64': 4.5.0 - '@rollup/rollup-darwin-x64': 4.5.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.5.0 - '@rollup/rollup-linux-arm64-gnu': 4.5.0 - '@rollup/rollup-linux-arm64-musl': 4.5.0 - '@rollup/rollup-linux-x64-gnu': 4.5.0 - '@rollup/rollup-linux-x64-musl': 4.5.0 - '@rollup/rollup-win32-arm64-msvc': 4.5.0 - '@rollup/rollup-win32-ia32-msvc': 4.5.0 - '@rollup/rollup-win32-x64-msvc': 4.5.0 + '@rollup/rollup-android-arm-eabi': 4.5.1 + '@rollup/rollup-android-arm64': 4.5.1 + '@rollup/rollup-darwin-arm64': 4.5.1 + '@rollup/rollup-darwin-x64': 4.5.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.5.1 + '@rollup/rollup-linux-arm64-gnu': 4.5.1 + '@rollup/rollup-linux-arm64-musl': 4.5.1 + '@rollup/rollup-linux-x64-gnu': 4.5.1 + '@rollup/rollup-linux-x64-musl': 4.5.1 + '@rollup/rollup-win32-arm64-msvc': 4.5.1 + '@rollup/rollup-win32-ia32-msvc': 4.5.1 + '@rollup/rollup-win32-x64-msvc': 4.5.1 fsevents: 2.3.3 dev: true @@ -6330,7 +6330,7 @@ packages: '@types/node': 20.9.4 esbuild: 0.19.7 postcss: 8.4.31 - rollup: 4.5.0 + rollup: 4.5.1 optionalDependencies: fsevents: 2.3.3 dev: true @@ -6360,14 +6360,14 @@ packages: dependencies: '@docsearch/css': 3.5.2 '@docsearch/js': 3.5.2(@algolia/client-search@4.20.0)(search-insights@2.11.0) - '@types/markdown-it': 13.0.6 + '@types/markdown-it': 13.0.7 '@vitejs/plugin-vue': 4.5.0(vite@5.0.2)(vue@3.3.8) '@vue/devtools-api': 6.5.1 '@vueuse/core': 10.6.1(vue@3.3.8) '@vueuse/integrations': 10.6.1(focus-trap@7.5.4)(vue@3.3.8) focus-trap: 7.5.4 mark.js: 8.11.1 - minisearch: 6.2.0 + minisearch: 6.3.0 mrmime: 1.0.1 shiki: 0.14.5 vite: 5.0.2(@types/node@20.9.4)