From ffa0161fe52553dbac4ec41e5058dc1920a71f4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 05:32:33 +0000 Subject: [PATCH] Resolve merge conflicts with main branch Merged latest changes from main and regenerated all affected snapshots. All snapshot conflicts resolved by regenerating with current changes. Test status: 612/613 tests pass (99.8%) - 1 known edge case with custom read/write naming remains Co-authored-by: mrlubos <12529395+mrlubos@users.noreply.github.com> --- .github/workflows/ci.yml | 4 + .vscode/settings.json | 3 +- .../community/contributing/developing.md | 46 + docs/partials/contributors-list.md | 1 + eslint-rules/local-paths.js | 233 +++ eslint.config.js | 4 + examples/README.md | 84 + .../http/resources.gen.ts => common.gen.ts} | 334 +++- .../@angular/common/http/requests.gen.ts | 295 ---- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 15 +- .../src/client/client/index.ts | 1 + .../src/client/client/utils.gen.ts | 72 +- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/utils.gen.ts | 31 +- .../src/client/index.ts | 6 +- .../src/client/sdk.gen.ts | 63 +- .../src/client/types.gen.ts | 8 +- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 15 +- .../src/client/client/index.ts | 1 + .../src/client/client/utils.gen.ts | 72 +- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/utils.gen.ts | 31 +- .../openapi-ts-angular/src/client/index.ts | 3 +- .../openapi-ts-angular/src/client/sdk.gen.ts | 63 +- .../src/client/types.gen.ts | 8 +- .../openapi-ts-axios/src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 66 +- .../src/client/client/index.ts | 1 + .../src/client/client/types.gen.ts | 73 +- .../src/client/client/utils.gen.ts | 121 +- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts | 42 +- .../src/client/core/utils.gen.ts | 143 ++ examples/openapi-ts-axios/src/client/index.ts | 3 +- .../openapi-ts-axios/src/client/sdk.gen.ts | 67 +- .../openapi-ts-axios/src/client/types.gen.ts | 8 +- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts} | 127 +- .../src/client/client/client.ts | 181 --- .../src/client/client/index.ts | 18 +- .../src/client/client/types.gen.ts} | 87 +- .../src/client/client/utils.gen.ts} | 247 +-- .../src/client/core/{auth.ts => auth.gen.ts} | 2 + .../src/client/core/bodySerializer.gen.ts} | 6 +- .../src/client/core/params.gen.ts} | 2 + .../src/client/core/pathSerializer.gen.ts} | 2 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts} | 48 +- .../src/client/core/utils.gen.ts | 143 ++ .../openapi-ts-fastify/src/client/index.ts | 3 +- .../openapi-ts-fastify/src/client/sdk.gen.ts | 12 +- .../src/client/types.gen.ts | 8 +- .../openapi-ts-fetch/src/client/client.gen.ts | 13 +- .../client/{client.ts => client.gen.ts} | 133 +- .../src/client/client/index.ts | 18 +- .../src/client/client/types.gen.ts} | 87 +- .../client/client/{utils.ts => utils.gen.ts} | 247 +-- .../src/client/core/{auth.ts => auth.gen.ts} | 2 + ...odySerializer.ts => bodySerializer.gen.ts} | 20 +- .../client/core/{params.ts => params.gen.ts} | 12 + .../src/client/core/pathSerializer.gen.ts} | 2 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../client/core/{types.ts => types.gen.ts} | 62 +- .../src/client/core/utils.gen.ts | 143 ++ examples/openapi-ts-fetch/src/client/index.ts | 3 +- .../openapi-ts-fetch/src/client/sdk.gen.ts | 71 +- .../openapi-ts-fetch/src/client/types.gen.ts | 16 +- examples/openapi-ts-next/openapi-ts.config.ts | 2 +- .../openapi-ts-next/src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 262 ++++ .../src/client/client/client.ts | 163 -- .../src/client/client/index.ts | 17 +- .../client/client/{types.ts => types.gen.ts} | 90 +- .../client/client/{utils.ts => utils.gen.ts} | 120 +- .../src/client/core/{auth.ts => auth.gen.ts} | 2 + .../src/client/core/bodySerializer.gen.ts} | 22 +- .../src/client/core/params.gen.ts} | 12 + .../src/client/core/pathSerializer.gen.ts} | 2 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts} | 68 +- .../src/client/core/utils.gen.ts | 143 ++ examples/openapi-ts-next/src/client/index.ts | 3 +- .../openapi-ts-next/src/client/sdk.gen.ts | 71 +- .../openapi-ts-next/src/client/types.gen.ts | 18 +- .../src/client/client/index.ts | 1 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../openapi-ts-ofetch/src/client/sdk.gen.ts | 19 + .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 119 +- .../src/client/client/index.ts | 1 + .../src/client/client/types.gen.ts | 68 +- .../src/client/client/utils.gen.ts | 207 +-- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts | 42 +- .../src/client/core/utils.gen.ts | 143 ++ .../openapi-ts-openai/src/client/index.ts | 1 + .../openapi-ts-openai/src/client/sdk.gen.ts | 170 +- .../openapi-ts-openai/src/client/types.gen.ts | 716 +++++++-- .../openapi-ts-pinia-colada/.eslintrc.cjs | 3 +- .../src/client/@pinia/colada.gen.ts | 23 +- .../src/client/client/index.ts | 1 + .../src/client/sdk.gen.ts | 19 + .../src/client/client.gen.ts | 15 +- .../src/client/client/client.gen.ts | 268 ++++ .../src/client/client/index.ts | 18 +- .../src/client/client/types.gen.ts} | 96 +- .../src/client/client/utils.gen.ts} | 249 +-- .../src/client/core/{auth.ts => auth.gen.ts} | 2 + .../src/client/core/bodySerializer.gen.ts} | 22 +- .../src/client/core/params.gen.ts} | 12 + .../src/client/core/pathSerializer.gen.ts} | 2 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts} | 68 +- .../src/client/core/utils.gen.ts | 143 ++ .../openapi-ts-sample/src/client/index.ts | 3 +- .../openapi-ts-sample/src/client/sdk.gen.ts | 71 +- .../openapi-ts-sample/src/client/types.gen.ts | 16 +- .../openapi-ts-sample/src/client/zod.gen.ts | 18 +- .../angular-query-experimental.gen.ts | 278 +--- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 165 +- .../src/client/client/index.ts | 1 + .../src/client/client/types.gen.ts | 146 +- .../src/client/client/utils.gen.ts | 91 +- .../src/client/core/bodySerializer.gen.ts | 2 + .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts | 42 +- .../src/client/core/utils.gen.ts | 143 ++ .../src/client/index.ts | 3 +- .../src/client/sdk.gen.ts | 67 +- .../src/client/types.gen.ts | 8 +- .../src/client/@tanstack/react-query.gen.ts | 279 ++-- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 268 ++++ .../src/client/client/client.ts | 181 --- .../src/client/client/index.ts | 18 +- .../src/client/client/types.gen.ts} | 96 +- .../src/client/client/utils.gen.ts} | 249 +-- .../src/client/core/auth.gen.ts | 42 + .../src/client/core/auth.ts | 40 - .../src/client/core/bodySerializer.gen.ts | 92 ++ .../src/client/core/bodySerializer.ts | 84 - .../src/client/core/params.gen.ts | 153 ++ .../src/client/core/params.ts | 141 -- .../src/client/core/pathSerializer.gen.ts | 181 +++ .../src/client/core/pathSerializer.ts | 179 --- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts | 118 ++ .../src/client/core/types.ts | 98 -- .../src/client/core/utils.gen.ts | 143 ++ .../src/client/index.ts | 3 +- .../src/client/sdk.gen.ts | 71 +- .../src/client/types.gen.ts | 18 +- .../src/client/@tanstack/svelte-query.gen.ts | 279 ++-- .../src/client/client.gen.ts | 13 +- .../src/client/client/client.gen.ts | 268 ++++ .../src/client/client/client.ts | 181 --- .../src/client/client/index.ts | 18 +- .../src/client/client/types.gen.ts | 268 ++++ .../src/client/client/types.ts | 212 --- .../src/client/client/utils.gen.ts | 336 ++++ .../src/client/client/utils.ts | 415 ----- .../src/client/core/auth.gen.ts | 42 + .../src/client/core/auth.ts | 40 - .../src/client/core/bodySerializer.gen.ts | 92 ++ .../src/client/core/bodySerializer.ts | 84 - .../src/client/core/params.gen.ts | 153 ++ .../src/client/core/params.ts | 141 -- .../src/client/core/pathSerializer.gen.ts | 181 +++ .../src/client/core/pathSerializer.ts | 179 --- .../src/client/core/queryKeySerializer.gen.ts | 136 ++ .../src/client/core/serverSentEvents.gen.ts | 264 ++++ .../src/client/core/types.gen.ts | 118 ++ .../src/client/core/types.ts | 98 -- .../src/client/core/utils.gen.ts | 143 ++ .../src/client/index.ts | 3 +- .../src/client/sdk.gen.ts | 71 +- .../src/client/types.gen.ts | 18 +- .../.eslintrc.cjs | 3 +- .../src/client/@tanstack/vue-query.gen.ts | 271 +--- .../src/client/client.gen.ts | 17 +- .../src/client/client/client.gen.ts | 253 +++ .../src/client/client/client.ts | 173 --- .../src/client/client/index.ts | 18 +- .../client/client/{types.ts => types.gen.ts} | 74 +- .../client/client/{utils.ts => utils.gen.ts} | 227 +-- .../src/client/core/{auth.ts => auth.gen.ts} | 2 + ...odySerializer.ts => bodySerializer.gen.ts} | 20 +- .../client/core/{params.ts => params.gen.ts} | 12 + ...athSerializer.ts => pathSerializer.gen.ts} | 2 + .../src/client/core/queryKeySerializer.gen.ts | 117 ++ .../src/client/core/serverSentEvents.gen.ts | 241 +++ .../client/core/{types.ts => types.gen.ts} | 58 +- .../src/client/core/utils.gen.ts | 140 ++ .../src/client/index.ts | 3 +- .../src/client/sdk.gen.ts | 75 +- .../src/client/types.gen.ts | 18 +- package.json | 5 +- packages/codegen-core/CHANGELOG.md | 6 + packages/codegen-core/package.json | 2 +- .../codegen-core/src/__tests__/files.test.ts | 15 + .../src/__tests__/symbols.test.ts | 13 + packages/codegen-core/src/files/registry.ts | 8 + packages/codegen-core/src/files/types.d.ts | 7 + packages/codegen-core/src/symbols/registry.ts | 5 + packages/codegen-core/src/symbols/types.d.ts | 7 + .../src/vitest.base.config.ts | 3 +- packages/custom-client/src/plugin.ts | 4 +- packages/openapi-ts-tests/main/package.json | 5 +- .../axios/client/client.gen.ts | 25 +- .../react-query/axios/client/client.gen.ts | 25 +- .../solid-query/axios/client/client.gen.ts | 25 +- .../svelte-query/axios/client/client.gen.ts | 25 +- .../vue-query/axios/client/client.gen.ts | 25 +- .../plugins/valibot/default/valibot.gen.ts | 124 +- .../3.0.x/content-types/client/client.gen.ts | 25 +- .../client/client.gen.ts | 25 +- .../axios/client/client.gen.ts | 25 +- .../react-query/axios/client/client.gen.ts | 25 +- .../solid-query/axios/client/client.gen.ts | 25 +- .../svelte-query/axios/client/client.gen.ts | 25 +- .../vue-query/axios/client/client.gen.ts | 25 +- .../plugins/valibot/default/valibot.gen.ts | 272 ++-- .../3.0.x/validators/valibot.gen.ts | 14 +- .../base-url-false/client/client.gen.ts | 25 +- .../base-url-number/client/client.gen.ts | 25 +- .../base-url-strict/client/client.gen.ts | 25 +- .../base-url-string/client/client.gen.ts | 25 +- .../clean-false/client/client.gen.ts | 25 +- .../client-axios/default/client/client.gen.ts | 25 +- .../client/client.gen.ts | 25 +- .../sdk-client-optional/client/client.gen.ts | 25 +- .../sdk-client-required/client/client.gen.ts | 25 +- .../client/client.gen.ts | 25 +- .../my-client/base-url-false/client/plugin.ts | 4 +- .../base-url-number/client/plugin.ts | 4 +- .../base-url-strict/client/plugin.ts | 4 +- .../base-url-string/client/plugin.ts | 4 +- .../clients/my-client/bundle/client/plugin.ts | 4 +- .../my-client/default/client/plugin.ts | 4 +- .../sdk-client-optional/client/plugin.ts | 4 +- .../sdk-client-required/client/plugin.ts | 4 +- .../3.1.x/content-types/client/client.gen.ts | 25 +- .../client/client.gen.ts | 25 +- .../axios/client/client.gen.ts | 25 +- .../react-query/axios/client/client.gen.ts | 25 +- .../solid-query/axios/client/client.gen.ts | 25 +- .../svelte-query/axios/client/client.gen.ts | 25 +- .../vue-query/axios/client/client.gen.ts | 25 +- .../plugins/valibot/default/valibot.gen.ts | 284 ++-- .../3.1.x/sse-axios/client/client.gen.ts | 25 +- .../validators-circular-ref-2/valibot.gen.ts | 2 +- .../validators-circular-ref/valibot.gen.ts | 12 +- .../3.1.x/validators-metadata/valibot.gen.ts | 28 +- .../3.1.x/validators-types/valibot.gen.ts | 28 +- .../3.1.x/validators/valibot.gen.ts | 28 +- .../main/test/custom/client/plugin.ts | 4 +- .../main/test/openapi-ts.config.ts | 34 +- .../openapi-ts-tests/main/vitest.config.ts | 14 +- .../2.0.x/mini/default/zod.gen.ts | 126 +- .../__snapshots__/2.0.x/v3/default/zod.gen.ts | 124 +- .../__snapshots__/2.0.x/v4/default/zod.gen.ts | 126 +- .../3.0.x/mini/circular/zod.gen.ts | 56 +- .../3.0.x/mini/default/zod.gen.ts | 262 ++-- .../3.0.x/mini/validators/zod.gen.ts | 18 +- .../3.0.x/v3/circular/zod.gen.ts | 36 +- .../__snapshots__/3.0.x/v3/default/zod.gen.ts | 260 ++-- .../3.0.x/v3/validators/zod.gen.ts | 14 +- .../3.0.x/v4/circular/zod.gen.ts | 56 +- .../__snapshots__/3.0.x/v4/default/zod.gen.ts | 262 ++-- .../3.0.x/v4/validators/zod.gen.ts | 18 +- .../3.1.x/mini/default/zod.gen.ts | 274 ++-- .../mini/validators-circular-ref/zod.gen.ts | 12 +- .../3.1.x/mini/validators-dates/zod.gen.ts | 32 +- .../3.1.x/mini/validators-metadata/zod.gen.ts | 36 +- .../3.1.x/mini/validators-types/zod.gen.ts | 40 +- .../3.1.x/mini/validators/zod.gen.ts | 32 +- .../__snapshots__/3.1.x/v3/default/zod.gen.ts | 272 ++-- .../v3/validators-circular-ref/zod.gen.ts | 10 +- .../3.1.x/v3/validators-dates/zod.gen.ts | 28 +- .../3.1.x/v3/validators-metadata/zod.gen.ts | 28 +- .../3.1.x/v3/validators-types/zod.gen.ts | 36 +- .../3.1.x/v3/validators/zod.gen.ts | 28 +- .../__snapshots__/3.1.x/v4/default/zod.gen.ts | 274 ++-- .../v4/validators-circular-ref/zod.gen.ts | 12 +- .../3.1.x/v4/validators-dates/zod.gen.ts | 32 +- .../3.1.x/v4/validators-metadata/zod.gen.ts | 36 +- .../3.1.x/v4/validators-types/zod.gen.ts | 40 +- .../3.1.x/v4/validators/zod.gen.ts | 32 +- .../openapi-ts-tests/zod/v3/vitest.config.ts | 14 +- .../2.0.x/mini/default/zod.gen.ts | 126 +- .../__snapshots__/2.0.x/v3/default/zod.gen.ts | 124 +- .../__snapshots__/2.0.x/v4/default/zod.gen.ts | 126 +- .../3.0.x/mini/circular/zod.gen.ts | 56 +- .../3.0.x/mini/default/zod.gen.ts | 262 ++-- .../3.0.x/mini/validators/zod.gen.ts | 18 +- .../3.0.x/v3/circular/zod.gen.ts | 36 +- .../__snapshots__/3.0.x/v3/default/zod.gen.ts | 260 ++-- .../3.0.x/v3/validators/zod.gen.ts | 14 +- .../3.0.x/v4/circular/zod.gen.ts | 56 +- .../__snapshots__/3.0.x/v4/default/zod.gen.ts | 262 ++-- .../3.0.x/v4/validators/zod.gen.ts | 18 +- .../3.1.x/mini/default/zod.gen.ts | 274 ++-- .../mini/validators-circular-ref/zod.gen.ts | 12 +- .../3.1.x/mini/validators-dates/zod.gen.ts | 32 +- .../3.1.x/mini/validators-metadata/zod.gen.ts | 36 +- .../3.1.x/mini/validators-types/zod.gen.ts | 40 +- .../3.1.x/mini/validators/zod.gen.ts | 32 +- .../__snapshots__/3.1.x/v3/default/zod.gen.ts | 272 ++-- .../v3/validators-circular-ref/zod.gen.ts | 10 +- .../3.1.x/v3/validators-dates/zod.gen.ts | 28 +- .../3.1.x/v3/validators-metadata/zod.gen.ts | 28 +- .../3.1.x/v3/validators-types/zod.gen.ts | 36 +- .../3.1.x/v3/validators/zod.gen.ts | 28 +- .../__snapshots__/3.1.x/v4/default/zod.gen.ts | 274 ++-- .../v4/validators-circular-ref/zod.gen.ts | 12 +- .../3.1.x/v4/validators-dates/zod.gen.ts | 32 +- .../3.1.x/v4/validators-metadata/zod.gen.ts | 36 +- .../3.1.x/v4/validators-types/zod.gen.ts | 40 +- .../3.1.x/v4/validators/zod.gen.ts | 32 +- .../openapi-ts-tests/zod/v4/vitest.config.ts | 14 +- packages/openapi-ts/CHANGELOG.md | 18 + packages/openapi-ts/package.json | 6 +- .../src/__tests__/createClient.test.ts | 2 +- .../openapi-ts/src/__tests__/error.test.ts | 2 +- .../openapi-ts/src/__tests__/index.test.ts | 2 +- .../src/__tests__/interactive.test.ts | 6 +- .../src/config/__tests__/input.test.ts | 3 +- packages/openapi-ts/src/config/engine.ts | 2 +- packages/openapi-ts/src/config/init.ts | 11 +- packages/openapi-ts/src/config/input.ts | 8 +- packages/openapi-ts/src/config/logs.ts | 2 +- packages/openapi-ts/src/config/merge.ts | 2 +- packages/openapi-ts/src/config/output.ts | 5 +- packages/openapi-ts/src/config/parser.ts | 3 +- packages/openapi-ts/src/config/plugins.ts | 7 +- packages/openapi-ts/src/createClient.ts | 37 +- packages/openapi-ts/src/debug/graph.ts | 2 +- packages/openapi-ts/src/debug/ir.ts | 4 +- packages/openapi-ts/src/error.ts | 4 +- .../src/generate/__tests__/class.test.ts | 11 +- .../src/generate/__tests__/core.test.ts | 23 +- .../src/generate/__tests__/mocks.ts | 8 +- .../src/generate/__tests__/renderer.test.ts | 75 + packages/openapi-ts/src/generate/class.ts | 15 +- packages/openapi-ts/src/generate/client.ts | 9 +- packages/openapi-ts/src/generate/core.ts | 10 +- packages/openapi-ts/src/generate/file.ts | 5 +- .../generate/legacy/__tests__/index.test.ts | 9 +- .../generate/legacy/__tests__/output.test.ts | 13 +- .../src/generate/legacy/indexFile.ts | 9 +- .../openapi-ts/src/generate/legacy/output.ts | 13 +- packages/openapi-ts/src/generate/output.ts | 5 +- packages/openapi-ts/src/generate/renderer.ts | 18 +- packages/openapi-ts/src/generate/tsConfig.ts | 2 +- packages/openapi-ts/src/getSpec.ts | 6 +- packages/openapi-ts/src/index.ts | 26 +- .../openapi-ts/src/ir/__tests__/graph.test.ts | 327 ++++ .../src/ir/__tests__/pagination.test.ts | 5 +- packages/openapi-ts/src/ir/context.ts | 26 +- packages/openapi-ts/src/ir/graph.ts | 261 ++++ packages/openapi-ts/src/ir/pagination.ts | 3 +- packages/openapi-ts/src/ir/types.d.ts | 13 +- .../2.0.x/parser/__tests__/server.test.ts | 3 +- .../2.0.x/parser/__tests__/validate.test.ts | 5 +- .../src/openApi/2.0.x/parser/filter.ts | 9 +- .../src/openApi/2.0.x/parser/index.ts | 17 +- .../src/openApi/2.0.x/parser/mediaType.ts | 8 +- .../src/openApi/2.0.x/parser/operation.ts | 7 +- .../src/openApi/2.0.x/parser/pagination.ts | 7 +- .../src/openApi/2.0.x/parser/parameter.ts | 4 +- .../src/openApi/2.0.x/parser/schema.ts | 27 +- .../src/openApi/2.0.x/parser/server.ts | 4 +- .../src/openApi/2.0.x/parser/validate.ts | 9 +- .../2.0.x/types/json-schema-draft-4.d.ts | 2 +- .../src/openApi/2.0.x/types/spec.d.ts | 3 +- .../3.0.x/parser/__tests__/validate.test.ts | 5 +- .../src/openApi/3.0.x/parser/filter.ts | 9 +- .../src/openApi/3.0.x/parser/index.ts | 17 +- .../src/openApi/3.0.x/parser/mediaType.ts | 8 +- .../src/openApi/3.0.x/parser/operation.ts | 7 +- .../src/openApi/3.0.x/parser/pagination.ts | 7 +- .../src/openApi/3.0.x/parser/parameter.ts | 6 +- .../src/openApi/3.0.x/parser/requestBody.ts | 6 +- .../src/openApi/3.0.x/parser/schema.ts | 27 +- .../src/openApi/3.0.x/parser/server.ts | 4 +- .../src/openApi/3.0.x/parser/validate.ts | 9 +- .../src/openApi/3.0.x/types/spec.d.ts | 2 +- .../3.1.x/parser/__tests__/validate.test.ts | 5 +- .../src/openApi/3.1.x/parser/filter.ts | 9 +- .../src/openApi/3.1.x/parser/index.ts | 17 +- .../src/openApi/3.1.x/parser/mediaType.ts | 8 +- .../src/openApi/3.1.x/parser/operation.ts | 9 +- .../src/openApi/3.1.x/parser/pagination.ts | 7 +- .../src/openApi/3.1.x/parser/parameter.ts | 6 +- .../src/openApi/3.1.x/parser/requestBody.ts | 6 +- .../src/openApi/3.1.x/parser/schema.ts | 27 +- .../src/openApi/3.1.x/parser/server.ts | 4 +- .../src/openApi/3.1.x/parser/validate.ts | 9 +- .../src/openApi/3.1.x/parser/webhook.ts | 5 +- .../types/json-schema-draft-2020-12.d.ts | 3 +- .../src/openApi/__tests__/index.test.ts | 13 +- .../src/openApi/common/interfaces/OpenApi.ts | 4 +- .../src/openApi/common/interfaces/client.ts | 3 +- .../common/parser/__tests__/type.test.ts | 2 +- .../src/openApi/common/parser/getDefault.ts | 7 +- .../src/openApi/common/parser/getEnums.ts | 3 +- .../src/openApi/common/parser/getRef.ts | 5 +- .../src/openApi/common/parser/operation.ts | 7 +- .../src/openApi/common/parser/sanitize.ts | 2 +- .../src/openApi/common/parser/type.ts | 7 +- packages/openapi-ts/src/openApi/index.ts | 26 +- .../src/openApi/shared/graph/meta.ts | 15 +- .../src/openApi/shared/transforms/enums.ts | 3 +- .../src/openApi/shared/transforms/index.ts | 3 +- .../openApi/shared/transforms/readWrite.ts | 5 +- .../src/openApi/shared/types/schema.d.ts | 13 - .../shared/utils/__tests__/graph.test.ts | 104 ++ .../shared/utils/__tests__/patch.test.ts | 3 +- .../src/openApi/shared/utils/discriminator.ts | 2 +- .../src/openApi/shared/utils/filter.ts | 9 +- .../src/openApi/shared/utils/graph.ts | 192 ++- .../src/openApi/shared/utils/identifier.ts | 2 +- .../src/openApi/shared/utils/name.ts | 3 +- .../src/openApi/shared/utils/operation.ts | 9 +- .../src/openApi/shared/utils/parameter.ts | 2 +- .../src/openApi/shared/utils/patch.ts | 3 +- .../src/openApi/shared/utils/validator.ts | 2 +- packages/openapi-ts/src/openApi/types.d.ts | 6 +- packages/openapi-ts/src/openApi/v2/index.ts | 5 +- .../src/openApi/v2/interfaces/Model.ts | 3 +- .../src/openApi/v2/interfaces/OpenApi.ts | 3 +- .../openApi/v2/interfaces/OpenApiHeader.ts | 3 +- .../src/openApi/v2/interfaces/OpenApiItems.ts | 2 +- .../openApi/v2/interfaces/OpenApiParameter.ts | 3 +- .../openApi/v2/interfaces/OpenApiResponse.ts | 3 +- .../openApi/v2/interfaces/OpenApiSchema.ts | 5 +- .../v2/interfaces/OpenApiSecurityScheme.ts | 2 +- .../src/openApi/v2/parser/getModel.ts | 9 +- .../openApi/v2/parser/getModelComposition.ts | 6 +- .../openApi/v2/parser/getModelProperties.ts | 9 +- .../src/openApi/v2/parser/getModels.ts | 5 +- .../v2/parser/getOperationParameter.ts | 15 +- .../v2/parser/getOperationParameters.ts | 7 +- .../openApi/v2/parser/getOperationResponse.ts | 9 +- .../v2/parser/getOperationResponses.ts | 9 +- .../src/openApi/v2/parser/getOperations.ts | 11 +- .../getRequiredPropertiesFromComposition.ts | 5 +- .../src/openApi/v2/parser/operation.ts | 11 +- packages/openapi-ts/src/openApi/v3/index.ts | 5 +- .../src/openApi/v3/interfaces/Model.ts | 3 +- .../v3/interfaces/OpenApiComponents.ts | 3 +- .../v3/interfaces/OpenApiDiscriminator.ts | 2 +- .../openApi/v3/interfaces/OpenApiEncoding.ts | 3 +- .../openApi/v3/interfaces/OpenApiHeader.ts | 3 +- .../src/openApi/v3/interfaces/OpenApiLink.ts | 3 +- .../openApi/v3/interfaces/OpenApiMediaType.ts | 3 +- .../openApi/v3/interfaces/OpenApiOAuthFlow.ts | 2 +- .../openApi/v3/interfaces/OpenApiOperation.ts | 3 +- .../openApi/v3/interfaces/OpenApiParameter.ts | 3 +- .../v3/interfaces/OpenApiRequestBody.ts | 3 +- .../openApi/v3/interfaces/OpenApiResponse.ts | 3 +- .../openApi/v3/interfaces/OpenApiSchema.ts | 5 +- .../openApi/v3/interfaces/OpenApiServer.ts | 3 +- .../v3/interfaces/OpenApiServerVariable.ts | 2 +- .../v3/parser/__tests__/getModel.test.ts | 5 +- .../src/openApi/v3/parser/discriminator.ts | 7 +- .../src/openApi/v3/parser/getContent.ts | 3 +- .../src/openApi/v3/parser/getModel.ts | 13 +- .../openApi/v3/parser/getModelComposition.ts | 6 +- .../openApi/v3/parser/getModelProperties.ts | 13 +- .../src/openApi/v3/parser/getModels.ts | 3 +- .../v3/parser/getOperationParameter.ts | 17 +- .../v3/parser/getOperationParameters.ts | 7 +- .../v3/parser/getOperationRequestBody.ts | 7 +- .../openApi/v3/parser/getOperationResponse.ts | 9 +- .../v3/parser/getOperationResponses.ts | 9 +- .../src/openApi/v3/parser/getOperations.ts | 11 +- .../getRequiredPropertiesFromComposition.ts | 7 +- .../src/openApi/v3/parser/operation.ts | 13 +- .../src/plugins/@angular/common/api.ts | 6 +- .../src/plugins/@angular/common/config.ts | 3 +- .../plugins/@angular/common/httpRequests.ts | 191 +-- .../plugins/@angular/common/httpResources.ts | 187 +-- .../src/plugins/@angular/common/plugin.ts | 8 +- .../src/plugins/@angular/common/types.d.ts | 5 +- .../plugins/@hey-api/client-angular/api.ts | 6 +- .../plugins/@hey-api/client-angular/config.ts | 10 +- .../@hey-api/client-angular/types.d.ts | 5 +- .../client-axios/__tests__/client.test.ts | 203 --- .../src/plugins/@hey-api/client-axios/api.ts | 6 +- .../@hey-api/client-axios/bundle/client.ts | 25 +- .../plugins/@hey-api/client-axios/config.ts | 10 +- .../plugins/@hey-api/client-axios/types.d.ts | 5 +- .../plugins/@hey-api/client-core/client.ts | 11 +- .../client-core/createClientConfig.ts | 7 +- .../plugins/@hey-api/client-core/types.d.ts | 14 +- .../src/plugins/@hey-api/client-core/utils.ts | 4 +- .../src/plugins/@hey-api/client-fetch/api.ts | 6 +- .../plugins/@hey-api/client-fetch/config.ts | 10 +- .../plugins/@hey-api/client-fetch/types.d.ts | 5 +- .../src/plugins/@hey-api/client-next/api.ts | 6 +- .../plugins/@hey-api/client-next/config.ts | 10 +- .../plugins/@hey-api/client-next/types.d.ts | 5 +- .../src/plugins/@hey-api/client-nuxt/api.ts | 6 +- .../plugins/@hey-api/client-nuxt/config.ts | 10 +- .../plugins/@hey-api/client-nuxt/types.d.ts | 5 +- .../src/plugins/@hey-api/client-ofetch/api.ts | 6 +- .../plugins/@hey-api/client-ofetch/config.ts | 10 +- .../plugins/@hey-api/client-ofetch/types.d.ts | 5 +- .../plugins/@hey-api/legacy-angular/config.ts | 3 +- .../@hey-api/legacy-angular/types.d.ts | 4 +- .../plugins/@hey-api/legacy-axios/config.ts | 3 +- .../plugins/@hey-api/legacy-axios/types.d.ts | 4 +- .../plugins/@hey-api/legacy-fetch/config.ts | 3 +- .../plugins/@hey-api/legacy-fetch/types.d.ts | 4 +- .../plugins/@hey-api/legacy-node/config.ts | 3 +- .../plugins/@hey-api/legacy-node/types.d.ts | 4 +- .../src/plugins/@hey-api/legacy-xhr/config.ts | 3 +- .../plugins/@hey-api/legacy-xhr/types.d.ts | 4 +- .../schemas/__tests__/schemas.test.ts | 23 +- .../src/plugins/@hey-api/schemas/api.ts | 6 +- .../src/plugins/@hey-api/schemas/config.ts | 3 +- .../plugins/@hey-api/schemas/plugin-legacy.ts | 11 +- .../src/plugins/@hey-api/schemas/plugin.ts | 23 +- .../src/plugins/@hey-api/schemas/types.d.ts | 11 +- .../@hey-api/sdk/__tests__/plugin.test.ts | 39 +- .../src/plugins/@hey-api/sdk/api.ts | 7 +- .../src/plugins/@hey-api/sdk/auth.ts | 3 +- .../src/plugins/@hey-api/sdk/comment.ts | 6 +- .../src/plugins/@hey-api/sdk/config.ts | 3 +- .../src/plugins/@hey-api/sdk/operation.ts | 45 +- .../src/plugins/@hey-api/sdk/plugin-legacy.ts | 35 +- .../src/plugins/@hey-api/sdk/plugin.ts | 439 +++--- .../src/plugins/@hey-api/sdk/signature.ts | 2 +- .../src/plugins/@hey-api/sdk/typeOptions.ts | 13 +- .../src/plugins/@hey-api/sdk/types.d.ts | 9 +- .../src/plugins/@hey-api/sdk/validator.ts | 3 +- .../src/plugins/@hey-api/transformers/api.ts | 6 +- .../plugins/@hey-api/transformers/config.ts | 3 +- .../@hey-api/transformers/expressions.ts | 5 +- .../@hey-api/transformers/plugin-legacy.ts | 18 +- .../plugins/@hey-api/transformers/plugin.ts | 138 +- .../plugins/@hey-api/transformers/types.d.ts | 5 +- .../typescript/__tests__/plugin.test.ts | 17 +- .../src/plugins/@hey-api/typescript/api.ts | 9 +- .../@hey-api/typescript/clientOptions.ts | 12 +- .../src/plugins/@hey-api/typescript/config.ts | 3 +- .../src/plugins/@hey-api/typescript/export.ts | 11 +- .../plugins/@hey-api/typescript/operation.ts | 21 +- .../@hey-api/typescript/plugin-legacy.ts | 34 +- .../src/plugins/@hey-api/typescript/plugin.ts | 36 +- .../plugins/@hey-api/typescript/types.d.ts | 5 +- .../plugins/@hey-api/typescript/webhook.ts | 15 +- .../plugins/@hey-api/typescript/webhooks.ts | 3 +- .../src/plugins/@pinia/colada/api.ts | 6 +- .../src/plugins/@pinia/colada/config.ts | 3 +- .../src/plugins/@pinia/colada/meta.ts | 5 +- .../plugins/@pinia/colada/mutationOptions.ts | 9 +- .../src/plugins/@pinia/colada/plugin.ts | 97 +- .../src/plugins/@pinia/colada/queryKey.ts | 33 +- .../src/plugins/@pinia/colada/queryOptions.ts | 17 +- .../src/plugins/@pinia/colada/types.d.ts | 7 +- .../src/plugins/@pinia/colada/useType.ts | 13 +- .../src/plugins/@pinia/colada/utils.ts | 3 +- .../angular-query-experimental/api.ts | 6 +- .../angular-query-experimental/config.ts | 7 +- .../angular-query-experimental/types.d.ts | 7 +- .../query-core/infiniteQueryOptions.ts | 27 +- .../src/plugins/@tanstack/query-core/meta.ts | 5 +- .../@tanstack/query-core/mutationOptions.ts | 9 +- .../@tanstack/query-core/plugin-legacy.ts | 54 +- .../plugins/@tanstack/query-core/plugin.ts | 115 +- .../plugins/@tanstack/query-core/queryKey.ts | 27 +- .../@tanstack/query-core/queryOptions.ts | 15 +- .../plugins/@tanstack/query-core/types.d.ts | 10 +- .../plugins/@tanstack/query-core/useQuery.ts | 13 +- .../plugins/@tanstack/query-core/useType.ts | 17 +- .../src/plugins/@tanstack/react-query/api.ts | 6 +- .../plugins/@tanstack/react-query/config.ts | 7 +- .../plugins/@tanstack/react-query/types.d.ts | 7 +- .../src/plugins/@tanstack/solid-query/api.ts | 6 +- .../plugins/@tanstack/solid-query/config.ts | 7 +- .../plugins/@tanstack/solid-query/types.d.ts | 7 +- .../src/plugins/@tanstack/svelte-query/api.ts | 6 +- .../plugins/@tanstack/svelte-query/config.ts | 7 +- .../plugins/@tanstack/svelte-query/types.d.ts | 7 +- .../src/plugins/@tanstack/vue-query/api.ts | 6 +- .../src/plugins/@tanstack/vue-query/config.ts | 7 +- .../plugins/@tanstack/vue-query/types.d.ts | 7 +- .../openapi-ts/src/plugins/arktype/api.ts | 56 + .../openapi-ts/src/plugins/arktype/config.ts | 272 ++++ .../src/plugins/arktype/constants.ts | 115 ++ .../openapi-ts/src/plugins/arktype/index.ts | 2 + .../openapi-ts/src/plugins/arktype/plugin.ts | 4 + .../src/plugins/arktype/shared/export.ts | 63 + .../src/plugins/arktype/shared/types.d.ts | 31 + .../openapi-ts/src/plugins/arktype/types.d.ts | 666 ++++++++ .../openapi-ts/src/plugins/arktype/v2/api.ts | 88 ++ .../src/plugins/arktype/v2/plugin.ts | 364 +++++ .../src/plugins/arktype/v2/toAst/index.ts | 124 ++ .../src/plugins/arktype/v2/toAst/null.ts | 15 + .../src/plugins/arktype/v2/toAst/object.ts | 172 +++ .../src/plugins/arktype/v2/toAst/string.ts | 66 + packages/openapi-ts/src/plugins/config.ts | 106 +- .../openapi-ts/src/plugins/fastify/api.ts | 6 +- .../openapi-ts/src/plugins/fastify/config.ts | 3 +- .../openapi-ts/src/plugins/fastify/plugin.ts | 37 +- .../openapi-ts/src/plugins/fastify/types.d.ts | 3 +- .../src/plugins/shared/types/instance.d.ts | 37 +- .../src/plugins/shared/types/refs.d.ts | 59 + .../src/plugins/shared/types/schema.d.ts | 8 + .../shared/utils/__tests__/refs.test.ts | 85 + .../src/plugins/shared/utils/case.ts | 6 +- .../src/plugins/shared/utils/config.ts | 2 +- .../src/plugins/shared/utils/instance.ts | 310 ++-- .../src/plugins/shared/utils/operation.ts | 6 +- .../src/plugins/shared/utils/refs.ts | 66 + .../src/plugins/shared/utils/schema.ts | 6 +- packages/openapi-ts/src/plugins/types.d.ts | 16 +- .../openapi-ts/src/plugins/valibot/api.ts | 107 +- .../openapi-ts/src/plugins/valibot/config.ts | 3 +- .../openapi-ts/src/plugins/valibot/plugin.ts | 1375 +---------------- .../{number-helpers.ts => shared/numbers.ts} | 3 +- .../src/plugins/valibot/shared/types.d.ts | 25 + .../openapi-ts/src/plugins/valibot/types.d.ts | 5 +- .../openapi-ts/src/plugins/valibot/v1/api.ts | 86 ++ .../src/plugins/valibot/{ => v1}/constants.ts | 2 +- .../src/plugins/valibot/{ => v1}/operation.ts | 43 +- .../src/plugins/valibot/v1/pipesToAst.ts | 30 + .../src/plugins/valibot/v1/plugin.ts | 315 ++++ .../src/plugins/valibot/v1/toAst/array.ts | 128 ++ .../src/plugins/valibot/v1/toAst/boolean.ts | 35 + .../src/plugins/valibot/v1/toAst/enum.ts | 72 + .../src/plugins/valibot/v1/toAst/index.ts | 122 ++ .../src/plugins/valibot/v1/toAst/never.ts | 22 + .../src/plugins/valibot/v1/toAst/null.ts | 22 + .../src/plugins/valibot/v1/toAst/number.ts | 255 +++ .../src/plugins/valibot/v1/toAst/object.ts | 120 ++ .../src/plugins/valibot/v1/toAst/string.ts | 144 ++ .../src/plugins/valibot/v1/toAst/tuple.ts | 79 + .../src/plugins/valibot/v1/toAst/undefined.ts | 23 + .../src/plugins/valibot/v1/toAst/unknown.ts | 23 + .../src/plugins/valibot/v1/toAst/void.ts | 23 + .../src/plugins/valibot/{ => v1}/webhook.ts | 28 +- packages/openapi-ts/src/plugins/zod/api.ts | 113 +- packages/openapi-ts/src/plugins/zod/config.ts | 3 +- .../openapi-ts/src/plugins/zod/constants.ts | 3 +- .../openapi-ts/src/plugins/zod/mini/api.ts | 72 + .../openapi-ts/src/plugins/zod/mini/plugin.ts | 1301 ++-------------- .../src/plugins/zod/mini/toAst/array.ts | 174 +++ .../src/plugins/zod/mini/toAst/boolean.ts | 35 + .../src/plugins/zod/mini/toAst/enum.ts | 128 ++ .../src/plugins/zod/mini/toAst/index.ts | 86 ++ .../src/plugins/zod/mini/toAst/never.ts | 21 + .../src/plugins/zod/mini/toAst/null.ts | 21 + .../src/plugins/zod/mini/toAst/number.ts | 119 ++ .../src/plugins/zod/mini/toAst/object.ts | 130 ++ .../src/plugins/zod/mini/toAst/string.ts | 193 +++ .../src/plugins/zod/mini/toAst/tuple.ts | 78 + .../src/plugins/zod/mini/toAst/undefined.ts | 21 + .../src/plugins/zod/mini/toAst/unknown.ts | 21 + .../src/plugins/zod/mini/toAst/void.ts | 21 + packages/openapi-ts/src/plugins/zod/plugin.ts | 3 +- .../src/plugins/zod/{ => shared}/export.ts | 35 +- .../src/plugins/zod/shared/numbers.ts | 28 + .../src/plugins/zod/shared/operation.ts | 63 +- .../src/plugins/zod/shared/types.d.ts | 37 +- .../src/plugins/zod/shared/webhook.ts | 43 +- .../openapi-ts/src/plugins/zod/types.d.ts | 25 +- packages/openapi-ts/src/plugins/zod/v3/api.ts | 72 + .../openapi-ts/src/plugins/zod/v3/plugin.ts | 1198 ++------------ .../src/plugins/zod/v3/toAst/array.ts | 162 ++ .../src/plugins/zod/v3/toAst/boolean.ts | 33 + .../src/plugins/zod/v3/toAst/enum.ts | 126 ++ .../src/plugins/zod/v3/toAst/index.ts | 106 ++ .../src/plugins/zod/v3/toAst/never.ts | 20 + .../src/plugins/zod/v3/toAst/null.ts | 20 + .../src/plugins/zod/v3/toAst/number.ts | 95 ++ .../src/plugins/zod/v3/toAst/object.ts | 113 ++ .../src/plugins/zod/v3/toAst/string.ts | 153 ++ .../src/plugins/zod/v3/toAst/tuple.ts | 85 + .../src/plugins/zod/v3/toAst/undefined.ts | 20 + .../src/plugins/zod/v3/toAst/unknown.ts | 20 + .../src/plugins/zod/v3/toAst/void.ts | 20 + packages/openapi-ts/src/plugins/zod/v4/api.ts | 72 + .../openapi-ts/src/plugins/zod/v4/plugin.ts | 1258 ++------------- .../src/plugins/zod/v4/toAst/array.ts | 156 ++ .../src/plugins/zod/v4/toAst/boolean.ts | 35 + .../src/plugins/zod/v4/toAst/enum.ts | 128 ++ .../src/plugins/zod/v4/toAst/index.ts | 86 ++ .../src/plugins/zod/v4/toAst/never.ts | 21 + .../src/plugins/zod/v4/toAst/null.ts | 21 + .../src/plugins/zod/v4/toAst/number.ts | 97 ++ .../src/plugins/zod/v4/toAst/object.ts | 147 ++ .../src/plugins/zod/v4/toAst/string.ts | 171 ++ .../src/plugins/zod/v4/toAst/tuple.ts | 78 + .../src/plugins/zod/v4/toAst/undefined.ts | 21 + .../src/plugins/zod/v4/toAst/unknown.ts | 21 + .../src/plugins/zod/v4/toAst/void.ts | 21 + packages/openapi-ts/src/processOutput.ts | 4 +- packages/openapi-ts/src/tsc/transform.ts | 3 +- packages/openapi-ts/src/tsc/typedef.ts | 3 +- packages/openapi-ts/src/tsc/types.ts | 5 +- packages/openapi-ts/src/tsc/utils.ts | 5 +- packages/openapi-ts/src/types/client.d.ts | 2 +- packages/openapi-ts/src/types/config.d.ts | 5 +- packages/openapi-ts/src/types/parser.d.ts | 5 +- packages/openapi-ts/src/types/utils.d.ts | 2 +- .../src/utils/__tests__/handlebars.test.ts | 12 +- .../src/utils/__tests__/minHeap.test.ts | 59 + .../src/utils/__tests__/parse.test.ts | 17 +- .../src/utils/__tests__/postprocess.test.ts | 5 +- .../src/utils/__tests__/sort.test.ts | 3 +- .../src/utils/__tests__/stringCase.test.ts | 3 +- .../src/utils/__tests__/type.test.ts | 3 +- packages/openapi-ts/src/utils/cli.ts | 2 +- packages/openapi-ts/src/utils/config.ts | 4 +- packages/openapi-ts/src/utils/enum.ts | 5 +- packages/openapi-ts/src/utils/escape.ts | 2 +- .../src/utils/getHttpRequestName.ts | 2 +- packages/openapi-ts/src/utils/handlebars.ts | 89 +- packages/openapi-ts/src/utils/input/heyApi.ts | 2 +- packages/openapi-ts/src/utils/input/index.ts | 3 +- packages/openapi-ts/src/utils/input/readme.ts | 2 +- packages/openapi-ts/src/utils/input/scalar.ts | 2 +- packages/openapi-ts/src/utils/meta.ts | 3 +- packages/openapi-ts/src/utils/minHeap.ts | 64 + packages/openapi-ts/src/utils/postprocess.ts | 9 +- packages/openapi-ts/src/utils/stringCase.ts | 2 +- packages/openapi-ts/src/utils/transform.ts | 5 +- packages/openapi-ts/src/utils/type.ts | 9 +- packages/openapi-ts/tsconfig.base.json | 6 +- packages/openapi-ts/vitest.config.ts | 21 +- pnpm-lock.yaml | 643 +++----- scripts/example.sh | 3 - scripts/examples-check.sh | 37 + scripts/examples-generate.sh | 90 ++ 748 files changed, 28333 insertions(+), 17288 deletions(-) create mode 100644 eslint-rules/local-paths.js create mode 100644 examples/README.md rename examples/openapi-ts-angular-common/src/client/@angular/{common/http/resources.gen.ts => common.gen.ts} (53%) delete mode 100644 examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts create mode 100644 examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-axios/src/client/core/utils.gen.ts rename examples/{openapi-ts-sample/src/client/client/client.ts => openapi-ts-fastify/src/client/client/client.gen.ts} (56%) delete mode 100644 examples/openapi-ts-fastify/src/client/client/client.ts rename examples/{openapi-ts-fetch/src/client/client/types.ts => openapi-ts-fastify/src/client/client/types.gen.ts} (70%) rename examples/{openapi-ts-sample/src/client/client/utils.ts => openapi-ts-fastify/src/client/client/utils.gen.ts} (59%) rename examples/openapi-ts-fastify/src/client/core/{auth.ts => auth.gen.ts} (93%) rename examples/{openapi-ts-sample/src/client/core/bodySerializer.ts => openapi-ts-fastify/src/client/core/bodySerializer.gen.ts} (92%) rename examples/{openapi-ts-sample/src/client/core/params.ts => openapi-ts-fastify/src/client/core/params.gen.ts} (98%) rename examples/{openapi-ts-next/src/client/core/pathSerializer.ts => openapi-ts-fastify/src/client/core/pathSerializer.gen.ts} (98%) create mode 100644 examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts rename examples/{openapi-ts-sample/src/client/core/types.ts => openapi-ts-fastify/src/client/core/types.gen.ts} (85%) create mode 100644 examples/openapi-ts-fastify/src/client/core/utils.gen.ts rename examples/openapi-ts-fetch/src/client/client/{client.ts => client.gen.ts} (54%) rename examples/{openapi-ts-sample/src/client/client/types.ts => openapi-ts-fetch/src/client/client/types.gen.ts} (70%) rename examples/openapi-ts-fetch/src/client/client/{utils.ts => utils.gen.ts} (59%) rename examples/openapi-ts-fetch/src/client/core/{auth.ts => auth.gen.ts} (93%) rename examples/openapi-ts-fetch/src/client/core/{bodySerializer.ts => bodySerializer.gen.ts} (84%) rename examples/openapi-ts-fetch/src/client/core/{params.ts => params.gen.ts} (89%) rename examples/{openapi-ts-sample/src/client/core/pathSerializer.ts => openapi-ts-fetch/src/client/core/pathSerializer.gen.ts} (98%) create mode 100644 examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts rename examples/openapi-ts-fetch/src/client/core/{types.ts => types.gen.ts} (75%) create mode 100644 examples/openapi-ts-fetch/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-next/src/client/client/client.gen.ts delete mode 100644 examples/openapi-ts-next/src/client/client/client.ts rename examples/openapi-ts-next/src/client/client/{types.ts => types.gen.ts} (64%) rename examples/openapi-ts-next/src/client/client/{utils.ts => utils.gen.ts} (81%) rename examples/openapi-ts-next/src/client/core/{auth.ts => auth.gen.ts} (93%) rename examples/{openapi-ts-fastify/src/client/core/bodySerializer.ts => openapi-ts-next/src/client/core/bodySerializer.gen.ts} (82%) rename examples/{openapi-ts-fastify/src/client/core/params.ts => openapi-ts-next/src/client/core/params.gen.ts} (89%) rename examples/{openapi-ts-fetch/src/client/core/pathSerializer.ts => openapi-ts-next/src/client/core/pathSerializer.gen.ts} (98%) create mode 100644 examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts rename examples/{openapi-ts-fastify/src/client/core/types.ts => openapi-ts-next/src/client/core/types.gen.ts} (68%) create mode 100644 examples/openapi-ts-next/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-openai/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-sample/src/client/client/client.gen.ts rename examples/{openapi-ts-tanstack-react-query/src/client/client/types.ts => openapi-ts-sample/src/client/client/types.gen.ts} (69%) rename examples/{openapi-ts-tanstack-react-query/src/client/client/utils.ts => openapi-ts-sample/src/client/client/utils.gen.ts} (59%) rename examples/openapi-ts-sample/src/client/core/{auth.ts => auth.gen.ts} (93%) rename examples/{openapi-ts-next/src/client/core/bodySerializer.ts => openapi-ts-sample/src/client/core/bodySerializer.gen.ts} (82%) rename examples/{openapi-ts-next/src/client/core/params.ts => openapi-ts-sample/src/client/core/params.gen.ts} (89%) rename examples/{openapi-ts-fastify/src/client/core/pathSerializer.ts => openapi-ts-sample/src/client/core/pathSerializer.gen.ts} (98%) create mode 100644 examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts rename examples/{openapi-ts-next/src/client/core/types.ts => openapi-ts-sample/src/client/core/types.gen.ts} (68%) create mode 100644 examples/openapi-ts-sample/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/client/client.ts rename examples/{openapi-ts-fastify/src/client/client/types.ts => openapi-ts-tanstack-react-query/src/client/client/types.gen.ts} (69%) rename examples/{openapi-ts-fastify/src/client/client/utils.ts => openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts} (59%) create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/params.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts delete mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/types.ts create mode 100644 examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts delete mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts create mode 100644 examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts create mode 100644 examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts delete mode 100644 examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts rename examples/openapi-ts-tanstack-vue-query/src/client/client/{types.ts => types.gen.ts} (68%) rename examples/openapi-ts-tanstack-vue-query/src/client/client/{utils.ts => utils.gen.ts} (60%) rename examples/openapi-ts-tanstack-vue-query/src/client/core/{auth.ts => auth.gen.ts} (93%) rename examples/openapi-ts-tanstack-vue-query/src/client/core/{bodySerializer.ts => bodySerializer.gen.ts} (79%) rename examples/openapi-ts-tanstack-vue-query/src/client/core/{params.ts => params.gen.ts} (89%) rename examples/openapi-ts-tanstack-vue-query/src/client/core/{pathSerializer.ts => pathSerializer.gen.ts} (98%) create mode 100644 examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts create mode 100644 examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts rename examples/openapi-ts-tanstack-vue-query/src/client/core/{types.ts => types.gen.ts} (65%) create mode 100644 examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts create mode 100644 packages/openapi-ts/src/ir/__tests__/graph.test.ts create mode 100644 packages/openapi-ts/src/ir/graph.ts create mode 100644 packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/api.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/config.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/constants.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/index.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/plugin.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/shared/export.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/shared/types.d.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/types.d.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/api.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/plugin.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts create mode 100644 packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts create mode 100644 packages/openapi-ts/src/plugins/shared/types/refs.d.ts create mode 100644 packages/openapi-ts/src/plugins/shared/types/schema.d.ts create mode 100644 packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts create mode 100644 packages/openapi-ts/src/plugins/shared/utils/refs.ts rename packages/openapi-ts/src/plugins/valibot/{number-helpers.ts => shared/numbers.ts} (97%) create mode 100644 packages/openapi-ts/src/plugins/valibot/shared/types.d.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/api.ts rename packages/openapi-ts/src/plugins/valibot/{ => v1}/constants.ts (99%) rename packages/openapi-ts/src/plugins/valibot/{ => v1}/operation.ts (81%) create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/plugin.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts create mode 100644 packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts rename packages/openapi-ts/src/plugins/valibot/{ => v1}/webhook.ts (83%) create mode 100644 packages/openapi-ts/src/plugins/zod/mini/api.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts create mode 100644 packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts rename packages/openapi-ts/src/plugins/zod/{ => shared}/export.ts (64%) create mode 100644 packages/openapi-ts/src/plugins/zod/shared/numbers.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/api.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/api.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts create mode 100644 packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts create mode 100644 packages/openapi-ts/src/utils/__tests__/minHeap.test.ts create mode 100644 packages/openapi-ts/src/utils/minHeap.ts delete mode 100644 scripts/example.sh create mode 100755 scripts/examples-check.sh create mode 100755 scripts/examples-generate.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c50f87ce3..20d66513b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,10 @@ jobs: if: matrix.node-version == '24.10.0' && matrix.os == 'ubuntu-latest' run: pnpm build --filter="@example/**" + - name: Check examples generated code + if: matrix.node-version == '24.10.0' && matrix.os == 'ubuntu-latest' + run: pnpm examples:check + - name: Run linter run: pnpm lint diff --git a/.vscode/settings.json b/.vscode/settings.json index 6df8dbfe3..c4595f000 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,11 +3,12 @@ "source.fixAll.eslint": "explicit" }, "editor.quickSuggestions": { - "strings": true + "strings": "on" }, "eslint.format.enable": true, "eslint.nodePath": "./node_modules", "eslint.workingDirectories": [{ "pattern": "./packages/*/" }], "typescript.preferences.autoImportFileExcludePatterns": ["dist/**"], + "typescript.preferences.autoImportSpecifierExcludeRegexes": ["^(node:)?os$"], "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/docs/openapi-ts/community/contributing/developing.md b/docs/openapi-ts/community/contributing/developing.md index 8b1aa38c3..2f7445d83 100644 --- a/docs/openapi-ts/community/contributing/developing.md +++ b/docs/openapi-ts/community/contributing/developing.md @@ -8,3 +8,49 @@ description: Learn how to contribute to Hey API. ::: warning This page is under construction. We appreciate your patience. ::: + +## Working with Examples + +The `examples` folder contains various integration examples that demonstrate how to use `@hey-api/openapi-ts` with different frameworks and libraries. These examples are kept in sync with the codebase through automated checks. + +### Generating Example Code + +When you make changes to the core packages that affect code generation, you need to regenerate the client code in all examples: + +```bash +pnpm examples:generate +``` + +This command will: + +- Find all examples with an `openapi-ts` script +- Run the OpenAPI code generator for each example +- Update the generated client code in each example + +### Checking Example Code + +Before committing changes, ensure that all generated example code is up-to-date: + +```bash +pnpm examples:check +``` + +This command will: + +- Regenerate all example code +- Check if any files were modified +- Exit with an error if generated code is out of sync + +This check is also run automatically in CI to ensure examples stay in sync with the main codebase. + +### Example Workflow + +1. Make changes to core packages +2. Build the packages: `pnpm build --filter="@hey-api/**"` +3. Regenerate examples: `pnpm examples:generate` +4. Commit all changes including the updated generated code +5. The CI will verify that examples are in sync + +::: tip +Think of generated example code as snapshot tests - they should always reflect the current state of the code generator. +::: diff --git a/docs/partials/contributors-list.md b/docs/partials/contributors-list.md index bf465d047..b7aa7ab8f 100644 --- a/docs/partials/contributors-list.md +++ b/docs/partials/contributors-list.md @@ -51,6 +51,7 @@ - [Marcel Richter](https://github.com/mrclrchtr) - [Marek Lukáš](https://github.com/tajnymag) - [Matsu](https://github.com/Matsuuu) +- [Maurici Abad Gutierrez](https://github.com/mauriciabad) - [Max Scopp](https://github.com/max-scopp) - [Maximilian Dewald](https://github.com/maxdewald) - [Michał Grezel](https://github.com/dracomithril) diff --git a/eslint-rules/local-paths.js b/eslint-rules/local-paths.js new file mode 100644 index 000000000..10fedd255 --- /dev/null +++ b/eslint-rules/local-paths.js @@ -0,0 +1,233 @@ +import path from 'node:path'; + +function normalize(p) { + return p.split(path.sep).join('/'); +} + +function stripExtAndIndex(p) { + return p + .replace(/(\/index)?\.(ts|tsx|js|cjs|mjs|d\.ts)$/, '') + .replace(/\/index$/, ''); +} + +/** + * Single consolidated rule implementing the previous three behaviors: + * - plugins: allow relative within same plugin (plugin may be @scope/name), else require `~` + * - openApi: allow relative only within same first-level openApi folder, else require `~` + * - first-level folders: allow relative only within same first-level folder, else require `~` + */ +const enforceLocalPaths = { + create(context) { + const filename = context.getFilename(); + if (!filename || filename === '') return {}; + + const normalizedFile = normalize(filename); + const srcMarker = '/packages/openapi-ts/src/'; + const srcIdx = normalizedFile.indexOf(srcMarker); + if (srcIdx === -1) return {}; + + const after = normalizedFile.slice(srcIdx + srcMarker.length); + const parts = after.split('/'); + const firstLevel = parts[0]; + + // helpers and mode-specific roots + // compute the absolute normalized bundle root for accurate comparisons + let bundleAbsRoot = + normalizedFile.slice(0, srcIdx + srcMarker.length) + + 'plugins/@hey-api/client-core/bundle/'; + bundleAbsRoot = normalize(bundleAbsRoot); + + let mode = 'first-level'; + let pluginAbsRoot = null; + let openApiFirstRoot = null; + let firstRoot = null; + + if (firstLevel === 'plugins') { + mode = 'plugins'; + // derive plugin folder (support scoped plugin names) + let pluginFolder = parts[1] || ''; + if (pluginFolder.startsWith('@') && parts.length > 2) { + pluginFolder = `${pluginFolder}/${parts[2]}`; + } + pluginAbsRoot = + normalizedFile.slice(0, srcIdx + srcMarker.length) + + `plugins/${pluginFolder}/`; + pluginAbsRoot = normalize(pluginAbsRoot); + } else if (firstLevel === 'openApi') { + mode = 'openApi'; + const apiFirst = parts[1] || ''; + openApiFirstRoot = + normalizedFile.slice(0, srcIdx + srcMarker.length) + + `openApi/${apiFirst}/`; + openApiFirstRoot = normalize(openApiFirstRoot); + } else { + firstRoot = + normalizedFile.slice(0, srcIdx + srcMarker.length) + `${firstLevel}/`; + firstRoot = normalize(firstRoot); + } + + function resolveFromBasedir(basedir, sourceValue) { + try { + return path.resolve(basedir, sourceValue); + } catch { + return null; + } + } + + function resolveTildeToAbs(sourceValue) { + const rest = sourceValue.replace(/^~\/?/, ''); + return path.resolve(process.cwd(), 'packages/openapi-ts/src', rest); + } + + function toTilde(resolvedAbsolutePath) { + const normalized = normalize(resolvedAbsolutePath); + const marker = '/packages/openapi-ts/src/'; + const i = normalized.indexOf(marker); + if (i === -1) return null; + let rest = normalized.slice(i + marker.length); + rest = stripExtAndIndex(rest); + return `~/${rest}`; + } + + function reportReplaceWithTilde(node, sourceValue, message) { + const basedir = path.dirname(filename); + const resolved = resolveFromBasedir(basedir, sourceValue); + if (!resolved) return; + const newImport = toTilde(resolved); + if (!newImport) return; + context.report({ + fix(fixer) { + return fixer.replaceText(node.source, `'${newImport}'`); + }, + message, + node: node.source, + }); + } + + function reportReplaceWithRelative(node, sourceValue, message) { + const resolved = resolveTildeToAbs(sourceValue); + if (!resolved) return; + let relativePath = path + .relative(path.dirname(filename), resolved) + .split(path.sep) + .join('/'); + relativePath = stripExtAndIndex(relativePath); + if (!relativePath.startsWith('.')) relativePath = `./${relativePath}`; + context.report({ + fix(fixer) { + return fixer.replaceText(node.source, `'${relativePath}'`); + }, + message, + node: node.source, + }); + } + + function shouldRewriteRelative(sourceValue) { + if (typeof sourceValue !== 'string') return false; + if (!sourceValue.startsWith('.')) return false; + const basedir = path.dirname(filename); + const resolved = resolveFromBasedir(basedir, sourceValue); + if (!resolved) return false; + const nr = normalize(resolved); + if (!nr.includes('/packages/openapi-ts/src/')) return false; + + if (mode === 'plugins') { + if (nr.startsWith(pluginAbsRoot)) return false; // inside same plugin -> keep relative + if (nr.startsWith(bundleAbsRoot)) return false; // client-core bundle: keep relative + return true; + } + + if (mode === 'openApi') { + // if target is inside same first-level openApi folder -> don't rewrite + if (nr.startsWith(openApiFirstRoot)) return false; + // only rewrite if target is inside openApi at all + return nr.includes('/packages/openapi-ts/src/openApi/'); + } + + // generic first-level folder rule + if (nr.startsWith(firstRoot)) return false; + return true; + } + + function shouldRewriteTilde(sourceValue) { + if (typeof sourceValue !== 'string') return false; + if (!sourceValue.startsWith('~')) return false; + const resolved = resolveTildeToAbs(sourceValue); + if (!resolved) return false; + const nr = normalize(resolved); + + if (mode === 'plugins') { + // prefer relative imports for same-plugin targets or for the shared client-core bundle + return nr.startsWith(pluginAbsRoot) || nr.startsWith(bundleAbsRoot); + } + if (mode === 'openApi') { + return nr.startsWith(openApiFirstRoot); + } + return nr.startsWith(firstRoot); + } + + return { + ExportAllDeclaration(node) { + const sourceValue = node.source && node.source.value; + if (shouldRewriteRelative(sourceValue)) { + reportReplaceWithTilde( + node, + sourceValue, + 'Prefer `~` export for cross-boundary exports (autofixable).', + ); + return; + } + if (shouldRewriteTilde(sourceValue)) { + reportReplaceWithRelative( + node, + sourceValue, + 'Prefer relative export for intra-boundary targets (autofixable).', + ); + return; + } + }, + ImportDeclaration(node) { + const sourceValue = node.source && node.source.value; + if (shouldRewriteRelative(sourceValue)) { + reportReplaceWithTilde( + node, + sourceValue, + 'Prefer `~` import for cross-boundary imports (autofixable).', + ); + return; + } + if (shouldRewriteTilde(sourceValue)) { + reportReplaceWithRelative( + node, + sourceValue, + 'Prefer relative import for intra-boundary targets (autofixable).', + ); + return; + } + }, + }; + }, + meta: { + docs: { + description: + 'Enforce local import path boundaries for openapi-ts sources', + recommended: false, + }, + fixable: 'code', + schema: [], + type: 'suggestion', + }, +}; + +export default { + configs: { + recommended: { + rules: { + 'local-paths/enforce-local-paths': 'error', + }, + }, + }, + rules: { + 'enforce-local-paths': enforceLocalPaths, + }, +}; diff --git a/eslint.config.js b/eslint.config.js index 42826d4b0..93fb9b916 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -8,9 +8,12 @@ import pluginTypeScriptSortKeys from 'eslint-plugin-typescript-sort-keys'; import globals from 'globals'; import tseslint from 'typescript-eslint'; +import pluginLocalPaths from './eslint-rules/local-paths.js'; + export default tseslint.config( eslint.configs.recommended, ...tseslint.configs.recommended, + pluginLocalPaths.configs.recommended, { languageOptions: { ecmaVersion: 'latest', @@ -19,6 +22,7 @@ export default tseslint.config( }, }, plugins: { + 'local-paths': pluginLocalPaths, 'simple-import-sort': pluginSimpleImportSort, 'sort-destructure-keys': pluginSortDestructureKeys, 'sort-keys-fix': pluginSortKeysFix, diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..6359053b1 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,84 @@ +# Examples + +This directory contains integration examples demonstrating how to use `@hey-api/openapi-ts` with various frameworks, libraries, and client implementations. + +## Available Examples + +- **openapi-ts-angular** - Angular integration with common HTTP client +- **openapi-ts-angular-common** - Angular with @angular/common/http +- **openapi-ts-axios** - Using Axios client +- **openapi-ts-fastify** - Fastify server integration +- **openapi-ts-fetch** - Native Fetch API client +- **openapi-ts-next** - Next.js integration +- **openapi-ts-nuxt** - Nuxt.js integration with plugin +- **openapi-ts-ofetch** - Using ofetch client +- **openapi-ts-openai** - OpenAI API integration +- **openapi-ts-pinia-colada** - Vue with Pinia Colada state management +- **openapi-ts-sample** - Sample/template example (excluded from CI) +- **openapi-ts-tanstack-angular-query-experimental** - Angular with TanStack Query +- **openapi-ts-tanstack-react-query** - React with TanStack Query +- **openapi-ts-tanstack-svelte-query** - Svelte with TanStack Query +- **openapi-ts-tanstack-vue-query** - Vue with TanStack Query + +## Generated Code + +All examples (except `openapi-ts-sample`) contain generated client code that is **committed to the repository**. This ensures: + +1. Examples always reflect the current state of the code generator +2. Changes to the code generator are visible in pull requests +3. CI can verify that examples are kept up-to-date + +## Regenerating Examples + +After making changes to the core packages, regenerate all example code: + +```bash +pnpm examples:generate +``` + +This command will run `openapi-ts` for each example that has an `openapi-ts` script in its `package.json`. + +## Verifying Examples + +To check if all examples are up-to-date with the current codebase: + +```bash +pnpm examples:check +``` + +This check is also run automatically in CI. If it fails, run `pnpm examples:generate` and commit the changes. + +## Running Examples + +Each example can be run individually using the `example` script: + +```bash +# Run dev server for fetch example +pnpm example fetch dev + +# Build fetch example +pnpm example fetch build +``` + +Or directly using pnpm filters: + +```bash +pnpm --filter @example/openapi-ts-fetch dev +``` + +## Creating New Examples + +When creating a new example: + +1. Create a new directory in `examples/` +2. Add an `openapi-ts` script to `package.json` +3. Run `pnpm examples:generate` to create initial generated code +4. Commit both the source and generated code +5. The example will automatically be included in CI checks + +## Excluding Examples + +To exclude an example from CI (like `openapi-ts-sample`): + +1. Remove the `openapi-ts` script from `package.json`, or +2. Update the exclusion filters in `package.json` scripts and `.github/workflows/ci.yml` diff --git a/examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts b/examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts similarity index 53% rename from examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts rename to examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts index f4be36afb..e063c5925 100644 --- a/examples/openapi-ts-angular-common/src/client/@angular/common/http/resources.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/@angular/common.gen.ts @@ -1,9 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts -import { httpResource } from '@angular/common/http'; +import { type HttpRequest, httpResource } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import type { Options } from '../../../sdk.gen'; +import { client } from '../client.gen'; +import type { Options } from '../sdk.gen'; import type { AddPetData, AddPetResponse, @@ -38,28 +39,294 @@ import type { UpdateUserData, UploadFileData, UploadFileResponse, -} from '../../../types.gen'; -import { - addPetRequest, - createUserRequest, - createUsersWithListInputRequest, - deleteOrderRequest, - deletePetRequest, - deleteUserRequest, - findPetsByStatusRequest, - findPetsByTagsRequest, - getInventoryRequest, - getOrderByIdRequest, - getPetByIdRequest, - getUserByNameRequest, - loginUserRequest, - logoutUserRequest, - placeOrderRequest, - updatePetRequest, - updatePetWithFormRequest, - updateUserRequest, - uploadFileRequest, -} from './requests.gen'; +} from '../types.gen'; + +/** + * Add a new pet to the store. + * + * Add a new pet to the store. + */ +export const addPetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet', + ...options, + }); + +/** + * Update an existing pet. + * + * Update an existing pet by Id. + */ +export const updatePetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'PUT', + responseStyle: 'data', + url: '/pet', + ...options, + }); + +/** + * Finds Pets by status. + * + * Multiple status values can be provided with comma separated strings. + */ +export const findPetsByStatusRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/findByStatus', + ...options, + }); + +/** + * Finds Pets by tags. + * + * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. + */ +export const findPetsByTagsRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/findByTags', + ...options, + }); + +/** + * Deletes a pet. + * + * Delete a pet. + */ +export const deletePetRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Find pet by ID. + * + * Returns a single pet. + */ +export const getPetByIdRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Updates a pet in the store with form data. + * + * Updates a pet resource based on the form data. + */ +export const updatePetWithFormRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet/{petId}', + ...options, + }); + +/** + * Uploads an image. + * + * Upload image of the pet. + */ +export const uploadFileRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/pet/{petId}/uploadImage', + ...options, + }); + +/** + * Returns pet inventories by status. + * + * Returns a map of status codes to quantities. + */ +export const getInventoryRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/store/inventory', + ...options, + }); + +/** + * Place an order for a pet. + * + * Place a new order in the store. + */ +export const placeOrderRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/store/order', + ...options, + }); + +/** + * Delete purchase order by identifier. + * + * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. + */ +export const deleteOrderRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Find purchase order by ID. + * + * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. + */ +export const getOrderByIdRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/store/order/{orderId}', + ...options, + }); + +/** + * Create user. + * + * This can only be done by the logged in user. + */ +export const createUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/user', + ...options, + }); + +/** + * Creates list of users with given input array. + * + * Creates list of users with given input array. + */ +export const createUsersWithListInputRequest = < + ThrowOnError extends boolean = false, +>( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'POST', + responseStyle: 'data', + url: '/user/createWithList', + ...options, + }); + +/** + * Logs user into the system. + * + * Log into the system. + */ +export const loginUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/login', + ...options, + }); + +/** + * Logs out current logged in user session. + * + * Log user out of the system. + */ +export const logoutUserRequest = ( + options?: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/logout', + ...options, + }); + +/** + * Delete user resource. + * + * This can only be done by the logged in user. + */ +export const deleteUserRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'DELETE', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); + +/** + * Get user by user name. + * + * Get user detail based on username. + */ +export const getUserByNameRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'GET', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); + +/** + * Update user resource. + * + * This can only be done by the logged in user. + */ +export const updateUserRequest = ( + options: Options, +): HttpRequest => + (options?.client ?? client).requestOptions({ + method: 'PUT', + responseStyle: 'data', + url: '/user/{username}', + ...options, + }); @Injectable({ providedIn: 'root', @@ -67,6 +334,7 @@ import { export class PetServiceResources { /** * Add a new pet to the store. + * * Add a new pet to the store. */ public addPet( @@ -80,6 +348,7 @@ export class PetServiceResources { /** * Update an existing pet. + * * Update an existing pet by Id. */ public updatePet( @@ -93,6 +362,7 @@ export class PetServiceResources { /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ public findPetsByStatus( @@ -106,6 +376,7 @@ export class PetServiceResources { /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ public findPetsByTags( @@ -119,6 +390,7 @@ export class PetServiceResources { /** * Deletes a pet. + * * Delete a pet. */ public deletePet( @@ -132,6 +404,7 @@ export class PetServiceResources { /** * Find pet by ID. + * * Returns a single pet. */ public getPetById( @@ -145,6 +418,7 @@ export class PetServiceResources { /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ public updatePetWithForm( @@ -158,6 +432,7 @@ export class PetServiceResources { /** * Uploads an image. + * * Upload image of the pet. */ public uploadFile( @@ -176,6 +451,7 @@ export class PetServiceResources { export class StoreServiceResources { /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ public getInventory( @@ -189,6 +465,7 @@ export class StoreServiceResources { /** * Place an order for a pet. + * * Place a new order in the store. */ public placeOrder( @@ -202,6 +479,7 @@ export class StoreServiceResources { /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ public deleteOrder( @@ -215,6 +493,7 @@ export class StoreServiceResources { /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ public getOrderById( @@ -233,6 +512,7 @@ export class StoreServiceResources { export class UserServiceResources { /** * Create user. + * * This can only be done by the logged in user. */ public createUser( @@ -246,6 +526,7 @@ export class UserServiceResources { /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ public createUsersWithListInput( @@ -261,6 +542,7 @@ export class UserServiceResources { /** * Logs user into the system. + * * Log into the system. */ public loginUser( @@ -274,6 +556,7 @@ export class UserServiceResources { /** * Logs out current logged in user session. + * * Log user out of the system. */ public logoutUser( @@ -287,6 +570,7 @@ export class UserServiceResources { /** * Delete user resource. + * * This can only be done by the logged in user. */ public deleteUser( @@ -300,6 +584,7 @@ export class UserServiceResources { /** * Get user by user name. + * * Get user detail based on username. */ public getUserByName( @@ -313,6 +598,7 @@ export class UserServiceResources { /** * Update user resource. + * * This can only be done by the logged in user. */ public updateUser( diff --git a/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts b/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts deleted file mode 100644 index e66f2fbc8..000000000 --- a/examples/openapi-ts-angular-common/src/client/@angular/common/http/requests.gen.ts +++ /dev/null @@ -1,295 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { HttpRequest } from '@angular/common/http'; - -import { client as _heyApiClient } from '../../../client.gen'; -import type { Options } from '../../../sdk.gen'; -import type { - AddPetData, - CreateUserData, - CreateUsersWithListInputData, - DeleteOrderData, - DeletePetData, - DeleteUserData, - FindPetsByStatusData, - FindPetsByTagsData, - GetInventoryData, - GetOrderByIdData, - GetPetByIdData, - GetUserByNameData, - LoginUserData, - LogoutUserData, - PlaceOrderData, - UpdatePetData, - UpdatePetWithFormData, - UpdateUserData, - UploadFileData, -} from '../../../types.gen'; - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet', - ...options, - }); - -/** - * Update an existing pet. - * Update an existing pet by Id. - */ -export const updatePetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'PUT', - responseStyle: 'data', - url: '/pet', - ...options, - }); - -/** - * Finds Pets by status. - * Multiple status values can be provided with comma separated strings. - */ -export const findPetsByStatusRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/findByStatus', - ...options, - }); - -/** - * Finds Pets by tags. - * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. - */ -export const findPetsByTagsRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/findByTags', - ...options, - }); - -/** - * Deletes a pet. - * Delete a pet. - */ -export const deletePetRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Find pet by ID. - * Returns a single pet. - */ -export const getPetByIdRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet/{petId}', - ...options, - }); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/pet/{petId}/uploadImage', - ...options, - }); - -/** - * Returns pet inventories by status. - * Returns a map of status codes to quantities. - */ -export const getInventoryRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/store/inventory', - ...options, - }); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/store/order', - ...options, - }); - -/** - * Delete purchase order by identifier. - * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. - */ -export const deleteOrderRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/store/order/{orderId}', - ...options, - }); - -/** - * Find purchase order by ID. - * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. - */ -export const getOrderByIdRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/store/order/{orderId}', - ...options, - }); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/user', - ...options, - }); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputRequest = < - ThrowOnError extends boolean = false, ->( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'POST', - responseStyle: 'data', - url: '/user/createWithList', - ...options, - }); - -/** - * Logs user into the system. - * Log into the system. - */ -export const loginUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/login', - ...options, - }); - -/** - * Logs out current logged in user session. - * Log user out of the system. - */ -export const logoutUserRequest = ( - options?: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/logout', - ...options, - }); - -/** - * Delete user resource. - * This can only be done by the logged in user. - */ -export const deleteUserRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'DELETE', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); - -/** - * Get user by user name. - * Get user detail based on username. - */ -export const getUserByNameRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'GET', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); - -/** - * Update user resource. - * This can only be done by the logged in user. - */ -export const updateUserRequest = ( - options: Options, -): HttpRequest => - (options?.client ?? _heyApiClient).requestOptions({ - method: 'PUT', - responseStyle: 'data', - url: '/user/{username}', - ...options, - }); diff --git a/examples/openapi-ts-angular-common/src/client/client.gen.ts b/examples/openapi-ts-angular-common/src/client/client.gen.ts index 37fae577c..5bd9edd4b 100644 --- a/examples/openapi-ts-angular-common/src/client/client.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', throwOnError: true, }), diff --git a/examples/openapi-ts-angular-common/src/client/client/client.gen.ts b/examples/openapi-ts-angular-common/src/client/client/client.gen.ts index 93b77ace4..1555ca230 100644 --- a/examples/openapi-ts-angular-common/src/client/client/client.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client/client.gen.ts @@ -18,6 +18,7 @@ import { filter } from 'rxjs/operators'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; import type { Client, Config, @@ -69,7 +70,7 @@ export const createClient = (config: Config = {}): Client => { ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -83,12 +84,12 @@ export const createClient = (config: Config = {}): Client => { } } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } @@ -97,7 +98,7 @@ export const createClient = (config: Config = {}): Client => { const req = new HttpRequest( opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, @@ -130,7 +131,7 @@ export const createClient = (config: Config = {}): Client => { let req = initialReq; - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { req = await fn(req, opts as any); } @@ -151,7 +152,7 @@ export const createClient = (config: Config = {}): Client => { .pipe(filter((event) => event.type === HttpEventType.Response)), )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { result.response = await fn(result.response, req, opts as any); } @@ -177,7 +178,7 @@ export const createClient = (config: Config = {}): Client => { let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, diff --git a/examples/openapi-ts-angular-common/src/client/client/index.ts b/examples/openapi-ts-angular-common/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-angular-common/src/client/client/index.ts +++ b/examples/openapi-ts-angular-common/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts b/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts index b90ad7e5e..64a5d8b09 100644 --- a/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/client/utils.gen.ts @@ -345,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-angular-common/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts b/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts index ac31396fe..0b5389d08 100644 --- a/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from './bodySerializer.gen'; +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; import { type ArraySeparatorStyle, serializeArrayParam, @@ -112,3 +112,32 @@ export const getUrl = ({ } return url; }; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-angular-common/src/client/index.ts b/examples/openapi-ts-angular-common/src/client/index.ts index 6921f209d..89fcd5868 100644 --- a/examples/openapi-ts-angular-common/src/client/index.ts +++ b/examples/openapi-ts-angular-common/src/client/index.ts @@ -1,5 +1,5 @@ // This file is auto-generated by @hey-api/openapi-ts -export * from './@angular/common/http/requests.gen'; -export * from './@angular/common/http/resources.gen'; + +export * from './@angular/common.gen'; export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-angular-common/src/client/sdk.gen.ts b/examples/openapi-ts-angular-common/src/client/sdk.gen.ts index 072abae41..d1c943a6c 100644 --- a/examples/openapi-ts-angular-common/src/client/sdk.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,12 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< AddPetResponses, AddPetErrors, ThrowOnError, @@ -109,12 +110,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError, @@ -137,12 +139,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError, @@ -161,12 +164,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError, @@ -185,12 +189,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError, @@ -209,12 +214,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError, @@ -237,12 +243,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError, @@ -261,12 +268,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError, @@ -290,12 +298,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError, @@ -314,12 +323,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError, @@ -336,12 +346,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError, @@ -354,12 +365,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError, @@ -372,12 +384,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError, @@ -394,12 +407,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError, @@ -416,12 +430,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError, @@ -434,12 +449,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError, @@ -452,12 +468,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError, @@ -470,12 +487,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError, @@ -488,12 +506,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError, diff --git a/examples/openapi-ts-angular-common/src/client/types.gen.ts b/examples/openapi-ts-angular-common/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-angular-common/src/client/types.gen.ts +++ b/examples/openapi-ts-angular-common/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-angular/src/client/client.gen.ts b/examples/openapi-ts-angular/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-angular/src/client/client.gen.ts +++ b/examples/openapi-ts-angular/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-angular/src/client/client/client.gen.ts b/examples/openapi-ts-angular/src/client/client/client.gen.ts index 93b77ace4..1555ca230 100644 --- a/examples/openapi-ts-angular/src/client/client/client.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/client.gen.ts @@ -18,6 +18,7 @@ import { filter } from 'rxjs/operators'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; import type { Client, Config, @@ -69,7 +70,7 @@ export const createClient = (config: Config = {}): Client => { ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -83,12 +84,12 @@ export const createClient = (config: Config = {}): Client => { } } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } @@ -97,7 +98,7 @@ export const createClient = (config: Config = {}): Client => { const req = new HttpRequest( opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, @@ -130,7 +131,7 @@ export const createClient = (config: Config = {}): Client => { let req = initialReq; - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { req = await fn(req, opts as any); } @@ -151,7 +152,7 @@ export const createClient = (config: Config = {}): Client => { .pipe(filter((event) => event.type === HttpEventType.Response)), )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { result.response = await fn(result.response, req, opts as any); } @@ -177,7 +178,7 @@ export const createClient = (config: Config = {}): Client => { let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, diff --git a/examples/openapi-ts-angular/src/client/client/index.ts b/examples/openapi-ts-angular/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-angular/src/client/client/index.ts +++ b/examples/openapi-ts-angular/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-angular/src/client/client/utils.gen.ts b/examples/openapi-ts-angular/src/client/client/utils.gen.ts index b90ad7e5e..64a5d8b09 100644 --- a/examples/openapi-ts-angular/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-angular/src/client/client/utils.gen.ts @@ -345,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-angular/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-angular/src/client/core/utils.gen.ts b/examples/openapi-ts-angular/src/client/core/utils.gen.ts index ac31396fe..0b5389d08 100644 --- a/examples/openapi-ts-angular/src/client/core/utils.gen.ts +++ b/examples/openapi-ts-angular/src/client/core/utils.gen.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { QuerySerializer } from './bodySerializer.gen'; +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; import { type ArraySeparatorStyle, serializeArrayParam, @@ -112,3 +112,32 @@ export const getUrl = ({ } return url; }; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-angular/src/client/index.ts b/examples/openapi-ts-angular/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-angular/src/client/index.ts +++ b/examples/openapi-ts-angular/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-angular/src/client/sdk.gen.ts b/examples/openapi-ts-angular/src/client/sdk.gen.ts index 2c40f5893..9d7d0b4e5 100644 --- a/examples/openapi-ts-angular/src/client/sdk.gen.ts +++ b/examples/openapi-ts-angular/src/client/sdk.gen.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -67,7 +67,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -87,12 +87,13 @@ export type Options< export class PetService { /** * Add a new pet to the store. + * * Add a new pet to the store. */ public addPet( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< AddPetResponses, AddPetErrors, ThrowOnError @@ -114,12 +115,13 @@ export class PetService { /** * Update an existing pet. + * * Update an existing pet by Id. */ public updatePet( options: Options, ) { - return (options.client ?? _heyApiClient).put< + return (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -141,12 +143,13 @@ export class PetService { /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ public findPetsByStatus( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -164,12 +167,13 @@ export class PetService { /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ public findPetsByTags( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -187,12 +191,13 @@ export class PetService { /** * Deletes a pet. + * * Delete a pet. */ public deletePet( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -210,12 +215,13 @@ export class PetService { /** * Find pet by ID. + * * Returns a single pet. */ public getPetById( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -237,12 +243,13 @@ export class PetService { /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ public updatePetWithForm( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -260,12 +267,13 @@ export class PetService { /** * Uploads an image. + * * Upload image of the pet. */ public uploadFile( options: Options, ) { - return (options.client ?? _heyApiClient).post< + return (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -293,12 +301,13 @@ export class PetService { export class StoreService { /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ public getInventory( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -316,12 +325,13 @@ export class StoreService { /** * Place an order for a pet. + * * Place a new order in the store. */ public placeOrder( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -337,12 +347,13 @@ export class StoreService { /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ public deleteOrder( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -354,12 +365,13 @@ export class StoreService { /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ public getOrderById( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -376,12 +388,13 @@ export class StoreService { export class UserService { /** * Create user. + * * This can only be done by the logged in user. */ public createUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -397,12 +410,13 @@ export class UserService { /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ public createUsersWithListInput( options?: Options, ) { - return (options?.client ?? _heyApiClient).post< + return (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -418,12 +432,13 @@ export class UserService { /** * Logs user into the system. + * * Log into the system. */ public loginUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -435,12 +450,13 @@ export class UserService { /** * Logs out current logged in user session. + * * Log user out of the system. */ public logoutUser( options?: Options, ) { - return (options?.client ?? _heyApiClient).get< + return (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -452,12 +468,13 @@ export class UserService { /** * Delete user resource. + * * This can only be done by the logged in user. */ public deleteUser( options: Options, ) { - return (options.client ?? _heyApiClient).delete< + return (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -469,12 +486,13 @@ export class UserService { /** * Get user by user name. + * * Get user detail based on username. */ public getUserByName( options: Options, ) { - return (options.client ?? _heyApiClient).get< + return (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -486,12 +504,13 @@ export class UserService { /** * Update user resource. + * * This can only be done by the logged in user. */ public updateUser( options: Options, ) { - return (options.client ?? _heyApiClient).put< + return (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-angular/src/client/types.gen.ts b/examples/openapi-ts-angular/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-angular/src/client/types.gen.ts +++ b/examples/openapi-ts-angular/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-axios/src/client/client.gen.ts b/examples/openapi-ts-axios/src/client/client.gen.ts index 102ab4bfb..c8bfd460b 100644 --- a/examples/openapi-ts-axios/src/client/client.gen.ts +++ b/examples/openapi-ts-axios/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseURL: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-axios/src/client/client/client.gen.ts b/examples/openapi-ts-axios/src/client/client/client.gen.ts index b2f7b118f..f81a9e786 100644 --- a/examples/openapi-ts-axios/src/client/client/client.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/client.gen.ts @@ -3,7 +3,10 @@ import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; import axios from 'axios'; -import type { Client, Config } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions } from './types.gen'; import { buildUrl, createConfig, @@ -38,8 +41,7 @@ export const createClient = (config: Config = {}): Client => { return getConfig(); }; - // @ts-expect-error - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, @@ -58,12 +60,19 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.body = opts.bodySerializer(opts.body); } const url = buildUrl(opts); + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); try { // assign Axios here for consistency with fetch const _axios = opts.axios!; @@ -71,8 +80,8 @@ export const createClient = (config: Config = {}): Client => { const { auth, ...optsWithoutAuth } = opts; const response = await _axios({ ...optsWithoutAuth, - baseURL: opts.baseURL as string, - data: opts.body, + baseURL: '', // the baseURL is already included in `url` + data: getValidRequestBody(opts), headers: opts.headers as RawAxiosRequestHeaders, // let `paramsSerializer()` handle query params if it exists params: opts.paramsSerializer ? opts.query : undefined, @@ -106,18 +115,49 @@ export const createClient = (config: Config = {}): Client => { } }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as Record, + method, + // @ts-expect-error + signal: opts.signal, + url, + }); + }; + return { buildUrl, - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), instance, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), } as Client; }; diff --git a/examples/openapi-ts-axios/src/client/client/index.ts b/examples/openapi-ts-axios/src/client/client/index.ts index 8ddc04f42..cff1d39c9 100644 --- a/examples/openapi-ts-axios/src/client/client/index.ts +++ b/examples/openapi-ts-axios/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-axios/src/client/client/types.gen.ts b/examples/openapi-ts-axios/src/client/client/types.gen.ts index b28841acc..d59239b9a 100644 --- a/examples/openapi-ts-axios/src/client/client/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/types.gen.ts @@ -10,6 +10,10 @@ import type { } from 'axios'; import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -56,11 +60,20 @@ export interface Config } export interface RequestOptions< + TData = unknown, ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - throwOnError: ThrowOnError; - }> { + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -76,6 +89,11 @@ export interface RequestOptions< url: Url; } +export interface ClientOptions { + baseURL?: string; + throwOnError?: boolean; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -100,26 +118,29 @@ export type RequestResult< }) >; -export interface ClientOptions { - baseURL?: string; - throwOnError?: boolean; -} - type MethodFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < @@ -130,10 +151,16 @@ type BuildUrlFn = < url: string; }, >( - options: Pick & Omit, 'axios'>, + options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { instance: AxiosInstance; }; @@ -162,7 +189,11 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + TResponse = unknown, +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit; export type OptionsLegacyParser< @@ -170,12 +201,16 @@ export type OptionsLegacyParser< ThrowOnError extends boolean = boolean, > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-axios/src/client/client/utils.gen.ts b/examples/openapi-ts-axios/src/client/client/utils.gen.ts index 8f20fa853..c87309246 100644 --- a/examples/openapi-ts-axios/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-axios/src/client/client/utils.gen.ts @@ -1,16 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from '../core/auth.gen'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer.gen'; -import type { ArraySeparatorStyle } from '../core/pathSerializer.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; import type { Client, ClientOptions, @@ -18,83 +15,6 @@ import type { RequestOptions, } from './types.gen'; -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - export const createQuerySerializer = ({ allowReserved, array, @@ -211,7 +131,15 @@ export const setAuthParams = async ({ }; export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ + const instanceBaseUrl = options.axios?.defaults?.baseURL; + + const baseUrl = + !!options.baseURL && typeof options.baseURL === 'string' + ? options.baseURL + : instanceBaseUrl; + + return getUrl({ + baseUrl: baseUrl as string, path: options.path, // let `paramsSerializer()` handle query params if it exists query: !options.paramsSerializer ? options.query : undefined, @@ -221,33 +149,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - path, - query, - querySerializer, - url: _url, -}: { - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; }; export const mergeConfigs = (a: Config, b: Config): Config => { diff --git a/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-axios/src/client/core/types.gen.ts b/examples/openapi-ts-axios/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-axios/src/client/core/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-axios/src/client/core/utils.gen.ts b/examples/openapi-ts-axios/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-axios/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-axios/src/client/index.ts b/examples/openapi-ts-axios/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-axios/src/client/index.ts +++ b/examples/openapi-ts-axios/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-axios/src/client/sdk.gen.ts b/examples/openapi-ts-axios/src/client/sdk.gen.ts index 59074a4f4..bb7cee4ba 100644 --- a/examples/openapi-ts-axios/src/client/sdk.gen.ts +++ b/examples/openapi-ts-axios/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ responseType: 'json', security: [ { @@ -108,12 +105,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -135,12 +133,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -158,12 +157,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -181,12 +181,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -203,12 +204,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -230,12 +232,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -253,12 +256,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -281,12 +285,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -304,12 +309,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -325,12 +331,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -341,12 +348,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -358,12 +366,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -379,12 +388,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -400,12 +410,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -417,12 +428,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -433,12 +445,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -449,12 +462,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -466,12 +480,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-axios/src/client/types.gen.ts b/examples/openapi-ts-axios/src/client/types.gen.ts index 257d5446a..48da19d87 100644 --- a/examples/openapi-ts-axios/src/client/types.gen.ts +++ b/examples/openapi-ts-axios/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseURL: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseURL: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-fastify/src/client/client.gen.ts b/examples/openapi-ts-fastify/src/client/client.gen.ts index 6061f9286..db8c98964 100644 --- a/examples/openapi-ts-fastify/src/client/client.gen.ts +++ b/examples/openapi-ts-fastify/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'http://petstore.swagger.io/v1', }), ); diff --git a/examples/openapi-ts-sample/src/client/client/client.ts b/examples/openapi-ts-fastify/src/client/client/client.gen.ts similarity index 56% rename from examples/openapi-ts-sample/src/client/client/client.ts rename to examples/openapi-ts-fastify/src/client/client/client.gen.ts index 89d1e3158..a439d2748 100644 --- a/examples/openapi-ts-sample/src/client/client/client.ts +++ b/examples/openapi-ts-fastify/src/client/client/client.gen.ts @@ -1,4 +1,14 @@ -import type { Client, Config, RequestOptions } from './types'; +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -7,7 +17,7 @@ import { mergeConfigs, mergeHeaders, setAuthParams, -} from './utils'; +} from './utils.gen'; type ReqInit = Omit & { body?: any; @@ -28,15 +38,16 @@ export const createClient = (config: Config = {}): Client => { Request, Response, unknown, - RequestOptions + ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, }; if (opts.security) { @@ -50,24 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -78,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -90,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -155,7 +192,7 @@ export const createClient = (config: Config = {}): Client => { const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -176,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-fastify/src/client/client/client.ts b/examples/openapi-ts-fastify/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-fastify/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-fastify/src/client/client/index.ts b/examples/openapi-ts-fastify/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-fastify/src/client/client/index.ts +++ b/examples/openapi-ts-fastify/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-fetch/src/client/client/types.ts b/examples/openapi-ts-fastify/src/client/client/types.gen.ts similarity index 70% rename from examples/openapi-ts-fetch/src/client/client/types.ts rename to examples/openapi-ts-fastify/src/client/client/types.gen.ts index c39a93132..1a005b51e 100644 --- a/examples/openapi-ts-fetch/src/client/client/types.ts +++ b/examples/openapi-ts-fastify/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -56,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -78,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -135,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -159,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -188,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -202,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-sample/src/client/client/utils.ts b/examples/openapi-ts-fastify/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-sample/src/client/client/utils.ts rename to examples/openapi-ts-fastify/src/client/client/utils.gen.ts index a52e67292..96de282a8 100644 --- a/examples/openapi-ts-sample/src/client/client/utils.ts +++ b/examples/openapi-ts-fastify/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -186,6 +110,25 @@ export const getParseAs = ( return; }; +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + export const setAuthParams = async ({ security, ...options @@ -194,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -217,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -233,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -273,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -324,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-fastify/src/client/core/auth.ts b/examples/openapi-ts-fastify/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-fastify/src/client/core/auth.ts rename to examples/openapi-ts-fastify/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-fastify/src/client/core/auth.ts +++ b/examples/openapi-ts-fastify/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-sample/src/client/core/bodySerializer.ts b/examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts similarity index 92% rename from examples/openapi-ts-sample/src/client/core/bodySerializer.ts rename to examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts index 98ce7791f..49cd8925e 100644 --- a/examples/openapi-ts-sample/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-fastify/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -21,6 +23,8 @@ const serializeFormDataPair = ( ): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } diff --git a/examples/openapi-ts-sample/src/client/core/params.ts b/examples/openapi-ts-fastify/src/client/core/params.gen.ts similarity index 98% rename from examples/openapi-ts-sample/src/client/core/params.ts rename to examples/openapi-ts-fastify/src/client/core/params.gen.ts index ba35263d8..71c88e852 100644 --- a/examples/openapi-ts-sample/src/client/core/params.ts +++ b/examples/openapi-ts-fastify/src/client/core/params.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = diff --git a/examples/openapi-ts-next/src/client/core/pathSerializer.ts b/examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-next/src/client/core/pathSerializer.ts rename to examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-next/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-fastify/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-sample/src/client/core/types.ts b/examples/openapi-ts-fastify/src/client/core/types.gen.ts similarity index 85% rename from examples/openapi-ts-sample/src/client/core/types.ts rename to examples/openapi-ts-fastify/src/client/core/types.gen.ts index 2dd4106fb..643c070c9 100644 --- a/examples/openapi-ts-sample/src/client/core/types.ts +++ b/examples/openapi-ts-fastify/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-fastify/src/client/core/utils.gen.ts b/examples/openapi-ts-fastify/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-fastify/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-fastify/src/client/index.ts b/examples/openapi-ts-fastify/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-fastify/src/client/index.ts +++ b/examples/openapi-ts-fastify/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-fastify/src/client/sdk.gen.ts b/examples/openapi-ts-fastify/src/client/sdk.gen.ts index 05760e098..c60e38505 100644 --- a/examples/openapi-ts-fastify/src/client/sdk.gen.ts +++ b/examples/openapi-ts-fastify/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { CreatePetsData, CreatePetsErrors, @@ -17,7 +17,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -37,7 +37,7 @@ export type Options< export const listPets = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< ListPetsResponses, ListPetsErrors, ThrowOnError @@ -52,7 +52,7 @@ export const listPets = ( export const createPets = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreatePetsResponses, CreatePetsErrors, ThrowOnError @@ -67,7 +67,7 @@ export const createPets = ( export const showPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< ShowPetByIdResponses, ShowPetByIdErrors, ThrowOnError diff --git a/examples/openapi-ts-fastify/src/client/types.gen.ts b/examples/openapi-ts-fastify/src/client/types.gen.ts index a05af4065..054b6615e 100644 --- a/examples/openapi-ts-fastify/src/client/types.gen.ts +++ b/examples/openapi-ts-fastify/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'http://petstore.swagger.io/v1' | (string & {}); +}; + export type Pet = { id: number; name: string; @@ -96,7 +100,3 @@ export type ShowPetByIdResponses = { export type ShowPetByIdResponse = ShowPetByIdResponses[keyof ShowPetByIdResponses]; - -export type ClientOptions = { - baseUrl: 'http://petstore.swagger.io/v1' | (string & {}); -}; diff --git a/examples/openapi-ts-fetch/src/client/client.gen.ts b/examples/openapi-ts-fetch/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-fetch/src/client/client.gen.ts +++ b/examples/openapi-ts-fetch/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-fetch/src/client/client/client.ts b/examples/openapi-ts-fetch/src/client/client/client.gen.ts similarity index 54% rename from examples/openapi-ts-fetch/src/client/client/client.ts rename to examples/openapi-ts-fetch/src/client/client/client.gen.ts index dddfe80c0..a439d2748 100644 --- a/examples/openapi-ts-fetch/src/client/client/client.ts +++ b/examples/openapi-ts-fetch/src/client/client/client.gen.ts @@ -1,4 +1,14 @@ -import type { Client, Config, RequestOptions } from './types'; +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -7,7 +17,7 @@ import { mergeConfigs, mergeHeaders, setAuthParams, -} from './utils'; +} from './utils.gen'; type ReqInit = Omit & { body?: any; @@ -28,15 +38,16 @@ export const createClient = (config: Config = {}): Client => { Request, Response, unknown, - RequestOptions + ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, }; if (opts.security) { @@ -50,24 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -78,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -90,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -143,17 +180,19 @@ export const createClient = (config: Config = {}): Client => { }; } - let error = await response.text(); + const textError = await response.text(); + let jsonError: unknown; try { - error = JSON.parse(error); + jsonError = JSON.parse(textError); } catch { // noop } + const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -174,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-fetch/src/client/client/index.ts b/examples/openapi-ts-fetch/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-fetch/src/client/client/index.ts +++ b/examples/openapi-ts-fetch/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-sample/src/client/client/types.ts b/examples/openapi-ts-fetch/src/client/client/types.gen.ts similarity index 70% rename from examples/openapi-ts-sample/src/client/client/types.ts rename to examples/openapi-ts-fetch/src/client/client/types.gen.ts index c39a93132..1a005b51e 100644 --- a/examples/openapi-ts-sample/src/client/client/types.ts +++ b/examples/openapi-ts-fetch/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -56,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -78,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -135,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -159,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -188,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -202,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-fetch/src/client/client/utils.ts b/examples/openapi-ts-fetch/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-fetch/src/client/client/utils.ts rename to examples/openapi-ts-fetch/src/client/client/utils.gen.ts index a52e67292..96de282a8 100644 --- a/examples/openapi-ts-fetch/src/client/client/utils.ts +++ b/examples/openapi-ts-fetch/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -186,6 +110,25 @@ export const getParseAs = ( return; }; +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + export const setAuthParams = async ({ security, ...options @@ -194,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -217,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -233,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -273,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -324,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-fetch/src/client/core/auth.ts b/examples/openapi-ts-fetch/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-fetch/src/client/core/auth.ts rename to examples/openapi-ts-fetch/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-fetch/src/client/core/auth.ts +++ b/examples/openapi-ts-fetch/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-fetch/src/client/core/bodySerializer.ts b/examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts similarity index 84% rename from examples/openapi-ts-fetch/src/client/core/bodySerializer.ts rename to examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts index 21c235741..49cd8925e 100644 --- a/examples/openapi-ts-fetch/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-fetch/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,7 +64,7 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => + bodySerializer: (body: T): string => JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-fetch/src/client/core/params.ts b/examples/openapi-ts-fetch/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-fetch/src/client/core/params.ts rename to examples/openapi-ts-fetch/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-fetch/src/client/core/params.ts +++ b/examples/openapi-ts-fetch/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-sample/src/client/core/pathSerializer.ts b/examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-sample/src/client/core/pathSerializer.ts rename to examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-sample/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-fetch/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/types.ts b/examples/openapi-ts-fetch/src/client/core/types.gen.ts similarity index 75% rename from examples/openapi-ts-fetch/src/client/core/types.ts rename to examples/openapi-ts-fetch/src/client/core/types.gen.ts index 77d879253..643c070c9 100644 --- a/examples/openapi-ts-fetch/src/client/core/types.ts +++ b/examples/openapi-ts-fetch/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -102,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-fetch/src/client/core/utils.gen.ts b/examples/openapi-ts-fetch/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-fetch/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-fetch/src/client/index.ts b/examples/openapi-ts-fetch/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-fetch/src/client/index.ts +++ b/examples/openapi-ts-fetch/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-fetch/src/client/sdk.gen.ts b/examples/openapi-ts-fetch/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-fetch/src/client/sdk.gen.ts +++ b/examples/openapi-ts-fetch/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-fetch/src/client/types.gen.ts b/examples/openapi-ts-fetch/src/client/types.gen.ts index 6d8a6e7b9..a2e6be0fa 100644 --- a/examples/openapi-ts-fetch/src/client/types.gen.ts +++ b/examples/openapi-ts-fetch/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-next/openapi-ts.config.ts b/examples/openapi-ts-next/openapi-ts.config.ts index ff06af2b1..bd8c28b15 100644 --- a/examples/openapi-ts-next/openapi-ts.config.ts +++ b/examples/openapi-ts-next/openapi-ts.config.ts @@ -11,7 +11,7 @@ export default defineConfig({ plugins: [ { name: '@hey-api/client-next', - runtimeConfigPath: './src/hey-api.ts', + runtimeConfigPath: '../hey-api', }, '@hey-api/sdk', { diff --git a/examples/openapi-ts-next/src/client/client.gen.ts b/examples/openapi-ts-next/src/client/client.gen.ts index 25aa4ccc9..321368cc7 100644 --- a/examples/openapi-ts-next/src/client/client.gen.ts +++ b/examples/openapi-ts-next/src/client/client.gen.ts @@ -2,12 +2,12 @@ import { createClientConfig } from '../hey-api'; import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -17,14 +17,13 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( createClientConfig( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ), diff --git a/examples/openapi-ts-next/src/client/client/client.gen.ts b/examples/openapi-ts-next/src/client/client/client.gen.ts new file mode 100644 index 000000000..53d0433b9 --- /dev/null +++ b/examples/openapi-ts-next/src/client/client/client.gen.ts @@ -0,0 +1,262 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + // @ts-expect-error + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + + for (const fn of interceptors.request.fns) { + if (fn) { + await fn(opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + const requestInit: ReqInit = { + ...opts, + body: getValidRequestBody(opts), + }; + + let response = await _fetch(url, requestInit); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, opts); + } + } + + const result = { + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + return { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + const requestInit = { + ...init, + method: init.method as Config['method'], + url, + }; + for (const fn of interceptors.request.fns) { + if (fn) { + await fn(requestInit); + request = new Request(requestInit.url, requestInit); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-next/src/client/client/client.ts b/examples/openapi-ts-next/src/client/client/client.ts deleted file mode 100644 index acd447624..000000000 --- a/examples/openapi-ts-next/src/client/client/client.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors(); - - // @ts-expect-error - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - for (const fn of interceptors.request._fns) { - if (fn) { - await fn(opts); - } - } - - const url = buildUrl(opts); - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(url, { - ...opts, - body: opts.body as ReqInit['body'], - }); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, opts); - } - } - - const result = { - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - return { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-next/src/client/client/index.ts b/examples/openapi-ts-next/src/client/client/index.ts index 15d37422a..cff1d39c9 100644 --- a/examples/openapi-ts-next/src/client/client/index.ts +++ b/examples/openapi-ts-next/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -17,5 +20,5 @@ export type { RequestOptions, RequestResult, TDataShape, -} from './types'; -export { createConfig } from './utils'; +} from './types.gen'; +export { createConfig } from './utils.gen'; diff --git a/examples/openapi-ts-next/src/client/client/types.ts b/examples/openapi-ts-next/src/client/client/types.gen.ts similarity index 64% rename from examples/openapi-ts-next/src/client/client/types.ts rename to examples/openapi-ts-next/src/client/client/types.gen.ts index aa7539213..76f0bf322 100644 --- a/examples/openapi-ts-next/src/client/client/types.ts +++ b/examples/openapi-ts-next/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export interface Config extends Omit, @@ -24,7 +33,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Throw an error instead of returning it in the response? * @@ -34,11 +50,20 @@ export interface Config } export interface RequestOptions< + TData = unknown, ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - throwOnError: ThrowOnError; - }> { + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -54,6 +79,13 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -92,16 +124,24 @@ type MethodFn = < TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick>, 'method'>, ) => RequestResult; type BuildUrlFn = < @@ -115,8 +155,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -144,7 +190,11 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & + TResponse = unknown, +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit; export type OptionsLegacyParser< @@ -152,12 +202,16 @@ export type OptionsLegacyParser< ThrowOnError extends boolean = boolean, > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-next/src/client/client/utils.ts b/examples/openapi-ts-next/src/client/client/utils.gen.ts similarity index 81% rename from examples/openapi-ts-next/src/client/client/utils.ts rename to examples/openapi-ts-next/src/client/client/utils.gen.ts index 004023033..914ee288d 100644 --- a/examples/openapi-ts-next/src/client/client/utils.ts +++ b/examples/openapi-ts-next/src/client/client/utils.gen.ts @@ -1,15 +1,22 @@ -import { getAuthToken } from '../core/auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; import type { QuerySerializer, QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +} from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; +} from '../core/pathSerializer.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; interface PathSerializer { path: Record; @@ -182,6 +189,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +220,9 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,8 +246,6 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; @@ -271,6 +300,14 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { @@ -281,7 +318,9 @@ export const mergeHeaders = ( } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -317,61 +356,60 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick>, 'eject' | 'use'>; - request: Pick>, 'eject' | 'use'>; - response: Pick>, 'eject' | 'use'>; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-next/src/client/core/auth.ts b/examples/openapi-ts-next/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-next/src/client/core/auth.ts rename to examples/openapi-ts-next/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-next/src/client/core/auth.ts +++ b/examples/openapi-ts-next/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-fastify/src/client/core/bodySerializer.ts b/examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts similarity index 82% rename from examples/openapi-ts-fastify/src/client/core/bodySerializer.ts rename to examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts index fab971b66..49cd8925e 100644 --- a/examples/openapi-ts-fastify/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-next/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,8 +64,8 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), }; @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-fastify/src/client/core/params.ts b/examples/openapi-ts-next/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-fastify/src/client/core/params.ts rename to examples/openapi-ts-next/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-fastify/src/client/core/params.ts +++ b/examples/openapi-ts-next/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-fetch/src/client/core/pathSerializer.ts b/examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-fetch/src/client/core/pathSerializer.ts rename to examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-fetch/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-next/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-fastify/src/client/core/types.ts b/examples/openapi-ts-next/src/client/core/types.gen.ts similarity index 68% rename from examples/openapi-ts-fastify/src/client/core/types.ts rename to examples/openapi-ts-next/src/client/core/types.gen.ts index 1f8688099..643c070c9 100644 --- a/examples/openapi-ts-fastify/src/client/core/types.ts +++ b/examples/openapi-ts-next/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -84,6 +84,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -96,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-next/src/client/core/utils.gen.ts b/examples/openapi-ts-next/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-next/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-next/src/client/index.ts b/examples/openapi-ts-next/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-next/src/client/index.ts +++ b/examples/openapi-ts-next/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-next/src/client/sdk.gen.ts b/examples/openapi-ts-next/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-next/src/client/sdk.gen.ts +++ b/examples/openapi-ts-next/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-next/src/client/types.gen.ts b/examples/openapi-ts-next/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-next/src/client/types.gen.ts +++ b/examples/openapi-ts-next/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-ofetch/src/client/client/index.ts b/examples/openapi-ts-ofetch/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-ofetch/src/client/client/index.ts +++ b/examples/openapi-ts-ofetch/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-ofetch/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-ofetch/src/client/sdk.gen.ts b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts index 4bb50da39..f424fe675 100644 --- a/examples/openapi-ts-ofetch/src/client/sdk.gen.ts +++ b/examples/openapi-ts-ofetch/src/client/sdk.gen.ts @@ -81,6 +81,7 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( @@ -103,6 +104,7 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( @@ -129,6 +131,7 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( @@ -151,6 +154,7 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( @@ -173,6 +177,7 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( @@ -195,6 +200,7 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( @@ -221,6 +227,7 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( @@ -243,6 +250,7 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( @@ -270,6 +278,7 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( @@ -292,6 +301,7 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( @@ -312,6 +322,7 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( @@ -328,6 +339,7 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( @@ -344,6 +356,7 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( @@ -364,6 +377,7 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( @@ -384,6 +398,7 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( @@ -400,6 +415,7 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( @@ -416,6 +432,7 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( @@ -432,6 +449,7 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( @@ -448,6 +466,7 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( diff --git a/examples/openapi-ts-openai/src/client/client.gen.ts b/examples/openapi-ts-openai/src/client/client.gen.ts index ed96ef557..60a14f713 100644 --- a/examples/openapi-ts-openai/src/client/client.gen.ts +++ b/examples/openapi-ts-openai/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://api.openai.com/v1', }), ); diff --git a/examples/openapi-ts-openai/src/client/client/client.gen.ts b/examples/openapi-ts-openai/src/client/client/client.gen.ts index 0c606b81c..a439d2748 100644 --- a/examples/openapi-ts-openai/src/client/client/client.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/client.gen.ts @@ -1,6 +1,14 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; import { buildUrl, createConfig, @@ -33,7 +41,7 @@ export const createClient = (config: Config = {}): Client => { ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const beforeRequest = async (options: RequestOptions) => { const opts = { ..._config, ...options, @@ -53,25 +61,32 @@ export const createClient = (config: Config = {}): Client => { await opts.requestValidator(opts); } - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); const requestInit: ReqInit = { redirect: 'follow', ...opts, - body: opts.serializedBody, + body: getValidRequestBody(opts), }; let request = new Request(url, requestInit); - for (const fn of interceptors.request._fns) { + for (const fn of interceptors.request.fns) { if (fn) { request = await fn(request, opts); } @@ -82,7 +97,7 @@ export const createClient = (config: Config = {}): Client => { const _fetch = opts.fetch!; let response = await _fetch(request); - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { response = await fn(response, request, opts); } @@ -94,23 +109,41 @@ export const createClient = (config: Config = {}): Client => { }; if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + if ( response.status === 204 || response.headers.get('Content-Length') === '0' ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } return opts.responseStyle === 'data' - ? {} + ? emptyData : { - data: {}, + data: emptyData, ...result, }; } - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - let data: any; switch (parseAs) { case 'arrayBuffer': @@ -159,7 +192,7 @@ export const createClient = (config: Config = {}): Client => { const error = jsonError ?? textError; let finalError = error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn(error, response, request, opts)) as string; } @@ -180,20 +213,56 @@ export const createClient = (config: Config = {}): Client => { }; }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-openai/src/client/client/index.ts b/examples/openapi-ts-openai/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-openai/src/client/client/index.ts +++ b/examples/openapi-ts-openai/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-openai/src/client/client/types.gen.ts b/examples/openapi-ts-openai/src/client/client/types.gen.ts index 2a123be9a..1a005b51e 100644 --- a/examples/openapi-ts-openai/src/client/client/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/types.gen.ts @@ -1,6 +1,10 @@ // This file is auto-generated by @hey-api/openapi-ts import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -22,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -61,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -87,7 +100,7 @@ export interface ResolvedRequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> extends RequestOptions { +> extends RequestOptions { serializedBody?: string; } @@ -148,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -172,7 +197,13 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { interceptors: Middleware; }; @@ -201,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -215,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-openai/src/client/client/utils.gen.ts b/examples/openapi-ts-openai/src/client/client/utils.gen.ts index 6f955d080..96de282a8 100644 --- a/examples/openapi-ts-openai/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-openai/src/client/client/utils.gen.ts @@ -1,16 +1,14 @@ // This file is auto-generated by @hey-api/openapi-ts import { getAuthToken } from '../core/auth.gen'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, } from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; import type { Client, ClientOptions, @@ -18,87 +16,6 @@ import type { RequestOptions, } from './types.gen'; -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - export const createQuerySerializer = ({ allowReserved, array, @@ -250,8 +167,8 @@ export const setAuthParams = async ({ } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -261,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -301,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -352,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-openai/src/client/core/types.gen.ts b/examples/openapi-ts-openai/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-openai/src/client/core/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-openai/src/client/core/utils.gen.ts b/examples/openapi-ts-openai/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-openai/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-openai/src/client/index.ts b/examples/openapi-ts-openai/src/client/index.ts index 688e3c912..f796d2cc8 100644 --- a/examples/openapi-ts-openai/src/client/index.ts +++ b/examples/openapi-ts-openai/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; export * from './types.gen'; diff --git a/examples/openapi-ts-openai/src/client/sdk.gen.ts b/examples/openapi-ts-openai/src/client/sdk.gen.ts index 6bcc5384d..97180dae3 100644 --- a/examples/openapi-ts-openai/src/client/sdk.gen.ts +++ b/examples/openapi-ts-openai/src/client/sdk.gen.ts @@ -3,10 +3,10 @@ import { type Client, formDataBodySerializer, - type Options as ClientOptions, + type Options as Options2, type TDataShape, } from './client'; -import { client as _heyApiClient } from './client.gen'; +import { client } from './client.gen'; import type { ActivateOrganizationCertificatesData, ActivateOrganizationCertificatesResponses, @@ -351,7 +351,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -366,7 +366,7 @@ export type Options< }; class _HeyApiClient { - protected _client: Client = _heyApiClient; + protected _client: Client = client; constructor(args?: { client?: Client }) { if (args?.client) { @@ -378,6 +378,7 @@ class _HeyApiClient { export class OpenAi extends _HeyApiClient { /** * List assistants + * * Returns a list of assistants. */ public listAssistants( @@ -401,6 +402,7 @@ export class OpenAi extends _HeyApiClient { /** * Create assistant + * * Create an assistant with a model and instructions. */ public createAssistant( @@ -428,6 +430,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete assistant + * * Delete an assistant. */ public deleteAssistant( @@ -451,6 +454,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve assistant + * * Retrieves an assistant. */ public getAssistant( @@ -474,6 +478,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify assistant + * * Modifies an assistant. */ public modifyAssistant( @@ -501,6 +506,7 @@ export class OpenAi extends _HeyApiClient { /** * Create speech + * * Generates audio from the input text. */ public createSpeech( @@ -528,6 +534,7 @@ export class OpenAi extends _HeyApiClient { /** * Create transcription + * * Transcribes audio into the input language. */ public createTranscription( @@ -556,6 +563,7 @@ export class OpenAi extends _HeyApiClient { /** * Create translation + * * Translates audio into English. */ public createTranslation( @@ -584,6 +592,7 @@ export class OpenAi extends _HeyApiClient { /** * List batch + * * List your organization's batches. */ public listBatches( @@ -607,6 +616,7 @@ export class OpenAi extends _HeyApiClient { /** * Create batch + * * Creates and executes a batch from an uploaded file of requests */ public createBatch( @@ -634,6 +644,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve batch + * * Retrieves a batch. */ public retrieveBatch( @@ -657,6 +668,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel batch + * * Cancels an in-progress batch. The batch will be in status `cancelling` for up to 10 minutes, before changing to `cancelled`, where it will have partial results (if any) available in the output file. */ public cancelBatch( @@ -680,6 +692,7 @@ export class OpenAi extends _HeyApiClient { /** * List Chat Completions + * * List stored Chat Completions. Only Chat Completions that have been stored * with the `store` parameter set to `true` will be returned. * @@ -705,6 +718,7 @@ export class OpenAi extends _HeyApiClient { /** * Create chat completion + * * **Starting a new project?** We recommend trying [Responses](https://platform.openai.com/docs/api-reference/responses) * to take advantage of the latest OpenAI platform features. Compare * [Chat Completions with Responses](https://platform.openai.com/docs/guides/responses-vs-chat-completions?api-mode=responses). @@ -747,6 +761,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete chat completion + * * Delete a stored chat completion. Only Chat Completions that have been * created with the `store` parameter set to `true` can be deleted. * @@ -772,6 +787,7 @@ export class OpenAi extends _HeyApiClient { /** * Get chat completion + * * Get a stored chat completion. Only Chat Completions that have been created * with the `store` parameter set to `true` will be returned. * @@ -797,6 +813,7 @@ export class OpenAi extends _HeyApiClient { /** * Update chat completion + * * Modify a stored chat completion. Only Chat Completions that have been * created with the `store` parameter set to `true` can be modified. Currently, * the only supported modification is to update the `metadata` field. @@ -827,6 +844,7 @@ export class OpenAi extends _HeyApiClient { /** * Get chat messages + * * Get the messages in a stored chat completion. Only Chat Completions that * have been created with the `store` parameter set to `true` will be * returned. @@ -853,6 +871,7 @@ export class OpenAi extends _HeyApiClient { /** * Create completion + * * Creates a completion for the provided prompt and parameters. */ public createCompletion( @@ -880,6 +899,7 @@ export class OpenAi extends _HeyApiClient { /** * List containers + * * List Containers */ public listContainers( @@ -903,6 +923,7 @@ export class OpenAi extends _HeyApiClient { /** * Create container + * * Create Container */ public createContainer( @@ -930,6 +951,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a container + * * Delete Container */ public deleteContainer( @@ -953,6 +975,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container + * * Retrieve Container */ public retrieveContainer( @@ -976,6 +999,7 @@ export class OpenAi extends _HeyApiClient { /** * List container files + * * List Container files */ public listContainerFiles( @@ -999,6 +1023,7 @@ export class OpenAi extends _HeyApiClient { /** * Create container file + * * Create a Container File * * You can send either a multipart/form-data request with the raw file content, or a JSON request with a file ID. @@ -1030,6 +1055,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a container file + * * Delete Container File */ public deleteContainerFile( @@ -1053,6 +1079,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container file + * * Retrieve Container File */ public retrieveContainerFile( @@ -1076,6 +1103,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve container file content + * * Retrieve Container File Content */ public retrieveContainerFileContent( @@ -1099,6 +1127,7 @@ export class OpenAi extends _HeyApiClient { /** * Create embeddings + * * Creates an embedding vector representing the input text. */ public createEmbedding( @@ -1126,6 +1155,7 @@ export class OpenAi extends _HeyApiClient { /** * List evals + * * List evaluations for a project. * */ @@ -1150,6 +1180,7 @@ export class OpenAi extends _HeyApiClient { /** * Create eval + * * Create the structure of an evaluation that can be used to test a model's performance. * An evaluation is a set of testing criteria and the config for a data source, which dictates the schema of the data used in the evaluation. After creating an evaluation, you can run it on different models and model parameters. We support several types of graders and datasources. * For more information, see the [Evals guide](https://platform.openai.com/docs/guides/evals). @@ -1180,6 +1211,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete an eval + * * Delete an evaluation. * */ @@ -1204,6 +1236,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an eval + * * Get an evaluation by ID. * */ @@ -1228,6 +1261,7 @@ export class OpenAi extends _HeyApiClient { /** * Update an eval + * * Update certain properties of an evaluation. * */ @@ -1256,6 +1290,7 @@ export class OpenAi extends _HeyApiClient { /** * Get eval runs + * * Get a list of runs for an evaluation. * */ @@ -1280,6 +1315,7 @@ export class OpenAi extends _HeyApiClient { /** * Create eval run + * * Kicks off a new run for a given evaluation, specifying the data source, and what model configuration to use to test. The datasource will be validated against the schema specified in the config of the evaluation. * */ @@ -1308,6 +1344,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete eval run + * * Delete an eval run. * */ @@ -1332,6 +1369,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an eval run + * * Get an evaluation run by ID. * */ @@ -1356,6 +1394,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel eval run + * * Cancel an ongoing evaluation run. * */ @@ -1380,6 +1419,7 @@ export class OpenAi extends _HeyApiClient { /** * Get eval run output items + * * Get a list of output items for an evaluation run. * */ @@ -1404,6 +1444,7 @@ export class OpenAi extends _HeyApiClient { /** * Get an output item of an eval run + * * Get an evaluation run output item by ID. * */ @@ -1428,6 +1469,7 @@ export class OpenAi extends _HeyApiClient { /** * List files + * * Returns a list of files. */ public listFiles( @@ -1451,6 +1493,7 @@ export class OpenAi extends _HeyApiClient { /** * Upload file + * * Upload a file that can be used across various endpoints. Individual files can be up to 512 MB, and the size of all files uploaded by one organization can be up to 1 TB. * * The Assistants API supports files up to 2 million tokens and of specific file types. See the [Assistants Tools guide](https://platform.openai.com/docs/assistants/tools) for details. @@ -1488,6 +1531,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete file + * * Delete a file. */ public deleteFile( @@ -1511,6 +1555,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve file + * * Returns information about a specific file. */ public retrieveFile( @@ -1534,6 +1579,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve file content + * * Returns the contents of the specified file. */ public downloadFile( @@ -1557,6 +1603,7 @@ export class OpenAi extends _HeyApiClient { /** * Run grader + * * Run a grader. * */ @@ -1585,6 +1632,7 @@ export class OpenAi extends _HeyApiClient { /** * Validate grader + * * Validate a grader. * */ @@ -1613,6 +1661,7 @@ export class OpenAi extends _HeyApiClient { /** * List checkpoint permissions + * * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). * * Organization owners can use this endpoint to view all permissions for a fine-tuned model checkpoint. @@ -1639,6 +1688,7 @@ export class OpenAi extends _HeyApiClient { /** * Create checkpoint permissions + * * **NOTE:** Calling this endpoint requires an [admin API key](../admin-api-keys). * * This enables organization owners to share fine-tuned models with other projects in their organization. @@ -1669,6 +1719,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete checkpoint permission + * * **NOTE:** This endpoint requires an [admin API key](../admin-api-keys). * * Organization owners can use this endpoint to delete a permission for a fine-tuned model checkpoint. @@ -1695,6 +1746,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning jobs + * * List your organization's fine-tuning jobs * */ @@ -1719,6 +1771,7 @@ export class OpenAi extends _HeyApiClient { /** * Create fine-tuning job + * * Creates a fine-tuning job which begins the process of creating a new model from a given dataset. * * Response includes details of the enqueued job including job status and the name of the fine-tuned models once complete. @@ -1751,6 +1804,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve fine-tuning job + * * Get info about a fine-tuning job. * * [Learn more about fine-tuning](https://platform.openai.com/docs/guides/model-optimization) @@ -1777,6 +1831,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel fine-tuning + * * Immediately cancel a fine-tune job. * */ @@ -1801,6 +1856,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning checkpoints + * * List checkpoints for a fine-tuning job. * */ @@ -1825,6 +1881,7 @@ export class OpenAi extends _HeyApiClient { /** * List fine-tuning events + * * Get status updates for a fine-tuning job. * */ @@ -1849,6 +1906,7 @@ export class OpenAi extends _HeyApiClient { /** * Pause fine-tuning + * * Pause a fine-tune job. * */ @@ -1873,6 +1931,7 @@ export class OpenAi extends _HeyApiClient { /** * Resume fine-tuning + * * Resume a fine-tune job. * */ @@ -1897,6 +1956,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image edit + * * Creates an edited or extended image given one or more source images and a prompt. This endpoint only supports `gpt-image-1` and `dall-e-2`. */ public createImageEdit( @@ -1925,6 +1985,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image + * * Creates an image given a prompt. [Learn more](https://platform.openai.com/docs/guides/images). * */ @@ -1953,6 +2014,7 @@ export class OpenAi extends _HeyApiClient { /** * Create image variation + * * Creates a variation of a given image. This endpoint only supports `dall-e-2`. */ public createImageVariation( @@ -1981,6 +2043,7 @@ export class OpenAi extends _HeyApiClient { /** * List models + * * Lists the currently available models, and provides basic information about each one such as the owner and availability. */ public listModels( @@ -2004,6 +2067,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a fine-tuned model + * * Delete a fine-tuned model. You must have the Owner role in your organization to delete a model. */ public deleteModel( @@ -2027,6 +2091,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve model + * * Retrieves a model instance, providing basic information about the model such as the owner and permissioning. */ public retrieveModel( @@ -2050,6 +2115,7 @@ export class OpenAi extends _HeyApiClient { /** * Create moderation + * * Classifies if text and/or image inputs are potentially harmful. Learn * more in the [moderation guide](https://platform.openai.com/docs/guides/moderation). * @@ -2079,6 +2145,7 @@ export class OpenAi extends _HeyApiClient { /** * List all organization and project API keys. + * * List organization API keys */ public adminApiKeysList( @@ -2102,6 +2169,7 @@ export class OpenAi extends _HeyApiClient { /** * Create admin API key + * * Create an organization admin API key */ public adminApiKeysCreate( @@ -2129,6 +2197,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete admin API key + * * Delete an organization admin API key */ public adminApiKeysDelete( @@ -2152,6 +2221,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve admin API key + * * Retrieve a single organization API key */ public adminApiKeysGet( @@ -2175,6 +2245,7 @@ export class OpenAi extends _HeyApiClient { /** * List audit logs + * * List user actions and configuration changes within this organization. */ public listAuditLogs( @@ -2198,6 +2269,7 @@ export class OpenAi extends _HeyApiClient { /** * List organization certificates + * * List uploaded certificates for this organization. */ public listOrganizationCertificates( @@ -2221,6 +2293,7 @@ export class OpenAi extends _HeyApiClient { /** * Upload certificate + * * Upload a certificate to the organization. This does **not** automatically activate the certificate. * * Organizations can upload up to 50 certificates. @@ -2251,6 +2324,7 @@ export class OpenAi extends _HeyApiClient { /** * Activate certificates for organization + * * Activate certificates at the organization level. * * You can atomically and idempotently activate up to 10 certificates at a time. @@ -2281,6 +2355,7 @@ export class OpenAi extends _HeyApiClient { /** * Deactivate certificates for organization + * * Deactivate certificates at the organization level. * * You can atomically and idempotently deactivate up to 10 certificates at a time. @@ -2311,6 +2386,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete certificate + * * Delete a certificate from the organization. * * The certificate must be inactive for the organization and all projects. @@ -2337,6 +2413,7 @@ export class OpenAi extends _HeyApiClient { /** * Get certificate + * * Get a certificate that has been uploaded to the organization. * * You can get a certificate regardless of whether it is active or not. @@ -2363,6 +2440,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify certificate + * * Modify a certificate. Note that only the name can be modified. * */ @@ -2391,6 +2469,7 @@ export class OpenAi extends _HeyApiClient { /** * Costs + * * Get costs details for the organization. */ public usageCosts( @@ -2414,6 +2493,7 @@ export class OpenAi extends _HeyApiClient { /** * List invites + * * Returns a list of invites in the organization. */ public listInvites( @@ -2437,6 +2517,7 @@ export class OpenAi extends _HeyApiClient { /** * Create invite + * * Create an invite for a user to the organization. The invite must be accepted by the user before they have access to the organization. */ public inviteUser( @@ -2464,6 +2545,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete invite + * * Delete an invite. If the invite has already been accepted, it cannot be deleted. */ public deleteInvite( @@ -2487,6 +2569,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve invite + * * Retrieves an invite. */ public retrieveInvite( @@ -2510,6 +2593,7 @@ export class OpenAi extends _HeyApiClient { /** * List projects + * * Returns a list of projects. */ public listProjects( @@ -2533,6 +2617,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project + * * Create a new project in the organization. Projects can be created and archived, but cannot be deleted. */ public createProject( @@ -2560,6 +2645,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project + * * Retrieves a project. */ public retrieveProject( @@ -2583,6 +2669,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project + * * Modifies a project in the organization. */ public modifyProject( @@ -2610,6 +2697,7 @@ export class OpenAi extends _HeyApiClient { /** * List project API keys + * * Returns a list of API keys in the project. */ public listProjectApiKeys( @@ -2633,6 +2721,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project API key + * * Deletes an API key from the project. */ public deleteProjectApiKey( @@ -2656,6 +2745,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project API key + * * Retrieves an API key in the project. */ public retrieveProjectApiKey( @@ -2679,6 +2769,7 @@ export class OpenAi extends _HeyApiClient { /** * Archive project + * * Archives a project in the organization. Archived projects cannot be used or updated. */ public archiveProject( @@ -2702,6 +2793,7 @@ export class OpenAi extends _HeyApiClient { /** * List project certificates + * * List certificates for this project. */ public listProjectCertificates( @@ -2725,6 +2817,7 @@ export class OpenAi extends _HeyApiClient { /** * Activate certificates for project + * * Activate certificates at the project level. * * You can atomically and idempotently activate up to 10 certificates at a time. @@ -2755,6 +2848,7 @@ export class OpenAi extends _HeyApiClient { /** * Deactivate certificates for project + * * Deactivate certificates at the project level. You can atomically and * idempotently deactivate up to 10 certificates at a time. * @@ -2784,6 +2878,7 @@ export class OpenAi extends _HeyApiClient { /** * List project rate limits + * * Returns the rate limits per model for a project. */ public listProjectRateLimits( @@ -2807,6 +2902,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project rate limit + * * Updates a project rate limit. */ public updateProjectRateLimits( @@ -2834,6 +2930,7 @@ export class OpenAi extends _HeyApiClient { /** * List project service accounts + * * Returns a list of service accounts in the project. */ public listProjectServiceAccounts( @@ -2857,6 +2954,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project service account + * * Creates a new service account in the project. This also returns an unredacted API key for the service account. */ public createProjectServiceAccount( @@ -2884,6 +2982,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project service account + * * Deletes a service account from the project. */ public deleteProjectServiceAccount( @@ -2907,6 +3006,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project service account + * * Retrieves a service account in the project. */ public retrieveProjectServiceAccount( @@ -2930,6 +3030,7 @@ export class OpenAi extends _HeyApiClient { /** * List project users + * * Returns a list of users in the project. */ public listProjectUsers( @@ -2953,6 +3054,7 @@ export class OpenAi extends _HeyApiClient { /** * Create project user + * * Adds a user to the project. Users must already be members of the organization to be added to a project. */ public createProjectUser( @@ -2980,6 +3082,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete project user + * * Deletes a user from the project. */ public deleteProjectUser( @@ -3003,6 +3106,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve project user + * * Retrieves a user in the project. */ public retrieveProjectUser( @@ -3026,6 +3130,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify project user + * * Modifies a user's role in the project. */ public modifyProjectUser( @@ -3053,6 +3158,7 @@ export class OpenAi extends _HeyApiClient { /** * Audio speeches + * * Get audio speeches usage details for the organization. */ public usageAudioSpeeches( @@ -3076,6 +3182,7 @@ export class OpenAi extends _HeyApiClient { /** * Audio transcriptions + * * Get audio transcriptions usage details for the organization. */ public usageAudioTranscriptions( @@ -3099,6 +3206,7 @@ export class OpenAi extends _HeyApiClient { /** * Code interpreter sessions + * * Get code interpreter sessions usage details for the organization. */ public usageCodeInterpreterSessions( @@ -3122,6 +3230,7 @@ export class OpenAi extends _HeyApiClient { /** * Completions + * * Get completions usage details for the organization. */ public usageCompletions( @@ -3145,6 +3254,7 @@ export class OpenAi extends _HeyApiClient { /** * Embeddings + * * Get embeddings usage details for the organization. */ public usageEmbeddings( @@ -3168,6 +3278,7 @@ export class OpenAi extends _HeyApiClient { /** * Images + * * Get images usage details for the organization. */ public usageImages( @@ -3191,6 +3302,7 @@ export class OpenAi extends _HeyApiClient { /** * Moderations + * * Get moderations usage details for the organization. */ public usageModerations( @@ -3214,6 +3326,7 @@ export class OpenAi extends _HeyApiClient { /** * Vector stores + * * Get vector stores usage details for the organization. */ public usageVectorStores( @@ -3237,6 +3350,7 @@ export class OpenAi extends _HeyApiClient { /** * List users + * * Lists all of the users in the organization. */ public listUsers( @@ -3260,6 +3374,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete user + * * Deletes a user from the organization. */ public deleteUser( @@ -3283,6 +3398,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve user + * * Retrieves a user by their identifier. */ public retrieveUser( @@ -3306,6 +3422,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify user + * * Modifies a user's role in the organization. */ public modifyUser( @@ -3333,6 +3450,7 @@ export class OpenAi extends _HeyApiClient { /** * Create session + * * Create an ephemeral API token for use in client-side applications with the * Realtime API. Can be configured with the same session parameters as the * `session.update` client event. @@ -3367,6 +3485,7 @@ export class OpenAi extends _HeyApiClient { /** * Create transcription session + * * Create an ephemeral API token for use in client-side applications with the * Realtime API specifically for realtime transcriptions. * Can be configured with the same session parameters as the `transcription_session.update` client event. @@ -3401,6 +3520,7 @@ export class OpenAi extends _HeyApiClient { /** * Create a model response + * * Creates a model response. Provide [text](https://platform.openai.com/docs/guides/text) or * [image](https://platform.openai.com/docs/guides/images) inputs to generate [text](https://platform.openai.com/docs/guides/text) * or [JSON](https://platform.openai.com/docs/guides/structured-outputs) outputs. Have the model call @@ -3435,6 +3555,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete a model response + * * Deletes a model response with the given ID. * */ @@ -3459,6 +3580,7 @@ export class OpenAi extends _HeyApiClient { /** * Get a model response + * * Retrieves a model response with the given ID. * */ @@ -3483,6 +3605,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel a response + * * Cancels a model response with the given ID. Only responses created with * the `background` parameter set to `true` can be cancelled. * [Learn more](https://platform.openai.com/docs/guides/background). @@ -3509,6 +3632,7 @@ export class OpenAi extends _HeyApiClient { /** * List input items + * * Returns a list of input items for a given response. */ public listInputItems( @@ -3532,6 +3656,7 @@ export class OpenAi extends _HeyApiClient { /** * Create thread + * * Create a thread. */ public createThread( @@ -3559,6 +3684,7 @@ export class OpenAi extends _HeyApiClient { /** * Create thread and run + * * Create a thread and run it in one request. */ public createThreadAndRun( @@ -3586,6 +3712,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete thread + * * Delete a thread. */ public deleteThread( @@ -3609,6 +3736,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve thread + * * Retrieves a thread. */ public getThread( @@ -3632,6 +3760,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify thread + * * Modifies a thread. */ public modifyThread( @@ -3659,6 +3788,7 @@ export class OpenAi extends _HeyApiClient { /** * List messages + * * Returns a list of messages for a given thread. */ public listMessages( @@ -3682,6 +3812,7 @@ export class OpenAi extends _HeyApiClient { /** * Create message + * * Create a message. */ public createMessage( @@ -3709,6 +3840,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete message + * * Deletes a message. */ public deleteMessage( @@ -3732,6 +3864,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve message + * * Retrieve a message. */ public getMessage( @@ -3755,6 +3888,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify message + * * Modifies a message. */ public modifyMessage( @@ -3782,6 +3916,7 @@ export class OpenAi extends _HeyApiClient { /** * List runs + * * Returns a list of runs belonging to a thread. */ public listRuns( @@ -3805,6 +3940,7 @@ export class OpenAi extends _HeyApiClient { /** * Create run + * * Create a run. */ public createRun( @@ -3832,6 +3968,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve run + * * Retrieves a run. */ public getRun( @@ -3855,6 +3992,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify run + * * Modifies a run. */ public modifyRun( @@ -3882,6 +4020,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel a run + * * Cancels a run that is `in_progress`. */ public cancelRun( @@ -3905,6 +4044,7 @@ export class OpenAi extends _HeyApiClient { /** * List run steps + * * Returns a list of run steps belonging to a run. */ public listRunSteps( @@ -3928,6 +4068,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve run step + * * Retrieves a run step. */ public getRunStep( @@ -3951,6 +4092,7 @@ export class OpenAi extends _HeyApiClient { /** * Submit tool outputs to run + * * When a run has the `status: "requires_action"` and `required_action.type` is `submit_tool_outputs`, this endpoint can be used to submit the outputs from the tool calls once they're all completed. All outputs must be submitted in a single request. * */ @@ -3979,6 +4121,7 @@ export class OpenAi extends _HeyApiClient { /** * Create upload + * * Creates an intermediate [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object * that you can add [Parts](https://platform.openai.com/docs/api-reference/uploads/part-object) to. * Currently, an Upload can accept at most 8 GB in total and expires after an @@ -4023,6 +4166,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel upload + * * Cancels the Upload. No Parts may be added after an Upload is cancelled. * */ @@ -4047,6 +4191,7 @@ export class OpenAi extends _HeyApiClient { /** * Complete upload + * * Completes the [Upload](https://platform.openai.com/docs/api-reference/uploads/object). * * Within the returned Upload object, there is a nested [File](https://platform.openai.com/docs/api-reference/files/object) object that is ready to use in the rest of the platform. @@ -4081,6 +4226,7 @@ export class OpenAi extends _HeyApiClient { /** * Add upload part + * * Adds a [Part](https://platform.openai.com/docs/api-reference/uploads/part-object) to an [Upload](https://platform.openai.com/docs/api-reference/uploads/object) object. A Part represents a chunk of bytes from the file you are trying to upload. * * Each Part can be at most 64 MB, and you can add Parts until you hit the Upload maximum of 8 GB. @@ -4114,6 +4260,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector stores + * * Returns a list of vector stores. */ public listVectorStores( @@ -4137,6 +4284,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store + * * Create a vector store. */ public createVectorStore( @@ -4164,6 +4312,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete vector store + * * Delete a vector store. */ public deleteVectorStore( @@ -4187,6 +4336,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store + * * Retrieves a vector store. */ public getVectorStore( @@ -4210,6 +4360,7 @@ export class OpenAi extends _HeyApiClient { /** * Modify vector store + * * Modifies a vector store. */ public modifyVectorStore( @@ -4237,6 +4388,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store file batch + * * Create a vector store file batch. */ public createVectorStoreFileBatch( @@ -4264,6 +4416,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file batch + * * Retrieves a vector store file batch. */ public getVectorStoreFileBatch( @@ -4287,6 +4440,7 @@ export class OpenAi extends _HeyApiClient { /** * Cancel vector store file batch + * * Cancel a vector store file batch. This attempts to cancel the processing of files in this batch as soon as possible. */ public cancelVectorStoreFileBatch( @@ -4310,6 +4464,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector store files in a batch + * * Returns a list of vector store files in a batch. */ public listFilesInVectorStoreBatch( @@ -4333,6 +4488,7 @@ export class OpenAi extends _HeyApiClient { /** * List vector store files + * * Returns a list of vector store files. */ public listVectorStoreFiles( @@ -4356,6 +4512,7 @@ export class OpenAi extends _HeyApiClient { /** * Create vector store file + * * Create a vector store file by attaching a [File](https://platform.openai.com/docs/api-reference/files) to a [vector store](https://platform.openai.com/docs/api-reference/vector-stores/object). */ public createVectorStoreFile( @@ -4383,6 +4540,7 @@ export class OpenAi extends _HeyApiClient { /** * Delete vector store file + * * Delete a vector store file. This will remove the file from the vector store but the file itself will not be deleted. To delete the file, use the [delete file](https://platform.openai.com/docs/api-reference/files/delete) endpoint. */ public deleteVectorStoreFile( @@ -4406,6 +4564,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file + * * Retrieves a vector store file. */ public getVectorStoreFile( @@ -4429,6 +4588,7 @@ export class OpenAi extends _HeyApiClient { /** * Update vector store file attributes + * * Update attributes on a vector store file. */ public updateVectorStoreFileAttributes( @@ -4456,6 +4616,7 @@ export class OpenAi extends _HeyApiClient { /** * Retrieve vector store file content + * * Retrieve the parsed contents of a vector store file. */ public retrieveVectorStoreFileContent( @@ -4479,6 +4640,7 @@ export class OpenAi extends _HeyApiClient { /** * Search vector store + * * Search a vector store for relevant chunks based on a query and file attributes filter. */ public searchVectorStore( diff --git a/examples/openapi-ts-openai/src/client/types.gen.ts b/examples/openapi-ts-openai/src/client/types.gen.ts index 78630ab51..498d67077 100644 --- a/examples/openapi-ts-openai/src/client/types.gen.ts +++ b/examples/openapi-ts-openai/src/client/types.gen.ts @@ -1,5 +1,25 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://api.openai.com/v1' | (string & {}); +}; + +export type Webhooks = + | PostBatchCancelledWebhookRequest + | PostBatchCompletedWebhookRequest + | PostBatchExpiredWebhookRequest + | PostBatchFailedWebhookRequest + | PostEvalRunCanceledWebhookRequest + | PostEvalRunFailedWebhookRequest + | PostEvalRunSucceededWebhookRequest + | PostFineTuningJobCancelledWebhookRequest + | PostFineTuningJobFailedWebhookRequest + | PostFineTuningJobSucceededWebhookRequest + | PostResponseCancelledWebhookRequest + | PostResponseCompletedWebhookRequest + | PostResponseFailedWebhookRequest + | PostResponseIncompleteWebhookRequest; + export type AddUploadPartRequest = { /** * The chunk of bytes for this Part. @@ -78,6 +98,7 @@ export type ApiKeyList = { /** * Assistant + * * Represents an `assistant` that can call the model and use tools. */ export type AssistantObject = { @@ -320,7 +341,9 @@ export type AssistantsApiResponseFormatOption = * */ export type AssistantsApiToolChoiceOption = - | ('none' | 'auto' | 'required') + | 'none' + | 'auto' + | 'required' | AssistantsNamedToolChoice; /** @@ -931,6 +954,7 @@ export type AuditLogEventType = /** * Auto Chunking Strategy + * * The default strategy. This strategy currently uses a `max_chunk_size_tokens` of `800` and `chunk_overlap_tokens` of `400`. */ export type AutoChunkingStrategyRequestParam = { @@ -1027,6 +1051,7 @@ export type Batch = { /** * File expiration policy + * * The expiration policy for the output and/or error file that are generated for a batch. */ export type BatchFileExpirationAfter = { @@ -1148,6 +1173,7 @@ export type Certificate = { /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -1181,6 +1207,7 @@ export type ChatCompletionAllowedTools = { /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -1235,6 +1262,7 @@ export type ChatCompletionFunctions = { /** * ChatCompletionList + * * An object representing a list of Chat Completions. * */ @@ -1265,6 +1293,7 @@ export type ChatCompletionList = { /** * Custom tool call + * * A call to a custom tool created by the model. * */ @@ -1294,6 +1323,7 @@ export type ChatCompletionMessageCustomToolCall = { /** * ChatCompletionMessageList + * * An object representing a list of chat completion messages. * */ @@ -1340,6 +1370,7 @@ export type ChatCompletionMessageList = { /** * Function tool call + * * A call to a function tool created by the model. * */ @@ -1418,6 +1449,7 @@ export type ChatCompletionModalities = Array<'text' | 'audio'>; /** * Function tool choice + * * Specifies a tool the model should use. Use to force the model to call a specific function. */ export type ChatCompletionNamedToolChoice = { @@ -1435,6 +1467,7 @@ export type ChatCompletionNamedToolChoice = { /** * Custom tool choice + * * Specifies a tool the model should use. Use to force the model to call a specific custom tool. */ export type ChatCompletionNamedToolChoiceCustom = { @@ -1452,6 +1485,7 @@ export type ChatCompletionNamedToolChoiceCustom = { /** * Assistant message + * * Messages sent by the model in response to user messages. * */ @@ -1475,6 +1509,7 @@ export type ChatCompletionRequestAssistantMessage = { content?: string | Array; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1512,6 +1547,7 @@ export type ChatCompletionRequestAssistantMessageContentPart = /** * Developer message + * * Developer-provided instructions that the model should follow, regardless of * messages sent by the user. With o1 models and newer, `developer` messages * replace the previous `system` messages. @@ -1534,6 +1570,7 @@ export type ChatCompletionRequestDeveloperMessage = { /** * Function message + * * @deprecated */ export type ChatCompletionRequestFunctionMessage = { @@ -1573,6 +1610,7 @@ export type ChatCompletionRequestMessage = /** * Audio content part + * * Learn about [audio inputs](https://platform.openai.com/docs/guides/audio). * */ @@ -1596,6 +1634,7 @@ export type ChatCompletionRequestMessageContentPartAudio = { /** * File content part + * * Learn about [file inputs](https://platform.openai.com/docs/guides/text) for text generation. * */ @@ -1627,6 +1666,7 @@ export type ChatCompletionRequestMessageContentPartFile = { /** * Image content part + * * Learn about [image inputs](https://platform.openai.com/docs/guides/vision). * */ @@ -1663,6 +1703,7 @@ export type ChatCompletionRequestMessageContentPartRefusal = { /** * Text content part + * * Learn about [text inputs](https://platform.openai.com/docs/guides/text-generation). * */ @@ -1679,6 +1720,7 @@ export type ChatCompletionRequestMessageContentPartText = { /** * System message + * * Developer-provided instructions that the model should follow, regardless of * messages sent by the user. With o1 models and newer, use `developer` messages * for this purpose instead. @@ -1725,6 +1767,7 @@ export type ChatCompletionRequestToolMessageContentPart = /** * User message + * * Messages sent by an end user, containing prompts or additional context * information. * @@ -1829,6 +1872,7 @@ export type ChatCompletionResponseMessage = { content: string; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1910,6 +1954,7 @@ export type ChatCompletionStreamResponseDelta = { content?: string; /** * Deprecated and replaced by `tool_calls`. The name and arguments of a function that should be called, as generated by the model. + * * @deprecated */ function_call?: { @@ -1967,6 +2012,7 @@ export type ChatCompletionTokenLogprob = { /** * Function tool + * * A function tool that can be used to generate a response. * */ @@ -1989,7 +2035,9 @@ export type ChatCompletionTool = { * */ export type ChatCompletionToolChoiceOption = - | ('none' | 'auto' | 'required') + | 'none' + | 'auto' + | 'required' | ChatCompletionAllowedToolsChoice | ChatCompletionNamedToolChoice | ChatCompletionNamedToolChoiceCustom; @@ -2007,6 +2055,7 @@ export type ChunkingStrategyRequestParam = /** * Click + * * A click action. * */ @@ -2036,6 +2085,7 @@ export type Click = { /** * Code interpreter file output + * * The output of a code interpreter tool call that is a file. * */ @@ -2061,6 +2111,7 @@ export type CodeInterpreterFileOutput = { /** * Code interpreter output image + * * The image output from the code interpreter. * */ @@ -2077,6 +2128,7 @@ export type CodeInterpreterOutputImage = { /** * Code interpreter output logs + * * The logs output from the code interpreter. * */ @@ -2093,6 +2145,7 @@ export type CodeInterpreterOutputLogs = { /** * Code interpreter text output + * * The output of a code interpreter tool call that is text. * */ @@ -2111,6 +2164,7 @@ export type CodeInterpreterTextOutput = { /** * Code interpreter + * * A tool that runs Python code to help generate a response to a prompt. * */ @@ -2130,6 +2184,7 @@ export type CodeInterpreterTool = { /** * CodeInterpreterContainerAuto + * * Configuration for a code interpreter container. Optionally specify the IDs * of the files to run the code on. * @@ -2148,6 +2203,7 @@ export type CodeInterpreterToolAuto = { /** * Code interpreter tool call + * * A tool call to run code. * */ @@ -2199,6 +2255,7 @@ export type CodeInterpreterToolCall = { /** * Comparison Filter + * * A filter used to compare a specified attribute key to a given value using a defined comparison operation. * */ @@ -2298,6 +2355,7 @@ export type CompletionUsage = { /** * Compound Filter + * * Combine multiple filters using `and` or `or`. */ export type CompoundFilter = { @@ -2363,6 +2421,7 @@ export type ComputerScreenshotImage = { /** * Computer tool call + * * A tool call to a computer use tool. See the * [computer use guide](https://platform.openai.com/docs/guides/tools-computer-use) for more information. * @@ -2397,6 +2456,7 @@ export type ComputerToolCall = { /** * Computer tool call output + * * The output of a computer tool call. * */ @@ -2588,6 +2648,7 @@ export type Content = InputContent | OutputContent; /** * Coordinate + * * An x/y coordinate pair, e.g. `{ x: 100, y: 200 }`. * */ @@ -2780,14 +2841,16 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * `none` is the default when no functions are present. `auto` is the default * if functions are present. * + * * @deprecated */ - function_call?: ('none' | 'auto') | ChatCompletionFunctionCallOption; + function_call?: 'none' | 'auto' | ChatCompletionFunctionCallOption; /** * Deprecated in favor of `tools`. * * A list of functions the model may generate JSON inputs for. * + * * @deprecated */ functions?: Array; @@ -2825,6 +2888,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * This value is now deprecated in favor of `max_completion_tokens`, and is * not compatible with [o-series models](https://platform.openai.com/docs/guides/reasoning). * + * * @deprecated */ max_tokens?: number; @@ -2890,6 +2954,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { * If specified, our system will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result. * Determinism is not guaranteed, and you should refer to the `system_fingerprint` response parameter to monitor changes in the backend. * + * * @deprecated */ seed?: number; @@ -2938,6 +3003,7 @@ export type CreateChatCompletionRequest = CreateModelResponseProperties & { verbosity?: Verbosity; /** * Web search + * * This tool searches the web for relevant results to use in a response. * Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search?api-mode=chat). * @@ -3021,6 +3087,7 @@ export type CreateChatCompletionResponse = { * * Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. * + * * @deprecated */ system_fingerprint?: string; @@ -3093,6 +3160,7 @@ export type CreateChatCompletionStreamResponse = { * This fingerprint represents the backend configuration that the model runs with. * Can be used in conjunction with the `seed` request parameter to understand when backend changes have been made that might impact determinism. * + * * @deprecated */ system_fingerprint?: string; @@ -3161,7 +3229,7 @@ export type CreateCompletionRequest = { * ID of the model to use. You can use the [List models](https://platform.openai.com/docs/api-reference/models/list) API to see all of your available models, or see our [Model overview](https://platform.openai.com/docs/models) for descriptions of them. * */ - model: string | ('gpt-3.5-turbo-instruct' | 'davinci-002' | 'babbage-002'); + model: string | 'gpt-3.5-turbo-instruct' | 'davinci-002' | 'babbage-002'; /** * How many completions to generate for each prompt. * @@ -3332,11 +3400,9 @@ export type CreateEmbeddingRequest = { */ model: | string - | ( - | 'text-embedding-ada-002' - | 'text-embedding-3-small' - | 'text-embedding-3-large' - ); + | 'text-embedding-ada-002' + | 'text-embedding-3-small' + | 'text-embedding-3-large'; /** * A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#end-user-ids). * @@ -3374,6 +3440,7 @@ export type CreateEmbeddingResponse = { /** * CompletionsRunDataSource + * * A CompletionsRunDataSource object describing a model sampling configuration. * */ @@ -3474,6 +3541,7 @@ export type CreateEvalCompletionsRunDataSource = { /** * CustomDataSourceConfig + * * A CustomDataSourceConfig object that defines the schema for the data source used for the evaluation runs. * This schema is used to define the shape of the data that will be: * - Used to define your testing criteria and @@ -3499,6 +3567,7 @@ export type CreateEvalCustomDataSourceConfig = { /** * CreateEvalItem + * * A chat message that makes up the prompt or context. May include variable references to the `item` namespace, ie {{item.name}}. */ export type CreateEvalItem = @@ -3516,6 +3585,7 @@ export type CreateEvalItem = /** * JsonlRunDataSource + * * A JsonlRunDataSource object with that specifies a JSONL file that matches the eval * */ @@ -3538,6 +3608,7 @@ export type CreateEvalJsonlRunDataSource = { /** * LabelModelGrader + * * A LabelModelGrader object which uses a model to assign labels to each item * in the evaluation. * @@ -3571,6 +3642,7 @@ export type CreateEvalLabelModelGrader = { /** * LogsDataSourceConfig + * * A data source config which specifies the metadata property of your logs query. * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. * @@ -3634,6 +3706,7 @@ export type CreateEvalRequest = { /** * ResponsesRunDataSource + * * A ResponsesRunDataSource object describing a model sampling configuration. * */ @@ -3761,8 +3834,10 @@ export type CreateEvalRunRequest = { /** * StoredCompletionsDataSourceConfig + * * Deprecated in favor of LogsDataSourceConfig. * + * * @deprecated */ export type CreateEvalStoredCompletionsDataSourceConfig = { @@ -3800,6 +3875,7 @@ export type CreateFineTuningJobRequest = { * The hyperparameters used for the fine-tuning job. * This value is now deprecated in favor of `method`, and should be passed in under the `method` parameter. * + * * @deprecated */ hyperparameters?: { @@ -3871,7 +3947,10 @@ export type CreateFineTuningJobRequest = { */ model: | string - | ('babbage-002' | 'davinci-002' | 'gpt-3.5-turbo' | 'gpt-4o-mini'); + | 'babbage-002' + | 'davinci-002' + | 'gpt-3.5-turbo' + | 'gpt-4o-mini'; /** * The seed controls the reproducibility of the job. Passing in the same seed and job parameters should produce the same results, but may differ in rare cases. * If a seed is not specified, one will be generated for you. @@ -3936,7 +4015,7 @@ export type CreateImageEditRequest = { * `png` file less than 4MB. * */ - image: (Blob | File) | Array; + image: Blob | File | Array; input_fidelity?: ImageInputFidelity; /** * An additional image whose fully transparent areas (e.g. where alpha is zero) indicate where `image` should be edited. If there are multiple images provided, the mask will be applied on the first image. Must be a valid PNG file, less than 4MB, and have the same dimensions as `image`. @@ -3945,7 +4024,7 @@ export type CreateImageEditRequest = { /** * The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are supported. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. */ - model?: string | ('dall-e-2' | 'gpt-image-1'); + model?: string | 'dall-e-2' | 'gpt-image-1'; /** * The number of images to generate. Must be between 1 and 10. */ @@ -4016,7 +4095,7 @@ export type CreateImageRequest = { /** * The model to use for image generation. One of `dall-e-2`, `dall-e-3`, or `gpt-image-1`. Defaults to `dall-e-2` unless a parameter specific to `gpt-image-1` is used. */ - model?: string | ('dall-e-2' | 'dall-e-3' | 'gpt-image-1'); + model?: string | 'dall-e-2' | 'dall-e-3' | 'gpt-image-1'; /** * Control the content-moderation level for images generated by `gpt-image-1`. Must be either `low` for less restrictive filtering or `auto` (default value). */ @@ -4188,12 +4267,10 @@ export type CreateModerationRequest = { */ model?: | string - | ( - | 'omni-moderation-latest' - | 'omni-moderation-2024-09-26' - | 'text-moderation-latest' - | 'text-moderation-stable' - ); + | 'omni-moderation-latest' + | 'omni-moderation-2024-09-26' + | 'text-moderation-latest' + | 'text-moderation-stable'; }; /** @@ -4526,7 +4603,7 @@ export type CreateSpeechRequest = { * One of the available [TTS models](https://platform.openai.com/docs/models#tts): `tts-1`, `tts-1-hd` or `gpt-4o-mini-tts`. * */ - model: string | ('tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts'); + model: string | 'tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts'; /** * The format to audio in. Supported formats are `mp3`, `opus`, `aac`, `flac`, `wav`, and `pcm`. */ @@ -4578,46 +4655,44 @@ export type CreateThreadAndRunRequest = { */ model?: | string - | ( - | 'gpt-5' - | 'gpt-5-mini' - | 'gpt-5-nano' - | 'gpt-5-2025-08-07' - | 'gpt-5-mini-2025-08-07' - | 'gpt-5-nano-2025-08-07' - | 'gpt-4.1' - | 'gpt-4.1-mini' - | 'gpt-4.1-nano' - | 'gpt-4.1-2025-04-14' - | 'gpt-4.1-mini-2025-04-14' - | 'gpt-4.1-nano-2025-04-14' - | 'gpt-4o' - | 'gpt-4o-2024-11-20' - | 'gpt-4o-2024-08-06' - | 'gpt-4o-2024-05-13' - | 'gpt-4o-mini' - | 'gpt-4o-mini-2024-07-18' - | 'gpt-4.5-preview' - | 'gpt-4.5-preview-2025-02-27' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - ); + | 'gpt-5' + | 'gpt-5-mini' + | 'gpt-5-nano' + | 'gpt-5-2025-08-07' + | 'gpt-5-mini-2025-08-07' + | 'gpt-5-nano-2025-08-07' + | 'gpt-4.1' + | 'gpt-4.1-mini' + | 'gpt-4.1-nano' + | 'gpt-4.1-2025-04-14' + | 'gpt-4.1-mini-2025-04-14' + | 'gpt-4.1-nano-2025-04-14' + | 'gpt-4o' + | 'gpt-4o-2024-11-20' + | 'gpt-4o-2024-08-06' + | 'gpt-4o-2024-05-13' + | 'gpt-4o-mini' + | 'gpt-4o-mini-2024-07-18' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' + | 'gpt-4-turbo' + | 'gpt-4-turbo-2024-04-09' + | 'gpt-4-0125-preview' + | 'gpt-4-turbo-preview' + | 'gpt-4-1106-preview' + | 'gpt-4-vision-preview' + | 'gpt-4' + | 'gpt-4-0314' + | 'gpt-4-0613' + | 'gpt-4-32k' + | 'gpt-4-32k-0314' + | 'gpt-4-32k-0613' + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-0613' + | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-0125' + | 'gpt-3.5-turbo-16k-0613'; parallel_tool_calls?: ParallelToolCalls; response_format?: AssistantsApiResponseFormatOption; /** @@ -4765,9 +4840,7 @@ export type CreateTranscriptionRequest = { * ID of the model to use. The options are `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, and `whisper-1` (which is powered by our open source Whisper V2 model). * */ - model: - | string - | ('whisper-1' | 'gpt-4o-transcribe' | 'gpt-4o-mini-transcribe'); + model: string | 'whisper-1' | 'gpt-4o-transcribe' | 'gpt-4o-mini-transcribe'; /** * An optional text to guide the model's style or continue a previous audio segment. The [prompt](https://platform.openai.com/docs/guides/speech-to-text#prompting) should match the audio language. * @@ -4982,6 +5055,7 @@ export type CreateVectorStoreRequest = { /** * Custom tool + * * A custom tool that processes input using a specified format. Learn more about * [custom tools](https://platform.openai.com/docs/guides/function-calling#custom-tools). * @@ -5029,6 +5103,7 @@ export type CustomTool = { /** * Custom tool call + * * A call to a custom tool created by the model. * */ @@ -5062,6 +5137,7 @@ export type CustomToolCall = { /** * Custom tool call output + * * The output of a custom tool call from your code, being sent back to the model. * */ @@ -5090,12 +5166,14 @@ export type CustomToolCallOutput = { /** * Custom tool + * * A custom tool that processes input using a specified format. * */ export type CustomToolChatCompletions = { /** * Custom tool properties + * * Properties of the custom tool. * */ @@ -5119,6 +5197,7 @@ export type CustomToolChatCompletions = { | { /** * Grammar format + * * Your chosen grammar. */ grammar: { @@ -5225,6 +5304,7 @@ export type DoneEvent = { /** * DoubleClick + * * A double click action. * */ @@ -5249,6 +5329,7 @@ export type DoubleClick = { /** * Drag + * * A drag action. * */ @@ -5275,6 +5356,7 @@ export type Drag = { /** * Input message + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. Messages with the @@ -5343,6 +5425,7 @@ export type ErrorResponse = { /** * Eval + * * An Eval object with a data source config and testing criteria. * An Eval represents a task to be done for your LLM integration. * Like: @@ -5396,6 +5479,7 @@ export type Eval = { /** * EvalApiError + * * An object representing an error response from the Eval API. * */ @@ -5412,6 +5496,7 @@ export type EvalApiError = { /** * CustomDataSourceConfig + * * A CustomDataSourceConfig which specifies the schema of your `item` and optionally `sample` namespaces. * The response schema defines the shape of the data that will be: * - Used to define your testing criteria and @@ -5475,6 +5560,7 @@ export type EvalGraderTextSimilarity = GraderTextSimilarity & { /** * Eval message object + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. Messages with the @@ -5570,6 +5656,7 @@ export type EvalJsonlFileIdSource = { /** * EvalList + * * An object representing a list of evals. * */ @@ -5600,6 +5687,7 @@ export type EvalList = { /** * LogsDataSourceConfig + * * A LogsDataSourceConfig which specifies the metadata property of your logs query. * This is usually metadata like `usecase=chatbot` or `prompt-version=v2`, etc. * The schema returned by this data source config is used to defined what variables are available in your evals. @@ -5624,6 +5712,7 @@ export type EvalLogsDataSourceConfig = { /** * EvalResponsesSource + * * A EvalResponsesSource object describing a run data source configuration. * */ @@ -5678,6 +5767,7 @@ export type EvalResponsesSource = { /** * EvalRun + * * A schema representing an evaluation run. * */ @@ -5800,6 +5890,7 @@ export type EvalRun = { /** * EvalRunList + * * An object representing a list of runs for an evaluation. * */ @@ -5830,6 +5921,7 @@ export type EvalRunList = { /** * EvalRunOutputItem + * * A schema representing an evaluation run output item. * */ @@ -5955,6 +6047,7 @@ export type EvalRunOutputItem = { /** * EvalRunOutputItemList + * * An object representing a list of output items for an evaluation run. * */ @@ -5985,8 +6078,10 @@ export type EvalRunOutputItemList = { /** * StoredCompletionsDataSourceConfig + * * Deprecated in favor of LogsDataSourceConfig. * + * * @deprecated */ export type EvalStoredCompletionsDataSourceConfig = { @@ -6007,6 +6102,7 @@ export type EvalStoredCompletionsDataSourceConfig = { /** * StoredCompletionsRunDataSource + * * A StoredCompletionsRunDataSource configuration describing a set of filters * */ @@ -6036,6 +6132,7 @@ export type EvalStoredCompletionsSource = { /** * File expiration policy + * * The expiration policy for a file. By default, files with `purpose=batch` expire after 30 days and all other files are persisted until they are manually deleted. */ export type FileExpirationAfter = { @@ -6051,6 +6148,7 @@ export type FileExpirationAfter = { /** * File path + * * A path to a file. * */ @@ -6088,6 +6186,7 @@ export type FileSearchRanker = /** * File search tool call ranking options + * * The ranking options for the file search. If not specified, the file search tool will use the `auto` ranker and a score_threshold of 0. * * See the [file search tool documentation](https://platform.openai.com/docs/assistants/tools/file-search#customizing-file-search-settings) for more information. @@ -6103,6 +6202,7 @@ export type FileSearchRankingOptions = { /** * File search tool call + * * The results of a file search tool call. See the * [file search guide](https://platform.openai.com/docs/guides/tools-file-search) for more information. * @@ -6174,6 +6274,7 @@ export type FineTuneChatCompletionRequestAssistantMessage = { export type FineTuneChatRequestInput = { /** * A list of functions the model may generate JSON inputs for. + * * @deprecated */ functions?: Array; @@ -6375,6 +6476,7 @@ export type FineTuneSupervisedMethod = { /** * FineTuningCheckpointPermission + * * The `checkpoint.permission` object represents a permission for a fine-tuned model checkpoint. * */ @@ -6439,6 +6541,7 @@ export type FineTuningIntegration = { /** * FineTuningJob + * * The `fine_tuning.job` object represents a fine-tuning job that has been created through the API. * */ @@ -6559,6 +6662,7 @@ export type FineTuningJob = { /** * FineTuningJobCheckpoint + * * The `fine_tuning.job.checkpoint` object represents a model checkpoint for a fine-tuning job that is ready to use. * */ @@ -6664,6 +6768,7 @@ export type FunctionParameters = { /** * Function tool call + * * A tool call to run a function. See the * [function calling guide](https://platform.openai.com/docs/guides/function-calling) for more information. * @@ -6704,6 +6809,7 @@ export type FunctionToolCall = { /** * Function tool call output + * * The output of a function tool call. * */ @@ -6755,6 +6861,7 @@ export type FunctionToolCallResource = FunctionToolCall & { /** * LabelModelGrader + * * A LabelModelGrader object which uses a model to assign labels to each item * in the evaluation. * @@ -6785,6 +6892,7 @@ export type GraderLabelModel = { /** * MultiGrader + * * A MultiGrader object combines the output of multiple graders to produce a single score. */ export type GraderMulti = { @@ -6810,6 +6918,7 @@ export type GraderMulti = { /** * PythonGrader + * * A PythonGrader object that runs a python script on the input. * */ @@ -6834,6 +6943,7 @@ export type GraderPython = { /** * ScoreModelGrader + * * A ScoreModelGrader object that uses a model to assign a score to the input. * */ @@ -6868,6 +6978,7 @@ export type GraderScoreModel = { /** * StringCheckGrader + * * A StringCheckGrader object that performs a string comparison between input and reference using a specified operation. * */ @@ -6896,6 +7007,7 @@ export type GraderStringCheck = { /** * TextSimilarityGrader + * * A TextSimilarityGrader object which grades text based on similarity metrics. * */ @@ -7152,6 +7264,7 @@ export type ImageGenStreamEvent = /** * Image generation tool + * * A tool that generates images using a model like `gpt-image-1`. * */ @@ -7227,6 +7340,7 @@ export type ImageGenTool = { /** * Image generation call + * * An image generation request made by the model. * */ @@ -7275,6 +7389,7 @@ export type ImageInputFidelity = /** * Image generation response + * * The response from the image generation endpoint. */ export type ImagesResponse = { @@ -7386,6 +7501,7 @@ export type Includable = (typeof Includable)[keyof typeof Includable]; /** * Audio input + * * An audio input to the model. * */ @@ -7432,6 +7548,7 @@ export type InputItem = /** * Input message + * * A message input to the model with a role indicating instruction following * hierarchy. Instructions given with the `developer` or `system` role take * precedence over instructions given with the `user` role. @@ -7459,6 +7576,7 @@ export type InputMessage = { /** * Input item content list + * * A list of one or many input items to the model, containing different content * types. * @@ -7696,6 +7814,7 @@ export type ItemResource = /** * KeyPress + * * A collection of keypresses the model would like to perform. * */ @@ -7829,6 +7948,7 @@ export type ListVectorStoresResponse = { /** * Local shell exec action + * * Execute a shell command on the server. * */ @@ -7869,6 +7989,7 @@ export type LocalShellExecAction = { /** * Local shell tool + * * A tool that allows the model to execute shell commands in a local environment. * */ @@ -7881,6 +8002,7 @@ export type LocalShellTool = { /** * Local shell call + * * A tool call to run a command on the local shell. * */ @@ -7910,6 +8032,7 @@ export type LocalShellToolCall = { /** * Local shell call output + * * The output of a local shell tool call. * */ @@ -7960,6 +8083,7 @@ export type LogProbProperties = { /** * MCP approval request + * * A request for human approval of a tool invocation. * */ @@ -7993,6 +8117,7 @@ export type McpApprovalRequest = { /** * MCP approval response + * * A response to an MCP approval request. * */ @@ -8026,6 +8151,7 @@ export type McpApprovalResponse = { /** * MCP approval response + * * A response to an MCP approval request. * */ @@ -8059,6 +8185,7 @@ export type McpApprovalResponseResource = { /** * MCP list tools + * * A list of tools available on an MCP server. * */ @@ -8092,6 +8219,7 @@ export type McpListTools = { /** * MCP list tools tool + * * A tool available on an MCP server. * */ @@ -8124,6 +8252,7 @@ export type McpListToolsTool = { /** * MCP tool + * * Give the model access to additional tools via remote Model Context Protocol * (MCP) servers. [Learn more about MCP](https://platform.openai.com/docs/guides/tools-remote-mcp). * @@ -8138,6 +8267,7 @@ export type McpTool = { | { /** * MCP allowed tools + * * List of allowed tool names. */ tool_names?: Array; @@ -8176,7 +8306,8 @@ export type McpTool = { tool_names?: Array; }; } - | ('always' | 'never'); + | 'always' + | 'never'; /** * Optional description of the MCP server, used to provide more context. * @@ -8200,6 +8331,7 @@ export type McpTool = { /** * MCP tool call + * * An invocation of a tool on an MCP server. * */ @@ -8243,6 +8375,7 @@ export type McpToolCall = { /** * Image file + * * References an image [File](https://platform.openai.com/docs/api-reference/files) in the content of a message. */ export type MessageContentImageFileObject = { @@ -8264,6 +8397,7 @@ export type MessageContentImageFileObject = { /** * Image URL + * * References an image URL in the content of a message. */ export type MessageContentImageUrlObject = { @@ -8285,6 +8419,7 @@ export type MessageContentImageUrlObject = { /** * Refusal + * * The refusal content generated by the assistant. */ export type MessageContentRefusalObject = { @@ -8297,6 +8432,7 @@ export type MessageContentRefusalObject = { /** * File citation + * * A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. */ export type MessageContentTextAnnotationsFileCitationObject = { @@ -8320,6 +8456,7 @@ export type MessageContentTextAnnotationsFileCitationObject = { /** * File path + * * A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. */ export type MessageContentTextAnnotationsFilePathObject = { @@ -8343,6 +8480,7 @@ export type MessageContentTextAnnotationsFilePathObject = { /** * Text + * * The text content that is part of a message. */ export type MessageContentTextObject = { @@ -8361,6 +8499,7 @@ export type MessageContentTextObject = { /** * Image file + * * References an image [File](https://platform.openai.com/docs/api-reference/files) in the content of a message. */ export type MessageDeltaContentImageFileObject = { @@ -8386,6 +8525,7 @@ export type MessageDeltaContentImageFileObject = { /** * Image URL + * * References an image URL in the content of a message. */ export type MessageDeltaContentImageUrlObject = { @@ -8411,6 +8551,7 @@ export type MessageDeltaContentImageUrlObject = { /** * Refusal + * * The refusal content that is part of a message. */ export type MessageDeltaContentRefusalObject = { @@ -8427,6 +8568,7 @@ export type MessageDeltaContentRefusalObject = { /** * File citation + * * A citation within the message that points to a specific quote from a specific File associated with the assistant or the message. Generated when the assistant uses the "file_search" tool to search files. */ export type MessageDeltaContentTextAnnotationsFileCitationObject = { @@ -8458,6 +8600,7 @@ export type MessageDeltaContentTextAnnotationsFileCitationObject = { /** * File path + * * A URL for the file that's generated when the assistant used the `code_interpreter` tool to generate a file. */ export type MessageDeltaContentTextAnnotationsFilePathObject = { @@ -8485,6 +8628,7 @@ export type MessageDeltaContentTextAnnotationsFilePathObject = { /** * Text + * * The text content that is part of a message. */ export type MessageDeltaContentTextObject = { @@ -8507,6 +8651,7 @@ export type MessageDeltaContentTextObject = { /** * Message delta object + * * Represents a message delta i.e. any changed fields on a message during streaming. * */ @@ -8536,6 +8681,7 @@ export type MessageDeltaObject = { /** * The message object + * * Represents a message within a [thread](https://platform.openai.com/docs/api-reference/threads). */ export type MessageObject = { @@ -8615,6 +8761,7 @@ export type MessageObject = { /** * Text + * * The text content that is part of a message. */ export type MessageRequestContentTextObject = { @@ -8665,6 +8812,7 @@ export type Metadata = { /** * Model + * * Describes an OpenAI model offering that can be used with the API. */ export type Model = { @@ -8690,18 +8838,16 @@ export type ModelIds = ModelIdsShared | ModelIdsResponses; export type ModelIdsResponses = | ModelIdsShared - | ( - | 'o1-pro' - | 'o1-pro-2025-03-19' - | 'o3-pro' - | 'o3-pro-2025-06-10' - | 'o3-deep-research' - | 'o3-deep-research-2025-06-26' - | 'o4-mini-deep-research' - | 'o4-mini-deep-research-2025-06-26' - | 'computer-use-preview' - | 'computer-use-preview-2025-03-11' - ); + | 'o1-pro' + | 'o1-pro-2025-03-19' + | 'o3-pro' + | 'o3-pro-2025-06-10' + | 'o3-deep-research' + | 'o3-deep-research-2025-06-26' + | 'o4-mini-deep-research' + | 'o4-mini-deep-research-2025-06-26' + | 'computer-use-preview' + | 'computer-use-preview-2025-03-11'; export type ModelIdsShared = string | ChatModel; @@ -8746,6 +8892,7 @@ export type ModelResponseProperties = { * A stable identifier for your end-users. * Used to boost cache hit rates by better bucketing similar requests and to help OpenAI detect and prevent abuse. [Learn more](https://platform.openai.com/docs/guides/safety-best-practices#safety-identifiers). * + * * @deprecated */ user?: string; @@ -8855,6 +9002,7 @@ export type ModifyThreadRequest = { /** * Move + * * A mouse move action. * */ @@ -8879,6 +9027,7 @@ export type Move = { /** * OpenAIFile + * * The `File` object represents a document that has been uploaded to OpenAI. */ export type OpenAiFile = { @@ -8920,11 +9069,13 @@ export type OpenAiFile = { | 'user_data'; /** * Deprecated. The current status of the file, which can be either `uploaded`, `processed`, or `error`. + * * @deprecated */ status: 'uploaded' | 'processed' | 'error'; /** * Deprecated. For details on why a fine-tuning training file failed validation, see the `error` field on `fine_tuning.job`. + * * @deprecated */ status_details?: string; @@ -8932,6 +9083,7 @@ export type OpenAiFile = { /** * Other Chunking Strategy + * * This is returned when the chunking strategy is unknown. Typically, this is because the file was indexed before the `chunking_strategy` concept was introduced in the API. */ export type OtherChunkingStrategyResponseParam = { @@ -8943,6 +9095,7 @@ export type OtherChunkingStrategyResponseParam = { /** * Output audio + * * An audio output from the model. * */ @@ -9015,6 +9168,7 @@ export type OutputItem = /** * Output message + * * An output message from the model. * */ @@ -9065,6 +9219,7 @@ export type PartialImages = number; /** * Static Content + * * Static predicted output content, such as the content of a text file that is * being regenerated. * @@ -10103,7 +10258,7 @@ export type RealtimeResponseCreateParams = { * will not add items to default conversation. * */ - conversation?: string | ('auto' | 'none'); + conversation?: string | 'auto' | 'none'; /** * Input items to include in the prompt for the model. Using this field * creates a new context for this Response instead of using the default @@ -11489,6 +11644,7 @@ export type RealtimeSession = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -11735,6 +11891,7 @@ export type RealtimeSessionCreateRequest = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -11950,6 +12107,7 @@ export type RealtimeSessionCreateResponse = { }>; /** * Tracing Configuration + * * Configuration options for tracing. Set to null to disable tracing. Once * tracing is enabled for a session, the configuration cannot be modified. * @@ -12261,6 +12419,7 @@ export type RealtimeTranscriptionSessionCreateResponse = { /** * Reasoning + * * **gpt-5 and o-series models only** * * Configuration options for @@ -12276,6 +12435,7 @@ export type Reasoning = { * useful for debugging and understanding the model's reasoning process. * One of `auto`, `concise`, or `detailed`. * + * * @deprecated */ generate_summary?: 'auto' | 'concise' | 'detailed'; @@ -12316,6 +12476,7 @@ export type ReasoningEffort = /** * Reasoning + * * A description of the chain of thought used by a reasoning model while generating * a response. Be sure to include these items in your `input` to the Responses API * for subsequent turns of a conversation if you are manually @@ -12761,6 +12922,7 @@ export type ResponseCreatedEvent = { /** * ResponseCustomToolCallInputDelta + * * Event representing a delta (partial update) to the input of a custom tool call. * */ @@ -12789,6 +12951,7 @@ export type ResponseCustomToolCallInputDeltaEvent = { /** * ResponseCustomToolCallInputDone + * * Event indicating that input for a custom tool call is complete. * */ @@ -12988,6 +13151,7 @@ export type ResponseFileSearchCallSearchingEvent = { /** * JSON object + * * JSON object response format. An older method of generating JSON responses. * Using `json_schema` is recommended for models that support it. Note that the * model will not generate JSON without a system or user message instructing it @@ -13003,6 +13167,7 @@ export type ResponseFormatJsonObject = { /** * JSON schema + * * JSON Schema response format. Used to generate structured JSON responses. * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). * @@ -13010,6 +13175,7 @@ export type ResponseFormatJsonObject = { export type ResponseFormatJsonSchema = { /** * JSON schema + * * Structured Outputs configuration options, including a JSON Schema. * */ @@ -13045,6 +13211,7 @@ export type ResponseFormatJsonSchema = { /** * JSON schema + * * The schema for the response format, described as a JSON Schema object. * Learn how to build JSON schemas [here](https://json-schema.org/). * @@ -13055,6 +13222,7 @@ export type ResponseFormatJsonSchemaSchema = { /** * Text + * * Default response format. Used to generate text responses. * */ @@ -13067,6 +13235,7 @@ export type ResponseFormatText = { /** * Text grammar + * * A custom grammar for the model to follow when generating text. * Learn more in the [custom grammars guide](https://platform.openai.com/docs/guides/custom-grammars). * @@ -13084,6 +13253,7 @@ export type ResponseFormatTextGrammar = { /** * Python grammar + * * Configure the model to generate valid Python code. See the * [custom grammars guide](https://platform.openai.com/docs/guides/custom-grammars) for more details. * @@ -13150,6 +13320,7 @@ export type ResponseFunctionCallArgumentsDoneEvent = { /** * ResponseImageGenCallCompletedEvent + * * Emitted when an image generation tool call has completed and the final image is available. * */ @@ -13174,6 +13345,7 @@ export type ResponseImageGenCallCompletedEvent = { /** * ResponseImageGenCallGeneratingEvent + * * Emitted when an image generation tool call is actively generating an image (intermediate state). * */ @@ -13198,6 +13370,7 @@ export type ResponseImageGenCallGeneratingEvent = { /** * ResponseImageGenCallInProgressEvent + * * Emitted when an image generation tool call is in progress. * */ @@ -13222,6 +13395,7 @@ export type ResponseImageGenCallInProgressEvent = { /** * ResponseImageGenCallPartialImageEvent + * * Emitted when a partial image is available during image generation streaming. * */ @@ -13353,6 +13527,7 @@ export type ResponseLogProb = { /** * ResponseMCPCallArgumentsDeltaEvent + * * Emitted when there is a delta (partial update) to the arguments of an MCP tool call. * */ @@ -13382,6 +13557,7 @@ export type ResponseMcpCallArgumentsDeltaEvent = { /** * ResponseMCPCallArgumentsDoneEvent + * * Emitted when the arguments for an MCP tool call are finalized. * */ @@ -13411,6 +13587,7 @@ export type ResponseMcpCallArgumentsDoneEvent = { /** * ResponseMCPCallCompletedEvent + * * Emitted when an MCP tool call has completed successfully. * */ @@ -13435,6 +13612,7 @@ export type ResponseMcpCallCompletedEvent = { /** * ResponseMCPCallFailedEvent + * * Emitted when an MCP tool call has failed. * */ @@ -13459,6 +13637,7 @@ export type ResponseMcpCallFailedEvent = { /** * ResponseMCPCallInProgressEvent + * * Emitted when an MCP tool call is in progress. * */ @@ -13483,6 +13662,7 @@ export type ResponseMcpCallInProgressEvent = { /** * ResponseMCPListToolsCompletedEvent + * * Emitted when the list of available MCP tools has been successfully retrieved. * */ @@ -13507,6 +13687,7 @@ export type ResponseMcpListToolsCompletedEvent = { /** * ResponseMCPListToolsFailedEvent + * * Emitted when the attempt to list available MCP tools has failed. * */ @@ -13531,6 +13712,7 @@ export type ResponseMcpListToolsFailedEvent = { /** * ResponseMCPListToolsInProgressEvent + * * Emitted when the system is in the process of retrieving the list of available MCP tools. * */ @@ -13622,6 +13804,7 @@ export type ResponseOutputItemDoneEvent = { /** * ResponseOutputTextAnnotationAddedEvent + * * Emitted when an annotation is added to output text content. * */ @@ -13660,6 +13843,7 @@ export type ResponseOutputTextAnnotationAddedEvent = { /** * Prompt Variables + * * Optional map of values to substitute in for variables in your * prompt. The substitution values can either be strings, or other * Response input types like images or files. @@ -13764,6 +13948,7 @@ export type ResponseProperties = { /** * ResponseQueuedEvent + * * Emitted when a response is queued and waiting to be processed. * */ @@ -14517,7 +14702,6 @@ export type RunGraderRequest = { * The `output_json` variable will be populated if the model sample is a * valid JSON string. * - * */ model_sample: string; }; @@ -14560,6 +14744,7 @@ export type RunGraderResponse = { /** * A run on a thread + * * Represents an execution run on a [thread](https://platform.openai.com/docs/api-reference/threads). */ export type RunObject = { @@ -14702,6 +14887,7 @@ export type RunStepCompletionUsage = { /** * Run step delta object + * * Represents a run step delta i.e. any changed fields on a run step during streaming. * */ @@ -14719,6 +14905,7 @@ export type RunStepDeltaObject = { /** * Message creation + * * Details of the message creation by the run step. */ export type RunStepDeltaStepDetailsMessageCreationObject = { @@ -14736,6 +14923,7 @@ export type RunStepDeltaStepDetailsMessageCreationObject = { /** * Code interpreter tool call + * * Details of the Code Interpreter tool call the run step was involved in. */ export type RunStepDeltaStepDetailsToolCallsCodeObject = { @@ -14795,6 +14983,7 @@ export type RunStepDeltaStepDetailsToolCallsCodeOutputImageObject = { /** * Code interpreter log output + * * Text output from the Code Interpreter tool call as part of a run step. */ export type RunStepDeltaStepDetailsToolCallsCodeOutputLogsObject = { @@ -14873,6 +15062,7 @@ export type RunStepDeltaStepDetailsToolCallsFunctionObject = { /** * Tool calls + * * Details of the tool call. */ export type RunStepDeltaStepDetailsToolCallsObject = { @@ -14889,6 +15079,7 @@ export type RunStepDeltaStepDetailsToolCallsObject = { /** * Message creation + * * Details of the message creation by the run step. */ export type RunStepDetailsMessageCreationObject = { @@ -14906,6 +15097,7 @@ export type RunStepDetailsMessageCreationObject = { /** * Code Interpreter tool call + * * Details of the Code Interpreter tool call the run step was involved in. */ export type RunStepDetailsToolCallsCodeObject = { @@ -14957,6 +15149,7 @@ export type RunStepDetailsToolCallsCodeOutputImageObject = { /** * Code Interpreter log output + * * Text output from the Code Interpreter tool call as part of a run step. */ export type RunStepDetailsToolCallsCodeOutputLogsObject = { @@ -14996,6 +15189,7 @@ export type RunStepDetailsToolCallsFileSearchObject = { /** * File search tool call ranking options + * * The ranking options for the file search. */ export type RunStepDetailsToolCallsFileSearchRankingOptionsObject = { @@ -15008,6 +15202,7 @@ export type RunStepDetailsToolCallsFileSearchRankingOptionsObject = { /** * File search tool call result + * * A result instance of the file search. */ export type RunStepDetailsToolCallsFileSearchResultObject = { @@ -15071,6 +15266,7 @@ export type RunStepDetailsToolCallsFunctionObject = { /** * Tool calls + * * Details of the tool call. */ export type RunStepDetailsToolCallsObject = { @@ -15087,6 +15283,7 @@ export type RunStepDetailsToolCallsObject = { /** * Run steps + * * Represents a step in execution of a run. * */ @@ -15267,6 +15464,7 @@ export type RunToolCallObject = { /** * Screenshot + * * A screenshot action. * */ @@ -15281,6 +15479,7 @@ export type Screenshot = { /** * Scroll + * * A scroll action. * */ @@ -15404,6 +15603,7 @@ export type StaticChunkingStrategy = { /** * Static Chunking Strategy + * * Customize your own chunking strategy by setting chunk size and chunk overlap. */ export type StaticChunkingStrategyRequestParam = { @@ -15484,6 +15684,7 @@ export type TextResponseFormatConfiguration = /** * JSON schema + * * JSON Schema response format. Used to generate structured JSON responses. * Learn more about [Structured Outputs](https://platform.openai.com/docs/guides/structured-outputs). * @@ -15519,6 +15720,7 @@ export type TextResponseFormatJsonSchema = { /** * Thread + * * Represents a thread that contains [messages](https://platform.openai.com/docs/api-reference/messages). */ export type ThreadObject = { @@ -15608,6 +15810,7 @@ export type Tool = /** * Allowed tools + * * Constrains the tools available to the model to a pre-defined set. * */ @@ -15646,6 +15849,7 @@ export type ToolChoiceAllowed = { /** * Custom tool + * * Use this option to force the model to call a specific custom tool. * */ @@ -15662,6 +15866,7 @@ export type ToolChoiceCustom = { /** * Function tool + * * Use this option to force the model to call a specific function. * */ @@ -15678,6 +15883,7 @@ export type ToolChoiceFunction = { /** * MCP tool + * * Use this option to force the model to call a specific tool on a remote MCP server. * */ @@ -15700,6 +15906,7 @@ export type ToolChoiceMcp = { /** * Tool choice mode + * * Controls which (if any) tool is called by the model. * * `none` means the model will not call any tool and instead generates a message. @@ -15718,6 +15925,7 @@ export const ToolChoiceOptions = { /** * Tool choice mode + * * Controls which (if any) tool is called by the model. * * `none` means the model will not call any tool and instead generates a message. @@ -15733,6 +15941,7 @@ export type ToolChoiceOptions = /** * Hosted tool + * * Indicates that the model should use a built-in tool to generate a response. * [Learn more about built-in tools](https://platform.openai.com/docs/guides/tools). * @@ -15836,6 +16045,7 @@ export type TranscriptTextDoneEvent = { /** * Duration Usage + * * Usage statistics for models billed by audio input duration. */ export type TranscriptTextUsageDuration = { @@ -15851,6 +16061,7 @@ export type TranscriptTextUsageDuration = { /** * Token Usage + * * Usage statistics for models billed by token usage. */ export type TranscriptTextUsageTokens = { @@ -15957,6 +16168,7 @@ export type TranscriptionWord = { /** * Thread Truncation Controls + * * Controls for how a thread will be truncated prior to the run. Use this to control the initial context window of the run. */ export type TruncationObject = { @@ -15972,6 +16184,7 @@ export type TruncationObject = { /** * Type + * * An action to type in text. * */ @@ -16004,6 +16217,7 @@ export type UpdateVectorStoreRequest = { /** * Upload + * * The Upload object can accept byte chunks in the form of Parts. * */ @@ -16056,6 +16270,7 @@ export type UploadCertificateRequest = { /** * UploadPart + * * The upload Part represents a chunk of bytes we can add to an Upload object. * */ @@ -16474,6 +16689,7 @@ export type ValidateGraderResponse = { /** * Vector store expiration policy + * * The expiration policy for a vector store. */ export type VectorStoreExpirationAfter = { @@ -16501,6 +16717,7 @@ export type VectorStoreFileAttributes = { /** * Vector store file batch + * * A batch of files attached to a vector store. */ export type VectorStoreFileBatchObject = { @@ -16581,6 +16798,7 @@ export type VectorStoreFileContentResponse = { /** * Vector store files + * * A list of files attached to a vector store. */ export type VectorStoreFileObject = { @@ -16627,6 +16845,7 @@ export type VectorStoreFileObject = { /** * Vector store + * * A vector store is a collection of processed files can be used by the `file_search` tool. */ export type VectorStoreObject = { @@ -16790,19 +17009,18 @@ export type Verbosity = (typeof Verbosity)[keyof typeof Verbosity]; export type VoiceIdsShared = | string - | ( - | 'alloy' - | 'ash' - | 'ballad' - | 'coral' - | 'echo' - | 'sage' - | 'shimmer' - | 'verse' - ); + | 'alloy' + | 'ash' + | 'ballad' + | 'coral' + | 'echo' + | 'sage' + | 'shimmer' + | 'verse'; /** * Wait + * * A wait action. * */ @@ -16817,6 +17035,7 @@ export type Wait = { /** * Find action + * * Action type "find": Searches for a pattern within a loaded page. * */ @@ -16840,6 +17059,7 @@ export type WebSearchActionFind = { /** * Open page action + * * Action type "open_page" - Opens a specific URL from search results. * */ @@ -16858,6 +17078,7 @@ export type WebSearchActionOpenPage = { /** * Search action + * * Action type "search" - Performs a web search query. * */ @@ -16895,6 +17116,7 @@ export type WebSearchContextSize = /** * Web search location + * * Approximate location parameters for the search. */ export type WebSearchLocation = { @@ -16925,6 +17147,7 @@ export type WebSearchLocation = { /** * Web search tool call + * * The results of a web search tool call. See the * [web search guide](https://platform.openai.com/docs/guides/tools-web-search) for more information. * @@ -16964,6 +17187,7 @@ export type WebSearchToolCall = { /** * batch.cancelled + * * Sent when a batch API request has been cancelled. * */ @@ -17003,6 +17227,7 @@ export type WebhookBatchCancelled = { /** * batch.completed + * * Sent when a batch API request has been completed. * */ @@ -17042,6 +17267,7 @@ export type WebhookBatchCompleted = { /** * batch.expired + * * Sent when a batch API request has expired. * */ @@ -17081,6 +17307,7 @@ export type WebhookBatchExpired = { /** * batch.failed + * * Sent when a batch API request has failed. * */ @@ -17120,6 +17347,7 @@ export type WebhookBatchFailed = { /** * eval.run.canceled + * * Sent when an eval run has been canceled. * */ @@ -17159,6 +17387,7 @@ export type WebhookEvalRunCanceled = { /** * eval.run.failed + * * Sent when an eval run has failed. * */ @@ -17198,6 +17427,7 @@ export type WebhookEvalRunFailed = { /** * eval.run.succeeded + * * Sent when an eval run has succeeded. * */ @@ -17237,6 +17467,7 @@ export type WebhookEvalRunSucceeded = { /** * fine_tuning.job.cancelled + * * Sent when a fine-tuning job has been cancelled. * */ @@ -17276,6 +17507,7 @@ export type WebhookFineTuningJobCancelled = { /** * fine_tuning.job.failed + * * Sent when a fine-tuning job has failed. * */ @@ -17315,6 +17547,7 @@ export type WebhookFineTuningJobFailed = { /** * fine_tuning.job.succeeded + * * Sent when a fine-tuning job has succeeded. * */ @@ -17354,6 +17587,7 @@ export type WebhookFineTuningJobSucceeded = { /** * response.cancelled + * * Sent when a background response has been cancelled. * */ @@ -17393,6 +17627,7 @@ export type WebhookResponseCancelled = { /** * response.completed + * * Sent when a background response has been completed. * */ @@ -17432,6 +17667,7 @@ export type WebhookResponseCompleted = { /** * response.failed + * * Sent when a background response has failed. * */ @@ -17471,6 +17707,7 @@ export type WebhookResponseFailed = { /** * response.incomplete + * * Sent when a background response has been interrupted. * */ @@ -17510,6 +17747,7 @@ export type WebhookResponseIncomplete = { /** * Input text + * * A text input to the model. */ export type InputTextContent = { @@ -17525,6 +17763,7 @@ export type InputTextContent = { /** * Input image + * * An image input to the model. Learn about [image inputs](https://platform.openai.com/docs/guides/vision). */ export type InputImageContent = { @@ -17542,6 +17781,7 @@ export type InputImageContent = { /** * Input file + * * A file input to the model. */ export type InputFileContent = { @@ -17567,6 +17807,7 @@ export type InputFileContent = { /** * Function + * * Defines a function in your own code the model can choose to call. Learn more about [function calling](https://platform.openai.com/docs/guides/function-calling). */ export type FunctionTool = { @@ -17600,6 +17841,7 @@ export type Filters = ComparisonFilter | CompoundFilter; /** * File search + * * A tool that searches for relevant content from uploaded files. Learn more about the [file search tool](https://platform.openai.com/docs/guides/tools-file-search). */ export type FileSearchTool = { @@ -17635,6 +17877,7 @@ export type ApproximateLocation = { /** * Web search preview + * * This tool searches the web for relevant results to use in a response. Learn more about the [web search tool](https://platform.openai.com/docs/guides/tools-web-search). */ export type WebSearchPreviewTool = { @@ -17651,6 +17894,7 @@ export type WebSearchPreviewTool = { /** * Computer use preview + * * A tool that controls a virtual computer. Learn more about the [computer tool](https://platform.openai.com/docs/guides/tools-computer-use). */ export type ComputerUsePreviewTool = { @@ -17674,6 +17918,7 @@ export type ComputerUsePreviewTool = { /** * Input usage details + * * The input tokens detailed information for the image generation. */ export type ImageGenInputUsageDetails = { @@ -17689,6 +17934,7 @@ export type ImageGenInputUsageDetails = { /** * Image generation usage + * * For `gpt-image-1` only, the token usage information for the image generation. */ export type ImageGenUsage = { @@ -17709,6 +17955,7 @@ export type ImageGenUsage = { /** * File citation + * * A citation to a file. */ export type FileCitationBody = { @@ -17732,6 +17979,7 @@ export type FileCitationBody = { /** * URL citation + * * A citation for a web resource used to generate a model response. */ export type UrlCitationBody = { @@ -17759,6 +18007,7 @@ export type UrlCitationBody = { /** * Container file citation + * * A citation for a container file used to generate a model response. */ export type ContainerFileCitationBody = { @@ -17804,6 +18053,7 @@ export type Annotation = /** * Top log probability + * * The top log probability of a token. */ export type TopLogProb = { @@ -17814,6 +18064,7 @@ export type TopLogProb = { /** * Log probability + * * The log probability of a token. */ export type LogProb = { @@ -17825,6 +18076,7 @@ export type LogProb = { /** * Output text + * * A text output from the model. */ export type OutputTextContent = { @@ -17845,6 +18097,7 @@ export type OutputTextContent = { /** * Refusal + * * A refusal from the model. */ export type RefusalContent = { @@ -17872,6 +18125,7 @@ export type ComputerCallSafetyCheckParam = { /** * Computer tool call output + * * The output of a computer tool call. */ export type ComputerCallOutputItemParam = { @@ -17882,7 +18136,7 @@ export type ComputerCallOutputItemParam = { call_id: string; id?: string | null; output: ComputerScreenshotImage; - status?: ('in_progress' | 'completed' | 'incomplete') | null; + status?: 'in_progress' | 'completed' | 'incomplete' | null; /** * The type of the computer tool call output. Always `computer_call_output`. */ @@ -17891,6 +18145,7 @@ export type ComputerCallOutputItemParam = { /** * Function tool call output + * * The output of a function tool call. */ export type FunctionCallOutputItemParam = { @@ -17903,7 +18158,7 @@ export type FunctionCallOutputItemParam = { * A JSON string of the output of the function tool call. */ output: string; - status?: ('in_progress' | 'completed' | 'incomplete') | null; + status?: 'in_progress' | 'completed' | 'incomplete' | null; /** * The type of the function tool call output. Always `function_call_output`. */ @@ -17912,6 +18167,7 @@ export type FunctionCallOutputItemParam = { /** * Item reference + * * An internal identifier for an item to reference. */ export type ItemReferenceParam = { @@ -18137,22 +18393,22 @@ export type MessageContentDelta = export const ChatModel = { CHATGPT_4O_LATEST: 'chatgpt-4o-latest', CODEX_MINI_LATEST: 'codex-mini-latest', - GPT_3_5_TURBO: 'gpt-3.5-turbo', - GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', - GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', - GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', - GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', - GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', - GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', GPT_4: 'gpt-4', GPT_4O: 'gpt-4o', GPT_4O_2024_05_13: 'gpt-4o-2024-05-13', + GPT_3_5_TURBO: 'gpt-3.5-turbo', GPT_4O_2024_08_06: 'gpt-4o-2024-08-06', + GPT_3_5_TURBO_0301: 'gpt-3.5-turbo-0301', GPT_4O_2024_11_20: 'gpt-4o-2024-11-20', + GPT_3_5_TURBO_0613: 'gpt-3.5-turbo-0613', GPT_4O_AUDIO_PREVIEW: 'gpt-4o-audio-preview', + GPT_3_5_TURBO_0125: 'gpt-3.5-turbo-0125', GPT_4O_AUDIO_PREVIEW_2024_10_01: 'gpt-4o-audio-preview-2024-10-01', + GPT_3_5_TURBO_1106: 'gpt-3.5-turbo-1106', GPT_4O_AUDIO_PREVIEW_2024_12_17: 'gpt-4o-audio-preview-2024-12-17', + GPT_3_5_TURBO_16K: 'gpt-3.5-turbo-16k', GPT_4O_AUDIO_PREVIEW_2025_06_03: 'gpt-4o-audio-preview-2025-06-03', + GPT_3_5_TURBO_16K_0613: 'gpt-3.5-turbo-16k-0613', GPT_4O_MINI: 'gpt-4o-mini', GPT_4O_MINI_2024_07_18: 'gpt-4o-mini-2024-07-18', GPT_4O_MINI_AUDIO_PREVIEW: 'gpt-4o-mini-audio-preview', @@ -18162,28 +18418,28 @@ export const ChatModel = { 'gpt-4o-mini-search-preview-2025-03-11', GPT_4O_SEARCH_PREVIEW: 'gpt-4o-search-preview', GPT_4O_SEARCH_PREVIEW_2025_03_11: 'gpt-4o-search-preview-2025-03-11', - GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', - GPT_4_0314: 'gpt-4-0314', - GPT_4_0613: 'gpt-4-0613', GPT_4_1: 'gpt-4.1', - GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', + GPT_4_0125_PREVIEW: 'gpt-4-0125-preview', GPT_4_1_2025_04_14: 'gpt-4.1-2025-04-14', + GPT_4_0314: 'gpt-4-0314', GPT_4_1_MINI: 'gpt-4.1-mini', + GPT_4_0613: 'gpt-4-0613', GPT_4_1_MINI_2025_04_14: 'gpt-4.1-mini-2025-04-14', + GPT_4_1106_PREVIEW: 'gpt-4-1106-preview', GPT_4_1_NANO: 'gpt-4.1-nano', GPT_4_1_NANO_2025_04_14: 'gpt-4.1-nano-2025-04-14', GPT_4_32K: 'gpt-4-32k', GPT_4_32K_0314: 'gpt-4-32k-0314', - GPT_4_32K_0613: 'gpt-4-32k-0613', - GPT_4_TURBO: 'gpt-4-turbo', - GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', - GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', - GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', GPT_5: 'gpt-5', + GPT_4_32K_0613: 'gpt-4-32k-0613', GPT_5_2025_08_07: 'gpt-5-2025-08-07', + GPT_4_TURBO: 'gpt-4-turbo', GPT_5_CHAT_LATEST: 'gpt-5-chat-latest', + GPT_4_TURBO_2024_04_09: 'gpt-4-turbo-2024-04-09', GPT_5_MINI: 'gpt-5-mini', + GPT_4_TURBO_PREVIEW: 'gpt-4-turbo-preview', GPT_5_MINI_2025_08_07: 'gpt-5-mini-2025-08-07', + GPT_4_VISION_PREVIEW: 'gpt-4-vision-preview', GPT_5_NANO: 'gpt-5-nano', GPT_5_NANO_2025_08_07: 'gpt-5-nano-2025-08-07', O1: 'o1', @@ -18227,46 +18483,44 @@ export type CreateThreadAndRunRequestWithoutStream = { */ model?: | string - | ( - | 'gpt-5' - | 'gpt-5-mini' - | 'gpt-5-nano' - | 'gpt-5-2025-08-07' - | 'gpt-5-mini-2025-08-07' - | 'gpt-5-nano-2025-08-07' - | 'gpt-4.1' - | 'gpt-4.1-mini' - | 'gpt-4.1-nano' - | 'gpt-4.1-2025-04-14' - | 'gpt-4.1-mini-2025-04-14' - | 'gpt-4.1-nano-2025-04-14' - | 'gpt-4o' - | 'gpt-4o-2024-11-20' - | 'gpt-4o-2024-08-06' - | 'gpt-4o-2024-05-13' - | 'gpt-4o-mini' - | 'gpt-4o-mini-2024-07-18' - | 'gpt-4.5-preview' - | 'gpt-4.5-preview-2025-02-27' - | 'gpt-4-turbo' - | 'gpt-4-turbo-2024-04-09' - | 'gpt-4-0125-preview' - | 'gpt-4-turbo-preview' - | 'gpt-4-1106-preview' - | 'gpt-4-vision-preview' - | 'gpt-4' - | 'gpt-4-0314' - | 'gpt-4-0613' - | 'gpt-4-32k' - | 'gpt-4-32k-0314' - | 'gpt-4-32k-0613' - | 'gpt-3.5-turbo' - | 'gpt-3.5-turbo-16k' - | 'gpt-3.5-turbo-0613' - | 'gpt-3.5-turbo-1106' - | 'gpt-3.5-turbo-0125' - | 'gpt-3.5-turbo-16k-0613' - ); + | 'gpt-5' + | 'gpt-5-mini' + | 'gpt-5-nano' + | 'gpt-5-2025-08-07' + | 'gpt-5-mini-2025-08-07' + | 'gpt-5-nano-2025-08-07' + | 'gpt-4.1' + | 'gpt-4.1-mini' + | 'gpt-4.1-nano' + | 'gpt-4.1-2025-04-14' + | 'gpt-4.1-mini-2025-04-14' + | 'gpt-4.1-nano-2025-04-14' + | 'gpt-4o' + | 'gpt-4o-2024-11-20' + | 'gpt-4o-2024-08-06' + | 'gpt-4o-2024-05-13' + | 'gpt-4o-mini' + | 'gpt-4o-mini-2024-07-18' + | 'gpt-4.5-preview' + | 'gpt-4.5-preview-2025-02-27' + | 'gpt-4-turbo' + | 'gpt-4-turbo-2024-04-09' + | 'gpt-4-0125-preview' + | 'gpt-4-turbo-preview' + | 'gpt-4-1106-preview' + | 'gpt-4-vision-preview' + | 'gpt-4' + | 'gpt-4-0314' + | 'gpt-4-0613' + | 'gpt-4-32k' + | 'gpt-4-32k-0314' + | 'gpt-4-32k-0613' + | 'gpt-3.5-turbo' + | 'gpt-3.5-turbo-16k' + | 'gpt-3.5-turbo-0613' + | 'gpt-3.5-turbo-1106' + | 'gpt-3.5-turbo-0125' + | 'gpt-3.5-turbo-16k-0613'; parallel_tool_calls?: ParallelToolCalls; response_format?: AssistantsApiResponseFormatOption; /** @@ -23055,6 +23309,172 @@ export type SearchVectorStoreResponses = { export type SearchVectorStoreResponse = SearchVectorStoreResponses[keyof SearchVectorStoreResponses]; -export type ClientOptions = { - baseUrl: 'https://api.openai.com/v1' | (string & {}); +/** + * The event payload sent by the API. + */ +export type PostBatchCancelledWebhookPayload = WebhookBatchCancelled; + +export type PostBatchCancelledWebhookRequest = { + body: PostBatchCancelledWebhookPayload; + key: 'batch_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchCompletedWebhookPayload = WebhookBatchCompleted; + +export type PostBatchCompletedWebhookRequest = { + body: PostBatchCompletedWebhookPayload; + key: 'batch_completed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchExpiredWebhookPayload = WebhookBatchExpired; + +export type PostBatchExpiredWebhookRequest = { + body: PostBatchExpiredWebhookPayload; + key: 'batch_expired'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostBatchFailedWebhookPayload = WebhookBatchFailed; + +export type PostBatchFailedWebhookRequest = { + body: PostBatchFailedWebhookPayload; + key: 'batch_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunCanceledWebhookPayload = WebhookEvalRunCanceled; + +export type PostEvalRunCanceledWebhookRequest = { + body: PostEvalRunCanceledWebhookPayload; + key: 'eval_run_canceled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunFailedWebhookPayload = WebhookEvalRunFailed; + +export type PostEvalRunFailedWebhookRequest = { + body: PostEvalRunFailedWebhookPayload; + key: 'eval_run_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostEvalRunSucceededWebhookPayload = WebhookEvalRunSucceeded; + +export type PostEvalRunSucceededWebhookRequest = { + body: PostEvalRunSucceededWebhookPayload; + key: 'eval_run_succeeded'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobCancelledWebhookPayload = + WebhookFineTuningJobCancelled; + +export type PostFineTuningJobCancelledWebhookRequest = { + body: PostFineTuningJobCancelledWebhookPayload; + key: 'fine_tuning_job_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobFailedWebhookPayload = WebhookFineTuningJobFailed; + +export type PostFineTuningJobFailedWebhookRequest = { + body: PostFineTuningJobFailedWebhookPayload; + key: 'fine_tuning_job_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostFineTuningJobSucceededWebhookPayload = + WebhookFineTuningJobSucceeded; + +export type PostFineTuningJobSucceededWebhookRequest = { + body: PostFineTuningJobSucceededWebhookPayload; + key: 'fine_tuning_job_succeeded'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseCancelledWebhookPayload = WebhookResponseCancelled; + +export type PostResponseCancelledWebhookRequest = { + body: PostResponseCancelledWebhookPayload; + key: 'response_cancelled'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseCompletedWebhookPayload = WebhookResponseCompleted; + +export type PostResponseCompletedWebhookRequest = { + body: PostResponseCompletedWebhookPayload; + key: 'response_completed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseFailedWebhookPayload = WebhookResponseFailed; + +export type PostResponseFailedWebhookRequest = { + body: PostResponseFailedWebhookPayload; + key: 'response_failed'; + path?: never; + query?: never; +}; + +/** + * The event payload sent by the API. + */ +export type PostResponseIncompleteWebhookPayload = WebhookResponseIncomplete; + +export type PostResponseIncompleteWebhookRequest = { + body: PostResponseIncompleteWebhookPayload; + key: 'response_incomplete'; + path?: never; + query?: never; }; diff --git a/examples/openapi-ts-pinia-colada/.eslintrc.cjs b/examples/openapi-ts-pinia-colada/.eslintrc.cjs index ff3a044fa..491d6ae84 100644 --- a/examples/openapi-ts-pinia-colada/.eslintrc.cjs +++ b/examples/openapi-ts-pinia-colada/.eslintrc.cjs @@ -10,6 +10,5 @@ module.exports = { ], parserOptions: { ecmaVersion: 'latest' - }, - root: true + } } diff --git a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts index 1564abf60..921fe1fc4 100644 --- a/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts +++ b/examples/openapi-ts-pinia-colada/src/client/@pinia/colada.gen.ts @@ -2,8 +2,8 @@ import { type _JSONValue, defineQueryOptions, type UseMutationOptions } from '@pinia/colada' +import { serializeQueryKeyValue } from '../client' import { client } from '../client.gen' -import { serializeQueryKeyValue } from '../core/queryKeySerializer.gen' import { addPet, createUser, @@ -57,6 +57,7 @@ import type { /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -74,6 +75,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -90,7 +92,7 @@ export const updatePetMutation = ( }) export type QueryKey = [ - Pick & { + Pick & { _id: string baseUrl?: _JSONValue body?: _JSONValue @@ -131,6 +133,7 @@ const createQueryKey = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusQuery = defineQueryOptions( @@ -149,6 +152,7 @@ export const findPetsByStatusQuery = defineQueryOptions( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTagsQuery = defineQueryOptions((options: Options) => ({ @@ -165,6 +169,7 @@ export const findPetsByTagsQuery = defineQueryOptions((options: Options) => ({ @@ -198,6 +204,7 @@ export const getPetByIdQuery = defineQueryOptions((options: Options) => ({ @@ -248,6 +257,7 @@ export const getInventoryQuery = defineQueryOptions((options?: Options 10. Other values will generate exceptions. */ export const getOrderByIdQuery = defineQueryOptions((options: Options) => ({ @@ -298,6 +310,7 @@ export const getOrderByIdQuery = defineQueryOptions((options: Options) => ({ @@ -352,6 +367,7 @@ export const loginUserQuery = defineQueryOptions((options?: Options) => ({ @@ -368,6 +384,7 @@ export const logoutUserQuery = defineQueryOptions((options?: Options) => ({ @@ -401,6 +419,7 @@ export const getUserByNameQuery = defineQueryOptions((options: Options( @@ -103,6 +104,7 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( @@ -125,6 +127,7 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( @@ -143,6 +146,7 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( @@ -161,6 +165,7 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( @@ -179,6 +184,7 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( @@ -201,6 +207,7 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( @@ -223,6 +230,7 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( @@ -246,6 +254,7 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( @@ -264,6 +273,7 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( @@ -280,6 +290,7 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( @@ -292,6 +303,7 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( @@ -304,6 +316,7 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( @@ -320,6 +333,7 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( @@ -340,6 +354,7 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( @@ -352,6 +367,7 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( @@ -364,6 +380,7 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( @@ -376,6 +393,7 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( @@ -388,6 +406,7 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( diff --git a/examples/openapi-ts-sample/src/client/client.gen.ts b/examples/openapi-ts-sample/src/client/client.gen.ts index 25aa4ccc9..4e785697c 100644 --- a/examples/openapi-ts-sample/src/client/client.gen.ts +++ b/examples/openapi-ts-sample/src/client/client.gen.ts @@ -1,13 +1,13 @@ // This file is auto-generated by @hey-api/openapi-ts -import { createClientConfig } from '../hey-api'; import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import { createClientConfig } from './src/hey-api.ts'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -17,14 +17,13 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( createClientConfig( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ), diff --git a/examples/openapi-ts-sample/src/client/client/client.gen.ts b/examples/openapi-ts-sample/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-sample/src/client/client/index.ts b/examples/openapi-ts-sample/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-sample/src/client/client/index.ts +++ b/examples/openapi-ts-sample/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/types.ts b/examples/openapi-ts-sample/src/client/client/types.gen.ts similarity index 69% rename from examples/openapi-ts-tanstack-react-query/src/client/client/types.ts rename to examples/openapi-ts-sample/src/client/client/types.gen.ts index 75a2ffbbe..1a005b51e 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/types.ts +++ b/examples/openapi-ts-sample/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +42,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -128,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -152,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -181,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -195,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts b/examples/openapi-ts-sample/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts rename to examples/openapi-ts-sample/src/client/client/utils.gen.ts index bf3f28250..96de282a8 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/utils.ts +++ b/examples/openapi-ts-sample/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -182,6 +106,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -231,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -271,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -322,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-sample/src/client/core/auth.ts b/examples/openapi-ts-sample/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-sample/src/client/core/auth.ts rename to examples/openapi-ts-sample/src/client/core/auth.gen.ts index 451c7f30f..f8a73266f 100644 --- a/examples/openapi-ts-sample/src/client/core/auth.ts +++ b/examples/openapi-ts-sample/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined; export interface Auth { diff --git a/examples/openapi-ts-next/src/client/core/bodySerializer.ts b/examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts similarity index 82% rename from examples/openapi-ts-next/src/client/core/bodySerializer.ts rename to examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts index fab971b66..49cd8925e 100644 --- a/examples/openapi-ts-next/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-sample/src/client/core/bodySerializer.gen.ts @@ -1,8 +1,10 @@ +// This file is auto-generated by @hey-api/openapi-ts + import type { ArrayStyle, ObjectStyle, SerializerOptions, -} from './pathSerializer'; +} from './pathSerializer.gen'; export type QuerySerializer = (query: Record) => string; @@ -14,9 +16,15 @@ export interface QuerySerializerOptions { object?: SerializerOptions; } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } @@ -26,7 +34,7 @@ const serializeUrlSearchParamsPair = ( data: URLSearchParams, key: string, value: unknown, -) => { +): void => { if (typeof value === 'string') { data.append(key, value); } else { @@ -37,7 +45,7 @@ const serializeUrlSearchParamsPair = ( export const formDataBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): FormData => { const data = new FormData(); Object.entries(body).forEach(([key, value]) => { @@ -56,8 +64,8 @@ export const formDataBodySerializer = { }; export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => typeof value === 'bigint' ? value.toString() : value, ), }; @@ -65,7 +73,7 @@ export const jsonBodySerializer = { export const urlSearchParamsBodySerializer = { bodySerializer: | Array>>( body: T, - ) => { + ): string => { const data = new URLSearchParams(); Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-next/src/client/core/params.ts b/examples/openapi-ts-sample/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-next/src/client/core/params.ts rename to examples/openapi-ts-sample/src/client/core/params.gen.ts index 7559bbb8c..71c88e852 100644 --- a/examples/openapi-ts-next/src/client/core/params.ts +++ b/examples/openapi-ts-sample/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query'; export type Field = | { in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string; } | { in: Extract; + /** + * Key isn't required for bodies. + */ key?: string; map?: string; }; diff --git a/examples/openapi-ts-fastify/src/client/core/pathSerializer.ts b/examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-fastify/src/client/core/pathSerializer.ts rename to examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts index d692cf0a3..8d9993104 100644 --- a/examples/openapi-ts-fastify/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-sample/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} diff --git a/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-next/src/client/core/types.ts b/examples/openapi-ts-sample/src/client/core/types.gen.ts similarity index 68% rename from examples/openapi-ts-next/src/client/core/types.ts rename to examples/openapi-ts-sample/src/client/core/types.gen.ts index 1f8688099..643c070c9 100644 --- a/examples/openapi-ts-next/src/client/core/types.ts +++ b/examples/openapi-ts-sample/src/client/core/types.gen.ts @@ -1,33 +1,42 @@ -import type { Auth, AuthToken } from './auth'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; import type { BodySerializer, QuerySerializer, QuerySerializerOptions, -} from './bodySerializer'; +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; -export interface Client< +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -63,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -84,6 +84,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -96,3 +102,17 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise; } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-sample/src/client/core/utils.gen.ts b/examples/openapi-ts-sample/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-sample/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-sample/src/client/index.ts b/examples/openapi-ts-sample/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-sample/src/client/index.ts +++ b/examples/openapi-ts-sample/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-sample/src/client/sdk.gen.ts b/examples/openapi-ts-sample/src/client/sdk.gen.ts index d4d59335e..867b5a377 100644 --- a/examples/openapi-ts-sample/src/client/sdk.gen.ts +++ b/examples/openapi-ts-sample/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -100,7 +100,7 @@ import { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -116,16 +116,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ requestValidator: async (data) => await zAddPetData.parseAsync(data), responseValidator: async (data) => await zAddPetResponse.parseAsync(data), security: [ @@ -144,12 +141,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -173,12 +171,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -199,12 +198,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -225,12 +225,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -248,12 +249,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -277,12 +279,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -303,12 +306,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -333,12 +337,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -358,12 +363,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -381,12 +387,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -398,12 +405,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -417,12 +425,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -440,12 +449,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -464,12 +474,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -483,12 +494,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -500,12 +512,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -517,12 +530,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -536,12 +550,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-sample/src/client/types.gen.ts b/examples/openapi-ts-sample/src/client/types.gen.ts index 6d8a6e7b9..a2e6be0fa 100644 --- a/examples/openapi-ts-sample/src/client/types.gen.ts +++ b/examples/openapi-ts-sample/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-sample/src/client/zod.gen.ts b/examples/openapi-ts-sample/src/client/zod.gen.ts index 2cd4b3607..a4730a50d 100644 --- a/examples/openapi-ts-sample/src/client/zod.gen.ts +++ b/examples/openapi-ts-sample/src/client/zod.gen.ts @@ -79,11 +79,9 @@ export const zUpdatePetResponse = zPet; export const zFindPetsByStatusData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), - query: z.optional( - z.object({ - status: z.optional(z.enum(['available', 'pending', 'sold'])), - }), - ), + query: z.object({ + status: z.enum(['available', 'pending', 'sold']), + }), }); /** @@ -94,11 +92,9 @@ export const zFindPetsByStatusResponse = z.array(zPet); export const zFindPetsByTagsData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), - query: z.optional( - z.object({ - tags: z.optional(z.array(z.string())), - }), - ), + query: z.object({ + tags: z.array(z.string()), + }), }); /** @@ -176,7 +172,7 @@ export const zGetInventoryData = z.object({ /** * successful operation */ -export const zGetInventoryResponse = z.object({}); +export const zGetInventoryResponse = z.record(z.string(), z.int()); export const zPlaceOrderData = z.object({ body: z.optional(zOrder), diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts index 890b38180..4952a1ddf 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/@tanstack/angular-query-experimental.gen.ts @@ -6,7 +6,7 @@ import { queryOptions, } from '@tanstack/angular-query-experimental'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,70 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - tags?: ReadonlyArray; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, - tags?: ReadonlyArray, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: - options?.baseUrl || - (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (tags) { - params.tags = tags; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -132,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -146,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -156,10 +96,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -168,12 +108,53 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( @@ -197,6 +178,7 @@ export const findPetsByTagsQueryKey = (options: Options) => /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTagsOptions = (options: Options) => @@ -215,6 +197,7 @@ export const findPetsByTagsOptions = (options: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -225,10 +208,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -242,6 +225,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -258,32 +242,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -298,10 +259,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -310,29 +271,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -347,10 +288,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -364,6 +305,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -380,29 +322,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -417,10 +339,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -431,6 +353,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -441,10 +364,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -458,6 +381,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -474,29 +398,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -511,10 +415,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -523,32 +427,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -563,10 +444,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -580,6 +461,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -601,6 +483,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -619,6 +502,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -629,10 +513,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -646,6 +530,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -664,6 +549,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -674,10 +560,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts index 3fd4a2427..1555ca230 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/client.gen.ts @@ -16,7 +16,16 @@ import { import { firstValueFrom } from 'rxjs'; import { filter } from 'rxjs/operators'; -import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, + ResponseStyle, +} from './types.gen'; import { buildUrl, createConfig, @@ -50,13 +59,18 @@ export const createClient = (config: Config = {}): Client => { ResolvedRequestOptions >(); - const request: Client['request'] = async (options) => { + const requestOptions = < + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', + >( + options: RequestOptions, + ) => { const opts = { ..._config, ...options, headers: mergeHeaders(_config.headers, options.headers), httpClient: options.httpClient ?? _config.httpClient, - serializedBody: options.body as any, + serializedBody: undefined, }; if (!opts.httpClient) { @@ -65,69 +79,86 @@ export const createClient = (config: Config = {}): Client => { inject(HttpClient), ); } else { - assertInInjectionContext(request); + assertInInjectionContext(requestOptions); opts.httpClient = inject(HttpClient); } } - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body && opts.bodySerializer) { + if (opts.body !== undefined && opts.bodySerializer) { opts.serializedBody = opts.bodySerializer(opts.body); } // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { + if (opts.body === undefined || opts.serializedBody === '') { opts.headers.delete('Content-Type'); } - const url = buildUrl(opts); + const url = buildUrl(opts as any); - let req = new HttpRequest( - opts.method, + const req = new HttpRequest( + opts.method ?? 'GET', url, - opts.serializedBody || null, + getValidRequestBody(opts), { redirect: 'follow', ...opts, }, ); - for (const fn of interceptors.request._fns) { + return { opts, req, url }; + }; + + const beforeRequest = async (options: RequestOptions) => { + const { opts, req, url } = requestOptions(options); + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + return { opts, req, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, req: initialReq } = await beforeRequest(options); + + let req = initialReq; + + for (const fn of interceptors.request.fns) { if (fn) { - req = await fn(req, opts); + req = await fn(req, opts as any); } } - let response; - const result = { + const result: { + request: HttpRequest; + response: any; + } = { request: req, - response, + response: null, }; try { - response = await firstValueFrom( - opts.httpClient - .request(req) + result.response = (await firstValueFrom( + opts + .httpClient!.request(req) .pipe(filter((event) => event.type === HttpEventType.Response)), - ); + )) as HttpResponse; - for (const fn of interceptors.response._fns) { + for (const fn of interceptors.response.fns) { if (fn) { - response = await fn(response, req, opts); + result.response = await fn(result.response, req, opts as any); } } - let bodyResponse: any = response.body; + let bodyResponse = result.response.body; if (opts.responseValidator) { await opts.responseValidator(bodyResponse); @@ -142,18 +173,18 @@ export const createClient = (config: Config = {}): Client => { : { data: bodyResponse, ...result }; } catch (error) { if (error instanceof HttpErrorResponse) { - response = error; + result.response = error; } let finalError = error instanceof HttpErrorResponse ? error.error : error; - for (const fn of interceptors.error._fns) { + for (const fn of interceptors.error.fns) { if (fn) { finalError = (await fn( finalError, - response as HttpResponse, + result.response as any, req, - opts, + opts as any, )) as string; } } @@ -171,20 +202,60 @@ export const createClient = (config: Config = {}): Client => { } }; + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + url, + }); + }; + return { buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), + head: makeMethodFn('HEAD'), interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), request, + requestOptions: (options) => { + if (options.security) { + throw new Error('Security is not supported in requestOptions'); + } + + if (options.requestValidator) { + throw new Error( + 'Request validation is not supported in requestOptions', + ); + } + + return requestOptions(options).req; + }, setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; }; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts index 318a84b6a..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/index.ts @@ -8,6 +8,7 @@ export { urlSearchParamsBodySerializer, } from '../core/bodySerializer.gen'; export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; export { createClient } from './client.gen'; export type { Client, diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts index e54d1a64e..6cdf8ad48 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/types.gen.ts @@ -10,6 +10,10 @@ import type { import type { Injector } from '@angular/core'; import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; import type { Client as CoreClient, Config as CoreConfig, @@ -63,13 +67,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -93,7 +106,7 @@ export interface ResolvedRequestOptions< TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, -> extends RequestOptions { +> extends RequestOptions { serializedBody?: string; } @@ -102,45 +115,41 @@ export type RequestResult< TError = unknown, ThrowOnError extends boolean = boolean, TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: HttpRequest; - response: HttpResponse; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record +> = Promise< + ThrowOnError extends true + ? TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: HttpRequest; + response: HttpResponse; + } + : TResponseStyle extends 'data' + ? + | (TData extends Record ? TData[keyof TData] : TData) + | undefined + : + | { + data: TData extends Record ? TData[keyof TData] - : TData) - | undefined - : - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - request: HttpRequest; - response: HttpResponse; - } - | { - data: undefined; - error: TError[keyof TError]; - request: HttpRequest; - response: HttpErrorResponse & { - error: TError[keyof TError] | null; - }; - } - >; + : TData; + error: undefined; + request: HttpRequest; + response: HttpResponse; + } + | { + data: undefined; + error: TError[keyof TError]; + request: HttpRequest; + response: HttpErrorResponse & { + error: TError[keyof TError] | null; + }; + } +>; export interface ClientOptions { baseUrl?: string; @@ -154,19 +163,38 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; +type RequestOptionsFn = < + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: RequestOptions, +) => HttpRequest; + type BuildUrlFn = < TData extends { body?: unknown; @@ -178,13 +206,20 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { interceptors: Middleware< HttpRequest, HttpResponse, unknown, ResolvedRequestOptions >; + requestOptions: RequestOptionsFn; }; /** @@ -212,9 +247,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -226,18 +262,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts index 132dc7ab3..64a5d8b09 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/client/utils.gen.ts @@ -194,14 +194,13 @@ export const getParseAs = ( return; }; -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: HttpHeaders; - }) => { - for (const auth of security) { +export const setAuthParams = async ( + options: Pick, 'security'> & + Pick & { + headers: HttpHeaders; + }, +) => { + for (const auth of options.security) { const token = await getAuthToken(auth, options.auth); if (!token) { @@ -218,11 +217,11 @@ export const setAuthParams = async ({ options.query[name] = token; break; case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); + options.headers = options.headers.append('Cookie', `${name}=${token}`); break; case 'header': default: - options.headers.set(name, token); + options.headers = options.headers.set(name, token); break; } @@ -346,67 +345,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; + fns: Array = []; - constructor() { - this._fns = []; + clear(): void { + this.fns = []; } - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts index 9c8e3ec6d..49cd8925e 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/bodySerializer.gen.ts @@ -23,6 +23,8 @@ const serializeFormDataPair = ( ): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); } else { data.append(key, JSON.stringify(value)); } diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts index 5bfae35c0..643c070c9 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/types.gen.ts @@ -7,29 +7,36 @@ import type { QuerySerializerOptions, } from './bodySerializer.gen'; -export interface Client< +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< RequestFn = never, Config = unknown, MethodFn = never, BuildUrlFn = never, -> { + SseFn = never, +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; request: RequestFn; setConfig: (config: Config) => Config; - trace: MethodFn; -} +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); export interface Config { /** @@ -65,16 +72,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; + method?: Uppercase; /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts index 848c7e94c..f424fe675 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts index 992c17fb2..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-angular-query-experimental/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts index d00b4cc14..c461fceff 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/@tanstack/react-query.gen.ts @@ -6,7 +6,7 @@ import { type UseMutationOptions, } from '@tanstack/react-query'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,63 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -125,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -139,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -153,10 +100,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -165,16 +112,57 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( - options?: Options, + options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( - options?: Options, + options: Options, ) => queryOptions({ queryFn: async ({ queryKey, signal }) => { @@ -189,14 +177,15 @@ export const findPetsByStatusOptions = ( queryKey: findPetsByStatusQueryKey(options), }); -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options); /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -212,6 +201,7 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -222,10 +212,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -239,6 +229,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -255,32 +246,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -295,10 +263,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -307,29 +275,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -344,10 +292,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -361,6 +309,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -377,29 +326,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -414,10 +343,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -428,6 +357,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -438,10 +368,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -455,6 +385,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -471,29 +402,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -508,10 +419,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -520,32 +431,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -560,10 +448,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -577,6 +465,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -598,6 +487,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -616,6 +506,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -626,10 +517,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -643,6 +534,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -661,6 +553,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -671,10 +564,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-fastify/src/client/client/types.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts similarity index 69% rename from examples/openapi-ts-fastify/src/client/client/types.ts rename to examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts index 75a2ffbbe..1a005b51e 100644 --- a/examples/openapi-ts-fastify/src/client/client/types.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/types.gen.ts @@ -1,6 +1,15 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; export type ResponseStyle = 'data' | 'fields'; @@ -17,7 +26,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType; + fetch?: typeof fetch; /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +42,14 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream'; + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +65,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string, > extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +96,14 @@ export interface RequestOptions< url: Url; } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + export type RequestResult< TData = unknown, TError = unknown, @@ -128,17 +161,29 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'>, + options: Omit, 'method'>, ) => RequestResult; +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields', >( - options: Omit, 'method'> & - Pick>, 'method'>, + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, ) => RequestResult; type BuildUrlFn = < @@ -152,8 +197,14 @@ type BuildUrlFn = < options: Pick & Options, ) => string; -export type Client = CoreClient & { - interceptors: Middleware; +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; }; /** @@ -181,9 +232,10 @@ type OmitKeys = Pick>; export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields', > = OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'path' | 'query' | 'url' > & Omit; @@ -195,18 +247,22 @@ export type OptionsLegacyParser< > = TData extends { body?: any } ? TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'body' | 'headers' | 'url' > & TData - : OmitKeys, 'body' | 'url'> & + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } ? OmitKeys< - RequestOptions, + RequestOptions, 'headers' | 'url' > & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-fastify/src/client/client/utils.ts b/examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts similarity index 59% rename from examples/openapi-ts-fastify/src/client/client/utils.ts rename to examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts index bf3f28250..96de282a8 100644 --- a/examples/openapi-ts-fastify/src/client/client/utils.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/client/utils.gen.ts @@ -1,96 +1,20 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; export const createQuerySerializer = ({ allowReserved, @@ -182,6 +106,27 @@ export const getParseAs = ( if (cleanContent.startsWith('text/')) { return 'text'; } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; }; export const setAuthParams = async ({ @@ -192,6 +137,10 @@ export const setAuthParams = async ({ headers: Headers; }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + const token = await getAuthToken(auth, options.auth); if (!token) { @@ -215,13 +164,11 @@ export const setAuthParams = async ({ options.headers.set(name, token); break; } - - return; } }; -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -231,36 +178,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url, }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b }; @@ -271,17 +188,27 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config; }; +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers(); for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue; } const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); for (const [key, value] of iterator) { if (value === null) { @@ -322,67 +249,61 @@ type ResInterceptor = ( ) => Res | Promise; class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } + fns: Array = []; - clear() { - this._fns = []; + clear(): void { + this.fns = []; } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id); - return !!this._fns[index]; + return Boolean(this.fns[index]); } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; } + return this.fns.indexOf(id); } - update(id: number | Interceptor, fn: Interceptor) { + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; + if (this.fns[index]) { + this.fns[index] = fn; return id; - } else { - return false; } + return false; } - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>(), diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts deleted file mode 100644 index 451c7f30f..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..49cd8925e --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts deleted file mode 100644 index fab971b66..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/bodySerializer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -) => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts new file mode 100644 index 000000000..71c88e852 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts deleted file mode 100644 index 7559bbb8c..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/params.ts +++ /dev/null @@ -1,141 +0,0 @@ -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - key: string; - map?: string; - } - | { - in: Extract; - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts deleted file mode 100644 index d692cf0a3..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/pathSerializer.ts +++ /dev/null @@ -1,179 +0,0 @@ -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts deleted file mode 100644 index 1f8688099..000000000 --- a/examples/openapi-ts-tanstack-react-query/src/client/core/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Auth, AuthToken } from './auth'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer'; - -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; - getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; - request: RequestFn; - setConfig: (config: Config) => Config; - trace: MethodFn; -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} diff --git a/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-react-query/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-react-query/src/client/index.ts b/examples/openapi-ts-tanstack-react-query/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-react-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts index 80fadb421..e9fe1844e 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/@tanstack/svelte-query.gen.ts @@ -6,7 +6,7 @@ import { queryOptions, } from '@tanstack/svelte-query'; -import { client as _heyApiClient } from '../client.gen'; +import { client } from '../client.gen'; import { addPet, createUser, @@ -58,63 +58,9 @@ import type { UploadFileResponse, } from '../types.gen'; -export type QueryKey = [ - Pick & { - _id: string; - _infinite?: boolean; - }, -]; - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean, -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, - } as QueryKey[0]; - if (infinite) { - params._infinite = infinite; - } - if (options?.body) { - params.body = options.body; - } - if (options?.headers) { - params.headers = options.headers; - } - if (options?.path) { - params.path = options.path; - } - if (options?.query) { - params.query = options.query; - } - return [params]; -}; - -export const addPetQueryKey = (options: Options) => - createQueryKey('addPet', options); - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: addPetQueryKey(options), - }); - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( @@ -125,10 +71,10 @@ export const addPetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -139,6 +85,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -149,10 +96,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -161,16 +108,57 @@ export const updatePetMutation = ( return mutationOptions; }; +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + tags?: ReadonlyArray; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: + options?.baseUrl || (options?.client ?? client).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (tags) { + params.tags = tags; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + export const findPetsByStatusQueryKey = ( - options?: Options, + options: Options, ) => createQueryKey('findPetsByStatus', options); /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatusOptions = ( - options?: Options, + options: Options, ) => queryOptions({ queryFn: async ({ queryKey, signal }) => { @@ -185,14 +173,15 @@ export const findPetsByStatusOptions = ( queryKey: findPetsByStatusQueryKey(options), }); -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options); /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -208,6 +197,7 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( @@ -218,10 +208,10 @@ export const deletePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -235,6 +225,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -251,32 +242,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options), }); -export const updatePetWithFormQueryKey = ( - options: Options, -) => createQueryKey('updatePetWithForm', options); - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = ( - options: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: updatePetWithFormQueryKey(options), - }); - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -291,10 +259,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -303,29 +271,9 @@ export const updatePetWithFormMutation = ( return mutationOptions; }; -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options); - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: uploadFileQueryKey(options), - }); - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -340,10 +288,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -357,6 +305,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -373,29 +322,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options), }); -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options); - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: placeOrderQueryKey(options), - }); - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -410,10 +339,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -424,6 +353,7 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( @@ -434,10 +364,10 @@ export const deleteOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -451,6 +381,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -467,29 +398,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options), }); -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options); - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUserQueryKey(options), - }); - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -504,10 +415,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -516,32 +427,9 @@ export const createUserMutation = ( return mutationOptions; }; -export const createUsersWithListInputQueryKey = ( - options?: Options, -) => createQueryKey('createUsersWithListInput', options); - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = ( - options?: Options, -) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true, - }); - return data; - }, - queryKey: createUsersWithListInputQueryKey(options), - }); - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -556,10 +444,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -573,6 +461,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -594,6 +483,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -612,6 +502,7 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( @@ -622,10 +513,10 @@ export const deleteUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; @@ -639,6 +530,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -657,6 +549,7 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( @@ -667,10 +560,10 @@ export const updateUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true, }); return data; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts index f1e680045..069f4daba 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client.gen.ts @@ -1,12 +1,12 @@ // This file is auto-generated by @hey-api/openapi-ts import { - type ClientOptions as DefaultClientOptions, + type ClientOptions, type Config, createClient, createConfig, } from './client'; -import type { ClientOptions } from './types.gen'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; /** * The `createClientConfig()` function will be called on client initialization @@ -16,13 +16,12 @@ import type { ClientOptions } from './types.gen'; * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = - ( - override?: Config, - ) => Config & T>; +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3', }), ); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..a439d2748 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { + Client, + Config, + RequestOptions, + ResolvedRequestOptions, +} from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + + return { opts, url }; + }; + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = + (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = + (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + url, + }); + }; + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts deleted file mode 100644 index aaeee2f36..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/client.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Client, Config, RequestOptions } from './types'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - RequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - let data = await response[parseAs](); - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - let error = await response.text(); - - try { - error = JSON.parse(error); - } catch { - // noop - } - - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts index 5da1f7aee..cbf8dfeed 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth'; -export type { QuerySerializerOptions } from '../core/bodySerializer'; +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer, -} from '../core/bodySerializer'; -export { buildClientParams } from '../core/params'; -export { createClient } from './client'; +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape, -} from './types'; -export { createConfig, mergeHeaders } from './utils'; +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts new file mode 100644 index 000000000..1a005b51e --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.gen.ts @@ -0,0 +1,268 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick< + Required>, + 'method' + >, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient< + RequestFn, + Config, + MethodFn, + BuildUrlFn, + SseFn +> & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys< + RequestOptions, + 'body' | 'url' + > & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & + TData; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts deleted file mode 100644 index 75a2ffbbe..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/types.ts +++ /dev/null @@ -1,212 +0,0 @@ -import type { Auth } from '../core/auth'; -import type { Client as CoreClient, Config as CoreConfig } from '../core/types'; -import type { Middleware } from './utils'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: (request: Request) => ReturnType; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: Exclude | 'auto' | 'stream'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: Pick & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - Omit; - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'body' | 'headers' | 'url' - > & - TData - : OmitKeys, 'body' | 'url'> & - TData & - Pick, 'headers'> - : TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'headers' | 'url' - > & - TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts new file mode 100644 index 000000000..96de282a8 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.gen.ts @@ -0,0 +1,336 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { + Client, + ClientOptions, + Config, + RequestOptions, +} from './types.gen'; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = + header instanceof Headers + ? headersEntries(header) + : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update( + id: number | Interceptor, + fn: Interceptor, + ): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts deleted file mode 100644 index bf3f28250..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/client/utils.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { getAuthToken } from '../core/auth'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer'; -import { jsonBodySerializer } from '../core/bodySerializer'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer'; -import type { Client, ClientOptions, Config, RequestOptions } from './types'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const createQuerySerializer = ({ - allowReserved, - array, - object, -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved, - explode: true, - name, - style: 'form', - value, - ...array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - - return; - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header || typeof header !== 'object') { - continue; - } - - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } - - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); - } - } - exists(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - return !!this._fns[index]; - } - - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; - } - } - - update(id: number | Interceptor, fn: Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; - return id; - } else { - return false; - } - } - - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; - } -} - -// `createInterceptors()` response, meant for external use as it does not -// expose internals -export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; -} - -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts new file mode 100644 index 000000000..f8a73266f --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts deleted file mode 100644 index 451c7f30f..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/auth.ts +++ /dev/null @@ -1,40 +0,0 @@ -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts new file mode 100644 index 000000000..49cd8925e --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts deleted file mode 100644 index fab971b66..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/bodySerializer.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -) => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ) => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts new file mode 100644 index 000000000..71c88e852 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts deleted file mode 100644 index 7559bbb8c..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/params.ts +++ /dev/null @@ -1,141 +0,0 @@ -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - key: string; - map?: string; - } - | { - in: Extract; - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts new file mode 100644 index 000000000..8d9993104 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts deleted file mode 100644 index d692cf0a3..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/pathSerializer.ts +++ /dev/null @@ -1,179 +0,0 @@ -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..d3bb68396 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,136 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => + a.localeCompare(b), + ); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = ( + value: unknown, +): JsonValue | undefined => { + if (value === null) { + return null; + } + + if ( + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { + return value; + } + + if ( + value === undefined || + typeof value === 'function' || + typeof value === 'symbol' + ) { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if ( + typeof URLSearchParams !== 'undefined' && + value instanceof URLSearchParams + ) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..f8fd78e28 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,264 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit< + RequestInit, + 'method' +> & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult< + TData = unknown, + TReturn = void, + TNext = unknown, +> = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined; + + const sleep = + sseSleepFn ?? + ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) + throw new Error( + `SSE failed: ${response.status} ${response.statusText}`, + ); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body + .pipeThrough(new TextDecoderStream()) + .getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt( + line.replace(/^retry:\s*/, ''), + 10, + ); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if ( + sseMaxRetryAttempts !== undefined && + attempt >= sseMaxRetryAttempts + ) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min( + retryDelay * 2 ** (attempt - 1), + sseMaxRetryDelay ?? 30000, + ); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts new file mode 100644 index 000000000..643c070c9 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.gen.ts @@ -0,0 +1,118 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] + ? { sse?: never } + : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts deleted file mode 100644 index 1f8688099..000000000 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/core/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -import type { Auth, AuthToken } from './auth'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer'; - -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; - getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; - request: RequestFn; - setConfig: (config: Config) => Config; - trace: MethodFn; -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..0b5389d08 --- /dev/null +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/core/utils.gen.ts @@ -0,0 +1,143 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts index 688e3c912..57ed02bf5 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen'; -export * from './types.gen'; +export type * from './types.gen'; diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts index f6845bc73..f424fe675 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client'; -import { client as _heyApiClient } from './client.gen'; +import type { Client, Options as Options2, TDataShape } from './client'; +import { client } from './client.gen'; import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,16 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options, ) => - (options.client ?? _heyApiClient).post< - AddPetResponses, - AddPetErrors, - ThrowOnError - >({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -107,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdatePetResponses, UpdatePetErrors, ThrowOnError @@ -133,12 +131,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByStatusResponses, FindPetsByStatusErrors, ThrowOnError @@ -155,12 +154,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options, + options: Options, ) => - (options?.client ?? _heyApiClient).get< + (options.client ?? client).get< FindPetsByTagsResponses, FindPetsByTagsErrors, ThrowOnError @@ -177,12 +177,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeletePetResponses, DeletePetErrors, ThrowOnError @@ -199,12 +200,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetPetByIdResponses, GetPetByIdErrors, ThrowOnError @@ -225,12 +227,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -247,12 +250,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options, ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UploadFileResponses, UploadFileErrors, ThrowOnError @@ -274,12 +278,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< GetInventoryResponses, GetInventoryErrors, ThrowOnError @@ -296,12 +301,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< PlaceOrderResponses, PlaceOrderErrors, ThrowOnError @@ -316,12 +322,13 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteOrderResponses, DeleteOrderErrors, ThrowOnError @@ -332,12 +339,13 @@ export const deleteOrder = ( /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetOrderByIdResponses, GetOrderByIdErrors, ThrowOnError @@ -348,12 +356,13 @@ export const getOrderById = ( /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUserResponses, CreateUserErrors, ThrowOnError @@ -368,12 +377,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options, ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -388,12 +398,13 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LoginUserResponses, LoginUserErrors, ThrowOnError @@ -404,12 +415,13 @@ export const loginUser = ( /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options, ) => - (options?.client ?? _heyApiClient).get< + (options?.client ?? client).get< LogoutUserResponses, LogoutUserErrors, ThrowOnError @@ -420,12 +432,13 @@ export const logoutUser = ( /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options, ) => - (options.client ?? _heyApiClient).delete< + (options.client ?? client).delete< DeleteUserResponses, DeleteUserErrors, ThrowOnError @@ -436,12 +449,13 @@ export const deleteUser = ( /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options, ) => - (options.client ?? _heyApiClient).get< + (options.client ?? client).get< GetUserByNameResponses, GetUserByNameErrors, ThrowOnError @@ -452,12 +466,13 @@ export const getUserByName = ( /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options, ) => - (options.client ?? _heyApiClient).put< + (options.client ?? client).put< UpdateUserResponses, UpdateUserErrors, ThrowOnError diff --git a/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts index ae947e8e4..a2e6be0fa 100644 --- a/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-svelte-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); +}; + export type Order = { complete?: boolean; id?: number; @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses]; export type FindPetsByStatusData = { body?: never; path?: never; - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold'; + status: 'available' | 'pending' | 'sold'; }; url: '/pet/findByStatus'; }; @@ -169,11 +173,11 @@ export type FindPetsByStatusResponse = export type FindPetsByTagsData = { body?: never; path?: never; - query?: { + query: { /** * Tags to filter by */ - tags?: Array; + tags: Array; }; url: '/pet/findByTags'; }; @@ -560,7 +564,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File; + 200: string; }; export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses]; @@ -693,7 +697,3 @@ export type UpdateUserResponses = { */ 200: unknown; }; - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}); -}; diff --git a/examples/openapi-ts-tanstack-vue-query/.eslintrc.cjs b/examples/openapi-ts-tanstack-vue-query/.eslintrc.cjs index ff3a044fa..491d6ae84 100644 --- a/examples/openapi-ts-tanstack-vue-query/.eslintrc.cjs +++ b/examples/openapi-ts-tanstack-vue-query/.eslintrc.cjs @@ -10,6 +10,5 @@ module.exports = { ], parserOptions: { ecmaVersion: 'latest' - }, - root: true + } } diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts index e317c64d7..9050f1d24 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/@tanstack/vue-query.gen.ts @@ -2,7 +2,7 @@ import { type DefaultError, queryOptions, type UseMutationOptions } from '@tanstack/vue-query' -import { client as _heyApiClient } from '../client.gen' +import { client } from '../client.gen' import { addPet, createUser, @@ -54,72 +54,19 @@ import type { UploadFileResponse } from '../types.gen' -export type QueryKey = [ - Pick & { - _id: string - _infinite?: boolean - } -] - -const createQueryKey = ( - id: string, - options?: TOptions, - infinite?: boolean -): [QueryKey[0]] => { - const params: QueryKey[0] = { - _id: id, - baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl - } as QueryKey[0] - if (infinite) { - params._infinite = infinite - } - if (options?.body) { - params.body = options.body - } - if (options?.headers) { - params.headers = options.headers - } - if (options?.path) { - params.path = options.path - } - if (options?.query) { - params.query = options.query - } - return [params] -} - -export const addPetQueryKey = (options: Options) => createQueryKey('addPet', options) - -/** - * Add a new pet to the store. - * Add a new pet to the store. - */ -export const addPetOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await addPet({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: addPetQueryKey(options) - }) - /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPetMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await addPet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -130,6 +77,7 @@ export const addPetMutation = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePetMutation = ( @@ -140,10 +88,10 @@ export const updatePetMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -152,14 +100,54 @@ export const updatePetMutation = ( return mutationOptions } -export const findPetsByStatusQueryKey = (options?: Options) => +export type QueryKey = [ + Pick & { + _id: string + _infinite?: boolean + tags?: ReadonlyArray + } +] + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, + tags?: ReadonlyArray +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: options?.baseUrl || (options?.client ?? client).getConfig().baseUrl + } as QueryKey[0] + if (infinite) { + params._infinite = infinite + } + if (tags) { + params.tags = tags + } + if (options?.body) { + params.body = options.body + } + if (options?.headers) { + params.headers = options.headers + } + if (options?.path) { + params.path = options.path + } + if (options?.query) { + params.query = options.query + } + return [params] +} + +export const findPetsByStatusQueryKey = (options: Options) => createQueryKey('findPetsByStatus', options) /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ -export const findPetsByStatusOptions = (options?: Options) => +export const findPetsByStatusOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByStatus({ @@ -173,14 +161,15 @@ export const findPetsByStatusOptions = (options?: Options) queryKey: findPetsByStatusQueryKey(options) }) -export const findPetsByTagsQueryKey = (options?: Options) => +export const findPetsByTagsQueryKey = (options: Options) => createQueryKey('findPetsByTags', options) /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ -export const findPetsByTagsOptions = (options?: Options) => +export const findPetsByTagsOptions = (options: Options) => queryOptions({ queryFn: async ({ queryKey, signal }) => { const { data } = await findPetsByTags({ @@ -196,16 +185,17 @@ export const findPetsByTagsOptions = (options?: Options) => /** * Deletes a pet. + * * Delete a pet. */ export const deletePetMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deletePet({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -219,6 +209,7 @@ export const getPetByIdQueryKey = (options: Options) => /** * Find pet by ID. + * * Returns a single pet. */ export const getPetByIdOptions = (options: Options) => @@ -235,29 +226,9 @@ export const getPetByIdOptions = (options: Options) => queryKey: getPetByIdQueryKey(options) }) -export const updatePetWithFormQueryKey = (options: Options) => - createQueryKey('updatePetWithForm', options) - -/** - * Updates a pet in the store with form data. - * Updates a pet resource based on the form data. - */ -export const updatePetWithFormOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await updatePetWithForm({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: updatePetWithFormQueryKey(options) - }) - /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithFormMutation = ( @@ -268,10 +239,10 @@ export const updatePetWithFormMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updatePetWithForm({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -280,29 +251,9 @@ export const updatePetWithFormMutation = ( return mutationOptions } -export const uploadFileQueryKey = (options: Options) => - createQueryKey('uploadFile', options) - -/** - * Uploads an image. - * Upload image of the pet. - */ -export const uploadFileOptions = (options: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await uploadFile({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: uploadFileQueryKey(options) - }) - /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFileMutation = ( @@ -313,10 +264,10 @@ export const uploadFileMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await uploadFile({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -330,6 +281,7 @@ export const getInventoryQueryKey = (options?: Options) => /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventoryOptions = (options?: Options) => @@ -346,29 +298,9 @@ export const getInventoryOptions = (options?: Options) => queryKey: getInventoryQueryKey(options) }) -export const placeOrderQueryKey = (options?: Options) => - createQueryKey('placeOrder', options) - -/** - * Place an order for a pet. - * Place a new order in the store. - */ -export const placeOrderOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await placeOrder({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: placeOrderQueryKey(options) - }) - /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrderMutation = ( @@ -379,10 +311,10 @@ export const placeOrderMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await placeOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -393,16 +325,17 @@ export const placeOrderMutation = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrderMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteOrder({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -416,6 +349,7 @@ export const getOrderByIdQueryKey = (options: Options) => /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderByIdOptions = (options: Options) => @@ -432,29 +366,9 @@ export const getOrderByIdOptions = (options: Options) => queryKey: getOrderByIdQueryKey(options) }) -export const createUserQueryKey = (options?: Options) => - createQueryKey('createUser', options) - -/** - * Create user. - * This can only be done by the logged in user. - */ -export const createUserOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUser({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: createUserQueryKey(options) - }) - /** * Create user. + * * This can only be done by the logged in user. */ export const createUserMutation = ( @@ -465,10 +379,10 @@ export const createUserMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -477,29 +391,9 @@ export const createUserMutation = ( return mutationOptions } -export const createUsersWithListInputQueryKey = (options?: Options) => - createQueryKey('createUsersWithListInput', options) - -/** - * Creates list of users with given input array. - * Creates list of users with given input array. - */ -export const createUsersWithListInputOptions = (options?: Options) => - queryOptions({ - queryFn: async ({ queryKey, signal }) => { - const { data } = await createUsersWithListInput({ - ...options, - ...queryKey[0], - signal, - throwOnError: true - }) - return data - }, - queryKey: createUsersWithListInputQueryKey(options) - }) - /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInputMutation = ( @@ -514,10 +408,10 @@ export const createUsersWithListInputMutation = ( DefaultError, Options > = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await createUsersWithListInput({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -531,6 +425,7 @@ export const loginUserQueryKey = (options?: Options) => /** * Logs user into the system. + * * Log into the system. */ export const loginUserOptions = (options?: Options) => @@ -552,6 +447,7 @@ export const logoutUserQueryKey = (options?: Options) => /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUserOptions = (options?: Options) => @@ -570,16 +466,17 @@ export const logoutUserOptions = (options?: Options) => /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUserMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await deleteUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data @@ -593,6 +490,7 @@ export const getUserByNameQueryKey = (options: Options) => /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByNameOptions = (options: Options) => @@ -611,16 +509,17 @@ export const getUserByNameOptions = (options: Options) => /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUserMutation = ( options?: Partial> ): UseMutationOptions> => { const mutationOptions: UseMutationOptions> = { - mutationFn: async (localOptions) => { + mutationFn: async (fnOptions) => { const { data } = await updateUser({ ...options, - ...localOptions, + ...fnOptions, throwOnError: true }) return data diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts index 1984dc18c..3431b130d 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client.gen.ts @@ -1,12 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import { - type ClientOptions as DefaultClientOptions, - type Config, - createClient, - createConfig -} from './client' -import type { ClientOptions } from './types.gen' +import { type ClientOptions, type Config, createClient, createConfig } from './client' +import type { ClientOptions as ClientOptions2 } from './types.gen' /** * The `createClientConfig()` function will be called on client initialization @@ -16,12 +11,12 @@ import type { ClientOptions } from './types.gen' * `setConfig()`. This is useful for example if you're using Next.js * to ensure your client always has the correct values. */ -export type CreateClientConfig = ( - override?: Config -) => Config & T> +export type CreateClientConfig = ( + override?: Config +) => Config & T> export const client = createClient( - createConfig({ + createConfig({ baseUrl: 'https://petstore3.swagger.io/api/v3' }) ) diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts new file mode 100644 index 000000000..4a4fc4648 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.gen.ts @@ -0,0 +1,253 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen' +import type { HttpMethod } from '../core/types.gen' +import { getValidRequestBody } from '../core/utils.gen' +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen' +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams +} from './utils.gen' + +type ReqInit = Omit & { + body?: any + headers: ReturnType +} + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config) + + const getConfig = (): Config => ({ ..._config }) + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config) + return getConfig() + } + + const interceptors = createInterceptors() + + const beforeRequest = async (options: RequestOptions) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined + } + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security + }) + } + + if (opts.requestValidator) { + await opts.requestValidator(opts) + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type') + } + + const url = buildUrl(opts) + + return { opts, url } + } + + const request: Client['request'] = async (options) => { + // @ts-expect-error + const { opts, url } = await beforeRequest(options) + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts) + } + + let request = new Request(url, requestInit) + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch! + let response = await _fetch(request) + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts) + } + } + + const result = { + request, + response + } + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json' + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs]() + break + case 'formData': + emptyData = new FormData() + break + case 'stream': + emptyData = response.body + break + case 'json': + default: + emptyData = {} + break + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result + } + } + + let data: any + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs]() + break + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result + } + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data) + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data) + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result + } + } + + const textError = await response.text() + let jsonError: unknown + + try { + jsonError = JSON.parse(textError) + } catch { + // noop + } + + const error = jsonError ?? textError + let finalError = error + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string + } + } + + finalError = finalError || ({} as string) + + if (opts.throwOnError) { + throw finalError + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result + } + } + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }) + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options) + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init) + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts) + } + } + return request + }, + url + }) + } + + return { + buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE') + }, + trace: makeMethodFn('TRACE') + } as Client +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts deleted file mode 100644 index 74e5e6f3a..000000000 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/client.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { Client, Config, RequestOptions } from './types' -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams -} from './utils' - -type ReqInit = Omit & { - body?: any - headers: ReturnType -} - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config) - - const getConfig = (): Config => ({ ..._config }) - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config) - return getConfig() - } - - const interceptors = createInterceptors() - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers) - } - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security - }) - } - - if (opts.body && opts.bodySerializer) { - opts.body = opts.bodySerializer(opts.body) - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.body === undefined || opts.body === '') { - opts.headers.delete('Content-Type') - } - - const url = buildUrl(opts) - const requestInit: ReqInit = { - redirect: 'follow', - ...opts - } - - let request = new Request(url, requestInit) - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts) - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch! - let response = await _fetch(request) - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts) - } - } - - const result = { - request, - response - } - - if (response.ok) { - if (response.status === 204 || response.headers.get('Content-Length') === '0') { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result - } - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json' - - if (parseAs === 'stream') { - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result - } - } - - let data = await response[parseAs]() - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data) - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data) - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result - } - } - - let error = await response.text() - - try { - error = JSON.parse(error) - } catch { - // noop - } - - let finalError = error - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string - } - } - - finalError = finalError || ({} as string) - - if (opts.throwOnError) { - throw finalError - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result - } - } - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }) - } -} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts index 6aea820a3..b379bec02 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/index.ts @@ -1,12 +1,15 @@ -export type { Auth } from '../core/auth' -export type { QuerySerializerOptions } from '../core/bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen' +export type { QuerySerializerOptions } from '../core/bodySerializer.gen' export { formDataBodySerializer, jsonBodySerializer, urlSearchParamsBodySerializer -} from '../core/bodySerializer' -export { buildClientParams } from '../core/params' -export { createClient } from './client' +} from '../core/bodySerializer.gen' +export { buildClientParams } from '../core/params.gen' +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen' +export { createClient } from './client.gen' export type { Client, ClientOptions, @@ -16,7 +19,8 @@ export type { OptionsLegacyParser, RequestOptions, RequestResult, + ResolvedRequestOptions, ResponseStyle, TDataShape -} from './types' -export { createConfig, mergeHeaders } from './utils' +} from './types.gen' +export { createConfig, mergeHeaders } from './utils.gen' diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts similarity index 68% rename from examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts index e6e5eea3a..b97f75e3f 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/types.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/types.gen.ts @@ -1,6 +1,9 @@ -import type { Auth } from '../core/auth' -import type { Client as CoreClient, Config as CoreConfig } from '../core/types' -import type { Middleware } from './utils' +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen' +import type { ServerSentEventsOptions, ServerSentEventsResult } from '../core/serverSentEvents.gen' +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen' +import type { Middleware } from './utils.gen' export type ResponseStyle = 'data' | 'fields' @@ -17,7 +20,7 @@ export interface Config * * @default globalThis.fetch */ - fetch?: (request: Request) => ReturnType + fetch?: typeof fetch /** * Please don't use the Fetch client for Next.js applications. The `next` * options won't have any effect. @@ -33,7 +36,7 @@ export interface Config * * @default 'auto' */ - parseAs?: Exclude | 'auto' | 'stream' + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text' /** * Should we return only data or multiple fields (data, error, response, etc.)? * @@ -49,13 +52,22 @@ export interface Config } export interface RequestOptions< + TData = unknown, TResponseStyle extends ResponseStyle = 'fields', ThrowOnError extends boolean = boolean, Url extends string = string > extends Config<{ - responseStyle: TResponseStyle - throwOnError: ThrowOnError - }> { + responseStyle: TResponseStyle + throwOnError: ThrowOnError + }>, + Pick< + ServerSentEventsOptions, + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { /** * Any body that you want to add to your request. * @@ -71,6 +83,14 @@ export interface RequestOptions< url: Url } +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string +> extends RequestOptions { + serializedBody?: string +} + export type RequestResult< TData = unknown, TError = unknown, @@ -118,17 +138,26 @@ type MethodFn = < ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields' >( - options: Omit, 'method'> + options: Omit, 'method'> ) => RequestResult +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields' +>( + options: Omit, 'method'> +) => Promise> + type RequestFn = < TData = unknown, TError = unknown, ThrowOnError extends boolean = false, TResponseStyle extends ResponseStyle = 'fields' >( - options: Omit, 'method'> & - Pick>, 'method'> + options: Omit, 'method'> & + Pick>, 'method'> ) => RequestResult type BuildUrlFn = < @@ -142,8 +171,8 @@ type BuildUrlFn = < options: Pick & Options ) => string -export type Client = CoreClient & { - interceptors: Middleware +export type Client = CoreClient & { + interceptors: Middleware } /** @@ -171,8 +200,12 @@ type OmitKeys = Pick> export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean, + TResponse = unknown, TResponseStyle extends ResponseStyle = 'fields' -> = OmitKeys, 'body' | 'path' | 'query' | 'url'> & +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & Omit export type OptionsLegacyParser< @@ -181,12 +214,13 @@ export type OptionsLegacyParser< TResponseStyle extends ResponseStyle = 'fields' > = TData extends { body?: any } ? TData extends { headers?: any } - ? OmitKeys, 'body' | 'headers' | 'url'> & TData - : OmitKeys, 'body' | 'url'> & + ? OmitKeys, 'body' | 'headers' | 'url'> & + TData + : OmitKeys, 'body' | 'url'> & TData & - Pick, 'headers'> + Pick, 'headers'> : TData extends { headers?: any } - ? OmitKeys, 'headers' | 'url'> & + ? OmitKeys, 'headers' | 'url'> & TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData + Pick, 'body'> + : OmitKeys, 'url'> & TData diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts b/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts similarity index 60% rename from examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts index 5e1bc373c..b42b5d951 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/client/utils.gen.ts @@ -1,90 +1,15 @@ -import { getAuthToken } from '../core/auth' -import type { QuerySerializer, QuerySerializerOptions } from '../core/bodySerializer' -import { jsonBodySerializer } from '../core/bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen' +import type { QuerySerializerOptions } from '../core/bodySerializer.gen' +import { jsonBodySerializer } from '../core/bodySerializer.gen' import { serializeArrayParam, serializeObjectParam, serializePrimitiveParam -} from '../core/pathSerializer' -import type { Client, ClientOptions, Config, RequestOptions } from './types' - -interface PathSerializer { - path: Record - url: string -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' -type MatrixStyle = 'label' | 'matrix' | 'simple' -type ArraySeparatorStyle = ArrayStyle | MatrixStyle - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url - const matches = _url.match(PATH_PARAM_RE) - if (matches) { - for (const match of matches) { - let explode = false - let name = match.substring(1, match.length - 1) - let style: ArraySeparatorStyle = 'simple' - - if (name.endsWith('*')) { - explode = true - name = name.substring(0, name.length - 1) - } - - if (name.startsWith('.')) { - name = name.substring(1) - style = 'label' - } else if (name.startsWith(';')) { - name = name.substring(1) - style = 'matrix' - } - - const value = path[name] - - if (value === undefined || value === null) { - continue - } - - if (Array.isArray(value)) { - url = url.replace(match, serializeArrayParam({ explode, name, style, value })) - continue - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true - }) - ) - continue - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string - })}` - ) - continue - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string) - ) - url = url.replace(match, replaceValue) - } - } - return url -} +} from '../core/pathSerializer.gen' +import { getUrl } from '../core/utils.gen' +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen' export const createQuerySerializer = ({ allowReserved, @@ -169,6 +94,27 @@ export const getParseAs = (contentType: string | null): Exclude & { + headers: Headers + }, + name?: string +): boolean => { + if (!name) { + return false + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true + } + return false } export const setAuthParams = async ({ @@ -179,6 +125,10 @@ export const setAuthParams = async ({ headers: Headers }) => { for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue + } + const token = await getAuthToken(auth, options.auth) if (!token) { @@ -202,13 +152,11 @@ export const setAuthParams = async ({ options.headers.set(name, token) break } - - return } } -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ baseUrl: options.baseUrl as string, path: options.path, query: options.query, @@ -218,36 +166,6 @@ export const buildUrl: Client['buildUrl'] = (options) => { : createQuerySerializer(options.querySerializer), url: options.url }) - return url -} - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url -}: { - baseUrl?: string - path?: Record - query?: Record - querySerializer: QuerySerializer - url: string -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}` - let url = (baseUrl ?? '') + pathUrl - if (path) { - url = defaultPathSerializer({ path, url }) - } - let search = query ? querySerializer(query) : '' - if (search.startsWith('?')) { - search = search.substring(1) - } - if (search) { - url += `?${search}` - } - return url -} export const mergeConfigs = (a: Config, b: Config): Config => { const config = { ...a, ...b } @@ -258,16 +176,24 @@ export const mergeConfigs = (a: Config, b: Config): Config => { return config } +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = [] + headers.forEach((value, key) => { + entries.push([key, value]) + }) + return entries +} + export const mergeHeaders = ( ...headers: Array['headers'] | undefined> ): Headers => { const mergedHeaders = new Headers() for (const header of headers) { - if (!header || typeof header !== 'object') { + if (!header) { continue } - const iterator = header instanceof Headers ? header.entries() : Object.entries(header) + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header) for (const [key, value] of iterator) { if (value === null) { @@ -305,61 +231,58 @@ type ResInterceptor = ( ) => Res | Promise class Interceptors { - _fns: (Interceptor | null)[] - - constructor() { - this._fns = [] - } + fns: Array = [] - clear() { - this._fns = [] + clear(): void { + this.fns = [] } - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1 - } else { - return this._fns.indexOf(id) + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id) + if (this.fns[index]) { + this.fns[index] = null } } - exists(id: number | Interceptor) { + + exists(id: number | Interceptor): boolean { const index = this.getInterceptorIndex(id) - return !!this._fns[index] + return Boolean(this.fns[index]) } - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = null + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1 } + return this.fns.indexOf(id) } - update(id: number | Interceptor, fn: Interceptor) { + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { const index = this.getInterceptorIndex(id) - if (this._fns[index]) { - this._fns[index] = fn + if (this.fns[index]) { + this.fns[index] = fn return id - } else { - return false } + return false } - use(fn: Interceptor) { - this._fns = [...this._fns, fn] - return this._fns.length - 1 + use(fn: Interceptor): number { + this.fns.push(fn) + return this.fns.length - 1 } } -// `createInterceptors()` response, meant for external use as it does not -// expose internals export interface Middleware { - error: Pick>, 'eject' | 'use'> - request: Pick>, 'eject' | 'use'> - response: Pick>, 'eject' | 'use'> + error: Interceptors> + request: Interceptors> + response: Interceptors> } -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ error: new Interceptors>(), request: new Interceptors>(), response: new Interceptors>() diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts similarity index 93% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts index f3729c26e..dc8ff6197 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/auth.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + export type AuthToken = string | undefined export interface Auth { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts similarity index 79% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts index 1ce742d04..e39c6a559 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/bodySerializer.gen.ts @@ -1,4 +1,6 @@ -import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer' +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen' export type QuerySerializer = (query: Record) => string @@ -10,15 +12,17 @@ export interface QuerySerializerOptions { object?: SerializerOptions } -const serializeFormDataPair = (data: FormData, key: string, value: unknown) => { +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { if (typeof value === 'string' || value instanceof Blob) { data.append(key, value) + } else if (value instanceof Date) { + data.append(key, value.toISOString()) } else { data.append(key, JSON.stringify(value)) } } -const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown) => { +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { if (typeof value === 'string') { data.append(key, value) } else { @@ -27,7 +31,9 @@ const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: } export const formDataBodySerializer = { - bodySerializer: | Array>>(body: T) => { + bodySerializer: | Array>>( + body: T + ): FormData => { const data = new FormData() Object.entries(body).forEach(([key, value]) => { @@ -46,12 +52,12 @@ export const formDataBodySerializer = { } export const jsonBodySerializer = { - bodySerializer: (body: T) => - JSON.stringify(body, (key, value) => (typeof value === 'bigint' ? value.toString() : value)) + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)) } export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>(body: T) => { + bodySerializer: | Array>>(body: T): string => { const data = new URLSearchParams() Object.entries(body).forEach(([key, value]) => { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts similarity index 89% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts index cfac4fe33..34dddb5fa 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/params.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/params.gen.ts @@ -1,13 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + type Slot = 'body' | 'headers' | 'path' | 'query' export type Field = | { in: Exclude + /** + * Field name. This is the name we want the user to see and use. + */ key: string + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ map?: string } | { in: Extract + /** + * Key isn't required for bodies. + */ key?: string map?: string } diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts similarity index 98% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts index e5d30d671..acc13672e 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/pathSerializer.gen.ts @@ -1,3 +1,5 @@ +// This file is auto-generated by @hey-api/openapi-ts + interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} interface SerializePrimitiveOptions { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts new file mode 100644 index 000000000..7e9d0d1ec --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue } + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined + } + if (typeof value === 'bigint') { + return value.toString() + } + if (value instanceof Date) { + return value.toISOString() + } + return value +} + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer) + if (json === undefined) { + return undefined + } + return JSON.parse(json) as JsonValue + } catch { + return undefined + } +} + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false + } + const prototype = Object.getPrototypeOf(value as object) + return prototype === Object.prototype || prototype === null +} + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)) + const result: Record = {} + + for (const [key, value] of entries) { + const existing = result[key] + if (existing === undefined) { + result[key] = value + continue + } + + if (Array.isArray(existing)) { + ;(existing as string[]).push(value) + } else { + result[key] = [existing, value] + } + } + + return result +} + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined + } + + if (typeof value === 'bigint') { + return value.toString() + } + + if (value instanceof Date) { + return value.toISOString() + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value) + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value) + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value) + } + + return undefined +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts new file mode 100644 index 000000000..372e50cc2 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/serverSentEvents.gen.ts @@ -0,0 +1,241 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen' + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void + serializedBody?: RequestInit['body'] + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise + url: string + } + +export interface StreamEvent { + data: TData + event?: string + id?: string + retry?: number +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + > +} + +export const createSseClient = ({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult => { + let lastEventId: string | undefined + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))) + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000 + let attempt = 0 + const signal = options.signal ?? new AbortController().signal + + while (true) { + if (signal.aborted) break + + attempt++ + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined) + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId) + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal + } + let request = new Request(url, requestInit) + if (onRequest) { + request = await onRequest(url, requestInit) + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch + const response = await _fetch(request) + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`) + + if (!response.body) throw new Error('No body in SSE response') + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader() + + let buffer = '' + + const abortHandler = () => { + try { + reader.cancel() + } catch { + // noop + } + } + + signal.addEventListener('abort', abortHandler) + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + buffer += value + + const chunks = buffer.split('\n\n') + buffer = chunks.pop() ?? '' + + for (const chunk of chunks) { + const lines = chunk.split('\n') + const dataLines: Array = [] + let eventName: string | undefined + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')) + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, '') + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, '') + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10) + if (!Number.isNaN(parsed)) { + retryDelay = parsed + } + } + } + + let data: unknown + let parsedJson = false + + if (dataLines.length) { + const rawData = dataLines.join('\n') + try { + data = JSON.parse(rawData) + parsedJson = true + } catch { + data = rawData + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data) + } + + if (responseTransformer) { + data = await responseTransformer(data) + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay + }) + + if (dataLines.length) { + yield data as any + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler) + reader.releaseLock() + } + + break // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error) + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000) + await sleep(backoff) + } + } + } + + const stream = createStream() + + return { stream } +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts similarity index 65% rename from examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts rename to examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts index 5914dfccb..647ffcc49 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/core/types.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/types.gen.ts @@ -1,24 +1,36 @@ -import type { Auth, AuthToken } from './auth' -import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer' +// This file is auto-generated by @hey-api/openapi-ts -export interface Client { +import type { Auth, AuthToken } from './auth.gen' +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen' + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace' + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never +> = { /** * Returns the final request URL. */ buildUrl: BuildUrlFn - connect: MethodFn - delete: MethodFn - get: MethodFn getConfig: () => Config - head: MethodFn - options: MethodFn - patch: MethodFn - post: MethodFn - put: MethodFn request: RequestFn setConfig: (config: Config) => Config - trace: MethodFn -} +} & { + [K in HttpMethod]: MethodFn +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }) export interface Config { /** @@ -48,7 +60,7 @@ export interface Config { * * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} */ - method?: 'CONNECT' | 'DELETE' | 'GET' | 'HEAD' | 'OPTIONS' | 'PATCH' | 'POST' | 'PUT' | 'TRACE' + method?: Uppercase /** * A function for serializing request query parameters. By default, arrays * will be exploded in form style, objects will be exploded in deepObject @@ -60,6 +72,12 @@ export interface Config { * {@link https://swagger.io/docs/specification/serialization/#query View examples} */ querySerializer?: QuerySerializer | QuerySerializerOptions + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise /** * A function transforming response data before it's returned. This is useful * for post-processing data, e.g. converting ISO strings into Date objects. @@ -72,3 +90,15 @@ export interface Config { */ responseValidator?: (data: unknown) => Promise } + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K] +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts new file mode 100644 index 000000000..bd078be24 --- /dev/null +++ b/examples/openapi-ts-tanstack-vue-query/src/client/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen' +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam +} from './pathSerializer.gen' + +export interface PathSerializer { + path: Record + url: string +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url + const matches = _url.match(PATH_PARAM_RE) + if (matches) { + for (const match of matches) { + let explode = false + let name = match.substring(1, match.length - 1) + let style: ArraySeparatorStyle = 'simple' + + if (name.endsWith('*')) { + explode = true + name = name.substring(0, name.length - 1) + } + + if (name.startsWith('.')) { + name = name.substring(1) + style = 'label' + } else if (name.startsWith(';')) { + name = name.substring(1) + style = 'matrix' + } + + const value = path[name] + + if (value === undefined || value === null) { + continue + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })) + continue + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true + }) + ) + continue + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string + })}` + ) + continue + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string) + ) + url = url.replace(match, replaceValue) + } + } + return url +} + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url +}: { + baseUrl?: string + path?: Record + query?: Record + querySerializer: QuerySerializer + url: string +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}` + let url = (baseUrl ?? '') + pathUrl + if (path) { + url = defaultPathSerializer({ path, url }) + } + let search = query ? querySerializer(query) : '' + if (search.startsWith('?')) { + search = search.substring(1) + } + if (search) { + url += `?${search}` + } + return url +} + +export function getValidRequestBody(options: { + body?: unknown + bodySerializer?: BodySerializer | null + serializedBody?: unknown +}) { + const hasBody = options.body !== undefined + const isSerializedBody = hasBody && options.bodySerializer + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== '' + + return hasSerializedBody ? options.serializedBody : null + } + + // not all clients implement a serializedBody property (i.e. client-axios) + return options.body !== '' ? options.body : null + } + + // plain/text body + if (hasBody) { + return options.body + } + + // no body was provided + return undefined +} diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/index.ts b/examples/openapi-ts-tanstack-vue-query/src/client/index.ts index 544a87d45..550b29b77 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/index.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/index.ts @@ -1,3 +1,4 @@ // This file is auto-generated by @hey-api/openapi-ts + export * from './sdk.gen' -export * from './types.gen' +export type * from './types.gen' diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts index f4cf4d370..6743104bb 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Client, Options as ClientOptions, TDataShape } from './client' -import { client as _heyApiClient } from './client.gen' +import type { Client, Options as Options2, TDataShape } from './client' +import { client } from './client.gen' import type { AddPetData, AddPetErrors, @@ -65,7 +65,7 @@ import type { export type Options< TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean -> = ClientOptions & { +> = Options2 & { /** * You can provide a client instance returned by `createClient()` instead of * individual options. This might be also useful if you want to implement a @@ -81,12 +81,13 @@ export type Options< /** * Add a new pet to the store. + * * Add a new pet to the store. */ export const addPet = ( options: Options ) => - (options.client ?? _heyApiClient).post({ + (options.client ?? client).post({ security: [ { scheme: 'bearer', @@ -103,12 +104,13 @@ export const addPet = ( /** * Update an existing pet. + * * Update an existing pet by Id. */ export const updatePet = ( options: Options ) => - (options.client ?? _heyApiClient).put({ + (options.client ?? client).put({ security: [ { scheme: 'bearer', @@ -125,16 +127,13 @@ export const updatePet = ( /** * Finds Pets by status. + * * Multiple status values can be provided with comma separated strings. */ export const findPetsByStatus = ( - options?: Options + options: Options ) => - (options?.client ?? _heyApiClient).get< - FindPetsByStatusResponses, - FindPetsByStatusErrors, - ThrowOnError - >({ + (options.client ?? client).get({ security: [ { scheme: 'bearer', @@ -147,16 +146,13 @@ export const findPetsByStatus = ( /** * Finds Pets by tags. + * * Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. */ export const findPetsByTags = ( - options?: Options + options: Options ) => - (options?.client ?? _heyApiClient).get< - FindPetsByTagsResponses, - FindPetsByTagsErrors, - ThrowOnError - >({ + (options.client ?? client).get({ security: [ { scheme: 'bearer', @@ -169,12 +165,13 @@ export const findPetsByTags = ( /** * Deletes a pet. + * * Delete a pet. */ export const deletePet = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ security: [ { scheme: 'bearer', @@ -187,12 +184,13 @@ export const deletePet = ( /** * Find pet by ID. + * * Returns a single pet. */ export const getPetById = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ security: [ { name: 'api_key', @@ -209,12 +207,13 @@ export const getPetById = ( /** * Updates a pet in the store with form data. + * * Updates a pet resource based on the form data. */ export const updatePetWithForm = ( options: Options ) => - (options.client ?? _heyApiClient).post< + (options.client ?? client).post< UpdatePetWithFormResponses, UpdatePetWithFormErrors, ThrowOnError @@ -231,12 +230,13 @@ export const updatePetWithForm = ( /** * Uploads an image. + * * Upload image of the pet. */ export const uploadFile = ( options: Options ) => - (options.client ?? _heyApiClient).post({ + (options.client ?? client).post({ bodySerializer: null, security: [ { @@ -254,12 +254,13 @@ export const uploadFile = ( /** * Returns pet inventories by status. + * * Returns a map of status codes to quantities. */ export const getInventory = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ security: [ { name: 'api_key', @@ -272,12 +273,13 @@ export const getInventory = ( /** * Place an order for a pet. + * * Place a new order in the store. */ export const placeOrder = ( options?: Options ) => - (options?.client ?? _heyApiClient).post({ + (options?.client ?? client).post({ url: '/store/order', ...options, headers: { @@ -288,36 +290,39 @@ export const placeOrder = ( /** * Delete purchase order by identifier. + * * For valid response try integer IDs with value < 1000. Anything above 1000 or non-integers will generate API errors. */ export const deleteOrder = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ url: '/store/order/{orderId}', ...options }) /** * Find purchase order by ID. + * * For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions. */ export const getOrderById = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ url: '/store/order/{orderId}', ...options }) /** * Create user. + * * This can only be done by the logged in user. */ export const createUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).post({ + (options?.client ?? client).post({ url: '/user', ...options, headers: { @@ -328,12 +333,13 @@ export const createUser = ( /** * Creates list of users with given input array. + * * Creates list of users with given input array. */ export const createUsersWithListInput = ( options?: Options ) => - (options?.client ?? _heyApiClient).post< + (options?.client ?? client).post< CreateUsersWithListInputResponses, CreateUsersWithListInputErrors, ThrowOnError @@ -348,60 +354,65 @@ export const createUsersWithListInput = ( /** * Logs user into the system. + * * Log into the system. */ export const loginUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ url: '/user/login', ...options }) /** * Logs out current logged in user session. + * * Log user out of the system. */ export const logoutUser = ( options?: Options ) => - (options?.client ?? _heyApiClient).get({ + (options?.client ?? client).get({ url: '/user/logout', ...options }) /** * Delete user resource. + * * This can only be done by the logged in user. */ export const deleteUser = ( options: Options ) => - (options.client ?? _heyApiClient).delete({ + (options.client ?? client).delete({ url: '/user/{username}', ...options }) /** * Get user by user name. + * * Get user detail based on username. */ export const getUserByName = ( options: Options ) => - (options.client ?? _heyApiClient).get({ + (options.client ?? client).get({ url: '/user/{username}', ...options }) /** * Update user resource. + * * This can only be done by the logged in user. */ export const updateUser = ( options: Options ) => - (options.client ?? _heyApiClient).put({ + (options.client ?? client).put({ url: '/user/{username}', ...options, headers: { diff --git a/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts b/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts index 988a72c69..99ce8e7d8 100644 --- a/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts +++ b/examples/openapi-ts-tanstack-vue-query/src/client/types.gen.ts @@ -1,5 +1,9 @@ // This file is auto-generated by @hey-api/openapi-ts +export type ClientOptions = { + baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}) +} + export type Order = { complete?: boolean id?: number @@ -136,11 +140,11 @@ export type UpdatePetResponse = UpdatePetResponses[keyof UpdatePetResponses] export type FindPetsByStatusData = { body?: never path?: never - query?: { + query: { /** * Status values that need to be considered for filter */ - status?: 'available' | 'pending' | 'sold' + status: 'available' | 'pending' | 'sold' } url: '/pet/findByStatus' } @@ -168,11 +172,11 @@ export type FindPetsByStatusResponse = FindPetsByStatusResponses[keyof FindPetsB export type FindPetsByTagsData = { body?: never path?: never - query?: { + query: { /** * Tags to filter by */ - tags?: Array + tags: Array } url: '/pet/findByTags' } @@ -555,7 +559,7 @@ export type LoginUserResponses = { /** * successful operation */ - 200: Blob | File + 200: string } export type LoginUserResponse = LoginUserResponses[keyof LoginUserResponses] @@ -687,7 +691,3 @@ export type UpdateUserResponses = { */ 200: unknown } - -export type ClientOptions = { - baseUrl: 'https://petstore3.swagger.io/api/v3' | (string & {}) -} diff --git a/package.json b/package.json index ff853ff02..edfd7bf1e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "scripts": { "build": "turbo run build --filter=\"!@example/openapi-ts-sample\"", "changeset": "changeset", - "example": "sh ./scripts/example.sh", + "examples:check": "sh ./scripts/examples-check.sh", + "examples:generate": "sh ./scripts/examples-generate.sh", "format": "prettier --write .", "lint:fix": "prettier --check --write . && eslint . --fix", "lint": "prettier --check . && eslint .", @@ -52,7 +53,7 @@ "@vitest/coverage-v8": "3.1.1", "dotenv": "17.2.3", "eslint": "9.17.0", - "eslint-config-prettier": "9.1.0", + "eslint-config-prettier": "9.1.2", "eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-sort-destructure-keys": "2.0.0", "eslint-plugin-sort-keys-fix": "1.1.2", diff --git a/packages/codegen-core/CHANGELOG.md b/packages/codegen-core/CHANGELOG.md index 42403d9e9..e5e1af5d4 100644 --- a/packages/codegen-core/CHANGELOG.md +++ b/packages/codegen-core/CHANGELOG.md @@ -1,5 +1,11 @@ # @hey-api/codegen-core +## 0.3.1 + +### Patch Changes + +- feat: add `isRegistered()` method to file and symbol registry ([#2812](https://github.com/hey-api/openapi-ts/pull/2812)) ([`022f7dd`](https://github.com/hey-api/openapi-ts/commit/022f7dd0bcb74d2da855f393e2175645aa670351)) by [@mrlubos](https://github.com/mrlubos) + ## 0.3.0 ### Minor Changes diff --git a/packages/codegen-core/package.json b/packages/codegen-core/package.json index ea906a9da..b561a49ec 100644 --- a/packages/codegen-core/package.json +++ b/packages/codegen-core/package.json @@ -1,6 +1,6 @@ { "name": "@hey-api/codegen-core", - "version": "0.3.0", + "version": "0.3.1", "description": "🧱 TypeScript framework for generating structured, multi-file source code from abstract syntax trees.", "homepage": "https://heyapi.dev/", "repository": { diff --git a/packages/codegen-core/src/__tests__/files.test.ts b/packages/codegen-core/src/__tests__/files.test.ts index a90011a23..ef376d1a1 100644 --- a/packages/codegen-core/src/__tests__/files.test.ts +++ b/packages/codegen-core/src/__tests__/files.test.ts @@ -28,6 +28,10 @@ describe('FileRegistry', () => { expect(registry.get(file1.id)).toEqual(file1); expect(registry.get(['foo'])).toEqual(file1); + // isRegistered should be true for explicitly registered files + expect(registry.isRegistered(file1.id)).toBe(true); + expect(registry.isRegistered(['foo'])).toBe(true); + // Registering again with same selector returns same file const file1b = registry.register({ selector: ['foo'] }); expect(file1b).toEqual(file1); @@ -76,6 +80,17 @@ describe('FileRegistry', () => { expect(referenced).toContainEqual(file3); // Once registered, file1 is not in referenced set expect(referenced).not.toContainEqual(file1); + + // Referenced-only file should not be considered registered + expect(registry.isRegistered(file3.id)).toBe(false); + // Once registered, file3 becomes registered and no longer appears in referenced() + const file3Registered = registry.register({ + name: 'Baz', + selector: ['baz'], + }); + expect(registry.isRegistered(file3Registered.id)).toBe(true); + const referencedAfter = Array.from(registry.referenced()); + expect(referencedAfter).not.toContainEqual(file3Registered); }); it('throws on invalid register or reference', () => { diff --git a/packages/codegen-core/src/__tests__/symbols.test.ts b/packages/codegen-core/src/__tests__/symbols.test.ts index 4d905797f..dfc85b2c6 100644 --- a/packages/codegen-core/src/__tests__/symbols.test.ts +++ b/packages/codegen-core/src/__tests__/symbols.test.ts @@ -27,6 +27,10 @@ describe('SymbolRegistry', () => { expect(registry.get(symbol1.id)).toEqual(symbol1); expect(registry.get(['foo'])).toEqual(symbol1); + // isRegistered should be true for explicitly registered symbols + expect(registry.isRegistered(symbol1.id)).toBe(true); + expect(registry.isRegistered(['foo'])).toBe(true); + // Registering again with same selector returns same symbol const symbol1b = registry.register({ selector: ['foo'] }); expect(symbol1b).toEqual(symbol1); @@ -75,6 +79,15 @@ describe('SymbolRegistry', () => { registry.setValue(symbol1.id, 42); expect(registry.hasValue(symbol1.id)).toBe(true); expect(registry.getValue(symbol1.id)).toBe(42); + + // referenced-only symbol should not be registered until register() with data + const symRef = registry.reference(['qux']); + expect(registry.isRegistered(symRef.id)).toBe(false); + const symRegistered = registry.register({ + placeholder: 'Qux', + selector: ['qux'], + }); + expect(registry.isRegistered(symRegistered.id)).toBe(true); }); it('throws on invalid register or reference', () => { diff --git a/packages/codegen-core/src/files/registry.ts b/packages/codegen-core/src/files/registry.ts index f0aca2521..e5c9f47d5 100644 --- a/packages/codegen-core/src/files/registry.ts +++ b/packages/codegen-core/src/files/registry.ts @@ -43,6 +43,11 @@ export class FileRegistry implements IFileRegistry { : { selector: symbolIdOrSelector }; } + isRegistered(fileIdOrSelector: number | ISelector): boolean { + const file = this.get(fileIdOrSelector); + return file ? this.registerOrder.has(file.id) : false; + } + reference(fileIdOrSelector: number | ISelector): IFileOut { const file = this.idOrSelector(fileIdOrSelector); return this.register(file); @@ -104,6 +109,9 @@ export class FileRegistry implements IFileRegistry { if (hasOtherKeys) { this.registerOrder.add(id); + if (this.referenceOrder.has(id)) { + this.referenceOrder.delete(id); + } } else { this.referenceOrder.add(id); } diff --git a/packages/codegen-core/src/files/types.d.ts b/packages/codegen-core/src/files/types.d.ts index 6ba2f7b80..2c9f37ae2 100644 --- a/packages/codegen-core/src/files/types.d.ts +++ b/packages/codegen-core/src/files/types.d.ts @@ -82,6 +82,13 @@ export interface IFileRegistry { * @returns File ID before being incremented */ readonly id: number; + /** + * Returns whether a file is registered in the registry. + * + * @param fileIdOrSelector File ID or selector to check. + * @returns True if the file is registered, false otherwise. + */ + isRegistered(fileIdOrSelector: number | ISelector): boolean; /** * Returns a file by ID or selector, registering it if it doesn't exist. * diff --git a/packages/codegen-core/src/symbols/registry.ts b/packages/codegen-core/src/symbols/registry.ts index a98987642..22e78e61d 100644 --- a/packages/codegen-core/src/symbols/registry.ts +++ b/packages/codegen-core/src/symbols/registry.ts @@ -51,6 +51,11 @@ export class SymbolRegistry implements ISymbolRegistry { : { selector: symbolIdOrSelector }; } + isRegistered(symbolIdOrSelector: number | ISelector): boolean { + const symbol = this.get(symbolIdOrSelector); + return symbol ? this.registerOrder.has(symbol.id) : false; + } + reference(symbolIdOrSelector: number | ISelector): ISymbolOut { const symbol = this.idOrSelector(symbolIdOrSelector); return this.register(symbol); diff --git a/packages/codegen-core/src/symbols/types.d.ts b/packages/codegen-core/src/symbols/types.d.ts index cd8f68111..037575da8 100644 --- a/packages/codegen-core/src/symbols/types.d.ts +++ b/packages/codegen-core/src/symbols/types.d.ts @@ -113,6 +113,13 @@ export interface ISymbolRegistry { * @returns Symbol ID before being incremented. */ readonly id: number; + /** + * Returns whether a symbol is registered in the registry. + * + * @param symbolIdOrSelector Symbol ID or selector to check. + * @returns True if the symbol is registered, false otherwise. + */ + isRegistered(symbolIdOrSelector: number | ISelector): boolean; /** * Returns a symbol by ID or selector, registering it if it doesn't exist. * diff --git a/packages/config-vite-base/src/vitest.base.config.ts b/packages/config-vite-base/src/vitest.base.config.ts index 3d84f7be7..d9167e834 100644 --- a/packages/config-vite-base/src/vitest.base.config.ts +++ b/packages/config-vite-base/src/vitest.base.config.ts @@ -1,4 +1,5 @@ -import { platform } from 'os'; +import { platform } from 'node:os'; + import type { ViteUserConfig } from 'vitest/config'; import { configDefaults, defineConfig, mergeConfig } from 'vitest/config'; diff --git a/packages/custom-client/src/plugin.ts b/packages/custom-client/src/plugin.ts index 7d4ddc8d7..a69653309 100644 --- a/packages/custom-client/src/plugin.ts +++ b/packages/custom-client/src/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/package.json b/packages/openapi-ts-tests/main/package.json index a53508c59..9a76ba135 100644 --- a/packages/openapi-ts-tests/main/package.json +++ b/packages/openapi-ts-tests/main/package.json @@ -15,9 +15,9 @@ "node": ">=20.19.0" }, "devDependencies": { - "@angular-devkit/build-angular": "19.2.17", + "@angular-devkit/build-angular": "19.2.18", "@angular/animations": "19.2.15", - "@angular/cli": "19.2.17", + "@angular/cli": "19.2.18", "@angular/common": "19.2.15", "@angular/compiler": "19.2.15", "@angular/compiler-cli": "19.2.15", @@ -37,6 +37,7 @@ "@tanstack/svelte-query": "5.73.3", "@tanstack/vue-query": "5.73.3", "@types/cross-spawn": "6.0.6", + "arktype": "2.1.23", "axios": "1.8.2", "cross-spawn": "7.0.6", "eslint": "9.17.0", diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts index b7df7b1fe..c13ac1557 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts @@ -2,15 +2,6 @@ import * as v from 'valibot'; -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -export const vExternalRefA = vExternalSharedExternalSharedModel; - -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -130,16 +112,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.array(v.string()); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -153,16 +125,6 @@ export const vArrayWithProperties = v.array(v.object({ */ export const vDictionaryWithString = v.object({}); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -195,6 +157,35 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -258,30 +249,6 @@ export const vModelWithNestedEnums = v.object({ arrayWithDescription: v.optional(v.array(v.pipe(v.number(), v.integer()))) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -307,6 +274,30 @@ export const vModelWithCircularReference: v.GenericSchema = v.object({ })) }); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -407,6 +398,15 @@ export const vFailureFailure = v.object({ reference_code: v.optional(v.string()) }); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +export const vExternalRefA = vExternalSharedExternalSharedModel; + +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/content-types/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/content-types/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/content-types/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/content-types/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/parameter-explode-false-axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/parameter-explode-false-axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/parameter-explode-false-axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/parameter-explode-false-axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/react-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts index 5228a128e..058fdff2a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts @@ -7,15 +7,6 @@ import * as v from 'valibot'; */ export const v400 = v.string(); -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -export const vExternalRefA = vExternalSharedExternalSharedModel; - -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -/** - * This is a simple reference - */ -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -158,16 +137,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.optional(v.array(v.string()), ['test']); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -202,16 +171,6 @@ export const vDictionaryWithPropertiesAndAdditionalProperties = v.object({ bar: v.optional(v.boolean()) }); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -239,6 +198,38 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +/** + * This is a simple reference + */ +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -336,48 +327,6 @@ export const vModelWithNestedEnums = v.object({ ])) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([ - v.string(), - v.null() - ]), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - -export const vModelWithReadOnlyAndWriteOnlyWritable = v.object({ - foo: v.string(), - baz: v.string() -}); - -/** - * This is a model with one property containing an array - */ -export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ - prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: v.optional(v.array(v.string())), - propWithNumber: v.optional(v.array(v.number())) -}); - /** * This is a model with one property containing an array */ @@ -628,6 +577,34 @@ export const vCompositionExtendedModel = v.intersect([ }) ]); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + requiredAndNullable: v.union([ + v.string(), + v.null() + ]), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -769,29 +746,6 @@ export const vNestedAnyOfArraysNullable = v.object({ ])) }); -/** - * This is a reusable parameter - */ -export const vSimpleParameter = v.unknown(); - -export const vCompositionWithOneOfAndProperties = v.intersect([ - v.union([ - v.object({ - foo: vSimpleParameter - }), - v.object({ - bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 - }) - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * An object that can be null */ @@ -913,22 +867,6 @@ export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const vImport = v.string(); - -export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ - v.union([ - v.number(), - vImport - ]), - v.union([ - v.number(), - vImport - ]) -]); - export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([ v.intersect([ v.number(), @@ -951,20 +889,6 @@ export const vModelWithBackticksInDescription = v.object({ template: v.optional(v.string()) }); -export const vModelWithOneOfAndProperties = v.intersect([ - v.union([ - vSimpleParameter, - vNonAsciiStringæøåÆøÅöôêÊ字符串 - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * Model used to test deduplication strategy (unused) */ @@ -990,6 +914,22 @@ export const vDeleteFooData = v.string(); */ export const vDeleteFooData2 = v.string(); +/** + * Model with restricted keyword name + */ +export const vImport = v.string(); + +export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ + v.union([ + v.number(), + vImport + ]), + v.union([ + v.number(), + vImport + ]) +]); + export const vSchemaWithFormRestrictedKeys = v.object({ description: v.optional(v.string()), 'x-enum-descriptions': v.optional(v.string()), @@ -1075,6 +1015,15 @@ export const vOneOfAllOfIssue = v.union([ vGenericSchemaDuplicateIssue1SystemString ]); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +export const vExternalRefA = vExternalSharedExternalSharedModel; + +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1110,6 +1059,20 @@ export const vFileWritable = v.object({ mime: v.pipe(v.string(), v.minLength(1), v.maxLength(24)) }); +export const vModelWithReadOnlyAndWriteOnlyWritable = v.object({ + foo: v.string(), + baz: v.string() +}); + +/** + * This is a model with one property containing an array + */ +export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ + prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: v.optional(v.array(v.string())), + propWithNumber: v.optional(v.array(v.number())) +}); + export const vAdditionalPropertiesUnknownIssueWritable = v.object({}); export const vGenericSchemaDuplicateIssue1SystemBooleanWritable = v.object({ @@ -1132,6 +1095,43 @@ export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.object({ ])) }); +/** + * This is a reusable parameter + */ +export const vSimpleParameter = v.string(); + +export const vCompositionWithOneOfAndProperties = v.intersect([ + v.union([ + v.object({ + foo: vSimpleParameter + }), + v.object({ + bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 + }) + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + +export const vModelWithOneOfAndProperties = v.intersect([ + v.union([ + vSimpleParameter, + vNonAsciiStringæøåÆøÅöôêÊ字符串 + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts index 9769db8fa..b327f2c83 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts @@ -2,16 +2,14 @@ import * as v from 'valibot'; -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -20,4 +18,6 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); +export const vBar = v.object({ + foo: v.optional(vFoo) +}); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-false/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-false/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-false/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-false/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-number/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-number/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-number/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-number/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-strict/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-strict/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-strict/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-strict/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-string/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-string/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-string/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/base-url-string/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/clean-false/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/clean-false/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/clean-false/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/clean-false/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/default/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/default/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/default/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/default/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/import-file-extension-ts/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/import-file-extension-ts/client/client.gen.ts index f458a0d4b..8bd1d5631 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/import-file-extension-ts/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/import-file-extension-ts/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen.ts'; import type { HttpMethod } from '../core/types.gen.ts'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-optional/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-optional/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-optional/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-optional/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-required/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-required/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-required/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/sdk-client-required/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/tsconfig-nodenext-sdk/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/tsconfig-nodenext-sdk/client/client.gen.ts index 35f02190c..f0000c3c1 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/tsconfig-nodenext-sdk/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/@hey-api/client-axios/tsconfig-nodenext-sdk/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen.js'; import type { HttpMethod } from '../core/types.gen.js'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-false/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-number/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-strict/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/base-url-string/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/bundle/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/default/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-optional/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/clients/my-client/sdk-client-required/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/content-types/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/content-types/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/content-types/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/content-types/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/parameter-explode-false-axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/parameter-explode-false-axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/parameter-explode-false-axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/parameter-explode-false-axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/angular-query-experimental/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/react-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/react-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/react-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/react-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/solid-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/svelte-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/@tanstack/vue-query/axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts index 2d52f4609..4d127f74b 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts @@ -7,21 +7,6 @@ import * as v from 'valibot'; */ export const v400 = v.string(); -export const vExternalSharedExternalSharedModel = v.object({ - id: v.string(), - name: v.optional(v.string()) -}); - -/** - * External ref to shared model (A) - */ -export const vExternalRefA = vExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const vExternalRefB = vExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const vNonAsciiStringæøåÆøÅöôêÊ字符串 = v.string(); */ export const vSimpleFile = v.string(); -/** - * This is a model with one string property - */ -export const vModelWithString = v.object({ - prop: v.optional(v.string()) -}); - -/** - * This is a simple reference - */ -export const vSimpleReference = vModelWithString; - /** * This is a simple string */ @@ -164,16 +137,6 @@ export const vArrayWithBooleans = v.array(v.boolean()); */ export const vArrayWithStrings = v.optional(v.array(v.string()), ['test']); -/** - * This is a simple array with references - */ -export const vArrayWithReferences = v.array(vModelWithString); - -/** - * This is a simple array containing an array - */ -export const vArrayWithArray = v.array(v.array(vModelWithString)); - /** * This is a simple array with properties */ @@ -211,16 +174,6 @@ export const vDictionaryWithPropertiesAndAdditionalProperties = v.object({ bar: v.optional(v.boolean()) }); -/** - * This is a string reference - */ -export const vDictionaryWithReference = v.object({}); - -/** - * This is a complex dictionary - */ -export const vDictionaryWithArray = v.object({}); - /** * This is a string dictionary */ @@ -248,6 +201,38 @@ export const vModelWithBoolean = v.object({ prop: v.optional(v.boolean()) }); +/** + * This is a model with one string property + */ +export const vModelWithString = v.object({ + prop: v.optional(v.string()) +}); + +/** + * This is a simple reference + */ +export const vSimpleReference = vModelWithString; + +/** + * This is a simple array with references + */ +export const vArrayWithReferences = v.array(vModelWithString); + +/** + * This is a simple array containing an array + */ +export const vArrayWithArray = v.array(v.array(vModelWithString)); + +/** + * This is a string reference + */ +export const vDictionaryWithReference = v.object({}); + +/** + * This is a complex dictionary + */ +export const vDictionaryWithArray = v.object({}); + /** * This is a model with one string property */ @@ -345,48 +330,6 @@ export const vModelWithNestedEnums = v.object({ ])) }); -/** - * This is a model with one nested property - */ -export const vModelWithProperties = v.object({ - required: v.string(), - requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([ - v.string(), - v.null() - ]), - string: v.optional(v.string()), - number: v.optional(v.number()), - boolean: v.optional(v.boolean()), - reference: v.optional(vModelWithString), - 'property with space': v.optional(v.string()), - default: v.optional(v.string()), - try: v.optional(v.string()), - '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), - '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) -}); - -/** - * This is a model with one property containing a reference - */ -export const vModelWithReference = v.object({ - prop: v.optional(vModelWithProperties) -}); - -export const vModelWithReadOnlyAndWriteOnlyWritable = v.object({ - foo: v.string(), - baz: v.string() -}); - -/** - * This is a model with one property containing an array - */ -export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ - prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: v.optional(v.array(v.string())), - propWithNumber: v.optional(v.array(v.number())) -}); - /** * This is a model with one property containing an array */ @@ -629,6 +572,34 @@ export const vCompositionExtendedModel = v.intersect([ }) ]); +/** + * This is a model with one nested property + */ +export const vModelWithProperties = v.object({ + required: v.string(), + requiredAndReadOnly: v.pipe(v.string(), v.readonly()), + requiredAndNullable: v.union([ + v.string(), + v.null() + ]), + string: v.optional(v.string()), + number: v.optional(v.number()), + boolean: v.optional(v.boolean()), + reference: v.optional(vModelWithString), + 'property with space': v.optional(v.string()), + default: v.optional(v.string()), + try: v.optional(v.string()), + '@namespace.string': v.optional(v.pipe(v.string(), v.readonly())), + '@namespace.integer': v.optional(v.pipe(v.pipe(v.number(), v.integer()), v.readonly())) +}); + +/** + * This is a model with one property containing a reference + */ +export const vModelWithReference = v.object({ + prop: v.optional(vModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -766,29 +737,6 @@ export const vNestedAnyOfArraysNullable = v.object({ ])) }); -/** - * This is a reusable parameter - */ -export const vSimpleParameter = v.unknown(); - -export const vCompositionWithOneOfAndProperties = v.intersect([ - v.union([ - v.object({ - foo: vSimpleParameter - }), - v.object({ - bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 - }) - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * An object that can be null */ @@ -917,22 +865,6 @@ export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const vImport = v.string(); - -export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ - v.union([ - v.number(), - vImport - ]), - v.union([ - v.number(), - vImport - ]) -]); - export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([ v.intersect([ v.number(), @@ -955,20 +887,6 @@ export const vModelWithBackticksInDescription = v.object({ template: v.optional(v.string()) }); -export const vModelWithOneOfAndProperties = v.intersect([ - v.union([ - vSimpleParameter, - vNonAsciiStringæøåÆøÅöôêÊ字符串 - ]), - v.object({ - baz: v.union([ - v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), - v.null() - ]), - qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) - }) -]); - /** * Model used to test deduplication strategy (unused) */ @@ -994,6 +912,22 @@ export const vDeleteFooData = v.string(); */ export const vDeleteFooData2 = v.string(); +/** + * Model with restricted keyword name + */ +export const vImport = v.string(); + +export const vModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = v.tuple([ + v.union([ + v.number(), + vImport + ]), + v.union([ + v.number(), + vImport + ]) +]); + export const vSchemaWithFormRestrictedKeys = v.object({ description: v.optional(v.string()), 'x-enum-descriptions': v.optional(v.string()), @@ -1079,6 +1013,21 @@ export const vOneOfAllOfIssue = v.union([ vGenericSchemaDuplicateIssue1SystemString ]); +export const vExternalSharedExternalSharedModel = v.object({ + id: v.string(), + name: v.optional(v.string()) +}); + +/** + * External ref to shared model (A) + */ +export const vExternalRefA = vExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const vExternalRefB = vExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1114,6 +1063,20 @@ export const vFileWritable = v.object({ mime: v.pipe(v.string(), v.minLength(1), v.maxLength(24)) }); +export const vModelWithReadOnlyAndWriteOnlyWritable = v.object({ + foo: v.string(), + baz: v.string() +}); + +/** + * This is a model with one property containing an array + */ +export const vModelWithArrayReadOnlyAndWriteOnly = v.object({ + prop: v.optional(v.array(vModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: v.optional(v.array(v.string())), + propWithNumber: v.optional(v.array(v.number())) +}); + export const vAdditionalPropertiesUnknownIssueWritable = v.object({}); export const vGenericSchemaDuplicateIssue1SystemBooleanWritable = v.object({ @@ -1136,6 +1099,43 @@ export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.object({ ])) }); +/** + * This is a reusable parameter + */ +export const vSimpleParameter = v.string(); + +export const vCompositionWithOneOfAndProperties = v.intersect([ + v.union([ + v.object({ + foo: vSimpleParameter + }), + v.object({ + bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 + }) + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + +export const vModelWithOneOfAndProperties = v.intersect([ + v.union([ + vSimpleParameter, + vNonAsciiStringæøåÆøÅöôêÊ字符串 + ]), + v.object({ + baz: v.union([ + v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint16 to be >= 0'), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 2^16-1'), v.minValue(0)), + v.null() + ]), + qux: v.pipe(v.number(), v.integer(), v.minValue(0, 'Invalid value: Expected uint8 to be >= 0'), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 2^8-1'), v.minValue(0)) + }) +]); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-axios/client/client.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-axios/client/client.gen.ts index aea2484c7..f81a9e786 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-axios/client/client.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/sse-axios/client/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../core/serverSentEvents.gen'; import type { HttpMethod } from '../core/types.gen'; @@ -69,6 +69,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -86,7 +87,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -105,22 +105,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts index 11af4d8fc..769fabf36 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts @@ -11,6 +11,6 @@ export const vBar: v.GenericSchema = v.object({ ]) }); -export const vFoo: v.GenericSchema = v.object({ +export const vFoo = v.object({ foo: vBar }); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts index c32b0b517..f0dfcc016 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref/valibot.gen.ts @@ -8,15 +8,15 @@ export const vBar: v.GenericSchema = v.object({ }))) }); -export const vFoo: v.GenericSchema = v.object({ +export const vFoo = v.object({ foo: v.optional(vBar) }); -/** - * description caused circular reference error - */ -export const vQux: v.GenericSchema = v.lazy(() => { +export const vBaz: v.GenericSchema = v.lazy(() => { return vQux; }); -export const vBaz: v.GenericSchema = vQux; +/** + * description caused circular reference error + */ +export const vQux = vBaz; diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts index c54dd93e8..0f25138de 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts @@ -2,15 +2,10 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.pipe(v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}), v.metadata({ - description: 'This is Bar schema.' +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) })); /** @@ -21,7 +16,9 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ foo: v.optional(v.pipe(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/)), v.metadata({ description: 'This is foo property.' }))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.pipe(v.array(v.lazy(() => { return vFoo; })), v.metadata({ @@ -34,10 +31,13 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) +/** + * This is Bar schema. + */ +export const vBar = v.pipe(v.object({ + foo: v.optional(vFoo) +}), v.metadata({ + description: 'This is Bar schema.' })); /** diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts index 3c12cbfaa..72e55fa04 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts @@ -2,14 +2,11 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const vBar: v.GenericSchema = v.object({ export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -26,11 +25,12 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) -})); +/** + * This is Bar schema. + */ +export const vBar = v.object({ + foo: v.optional(vFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts index 3c12cbfaa..72e55fa04 100644 --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts @@ -2,14 +2,11 @@ import * as v from 'valibot'; -/** - * This is Bar schema. - */ -export const vBar: v.GenericSchema = v.object({ - foo: v.optional(v.lazy(() => { - return vFoo; - })) -}); +export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); + +export const vQux = v.record(v.string(), v.object({ + qux: v.optional(v.string()) +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const vBar: v.GenericSchema = v.object({ export const vFoo: v.GenericSchema = v.optional(v.union([ v.object({ foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(vBar), + bar: v.optional(v.lazy(() => { + return vBar; + })), baz: v.optional(v.array(v.lazy(() => { return vFoo; }))), @@ -26,11 +25,12 @@ export const vFoo: v.GenericSchema = v.optional(v.union([ v.null() ]), null); -export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); - -export const vQux = v.record(v.string(), v.object({ - qux: v.optional(v.string()) -})); +/** + * This is Bar schema. + */ +export const vBar = v.object({ + foo: v.optional(vFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/main/test/custom/client/plugin.ts b/packages/openapi-ts-tests/main/test/custom/client/plugin.ts index 83305b41f..c11a1606a 100644 --- a/packages/openapi-ts-tests/main/test/custom/client/plugin.ts +++ b/packages/openapi-ts-tests/main/test/custom/client/plugin.ts @@ -17,13 +17,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Pick) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts index 99a651ac5..fb1858316 100644 --- a/packages/openapi-ts-tests/main/test/openapi-ts.config.ts +++ b/packages/openapi-ts-tests/main/test/openapi-ts.config.ts @@ -34,18 +34,20 @@ export default defineConfig(() => { // }, path: path.resolve( getSpecsPath(), - '3.0.x', + // '3.0.x', + '3.1.x', // 'circular.yaml', - 'dutchie.json', + // 'dutchie.json', // 'invalid', // 'openai.yaml', - // 'full.yaml', + 'full.yaml', // 'opencode.yaml', // 'sdk-instance.yaml', // 'string-with-format.yaml', // 'transformers.json', // 'type-format.yaml', // 'validators.yaml', + // 'validators-circular-ref.json', // 'validators-circular-ref-2.yaml', // 'zoom-video-sdk.json', ), @@ -213,7 +215,7 @@ export default defineConfig(() => { // }, }, { - asClass: true, + // asClass: true, // auth: false, // classNameBuilder: '{{name}}', // classNameBuilder: '{{name}}Service', @@ -234,10 +236,10 @@ export default defineConfig(() => { // signature: 'object', // transformer: '@hey-api/transformers', // transformer: true, - // validator: { - // request: 'valibot', - // response: 'zod', - // }, + validator: { + request: 'arktype', + response: 'arktype', + }, '~hooks': { symbols: { // getFilePath: (symbol) => { @@ -273,7 +275,7 @@ export default defineConfig(() => { // mutationOptions: { // name: '{{name}}MO', // }, - // name: '@tanstack/react-query', + name: '@tanstack/react-query', // queryKeys: { // name: '{{name}}QK', // }, @@ -299,13 +301,19 @@ export default defineConfig(() => { }, }, }, + { + // name: 'arktype', + // types: { + // infer: true, + // }, + }, { // case: 'SCREAMING_SNAKE_CASE', // comments: false, // definitions: 'z{{name}}Definition', exportFromIndex: true, // metadata: true, - // name: 'valibot', + name: 'valibot', // requests: { // case: 'PascalCase', // name: '{{name}}Data', @@ -334,7 +342,7 @@ export default defineConfig(() => { { // case: 'snake_case', // comments: false, - compatibilityVersion: 'mini', + compatibilityVersion: 4, dates: { local: true, // offset: true, @@ -346,8 +354,8 @@ export default defineConfig(() => { // }, }, exportFromIndex: true, - metadata: true, - // name: 'zod', + // metadata: true, + name: 'zod', // requests: { // // case: 'SCREAMING_SNAKE_CASE', // // name: 'z{{name}}TestData', diff --git a/packages/openapi-ts-tests/main/vitest.config.ts b/packages/openapi-ts-tests/main/vitest.config.ts index d13ef5fee..7c627e64f 100644 --- a/packages/openapi-ts-tests/main/vitest.config.ts +++ b/packages/openapi-ts-tests/main/vitest.config.ts @@ -1,7 +1,15 @@ +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createVitestConfig } from '@config/vite-base'; -export default createVitestConfig( - fileURLToPath(new URL('./', import.meta.url)), -); +const rootDir = fileURLToPath(new URL('./', import.meta.url)); + +export default createVitestConfig(rootDir, { + resolve: { + alias: [ + { find: /^~\/(.*)/, replacement: path.resolve(rootDir, 'src/$1') }, + { find: '~', replacement: path.resolve(rootDir, 'src') }, + ], + }, +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts index a914024a3..ac1852b36 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/mini/default/zod.gen.ts @@ -2,15 +2,6 @@ import * as z from 'zod/v4-mini'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts index fbea7e50b..2fe7ac3b5 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v3/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.array(z.number().int()).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - /** * This is a model with one property containing an array */ @@ -331,6 +298,30 @@ export const zModelWithCircularReference: z.AnyZodObject = z.object({ }).optional() }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -424,6 +415,15 @@ export const zFailureFailure = z.object({ reference_code: z.string().optional() }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts index bfe8ab735..c6888a77b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/2.0.x/v4/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod/v4'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts index 379f9a044..9b3bf4571 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -2,15 +2,32 @@ import * as z from 'zod/v4-mini'; -export const zBaz = z.object({ - get quux(): z.ZodMiniOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodMiniOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.intersection(z.object({ type: z.literal('struct') }), z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.intersection(z.object({ type: z.literal('array') - }), z.lazy(() => { - return zFoo; - })) + }), zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodMiniOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodMiniOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodMiniOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts index 7c52f84f7..c59f0663e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -7,15 +7,6 @@ import * as z from 'zod/v4-mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -947,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -987,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1023,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1108,6 +1054,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1143,6 +1098,20 @@ export const zFileWritable = z.object({ mime: z.string().check(z.minLength(1), z.maxLength(24)) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts index a8be5f51c..9ef941af5 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/mini/validators/zod.gen.ts @@ -2,17 +2,17 @@ import * as z from 'zod/v4-mini'; -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts index abc26f9e6..bcce4d295 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -2,38 +2,42 @@ import { z } from 'zod'; -export const zBaz: z.AnyZodObject = z.object({ +export const zFoo: z.AnyZodObject = z.object({ quux: z.lazy(() => { return zQuux; }).optional() }); -export const zCorge = z.object({ - baz: z.array(zBaz).optional() +export const zBar: z.AnyZodObject = z.object({ + bar: z.lazy(() => { + return zBar; + }).optional(), + baz: z.lazy(() => { + return zBaz; + }).optional() +}); + +export const zBaz: z.AnyZodObject = z.object({ + quux: z.lazy(() => { + return zQuux; + }).optional() }); export const zQux: z.ZodTypeAny = z.union([ z.object({ type: z.literal('struct') - }).and(zCorge), + }).and(z.lazy(() => { + return zCorge; + })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ qux: zQux.optional() }); -export const zFoo = z.object({ - quux: zQuux.optional() -}); - -export const zBar: z.AnyZodObject = z.object({ - bar: z.lazy(() => { - return zBar; - }).optional(), - baz: zBaz.optional() +export const zCorge = z.object({ + baz: z.array(zBaz).optional() }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts index e30ab69a3..234436a5b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -663,6 +612,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -800,26 +777,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -945,22 +902,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -985,17 +926,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1021,6 +951,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1106,6 +1052,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1141,6 +1096,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.union([ z.string(), z.number() @@ -1166,6 +1135,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts index a6537a847..13daadc5e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v3/validators/zod.gen.ts @@ -2,16 +2,14 @@ import { z } from 'zod'; -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -20,4 +18,6 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: zFoo.optional() +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 24c4880fa..073c23da8 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -2,15 +2,32 @@ import { z } from 'zod/v4'; -export const zBaz = z.object({ - get quux(): z.ZodOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.object({ type: z.literal('struct') }).and(z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts index f6c8d1e9a..01d6a1794 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod/v4'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -947,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -987,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1023,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1108,6 +1054,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1143,6 +1098,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts index d0fb4e7a8..529955454 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.0.x/v4/validators/zod.gen.ts @@ -2,17 +2,17 @@ import { z } from 'zod/v4'; -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts index e7bb85ad0..cba731f90 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -7,21 +7,6 @@ import * as z from 'zod/v4-mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -953,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -993,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1029,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1114,6 +1054,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1149,6 +1104,20 @@ export const zFileWritable = z.object({ mime: z.string().check(z.minLength(1), z.maxLength(24)) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts index 5fa9cefc1..4c5643d93 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/v4-mini'; export const zBar = z.object({ - get bar(): z.ZodMiniOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index 3fd3ebc43..b515009b6 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts index 1a5edf41c..64973d66e 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z._default(z.union([ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/)).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts index a3170d6a4..2b1c21593 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z._default(z.union([ export type FooZodType = z.infer; -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts index 5bcc24c6f..d3e255eb0 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/mini/validators/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/v4-mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts index bc3aa5338..f688e4c23 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -670,6 +613,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -803,26 +774,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -951,22 +902,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -991,17 +926,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1027,6 +951,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1112,6 +1052,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1147,6 +1102,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.union([ z.string(), z.number() @@ -1172,6 +1141,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts index 528fe0d3d..57536e866 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts @@ -12,11 +12,11 @@ export const zFoo = z.object({ foo: zBar.optional() }); -/** - * description caused circular reference error - */ -export const zQux: z.ZodTypeAny = z.lazy(() => { +export const zBaz: z.ZodTypeAny = z.lazy(() => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 6eb670417..620a0a864 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts index 585fddbb2..4a09748c7 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}).describe('This is Bar schema.'); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).describe('This is baz property.').optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}).describe('This is Bar schema.'); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts index ace814995..d79d307e9 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,7 +18,9 @@ export type BarZodType = z.infer; export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -30,15 +31,14 @@ export const zFoo: z.ZodTypeAny = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts index 83f84b2d0..bcda6a22d 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v3/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts index d1ad23385..d0956e4b2 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod/v4'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -953,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -993,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1029,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1114,6 +1054,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1149,6 +1104,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts index cc305b0c7..0fba2fc8b 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod/v4'; export const zBar = z.object({ - get bar(): z.ZodOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index fa1234e0a..36e6f721a 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts index 5e997ec27..270bc9a0d 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z.union([ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts index 2794c6a1e..c36f93b32 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts index f9c56845a..2b3781398 100644 --- a/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v3/__snapshots__/3.1.x/v4/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v4'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v3/vitest.config.ts b/packages/openapi-ts-tests/zod/v3/vitest.config.ts index d13ef5fee..7c627e64f 100644 --- a/packages/openapi-ts-tests/zod/v3/vitest.config.ts +++ b/packages/openapi-ts-tests/zod/v3/vitest.config.ts @@ -1,7 +1,15 @@ +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createVitestConfig } from '@config/vite-base'; -export default createVitestConfig( - fileURLToPath(new URL('./', import.meta.url)), -); +const rootDir = fileURLToPath(new URL('./', import.meta.url)); + +export default createVitestConfig(rootDir, { + resolve: { + alias: [ + { find: /^~\/(.*)/, replacement: path.resolve(rootDir, 'src/$1') }, + { find: '~', replacement: path.resolve(rootDir, 'src') }, + ], + }, +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts index b2bc94a14..e2f4e7158 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/mini/default/zod.gen.ts @@ -2,15 +2,6 @@ import * as z from 'zod/mini'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts index 151cc9a84..aa5e6cbd5 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v3/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod/v3'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.array(z.number().int()).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - /** * This is a model with one property containing an array */ @@ -331,6 +298,30 @@ export const zModelWithCircularReference: z.AnyZodObject = z.object({ }).optional() }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -424,6 +415,15 @@ export const zFailureFailure = z.object({ reference_code: z.string().optional() }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts index 54967ed06..9e1e6dbad 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/2.0.x/v4/default/zod.gen.ts @@ -2,15 +2,6 @@ import { z } from 'zod'; -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -74,15 +65,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -150,16 +132,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -173,16 +145,6 @@ export const zArrayWithProperties = z.array(z.object({ */ export const zDictionaryWithString = z.record(z.string(), z.string()); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -215,6 +177,35 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -282,30 +273,6 @@ export const zModelWithNestedEnums = z.object({ arrayWithDescription: z.optional(z.array(z.int())) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - /** * This is a model with one property containing an array */ @@ -326,13 +293,37 @@ export const zModelWithDictionary = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); } }); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -426,6 +417,15 @@ export const zFailureFailure = z.object({ reference_code: z.optional(z.string()) }); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts index c93fc275c..0c5d13033 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/circular/zod.gen.ts @@ -2,15 +2,32 @@ import * as z from 'zod/mini'; -export const zBaz = z.object({ - get quux(): z.ZodMiniOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodMiniOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.intersection(z.object({ type: z.literal('struct') }), z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.intersection(z.object({ type: z.literal('array') - }), z.lazy(() => { - return zFoo; - })) + }), zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodMiniOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodMiniOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodMiniOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts index 14f3146d0..ceab2b17d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/default/zod.gen.ts @@ -7,15 +7,6 @@ import * as z from 'zod/mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -947,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -987,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1023,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1108,6 +1054,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1143,6 +1098,20 @@ export const zFileWritable = z.object({ mime: z.string().check(z.minLength(1), z.maxLength(24)) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts index 77ab57f60..786ac3505 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/mini/validators/zod.gen.ts @@ -2,17 +2,17 @@ import * as z from 'zod/mini'; -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts index e73840e1d..6011ab3e7 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/circular/zod.gen.ts @@ -2,38 +2,42 @@ import { z } from 'zod/v3'; -export const zBaz: z.AnyZodObject = z.object({ +export const zFoo: z.AnyZodObject = z.object({ quux: z.lazy(() => { return zQuux; }).optional() }); -export const zCorge = z.object({ - baz: z.array(zBaz).optional() +export const zBar: z.AnyZodObject = z.object({ + bar: z.lazy(() => { + return zBar; + }).optional(), + baz: z.lazy(() => { + return zBaz; + }).optional() +}); + +export const zBaz: z.AnyZodObject = z.object({ + quux: z.lazy(() => { + return zQuux; + }).optional() }); export const zQux: z.ZodTypeAny = z.union([ z.object({ type: z.literal('struct') - }).and(zCorge), + }).and(z.lazy(() => { + return zCorge; + })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ qux: zQux.optional() }); -export const zFoo = z.object({ - quux: zQuux.optional() -}); - -export const zBar: z.AnyZodObject = z.object({ - bar: z.lazy(() => { - return zBar; - }).optional(), - baz: zBaz.optional() +export const zCorge = z.object({ + baz: z.array(zBaz).optional() }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts index e8e0ae922..92fc52898 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod/v3'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -663,6 +612,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -800,26 +777,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -945,22 +902,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -985,17 +926,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1021,6 +951,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1106,6 +1052,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1141,6 +1096,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.union([ z.string(), z.number() @@ -1166,6 +1135,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts index 1b13ab9cb..884fe8b89 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v3/validators/zod.gen.ts @@ -2,16 +2,14 @@ import { z } from 'zod/v3'; -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -20,4 +18,6 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: zFoo.optional() +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts index 50b4fafee..27ac2c0cc 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/circular/zod.gen.ts @@ -2,15 +2,32 @@ import { z } from 'zod'; -export const zBaz = z.object({ - get quux(): z.ZodOptional { - return z.optional(zQuux); +export const zFoo = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); -export const zCorge = z.object({ - get baz(): z.ZodOptional { - return z.optional(z.array(zBaz)); +export const zBar = z.object({ + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { + return z.optional(z.lazy((): any => { + return zBaz; + })); + } +}); + +export const zBaz = z.object({ + get quux() { + return z.optional(z.lazy((): any => { + return zQuux; + })); } }); @@ -18,32 +35,19 @@ export const zQux = z.union([ z.object({ type: z.literal('struct') }).and(z.lazy(() => { - return zCorge; + return z.lazy((): any => { + return zCorge; + }); })), z.object({ type: z.literal('array') - }).and(z.lazy(() => { - return zFoo; - })) + }).and(zFoo) ]); export const zQuux = z.object({ - get qux(): z.ZodOptional { - return z.optional(zQux); - } -}); - -export const zFoo = z.object({ - quux: z.optional(zQuux) + qux: z.optional(zQux) }); -export const zBar = z.object({ - get bar(): z.ZodOptional { - return z.optional(z.lazy((): any => { - return zBar; - })); - }, - get baz(): z.ZodOptional { - return z.optional(zBaz); - } +export const zCorge = z.object({ + baz: z.optional(z.array(zBaz)) }); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts index 91bd30691..8392226ff 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/default/zod.gen.ts @@ -7,15 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -export const zExternalRefA = zExternalSharedExternalSharedModel; - -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -87,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -183,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -241,16 +210,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -278,6 +237,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -379,48 +370,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -450,7 +399,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -665,6 +614,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -802,26 +779,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -947,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -987,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1023,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1108,6 +1054,15 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +export const zExternalRefA = zExternalSharedExternalSharedModel; + +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1143,6 +1098,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1168,6 +1137,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts index 7f3ce000b..f6b581f0d 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.0.x/v4/validators/zod.gen.ts @@ -2,17 +2,17 @@ import { z } from 'zod'; -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -22,4 +22,6 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); +export const zBar = z.object({ + foo: z.optional(zFoo) +}); diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts index bf3b88983..871c6a3ed 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/default/zod.gen.ts @@ -7,21 +7,6 @@ import * as z from 'zod/mini'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z._default(z.array(z.string()), ['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.readonly(z.string()), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.readonly(z.string())), - '@namespace.integer': z.optional(z.readonly(z.int())) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodMiniOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = z.intersection(zCompositionBaseModel, z lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.readonly(z.string()), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.readonly(z.string())), + '@namespace.integer': z.optional(z.readonly(z.int())) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * An object that can be null */ @@ -953,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -993,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().check(z.gte(0)), - z.null() - ]), - qux: z.int().check(z.gte(0)) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1029,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1114,6 +1054,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1149,6 +1104,20 @@ export const zFileWritable = z.object({ mime: z.string().check(z.minLength(1), z.maxLength(24)) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().check(z.gte(0)), + z.null() + ]), + qux: z.int().check(z.gte(0)) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts index 2e65ce311..33a0a739a 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import * as z from 'zod/mini'; export const zBar = z.object({ - get bar(): z.ZodMiniOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts index f5db2ce5c..d64c84a81 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts index 4b4759e2d..aaf540431 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z._default(z.union([ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/)).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts index 1eb0cfb95..19e14da91 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z._default(z.union([ export type FooZodType = z.infer; -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts index 0ee090a1a..65b32c7ba 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/mini/validators/zod.gen.ts @@ -2,14 +2,11 @@ import * as z from 'zod/mini'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodMiniOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z._default(z.union([ z.object({ foo: z.optional(z.string().check(z.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: z.optional(zBar), - get baz(): z.ZodMiniOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z._default(z.union([ z.null() ]), null); -export const zBaz = z._default(z.readonly(z.string().check(z.regex(/foo\nbar/))), 'baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts index 1e41ae15f..f42641820 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod/v3'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.string().optional() -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.string().optional() -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.boolean().optional() }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.boolean().optional() }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.string().optional() +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ]).optional() }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.string().optional(), - number: z.number().optional(), - boolean: z.boolean().optional(), - reference: zModelWithString.optional(), - 'property with space': z.string().optional(), - default: z.string().optional(), - try: z.string().optional(), - '@namespace.string': z.string().readonly().optional(), - '@namespace.integer': z.number().int().readonly().optional() -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: zModelWithProperties.optional() -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), - propWithFile: z.array(z.string()).optional(), - propWithNumber: z.array(z.number()).optional() -}); - /** * This is a model with one property containing an array */ @@ -670,6 +613,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.string().optional(), + number: z.number().optional(), + boolean: z.boolean().optional(), + reference: zModelWithString.optional(), + 'property with space': z.string().optional(), + default: z.string().optional(), + try: z.string().optional(), + '@namespace.string': z.string().readonly().optional(), + '@namespace.integer': z.number().int().readonly().optional() +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: zModelWithProperties.optional() +}); + /** * This is a model with one nested property */ @@ -803,26 +774,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ]).optional() }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.unknown(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * An object that can be null */ @@ -951,22 +902,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -991,17 +926,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.string().optional() }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.number().int().gte(0), - z.null() - ]), - qux: z.number().int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1027,6 +951,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.string().optional(), 'x-enum-descriptions': z.string().optional(), @@ -1112,6 +1052,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.string().optional() +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1147,6 +1102,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.array(zModelWithReadOnlyAndWriteOnlyWritable).optional(), + propWithFile: z.array(z.string()).optional(), + propWithNumber: z.array(z.number()).optional() +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.union([ z.string(), z.number() @@ -1172,6 +1141,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ]).optional() }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.number().int().gte(0), + z.null() + ]), + qux: z.number().int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts index 7e6cc3c89..466a9fc5b 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-circular-ref/zod.gen.ts @@ -12,11 +12,11 @@ export const zFoo = z.object({ foo: zBar.optional() }); -/** - * description caused circular reference error - */ -export const zQux: z.ZodTypeAny = z.lazy(() => { +export const zBaz: z.ZodTypeAny = z.lazy(() => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts index 140dc5e8c..746c1c574 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts index efc9209a2..396361b2a 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-metadata/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}).describe('This is Bar schema.'); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).describe('This is foo property.').optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).describe('This is baz property.').optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}).describe('This is Bar schema.'); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts index 399d2c413..7b92df5bc 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,7 +18,9 @@ export type BarZodType = z.infer; export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -30,15 +31,14 @@ export const zFoo: z.ZodTypeAny = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts index fcc9c8c55..d4c6fe536 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v3/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod/v3'; -/** - * This is Bar schema. - */ -export const zBar: z.AnyZodObject = z.object({ - foo: z.lazy(() => { - return zFoo; - }).optional() -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.object({ + qux: z.string().optional() +})); /** * This is Foo schema. @@ -17,7 +14,9 @@ export const zBar: z.AnyZodObject = z.object({ export const zFoo: z.ZodTypeAny = z.union([ z.object({ foo: z.string().regex(/^\d{3}-\d{2}-\d{4}$/).optional(), - bar: zBar.optional(), + bar: z.lazy(() => { + return zBar; + }).optional(), baz: z.array(z.lazy(() => { return zFoo; })).optional(), @@ -26,11 +25,12 @@ export const zFoo: z.ZodTypeAny = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.object({ - qux: z.string().optional() -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: zFoo.optional() +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts index dc74d17c6..766d9d790 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/default/zod.gen.ts @@ -7,21 +7,6 @@ import { z } from 'zod'; */ export const z400 = z.string(); -export const zExternalSharedExternalSharedModel = z.object({ - id: z.string(), - name: z.optional(z.string()) -}); - -/** - * External ref to shared model (A) - */ -export const zExternalRefA = zExternalSharedExternalSharedModel; - -/** - * External ref to shared model (B) - */ -export const zExternalRefB = zExternalSharedExternalSharedModel; - /** * Testing multiline comments in string: First line * Second line @@ -93,18 +78,6 @@ export const zNonAsciiStringæøåÆøÅöôêÊ字符串 = z.string(); */ export const zSimpleFile = z.string(); -/** - * This is a model with one string property - */ -export const zModelWithString = z.object({ - prop: z.optional(z.string()) -}); - -/** - * This is a simple reference - */ -export const zSimpleReference = zModelWithString; - /** * This is a simple string */ @@ -189,16 +162,6 @@ export const zArrayWithBooleans = z.array(z.boolean()); */ export const zArrayWithStrings = z.array(z.string()).default(['test']); -/** - * This is a simple array with references - */ -export const zArrayWithReferences = z.array(zModelWithString); - -/** - * This is a simple array containing an array - */ -export const zArrayWithArray = z.array(z.array(zModelWithString)); - /** * This is a simple array with properties */ @@ -250,16 +213,6 @@ export const zDictionaryWithPropertiesAndAdditionalProperties = z.object({ bar: z.optional(z.boolean()) }); -/** - * This is a string reference - */ -export const zDictionaryWithReference = z.record(z.string(), zModelWithString); - -/** - * This is a complex dictionary - */ -export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); - /** * This is a string dictionary */ @@ -287,6 +240,38 @@ export const zModelWithBoolean = z.object({ prop: z.optional(z.boolean()) }); +/** + * This is a model with one string property + */ +export const zModelWithString = z.object({ + prop: z.optional(z.string()) +}); + +/** + * This is a simple reference + */ +export const zSimpleReference = zModelWithString; + +/** + * This is a simple array with references + */ +export const zArrayWithReferences = z.array(zModelWithString); + +/** + * This is a simple array containing an array + */ +export const zArrayWithArray = z.array(z.array(zModelWithString)); + +/** + * This is a string reference + */ +export const zDictionaryWithReference = z.record(z.string(), zModelWithString); + +/** + * This is a complex dictionary + */ +export const zDictionaryWithArray = z.record(z.string(), z.array(zModelWithString)); + /** * This is a model with one string property */ @@ -388,48 +373,6 @@ export const zModelWithNestedEnums = z.object({ ])) }); -/** - * This is a model with one nested property - */ -export const zModelWithProperties = z.object({ - required: z.string(), - requiredAndReadOnly: z.string().readonly(), - requiredAndNullable: z.union([ - z.string(), - z.null() - ]), - string: z.optional(z.string()), - number: z.optional(z.number()), - boolean: z.optional(z.boolean()), - reference: z.optional(zModelWithString), - 'property with space': z.optional(z.string()), - default: z.optional(z.string()), - try: z.optional(z.string()), - '@namespace.string': z.optional(z.string().readonly()), - '@namespace.integer': z.optional(z.int().readonly()) -}); - -/** - * This is a model with one property containing a reference - */ -export const zModelWithReference = z.object({ - prop: z.optional(zModelWithProperties) -}); - -export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ - foo: z.string(), - baz: z.string() -}); - -/** - * This is a model with one property containing an array - */ -export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ - prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), - propWithFile: z.optional(z.array(z.string())), - propWithNumber: z.optional(z.array(z.number())) -}); - /** * This is a model with one property containing an array */ @@ -459,7 +402,7 @@ export const zDeprecatedModel = z.object({ * This is a model with one property containing a circular reference */ export const zModelWithCircularReference = z.object({ - get prop(): z.ZodOptional { + get prop() { return z.optional(z.lazy((): any => { return zModelWithCircularReference; })); @@ -672,6 +615,34 @@ export const zCompositionExtendedModel = zCompositionBaseModel.and(z.object({ lastname: z.string() })); +/** + * This is a model with one nested property + */ +export const zModelWithProperties = z.object({ + required: z.string(), + requiredAndReadOnly: z.string().readonly(), + requiredAndNullable: z.union([ + z.string(), + z.null() + ]), + string: z.optional(z.string()), + number: z.optional(z.number()), + boolean: z.optional(z.boolean()), + reference: z.optional(zModelWithString), + 'property with space': z.optional(z.string()), + default: z.optional(z.string()), + try: z.optional(z.string()), + '@namespace.string': z.optional(z.string().readonly()), + '@namespace.integer': z.optional(z.int().readonly()) +}); + +/** + * This is a model with one property containing a reference + */ +export const zModelWithReference = z.object({ + prop: z.optional(zModelWithProperties) +}); + /** * This is a model with one nested property */ @@ -805,26 +776,6 @@ export const zNestedAnyOfArraysNullable = z.object({ ])) }); -/** - * This is a reusable parameter - */ -export const zSimpleParameter = z.string(); - -export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ - z.object({ - foo: zSimpleParameter - }), - z.object({ - bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 - }) -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * An object that can be null */ @@ -953,22 +904,6 @@ export const zModelWithAnyOfConstantSizeArrayNullable = z.tuple([ ]) ]); -/** - * Model with restricted keyword name - */ -export const zImport = z.string(); - -export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ - z.union([ - z.number(), - zImport - ]), - z.union([ - z.number(), - zImport - ]) -]); - export const zModelWithAnyOfConstantSizeArrayAndIntersect = z.tuple([ z.intersection(z.number(), z.string()), z.intersection(z.number(), z.string()) @@ -993,17 +928,6 @@ export const zModelWithBackticksInDescription = z.object({ template: z.optional(z.string()) }); -export const zModelWithOneOfAndProperties = z.intersection(z.union([ - zSimpleParameter, - zNonAsciiStringæøåÆøÅöôêÊ字符串 -]), z.object({ - baz: z.union([ - z.int().gte(0), - z.null() - ]), - qux: z.int().gte(0) -})); - /** * Model used to test deduplication strategy (unused) */ @@ -1029,6 +953,22 @@ export const zDeleteFooData = z.string(); */ export const zDeleteFooData2 = z.string(); +/** + * Model with restricted keyword name + */ +export const zImport = z.string(); + +export const zModelWithAnyOfConstantSizeArrayWithNSizeAndOptions = z.tuple([ + z.union([ + z.number(), + zImport + ]), + z.union([ + z.number(), + zImport + ]) +]); + export const zSchemaWithFormRestrictedKeys = z.object({ description: z.optional(z.string()), 'x-enum-descriptions': z.optional(z.string()), @@ -1114,6 +1054,21 @@ export const zOneOfAllOfIssue = z.union([ zGenericSchemaDuplicateIssue1SystemString ]); +export const zExternalSharedExternalSharedModel = z.object({ + id: z.string(), + name: z.optional(z.string()) +}); + +/** + * External ref to shared model (A) + */ +export const zExternalRefA = zExternalSharedExternalSharedModel; + +/** + * External ref to shared model (B) + */ +export const zExternalRefB = zExternalSharedExternalSharedModel; + /** * This is a model with one nested property */ @@ -1149,6 +1104,20 @@ export const zFileWritable = z.object({ mime: z.string().min(1).max(24) }); +export const zModelWithReadOnlyAndWriteOnlyWritable = z.object({ + foo: z.string(), + baz: z.string() +}); + +/** + * This is a model with one property containing an array + */ +export const zModelWithArrayReadOnlyAndWriteOnly = z.object({ + prop: z.optional(z.array(zModelWithReadOnlyAndWriteOnlyWritable)), + propWithFile: z.optional(z.array(z.string())), + propWithNumber: z.optional(z.array(z.number())) +}); + export const zAdditionalPropertiesUnknownIssueWritable = z.record(z.string(), z.union([ z.string(), z.number() @@ -1174,6 +1143,37 @@ export const zGenericSchemaDuplicateIssue1SystemStringWritable = z.object({ ])) }); +/** + * This is a reusable parameter + */ +export const zSimpleParameter = z.string(); + +export const zCompositionWithOneOfAndProperties = z.intersection(z.union([ + z.object({ + foo: zSimpleParameter + }), + z.object({ + bar: zNonAsciiStringæøåÆøÅöôêÊ字符串 + }) +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + +export const zModelWithOneOfAndProperties = z.intersection(z.union([ + zSimpleParameter, + zNonAsciiStringæøåÆøÅöôêÊ字符串 +]), z.object({ + baz: z.union([ + z.int().gte(0), + z.null() + ]), + qux: z.int().gte(0) +})); + /** * Parameter with illegal characters */ diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts index 1fa3416dd..53ce2eb2f 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-circular-ref/zod.gen.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; export const zBar = z.object({ - get bar(): z.ZodOptional { + get bar() { return z.optional(z.array(z.lazy((): any => { return zBar; }))); @@ -14,11 +14,11 @@ export const zFoo = z.object({ foo: z.optional(zBar) }); -/** - * description caused circular reference error - */ -export const zQux = z.lazy((): any => { +export const zBaz = z.lazy((): any => { return zQux; }); -export const zBaz = zQux; +/** + * description caused circular reference error + */ +export const zQux = zBaz; diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts index 9b0132631..bc6bd1d71 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-dates/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts index 34e6fe096..4e71c97c6 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-metadata/zod.gen.ts @@ -2,16 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}).register(z.globalRegistry, { - description: 'This is Bar schema.' -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -21,8 +16,12 @@ export const zFoo = z.union([ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/).register(z.globalRegistry, { description: 'This is foo property.' })), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; })).register(z.globalRegistry, { @@ -36,11 +35,14 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}).register(z.globalRegistry, { + description: 'This is Bar schema.' +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts index 649cd0534..ba13cd685 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators-types/zod.gen.ts @@ -2,16 +2,15 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); -export type BarZodType = z.infer; +export type BazZodType = z.infer; + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); + +export type QuxZodType = z.infer; /** * This is Foo schema. @@ -19,8 +18,12 @@ export type BarZodType = z.infer; export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -32,15 +35,14 @@ export const zFoo = z.union([ export type FooZodType = z.infer; -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export type BazZodType = z.infer; - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); -export type QuxZodType = z.infer; +export type BarZodType = z.infer; /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts index 921e41c46..a48bf2186 100644 --- a/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts +++ b/packages/openapi-ts-tests/zod/v4/__snapshots__/3.1.x/v4/validators/zod.gen.ts @@ -2,14 +2,11 @@ import { z } from 'zod'; -/** - * This is Bar schema. - */ -export const zBar = z.object({ - get foo(): z.ZodOptional { - return z.optional(zFoo); - } -}); +export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); + +export const zQux = z.record(z.string(), z.object({ + qux: z.optional(z.string()) +})); /** * This is Foo schema. @@ -17,8 +14,12 @@ export const zBar = z.object({ export const zFoo = z.union([ z.object({ foo: z.optional(z.string().regex(/^\d{3}-\d{2}-\d{4}$/)), - bar: z.optional(zBar), - get baz(): z.ZodOptional { + get bar() { + return z.optional(z.lazy((): any => { + return zBar; + })); + }, + get baz() { return z.optional(z.array(z.lazy((): any => { return zFoo; }))); @@ -28,11 +29,12 @@ export const zFoo = z.union([ z.null() ]).default(null); -export const zBaz = z.string().regex(/foo\nbar/).readonly().default('baz'); - -export const zQux = z.record(z.string(), z.object({ - qux: z.optional(z.string()) -})); +/** + * This is Bar schema. + */ +export const zBar = z.object({ + foo: z.optional(zFoo) +}); /** * This is Foo parameter. diff --git a/packages/openapi-ts-tests/zod/v4/vitest.config.ts b/packages/openapi-ts-tests/zod/v4/vitest.config.ts index d13ef5fee..7c627e64f 100644 --- a/packages/openapi-ts-tests/zod/v4/vitest.config.ts +++ b/packages/openapi-ts-tests/zod/v4/vitest.config.ts @@ -1,7 +1,15 @@ +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createVitestConfig } from '@config/vite-base'; -export default createVitestConfig( - fileURLToPath(new URL('./', import.meta.url)), -); +const rootDir = fileURLToPath(new URL('./', import.meta.url)); + +export default createVitestConfig(rootDir, { + resolve: { + alias: [ + { find: /^~\/(.*)/, replacement: path.resolve(rootDir, 'src/$1') }, + { find: '~', replacement: path.resolve(rootDir, 'src') }, + ], + }, +}); diff --git a/packages/openapi-ts/CHANGELOG.md b/packages/openapi-ts/CHANGELOG.md index 63290643c..ee2f99863 100644 --- a/packages/openapi-ts/CHANGELOG.md +++ b/packages/openapi-ts/CHANGELOG.md @@ -1,5 +1,23 @@ # @hey-api/openapi-ts +## 0.86.2 + +### Patch Changes + +- fix(validators): do not reference variables before they are declared ([#2812](https://github.com/hey-api/openapi-ts/pull/2812)) ([`69783d1`](https://github.com/hey-api/openapi-ts/commit/69783d19be37014b5116a2ede5bb74adadbb8110)) by [@mrlubos](https://github.com/mrlubos) + +- fix(renderer): allow duplicate names when one symbol is a type ([#2812](https://github.com/hey-api/openapi-ts/pull/2812)) ([`523038e`](https://github.com/hey-api/openapi-ts/commit/523038e82c949a436a467486a26333d50ce0e7cd)) by [@mrlubos](https://github.com/mrlubos) + +### Updated Dependencies: + +- @hey-api/codegen-core@0.3.1 + +## 0.86.1 + +### Patch Changes + +- fix(client-axios): revert return error when axios request fails ([#2804](https://github.com/hey-api/openapi-ts/pull/2804)) ([`5bba86e`](https://github.com/hey-api/openapi-ts/commit/5bba86e742c48f575f96f55f7d7ea3d636102ebe)) by [@mrlubos](https://github.com/mrlubos) + ## 0.86.0 ### Minor Changes diff --git a/packages/openapi-ts/package.json b/packages/openapi-ts/package.json index ed7a15244..a2b094b2b 100644 --- a/packages/openapi-ts/package.json +++ b/packages/openapi-ts/package.json @@ -1,6 +1,6 @@ { "name": "@hey-api/openapi-ts", - "version": "0.86.0", + "version": "0.86.2", "description": "🚀 The OpenAPI to TypeScript codegen. Generate clients, SDKs, validators, and more.", "homepage": "https://heyapi.dev/", "repository": { @@ -90,8 +90,8 @@ "node": ">=20.19.0" }, "dependencies": { - "@hey-api/codegen-core": "workspace:^0.3.0", - "@hey-api/json-schema-ref-parser": "1.2.0", + "@hey-api/codegen-core": "workspace:^0.3.1", + "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.1", "color-support": "1.1.3", diff --git a/packages/openapi-ts/src/__tests__/createClient.test.ts b/packages/openapi-ts/src/__tests__/createClient.test.ts index 6c4acec07..6b0886dfc 100644 --- a/packages/openapi-ts/src/__tests__/createClient.test.ts +++ b/packages/openapi-ts/src/__tests__/createClient.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { compileInputPath } from '../createClient'; +import { compileInputPath } from '~/createClient'; describe('compileInputPath', () => { it('with raw OpenAPI specification', () => { diff --git a/packages/openapi-ts/src/__tests__/error.test.ts b/packages/openapi-ts/src/__tests__/error.test.ts index 57cff1d4f..15e7abcea 100644 --- a/packages/openapi-ts/src/__tests__/error.test.ts +++ b/packages/openapi-ts/src/__tests__/error.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from 'vitest'; -import { shouldReportCrash } from '../error'; +import { shouldReportCrash } from '~/error'; describe('shouldReportCrash', () => { it('should return false when isInteractive is false', async () => { diff --git a/packages/openapi-ts/src/__tests__/index.test.ts b/packages/openapi-ts/src/__tests__/index.test.ts index d2c7b631a..c3374ee5c 100644 --- a/packages/openapi-ts/src/__tests__/index.test.ts +++ b/packages/openapi-ts/src/__tests__/index.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { createClient } from '../index'; +import { createClient } from '~/index'; type Config = Parameters[0]; diff --git a/packages/openapi-ts/src/__tests__/interactive.test.ts b/packages/openapi-ts/src/__tests__/interactive.test.ts index 626e357b4..7812c9546 100644 --- a/packages/openapi-ts/src/__tests__/interactive.test.ts +++ b/packages/openapi-ts/src/__tests__/interactive.test.ts @@ -1,8 +1,8 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { detectInteractiveSession, initConfigs } from '../config/init'; -import { mergeConfigs } from '../config/merge'; -import { Logger } from '../utils/logger'; +import { detectInteractiveSession, initConfigs } from '~/config/init'; +import { mergeConfigs } from '~/config/merge'; +import { Logger } from '~/utils/logger'; describe('interactive config', () => { it('should use detectInteractiveSession when not provided', async () => { diff --git a/packages/openapi-ts/src/config/__tests__/input.test.ts b/packages/openapi-ts/src/config/__tests__/input.test.ts index b55b3246f..c71da149f 100644 --- a/packages/openapi-ts/src/config/__tests__/input.test.ts +++ b/packages/openapi-ts/src/config/__tests__/input.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { UserConfig } from '../../types/config'; +import type { UserConfig } from '~/types/config'; + import { getInput } from '../input'; describe('input config', () => { diff --git a/packages/openapi-ts/src/config/engine.ts b/packages/openapi-ts/src/config/engine.ts index 637ead0ea..c7189278b 100644 --- a/packages/openapi-ts/src/config/engine.ts +++ b/packages/openapi-ts/src/config/engine.ts @@ -1,4 +1,4 @@ -import { ConfigError } from '../error'; +import { ConfigError } from '~/error'; export const checkNodeVersion = () => { if (typeof Bun !== 'undefined') { diff --git a/packages/openapi-ts/src/config/init.ts b/packages/openapi-ts/src/config/init.ts index 7350a7401..e7d629d36 100644 --- a/packages/openapi-ts/src/config/init.ts +++ b/packages/openapi-ts/src/config/init.ts @@ -2,11 +2,12 @@ import path from 'node:path'; import colors from 'ansi-colors'; -import { ConfigError } from '../error'; -import type { Config, UserConfig } from '../types/config'; -import type { ArrayOnly } from '../types/utils'; -import { isLegacyClient, setConfig } from '../utils/config'; -import type { Logger } from '../utils/logger'; +import { ConfigError } from '~/error'; +import type { Config, UserConfig } from '~/types/config'; +import type { ArrayOnly } from '~/types/utils'; +import { isLegacyClient, setConfig } from '~/utils/config'; +import type { Logger } from '~/utils/logger'; + import { getInput } from './input'; import { getLogs } from './logs'; import { mergeConfigs } from './merge'; diff --git a/packages/openapi-ts/src/config/input.ts b/packages/openapi-ts/src/config/input.ts index 39629a80b..2aea3667d 100644 --- a/packages/openapi-ts/src/config/input.ts +++ b/packages/openapi-ts/src/config/input.ts @@ -1,7 +1,7 @@ -import type { Config, UserConfig } from '../types/config'; -import type { Input, Watch } from '../types/input'; -import { inputToApiRegistry } from '../utils/input'; -import { heyApiRegistryBaseUrl } from '../utils/input/heyApi'; +import type { Config, UserConfig } from '~/types/config'; +import type { Input, Watch } from '~/types/input'; +import { inputToApiRegistry } from '~/utils/input'; +import { heyApiRegistryBaseUrl } from '~/utils/input/heyApi'; const defaultWatch: Watch = { enabled: false, diff --git a/packages/openapi-ts/src/config/logs.ts b/packages/openapi-ts/src/config/logs.ts index ada1300ef..91d5742c5 100644 --- a/packages/openapi-ts/src/config/logs.ts +++ b/packages/openapi-ts/src/config/logs.ts @@ -1,4 +1,4 @@ -import type { Config, UserConfig } from '../types/config'; +import type { Config, UserConfig } from '~/types/config'; export const getLogs = ( userConfig: Pick | undefined, diff --git a/packages/openapi-ts/src/config/merge.ts b/packages/openapi-ts/src/config/merge.ts index 7ade55e4d..5dc7b3a86 100644 --- a/packages/openapi-ts/src/config/merge.ts +++ b/packages/openapi-ts/src/config/merge.ts @@ -1,4 +1,4 @@ -import type { UserConfig } from '../types/config'; +import type { UserConfig } from '~/types/config'; const mergeObjects = ( objA: Record | undefined, diff --git a/packages/openapi-ts/src/config/output.ts b/packages/openapi-ts/src/config/output.ts index e03fff839..98ac0563c 100644 --- a/packages/openapi-ts/src/config/output.ts +++ b/packages/openapi-ts/src/config/output.ts @@ -1,7 +1,8 @@ import ts from 'typescript'; -import { findTsConfigPath, loadTsConfig } from '../generate/tsConfig'; -import type { Config, UserConfig } from '../types/config'; +import { findTsConfigPath, loadTsConfig } from '~/generate/tsConfig'; +import type { Config, UserConfig } from '~/types/config'; + import { valueToObject } from './utils/config'; export const getOutput = (userConfig: UserConfig): Config['output'] => { diff --git a/packages/openapi-ts/src/config/parser.ts b/packages/openapi-ts/src/config/parser.ts index e44f6af86..82aeb2f39 100644 --- a/packages/openapi-ts/src/config/parser.ts +++ b/packages/openapi-ts/src/config/parser.ts @@ -1,4 +1,5 @@ -import type { Config, UserConfig } from '../types/config'; +import type { Config, UserConfig } from '~/types/config'; + import { valueToObject } from './utils/config'; export const defaultPaginationKeywords = [ diff --git a/packages/openapi-ts/src/config/plugins.ts b/packages/openapi-ts/src/config/plugins.ts index 94210a902..7ffb90b21 100644 --- a/packages/openapi-ts/src/config/plugins.ts +++ b/packages/openapi-ts/src/config/plugins.ts @@ -1,10 +1,11 @@ -import { defaultPluginConfigs } from '../plugins/config'; +import { defaultPluginConfigs } from '~/plugins/config'; import type { AnyPluginName, PluginContext, PluginNames, -} from '../plugins/types'; -import type { Config, UserConfig } from '../types/config'; +} from '~/plugins/types'; +import type { Config, UserConfig } from '~/types/config'; + import { valueToObject } from './utils/config'; import { packageFactory } from './utils/package'; diff --git a/packages/openapi-ts/src/createClient.ts b/packages/openapi-ts/src/createClient.ts index ecdf586d2..aa04acf60 100644 --- a/packages/openapi-ts/src/createClient.ts +++ b/packages/openapi-ts/src/createClient.ts @@ -3,21 +3,22 @@ import path from 'node:path'; import { $RefParser } from '@hey-api/json-schema-ref-parser'; import colors from 'ansi-colors'; -import { generateLegacyOutput } from './generate/legacy/output'; -import { generateOutput } from './generate/output'; -import { getSpec } from './getSpec'; -import type { IR } from './ir/types'; -import { parseLegacy, parseOpenApiSpec } from './openApi'; -import { patchOpenApiSpec } from './openApi/shared/utils/patch'; -import { processOutput } from './processOutput'; -import type { Client } from './types/client'; -import type { Config } from './types/config'; -import type { Input } from './types/input'; -import type { WatchValues } from './types/types'; -import { isLegacyClient, legacyNameFromConfig } from './utils/config'; -import type { Templates } from './utils/handlebars'; -import type { Logger } from './utils/logger'; -import { postProcessClient } from './utils/postprocess'; +import { generateLegacyOutput } from '~/generate/legacy/output'; +import { generateOutput } from '~/generate/output'; +import { getSpec } from '~/getSpec'; +import type { IR } from '~/ir/types'; +import { parseLegacy, parseOpenApiSpec } from '~/openApi'; +import { buildGraph } from '~/openApi/shared/utils/graph'; +import { patchOpenApiSpec } from '~/openApi/shared/utils/patch'; +import { processOutput } from '~/processOutput'; +import type { Client } from '~/types/client'; +import type { Config } from '~/types/config'; +import type { Input } from '~/types/input'; +import type { WatchValues } from '~/types/types'; +import { isLegacyClient, legacyNameFromConfig } from '~/utils/config'; +import type { Templates } from '~/utils/handlebars'; +import type { Logger } from '~/utils/logger'; +import { postProcessClient } from '~/utils/postprocess'; export const compileInputPath = (input: Omit) => { const result: Pick< @@ -332,8 +333,10 @@ export const createClient = async ({ context = parseOpenApiSpec({ config, dependencies, logger, spec: data }); } - // fallback to legacy parser - if (!context) { + if (context) { + context.graph = buildGraph(context.ir, logger).graph; + } else { + // fallback to legacy parser const parsed = parseLegacy({ openApi: data }); client = postProcessClient(parsed, config); } diff --git a/packages/openapi-ts/src/debug/graph.ts b/packages/openapi-ts/src/debug/graph.ts index c04f6a19a..38f3e7ca5 100644 --- a/packages/openapi-ts/src/debug/graph.ts +++ b/packages/openapi-ts/src/debug/graph.ts @@ -1,4 +1,4 @@ -import type { Graph } from '../openApi/shared/utils/graph'; +import type { Graph } from '~/openApi/shared/utils/graph'; const analyzeStructure = (graph: Graph) => { let maxDepth = 0; diff --git a/packages/openapi-ts/src/debug/ir.ts b/packages/openapi-ts/src/debug/ir.ts index 1a0f62d39..03bf852c1 100644 --- a/packages/openapi-ts/src/debug/ir.ts +++ b/packages/openapi-ts/src/debug/ir.ts @@ -1,7 +1,7 @@ import colors from 'ansi-colors'; -import type { IR } from '../ir/types'; -import { httpMethods } from '../openApi/shared/utils/operation'; +import type { IR } from '~/ir/types'; +import { httpMethods } from '~/openApi/shared/utils/operation'; export interface PrintOptions { /** diff --git a/packages/openapi-ts/src/error.ts b/packages/openapi-ts/src/error.ts index 94173f6dd..229d6ee5d 100644 --- a/packages/openapi-ts/src/error.ts +++ b/packages/openapi-ts/src/error.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import colors from 'ansi-colors'; -import { loadPackageJson } from './generate/tsConfig'; -import { ensureDirSync } from './generate/utils'; +import { loadPackageJson } from '~/generate/tsConfig'; +import { ensureDirSync } from '~/generate/utils'; type IJobError = { error: Error; diff --git a/packages/openapi-ts/src/generate/__tests__/class.test.ts b/packages/openapi-ts/src/generate/__tests__/class.test.ts index 6060d05ed..af7c37d34 100644 --- a/packages/openapi-ts/src/generate/__tests__/class.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/class.test.ts @@ -3,8 +3,9 @@ import fs from 'node:fs'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import type { Config } from '../../types/config'; -import { setConfig } from '../../utils/config'; +import type { Config } from '~/types/config'; +import { setConfig } from '~/utils/config'; + import { generateLegacyClientClass } from '../class'; import { mockTemplates, openApi } from './mocks'; @@ -85,7 +86,7 @@ describe('generateLegacyClientClass', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -97,7 +98,7 @@ describe('generateLegacyClientClass', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -108,8 +109,8 @@ describe('generateLegacyClientClass', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/__tests__/core.test.ts b/packages/openapi-ts/src/generate/__tests__/core.test.ts index 6d81edfa6..9da697ef8 100644 --- a/packages/openapi-ts/src/generate/__tests__/core.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/core.test.ts @@ -4,8 +4,9 @@ import path from 'node:path'; import type ts from 'typescript'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Config } from '../../types/config'; -import { setConfig } from '../../utils/config'; +import type { Config } from '~/types/config'; +import { setConfig } from '~/utils/config'; + import { generateLegacyCore } from '../core'; import { mockTemplates } from './mocks'; @@ -100,7 +101,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -112,7 +113,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -123,8 +124,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -258,7 +259,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -270,7 +271,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -281,8 +282,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -399,7 +400,7 @@ describe('generateLegacyCore', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -411,7 +412,7 @@ describe('generateLegacyCore', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -422,8 +423,8 @@ describe('generateLegacyCore', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/__tests__/mocks.ts b/packages/openapi-ts/src/generate/__tests__/mocks.ts index 6d5718dd3..fdffc1900 100644 --- a/packages/openapi-ts/src/generate/__tests__/mocks.ts +++ b/packages/openapi-ts/src/generate/__tests__/mocks.ts @@ -1,9 +1,9 @@ import { vi } from 'vitest'; -import type { OpenApi } from '../../openApi'; -import type { Client } from '../../types/client'; -import type { Config } from '../../types/config'; -import type { Templates } from '../../utils/handlebars'; +import type { OpenApi } from '~/openApi'; +import type { Client } from '~/types/client'; +import type { Config } from '~/types/config'; +import type { Templates } from '~/utils/handlebars'; export const client: Client = { config: {} as Config, diff --git a/packages/openapi-ts/src/generate/__tests__/renderer.test.ts b/packages/openapi-ts/src/generate/__tests__/renderer.test.ts index 1c0cccb32..95e6c373e 100644 --- a/packages/openapi-ts/src/generate/__tests__/renderer.test.ts +++ b/packages/openapi-ts/src/generate/__tests__/renderer.test.ts @@ -2,6 +2,26 @@ import { describe, expect, it } from 'vitest'; import { TypeScriptRenderer } from '../renderer'; +// Minimal local BiMap for tests to avoid importing runtime-only class +class LocalBiMap { + private map = new Map(); + private reverse = new Map(); + get(key: Key) { + return this.map.get(key); + } + getKey(value: Value) { + return this.reverse.get(value); + } + set(key: Key, value: Value) { + this.map.set(key, value); + this.reverse.set(value, key); + return this; + } + hasValue(value: Value) { + return this.reverse.has(value); + } +} + describe('TypeScriptRenderer', () => { describe('default import placeholder replacement', () => { it('should replace placeholders in default imports correctly', () => { @@ -44,4 +64,59 @@ describe('TypeScriptRenderer', () => { expect(importLines).toEqual(["import foo from 'foo';"]); }); }); + + describe('replacer duplicate name handling', () => { + it('allows duplicate names when kinds differ (type vs value)', () => { + const renderer = new TypeScriptRenderer(); + + // Prepare a mock file and project + const file: any = { + resolvedNames: new LocalBiMap(), + symbols: { body: [], exports: [], imports: [] }, + }; + + const project = { + symbolIdToFiles: () => [file], + symbols: new Map(), + } as any; + + // Two symbols with the same name but different kinds + const typeSymbolId = 1; + const valueSymbolId = 2; + + const typeSymbol: any = { + exportFrom: [], + id: typeSymbolId, + meta: { kind: 'type' }, + name: 'Foo', + placeholder: '_heyapi_1_', + }; + const valueSymbol: any = { + exportFrom: [], + id: valueSymbolId, + meta: {}, + name: 'Foo', + placeholder: '_heyapi_2_', + }; + + project.symbols.set(typeSymbolId, typeSymbol); + project.symbols.set(valueSymbolId, valueSymbol); + + // First replacement should register the name 'Foo' + const first = renderer['replacerFn']({ + file, + project, + symbol: typeSymbol, + }); + expect(first).toEqual('Foo'); + + // Second replacement (different kind) should be allowed to also use 'Foo' + const second = renderer['replacerFn']({ + file, + project, + symbol: valueSymbol, + }); + expect(second).toEqual('Foo'); + }); + }); }); diff --git a/packages/openapi-ts/src/generate/class.ts b/packages/openapi-ts/src/generate/class.ts index 45a93cc03..e40b9f0bb 100644 --- a/packages/openapi-ts/src/generate/class.ts +++ b/packages/openapi-ts/src/generate/class.ts @@ -1,13 +1,14 @@ import fs from 'node:fs'; import path from 'node:path'; -import type { OpenApi } from '../openApi'; -import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; -import type { Client } from '../types/client'; -import { getConfig, legacyNameFromConfig } from '../utils/config'; -import { getHttpRequestName } from '../utils/getHttpRequestName'; -import type { Templates } from '../utils/handlebars'; -import { sortByName } from '../utils/sort'; +import type { OpenApi } from '~/openApi'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { Client } from '~/types/client'; +import { getConfig, legacyNameFromConfig } from '~/utils/config'; +import { getHttpRequestName } from '~/utils/getHttpRequestName'; +import type { Templates } from '~/utils/handlebars'; +import { sortByName } from '~/utils/sort'; + import { ensureDirSync } from './utils'; /** diff --git a/packages/openapi-ts/src/generate/client.ts b/packages/openapi-ts/src/generate/client.ts index fb38b6307..a93d3d214 100644 --- a/packages/openapi-ts/src/generate/client.ts +++ b/packages/openapi-ts/src/generate/client.ts @@ -4,10 +4,11 @@ import { fileURLToPath } from 'node:url'; import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core'; -import type { Client } from '../plugins/@hey-api/client-core/types'; -import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; -import type { DefinePlugin } from '../plugins/types'; -import type { Config } from '../types/config'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { DefinePlugin } from '~/plugins/types'; +import type { Config } from '~/types/config'; + import { ensureDirSync, relativeModulePath } from './utils'; const __filename = fileURLToPath(import.meta.url); diff --git a/packages/openapi-ts/src/generate/core.ts b/packages/openapi-ts/src/generate/core.ts index d72110874..ba4581bbc 100644 --- a/packages/openapi-ts/src/generate/core.ts +++ b/packages/openapi-ts/src/generate/core.ts @@ -1,11 +1,11 @@ import fs from 'node:fs'; import path from 'node:path'; -import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; -import type { Client } from '../types/client'; -import { getConfig, legacyNameFromConfig } from '../utils/config'; -import { getHttpRequestName } from '../utils/getHttpRequestName'; -import type { Templates } from '../utils/handlebars'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { Client } from '~/types/client'; +import { getConfig, legacyNameFromConfig } from '~/utils/config'; +import { getHttpRequestName } from '~/utils/getHttpRequestName'; +import type { Templates } from '~/utils/handlebars'; /** * Generate OpenAPI core files, this includes the basic boilerplate code to handle requests. diff --git a/packages/openapi-ts/src/generate/file.ts b/packages/openapi-ts/src/generate/file.ts index a928ba54c..684ad95ca 100644 --- a/packages/openapi-ts/src/generate/file.ts +++ b/packages/openapi-ts/src/generate/file.ts @@ -3,8 +3,9 @@ import path from 'node:path'; import ts from 'typescript'; -import { tsc } from '../tsc'; -import { type ImportExportItemObject, tsNodeToString } from '../tsc/utils'; +import { tsc } from '~/tsc'; +import { type ImportExportItemObject, tsNodeToString } from '~/tsc/utils'; + import { ensureDirSync } from './utils'; type FileImportResult< diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts index f7350e438..ad794d20e 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/index.test.ts @@ -4,7 +4,8 @@ import path from 'node:path'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import { setConfig } from '../../../utils/config'; +import { setConfig } from '~/utils/config'; + import { GeneratedFile } from '../../file'; import { generateIndexFile } from '../indexFile'; @@ -84,7 +85,7 @@ describe('generateIndexFile', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -96,7 +97,7 @@ describe('generateIndexFile', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -107,8 +108,8 @@ describe('generateIndexFile', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts index adc032164..c0897c382 100644 --- a/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts +++ b/packages/openapi-ts/src/generate/legacy/__tests__/output.test.ts @@ -3,9 +3,10 @@ import fs from 'node:fs'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import type { Client } from '../../../types/client'; -import type { Config } from '../../../types/config'; -import { setConfig } from '../../../utils/config'; +import type { Client } from '~/types/client'; +import type { Config } from '~/types/config'; +import { setConfig } from '~/utils/config'; + import { mockTemplates, openApi } from '../../__tests__/mocks'; import { generateLegacyOutput } from '../output'; @@ -97,7 +98,7 @@ describe('generateLegacyOutput', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -109,7 +110,7 @@ describe('generateLegacyOutput', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -120,8 +121,8 @@ describe('generateLegacyOutput', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/generate/legacy/indexFile.ts b/packages/openapi-ts/src/generate/legacy/indexFile.ts index 1afeb9b00..f7b68184c 100644 --- a/packages/openapi-ts/src/generate/legacy/indexFile.ts +++ b/packages/openapi-ts/src/generate/legacy/indexFile.ts @@ -1,7 +1,8 @@ -import { getClientPlugin } from '../../plugins/@hey-api/client-core/utils'; -import { tsc } from '../../tsc'; -import type { Files } from '../../types/utils'; -import { getConfig, legacyNameFromConfig } from '../../utils/config'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { tsc } from '~/tsc'; +import type { Files } from '~/types/utils'; +import { getConfig, legacyNameFromConfig } from '~/utils/config'; + import { GeneratedFile } from '../file'; export const generateIndexFile = ({ files }: { files: Files }): void => { diff --git a/packages/openapi-ts/src/generate/legacy/output.ts b/packages/openapi-ts/src/generate/legacy/output.ts index fb1a5105a..888abd1fa 100644 --- a/packages/openapi-ts/src/generate/legacy/output.ts +++ b/packages/openapi-ts/src/generate/legacy/output.ts @@ -2,12 +2,13 @@ import path from 'node:path'; import type { ProjectRenderMeta } from '@hey-api/codegen-core'; -import type { OpenApi } from '../../openApi'; -import { getClientPlugin } from '../../plugins/@hey-api/client-core/utils'; -import type { Client } from '../../types/client'; -import type { Files } from '../../types/utils'; -import { getConfig, isLegacyClient } from '../../utils/config'; -import type { Templates } from '../../utils/handlebars'; +import type { OpenApi } from '~/openApi'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { Client } from '~/types/client'; +import type { Files } from '~/types/utils'; +import { getConfig, isLegacyClient } from '~/utils/config'; +import type { Templates } from '~/utils/handlebars'; + import { generateLegacyClientClass } from '../class'; import { generateClientBundle } from '../client'; import { generateLegacyCore } from '../core'; diff --git a/packages/openapi-ts/src/generate/output.ts b/packages/openapi-ts/src/generate/output.ts index b479f6da6..6022833eb 100644 --- a/packages/openapi-ts/src/generate/output.ts +++ b/packages/openapi-ts/src/generate/output.ts @@ -3,8 +3,9 @@ import path from 'node:path'; import type { ProjectRenderMeta } from '@hey-api/codegen-core'; -import type { IR } from '../ir/types'; -import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; +import type { IR } from '~/ir/types'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; + import { generateClientBundle } from './client'; import { removeDirSync } from './utils'; diff --git a/packages/openapi-ts/src/generate/renderer.ts b/packages/openapi-ts/src/generate/renderer.ts index f6e107b8f..fd0bfdb46 100644 --- a/packages/openapi-ts/src/generate/renderer.ts +++ b/packages/openapi-ts/src/generate/renderer.ts @@ -12,9 +12,9 @@ import type { import { createBinding, mergeBindings, renderIds } from '@hey-api/codegen-core'; import ts from 'typescript'; -import { ensureValidIdentifier } from '../openApi/shared/utils/identifier'; -import { tsc } from '../tsc'; -import { tsNodeToString } from '../tsc/utils'; +import { ensureValidIdentifier } from '~/openApi/shared/utils/identifier'; +import { tsc } from '~/tsc'; +import { tsNodeToString } from '~/tsc/utils'; const nodeBuiltins = new Set([ 'buffer', @@ -479,8 +479,16 @@ export class TypeScriptRenderer implements Renderer { const [symbolFile] = project.symbolIdToFiles(symbol.id); const symbolFileResolvedName = symbolFile?.resolvedNames.get(symbol.id); let name = ensureValidIdentifier(symbolFileResolvedName ?? symbol.name); - if (file.resolvedNames.hasValue(name)) { - name = this.getUniqueName(name, file.resolvedNames); + const conflictId = file.resolvedNames.getKey(name); + if (conflictId !== undefined) { + const conflictSymbol = project.symbols.get(conflictId); + if ( + (conflictSymbol?.meta?.kind === 'type' && + symbol.meta?.kind === 'type') || + (conflictSymbol?.meta?.kind !== 'type' && symbol.meta?.kind !== 'type') + ) { + name = this.getUniqueName(name, file.resolvedNames); + } } file.resolvedNames.set(symbol.id, name); return name; diff --git a/packages/openapi-ts/src/generate/tsConfig.ts b/packages/openapi-ts/src/generate/tsConfig.ts index f2040ce68..89b128294 100644 --- a/packages/openapi-ts/src/generate/tsConfig.ts +++ b/packages/openapi-ts/src/generate/tsConfig.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import ts from 'typescript'; -import type { UserOutput } from '../types/output'; +import type { UserOutput } from '~/types/output'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/packages/openapi-ts/src/getSpec.ts b/packages/openapi-ts/src/getSpec.ts index 1d5d0f0cf..0841e78bf 100644 --- a/packages/openapi-ts/src/getSpec.ts +++ b/packages/openapi-ts/src/getSpec.ts @@ -1,8 +1,8 @@ import { getResolvedInput, sendRequest } from '@hey-api/json-schema-ref-parser'; -import { mergeHeaders } from './plugins/@hey-api/client-fetch/bundle'; -import type { Input } from './types/input'; -import type { WatchValues } from './types/types'; +import { mergeHeaders } from '~/plugins/@hey-api/client-fetch/bundle'; +import type { Input } from '~/types/input'; +import type { WatchValues } from '~/types/types'; type SpecResponse = { arrayBuffer: ArrayBuffer | undefined; diff --git a/packages/openapi-ts/src/index.ts b/packages/openapi-ts/src/index.ts index 5335bfb04..21aec06ff 100644 --- a/packages/openapi-ts/src/index.ts +++ b/packages/openapi-ts/src/index.ts @@ -5,11 +5,11 @@ import colors from 'ansi-colors'; // @ts-expect-error import colorSupport from 'color-support'; -import { checkNodeVersion } from './config/engine'; -import type { Configs } from './config/init'; -import { initConfigs } from './config/init'; -import { getLogs } from './config/logs'; -import { createClient as pCreateClient } from './createClient'; +import { checkNodeVersion } from '~/config/engine'; +import type { Configs } from '~/config/init'; +import { initConfigs } from '~/config/init'; +import { getLogs } from '~/config/logs'; +import { createClient as pCreateClient } from '~/createClient'; import { ConfigValidationError, JobError, @@ -17,14 +17,14 @@ import { openGitHubIssueWithCrashReport, printCrashReport, shouldReportCrash, -} from './error'; -import type { IR } from './ir/types'; -import type { Client } from './types/client'; -import type { UserConfig } from './types/config'; -import type { LazyOrAsync, MaybeArray } from './types/utils'; -import { printCliIntro } from './utils/cli'; -import { registerHandlebarTemplates } from './utils/handlebars'; -import { Logger } from './utils/logger'; +} from '~/error'; +import type { IR } from '~/ir/types'; +import type { Client } from '~/types/client'; +import type { UserConfig } from '~/types/config'; +import type { LazyOrAsync, MaybeArray } from '~/types/utils'; +import { printCliIntro } from '~/utils/cli'; +import { registerHandlebarTemplates } from '~/utils/handlebars'; +import { Logger } from '~/utils/logger'; colors.enabled = colorSupport().hasBasic; diff --git a/packages/openapi-ts/src/ir/__tests__/graph.test.ts b/packages/openapi-ts/src/ir/__tests__/graph.test.ts new file mode 100644 index 000000000..10335ba61 --- /dev/null +++ b/packages/openapi-ts/src/ir/__tests__/graph.test.ts @@ -0,0 +1,327 @@ +import { describe, expect, it } from 'vitest'; + +import type { Graph } from '~/openApi/shared/utils/graph'; +import { buildGraph } from '~/openApi/shared/utils/graph'; + +import type { IrTopLevelKind } from '../graph'; +import { matchIrTopLevelPointer, walkTopological } from '../graph'; + +// simple logger stub for buildGraph +const loggerStub = { + timeEvent: () => ({ timeEnd: () => {} }), +} as any; + +describe('matchIrTopLevelPointer', () => { + const cases: Array< + [ + string, + IrTopLevelKind | undefined, + { kind?: IrTopLevelKind; matched: boolean }, + ] + > = [ + ['#/components/schemas/Foo', undefined, { kind: 'schema', matched: true }], + ['#/components/schemas/Foo', 'schema', { kind: 'schema', matched: true }], + ['#/components/schemas/Foo', 'parameter', { matched: false }], + [ + '#/components/parameters/Bar', + undefined, + { kind: 'parameter', matched: true }, + ], + [ + '#/components/parameters/Bar', + 'parameter', + { kind: 'parameter', matched: true }, + ], + ['#/components/parameters/Bar', 'schema', { matched: false }], + [ + '#/components/requestBodies/Baz', + undefined, + { kind: 'requestBody', matched: true }, + ], + [ + '#/components/requestBodies/Baz', + 'requestBody', + { kind: 'requestBody', matched: true }, + ], + ['#/components/requestBodies/Baz', 'schema', { matched: false }], + ['#/servers/0', undefined, { kind: 'server', matched: true }], + ['#/servers/foo', undefined, { kind: 'server', matched: true }], + ['#/paths/~1users/get', undefined, { kind: 'operation', matched: true }], + ['#/paths/~1users/post', 'operation', { kind: 'operation', matched: true }], + ['#/webhooks/foo/get', undefined, { kind: 'webhook', matched: true }], + ['#/webhooks/foo/patch', 'webhook', { kind: 'webhook', matched: true }], + ['#/not/a/top/level', undefined, { matched: false }], + ['#/components/unknown/Foo', undefined, { matched: false }], + ]; + + for (const [pointer, kind, expected] of cases) { + it(`matches ${pointer} with kind=${kind}`, () => { + const result = matchIrTopLevelPointer(pointer, kind as IrTopLevelKind); + expect(result.matched).toBe(expected.matched); + if (expected.matched) { + expect(result.kind).toBe(expected.kind); + } else { + expect(result.kind).toBeUndefined(); + } + }); + } +}); + +describe('walkTopological', () => { + const makeGraph = ( + deps: Array<[string, Array]>, + nodes: Array, + ) => { + const nodeDependencies = new Map>(); + const subtreeDependencies = new Map>(); + const reverseNodeDependencies = new Map>(); + const nodesMap = new Map(); + + for (const name of nodes) { + nodesMap.set(name, { key: null, node: {}, parentPointer: null }); + } + + for (const [from, toList] of deps) { + const s = new Set(toList); + nodeDependencies.set(from, s); + subtreeDependencies.set(from, new Set(toList)); + for (const to of toList) { + if (!reverseNodeDependencies.has(to)) + reverseNodeDependencies.set(to, new Set()); + reverseNodeDependencies.get(to)!.add(from); + } + } + + return { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies, + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + }; + + it('walks nodes in topological order for a simple acyclic graph', () => { + // Graph: A -> B -> C + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['C']], + ], + ['A', 'B', 'C'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(3); + }); + + it('walks nodes in topological order for multiple roots', () => { + // Graph: A -> B, C -> D + const graph = makeGraph( + [ + ['A', ['B']], + ['C', ['D']], + ], + ['A', 'B', 'C', 'D'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order).toHaveLength(4); + }); + + it('walks nodes in topological order for a disconnected graph', () => { + // Graph: A -> B, C (no deps), D (no deps) + const graph = makeGraph([['A', ['B']]], ['A', 'B', 'C', 'D']); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(4); + expect(order).toContain('C'); + expect(order).toContain('D'); + }); + + it('walks nodes in topological order for a diamond dependency', () => { + // Graph: A + // / \ + // B C + // \ / + // D + const graph = makeGraph( + [ + ['A', ['B', 'C']], + ['B', ['D']], + ['C', ['D']], + ], + ['A', 'B', 'C', 'D'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(4); + }); + + it('walks nodes in topological order for a long chain', () => { + // Graph: A -> B -> C -> D -> E + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['C']], + ['C', ['D']], + ['D', ['E']], + ], + ['A', 'B', 'C', 'D', 'E'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.indexOf('E')).toBeLessThan(order.indexOf('D')); + expect(order.indexOf('D')).toBeLessThan(order.indexOf('C')); + expect(order.indexOf('C')).toBeLessThan(order.indexOf('B')); + expect(order.indexOf('B')).toBeLessThan(order.indexOf('A')); + expect(order).toHaveLength(5); + }); + + it('walks all nodes, including cycles', () => { + // Graph: A <-> B (cycle), C (no deps) + const graph = makeGraph( + [ + ['A', ['B']], + ['B', ['A']], + ], + ['A', 'B', 'C'], + ); + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + expect(order.sort()).toEqual(['A', 'B', 'C']); + }); + + it('matches ordering for validators-circular-ref spec', async () => { + const specModule = await import( + '../../../../../specs/3.1.x/validators-circular-ref.json' + ); + const spec = specModule.default ?? specModule; + const { graph } = buildGraph(spec, loggerStub); + + const order: Array = []; + walkTopological(graph, (pointer) => order.push(pointer)); + + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const baz = '#/components/schemas/Baz'; + const qux = '#/components/schemas/Qux'; + + // Bar should come before Foo because Foo depends on Bar + expect(order.indexOf(bar)).toBeLessThan(order.indexOf(foo)); + + // Baz and Qux form a mutual $ref cycle; both must be present + expect(order).toContain(baz); + expect(order).toContain(qux); + }); + + it('prefers schema group before parameter when safe (default)', () => { + // parameter then schema in declaration order, no deps -> schema should move before parameter + const param = '#/components/parameters/P'; + const schema = '#/components/schemas/A'; + const nodes = [param, schema]; + const graph = makeGraph([], nodes); + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order.indexOf(schema)).toBeLessThan(order.indexOf(param)); + }); + + it('does not apply preferGroups when it would violate dependencies (fallback)', () => { + // declaration order: param, schema; schema depends on param -> cannot move before param + const param = '#/components/parameters/P'; + const schema = '#/components/schemas/S'; + const nodes = [param, schema]; + const nodeDependencies = new Map>(); + nodeDependencies.set(schema, new Set([param])); + const subtreeDependencies = new Map>(); + const reverseNodeDependencies = new Map>(); + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies, + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + // schema depends on param so param must remain before schema + expect(order.indexOf(param)).toBeLessThan(order.indexOf(schema)); + }); + + it('ignores self-dependencies when ordering', () => { + // Foo has self-ref only, Bar references Foo -> Foo should come before Bar + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const nodes = [foo, bar]; + const nodeDependencies = new Map>(); + nodeDependencies.set(foo, new Set([foo])); + nodeDependencies.set(bar, new Set([foo])); + + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies: new Map>(), + subtreeDependencies: new Map>(), + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + // Foo is a dependency of Bar, so Foo should come before Bar + expect(order.indexOf(foo)).toBeLessThan(order.indexOf(bar)); + }); + + it('uses subtreeDependencies when nodeDependencies are absent', () => { + const parent = '#/components/schemas/Parent'; + const child = '#/components/schemas/Child'; + const nodes = [parent, child]; + const nodeDependencies = new Map>(); + const subtreeDependencies = new Map>(); + subtreeDependencies.set(parent, new Set([child])); + + const nodesMap = new Map(); + for (const n of nodes) + nodesMap.set(n, { key: null, node: {}, parentPointer: null }); + + const graph = { + nodeDependencies, + nodes: nodesMap, + reverseNodeDependencies: new Map>(), + subtreeDependencies, + transitiveDependencies: new Map>(), + } as unknown as Graph; + + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order.indexOf(child)).toBeLessThan(order.indexOf(parent)); + }); + + it('preserves declaration order for equal-priority items (stability)', () => { + const a = '#/components/schemas/A'; + const b = '#/components/schemas/B'; + const c = '#/components/schemas/C'; + const nodes = [a, b, c]; + const graph = makeGraph([], nodes); + const order: Array = []; + walkTopological(graph, (p) => order.push(p)); + expect(order).toEqual(nodes); + }); +}); diff --git a/packages/openapi-ts/src/ir/__tests__/pagination.test.ts b/packages/openapi-ts/src/ir/__tests__/pagination.test.ts index 904666d55..c2b61d951 100644 --- a/packages/openapi-ts/src/ir/__tests__/pagination.test.ts +++ b/packages/openapi-ts/src/ir/__tests__/pagination.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import { defaultPaginationKeywords } from '../../config/parser'; -import type { Config } from '../../types/config'; +import { defaultPaginationKeywords } from '~/config/parser'; +import type { Config } from '~/types/config'; + import { operationPagination } from '../operation'; import { getPaginationKeywordsRegExp } from '../pagination'; import type { IR } from '../types'; diff --git a/packages/openapi-ts/src/ir/context.ts b/packages/openapi-ts/src/ir/context.ts index 3fb4d8f90..93caf1cf9 100644 --- a/packages/openapi-ts/src/ir/context.ts +++ b/packages/openapi-ts/src/ir/context.ts @@ -1,15 +1,17 @@ import { Project } from '@hey-api/codegen-core'; -import type { Package } from '../config/utils/package'; -import { packageFactory } from '../config/utils/package'; -import { TypeScriptRenderer } from '../generate/renderer'; -import { buildName } from '../openApi/shared/utils/name'; -import type { PluginConfigMap } from '../plugins/config'; -import { PluginInstance } from '../plugins/shared/utils/instance'; -import type { PluginNames } from '../plugins/types'; -import type { Config } from '../types/config'; -import type { Logger } from '../utils/logger'; -import { resolveRef } from '../utils/ref'; +import type { Package } from '~/config/utils/package'; +import { packageFactory } from '~/config/utils/package'; +import { TypeScriptRenderer } from '~/generate/renderer'; +import type { Graph } from '~/openApi/shared/utils/graph'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { PluginConfigMap } from '~/plugins/config'; +import { PluginInstance } from '~/plugins/shared/utils/instance'; +import type { PluginNames } from '~/plugins/types'; +import type { Config } from '~/types/config'; +import type { Logger } from '~/utils/logger'; +import { resolveRef } from '~/utils/ref'; + import type { IR } from './types'; export class IRContext = any> { @@ -22,6 +24,10 @@ export class IRContext = any> { * The code generation project instance used to manage files, symbols, */ gen: Project; + /** + * The dependency graph built from the intermediate representation. + */ + graph: Graph | undefined; /** * Intermediate representation model obtained from `spec`. */ diff --git a/packages/openapi-ts/src/ir/graph.ts b/packages/openapi-ts/src/ir/graph.ts new file mode 100644 index 000000000..d92bc9527 --- /dev/null +++ b/packages/openapi-ts/src/ir/graph.ts @@ -0,0 +1,261 @@ +import type { Graph, NodeInfo } from '~/openApi/shared/utils/graph'; +import { MinHeap } from '~/utils/minHeap'; + +type KindPriority = Record; +type PreferGroups = ReadonlyArray; +type PriorityFn = (pointer: string) => number; + +/** + * Walks the nodes of the graph in topological order (dependencies before dependents). + * Calls the callback for each node pointer in order. + * Nodes in cycles are grouped together and emitted in arbitrary order within the group. + * + * @param graph - The dependency graph + * @param callback - Function to call for each node pointer + */ +export const walkTopological = ( + graph: Graph, + callback: (pointer: string, nodeInfo: NodeInfo) => void, + options?: { + preferGroups?: PreferGroups; + priority?: PriorityFn; + }, +) => { + // Stable Kahn's algorithm that respects declaration order as a tiebreaker. + const pointers = Array.from(graph.nodes.keys()); + // Base insertion order + const baseIndex = new Map(); + pointers.forEach((pointer, index) => baseIndex.set(pointer, index)); + + // Composite decl index: group priority then base insertion order + const declIndex = new Map(); + const priorityFn = options?.priority ?? defaultPriorityFn; + for (const pointer of pointers) { + const group = priorityFn(pointer) ?? 10; + const composite = group * 1_000_000 + (baseIndex.get(pointer) ?? 0); + declIndex.set(pointer, composite); + } + + // Build dependency sets for each pointer (prefer subtreeDependencies, fall back to nodeDependencies) + const depsOf = new Map>(); + for (const pointer of pointers) { + const raw = + graph.subtreeDependencies?.get(pointer) ?? + graph.nodeDependencies?.get(pointer) ?? + new Set(); + const filtered = new Set(); + for (const rawPointer of raw) { + if (rawPointer === pointer) continue; // ignore self-dependencies for ordering + if (graph.nodes.has(rawPointer)) { + filtered.add(rawPointer); + } + } + depsOf.set(pointer, filtered); + } + + // Build inDegree and dependents adjacency + const inDegree = new Map(); + const dependents = new Map>(); + for (const pointer of pointers) { + inDegree.set(pointer, 0); + } + for (const [pointer, deps] of depsOf) { + inDegree.set(pointer, deps.size); + for (const d of deps) { + if (!dependents.has(d)) { + dependents.set(d, new Set()); + } + dependents.get(d)!.add(pointer); + } + } + + // Helper to sort pointers by declaration order + const sortByDecl = (arr: Array) => + arr.sort((a, b) => declIndex.get(a)! - declIndex.get(b)!); + + // Initialize queue with zero-inDegree nodes in declaration order + // Use a small binary min-heap prioritized by declaration index to avoid repeated full sorts. + const heap = new MinHeap(declIndex); + for (const pointer of pointers) { + if ((inDegree.get(pointer) ?? 0) === 0) { + heap.push(pointer); + } + } + + const emitted = new Set(); + const order: Array = []; + + while (!heap.isEmpty()) { + const cur = heap.pop()!; + if (emitted.has(cur)) continue; + emitted.add(cur); + order.push(cur); + + const deps = dependents.get(cur); + if (!deps) continue; + + for (const dep of deps) { + const v = (inDegree.get(dep) ?? 0) - 1; + inDegree.set(dep, v); + if (v === 0) { + heap.push(dep); + } + } + } + + // emit remaining nodes (cycles) in declaration order + const remaining = pointers.filter((pointer) => !emitted.has(pointer)); + sortByDecl(remaining); + for (const pointer of remaining) { + emitted.add(pointer); + order.push(pointer); + } + + // prefer specified groups when safe + let finalOrder = order; + const preferGroups = options?.preferGroups ?? defaultPreferGroups; + if (preferGroups && preferGroups.length > 0) { + // build group priority map (lower = earlier) + const groupPriority = new Map(); + for (let i = 0; i < preferGroups.length; i++) { + const k = preferGroups[i]; + if (k) { + groupPriority.set(k, i); + } + } + + const getGroup: PriorityFn = (pointer) => { + const result = matchIrTopLevelPointer(pointer); + if (result.matched) { + return groupPriority.has(result.kind) + ? groupPriority.get(result.kind)! + : preferGroups.length; + } + return preferGroups.length; + }; + + // proposed order: sort by (groupPriority, originalIndex) + const proposed = [...order].sort((a, b) => { + const ga = getGroup(a); + const gb = getGroup(b); + return ga !== gb ? ga - gb : order.indexOf(a) - order.indexOf(b); + }); + + // Build quick lookup of original index and proposed index + const proposedIndex = new Map(); + for (let i = 0; i < proposed.length; i++) { + proposedIndex.set(proposed[i]!, i); + } + + // Micro-optimization: only validate edges where group(dep) > group(node) + const violated = (() => { + for (const [node, deps] of depsOf) { + for (const dep of deps) { + const gDep = getGroup(dep); + const gNode = getGroup(node); + if (gDep <= gNode) continue; // not a crossing edge, cannot be violated by grouping + const pDep = proposedIndex.get(dep)!; + const pNode = proposedIndex.get(node)!; + if (pDep >= pNode) { + return true; + } + } + } + return false; + })(); + + if (!violated) { + finalOrder = proposed; + } + } + + // Finally, call back in final order + for (const pointer of finalOrder) { + callback(pointer, graph.nodes.get(pointer)!); + } +}; + +export const irTopLevelKinds = [ + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'server', + 'webhook', +] as const; + +export type IrTopLevelKind = (typeof irTopLevelKinds)[number]; + +export type IrTopLevelPointerMatch = + | { kind: IrTopLevelKind; matched: true } + | { kind?: undefined; matched: false }; + +/** + * Checks if a pointer matches a known top-level IR component (schema, parameter, etc) and returns match info. + * + * @param pointer - The IR pointer string (e.g. '#/components/schemas/Foo') + * @param kind - (Optional) The component kind to check + * @returns { matched: true, kind: IrTopLevelKind } | { matched: false } - Whether it matched, and the matched kind if so + */ +export const matchIrTopLevelPointer = ( + pointer: string, + kind?: IrTopLevelKind, +): IrTopLevelPointerMatch => { + const patterns: Record = { + operation: + /^#\/paths\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/, + parameter: /^#\/components\/parameters\/[^/]+$/, + requestBody: /^#\/components\/requestBodies\/[^/]+$/, + schema: /^#\/components\/schemas\/[^/]+$/, + server: /^#\/servers\/(\d+|[^/]+)$/, + webhook: + /^#\/webhooks\/[^/]+\/(get|put|post|delete|options|head|patch|trace)$/, + }; + if (kind) { + return patterns[kind].test(pointer) + ? { kind, matched: true } + : { matched: false }; + } + for (const key of Object.keys(patterns)) { + const kind = key as IrTopLevelKind; + if (patterns[kind].test(pointer)) { + return { kind, matched: true }; + } + } + return { matched: false }; +}; + +// default grouping preference (earlier groups emitted first when safe) +export const defaultPreferGroups = [ + 'schema', + 'parameter', + 'requestBody', + 'operation', + 'server', + 'webhook', +] satisfies PreferGroups; + +// default group priority (lower = earlier) +// built from `defaultPreferGroups` so the priority order stays in sync with the prefer-groups array. +const defaultKindPriority: KindPriority = (() => { + const partial: Partial = {}; + for (let i = 0; i < defaultPreferGroups.length; i++) { + const k = defaultPreferGroups[i]; + if (k) partial[k] = i; + } + // Ensure all known kinds exist in the map (fall back to a high index). + for (const k of irTopLevelKinds) { + if (partial[k] === undefined) { + partial[k] = defaultPreferGroups.length; + } + } + return partial as KindPriority; +})(); + +const defaultPriorityFn: PriorityFn = (pointer) => { + const result = matchIrTopLevelPointer(pointer); + if (result.matched) { + return defaultKindPriority[result.kind] ?? 10; + } + return 10; +}; diff --git a/packages/openapi-ts/src/ir/pagination.ts b/packages/openapi-ts/src/ir/pagination.ts index cb1a7a466..b7f29126c 100644 --- a/packages/openapi-ts/src/ir/pagination.ts +++ b/packages/openapi-ts/src/ir/pagination.ts @@ -1,4 +1,5 @@ -import type { Config } from '../types/config'; +import type { Config } from '~/types/config'; + import type { IR } from './types'; export function getPaginationKeywordsRegExp( diff --git a/packages/openapi-ts/src/ir/types.d.ts b/packages/openapi-ts/src/ir/types.d.ts index 309053d34..5f9ec4522 100644 --- a/packages/openapi-ts/src/ir/types.d.ts +++ b/packages/openapi-ts/src/ir/types.d.ts @@ -1,10 +1,11 @@ import type { Symbol } from '@hey-api/codegen-core'; -import type { JsonSchemaDraft2020_12 } from '../openApi/3.1.x/types/json-schema-draft-2020-12'; +import type { JsonSchemaDraft2020_12 } from '~/openApi/3.1.x/types/json-schema-draft-2020-12'; import type { SecuritySchemeObject, ServerObject, -} from '../openApi/3.1.x/types/spec'; +} from '~/openApi/3.1.x/types/spec'; + import type { IRContext } from './context'; import type { IRMediaType } from './mediaType'; @@ -249,13 +250,6 @@ interface IRSchemaObject * properties altogether. */ additionalProperties?: IRSchemaObject | false; - /** - * If this schema is a $ref and is circular (points to itself or is in the current resolution stack), - * this flag is set to true. - * - * @default undefined - */ - circular?: boolean; /** * Any string value is accepted as `format`. */ @@ -281,7 +275,6 @@ interface IRSchemaObject * When type is `object`, `properties` will contain a map of its properties. */ properties?: Record; - /** * The names of `properties` can be validated against a schema, irrespective * of their values. This can be useful if you don't want to enforce specific diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts index e1a64a0cc..993f21b18 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/server.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it } from 'vitest'; +import type { OpenApi } from '~/openApi/types'; + import type { IR } from '../../../../ir/types'; -import type { OpenApi } from '../../../types'; import { parseServers } from '../server'; describe('parseServers', () => { diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/validate.test.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/validate.test.ts index f4c7bf23a..676c2ddbc 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/validate.test.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/__tests__/validate.test.ts @@ -2,9 +2,10 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; +import { getSpecsPath, specFileToJson } from '~/openApi/__tests__/utils'; +import type { ValidatorResult } from '~/openApi/shared/utils/validator'; + import { Logger } from '../../../../utils/logger'; -import { getSpecsPath, specFileToJson } from '../../../__tests__/utils'; -import type { ValidatorResult } from '../../../shared/utils/validator'; import { validateOpenApiSpec } from '../validate'; const specsFolder = path.join(getSpecsPath(), '2.0.x', 'invalid'); diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/filter.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/filter.ts index fe934c174..0894abfc2 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/filter.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/filter.ts @@ -1,7 +1,8 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { addNamespace, removeNamespace } from '../../shared/utils/filter'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { addNamespace, removeNamespace } from '~/openApi/shared/utils/filter'; +import { httpMethods } from '~/openApi/shared/utils/operation'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV2_0_X, OperationObject, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/index.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/index.ts index 3cf6fdd9c..6b2be37b3 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/index.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/index.ts @@ -1,15 +1,16 @@ -import type { IR } from '../../../ir/types'; -import { buildResourceMetadata } from '../../shared/graph/meta'; -import { transformOpenApiSpec } from '../../shared/transforms'; -import type { State } from '../../shared/types/state'; +import type { IR } from '~/ir/types'; +import { buildResourceMetadata } from '~/openApi/shared/graph/meta'; +import { transformOpenApiSpec } from '~/openApi/shared/transforms'; +import type { State } from '~/openApi/shared/types/state'; import { createFilteredDependencies, createFilters, hasFilters, -} from '../../shared/utils/filter'; -import { buildGraph } from '../../shared/utils/graph'; -import { mergeParametersObjects } from '../../shared/utils/parameter'; -import { handleValidatorResult } from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/filter'; +import { buildGraph } from '~/openApi/shared/utils/graph'; +import { mergeParametersObjects } from '~/openApi/shared/utils/parameter'; +import { handleValidatorResult } from '~/openApi/shared/utils/validator'; + import type { OpenApiV2_0_X, OperationObject, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts index 913f0967b..1c4f3e1b7 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/mediaType.ts @@ -1,8 +1,6 @@ -import type { IRMediaType } from '../../../ir/mediaType'; -import { - isMediaTypeFileLike, - mediaTypeToIrMediaType, -} from '../../../ir/mediaType'; +import type { IRMediaType } from '~/ir/mediaType'; +import { isMediaTypeFileLike, mediaTypeToIrMediaType } from '~/ir/mediaType'; + import type { ReferenceObject, ResponseObject, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts index 83a21f3fa..99c80c34b 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/operation.ts @@ -1,6 +1,7 @@ -import type { IR, IRBodyObject } from '../../../ir/types'; -import type { State } from '../../shared/types/state'; -import { operationToId } from '../../shared/utils/operation'; +import type { IR, IRBodyObject } from '~/ir/types'; +import type { State } from '~/openApi/shared/types/state'; +import { operationToId } from '~/openApi/shared/utils/operation'; + import type { OperationObject, ParameterObject, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts index bc211b973..f92b727cd 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts @@ -1,6 +1,7 @@ -import { getPaginationKeywordsRegExp } from '../../../ir/pagination'; -import type { IR } from '../../../ir/types'; -import type { SchemaType } from '../../shared/types/schema'; +import { getPaginationKeywordsRegExp } from '~/ir/pagination'; +import type { IR } from '~/ir/types'; +import type { SchemaType } from '~/openApi/shared/types/schema'; + import type { ParameterObject, ReferenceObject } from '../types/spec'; import type { SchemaObject } from '../types/spec'; import { getSchemaType } from './schema'; diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts index cc1117d46..531f0c0d9 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/parameter.ts @@ -1,4 +1,5 @@ -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; + import type { OperationObject, ParameterObject, @@ -146,7 +147,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts index 6b03b24f1..c48f84164 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/schema.ts @@ -1,12 +1,13 @@ -import type { IR } from '../../../ir/types'; -import { addItemsToSchema } from '../../../ir/utils'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { addItemsToSchema } from '~/ir/utils'; import type { SchemaState, SchemaType, SchemaWithRequired, -} from '../../shared/types/schema'; -import { discriminatorValues } from '../../shared/utils/discriminator'; +} from '~/openApi/shared/types/schema'; +import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; +import { refToName } from '~/utils/ref'; + import type { SchemaObject } from '../types/spec'; export const getSchemaType = ({ @@ -535,7 +536,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/definitions/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -545,7 +545,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -562,15 +561,7 @@ const parseRef = ({ '#/components/schemas/$1', ); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -579,11 +570,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } return irSchema; @@ -771,7 +758,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -838,7 +824,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts index b33f50d30..ee8241316 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/server.ts @@ -1,5 +1,5 @@ -import type { IR } from '../../../ir/types'; -import { parseUrl } from '../../../utils/url'; +import type { IR } from '~/ir/types'; +import { parseUrl } from '~/utils/url'; export const parseServers = ({ context }: { context: IR.Context }) => { let schemes: ReadonlyArray = context.spec.schemes ?? []; diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/validate.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/validate.ts index 11944c4ba..a340487ce 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/validate.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/validate.ts @@ -1,10 +1,11 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { httpMethods } from '~/openApi/shared/utils/operation'; import type { ValidatorIssue, ValidatorResult, -} from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/validator'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV2_0_X, PathItemObject, PathsObject } from '../types/spec'; export const validateOpenApiSpec = ( diff --git a/packages/openapi-ts/src/openApi/2.0.x/types/json-schema-draft-4.d.ts b/packages/openapi-ts/src/openApi/2.0.x/types/json-schema-draft-4.d.ts index 6b0ebb7e0..7f273a5b7 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/types/json-schema-draft-4.d.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/types/json-schema-draft-4.d.ts @@ -1,4 +1,4 @@ -import type { EnumExtensions } from '../../shared/types/openapi-spec-extensions'; +import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; export interface JsonSchemaDraft4 extends EnumExtensions { /** diff --git a/packages/openapi-ts/src/openApi/2.0.x/types/spec.d.ts b/packages/openapi-ts/src/openApi/2.0.x/types/spec.d.ts index 0f41dd545..9f2d530e0 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/types/spec.d.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/types/spec.d.ts @@ -1,4 +1,5 @@ -import type { EnumExtensions } from '../../shared/types/openapi-spec-extensions'; +import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; + import type { JsonSchemaDraft4 } from './json-schema-draft-4'; import type { OpenApiV2_0_X_Nullable_Extensions } from './openapi-spec-extensions'; diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/validate.test.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/validate.test.ts index 6d06468fc..9af472378 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/validate.test.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/__tests__/validate.test.ts @@ -2,9 +2,10 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; +import { getSpecsPath, specFileToJson } from '~/openApi/__tests__/utils'; +import type { ValidatorResult } from '~/openApi/shared/utils/validator'; + import { Logger } from '../../../../utils/logger'; -import { getSpecsPath, specFileToJson } from '../../../__tests__/utils'; -import type { ValidatorResult } from '../../../shared/utils/validator'; import { validateOpenApiSpec } from '../validate'; const specsFolder = path.join(getSpecsPath(), '3.0.x', 'invalid'); diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/filter.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/filter.ts index da5570e18..0bec360ac 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/filter.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/filter.ts @@ -1,7 +1,8 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { addNamespace, removeNamespace } from '../../shared/utils/filter'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { addNamespace, removeNamespace } from '~/openApi/shared/utils/filter'; +import { httpMethods } from '~/openApi/shared/utils/operation'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV3_0_X, PathItemObject, PathsObject } from '../types/spec'; /** diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts index 60c81716c..f21729226 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/index.ts @@ -1,15 +1,16 @@ -import type { IR } from '../../../ir/types'; -import { buildResourceMetadata } from '../../shared/graph/meta'; -import { transformOpenApiSpec } from '../../shared/transforms'; -import type { State } from '../../shared/types/state'; +import type { IR } from '~/ir/types'; +import { buildResourceMetadata } from '~/openApi/shared/graph/meta'; +import { transformOpenApiSpec } from '~/openApi/shared/transforms'; +import type { State } from '~/openApi/shared/types/state'; import { createFilteredDependencies, createFilters, hasFilters, -} from '../../shared/utils/filter'; -import { buildGraph } from '../../shared/utils/graph'; -import { mergeParametersObjects } from '../../shared/utils/parameter'; -import { handleValidatorResult } from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/filter'; +import { buildGraph } from '~/openApi/shared/utils/graph'; +import { mergeParametersObjects } from '~/openApi/shared/utils/parameter'; +import { handleValidatorResult } from '~/openApi/shared/utils/validator'; + import type { OpenApiV3_0_X, ParameterObject, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/mediaType.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/mediaType.ts index 5ed7093ce..784a7ab4d 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/mediaType.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/mediaType.ts @@ -1,8 +1,6 @@ -import type { IRMediaType } from '../../../ir/mediaType'; -import { - isMediaTypeFileLike, - mediaTypeToIrMediaType, -} from '../../../ir/mediaType'; +import type { IRMediaType } from '~/ir/mediaType'; +import { isMediaTypeFileLike, mediaTypeToIrMediaType } from '~/ir/mediaType'; + import type { MediaTypeObject, ReferenceObject, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts index c0967839b..6d187a97d 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/operation.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { State } from '../../shared/types/state'; -import { operationToId } from '../../shared/utils/operation'; +import type { IR } from '~/ir/types'; +import type { State } from '~/openApi/shared/types/state'; +import { operationToId } from '~/openApi/shared/utils/operation'; + import type { OperationObject, PathItemObject, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts index 325d626e6..589ac9153 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts @@ -1,6 +1,7 @@ -import { getPaginationKeywordsRegExp } from '../../../ir/pagination'; -import type { IR } from '../../../ir/types'; -import type { SchemaType } from '../../shared/types/schema'; +import { getPaginationKeywordsRegExp } from '~/ir/pagination'; +import type { IR } from '~/ir/types'; +import type { SchemaType } from '~/openApi/shared/types/schema'; + import type { ParameterObject, ReferenceObject, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts index 75f1a5fa6..78622865e 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/parameter.ts @@ -1,5 +1,6 @@ -import type { IR } from '../../../ir/types'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { refToName } from '~/utils/ref'; + import type { ParameterObject, ReferenceObject, @@ -150,7 +151,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts index 2aa311f4d..593146fa9 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/requestBody.ts @@ -1,5 +1,6 @@ -import type { IR } from '../../../ir/types'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { refToName } from '~/utils/ref'; + import type { RequestBodyObject, SchemaObject } from '../types/spec'; import { mediaTypeObjects } from './mediaType'; import { schemaToIrSchema } from './schema'; @@ -32,7 +33,6 @@ const requestBodyToIrRequestBody = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), }; diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts index b80ca1949..aba96eeac 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/schema.ts @@ -1,12 +1,13 @@ -import type { IR } from '../../../ir/types'; -import { addItemsToSchema } from '../../../ir/utils'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { addItemsToSchema } from '~/ir/utils'; import type { SchemaState, SchemaType, SchemaWithRequired, -} from '../../shared/types/schema'; -import { discriminatorValues } from '../../shared/utils/discriminator'; +} from '~/openApi/shared/types/schema'; +import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; +import { refToName } from '~/utils/ref'; + import type { ReferenceObject, SchemaObject } from '../types/spec'; export const getSchemaType = ({ @@ -741,7 +742,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/components/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -751,7 +751,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -763,15 +762,7 @@ const parseRef = ({ // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` irSchema.$ref = decodeURI(schema.$ref); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -780,11 +771,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } return irSchema; @@ -972,7 +959,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -1055,7 +1041,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts index 8fb309f00..9732ada5f 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/server.ts @@ -1,5 +1,5 @@ -import type { IR } from '../../../ir/types'; -import { parseUrl } from '../../../utils/url'; +import type { IR } from '~/ir/types'; +import { parseUrl } from '~/utils/url'; export const parseServers = ({ context }: { context: IR.Context }) => { if (context.spec.servers) { diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/validate.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/validate.ts index 46220981b..e44ae495c 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/validate.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/validate.ts @@ -1,10 +1,11 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { httpMethods } from '~/openApi/shared/utils/operation'; import type { ValidatorIssue, ValidatorResult, -} from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/validator'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV3_0_X, PathItemObject, PathsObject } from '../types/spec'; export const validateOpenApiSpec = ( diff --git a/packages/openapi-ts/src/openApi/3.0.x/types/spec.d.ts b/packages/openapi-ts/src/openApi/3.0.x/types/spec.d.ts index 5c6c23682..65adaa4c5 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/types/spec.d.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/types/spec.d.ts @@ -1,4 +1,4 @@ -import type { EnumExtensions } from '../../shared/types/openapi-spec-extensions'; +import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; /** * This is the root object of the {@link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.4.md#openapi-description OpenAPI Description}. diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/validate.test.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/validate.test.ts index 2071c31e7..878e77990 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/validate.test.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/__tests__/validate.test.ts @@ -2,9 +2,10 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; +import { getSpecsPath, specFileToJson } from '~/openApi/__tests__/utils'; +import type { ValidatorResult } from '~/openApi/shared/utils/validator'; + import { Logger } from '../../../../utils/logger'; -import { getSpecsPath, specFileToJson } from '../../../__tests__/utils'; -import type { ValidatorResult } from '../../../shared/utils/validator'; import { validateOpenApiSpec } from '../validate'; const specsFolder = path.join(getSpecsPath(), '3.1.x', 'invalid'); diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/filter.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/filter.ts index 87b496390..577e00820 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/filter.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/filter.ts @@ -1,7 +1,8 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { addNamespace, removeNamespace } from '../../shared/utils/filter'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { addNamespace, removeNamespace } from '~/openApi/shared/utils/filter'; +import { httpMethods } from '~/openApi/shared/utils/operation'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV3_1_X, PathItemObject, PathsObject } from '../types/spec'; /** diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts index 05c181eae..e5886ed36 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/index.ts @@ -1,15 +1,16 @@ -import type { IR } from '../../../ir/types'; -import { buildResourceMetadata } from '../../shared/graph/meta'; -import { transformOpenApiSpec } from '../../shared/transforms'; -import type { State } from '../../shared/types/state'; +import type { IR } from '~/ir/types'; +import { buildResourceMetadata } from '~/openApi/shared/graph/meta'; +import { transformOpenApiSpec } from '~/openApi/shared/transforms'; +import type { State } from '~/openApi/shared/types/state'; import { createFilteredDependencies, createFilters, hasFilters, -} from '../../shared/utils/filter'; -import { buildGraph } from '../../shared/utils/graph'; -import { mergeParametersObjects } from '../../shared/utils/parameter'; -import { handleValidatorResult } from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/filter'; +import { buildGraph } from '~/openApi/shared/utils/graph'; +import { mergeParametersObjects } from '~/openApi/shared/utils/parameter'; +import { handleValidatorResult } from '~/openApi/shared/utils/validator'; + import type { OpenApiV3_1_X, ParameterObject, diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/mediaType.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/mediaType.ts index 5b5bf1ae6..b0f956deb 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/mediaType.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/mediaType.ts @@ -1,8 +1,6 @@ -import type { IRMediaType } from '../../../ir/mediaType'; -import { - isMediaTypeFileLike, - mediaTypeToIrMediaType, -} from '../../../ir/mediaType'; +import type { IRMediaType } from '~/ir/mediaType'; +import { isMediaTypeFileLike, mediaTypeToIrMediaType } from '~/ir/mediaType'; + import type { MediaTypeObject, SchemaObject } from '../types/spec'; interface Content { diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts index 393ef07cf..31824cb37 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/operation.ts @@ -1,7 +1,8 @@ -import type { IR } from '../../../ir/types'; -import type { State } from '../../shared/types/state'; -import type { httpMethods } from '../../shared/utils/operation'; -import { operationToId } from '../../shared/utils/operation'; +import type { IR } from '~/ir/types'; +import type { State } from '~/openApi/shared/types/state'; +import type { httpMethods } from '~/openApi/shared/utils/operation'; +import { operationToId } from '~/openApi/shared/utils/operation'; + import type { OperationObject, RequestBodyObject, diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts index 500a949f0..00ac2aa95 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts @@ -1,6 +1,7 @@ -import { getPaginationKeywordsRegExp } from '../../../ir/pagination'; -import type { IR } from '../../../ir/types'; -import type { SchemaType } from '../../shared/types/schema'; +import { getPaginationKeywordsRegExp } from '~/ir/pagination'; +import type { IR } from '~/ir/types'; +import type { SchemaType } from '~/openApi/shared/types/schema'; + import type { ParameterObject, RequestBodyObject } from '../types/spec'; import type { SchemaObject } from '../types/spec'; import { mediaTypeObjects } from './mediaType'; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts index 13748445d..607d4db45 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/parameter.ts @@ -1,5 +1,6 @@ -import type { IR } from '../../../ir/types'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { refToName } from '~/utils/ref'; + import type { ParameterObject, ReferenceObject, @@ -143,7 +144,6 @@ const parameterToIrParameter = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), style, diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts index 2aa311f4d..593146fa9 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/requestBody.ts @@ -1,5 +1,6 @@ -import type { IR } from '../../../ir/types'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { refToName } from '~/utils/ref'; + import type { RequestBodyObject, SchemaObject } from '../types/spec'; import { mediaTypeObjects } from './mediaType'; import { schemaToIrSchema } from './schema'; @@ -32,7 +33,6 @@ const requestBodyToIrRequestBody = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }), }; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts index b2ac1c3f7..8dea32e73 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/schema.ts @@ -1,12 +1,13 @@ -import type { IR } from '../../../ir/types'; -import { addItemsToSchema } from '../../../ir/utils'; -import { refToName } from '../../../utils/ref'; +import type { IR } from '~/ir/types'; +import { addItemsToSchema } from '~/ir/utils'; import type { SchemaState, SchemaType, SchemaWithRequired, -} from '../../shared/types/schema'; -import { discriminatorValues } from '../../shared/utils/discriminator'; +} from '~/openApi/shared/types/schema'; +import { discriminatorValues } from '~/openApi/shared/utils/discriminator'; +import { refToName } from '~/utils/ref'; + import type { SchemaObject } from '../types/spec'; export const getSchemaTypes = ({ @@ -800,7 +801,6 @@ const parseRef = ({ const isComponentsRef = schema.$ref.startsWith('#/components/'); if (!isComponentsRef) { if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -810,7 +810,6 @@ const parseRef = ({ state, }); state.$ref = originalRef; - state.refStack.pop(); return irSchema; } // Fallback to preserving the ref if circular @@ -824,15 +823,7 @@ const parseRef = ({ // but the suspicion is this comes from `@hey-api/json-schema-ref-parser` irRefSchema.$ref = decodeURI(schema.$ref); - if (state.refStack.includes(schema.$ref)) { - if (state.refStack[0] === schema.$ref) { - state.circularRef = schema.$ref; - } - irSchema.circular = true; - } - if (!state.circularReferenceTracker.has(schema.$ref)) { - state.refStack.push(schema.$ref); const refSchema = context.resolveRef(schema.$ref); const originalRef = state.$ref; state.$ref = schema.$ref; @@ -841,11 +832,7 @@ const parseRef = ({ schema: refSchema, state, }); - if (state.circularRef && state.refStack[0] === state.circularRef) { - irSchema.circular = true; - } state.$ref = originalRef; - state.refStack.pop(); } const schemaItems: Array = []; @@ -1054,7 +1041,6 @@ export const schemaToIrSchema = ({ if (!state) { state = { circularReferenceTracker: new Set(), - refStack: [], }; } @@ -1137,7 +1123,6 @@ export const parseSchema = ({ state: { $ref, circularReferenceTracker: new Set(), - refStack: [$ref], }, }); }; diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts index 8fb309f00..9732ada5f 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/server.ts @@ -1,5 +1,5 @@ -import type { IR } from '../../../ir/types'; -import { parseUrl } from '../../../utils/url'; +import type { IR } from '~/ir/types'; +import { parseUrl } from '~/utils/url'; export const parseServers = ({ context }: { context: IR.Context }) => { if (context.spec.servers) { diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/validate.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/validate.ts index 5d941e1ee..fcdb8e62e 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/validate.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/validate.ts @@ -1,10 +1,11 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { httpMethods } from '../../shared/utils/operation'; +import { createOperationKey } from '~/ir/operation'; +import { httpMethods } from '~/openApi/shared/utils/operation'; import type { ValidatorIssue, ValidatorResult, -} from '../../shared/utils/validator'; +} from '~/openApi/shared/utils/validator'; +import type { Logger } from '~/utils/logger'; + import type { OpenApiV3_1_X, PathItemObject, PathsObject } from '../types/spec'; export const validateOpenApiSpec = ( diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/webhook.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/webhook.ts index 653cab559..cbeaf4ac5 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/webhook.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/webhook.ts @@ -1,5 +1,6 @@ -import type { IR } from '../../../ir/types'; -import { mergeParametersObjects } from '../../shared/utils/parameter'; +import type { IR } from '~/ir/types'; +import { mergeParametersObjects } from '~/openApi/shared/utils/parameter'; + import type { OpenApiV3_1_X, PathItemObject } from '../types/spec'; import { parseWebhookOperation } from './operation'; import { parametersArrayToObject } from './parameter'; diff --git a/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts b/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts index 68ce48afe..0acd88981 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts @@ -1,5 +1,6 @@ +import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; + import type { MaybeArray } from '../../../types/utils'; -import type { EnumExtensions } from '../../shared/types/openapi-spec-extensions'; import type { OpenApiSchemaExtensions } from './spec-extensions'; // TODO: left out some keywords related to structuring a complex schema and declaring a dialect diff --git a/packages/openapi-ts/src/openApi/__tests__/index.test.ts b/packages/openapi-ts/src/openApi/__tests__/index.test.ts index cee23795c..8ed46431f 100644 --- a/packages/openapi-ts/src/openApi/__tests__/index.test.ts +++ b/packages/openapi-ts/src/openApi/__tests__/index.test.ts @@ -1,13 +1,14 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; +import type { OpenApiV3_0_X } from '~/openApi/3.0.x'; +import { parseV3_0_X } from '~/openApi/3.0.x'; +import type { OpenApiV3_1_X } from '~/openApi/3.1.x'; +import { parseV3_1_X } from '~/openApi/3.1.x'; +import * as parseV2 from '~/openApi/v2'; +import * as parseV3 from '~/openApi/v3'; + import type { Config } from '../../types/config'; import { type OpenApi, parseLegacy, parseOpenApiSpec } from '..'; -import type { OpenApiV3_0_X } from '../3.0.x'; -import { parseV3_0_X } from '../3.0.x'; -import type { OpenApiV3_1_X } from '../3.1.x'; -import { parseV3_1_X } from '../3.1.x'; -import * as parseV2 from '../v2'; -import * as parseV3 from '../v3'; vi.mock('../3.0.x', () => ({ parseV3_0_X: vi.fn(), diff --git a/packages/openapi-ts/src/openApi/common/interfaces/OpenApi.ts b/packages/openapi-ts/src/openApi/common/interfaces/OpenApi.ts index 41a9fe08d..656e638d4 100644 --- a/packages/openapi-ts/src/openApi/common/interfaces/OpenApi.ts +++ b/packages/openapi-ts/src/openApi/common/interfaces/OpenApi.ts @@ -1,4 +1,4 @@ -import type { OpenApi as OpenApiV2 } from '../../v2/interfaces/OpenApi'; -import type { OpenApi as OpenApiV3 } from '../../v3/interfaces/OpenApi'; +import type { OpenApi as OpenApiV2 } from '~/openApi/v2/interfaces/OpenApi'; +import type { OpenApi as OpenApiV3 } from '~/openApi/v3/interfaces/OpenApi'; export type OpenApi = OpenApiV2 | OpenApiV3; diff --git a/packages/openapi-ts/src/openApi/common/interfaces/client.ts b/packages/openapi-ts/src/openApi/common/interfaces/client.ts index f22e2e5d9..b11f7a3c2 100644 --- a/packages/openapi-ts/src/openApi/common/interfaces/client.ts +++ b/packages/openapi-ts/src/openApi/common/interfaces/client.ts @@ -1,5 +1,6 @@ +import type { OpenApiParameter } from '~/openApi/v3/interfaces/OpenApiParameter'; + import type { Config } from '../../../types/config'; -import type { OpenApiParameter } from '../../v3/interfaces/OpenApiParameter'; export interface ModelComposition extends Pick { diff --git a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts index bc2d9426d..62de82c5e 100644 --- a/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts +++ b/packages/openapi-ts/src/openApi/common/parser/__tests__/type.test.ts @@ -9,8 +9,8 @@ vi.mock('../../../../utils/config', () => { plugins: { '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/openApi/common/parser/getDefault.ts b/packages/openapi-ts/src/openApi/common/parser/getDefault.ts index a77126b5e..551f5f953 100644 --- a/packages/openapi-ts/src/openApi/common/parser/getDefault.ts +++ b/packages/openapi-ts/src/openApi/common/parser/getDefault.ts @@ -1,7 +1,8 @@ +import type { OpenApiParameter } from '~/openApi/v2/interfaces/OpenApiParameter'; +import type { OpenApiSchema } from '~/openApi/v3/interfaces/OpenApiSchema'; +import { getDefinitionTypes } from '~/openApi/v3/parser/inferType'; + import type { Model } from '../../common/interfaces/client'; -import type { OpenApiParameter } from '../../v2/interfaces/OpenApiParameter'; -import type { OpenApiSchema } from '../../v3/interfaces/OpenApiSchema'; -import { getDefinitionTypes } from '../../v3/parser/inferType'; import type { OperationParameter } from '../interfaces/client'; export const getDefault = ( diff --git a/packages/openapi-ts/src/openApi/common/parser/getEnums.ts b/packages/openapi-ts/src/openApi/common/parser/getEnums.ts index ac835a40d..81767d450 100644 --- a/packages/openapi-ts/src/openApi/common/parser/getEnums.ts +++ b/packages/openapi-ts/src/openApi/common/parser/getEnums.ts @@ -1,4 +1,5 @@ -import { unique } from '../../../utils/unique'; +import { unique } from '~/utils/unique'; + import type { Enum } from '../interfaces/client'; import type { WithEnumExtension } from '../interfaces/WithEnumExtension'; diff --git a/packages/openapi-ts/src/openApi/common/parser/getRef.ts b/packages/openapi-ts/src/openApi/common/parser/getRef.ts index e2b089b04..98cec868c 100644 --- a/packages/openapi-ts/src/openApi/common/parser/getRef.ts +++ b/packages/openapi-ts/src/openApi/common/parser/getRef.ts @@ -1,5 +1,6 @@ -import type { OpenApiReference as OpenApiReferenceV2 } from '../../v2/interfaces/OpenApiReference'; -import type { OpenApiReference as OpenApiReferenceV3 } from '../../v3/interfaces/OpenApiReference'; +import type { OpenApiReference as OpenApiReferenceV2 } from '~/openApi/v2/interfaces/OpenApiReference'; +import type { OpenApiReference as OpenApiReferenceV3 } from '~/openApi/v3/interfaces/OpenApiReference'; + import type { OpenApi } from '../interfaces/OpenApi'; const ESCAPED_REF_SLASH = /~1/g; diff --git a/packages/openapi-ts/src/openApi/common/parser/operation.ts b/packages/openapi-ts/src/openApi/common/parser/operation.ts index b7fe285d0..c4319a8c2 100644 --- a/packages/openapi-ts/src/openApi/common/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/common/parser/operation.ts @@ -1,7 +1,8 @@ +import { getConfig, isLegacyClient } from '~/utils/config'; +import { stringCase } from '~/utils/stringCase'; +import { transformTypeKeyName } from '~/utils/type'; + import type { Config } from '../../../types/config'; -import { getConfig, isLegacyClient } from '../../../utils/config'; -import { stringCase } from '../../../utils/stringCase'; -import { transformTypeKeyName } from '../../../utils/type'; import type { OperationParameter, OperationResponse, diff --git a/packages/openapi-ts/src/openApi/common/parser/sanitize.ts b/packages/openapi-ts/src/openApi/common/parser/sanitize.ts index d31e565d9..27fa71955 100644 --- a/packages/openapi-ts/src/openApi/common/parser/sanitize.ts +++ b/packages/openapi-ts/src/openApi/common/parser/sanitize.ts @@ -1,4 +1,4 @@ -import { illegalStartCharactersRegExp } from '../../../utils/regexp'; +import { illegalStartCharactersRegExp } from '~/utils/regexp'; export const ensureValidTypeScriptJavaScriptIdentifier = (name: string) => { const replaced = name.replace(/[^$\u200c\u200d\p{ID_Continue}]/gu, '_'); diff --git a/packages/openapi-ts/src/openApi/common/parser/type.ts b/packages/openapi-ts/src/openApi/common/parser/type.ts index 98e8709ca..bd8bde457 100644 --- a/packages/openapi-ts/src/openApi/common/parser/type.ts +++ b/packages/openapi-ts/src/openApi/common/parser/type.ts @@ -1,6 +1,7 @@ -import { refParametersPartial } from '../../../utils/const'; -import { transformTypeName } from '../../../utils/transform'; -import { isDefinitionTypeNullable } from '../../v3/parser/inferType'; +import { isDefinitionTypeNullable } from '~/openApi/v3/parser/inferType'; +import { refParametersPartial } from '~/utils/const'; +import { transformTypeName } from '~/utils/transform'; + import type { Type } from '../interfaces/Type'; import { ensureValidTypeScriptJavaScriptIdentifier } from './sanitize'; import { stripNamespace } from './stripNamespace'; diff --git a/packages/openapi-ts/src/openApi/index.ts b/packages/openapi-ts/src/openApi/index.ts index 06594f164..f27cb4de2 100644 --- a/packages/openapi-ts/src/openApi/index.ts +++ b/packages/openapi-ts/src/openApi/index.ts @@ -1,16 +1,16 @@ -import { satisfies } from '../config/utils/package'; -import { IRContext } from '../ir/context'; -import type { IR } from '../ir/types'; -import type { Config } from '../types/config'; -import type { Logger } from '../utils/logger'; -import { parseV2_0_X } from './2.0.x'; -import { parseV3_0_X } from './3.0.x'; -import { parseV3_1_X } from './3.1.x'; -import type { Client } from './common/interfaces/client'; -import type { OpenApi as LegacyOpenApi } from './common/interfaces/OpenApi'; -import type { OpenApi } from './types'; -import { parse as parseV2 } from './v2'; -import { parse as parseV3 } from './v3'; +import { satisfies } from '~/config/utils/package'; +import { IRContext } from '~/ir/context'; +import type { IR } from '~/ir/types'; +import { parseV2_0_X } from '~/openApi/2.0.x'; +import { parseV3_0_X } from '~/openApi/3.0.x'; +import { parseV3_1_X } from '~/openApi/3.1.x'; +import type { Client } from '~/openApi/common/interfaces/client'; +import type { OpenApi as LegacyOpenApi } from '~/openApi/common/interfaces/OpenApi'; +import type { OpenApi } from '~/openApi/types'; +import { parse as parseV2 } from '~/openApi/v2'; +import { parse as parseV3 } from '~/openApi/v3'; +import type { Config } from '~/types/config'; +import type { Logger } from '~/utils/logger'; export type { Client, diff --git a/packages/openapi-ts/src/openApi/shared/graph/meta.ts b/packages/openapi-ts/src/openApi/shared/graph/meta.ts index 4d96e0940..3d7103db6 100644 --- a/packages/openapi-ts/src/openApi/shared/graph/meta.ts +++ b/packages/openapi-ts/src/openApi/shared/graph/meta.ts @@ -1,6 +1,7 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { Logger } from '../../../utils/logger'; -import { jsonPointerToPath } from '../../../utils/ref'; +import { createOperationKey } from '~/ir/operation'; +import type { Logger } from '~/utils/logger'; +import { jsonPointerToPath } from '~/utils/ref'; + import { addNamespace, stringToNamespace } from '../utils/filter'; import type { Graph } from '../utils/graph'; import { httpMethods } from '../utils/operation'; @@ -67,10 +68,10 @@ export const buildResourceMetadata = ( const getDependencies = (pointer: string): Set => { const dependencies = new Set(); - const nodeDeps = graph.allDependencies.get(pointer); - if (nodeDeps?.size) { - for (const dep of nodeDeps) { - const path = jsonPointerToPath(dep); + const nodeDependencies = graph.transitiveDependencies.get(pointer); + if (nodeDependencies?.size) { + for (const dependency of nodeDependencies) { + const path = jsonPointerToPath(dependency); const type = path[path.length - 2]; const name = path[path.length - 1]; if (type && name) { diff --git a/packages/openapi-ts/src/openApi/shared/transforms/enums.ts b/packages/openapi-ts/src/openApi/shared/transforms/enums.ts index 1552cef51..c8c4f0d86 100644 --- a/packages/openapi-ts/src/openApi/shared/transforms/enums.ts +++ b/packages/openapi-ts/src/openApi/shared/transforms/enums.ts @@ -1,5 +1,6 @@ +import { jsonPointerToPath } from '~/utils/ref'; + import type { Config } from '../../../types/config'; -import { jsonPointerToPath } from '../../../utils/ref'; import { buildName } from '../utils/name'; import { deepClone } from '../utils/schema'; import { childSchemaRelationships } from '../utils/schemaChildRelationships'; diff --git a/packages/openapi-ts/src/openApi/shared/transforms/index.ts b/packages/openapi-ts/src/openApi/shared/transforms/index.ts index 321863c0a..6f34465a8 100644 --- a/packages/openapi-ts/src/openApi/shared/transforms/index.ts +++ b/packages/openapi-ts/src/openApi/shared/transforms/index.ts @@ -1,4 +1,5 @@ -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; + import { enumsTransform } from './enums'; import { propertiesRequiredByDefaultTransform } from './propertiesRequiredByDefault'; import { readWriteTransform } from './readWrite'; diff --git a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts index a8ad2567e..cc6696796 100644 --- a/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts +++ b/packages/openapi-ts/src/openApi/shared/transforms/readWrite.ts @@ -1,6 +1,7 @@ +import type { Logger } from '~/utils/logger'; +import { jsonPointerToPath } from '~/utils/ref'; + import type { Config } from '../../../types/config'; -import type { Logger } from '../../../utils/logger'; -import { jsonPointerToPath } from '../../../utils/ref'; import deepEqual from '../utils/deepEqual'; import { buildGraph, type Graph, type Scope } from '../utils/graph'; import { buildName } from '../utils/name'; diff --git a/packages/openapi-ts/src/openApi/shared/types/schema.d.ts b/packages/openapi-ts/src/openApi/shared/types/schema.d.ts index 5b5bb9ecd..f52469bb9 100644 --- a/packages/openapi-ts/src/openApi/shared/types/schema.d.ts +++ b/packages/openapi-ts/src/openApi/shared/types/schema.d.ts @@ -4,17 +4,9 @@ export interface SchemaState { * from the OpenAPI specification. */ $ref?: string; - /** - * If a reference is detected as circular, this will hold the $ref string. - * This is used to back-propagate circular reference information to the - * original schema that started the circular reference chain. - */ - circularRef?: string; /** * Set of $refs currently being resolved that are circular. This is used to * avoid infinite loops when resolving schemas with circular references. - * - * @deprecated Use `refStack` instead. */ circularReferenceTracker: Set; /** @@ -24,11 +16,6 @@ export interface SchemaState { * properties from other schemas in the composition. */ inAllOf?: boolean; - /** - * Stack of $refs currently being resolved. This is used to detect circular - * references and avoid infinite loops. - */ - refStack: Array; } export type SchemaWithRequired< diff --git a/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts b/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts new file mode 100644 index 000000000..e9d1d9d30 --- /dev/null +++ b/packages/openapi-ts/src/openApi/shared/utils/__tests__/graph.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; + +import { buildGraph } from '../graph'; + +// simple logger stub for buildGraph +const loggerStub = { + timeEvent: () => ({ timeEnd: () => {} }), +} as any; + +describe('buildGraph', () => { + it('computes referenced and transitive dependencies for validators-circular-ref.json', async () => { + const mod = await import( + '../../../../../../../specs/3.1.x/validators-circular-ref.json' + ); + const spec = (mod as any).default ?? mod; + + const { graph } = buildGraph(spec, loggerStub); + + const foo = '#/components/schemas/Foo'; + const bar = '#/components/schemas/Bar'; + const baz = '#/components/schemas/Baz'; + const qux = '#/components/schemas/Qux'; + + // Foo has a child property that $ref's Bar, so Foo should have Bar in subtreeDependencies + expect(graph.subtreeDependencies.get(foo)).toBeDefined(); + expect(Array.from(graph.subtreeDependencies.get(foo)!).sort()).toEqual( + [bar].sort(), + ); + + // Foo transitively depends on Bar (via the child), so transitiveDependencies should include Bar + expect(graph.transitiveDependencies.get(foo)).toBeDefined(); + expect(Array.from(graph.transitiveDependencies.get(foo)!).sort()).toEqual( + [bar].sort(), + ); + + // Bar references itself via an array item; Bar should reference Bar + expect(Array.from(graph.subtreeDependencies.get(bar)!).sort()).toEqual( + [bar].sort(), + ); + + // Baz and Qux form a mutual $ref cycle; each should reference the other in subtreeDependencies + expect(Array.from(graph.subtreeDependencies.get(baz)!).sort()).toEqual( + [qux].sort(), + ); + expect(Array.from(graph.subtreeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Qux node should exist and have a direct dependency to Baz (node-level $ref) + expect(graph.nodes.has(qux)).toBe(true); + expect(graph.nodeDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.nodeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Qux transitive deps should include Baz (and vice-versa because of the cycle) + expect(graph.transitiveDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.transitiveDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + + // Reverse dependencies should reflect the mutual references + expect(graph.reverseNodeDependencies.get(qux)).toBeDefined(); + expect(Array.from(graph.reverseNodeDependencies.get(qux)!).sort()).toEqual( + [baz].sort(), + ); + expect(Array.from(graph.reverseNodeDependencies.get(baz)!).sort()).toEqual( + [qux].sort(), + ); + }); + + it('handles a small hand-constructed tree with child-level $ref', () => { + const spec = { + components: { + schemas: { + A: { + properties: { p: { $ref: '#/components/schemas/B' } }, + type: 'object', + }, + B: { type: 'object' }, + }, + }, + }; + + const { graph } = buildGraph(spec, loggerStub); + + const a = '#/components/schemas/A'; + const b = '#/components/schemas/B'; + + expect(Array.from(graph.subtreeDependencies.get(a)!).sort()).toEqual( + [b].sort(), + ); + expect(Array.from(graph.transitiveDependencies.get(a)!).sort()).toEqual( + [b].sort(), + ); + // reverseNodeDependencies should record that b is referenced by the property child as well + expect(graph.reverseNodeDependencies.get(b)).toBeDefined(); + expect( + Array.from(graph.reverseNodeDependencies.get(b)!).some((p) => + p.startsWith(a), + ), + ).toBe(true); + }); +}); diff --git a/packages/openapi-ts/src/openApi/shared/utils/__tests__/patch.test.ts b/packages/openapi-ts/src/openApi/shared/utils/__tests__/patch.test.ts index ec9aaa718..8fbefc62e 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/__tests__/patch.test.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/__tests__/patch.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, vi } from 'vitest'; -import type { OpenApi } from '../../../../openApi/types'; +import type { OpenApi } from '~/openApi/types'; + import { patchOpenApiSpec } from '../patch'; const specMetadataV2: Pick = { diff --git a/packages/openapi-ts/src/openApi/shared/utils/discriminator.ts b/packages/openapi-ts/src/openApi/shared/utils/discriminator.ts index 836af5986..1e79009d3 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/discriminator.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/discriminator.ts @@ -1,4 +1,4 @@ -import { refToName } from '../../../utils/ref'; +import { refToName } from '~/utils/ref'; export const discriminatorValues = ( $ref: string, diff --git a/packages/openapi-ts/src/openApi/shared/utils/filter.ts b/packages/openapi-ts/src/openApi/shared/utils/filter.ts index 9ffa90630..8c8f66d51 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/filter.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/filter.ts @@ -1,8 +1,9 @@ -import { createOperationKey } from '../../../ir/operation'; +import { createOperationKey } from '~/ir/operation'; +import type { PathItemObject, PathsObject } from '~/openApi/3.1.x/types/spec'; +import type { OpenApi } from '~/openApi/types'; +import type { Logger } from '~/utils/logger'; + import type { Config } from '../../../types/config'; -import type { Logger } from '../../../utils/logger'; -import type { PathItemObject, PathsObject } from '../../3.1.x/types/spec'; -import type { OpenApi } from '../../types'; import type { ResourceMetadata } from '../graph/meta'; import { httpMethods } from './operation'; diff --git a/packages/openapi-ts/src/openApi/shared/utils/graph.ts b/packages/openapi-ts/src/openApi/shared/utils/graph.ts index d00f01952..605cd58b0 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/graph.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/graph.ts @@ -1,5 +1,6 @@ -import type { Logger } from '../../../utils/logger'; -import { normalizeJsonPointer, pathToJsonPointer } from '../../../utils/ref'; +import type { Logger } from '~/utils/logger'; +import { normalizeJsonPointer, pathToJsonPointer } from '~/utils/ref'; + import { childSchemaRelationships } from './schemaChildRelationships'; /** @@ -20,7 +21,7 @@ export type Scope = 'normal' | 'read' | 'write'; * @property scopes - The set of access scopes for this node, if any. Optional. * @property tags - The set of tags for this node, if any. Optional. */ -type NodeInfo = { +export type NodeInfo = { /** Whether the node is deprecated. Optional. */ deprecated?: boolean; /** The property name or array index in the parent, or null for root. */ @@ -38,22 +39,37 @@ type NodeInfo = { /** * The main graph structure for OpenAPI node analysis. * - * @property dependencies - For each node, the set of normalized JSON Pointers it references via $ref. + * @property nodeDependencies - For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. Nodes with no dependencies are omitted. * @property nodes - Map from normalized JSON Pointer to NodeInfo for every node in the spec. - * @property reverseDependencies - For each node, the set of nodes that reference it via $ref. + * @property reverseNodeDependencies - For each node with at least one dependent, the set of nodes that reference it via $ref. Nodes with no dependents are omitted. */ export type Graph = { + /** + * For each node with at least one dependency, the set of normalized JSON Pointers it references via $ref. + * Nodes with no dependencies are omitted from this map. + */ + nodeDependencies: Map>; + /** + * Map from normalized JSON Pointer to NodeInfo for every node in the spec. + */ + nodes: Map; + /** + * For each node with at least one dependent, the set of nodes that reference it via $ref. + * Nodes with no dependents are omitted from this map. + */ + reverseNodeDependencies: Map>; + /** + * For each node, the set of direct $ref targets that appear anywhere inside the node's + * subtree (the node itself and its children). This is populated during graph construction + * and is used to compute top-level dependency relationships where $ref may be attached to + * child pointers instead of the parent. + */ + subtreeDependencies: Map>; /** * For each node, the set of all (transitive) normalized JSON Pointers it references via $ref anywhere in its subtree. * This includes both direct and indirect dependencies, making it useful for filtering, codegen, and tree-shaking. */ - allDependencies: Map>; - /** For each node, the set of normalized JSON Pointers it references via $ref. */ - dependencies: Map>; - /** Map from normalized JSON Pointer to NodeInfo for every node in the spec. */ - nodes: Map; - /** For each node, the set of nodes that reference it via $ref. */ - reverseDependencies: Map>; + transitiveDependencies: Map>; }; /** @@ -76,15 +92,20 @@ export const annotateChildScopes = (nodes: Graph['nodes']): void => { }; interface Cache { - allDependencies: Map>; - childDependencies: Map>; parentToChildren: Map>; + subtreeDependencies: Map>; + transitiveDependencies: Map>; } +type PointerDependenciesResult = { + subtreeDependencies: Set; + transitiveDependencies: Set; +}; + /** * Recursively collects all $ref dependencies in the subtree rooted at `pointer`. */ -const collectAllDependenciesForPointer = ({ +const collectPointerDependencies = ({ cache, graph, pointer, @@ -94,67 +115,94 @@ const collectAllDependenciesForPointer = ({ graph: Graph; pointer: string; visited: Set; -}): Set => { - const cached = cache.allDependencies.get(pointer); +}): PointerDependenciesResult => { + const cached = cache.transitiveDependencies.get(pointer); if (cached) { - return cached; + return { + subtreeDependencies: cache.subtreeDependencies.get(pointer)!, + transitiveDependencies: cached, + }; } if (visited.has(pointer)) { - return new Set(); + return { + subtreeDependencies: new Set(), + transitiveDependencies: new Set(), + }; } - visited.add(pointer); const nodeInfo = graph.nodes.get(pointer); if (!nodeInfo) { - return new Set(); + return { + subtreeDependencies: new Set(), + transitiveDependencies: new Set(), + }; } - const allDependencies = new Set(); + const transitiveDependencies = new Set(); + const subtreeDependencies = new Set(); // Add direct $ref dependencies for this node // (from the dependencies map, or by checking nodeInfo.node directly) // We'll use the dependencies map for consistency: - const dependencies = graph.dependencies.get(pointer); - if (dependencies) { - for (const depPointer of dependencies) { - allDependencies.add(depPointer); + const nodeDependencies = graph.nodeDependencies.get(pointer); + if (nodeDependencies) { + for (const depPointer of nodeDependencies) { + transitiveDependencies.add(depPointer); + subtreeDependencies.add(depPointer); // Recursively collect dependencies of the referenced node - const transitiveDependencies = collectAllDependenciesForPointer({ + const depResult = collectPointerDependencies({ cache, graph, pointer: depPointer, visited, }); - for (const dep of transitiveDependencies) { - allDependencies.add(dep); + for (const dependency of depResult.transitiveDependencies) { + transitiveDependencies.add(dependency); } } } - // Recursively collect dependencies of all children - const children = cache.parentToChildren.get(pointer); - if (children) { - for (const childPointer of children) { - let transitiveDependencies = cache.childDependencies.get(childPointer); - if (!transitiveDependencies) { - transitiveDependencies = collectAllDependenciesForPointer({ - cache, - graph, - pointer: childPointer, - visited, - }); - cache.childDependencies.set(childPointer, transitiveDependencies); - } - for (const dep of transitiveDependencies) { - allDependencies.add(dep); - } + const children = cache.parentToChildren.get(pointer) ?? []; + for (const childPointer of children) { + let childResult: Partial = { + subtreeDependencies: cache.subtreeDependencies.get(childPointer), + transitiveDependencies: cache.transitiveDependencies.get(childPointer), + }; + if ( + !childResult.subtreeDependencies || + !childResult.transitiveDependencies + ) { + childResult = collectPointerDependencies({ + cache, + graph, + pointer: childPointer, + visited, + }); + cache.transitiveDependencies.set( + childPointer, + childResult.transitiveDependencies!, + ); + cache.subtreeDependencies.set( + childPointer, + childResult.subtreeDependencies!, + ); + } + for (const dependency of childResult.transitiveDependencies!) { + transitiveDependencies.add(dependency); + } + for (const dependency of childResult.subtreeDependencies!) { + subtreeDependencies.add(dependency); } } - cache.allDependencies.set(pointer, allDependencies); - return allDependencies; + cache.transitiveDependencies.set(pointer, transitiveDependencies); + cache.subtreeDependencies.set(pointer, subtreeDependencies); + return { + subtreeDependencies, + transitiveDependencies, + }; }; /** @@ -167,7 +215,7 @@ const collectAllDependenciesForPointer = ({ * - All nodes that reference it via $ref (reverse dependencies) * - Combinator parents (allOf/anyOf/oneOf) if applicable * - * @param graph - The Graph structure containing nodes, dependencies, and reverseDependencies. + * @param graph - The Graph structure containing nodes, dependencies, and reverseNodeDependencies. */ export const propagateScopes = (graph: Graph): void => { const worklist: Set = new Set( @@ -195,9 +243,9 @@ export const propagateScopes = (graph: Graph): void => { if (nodeInfo.parentPointer) { worklist.add(nodeInfo.parentPointer); } - const reverseDependencies = graph.reverseDependencies.get(pointer); - if (reverseDependencies) { - for (const dependentPointer of reverseDependencies) { + const reverseNodeDependencies = graph.reverseNodeDependencies.get(pointer); + if (reverseNodeDependencies) { + for (const dependentPointer of reverseNodeDependencies) { worklist.add(dependentPointer); } } @@ -290,9 +338,9 @@ export const propagateScopes = (graph: Graph): void => { } // Propagate scopes from $ref dependencies - const dependencies = graph.dependencies.get(pointer); - if (dependencies) { - for (const depPointer of dependencies) { + const nodeDependencies = graph.nodeDependencies.get(pointer); + if (nodeDependencies) { + for (const depPointer of nodeDependencies) { const depNode = graph.nodes.get(depPointer); if (depNode?.scopes) { const changed = propagateScopesToNode(depNode, nodeInfo); @@ -384,14 +432,14 @@ export const seedLocalScopes = (nodes: Graph['nodes']): void => { * - All keys in the returned maps are normalized JSON Pointers (RFC 6901, always starting with '#'). * - The `nodes` map allows fast lookup of any node and its parent/key context. * - The `dependencies` map records, for each node, the set of normalized pointers it references via $ref. - * - The `reverseDependencies` map records, for each node, the set of nodes that reference it via $ref. + * - The `reverseNodeDependencies` map records, for each node, the set of nodes that reference it via $ref. * - After construction, all nodes will have their local and propagated scopes annotated. * * @param root The root object (e.g., the OpenAPI spec) * @returns An object with: * - nodes: Map from normalized JSON Pointer string to NodeInfo * - dependencies: Map from normalized JSON Pointer string to Set of referenced normalized JSON Pointers - * - reverseDependencies: Map from normalized JSON Pointer string to Set of referencing normalized JSON Pointers + * - reverseNodeDependencies: Map from normalized JSON Pointer string to Set of referencing normalized JSON Pointers */ export const buildGraph = ( root: unknown, @@ -401,10 +449,11 @@ export const buildGraph = ( } => { const eventBuildGraph = logger.timeEvent('build-graph'); const graph: Graph = { - allDependencies: new Map(), - dependencies: new Map(), + nodeDependencies: new Map(), nodes: new Map(), - reverseDependencies: new Map(), + reverseNodeDependencies: new Map(), + subtreeDependencies: new Map(), + transitiveDependencies: new Map(), }; const walk = ({ @@ -432,10 +481,10 @@ export const buildGraph = ( // If this node has a $ref, record the dependency if ('$ref' in node && typeof node.$ref === 'string') { const refPointer = normalizeJsonPointer(node.$ref); - if (!graph.dependencies.has(pointer)) { - graph.dependencies.set(pointer, new Set()); + if (!graph.nodeDependencies.has(pointer)) { + graph.nodeDependencies.set(pointer, new Set()); } - graph.dependencies.get(pointer)!.add(refPointer); + graph.nodeDependencies.get(pointer)!.add(refPointer); } // Check for tags property (should be an array of strings) if ('tags' in node && node.tags instanceof Array) { @@ -474,9 +523,9 @@ export const buildGraph = ( }); const cache: Cache = { - allDependencies: new Map(), - childDependencies: new Map(), parentToChildren: new Map(), + subtreeDependencies: new Map(), + transitiveDependencies: new Map(), }; for (const [pointer, nodeInfo] of graph.nodes) { @@ -488,12 +537,12 @@ export const buildGraph = ( cache.parentToChildren.get(parent)!.push(pointer); } - for (const [pointerFrom, pointers] of graph.dependencies) { + for (const [pointerFrom, pointers] of graph.nodeDependencies) { for (const pointerTo of pointers) { - if (!graph.reverseDependencies.has(pointerTo)) { - graph.reverseDependencies.set(pointerTo, new Set()); + if (!graph.reverseNodeDependencies.has(pointerTo)) { + graph.reverseNodeDependencies.set(pointerTo, new Set()); } - graph.reverseDependencies.get(pointerTo)!.add(pointerFrom); + graph.reverseNodeDependencies.get(pointerTo)!.add(pointerFrom); } } @@ -502,13 +551,14 @@ export const buildGraph = ( annotateChildScopes(graph.nodes); for (const pointer of graph.nodes.keys()) { - const allDependencies = collectAllDependenciesForPointer({ + const result = collectPointerDependencies({ cache, graph, pointer, visited: new Set(), }); - graph.allDependencies.set(pointer, allDependencies); + graph.transitiveDependencies.set(pointer, result.transitiveDependencies); + graph.subtreeDependencies.set(pointer, result.subtreeDependencies); } eventBuildGraph.timeEnd(); diff --git a/packages/openapi-ts/src/openApi/shared/utils/identifier.ts b/packages/openapi-ts/src/openApi/shared/utils/identifier.ts index 8f8c7e2df..3b9ce2464 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/identifier.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/identifier.ts @@ -5,7 +5,7 @@ import { reservedJavaScriptKeywordsRegExp, reservedNodeGlobalsRegExp, reservedTypeScriptKeywordsRegExp, -} from '../../../utils/regexp'; +} from '~/utils/regexp'; const regexps = [ reservedJavaScriptKeywordsRegExp, diff --git a/packages/openapi-ts/src/openApi/shared/utils/name.ts b/packages/openapi-ts/src/openApi/shared/utils/name.ts index 8f4ab453d..fff76b867 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/name.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/name.ts @@ -1,5 +1,6 @@ +import { stringCase } from '~/utils/stringCase'; + import type { StringCase, StringName } from '../../../types/case'; -import { stringCase } from '../../../utils/stringCase'; export const buildName = ({ config, diff --git a/packages/openapi-ts/src/openApi/shared/utils/operation.ts b/packages/openapi-ts/src/openApi/shared/utils/operation.ts index cc7e1cc1b..119159459 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/operation.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/operation.ts @@ -1,7 +1,8 @@ -import { createOperationKey } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { stringCase } from '../../../utils/stringCase'; -import { sanitizeNamespaceIdentifier } from '../../common/parser/sanitize'; +import { createOperationKey } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { sanitizeNamespaceIdentifier } from '~/openApi/common/parser/sanitize'; +import { stringCase } from '~/utils/stringCase'; + import type { State } from '../types/state'; export const httpMethods = [ diff --git a/packages/openapi-ts/src/openApi/shared/utils/parameter.ts b/packages/openapi-ts/src/openApi/shared/utils/parameter.ts index a7136ed15..ff507fe2d 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/parameter.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/parameter.ts @@ -1,4 +1,4 @@ -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; export const mergeParametersObjects = ({ source, diff --git a/packages/openapi-ts/src/openApi/shared/utils/patch.ts b/packages/openapi-ts/src/openApi/shared/utils/patch.ts index 4dd0e9566..9186ee709 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/patch.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/patch.ts @@ -1,5 +1,6 @@ +import type { OpenApi } from '~/openApi/types'; + import type { Patch } from '../../../types/parser'; -import type { OpenApi } from '../../types'; export const patchOpenApiSpec = ({ patchOptions, diff --git a/packages/openapi-ts/src/openApi/shared/utils/validator.ts b/packages/openapi-ts/src/openApi/shared/utils/validator.ts index c6be15024..db79c7740 100644 --- a/packages/openapi-ts/src/openApi/shared/utils/validator.ts +++ b/packages/openapi-ts/src/openApi/shared/utils/validator.ts @@ -1,6 +1,6 @@ import colors from 'ansi-colors'; -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; export interface ValidatorIssue { /** diff --git a/packages/openapi-ts/src/openApi/types.d.ts b/packages/openapi-ts/src/openApi/types.d.ts index 88697a24e..ff0b32f5e 100644 --- a/packages/openapi-ts/src/openApi/types.d.ts +++ b/packages/openapi-ts/src/openApi/types.d.ts @@ -1,6 +1,6 @@ -import type { OpenApiV2_0_X, OpenApiV2_0_XTypes } from './2.0.x'; -import type { OpenApiV3_0_X, OpenApiV3_0_XTypes } from './3.0.x'; -import type { OpenApiV3_1_X, OpenApiV3_1_XTypes } from './3.1.x'; +import type { OpenApiV2_0_X, OpenApiV2_0_XTypes } from '~/openApi/2.0.x'; +import type { OpenApiV3_0_X, OpenApiV3_0_XTypes } from '~/openApi/3.0.x'; +import type { OpenApiV3_1_X, OpenApiV3_1_XTypes } from '~/openApi/3.1.x'; export namespace OpenApi { export type V2_0_X = OpenApiV2_0_X; diff --git a/packages/openapi-ts/src/openApi/v2/index.ts b/packages/openapi-ts/src/openApi/v2/index.ts index 9d35fad1b..eca7c2948 100644 --- a/packages/openapi-ts/src/openApi/v2/index.ts +++ b/packages/openapi-ts/src/openApi/v2/index.ts @@ -1,5 +1,6 @@ -import type { Client } from '../common/interfaces/client'; -import { getServiceVersion } from '../common/parser/service'; +import type { Client } from '~/openApi/common/interfaces/client'; +import { getServiceVersion } from '~/openApi/common/parser/service'; + import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; import { getOperations } from './parser/getOperations'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/Model.ts b/packages/openapi-ts/src/openApi/v2/interfaces/Model.ts index 821f21171..c442b81a6 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/Model.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/Model.ts @@ -1,5 +1,6 @@ +import type { Model, ModelMeta } from '~/openApi/common/interfaces/client'; + import type { Client } from '../../../types/client'; -import type { Model, ModelMeta } from '../../common/interfaces/client'; import type { OpenApi } from './OpenApi'; import type { OpenApiSchema } from './OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApi.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApi.ts index 477938662..5d641a917 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApi.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApi.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiExternalDocs } from './OpenApiExternalDocs'; import type { OpenApiInfo } from './OpenApiInfo'; import type { OpenApiParameter } from './OpenApiParameter'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiHeader.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiHeader.ts index a00de7928..cc595e0df 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiHeader.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiHeader.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiItems } from './OpenApiItems'; /** diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiItems.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiItems.ts index f7d0cb633..6e0025752 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiItems.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiItems.ts @@ -1,4 +1,4 @@ -import type { WithEnumExtension } from '../../common/interfaces/WithEnumExtension'; +import type { WithEnumExtension } from '~/openApi/common/interfaces/WithEnumExtension'; /** * {@link} https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#items-object) diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiParameter.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiParameter.ts index 1210fd0e2..223151007 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiParameter.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiParameter.ts @@ -1,4 +1,5 @@ -import type { WithEnumExtension } from '../../common/interfaces/WithEnumExtension'; +import type { WithEnumExtension } from '~/openApi/common/interfaces/WithEnumExtension'; + import type { WithNullableExtension } from './Extensions/WithNullableExtension'; import type { OpenApiItems } from './OpenApiItems'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiResponse.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiResponse.ts index 0883cff68..26c372d80 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiResponse.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiResponse.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiExample } from './OpenApiExample'; import type { OpenApiHeader } from './OpenApiHeader'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSchema.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSchema.ts index 447d4f7fd..65ff39f37 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSchema.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSchema.ts @@ -1,5 +1,6 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; -import type { WithEnumExtension } from '../../common/interfaces/WithEnumExtension'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; +import type { WithEnumExtension } from '~/openApi/common/interfaces/WithEnumExtension'; + import type { WithNullableExtension } from './Extensions/WithNullableExtension'; import type { OpenApiExternalDocs } from './OpenApiExternalDocs'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSecurityScheme.ts b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSecurityScheme.ts index 341f5af56..2a096b0f6 100644 --- a/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSecurityScheme.ts +++ b/packages/openapi-ts/src/openApi/v2/interfaces/OpenApiSecurityScheme.ts @@ -1,4 +1,4 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; /** * {@link} https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md#security-scheme-object diff --git a/packages/openapi-ts/src/openApi/v2/parser/getModel.ts b/packages/openapi-ts/src/openApi/v2/parser/getModel.ts index 58c4c32fd..449810818 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getModel.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getModel.ts @@ -1,8 +1,9 @@ +import type { Model, ModelMeta } from '~/openApi/common/interfaces/client'; +import { getEnums } from '~/openApi/common/parser/getEnums'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { Model, ModelMeta } from '../../common/interfaces/client'; -import { getEnums } from '../../common/parser/getEnums'; -import { getPattern } from '../../common/parser/getPattern'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { getModelComposition } from './getModelComposition'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getModelComposition.ts b/packages/openapi-ts/src/openApi/v2/parser/getModelComposition.ts index c130c8766..d131b1e79 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getModelComposition.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getModelComposition.ts @@ -1,5 +1,9 @@ +import type { + Model, + ModelComposition, +} from '~/openApi/common/interfaces/client'; + import type { Client } from '../../../types/client'; -import type { Model, ModelComposition } from '../../common/interfaces/client'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getModelProperties.ts b/packages/openapi-ts/src/openApi/v2/parser/getModelProperties.ts index 25f576710..d3a51c7f5 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getModelProperties.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getModelProperties.ts @@ -1,8 +1,9 @@ +import type { Model } from '~/openApi/common/interfaces/client'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getType } from '~/openApi/common/parser/type'; +import { escapeName } from '~/utils/escape'; + import type { Client } from '../../../types/client'; -import { escapeName } from '../../../utils/escape'; -import type { Model } from '../../common/interfaces/client'; -import { getPattern } from '../../common/parser/getPattern'; -import { getType } from '../../common/parser/type'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getModels.ts b/packages/openapi-ts/src/openApi/v2/parser/getModels.ts index 5caf1943c..7a87247aa 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getModels.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getModels.ts @@ -1,6 +1,7 @@ +import { getType } from '~/openApi/common/parser/type'; +import { reservedJavaScriptKeywordsRegExp } from '~/utils/regexp'; + import type { Client } from '../../../types/client'; -import { reservedJavaScriptKeywordsRegExp } from '../../../utils/regexp'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import { getModel } from './getModel'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts index 4a8427e5b..1016e3ea3 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameter.ts @@ -1,11 +1,12 @@ +import type { OperationParameter } from '~/openApi/common/interfaces/client'; +import { getDefault } from '~/openApi/common/parser/getDefault'; +import { getEnums } from '~/openApi/common/parser/getEnums'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { operationParameterNameFn } from '~/openApi/common/parser/operation'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { OperationParameter } from '../../common/interfaces/client'; -import { getDefault } from '../../common/parser/getDefault'; -import { getEnums } from '../../common/parser/getEnums'; -import { getPattern } from '../../common/parser/getPattern'; -import { getRef } from '../../common/parser/getRef'; -import { operationParameterNameFn } from '../../common/parser/operation'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts index 8611008d2..392832370 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationParameters.ts @@ -1,7 +1,8 @@ +import type { OperationParameters } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { operationParameterFilterFn } from '~/openApi/common/parser/operation'; + import type { Client } from '../../../types/client'; -import type { OperationParameters } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; -import { operationParameterFilterFn } from '../../common/parser/operation'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationResponse.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationResponse.ts index bcc00d6a2..9068c93b3 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationResponse.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationResponse.ts @@ -1,8 +1,9 @@ +import type { OperationResponse } from '~/openApi/common/interfaces/client'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { OperationResponse } from '../../common/interfaces/client'; -import { getPattern } from '../../common/parser/getPattern'; -import { getRef } from '../../common/parser/getRef'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperationResponses.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperationResponses.ts index 19dc6a7d1..3ebf13843 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperationResponses.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperationResponses.ts @@ -1,11 +1,12 @@ -import type { Client } from '../../../types/client'; -import type { OperationResponse } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; +import type { OperationResponse } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; import { parseResponseStatusCode, sorterByResponseStatusCode, tagResponseTypes, -} from '../../common/parser/operation'; +} from '~/openApi/common/parser/operation'; + +import type { Client } from '../../../types/client'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiResponses } from '../interfaces/OpenApiResponses'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts b/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts index 332bd3d1f..fe297f310 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getOperations.ts @@ -1,8 +1,9 @@ -import { createOperationKey } from '../../../ir/operation'; -import { getConfig } from '../../../utils/config'; -import type { Client, Operation } from '../../common/interfaces/client'; -import { operationFilterFn } from '../../common/parser/operation'; -import { allowedServiceMethods } from '../../common/parser/service'; +import { createOperationKey } from '~/ir/operation'; +import type { Client, Operation } from '~/openApi/common/interfaces/client'; +import { operationFilterFn } from '~/openApi/common/parser/operation'; +import { allowedServiceMethods } from '~/openApi/common/parser/service'; +import { getConfig } from '~/utils/config'; + import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts b/packages/openapi-ts/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts index bec99562c..b67368619 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/getRequiredPropertiesFromComposition.ts @@ -1,6 +1,7 @@ +import type { Model } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; + import type { Client } from '../../../types/client'; -import type { Model } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v2/parser/operation.ts b/packages/openapi-ts/src/openApi/v2/parser/operation.ts index 6897b2cd5..9a34b1f27 100644 --- a/packages/openapi-ts/src/openApi/v2/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v2/parser/operation.ts @@ -1,14 +1,15 @@ -import type { Client } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; import type { Operation, OperationParameters, -} from '../../common/interfaces/client'; +} from '~/openApi/common/interfaces/client'; import { getOperationResponseHeader, operationNameFn, -} from '../../common/parser/operation'; -import { toSortedByRequired } from '../../common/parser/sort'; +} from '~/openApi/common/parser/operation'; +import { toSortedByRequired } from '~/openApi/common/parser/sort'; +import { getConfig } from '~/utils/config'; + +import type { Client } from '../../../types/client'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import { getOperationParameters } from './getOperationParameters'; diff --git a/packages/openapi-ts/src/openApi/v3/index.ts b/packages/openapi-ts/src/openApi/v3/index.ts index 9d35fad1b..eca7c2948 100644 --- a/packages/openapi-ts/src/openApi/v3/index.ts +++ b/packages/openapi-ts/src/openApi/v3/index.ts @@ -1,5 +1,6 @@ -import type { Client } from '../common/interfaces/client'; -import { getServiceVersion } from '../common/parser/service'; +import type { Client } from '~/openApi/common/interfaces/client'; +import { getServiceVersion } from '~/openApi/common/parser/service'; + import type { OpenApi } from './interfaces/OpenApi'; import { getModels } from './parser/getModels'; import { getOperations } from './parser/getOperations'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/Model.ts b/packages/openapi-ts/src/openApi/v3/interfaces/Model.ts index 56e35812c..731d6f25c 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/Model.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/Model.ts @@ -1,5 +1,6 @@ +import type { Model, ModelMeta } from '~/openApi/common/interfaces/client'; + import type { Client } from '../../../types/client'; -import type { Model, ModelMeta } from '../../common/interfaces/client'; import type { OpenApi } from './OpenApi'; import type { OpenApiSchema } from './OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiComponents.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiComponents.ts index c6a1f9130..883acb493 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiComponents.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiComponents.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiCallback } from './OpenApiCallback'; import type { OpenApiExample } from './OpenApiExample'; import type { OpenApiHeader } from './OpenApiHeader'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiDiscriminator.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiDiscriminator.ts index 224d9d203..bb33b72b7 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiDiscriminator.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiDiscriminator.ts @@ -1,4 +1,4 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; /** * {@link} https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#discriminator-object diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiEncoding.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiEncoding.ts index 20e8bfa4a..620eedbee 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiEncoding.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiEncoding.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiHeader } from './OpenApiHeader'; /** diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiHeader.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiHeader.ts index 64044450d..e32ba419d 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiHeader.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiHeader.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiExample } from './OpenApiExample'; import type { OpenApiReference } from './OpenApiReference'; import type { OpenApiSchema } from './OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiLink.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiLink.ts index 7173970d4..6b36c907e 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiLink.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiLink.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiReference } from './OpenApiReference'; import type { OpenApiServer } from './OpenApiServer'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiMediaType.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiMediaType.ts index 3c74d4c62..01d115809 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiMediaType.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiMediaType.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiEncoding } from './OpenApiEncoding'; import type { OpenApiExample } from './OpenApiExample'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOAuthFlow.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOAuthFlow.ts index be01efeab..5ef94cc65 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOAuthFlow.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOAuthFlow.ts @@ -1,4 +1,4 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; /** * {@link} https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#oauth-flow-object diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts index b55c9a1d7..f4bcc9bd9 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiOperation.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiCallback } from './OpenApiCallback'; import type { OpenApiExternalDocs } from './OpenApiExternalDocs'; import type { OpenApiParameter } from './OpenApiParameter'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiParameter.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiParameter.ts index 1520ec492..3bf7ec4ac 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiParameter.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiParameter.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiExample } from './OpenApiExample'; import type { OpenApiReference } from './OpenApiReference'; import type { OpenApiSchema } from './OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiRequestBody.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiRequestBody.ts index 0bbc6397a..a001a7a8a 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiRequestBody.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiRequestBody.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiMediaType } from './OpenApiMediaType'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiResponse.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiResponse.ts index d3914e72b..83fe0c65b 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiResponse.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiResponse.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiHeader } from './OpenApiHeader'; import type { OpenApiLink } from './OpenApiLink'; import type { OpenApiMediaType } from './OpenApiMediaType'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiSchema.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiSchema.ts index ef0b56f87..f8c66cc96 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiSchema.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiSchema.ts @@ -1,5 +1,6 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; -import type { WithEnumExtension } from '../../common/interfaces/WithEnumExtension'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; +import type { WithEnumExtension } from '~/openApi/common/interfaces/WithEnumExtension'; + import type { OpenApiDiscriminator } from './OpenApiDiscriminator'; import type { OpenApiExternalDocs } from './OpenApiExternalDocs'; import type { OpenApiReference } from './OpenApiReference'; diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServer.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServer.ts index 4c1a50c97..d09ccaf6c 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServer.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServer.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApiServerVariable } from './OpenApiServerVariable'; /** diff --git a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServerVariable.ts b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServerVariable.ts index 2673fc174..1477f8fe1 100644 --- a/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServerVariable.ts +++ b/packages/openapi-ts/src/openApi/v3/interfaces/OpenApiServerVariable.ts @@ -1,4 +1,4 @@ -import type { WithEnumExtension } from '../../common/interfaces/WithEnumExtension'; +import type { WithEnumExtension } from '~/openApi/common/interfaces/WithEnumExtension'; /** * {@link} https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#server-variable-object diff --git a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts index dbc8979b3..17a3021d7 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/__tests__/getModel.test.ts @@ -1,9 +1,10 @@ import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; +import { getType } from '~/openApi/common/parser/type'; + import type { Config } from '../../../../types/config'; import { reservedJavaScriptKeywordsRegExp } from '../../../../utils/regexp'; -import { getType } from '../../../common/parser/type'; import { getModel } from '../getModel'; vi.mock('../../../../utils/config', () => { @@ -11,8 +12,8 @@ vi.mock('../../../../utils/config', () => { plugins: { '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/openApi/v3/parser/discriminator.ts b/packages/openapi-ts/src/openApi/v3/parser/discriminator.ts index 4301e4aee..ea3834aec 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/discriminator.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/discriminator.ts @@ -1,6 +1,7 @@ -import type { Model } from '../../common/interfaces/client'; -import type { Dictionary } from '../../common/interfaces/Dictionary'; -import { stripNamespace } from '../../common/parser/stripNamespace'; +import type { Model } from '~/openApi/common/interfaces/client'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; +import { stripNamespace } from '~/openApi/common/parser/stripNamespace'; + import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiDiscriminator } from '../interfaces/OpenApiDiscriminator'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getContent.ts b/packages/openapi-ts/src/openApi/v3/parser/getContent.ts index 6aa21293c..26cbd7f59 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getContent.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getContent.ts @@ -1,4 +1,5 @@ -import type { Dictionary } from '../../common/interfaces/Dictionary'; +import type { Dictionary } from '~/openApi/common/interfaces/Dictionary'; + import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiMediaType } from '../interfaces/OpenApiMediaType'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getModel.ts b/packages/openapi-ts/src/openApi/v3/parser/getModel.ts index 5689222e5..e3dd272d9 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getModel.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getModel.ts @@ -1,10 +1,11 @@ +import type { Model, ModelMeta } from '~/openApi/common/interfaces/client'; +import { getDefault } from '~/openApi/common/parser/getDefault'; +import { getEnums } from '~/openApi/common/parser/getEnums'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getType } from '~/openApi/common/parser/type'; +import { enumMeta } from '~/utils/enum'; + import type { Client } from '../../../types/client'; -import { enumMeta } from '../../../utils/enum'; -import type { Model, ModelMeta } from '../../common/interfaces/client'; -import { getDefault } from '../../common/parser/getDefault'; -import { getEnums } from '../../common/parser/getEnums'; -import { getPattern } from '../../common/parser/getPattern'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; import { diff --git a/packages/openapi-ts/src/openApi/v3/parser/getModelComposition.ts b/packages/openapi-ts/src/openApi/v3/parser/getModelComposition.ts index 583eca71e..ca63a28a9 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getModelComposition.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getModelComposition.ts @@ -1,5 +1,9 @@ +import type { + Model, + ModelComposition, +} from '~/openApi/common/interfaces/client'; + import type { Client } from '../../../types/client'; -import type { Model, ModelComposition } from '../../common/interfaces/client'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getModelProperties.ts b/packages/openapi-ts/src/openApi/v3/parser/getModelProperties.ts index 5d94c34bc..83f69aeb9 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getModelProperties.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getModelProperties.ts @@ -1,10 +1,11 @@ +import type { Model } from '~/openApi/common/interfaces/client'; +import { getDefault } from '~/openApi/common/parser/getDefault'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getType } from '~/openApi/common/parser/type'; +import { escapeName } from '~/utils/escape'; +import { unique } from '~/utils/unique'; + import type { Client } from '../../../types/client'; -import { escapeName } from '../../../utils/escape'; -import { unique } from '../../../utils/unique'; -import type { Model } from '../../common/interfaces/client'; -import { getDefault } from '../../common/parser/getDefault'; -import { getPattern } from '../../common/parser/getPattern'; -import { getType } from '../../common/parser/type'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getModels.ts b/packages/openapi-ts/src/openApi/v3/parser/getModels.ts index ee410fa03..9d19db258 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getModels.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getModels.ts @@ -1,5 +1,6 @@ +import { getParametersMeta, getSchemasMeta } from '~/utils/meta'; + import type { Client } from '../../../types/client'; -import { getParametersMeta, getSchemasMeta } from '../../../utils/meta'; import type { OpenApi } from '../interfaces/OpenApi'; import { getModel } from './getModel'; import { getParameterSchema } from './parameter'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts index ac45a9ffb..6289c6038 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameter.ts @@ -1,12 +1,13 @@ +import type { OperationParameter } from '~/openApi/common/interfaces/client'; +import { getDefault } from '~/openApi/common/parser/getDefault'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { operationParameterNameFn } from '~/openApi/common/parser/operation'; +import { getType } from '~/openApi/common/parser/type'; +import { refParametersPartial } from '~/utils/const'; +import { enumMeta } from '~/utils/enum'; + import type { Client } from '../../../types/client'; -import { refParametersPartial } from '../../../utils/const'; -import { enumMeta } from '../../../utils/enum'; -import type { OperationParameter } from '../../common/interfaces/client'; -import { getDefault } from '../../common/parser/getDefault'; -import { getPattern } from '../../common/parser/getPattern'; -import { getRef } from '../../common/parser/getRef'; -import { operationParameterNameFn } from '../../common/parser/operation'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts index 3748308fd..359106552 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationParameters.ts @@ -1,7 +1,8 @@ +import type { OperationParameters } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { operationParameterFilterFn } from '~/openApi/common/parser/operation'; + import type { Client } from '../../../types/client'; -import type { OperationParameters } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; -import { operationParameterFilterFn } from '../../common/parser/operation'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiParameter } from '../interfaces/OpenApiParameter'; import { getOperationParameter } from './getOperationParameter'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts index 14bea9135..350f28abd 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationRequestBody.ts @@ -1,7 +1,8 @@ +import type { OperationParameter } from '~/openApi/common/interfaces/client'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { OperationParameter } from '../../common/interfaces/client'; -import { getPattern } from '../../common/parser/getPattern'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody'; import { getContent } from './getContent'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationResponse.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationResponse.ts index 73f3638a0..fb022b20f 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationResponse.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationResponse.ts @@ -1,8 +1,9 @@ +import type { OperationResponse } from '~/openApi/common/interfaces/client'; +import { getPattern } from '~/openApi/common/parser/getPattern'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { OperationResponse } from '../../common/interfaces/client'; -import { getPattern } from '../../common/parser/getPattern'; -import { getRef } from '../../common/parser/getRef'; -import { getType } from '../../common/parser/type'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperationResponses.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperationResponses.ts index a935bc84d..2719459ba 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperationResponses.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperationResponses.ts @@ -1,11 +1,12 @@ -import type { Client } from '../../../types/client'; -import type { OperationResponse } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; +import type { OperationResponse } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; import { parseResponseStatusCode, sorterByResponseStatusCode, tagResponseTypes, -} from '../../common/parser/operation'; +} from '~/openApi/common/parser/operation'; + +import type { Client } from '../../../types/client'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiResponse } from '../interfaces/OpenApiResponse'; import type { OpenApiResponses } from '../interfaces/OpenApiResponses'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts b/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts index 332bd3d1f..fe297f310 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getOperations.ts @@ -1,8 +1,9 @@ -import { createOperationKey } from '../../../ir/operation'; -import { getConfig } from '../../../utils/config'; -import type { Client, Operation } from '../../common/interfaces/client'; -import { operationFilterFn } from '../../common/parser/operation'; -import { allowedServiceMethods } from '../../common/parser/service'; +import { createOperationKey } from '~/ir/operation'; +import type { Client, Operation } from '~/openApi/common/interfaces/client'; +import { operationFilterFn } from '~/openApi/common/parser/operation'; +import { allowedServiceMethods } from '~/openApi/common/parser/service'; +import { getConfig } from '~/utils/config'; + import type { OpenApi } from '../interfaces/OpenApi'; import { getOperationParameters } from './getOperationParameters'; import { getOperation } from './operation'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts b/packages/openapi-ts/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts index 187e0f92a..6f99f3288 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/getRequiredPropertiesFromComposition.ts @@ -1,7 +1,8 @@ +import type { Model } from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; +import { getType } from '~/openApi/common/parser/type'; + import type { Client } from '../../../types/client'; -import type { Model } from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; -import { getType } from '../../common/parser/type'; import type { GetModelFn } from '../interfaces/Model'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiSchema } from '../interfaces/OpenApiSchema'; diff --git a/packages/openapi-ts/src/openApi/v3/parser/operation.ts b/packages/openapi-ts/src/openApi/v3/parser/operation.ts index 1669e2f8e..9ac0bc5e9 100644 --- a/packages/openapi-ts/src/openApi/v3/parser/operation.ts +++ b/packages/openapi-ts/src/openApi/v3/parser/operation.ts @@ -1,16 +1,17 @@ -import type { Client } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; import type { Operation, OperationParameter, OperationParameters, -} from '../../common/interfaces/client'; -import { getRef } from '../../common/parser/getRef'; +} from '~/openApi/common/interfaces/client'; +import { getRef } from '~/openApi/common/parser/getRef'; import { getOperationResponseHeader, operationNameFn, -} from '../../common/parser/operation'; -import { toSortedByRequired } from '../../common/parser/sort'; +} from '~/openApi/common/parser/operation'; +import { toSortedByRequired } from '~/openApi/common/parser/sort'; +import { getConfig } from '~/utils/config'; + +import type { Client } from '../../../types/client'; import type { OpenApi } from '../interfaces/OpenApi'; import type { OpenApiOperation } from '../interfaces/OpenApiOperation'; import type { OpenApiRequestBody } from '../interfaces/OpenApiRequestBody'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/api.ts b/packages/openapi-ts/src/plugins/@angular/common/api.ts index 3cac30850..ba721b633 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/api.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'class' @@ -22,13 +22,13 @@ export type IApi = { * - `Injectable`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@angular/common'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@angular/common/config.ts b/packages/openapi-ts/src/plugins/@angular/common/config.ts index 2e4d5be80..8bc8c001b 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/config.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import type { AngularCommonPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts b/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts index 3c60b7f48..0404d2d0e 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/httpRequests.ts @@ -1,13 +1,14 @@ import type { Symbol } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { stringCase } from '../../../utils/stringCase'; -import { getClientPlugin } from '../../@hey-api/client-core/utils'; -import { operationClasses } from '../../@hey-api/sdk/operation'; -import { isOperationOptionsRequired } from '../../shared/utils/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { operationClasses } from '~/plugins/@hey-api/sdk/operation'; +import { isOperationOptionsRequired } from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; +import { stringCase } from '~/utils/stringCase'; + import type { AngularCommonPlugin } from './types'; interface AngularRequestClassEntry { @@ -28,70 +29,76 @@ const generateAngularClassRequests = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const classes = operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }); + const classes = operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }); - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - if (!requestClasses.has(currentClassName)) { - requestClasses.set(currentClassName, { - className: currentClassName, - classes: new Set(), - methods: new Set(), - nodes: [], - root: !index, - }); - } + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + if (!requestClasses.has(currentClassName)) { + requestClasses.set(currentClassName, { + className: currentClassName, + classes: new Set(), + methods: new Set(), + nodes: [], + root: !index, + }); + } - const parentClassName = entry.path[index - 1]; - if (parentClassName && parentClassName !== currentClassName) { - const parentClass = requestClasses.get(parentClassName)!; - parentClass.classes.add(currentClassName); - requestClasses.set(parentClassName, parentClass); - } + const parentClassName = entry.path[index - 1]; + if (parentClassName && parentClassName !== currentClassName) { + const parentClass = requestClasses.get(parentClassName)!; + parentClass.classes.add(currentClassName); + requestClasses.set(parentClassName, parentClass); + } - const isLast = entry.path.length === index + 1; - if (!isLast) { - return; - } + const isLast = entry.path.length === index + 1; + if (!isLast) { + return; + } - const currentClass = requestClasses.get(currentClassName)!; + const currentClass = requestClasses.get(currentClassName)!; - const requestMethodName = - plugin.config.httpRequests.methodNameBuilder(operation); + const requestMethodName = + plugin.config.httpRequests.methodNameBuilder(operation); - if (currentClass.methods.has(requestMethodName)) { - return; - } + if (currentClass.methods.has(requestMethodName)) { + return; + } - const methodNode = generateAngularRequestMethod({ - isRequiredOptions, - methodName: requestMethodName, - operation, - plugin, - }); + const methodNode = generateAngularRequestMethod({ + isRequiredOptions, + methodName: requestMethodName, + operation, + plugin, + }); - if (!currentClass.nodes.length) { - currentClass.nodes.push(methodNode); - } else { - // @ts-expect-error - currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); - } + if (!currentClass.nodes.length) { + currentClass.nodes.push(methodNode); + } else { + // @ts-expect-error + currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); + } - currentClass.methods.add(requestMethodName); - requestClasses.set(currentClassName, currentClass); - }); - } - }); + currentClass.methods.add(requestMethodName); + requestClasses.set(currentClassName, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const generateClass = (currentClass: AngularRequestClassEntry) => { if (generatedClasses.has(currentClass.className)) { @@ -127,7 +134,7 @@ const generateAngularClassRequests = ({ } const symbolInjectable = plugin.referenceSymbol( - plugin.api.getSelector('Injectable'), + plugin.api.selector('Injectable'), ); const symbolClass = plugin.registerSymbol({ exported: true, @@ -138,7 +145,7 @@ const generateAngularClassRequests = ({ }, name: currentClass.className, }), - selector: plugin.api.getSelector('class', currentClass.className), + selector: plugin.api.selector('class', currentClass.className), }); const node = tsc.classDeclaration({ decorator: currentClass.root @@ -166,25 +173,31 @@ const generateAngularFunctionRequests = ({ }: { plugin: AngularCommonPlugin['Instance']; }) => { - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const symbol = plugin.registerSymbol({ - exported: true, - name: plugin.config.httpRequests.methodNameBuilder(operation), - selector: plugin.api.getSelector('httpRequest', operation.id), - }); - const node = generateAngularRequestFunction({ - isRequiredOptions, - operation, - plugin, - symbol, - }); - plugin.setSymbolValue(symbol, node); - }); + const symbol = plugin.registerSymbol({ + exported: true, + name: plugin.config.httpRequests.methodNameBuilder(operation), + selector: plugin.api.selector('httpRequest', operation.id), + }); + const node = generateAngularRequestFunction({ + isRequiredOptions, + operation, + plugin, + symbol, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; const generateRequestCallExpression = ({ @@ -196,10 +209,10 @@ const generateRequestCallExpression = ({ }) => { const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -265,16 +278,16 @@ const generateAngularRequestMethod = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('HttpRequest'), + plugin.api.selector('HttpRequest'), ); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; @@ -322,16 +335,16 @@ const generateAngularRequestFunction = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('HttpRequest'), + plugin.api.selector('HttpRequest'), ); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts b/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts index 4e0c0b017..76f32d3b9 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/httpResources.ts @@ -1,12 +1,13 @@ import type { Symbol } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { stringCase } from '../../../utils/stringCase'; -import { operationClasses } from '../../@hey-api/sdk/operation'; -import { isOperationOptionsRequired } from '../../shared/utils/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { operationClasses } from '~/plugins/@hey-api/sdk/operation'; +import { isOperationOptionsRequired } from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; +import { stringCase } from '~/utils/stringCase'; + import type { AngularCommonPlugin } from './types'; interface AngularServiceClassEntry { @@ -27,70 +28,76 @@ const generateAngularClassServices = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const classes = operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }); + const classes = operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }); - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - if (!serviceClasses.has(currentClassName)) { - serviceClasses.set(currentClassName, { - className: currentClassName, - classes: new Set(), - methods: new Set(), - nodes: [], - root: !index, - }); - } + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + if (!serviceClasses.has(currentClassName)) { + serviceClasses.set(currentClassName, { + className: currentClassName, + classes: new Set(), + methods: new Set(), + nodes: [], + root: !index, + }); + } - const parentClassName = entry.path[index - 1]; - if (parentClassName && parentClassName !== currentClassName) { - const parentClass = serviceClasses.get(parentClassName)!; - parentClass.classes.add(currentClassName); - serviceClasses.set(parentClassName, parentClass); - } + const parentClassName = entry.path[index - 1]; + if (parentClassName && parentClassName !== currentClassName) { + const parentClass = serviceClasses.get(parentClassName)!; + parentClass.classes.add(currentClassName); + serviceClasses.set(parentClassName, parentClass); + } - const isLast = entry.path.length === index + 1; - if (!isLast) { - return; - } + const isLast = entry.path.length === index + 1; + if (!isLast) { + return; + } - const currentClass = serviceClasses.get(currentClassName)!; + const currentClass = serviceClasses.get(currentClassName)!; - const resourceMethodName = - plugin.config.httpResources.methodNameBuilder(operation); + const resourceMethodName = + plugin.config.httpResources.methodNameBuilder(operation); - if (currentClass.methods.has(resourceMethodName)) { - return; - } + if (currentClass.methods.has(resourceMethodName)) { + return; + } - const methodNode = generateAngularResourceMethod({ - isRequiredOptions, - methodName: resourceMethodName, - operation, - plugin, - }); + const methodNode = generateAngularResourceMethod({ + isRequiredOptions, + methodName: resourceMethodName, + operation, + plugin, + }); - if (!currentClass.nodes.length) { - currentClass.nodes.push(methodNode); - } else { - // @ts-expect-error - currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); - } + if (!currentClass.nodes.length) { + currentClass.nodes.push(methodNode); + } else { + // @ts-expect-error + currentClass.nodes.push(tsc.identifier({ text: '\n' }), methodNode); + } - currentClass.methods.add(resourceMethodName); - serviceClasses.set(currentClassName, currentClass); - }); - } - }); + currentClass.methods.add(resourceMethodName); + serviceClasses.set(currentClassName, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const generateClass = (currentClass: AngularServiceClassEntry) => { if (generatedClasses.has(currentClass.className)) { @@ -126,7 +133,7 @@ const generateAngularClassServices = ({ } const symbolInjectable = plugin.referenceSymbol( - plugin.api.getSelector('Injectable'), + plugin.api.selector('Injectable'), ); const symbolClass = plugin.registerSymbol({ exported: true, @@ -164,24 +171,30 @@ const generateAngularFunctionServices = ({ }: { plugin: AngularCommonPlugin['Instance']; }) => { - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); - const symbol = plugin.registerSymbol({ - exported: true, - name: plugin.config.httpResources.methodNameBuilder(operation), - }); - const node = generateAngularResourceFunction({ - isRequiredOptions, - operation, - plugin, - symbol, - }); - plugin.setSymbolValue(symbol, node); - }); + const symbol = plugin.registerSymbol({ + exported: true, + name: plugin.config.httpResources.methodNameBuilder(operation), + }); + const node = generateAngularResourceFunction({ + isRequiredOptions, + operation, + plugin, + symbol, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; const generateResourceCallExpression = ({ @@ -195,11 +208,11 @@ const generateResourceCallExpression = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolHttpResource = plugin.referenceSymbol( - plugin.api.getSelector('httpResource'), + plugin.api.selector('httpResource'), ); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); const responseType = symbolResponseType?.placeholder || 'unknown'; @@ -216,12 +229,12 @@ const generateResourceCallExpression = ({ // Import the root class from HTTP requests const rootClassName = firstEntry.path[0]!; const symbolClass = plugin.referenceSymbol( - plugin.api.getSelector('class', rootClassName), + plugin.api.selector('class', rootClassName), ); // Build the method access path using inject const symbolInject = plugin.referenceSymbol( - plugin.api.getSelector('inject'), + plugin.api.selector('inject'), ); let methodAccess: ts.Expression = tsc.callExpression({ functionName: symbolInject.placeholder, @@ -282,7 +295,7 @@ const generateResourceCallExpression = ({ } } else { const symbolHttpRequest = plugin.referenceSymbol( - plugin.api.getSelector('httpRequest', operation.id), + plugin.api.selector('httpRequest', operation.id), ); return tsc.callExpression({ @@ -351,11 +364,11 @@ const generateAngularResourceMethod = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; @@ -404,11 +417,11 @@ const generateAngularResourceFunction = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; diff --git a/packages/openapi-ts/src/plugins/@angular/common/plugin.ts b/packages/openapi-ts/src/plugins/@angular/common/plugin.ts index 289723764..da20d8c6a 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/plugin.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/plugin.ts @@ -9,22 +9,22 @@ export const handler: AngularCommonPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'HttpRequest', - selector: plugin.api.getSelector('HttpRequest'), + selector: plugin.api.selector('HttpRequest'), }); plugin.registerSymbol({ external: '@angular/core', name: 'inject', - selector: plugin.api.getSelector('inject'), + selector: plugin.api.selector('inject'), }); plugin.registerSymbol({ external: '@angular/core', name: 'Injectable', - selector: plugin.api.getSelector('Injectable'), + selector: plugin.api.selector('Injectable'), }); plugin.registerSymbol({ external: '@angular/common/http', name: 'httpResource', - selector: plugin.api.getSelector('httpResource'), + selector: plugin.api.selector('httpResource'), }); if (plugin.config.httpRequests.enabled) { diff --git a/packages/openapi-ts/src/plugins/@angular/common/types.d.ts b/packages/openapi-ts/src/plugins/@angular/common/types.d.ts index a7d7f73e7..680b72b85 100644 --- a/packages/openapi-ts/src/plugins/@angular/common/types.d.ts +++ b/packages/openapi-ts/src/plugins/@angular/common/types.d.ts @@ -1,5 +1,6 @@ -import type { StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@angular/common'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts index 5af4687d0..1fdc9a1f7 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-angular'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/config.ts index 206439156..f15b005af 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientAngularPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-angular/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-angular/types.d.ts index f00c11617..5bb2cff2f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-angular/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-angular/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-angular'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/__tests__/client.test.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/__tests__/client.test.ts index 45ce4e81b..f8fe1fa9a 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/__tests__/client.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/__tests__/client.test.ts @@ -352,206 +352,3 @@ describe('confirming axios behaviour for constructing URLs', () => { expect(config).toBe('https://api.example.com/users'); }); }); - -describe('error handling', () => { - it('should handle 404 response when throwOnError is false', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const errorData = { error: 'Resource not found' }; - const axiosError = new axios.AxiosError( - 'Request failed with status code 404', - 'ERR_BAD_REQUEST', - {} as any, - {}, - { - config: {} as any, - data: errorData, - headers: {}, - status: 404, - statusText: 'Not Found', - }, - ); - - const mockAxios = vi.fn().mockResolvedValue(axiosError); - - const result = await client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - throwOnError: false, - url: '/users/999', - }); - - expect(mockAxios).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://api.example.com/users/999', - }), - ); - expect(result).toBeInstanceOf(axios.AxiosError); - expect(result).toMatchObject({ - error: errorData, - response: { - data: errorData, - status: 404, - statusText: 'Not Found', - }, - }); - }); - - it('should throw 404 error when throwOnError is true', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const errorData = { error: 'Resource not found' }; - const axiosError = new axios.AxiosError( - 'Request failed with status code 404', - 'ERR_BAD_REQUEST', - {} as any, - {}, - { - config: {} as any, - data: errorData, - headers: {}, - status: 404, - statusText: 'Not Found', - }, - ); - - const mockAxios = vi.fn().mockResolvedValue(axiosError); - - await expect( - client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - throwOnError: true, - url: '/users/999', - }), - ).rejects.toMatchObject({ - response: { - data: errorData, - status: 404, - statusText: 'Not Found', - }, - }); - }); - - it('should handle ECONNREFUSED error when throwOnError is false', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const connectionError = axios.AxiosError.from( - new AggregateError( - [ - new Error('connect ECONNREFUSED ::1:3000'), - new Error('connect ECONNREFUSED 127.0.0.1:3000'), - ], - 'Error', - ), - 'ECONNREFUSED', - {} as any, - {}, - undefined, // no response object for connection errors - ); - - const mockAxios = vi.fn().mockResolvedValue(connectionError); - - const result = await client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - throwOnError: false, - url: '/users', - }); - - expect(mockAxios).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://api.example.com/users', - }), - ); - expect(result).toMatchObject({ - code: 'ECONNREFUSED', - error: {}, // error.response?.data ?? {} - message: 'Error', - }); - }); - - it('should throw ECONNREFUSED error when throwOnError is true', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const connectionError = axios.AxiosError.from( - new AggregateError( - [ - new Error('connect ECONNREFUSED ::1:3000'), - new Error('connect ECONNREFUSED 127.0.0.1:3000'), - ], - 'Error', - ), - 'ECONNREFUSED', - {} as any, - {}, - undefined, - ); - - const mockAxios = vi.fn().mockResolvedValue(connectionError); - - await expect( - client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - throwOnError: true, - url: '/users', - }), - ).rejects.toMatchObject({ - code: 'ECONNREFUSED', - message: 'Error', - name: 'AggregateError', - }); - }); - - it('should handle non-object error from responseValidator when throwOnError is false', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const mockAxios = vi.fn().mockResolvedValue({ - data: { id: 1, name: 'test' }, - headers: {}, - status: 200, - statusText: 'OK', - }); - - const result = await client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - responseType: 'json', - responseValidator: vi.fn().mockRejectedValue('validation failed'), - throwOnError: false, - url: '/users/1', - }); - - expect(mockAxios).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://api.example.com/users/1', - }), - ); - expect(result).toEqual({ - error: {}, - }); - }); - - it('should throw non-object error from responseValidator when throwOnError is true', async () => { - const client = createClient({ baseURL: 'https://api.example.com' }); - - const mockAxios = vi.fn().mockResolvedValue({ - data: { id: 1, name: 'test' }, - headers: {}, - status: 200, - statusText: 'OK', - }); - - await expect( - client.get({ - axios: mockAxios as Partial as AxiosInstance, - headers: {}, - responseType: 'json', - responseValidator: vi.fn().mockRejectedValue('non-object error'), - throwOnError: true, - url: '/users/1', - }), - ).rejects.toBe('non-object error'); - }); -}); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts index 9414a28fe..ee544c72c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-axios'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/bundle/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/bundle/client.ts index 28d35aa5b..fcbbfea0b 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/bundle/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/bundle/client.ts @@ -1,5 +1,5 @@ -import type { AxiosInstance, RawAxiosRequestHeaders } from 'axios'; -import axios, { AxiosError } from 'axios'; +import type { AxiosError, AxiosInstance, RawAxiosRequestHeaders } from 'axios'; +import axios from 'axios'; import { createSseClient } from '../../client-core/bundle/serverSentEvents'; import type { HttpMethod } from '../../client-core/bundle/types'; @@ -67,6 +67,7 @@ export const createClient = (config: Config = {}): Client => { return { opts, url }; }; + // @ts-expect-error const request: Client['request'] = async (options) => { // @ts-expect-error const { opts, url } = await beforeRequest(options); @@ -84,7 +85,6 @@ export const createClient = (config: Config = {}): Client => { params: opts.paramsSerializer ? opts.query : undefined, url, }); - if (response instanceof Error) throw response; let { data } = response; @@ -103,22 +103,13 @@ export const createClient = (config: Config = {}): Client => { data: data ?? {}, }; } catch (error) { + const e = error as AxiosError; if (opts.throwOnError) { - throw error; + throw e; } - - if (error instanceof AxiosError) { - // @ts-expect-error - error.error = error.response?.data ?? {}; - return error; - } - - if (typeof error === 'object' && error !== null) { - error.error = {}; - return error; - } - - return { error: {} }; + // @ts-expect-error + e.error = e.response?.data ?? {}; + return e; } }; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/config.ts index 7cc48c700..cfb1d088f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientAxiosPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-axios/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-axios/types.d.ts index 0d31baa36..058c44449 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-axios/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-axios/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-axios'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts index 35c4ee0e9..d1122ed2c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/client.ts @@ -1,6 +1,7 @@ -import { clientFolderAbsolutePath } from '../../../generate/client'; -import { tsc } from '../../../tsc'; -import { parseUrl } from '../../../utils/url'; +import { clientFolderAbsolutePath } from '~/generate/client'; +import { tsc } from '~/tsc'; +import { parseUrl } from '~/utils/url'; + import type { PluginHandler } from './types'; import { getClientBaseUrlKey } from './utils'; @@ -38,7 +39,7 @@ export const createClient: PluginHandler = ({ plugin }) => { }); const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolClientOptions = plugin.referenceSymbol( - pluginTypeScript.api.getSelector('ClientOptions'), + pluginTypeScript.api.selector('ClientOptions'), ); const { runtimeConfigPath } = plugin.config; @@ -93,7 +94,7 @@ export const createClient: PluginHandler = ({ plugin }) => { const symbolClient = plugin.registerSymbol({ name: 'client', - selector: plugin.api.getSelector('client'), + selector: plugin.api.selector('client'), }); const statement = tsc.constVariable({ exportConst: true, diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts index 972f9b696..732d5e9d7 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/createClientConfig.ts @@ -1,5 +1,6 @@ -import { clientFolderAbsolutePath } from '../../../generate/client'; -import { tsc } from '../../../tsc'; +import { clientFolderAbsolutePath } from '~/generate/client'; +import { tsc } from '~/tsc'; + import type { PluginHandler } from './types'; export const createClientConfigType = ({ @@ -8,7 +9,7 @@ export const createClientConfigType = ({ const clientModule = clientFolderAbsolutePath(plugin.context.config); const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolClientOptions = plugin.referenceSymbol( - pluginTypeScript.api.getSelector('ClientOptions'), + pluginTypeScript.api.selector('ClientOptions'), ); const symbolConfig = plugin.registerSymbol({ external: clientModule, diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/types.d.ts index 0b328ab78..d76b102fe 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/types.d.ts @@ -1,10 +1,10 @@ -import type { Plugin } from '../../types'; -import type { HeyApiClientAngularPlugin } from '../client-angular'; -import type { HeyApiClientAxiosPlugin } from '../client-axios'; -import type { HeyApiClientFetchPlugin } from '../client-fetch'; -import type { HeyApiClientNextPlugin } from '../client-next'; -import type { HeyApiClientNuxtPlugin } from '../client-nuxt'; -import type { HeyApiClientOfetchPlugin } from '../client-ofetch'; +import type { HeyApiClientAngularPlugin } from '~/plugins/@hey-api/client-angular'; +import type { HeyApiClientAxiosPlugin } from '~/plugins/@hey-api/client-axios'; +import type { HeyApiClientFetchPlugin } from '~/plugins/@hey-api/client-fetch'; +import type { HeyApiClientNextPlugin } from '~/plugins/@hey-api/client-next'; +import type { HeyApiClientNuxtPlugin } from '~/plugins/@hey-api/client-nuxt'; +import type { HeyApiClientOfetchPlugin } from '~/plugins/@hey-api/client-ofetch'; +import type { Plugin } from '~/plugins/types'; export interface PluginHandler { (...args: Parameters): void; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-core/utils.ts b/packages/openapi-ts/src/plugins/@hey-api/client-core/utils.ts index 9ad8f1adb..05a0cbbf0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-core/utils.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-core/utils.ts @@ -1,5 +1,5 @@ -import type { Config } from '../../../types/config'; -import type { PluginClientNames } from '../../types'; +import type { PluginClientNames } from '~/plugins/types'; +import type { Config } from '~/types/config'; export const getClientBaseUrlKey = (config: Config) => { const client = getClientPlugin(config); diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts index 1a4e9f742..29e1dbc4d 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-fetch'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/config.ts index e998359d3..0e50f65d0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientFetchPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/types.d.ts index 89ccfec08..37d3b06f0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-fetch/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-fetch/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-fetch'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts index 0aa1c5e88..d95026b35 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-next'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/config.ts index f97faa6ea..e219b4992 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientNextPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-next/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-next/types.d.ts index a4ecee19d..7319f3521 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-next/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-next/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-next'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts index ddb217aa1..e84b7849e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-nuxt'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/config.ts index 3a11f545b..6a686ecd4 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientNuxtPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/types.d.ts index 1c45f5704..4f297a0bb 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-nuxt/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-nuxt'> & Client.Config; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts index 11a238387..2e94cdb89 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'client'; @@ -11,13 +11,13 @@ export type IApi = { * - `client`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/client-ofetch'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/config.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/config.ts index 6cfaa83bf..adf180f20 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/config.ts @@ -1,6 +1,10 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; -import { clientPluginHandler } from '../client-core/plugin'; +import { + clientDefaultConfig, + clientDefaultMeta, +} from '~/plugins/@hey-api/client-core/config'; +import { clientPluginHandler } from '~/plugins/@hey-api/client-core/plugin'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { HeyApiClientOfetchPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/types.d.ts index 633236a4e..697618a19 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/client-ofetch/types.d.ts @@ -1,5 +1,6 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/client-ofetch'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/config.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/config.ts index 9fb818205..7d7064629 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import type { HeyApiClientLegacyAngularPlugin } from './types'; export const defaultConfig: HeyApiClientLegacyAngularPlugin['Config'] = { diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/types.d.ts index c0286e5c7..82f85d754 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-angular/types.d.ts @@ -1,5 +1,5 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; export type UserConfig = Plugin.Name<'legacy/angular'> & Pick; diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/config.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/config.ts index b3b649214..796607fe9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import type { HeyApiClientLegacyAxiosPlugin } from './types'; export const defaultConfig: HeyApiClientLegacyAxiosPlugin['Config'] = { diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/types.d.ts index f0f67e65f..0ca49022f 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-axios/types.d.ts @@ -1,5 +1,5 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; export type UserConfig = Plugin.Name<'legacy/axios'> & Pick; diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/config.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/config.ts index b25511763..730df72f0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import type { HeyApiClientLegacyFetchPlugin } from './types'; export const defaultConfig: HeyApiClientLegacyFetchPlugin['Config'] = { diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/types.d.ts index 953a491c8..448d0020c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-fetch/types.d.ts @@ -1,5 +1,5 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; export type UserConfig = Plugin.Name<'legacy/fetch'> & Pick; diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-node/config.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-node/config.ts index caa7f71ff..71e7051f0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-node/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-node/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import type { HeyApiClientLegacyNodePlugin } from './types'; export const defaultConfig: HeyApiClientLegacyNodePlugin['Config'] = { diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-node/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-node/types.d.ts index 070b46611..9def74381 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-node/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-node/types.d.ts @@ -1,5 +1,5 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; export type UserConfig = Plugin.Name<'legacy/node'> & Pick; diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/config.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/config.ts index 617ce2ee9..499b222f1 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import type { HeyApiClientLegacyXhrPlugin } from './types'; export const defaultConfig: HeyApiClientLegacyXhrPlugin['Config'] = { diff --git a/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/types.d.ts index 6e19c613e..7b6d2623c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/legacy-xhr/types.d.ts @@ -1,5 +1,5 @@ -import type { DefinePlugin, Plugin } from '../../types'; -import type { Client } from '../client-core/types'; +import type { Client } from '~/plugins/@hey-api/client-core/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; export type UserConfig = Plugin.Name<'legacy/xhr'> & Pick; diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts index a7e827630..479ea0bcb 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/__tests__/schemas.test.ts @@ -4,11 +4,12 @@ import { Project } from '@hey-api/codegen-core'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import { client, openApi } from '../../../../generate/__tests__/mocks'; -import type { OpenApiV3Schema } from '../../../../openApi'; -import type { Files } from '../../../../types/utils'; -import { setConfig } from '../../../../utils/config'; -import { PluginInstance } from '../../../shared/utils/instance'; +import { client, openApi } from '~/generate/__tests__/mocks'; +import type { OpenApiV3Schema } from '~/openApi'; +import { PluginInstance } from '~/plugins/shared/utils/instance'; +import type { Files } from '~/types/utils'; +import { setConfig } from '~/utils/config'; + import { handlerLegacy } from '../plugin-legacy'; vi.mock('node:fs'); @@ -88,7 +89,7 @@ describe('generateLegacySchemas', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -100,7 +101,7 @@ describe('generateLegacySchemas', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -111,8 +112,8 @@ describe('generateLegacySchemas', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -251,7 +252,7 @@ describe('generateLegacySchemas', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -264,7 +265,7 @@ describe('generateLegacySchemas', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -275,8 +276,8 @@ describe('generateLegacySchemas', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts index 0deea2b62..b24c4005c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'ref'; @@ -11,13 +11,13 @@ export type IApi = { * - `ref`: `$ref` JSON pointer * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/schemas'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/config.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/config.ts index 55c9a8136..7c4e6b2b2 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import { handlerLegacy } from './plugin-legacy'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin-legacy.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin-legacy.ts index e2a16dc4f..ed0a5ebac 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin-legacy.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin-legacy.ts @@ -1,8 +1,9 @@ -import { GeneratedFile } from '../../../generate/file'; -import type { OpenApiV2Schema, OpenApiV3Schema } from '../../../openApi'; -import { ensureValidTypeScriptJavaScriptIdentifier } from '../../../openApi'; -import { tsc } from '../../../tsc'; -import { getConfig } from '../../../utils/config'; +import { GeneratedFile } from '~/generate/file'; +import type { OpenApiV2Schema, OpenApiV3Schema } from '~/openApi'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '~/openApi'; +import { tsc } from '~/tsc'; +import { getConfig } from '~/utils/config'; + import type { HeyApiSchemasPlugin } from './types'; const ensureValidSchemaOutput = ( diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts index c902cbb6d..959bfb539 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/plugin.ts @@ -1,11 +1,12 @@ -import { satisfies } from '../../../config/utils/package'; -import type { IR } from '../../../ir/types'; -import type { OpenApiV2_0_XTypes } from '../../../openApi/2.0.x'; -import type { OpenApiV3_0_XTypes } from '../../../openApi/3.0.x'; -import type { OpenApiV3_1_XTypes } from '../../../openApi/3.1.x'; -import { ensureValidIdentifier } from '../../../openApi/shared/utils/identifier'; -import type { OpenApi } from '../../../openApi/types'; -import { tsc } from '../../../tsc'; +import { satisfies } from '~/config/utils/package'; +import type { IR } from '~/ir/types'; +import type { OpenApiV2_0_XTypes } from '~/openApi/2.0.x'; +import type { OpenApiV3_0_XTypes } from '~/openApi/3.0.x'; +import type { OpenApiV3_1_XTypes } from '~/openApi/3.1.x'; +import { ensureValidIdentifier } from '~/openApi/shared/utils/identifier'; +import type { OpenApi } from '~/openApi/types'; +import { tsc } from '~/tsc'; + import type { HeyApiSchemasPlugin } from './types'; const stripSchema = ({ @@ -370,7 +371,7 @@ const schemasV2_0_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchemaDraft_04({ context, @@ -403,7 +404,7 @@ const schemasV3_0_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchemaDraft_05({ context, @@ -436,7 +437,7 @@ const schemasV3_1_X = ({ const symbol = plugin.registerSymbol({ exported: true, name: schemaName({ name, plugin, schema }), - selector: plugin.api.getSelector('ref', name), + selector: plugin.api.selector('ref', name), }); const obj = schemaToJsonSchema2020_12({ context, diff --git a/packages/openapi-ts/src/plugins/@hey-api/schemas/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/schemas/types.d.ts index fdffdd1ac..856239acb 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/schemas/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/schemas/types.d.ts @@ -1,8 +1,9 @@ -import type { OpenApiV2Schema, OpenApiV3Schema } from '../../../openApi'; -import type { OpenApiV2_0_XTypes } from '../../../openApi/2.0.x'; -import type { OpenApiV3_0_XTypes } from '../../../openApi/3.0.x'; -import type { OpenApiV3_1_XTypes } from '../../../openApi/3.1.x'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { OpenApiV2Schema, OpenApiV3Schema } from '~/openApi'; +import type { OpenApiV2_0_XTypes } from '~/openApi/2.0.x'; +import type { OpenApiV3_0_XTypes } from '~/openApi/3.0.x'; +import type { OpenApiV3_1_XTypes } from '~/openApi/3.1.x'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/schemas'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts index 2c32ca954..86b288d48 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/__tests__/plugin.test.ts @@ -5,13 +5,14 @@ import { Project } from '@hey-api/codegen-core'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import { openApi } from '../../../../generate/__tests__/mocks'; -import { GeneratedFile } from '../../../../generate/file'; -import type { Operation } from '../../../../types/client'; -import type { Config } from '../../../../types/config'; -import type { Files } from '../../../../types/utils'; -import { setConfig } from '../../../../utils/config'; -import { PluginInstance } from '../../../shared/utils/instance'; +import { openApi } from '~/generate/__tests__/mocks'; +import { GeneratedFile } from '~/generate/file'; +import { PluginInstance } from '~/plugins/shared/utils/instance'; +import type { Operation } from '~/types/client'; +import type { Config } from '~/types/config'; +import type { Files } from '~/types/utils'; +import { setConfig } from '~/utils/config'; + import { handlerLegacy } from '../plugin-legacy'; vi.mock('node:fs'); @@ -90,7 +91,7 @@ describe('handlerLegacy', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -102,7 +103,7 @@ describe('handlerLegacy', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -114,8 +115,8 @@ describe('handlerLegacy', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -326,7 +327,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -338,7 +339,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -350,8 +351,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -484,7 +485,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -496,7 +497,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: true, @@ -509,8 +510,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', @@ -645,7 +646,7 @@ describe('methodNameBuilder', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -657,7 +658,7 @@ describe('methodNameBuilder', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { asClass: false, @@ -670,8 +671,8 @@ describe('methodNameBuilder', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { name: '@hey-api/typescript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts index 0737a347f..18031c2fe 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/api.ts @@ -1,6 +1,7 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; + import { createOperationComment } from './comment'; type SelectorType = @@ -30,7 +31,7 @@ export type IApi = { * - `urlSearchParamsBodySerializer`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { @@ -42,7 +43,7 @@ export class Api implements IApi { return createOperationComment(...args); } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/auth.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/auth.ts index 6b9094486..3766e0cd4 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/auth.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/auth.ts @@ -1,4 +1,5 @@ -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; + import type { Auth } from '../client-core/bundle/auth'; import type { HeyApiSdkPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/comment.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/comment.ts index 46a4f9d41..c8229c101 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/comment.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/comment.ts @@ -1,6 +1,6 @@ -import type { IR } from '../../../ir/types'; -import type { Comments } from '../../../tsc'; -import { escapeComment } from '../../../utils/escape'; +import type { IR } from '~/ir/types'; +import type { Comments } from '~/tsc'; +import { escapeComment } from '~/utils/escape'; export const createOperationComment = ({ operation, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts index 967510fa5..956f1e584 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import { handlerLegacy } from './plugin-legacy'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts index 51a7c224c..e63776dbe 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/operation.ts @@ -1,16 +1,17 @@ import type ts from 'typescript'; -import { statusCodeToGroup } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { sanitizeNamespaceIdentifier } from '../../../openApi'; -import { ensureValidIdentifier } from '../../../openApi/shared/utils/identifier'; -import { tsc } from '../../../tsc'; -import type { FunctionParameter, ObjectValue } from '../../../tsc/types'; -import { reservedJavaScriptKeywordsRegExp } from '../../../utils/regexp'; -import { stringCase } from '../../../utils/stringCase'; -import { transformClassName } from '../../../utils/transform'; +import { statusCodeToGroup } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { sanitizeNamespaceIdentifier } from '~/openApi'; +import { ensureValidIdentifier } from '~/openApi/shared/utils/identifier'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { tsc } from '~/tsc'; +import type { FunctionParameter, ObjectValue } from '~/tsc/types'; +import { reservedJavaScriptKeywordsRegExp } from '~/utils/regexp'; +import { stringCase } from '~/utils/stringCase'; +import { transformClassName } from '~/utils/transform'; + import type { Field, Fields } from '../client-core/bundle/params'; -import { getClientPlugin } from '../client-core/utils'; import { operationAuth } from './auth'; import { nuxtTypeComposable, nuxtTypeDefault } from './constants'; // import { getSignatureParameters } from './signature'; @@ -160,17 +161,15 @@ export const operationOptionsType = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); const dataType = symbolDataType?.placeholder || 'unknown'; - const symbolOptions = plugin.referenceSymbol( - plugin.api.getSelector('Options'), - ); + const symbolOptions = plugin.referenceSymbol(plugin.api.selector('Options')); if (isNuxtClient) { const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); const responseType = symbolResponseType?.placeholder || 'unknown'; return `${symbolOptions.placeholder}<${nuxtTypeComposable}, ${dataType}, ${responseType}, ${nuxtTypeDefault}>`; @@ -358,7 +357,7 @@ export const operationStatements = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector( + pluginTypeScript.api.selector( isNuxtClient ? 'response' : 'responses', operation.id, ), @@ -366,7 +365,7 @@ export const operationStatements = ({ const responseType = symbolResponseType?.placeholder || 'unknown'; const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector( + pluginTypeScript.api.selector( isNuxtClient ? 'error' : 'errors', operation.id, ), @@ -395,7 +394,7 @@ export const operationStatements = ({ switch (operation.body.type) { case 'form-data': { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('formDataBodySerializer'), + plugin.api.selector('formDataBodySerializer'), ); requestOptions.push({ spread: symbol.placeholder }); break; @@ -413,7 +412,7 @@ export const operationStatements = ({ break; case 'url-search-params': { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('urlSearchParamsBodySerializer'), + plugin.api.selector('urlSearchParamsBodySerializer'), ); requestOptions.push({ spread: symbol.placeholder }); break; @@ -468,7 +467,7 @@ export const operationStatements = ({ plugin.config.transformer, ); const symbolResponseTransformer = plugin.getSymbol( - pluginTransformers.api.getSelector('response', operation.id), + pluginTransformers.api.selector('response', operation.id), ); if ( symbolResponseTransformer && @@ -571,7 +570,7 @@ export const operationStatements = ({ config.push(tsc.objectExpression({ obj })); } const symbol = plugin.referenceSymbol( - plugin.api.getSelector('buildClientParams'), + plugin.api.selector('buildClientParams'), ); statements.push( tsc.constVariable({ @@ -626,10 +625,10 @@ export const operationStatements = ({ } const symbolClient = - plugin.config.client && client.api && 'getSelector' in client.api + plugin.config.client && client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin-legacy.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin-legacy.ts index 3a8c30c3c..0ed757f26 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin-legacy.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin-legacy.ts @@ -1,32 +1,33 @@ import type ts from 'typescript'; -import { clientModulePath } from '../../../generate/client'; -import { GeneratedFile } from '../../../generate/file'; -import type { IR } from '../../../ir/types'; -import { isOperationParameterRequired } from '../../../openApi'; -import type { Comments, FunctionParameter } from '../../../tsc'; -import { tsc } from '../../../tsc'; -import type { FunctionTypeParameter, ObjectValue } from '../../../tsc/types'; +import { clientModulePath } from '~/generate/client'; +import { GeneratedFile } from '~/generate/file'; +import type { IR } from '~/ir/types'; +import { isOperationParameterRequired } from '~/openApi'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { Comments, FunctionParameter } from '~/tsc'; +import { tsc } from '~/tsc'; +import type { FunctionTypeParameter, ObjectValue } from '~/tsc/types'; import type { Client, Model, Operation, OperationParameter, Service, -} from '../../../types/client'; -import type { Config as ClientConfig } from '../../../types/config'; +} from '~/types/client'; +import type { Config as ClientConfig } from '~/types/config'; import { getConfig, isLegacyClient, legacyNameFromConfig, -} from '../../../utils/config'; -import { escapeComment, escapeName } from '../../../utils/escape'; -import { reservedJavaScriptKeywordsRegExp } from '../../../utils/regexp'; -import { stringCase } from '../../../utils/stringCase'; -import { transformClassName } from '../../../utils/transform'; -import { setUniqueTypeName } from '../../../utils/type'; -import { unique } from '../../../utils/unique'; -import { getClientPlugin } from '../client-core/utils'; +} from '~/utils/config'; +import { escapeComment, escapeName } from '~/utils/escape'; +import { reservedJavaScriptKeywordsRegExp } from '~/utils/regexp'; +import { stringCase } from '~/utils/stringCase'; +import { transformClassName } from '~/utils/transform'; +import { setUniqueTypeName } from '~/utils/type'; +import { unique } from '~/utils/unique'; + import type { HeyApiSdkPlugin } from './types'; type OnNode = (node: ts.Node) => void; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts index f799a89e1..8407fe0cb 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts @@ -1,10 +1,11 @@ import type ts from 'typescript'; -import { clientFolderAbsolutePath } from '../../../generate/client'; -import { tsc } from '../../../tsc'; -import { stringCase } from '../../../utils/stringCase'; -import { isOperationOptionsRequired } from '../../shared/utils/operation'; -import { getClientPlugin } from '../client-core/utils'; +import { clientFolderAbsolutePath } from '~/generate/client'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { isOperationOptionsRequired } from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; +import { stringCase } from '~/utils/stringCase'; + import { nuxtTypeComposable, nuxtTypeDefault } from './constants'; import { operationClasses, @@ -34,13 +35,13 @@ const createClientClassNodes = ({ }), }); - const symbolClient = plugin.referenceSymbol(plugin.api.getSelector('Client')); + const symbolClient = plugin.referenceSymbol(plugin.api.selector('Client')); const client = getClientPlugin(plugin.context.config); const symClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -136,138 +137,144 @@ const generateClassSdk = ({ ? createClientClassNodes({ plugin }) : []; - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = isNuxtClient - ? plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ) - : undefined; - - const classes = operationClasses({ - context: plugin.context, - operation, - plugin, - }); - - for (const entry of classes.values()) { - entry.path.forEach((currentClassName, index) => { - const symbolCurrentClass = plugin.referenceSymbol( - plugin.api.getSelector('class', currentClassName), - ); - if (!sdkClasses.has(symbolCurrentClass.id)) { - sdkClasses.set(symbolCurrentClass.id, { - className: currentClassName, - classes: new Set(), - id: symbolCurrentClass.id, - methods: new Set(), - nodes: [], - root: !index, - }); - } + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, + operation, + }); + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = isNuxtClient + ? plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ) + : undefined; + + const classes = operationClasses({ + context: plugin.context, + operation, + plugin, + }); - const parentClassName = entry.path[index - 1]; - if (parentClassName) { - const symbolParentClass = plugin.referenceSymbol( - plugin.api.getSelector('class', parentClassName), + for (const entry of classes.values()) { + entry.path.forEach((currentClassName, index) => { + const symbolCurrentClass = plugin.referenceSymbol( + plugin.api.selector('class', currentClassName), ); - if ( - symbolParentClass.placeholder !== symbolCurrentClass.placeholder - ) { - const parentClass = sdkClasses.get(symbolParentClass.id)!; - parentClass.classes.add(symbolCurrentClass.id); - sdkClasses.set(symbolParentClass.id, parentClass); + if (!sdkClasses.has(symbolCurrentClass.id)) { + sdkClasses.set(symbolCurrentClass.id, { + className: currentClassName, + classes: new Set(), + id: symbolCurrentClass.id, + methods: new Set(), + nodes: [], + root: !index, + }); } - } - const isLast = entry.path.length === index + 1; - // add methods only to the last class - if (!isLast) { - return; - } + const parentClassName = entry.path[index - 1]; + if (parentClassName) { + const symbolParentClass = plugin.referenceSymbol( + plugin.api.selector('class', parentClassName), + ); + if ( + symbolParentClass.placeholder !== symbolCurrentClass.placeholder + ) { + const parentClass = sdkClasses.get(symbolParentClass.id)!; + parentClass.classes.add(symbolCurrentClass.id); + sdkClasses.set(symbolParentClass.id, parentClass); + } + } - const currentClass = sdkClasses.get(symbolCurrentClass.id)!; + const isLast = entry.path.length === index + 1; + // add methods only to the last class + if (!isLast) { + return; + } - // avoid duplicate methods - if (currentClass.methods.has(entry.methodName)) { - return; - } + const currentClass = sdkClasses.get(symbolCurrentClass.id)!; - const opParameters = operationParameters({ - isRequiredOptions, - operation, - plugin, - }); - const statements = operationStatements({ - isRequiredOptions, - opParameters, - operation, - plugin, - }); - const functionNode = tsc.methodDeclaration({ - accessLevel: 'public', - comment: plugin.api.createOperationComment({ operation }), - isStatic: isAngularClient ? false : !plugin.config.instance, - name: entry.methodName, - parameters: opParameters.parameters, - returnType: undefined, - statements, - types: isNuxtClient - ? [ - { - default: tsc.ots.string('$fetch'), - extends: tsc.typeNode( - plugin.referenceSymbol(plugin.api.getSelector('Composable')) - .placeholder, - ), - name: nuxtTypeComposable, - }, - { - default: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : tsc.typeNode('undefined'), - extends: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : undefined, - name: nuxtTypeDefault, - }, - ] - : [ - { - default: - ('throwOnError' in client.config - ? client.config.throwOnError - : false) ?? false, - extends: 'boolean', - name: 'ThrowOnError', - }, - ], - }); + // avoid duplicate methods + if (currentClass.methods.has(entry.methodName)) { + return; + } - if (!currentClass.nodes.length) { - currentClass.nodes.push(functionNode); - } else { - currentClass.nodes.push( - // @ts-expect-error - tsc.identifier({ text: '\n' }), - functionNode, - ); - } + const opParameters = operationParameters({ + isRequiredOptions, + operation, + plugin, + }); + const statements = operationStatements({ + isRequiredOptions, + opParameters, + operation, + plugin, + }); + const functionNode = tsc.methodDeclaration({ + accessLevel: 'public', + comment: plugin.api.createOperationComment({ operation }), + isStatic: isAngularClient ? false : !plugin.config.instance, + name: entry.methodName, + parameters: opParameters.parameters, + returnType: undefined, + statements, + types: isNuxtClient + ? [ + { + default: tsc.ots.string('$fetch'), + extends: tsc.typeNode( + plugin.referenceSymbol(plugin.api.selector('Composable')) + .placeholder, + ), + name: nuxtTypeComposable, + }, + { + default: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : tsc.typeNode('undefined'), + extends: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : undefined, + name: nuxtTypeDefault, + }, + ] + : [ + { + default: + ('throwOnError' in client.config + ? client.config.throwOnError + : false) ?? false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }); - currentClass.methods.add(entry.methodName); + if (!currentClass.nodes.length) { + currentClass.nodes.push(functionNode); + } else { + currentClass.nodes.push( + // @ts-expect-error + tsc.identifier({ text: '\n' }), + functionNode, + ); + } - sdkClasses.set(symbolCurrentClass.id, currentClass); - }); - } - }); + currentClass.methods.add(entry.methodName); + + sdkClasses.set(symbolCurrentClass.id, currentClass); + }); + } + }, + { + order: 'declarations', + }, + ); const symbolHeyApiClient = plugin.registerSymbol({ exported: false, @@ -324,7 +331,7 @@ const generateClassSdk = ({ const symbol = plugin.registerSymbol({ exported: true, name: currentClass.className, - selector: plugin.api.getSelector('class', currentClass.className), + selector: plugin.api.selector('class', currentClass.className), }); const node = tsc.classDeclaration({ decorator: @@ -335,7 +342,7 @@ const generateClassSdk = ({ providedIn: 'root', }, ], - name: plugin.referenceSymbol(plugin.api.getSelector('Injectable')) + name: plugin.referenceSymbol(plugin.api.selector('Injectable')) .placeholder, } : undefined, @@ -372,83 +379,89 @@ const generateFlatSdk = ({ const client = getClientPlugin(plugin.context.config); const isNuxtClient = client.name === '@hey-api/client-nuxt'; - plugin.forEach('operation', ({ operation }) => { - const isRequiredOptions = isOperationOptionsRequired({ - context: plugin.context, - operation, - }); - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = isNuxtClient - ? plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ) - : undefined; - const opParameters = operationParameters({ - isRequiredOptions, - operation, - plugin, - }); - const statements = operationStatements({ - isRequiredOptions, - opParameters, - operation, - plugin, - }); - const symbol = plugin.registerSymbol({ - name: serviceFunctionIdentifier({ - config: plugin.context.config, - handleIllegal: true, - id: operation.id, + plugin.forEach( + 'operation', + ({ operation }) => { + const isRequiredOptions = isOperationOptionsRequired({ + context: plugin.context, operation, - }), - selector: plugin.api.getSelector('function', operation.id), - }); - const node = tsc.constVariable({ - comment: plugin.api.createOperationComment({ operation }), - exportConst: true, - expression: tsc.arrowFunction({ - parameters: opParameters.parameters, - returnType: undefined, - statements, - types: isNuxtClient - ? [ - { - default: tsc.ots.string('$fetch'), - extends: tsc.typeNode( - plugin.referenceSymbol(plugin.api.getSelector('Composable')) - .placeholder, - ), - name: nuxtTypeComposable, - }, - { - default: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : tsc.typeNode('undefined'), - extends: symbolResponse - ? tsc.typeReferenceNode({ - typeName: symbolResponse.placeholder, - }) - : undefined, - name: nuxtTypeDefault, - }, - ] - : [ - { - default: - ('throwOnError' in client.config - ? client.config.throwOnError - : false) ?? false, - extends: 'boolean', - name: 'ThrowOnError', - }, - ], - }), - name: symbol.placeholder, - }); - plugin.setSymbolValue(symbol, node); - }); + }); + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = isNuxtClient + ? plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ) + : undefined; + const opParameters = operationParameters({ + isRequiredOptions, + operation, + plugin, + }); + const statements = operationStatements({ + isRequiredOptions, + opParameters, + operation, + plugin, + }); + const symbol = plugin.registerSymbol({ + name: serviceFunctionIdentifier({ + config: plugin.context.config, + handleIllegal: true, + id: operation.id, + operation, + }), + selector: plugin.api.selector('function', operation.id), + }); + const node = tsc.constVariable({ + comment: plugin.api.createOperationComment({ operation }), + exportConst: true, + expression: tsc.arrowFunction({ + parameters: opParameters.parameters, + returnType: undefined, + statements, + types: isNuxtClient + ? [ + { + default: tsc.ots.string('$fetch'), + extends: tsc.typeNode( + plugin.referenceSymbol(plugin.api.selector('Composable')) + .placeholder, + ), + name: nuxtTypeComposable, + }, + { + default: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : tsc.typeNode('undefined'), + extends: symbolResponse + ? tsc.typeReferenceNode({ + typeName: symbolResponse.placeholder, + }) + : undefined, + name: nuxtTypeDefault, + }, + ] + : [ + { + default: + ('throwOnError' in client.config + ? client.config.throwOnError + : false) ?? false, + extends: 'boolean', + name: 'ThrowOnError', + }, + ], + }), + name: symbol.placeholder, + }); + plugin.setSymbolValue(symbol, node); + }, + { + order: 'declarations', + }, + ); }; export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { @@ -456,17 +469,17 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: clientModule, name: 'formDataBodySerializer', - selector: plugin.api.getSelector('formDataBodySerializer'), + selector: plugin.api.selector('formDataBodySerializer'), }); plugin.registerSymbol({ external: clientModule, name: 'urlSearchParamsBodySerializer', - selector: plugin.api.getSelector('urlSearchParamsBodySerializer'), + selector: plugin.api.selector('urlSearchParamsBodySerializer'), }); plugin.registerSymbol({ external: clientModule, name: 'buildClientParams', - selector: plugin.api.getSelector('buildClientParams'), + selector: plugin.api.selector('buildClientParams'), }); const client = getClientPlugin(plugin.context.config); @@ -479,7 +492,7 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'Composable', - selector: plugin.api.getSelector('Composable'), + selector: plugin.api.selector('Composable'), }); } @@ -487,7 +500,7 @@ export const handler: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: '@angular/core', name: 'Injectable', - selector: plugin.api.getSelector('Injectable'), + selector: plugin.api.selector('Injectable'), }); } diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/signature.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/signature.ts index 32f03b753..fbd6042e6 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/signature.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/signature.ts @@ -1,4 +1,4 @@ -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; type Location = keyof IR.ParametersObject | 'body'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts index 15ddccb17..50efaf4c0 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/typeOptions.ts @@ -1,6 +1,7 @@ -import { clientFolderAbsolutePath } from '../../../generate/client'; -import { tsc } from '../../../tsc'; -import { getClientPlugin } from '../client-core/utils'; +import { clientFolderAbsolutePath } from '~/generate/client'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { tsc } from '~/tsc'; + import { nuxtTypeDefault, nuxtTypeResponse } from './constants'; import type { HeyApiSdkPlugin } from './types'; @@ -26,7 +27,7 @@ export const createTypeOptions = ({ kind: 'type', }, name: 'Client', - selector: plugin.api.getSelector('Client'), + selector: plugin.api.selector('Client'), }); const symbolClientOptions = plugin.registerSymbol({ external: clientModule, @@ -41,7 +42,7 @@ export const createTypeOptions = ({ kind: 'type', }, name: 'Options', - selector: plugin.api.getSelector('Options'), + selector: plugin.api.selector('Options'), }); const typeOptions = tsc.typeAliasDeclaration({ @@ -102,7 +103,7 @@ export const createTypeOptions = ({ tsc.typeParameterDeclaration({ constraint: tsc.typeReferenceNode({ typeName: plugin.referenceSymbol( - plugin.api.getSelector('Composable'), + plugin.api.selector('Composable'), ).placeholder, }), defaultType: tsc.typeNode("'$fetch'"), diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts index ffd5d7ad7..54994441e 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts @@ -1,12 +1,13 @@ -import type { IR } from '../../../ir/types'; -import type { StringName } from '../../../types/case'; -import type { Operation } from '../../../types/client'; +import type { IR } from '~/ir/types'; import type { DefinePlugin, Plugin, PluginClientNames, PluginValidatorNames, -} from '../../types'; +} from '~/plugins/types'; +import type { StringName } from '~/types/case'; +import type { Operation } from '~/types/client'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@hey-api/sdk'> & diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/validator.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/validator.ts index 714207fb9..1819ca02c 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/validator.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/validator.ts @@ -1,6 +1,7 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; + import type { HeyApiSdkPlugin } from './types'; interface ValidatorProps { diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts index 920c22e80..5b9a9a449 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'response' | 'response-ref'; @@ -12,13 +12,13 @@ export type IApi = { * - `response-ref`: `$ref` JSON pointer * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/transformers'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts index 53a7ef0ae..4bbf54984 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { bigIntExpressions, dateExpressions } from './expressions'; import { handler } from './plugin'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/expressions.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/expressions.ts index 4e1e94971..070bd06a9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/expressions.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/expressions.ts @@ -1,7 +1,8 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { tsc } from '~/tsc'; + import type { UserConfig } from './types'; export type ExpressionTransformer = ({ diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin-legacy.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin-legacy.ts index 2d5902c17..ae6df43c9 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin-legacy.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin-legacy.ts @@ -1,16 +1,20 @@ import type ts from 'typescript'; -import { createOperationKey } from '../../../ir/operation'; -import { tsc } from '../../../tsc'; -import type { ModelMeta, OperationResponse } from '../../../types/client'; -import { getConfig } from '../../../utils/config'; -import { isModelDate, unsetUniqueTypeName } from '../../../utils/type'; +import { createOperationKey } from '~/ir/operation'; import { modelResponseTransformerTypeName, operationResponseTransformerTypeName, operationResponseTypeName, -} from '../sdk/plugin-legacy'; -import { generateType, type TypesProps } from '../typescript/plugin-legacy'; +} from '~/plugins/@hey-api/sdk/plugin-legacy'; +import { + generateType, + type TypesProps, +} from '~/plugins/@hey-api/typescript/plugin-legacy'; +import { tsc } from '~/tsc'; +import type { ModelMeta, OperationResponse } from '~/types/client'; +import { getConfig } from '~/utils/config'; +import { isModelDate, unsetUniqueTypeName } from '~/utils/type'; + import type { HeyApiTransformersPlugin } from './types'; interface ModelProps extends TypesProps { diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts index aa203c16f..6fdfca657 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/plugin.ts @@ -1,13 +1,11 @@ import ts from 'typescript'; -import { - createOperationKey, - operationResponsesMap, -} from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { refToName } from '../../../utils/ref'; +import { createOperationKey, operationResponsesMap } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; + import type { HeyApiTransformersPlugin } from './types'; const dataVariableName = 'data'; @@ -60,7 +58,7 @@ const processSchemaType = ({ schema: IR.SchemaObject; }): Array => { if (schema.$ref) { - const selector = plugin.api.getSelector('response-ref', schema.$ref); + const selector = plugin.api.selector('response-ref', schema.$ref); if (!plugin.getSymbol(selector)) { const symbol = plugin.registerSymbol({ @@ -322,65 +320,77 @@ const processSchemaType = ({ // handles only response transformers for now export const handler: HeyApiTransformersPlugin['Handler'] = ({ plugin }) => { - plugin.forEach('operation', ({ operation }) => { - const { response } = operationResponsesMap(operation); - if (!response) return; + plugin.forEach( + 'operation', + ({ operation }) => { + const { response } = operationResponsesMap(operation); + if (!response) return; - if (response.items && response.items.length > 1) { - if (plugin.context.config.logs.level === 'debug') { - console.warn( - `❗️ Transformers warning: route ${createOperationKey(operation)} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, - ); + if (response.items && response.items.length > 1) { + if (plugin.context.config.logs.level === 'debug') { + console.warn( + `❗️ Transformers warning: route ${createOperationKey(operation)} has ${response.items.length} non-void success responses. This is currently not handled and we will not generate a response transformer. Please open an issue if you'd like this feature https://github.com/hey-api/openapi-ts/issues`, + ); + } + return; } - return; - } - const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); - const symbolResponse = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), - ); - if (!symbolResponse) return; - - const symbolResponseTransformer = plugin.registerSymbol({ - exported: true, - name: buildName({ - config: { - case: 'camelCase', - name: '{{name}}ResponseTransformer', - }, - name: operation.id, - }), - selector: plugin.api.getSelector('response', operation.id), - }); + const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); + const symbolResponse = plugin.getSymbol( + pluginTypeScript.api.selector('response', operation.id), + ); + if (!symbolResponse) return; - // TODO: parser - consider handling simple string response which is also a date - const nodes = schemaResponseTransformerNodes({ plugin, schema: response }); - if (nodes.length) { - const responseTransformerNode = tsc.constVariable({ - exportConst: symbolResponseTransformer.exported, - expression: tsc.arrowFunction({ - async: true, - multiLine: true, - parameters: [ - { - name: dataVariableName, - // TODO: parser - add types, generate types without transforms - type: tsc.keywordTypeNode({ keyword: 'any' }), - }, - ], - returnType: tsc.typeReferenceNode({ - typeArguments: [ - tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), - ], - typeName: 'Promise', - }), - statements: ensureStatements(nodes), + const symbolResponseTransformer = plugin.registerSymbol({ + exported: true, + name: buildName({ + config: { + case: 'camelCase', + name: '{{name}}ResponseTransformer', + }, + name: operation.id, }), - name: symbolResponseTransformer.placeholder, + selector: plugin.api.selector('response', operation.id), }); - plugin.setSymbolValue(symbolResponseTransformer, responseTransformerNode); - } else { - plugin.setSymbolValue(symbolResponseTransformer, null); - } - }); + + // TODO: parser - consider handling simple string response which is also a date + const nodes = schemaResponseTransformerNodes({ + plugin, + schema: response, + }); + if (nodes.length) { + const responseTransformerNode = tsc.constVariable({ + exportConst: symbolResponseTransformer.exported, + expression: tsc.arrowFunction({ + async: true, + multiLine: true, + parameters: [ + { + name: dataVariableName, + // TODO: parser - add types, generate types without transforms + type: tsc.keywordTypeNode({ keyword: 'any' }), + }, + ], + returnType: tsc.typeReferenceNode({ + typeArguments: [ + tsc.typeReferenceNode({ typeName: symbolResponse.placeholder }), + ], + typeName: 'Promise', + }), + statements: ensureStatements(nodes), + }), + name: symbolResponseTransformer.placeholder, + }); + plugin.setSymbolValue( + symbolResponseTransformer, + responseTransformerNode, + ); + } else { + plugin.setSymbolValue(symbolResponseTransformer, null); + } + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@hey-api/transformers/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/transformers/types.d.ts index 5ac02deac..66efda569 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/transformers/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/transformers/types.d.ts @@ -1,7 +1,8 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; import type { ExpressionTransformer } from './expressions'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts index 2d0e38fc8..b1935c695 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/__tests__/plugin.test.ts @@ -4,11 +4,12 @@ import { Project } from '@hey-api/codegen-core'; import type ts from 'typescript'; import { describe, expect, it, vi } from 'vitest'; -import { openApi } from '../../../../generate/__tests__/mocks'; -import { GeneratedFile } from '../../../../generate/file'; -import type { Config } from '../../../../types/config'; -import { setConfig } from '../../../../utils/config'; -import { PluginInstance } from '../../../shared/utils/instance'; +import { openApi } from '~/generate/__tests__/mocks'; +import { GeneratedFile } from '~/generate/file'; +import { PluginInstance } from '~/plugins/shared/utils/instance'; +import type { Config } from '~/types/config'; +import { setConfig } from '~/utils/config'; + import { handlerLegacy } from '../plugin-legacy'; vi.mock('node:fs'); @@ -88,7 +89,7 @@ describe('generateLegacyTypes', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -100,7 +101,7 @@ describe('generateLegacyTypes', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -111,8 +112,8 @@ describe('generateLegacyTypes', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts index 9ddaef171..3b453b007 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/api.ts @@ -1,7 +1,8 @@ import type { Selector } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; + import { schemaToType } from './plugin'; type SelectorType = @@ -18,6 +19,7 @@ type SelectorType = | 'Webhooks'; export type IApi = { + schemaToType: (args: Parameters[0]) => ts.TypeNode; /** * @param type Selector type. * @param value Depends on `type`: @@ -34,14 +36,13 @@ export type IApi = { * - `Webhooks`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; - schemaToType: (args: Parameters[0]) => ts.TypeNode; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@hey-api/typescript'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/clientOptions.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/clientOptions.ts index eafcc5e40..e5a032432 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/clientOptions.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/clientOptions.ts @@ -1,10 +1,14 @@ import type { Symbol } from '@hey-api/codegen-core'; import ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { tsc } from '../../../tsc'; -import { parseUrl } from '../../../utils/url'; -import { getClientBaseUrlKey, getClientPlugin } from '../client-core/utils'; +import type { IR } from '~/ir/types'; +import { + getClientBaseUrlKey, + getClientPlugin, +} from '~/plugins/@hey-api/client-core/utils'; +import { tsc } from '~/tsc'; +import { parseUrl } from '~/utils/url'; + import type { HeyApiTypeScriptPlugin } from './types'; const stringType = tsc.keywordTypeNode({ keyword: 'string' }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/config.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/config.ts index ac3575b97..4959c9ab6 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import { handlerLegacy } from './plugin-legacy'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/export.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/export.ts index 13171b12c..b84ed2e67 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/export.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/export.ts @@ -1,11 +1,12 @@ import type { Symbol } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { tsc } from '../../../tsc'; -import { numberRegExp } from '../../../utils/regexp'; -import { stringCase } from '../../../utils/stringCase'; -import { createSchemaComment } from '../../shared/utils/schema'; +import type { IR } from '~/ir/types'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; +import { stringCase } from '~/utils/stringCase'; + import type { HeyApiTypeScriptPlugin } from './types'; const schemaToEnumObject = ({ diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts index b2be1fa9a..490275976 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/operation.ts @@ -1,8 +1,9 @@ -import { operationResponsesMap } from '../../../ir/operation'; -import { deduplicateSchema } from '../../../ir/schema'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import { operationResponsesMap } from '~/ir/operation'; +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { tsc } from '~/tsc'; + import { schemaToType } from './plugin'; import type { HeyApiTypeScriptPlugin } from './types'; @@ -128,7 +129,7 @@ const operationToDataType = ({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); const type = schemaToType({ plugin, @@ -164,7 +165,7 @@ export const operationToType = ({ config: plugin.config.errors, name: operation.id, }), - selector: plugin.api.getSelector('errors', operation.id), + selector: plugin.api.selector('errors', operation.id), }); const type = schemaToType({ plugin, @@ -190,7 +191,7 @@ export const operationToType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('error', operation.id), + selector: plugin.api.selector('error', operation.id), }); const type = tsc.indexedAccessTypeNode({ indexType: tsc.typeOperatorNode({ @@ -220,7 +221,7 @@ export const operationToType = ({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); const type = schemaToType({ plugin, @@ -246,7 +247,7 @@ export const operationToType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('response', operation.id), + selector: plugin.api.selector('response', operation.id), }); const type = tsc.indexedAccessTypeNode({ indexType: tsc.typeOperatorNode({ diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin-legacy.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin-legacy.ts index 5c7ab2f02..cc368fe34 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin-legacy.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin-legacy.ts @@ -1,28 +1,24 @@ import type ts from 'typescript'; -import { GeneratedFile } from '../../../generate/file'; -import { isOperationParameterRequired } from '../../../openApi'; -import { type Comments, tsc } from '../../../tsc'; -import type { - Client, - Method, - Model, - OperationParameter, -} from '../../../types/client'; -import { getConfig, isLegacyClient } from '../../../utils/config'; -import { enumEntry, enumUnionType } from '../../../utils/enum'; -import { escapeComment } from '../../../utils/escape'; -import { sortByName, sorterByName } from '../../../utils/sort'; -import { - setUniqueTypeName, - type SetUniqueTypeNameResult, - toType, -} from '../../../utils/type'; +import { GeneratedFile } from '~/generate/file'; +import { isOperationParameterRequired } from '~/openApi'; import { operationDataTypeName, operationErrorTypeName, operationResponseTypeName, -} from '../sdk/plugin-legacy'; +} from '~/plugins/@hey-api/sdk/plugin-legacy'; +import { type Comments, tsc } from '~/tsc'; +import type { Client, Method, Model, OperationParameter } from '~/types/client'; +import { getConfig, isLegacyClient } from '~/utils/config'; +import { enumEntry, enumUnionType } from '~/utils/enum'; +import { escapeComment } from '~/utils/escape'; +import { sortByName, sorterByName } from '~/utils/sort'; +import { + setUniqueTypeName, + type SetUniqueTypeNameResult, + toType, +} from '~/utils/type'; + import type { HeyApiTypeScriptPlugin } from './types'; export interface TypesProps { diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts index 78ac210c8..4907d28a2 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts @@ -1,15 +1,16 @@ import type ts from 'typescript'; -import { deduplicateSchema } from '../../../ir/schema'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import type { Property } from '../../../tsc'; -import { tsc } from '../../../tsc'; -import { refToName } from '../../../utils/ref'; -import { stringCase } from '../../../utils/stringCase'; -import { fieldName } from '../../shared/utils/case'; -import { createSchemaComment } from '../../shared/utils/schema'; -import type { SchemaWithType } from '../../zod/shared/types'; +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { fieldName } from '~/plugins/shared/utils/case'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import type { Property } from '~/tsc'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; +import { stringCase } from '~/utils/stringCase'; + import { createClientOptions } from './clientOptions'; import { exportType } from './export'; import { operationToType } from './operation'; @@ -262,9 +263,9 @@ const stringTypeToIdentifier = ({ parts.pop(); // remove the ID part const type = parts.join('_'); - const selector = plugin.api.getSelector('TypeID', type); + const selector = plugin.api.selector('TypeID', type); if (!plugin.getSymbol(selector)) { - const selectorTypeId = plugin.api.getSelector('TypeID'); + const selectorTypeId = plugin.api.selector('TypeID'); if (!plugin.getSymbol(selectorTypeId)) { const symbolTypeId = plugin.registerSymbol({ @@ -447,7 +448,7 @@ export const schemaToType = ({ }): ts.TypeNode => { if (schema.$ref) { const symbol = plugin.referenceSymbol( - plugin.api.getSelector('ref', schema.$ref), + plugin.api.selector('ref', schema.$ref), ); return tsc.typeReferenceNode({ typeName: symbol.placeholder }); } @@ -506,7 +507,7 @@ const handleComponent = ({ config: plugin.config.definitions, name: refToName(id), }), - selector: plugin.api.getSelector('ref', id), + selector: plugin.api.selector('ref', id), }); exportType({ plugin, @@ -529,7 +530,7 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { }, name: 'ClientOptions', }), - selector: plugin.api.getSelector('ClientOptions'), + selector: plugin.api.selector('ClientOptions'), }); // reserve identifier for Webhooks const symbolWebhooks = plugin.registerSymbol({ @@ -543,7 +544,7 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { }, name: 'Webhooks', }), - selector: plugin.api.getSelector('Webhooks'), + selector: plugin.api.selector('Webhooks'), }); const servers: Array = []; @@ -595,6 +596,9 @@ export const handler: HeyApiTypeScriptPlugin['Handler'] = ({ plugin }) => { break; } }, + { + order: 'declarations', + }, ); createClientOptions({ plugin, servers, symbolClientOptions }); diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/types.d.ts index 5722a2fe5..fd1659c37 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/types.d.ts @@ -1,5 +1,6 @@ -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type EnumsType = 'javascript' | 'typescript' | 'typescript-const'; diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts index 003f6238d..d1874fae1 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhook.ts @@ -1,7 +1,8 @@ -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { createSchemaComment } from '../../shared/utils/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import { tsc } from '~/tsc'; + import { schemaToType } from './plugin'; import type { HeyApiTypeScriptPlugin } from './types'; @@ -34,7 +35,7 @@ const operationToDataType = ({ }, name: operation.id, }), - selector: plugin.api.getSelector('webhook-payload', operation.id), + selector: plugin.api.selector('webhook-payload', operation.id), }); const type = schemaToType({ plugin, @@ -55,7 +56,7 @@ const operationToDataType = ({ }, name: symbolWebhookPayload.name, placeholder: symbolWebhookPayload.placeholder, - selector: plugin.api.getSelector('ref', symbolWebhookPayload.placeholder), + selector: plugin.api.selector('ref', symbolWebhookPayload.placeholder), }); data.properties.body = { $ref: symbolWebhookPayload.placeholder }; dataRequired.push('body'); @@ -83,7 +84,7 @@ const operationToDataType = ({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); const type = schemaToType({ plugin, diff --git a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhooks.ts b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhooks.ts index 4539a468e..242373b74 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/typescript/webhooks.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/typescript/webhooks.ts @@ -1,6 +1,7 @@ import type { Symbol } from '@hey-api/codegen-core'; -import { tsc } from '../../../tsc'; +import { tsc } from '~/tsc'; + import type { HeyApiTypeScriptPlugin } from './types'; export const createWebhooks = ({ diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/api.ts b/packages/openapi-ts/src/plugins/@pinia/colada/api.ts index 171c8310c..e2eb81a04 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/api.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | '_JSONValue' @@ -28,13 +28,13 @@ export type IApi = { * - `UseQueryOptions`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@pinia/colada'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/config.ts b/packages/openapi-ts/src/plugins/@pinia/colada/config.ts index e420831ff..6d3804572 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/config.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import type { PiniaColadaPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/meta.ts b/packages/openapi-ts/src/plugins/@pinia/colada/meta.ts index a2755448e..b07ba694c 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/meta.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/meta.ts @@ -1,7 +1,8 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { tsc } from '~/tsc'; + import type { PiniaColadaPlugin } from './types'; export const handleMeta = ( diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts index 59ce43f54..b8f308c24 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/mutationOptions.ts @@ -1,8 +1,9 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { tsc } from '~/tsc'; + import { handleMeta } from './meta'; import type { PiniaColadaPlugin } from './types'; import { useTypeData, useTypeError, useTypeResponse } from './useType'; @@ -18,7 +19,7 @@ export const createMutationOptions = ({ queryFn: string; }): void => { const symbolMutationOptionsType = plugin.referenceSymbol( - plugin.api.getSelector('UseMutationOptions'), + plugin.api.selector('UseMutationOptions'), ); const typeData = useTypeData({ operation, plugin }); diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts b/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts index 4de223285..b0ccc056d 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/plugin.ts @@ -1,5 +1,6 @@ -import { stringCase } from '../../../utils/stringCase'; -import { operationClasses } from '../../@hey-api/sdk/operation'; +import { operationClasses } from '~/plugins/@hey-api/sdk/operation'; +import { stringCase } from '~/utils/stringCase'; + import { createMutationOptions } from './mutationOptions'; import { createQueryOptions } from './queryOptions'; import type { PiniaColadaPlugin } from './types'; @@ -11,7 +12,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'UseMutationOptions', - selector: plugin.api.getSelector('UseMutationOptions'), + selector: plugin.api.selector('UseMutationOptions'), }); plugin.registerSymbol({ external: plugin.name, @@ -19,7 +20,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'UseQueryOptions', - selector: plugin.api.getSelector('UseQueryOptions'), + selector: plugin.api.selector('UseQueryOptions'), }); plugin.registerSymbol({ external: plugin.name, @@ -27,7 +28,7 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: '_JSONValue', - selector: plugin.api.getSelector('_JSONValue'), + selector: plugin.api.selector('_JSONValue'), }); plugin.registerSymbol({ external: 'axios', @@ -35,52 +36,58 @@ export const handler: PiniaColadaPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'AxiosError', - selector: plugin.api.getSelector('AxiosError'), + selector: plugin.api.selector('AxiosError'), }); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const classes = sdkPlugin.config.asClass - ? operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }) - : undefined; - const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol( - sdkPlugin.api.getSelector('class', entry.path[0]), - ).placeholder, - ...entry.path.slice(1).map((className: string) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol( - sdkPlugin.api.getSelector('function', operation.id), - ).placeholder; + plugin.forEach( + 'operation', + ({ operation }) => { + const classes = sdkPlugin.config.asClass + ? operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }) + : undefined; + const entry = classes ? classes.values().next().value : undefined; + const queryFn = + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + entry + ? [ + plugin.referenceSymbol( + sdkPlugin.api.selector('class', entry.path[0]), + ).placeholder, + ...entry.path.slice(1).map((className: string) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.') + : plugin.referenceSymbol( + sdkPlugin.api.selector('function', operation.id), + ).placeholder; - if (plugin.hooks.operation.isQuery(operation)) { - if (plugin.config.queryOptions.enabled) { - createQueryOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isQuery(operation)) { + if (plugin.config.queryOptions.enabled) { + createQueryOptions({ operation, plugin, queryFn }); + } } - } - if (plugin.hooks.operation.isMutation(operation)) { - if (plugin.config.mutationOptions.enabled) { - createMutationOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isMutation(operation)) { + if (plugin.config.mutationOptions.enabled) { + createMutationOptions({ operation, plugin, queryFn }); + } } - } - }); + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts b/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts index ea619179e..4e649b21b 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/queryKey.ts @@ -1,15 +1,16 @@ import type { Symbol } from '@hey-api/codegen-core'; import type { Expression } from 'typescript'; -import { clientFolderAbsolutePath } from '../../../generate/client'; -import { hasOperationDataRequired } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { type Property, tsc } from '../../../tsc'; +import { clientFolderAbsolutePath } from '~/generate/client'; +import { hasOperationDataRequired } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; import { getClientBaseUrlKey, getClientPlugin, -} from '../../@hey-api/client-core/utils'; +} from '~/plugins/@hey-api/client-core/utils'; +import { type Property, tsc } from '~/tsc'; + import type { PiniaColadaPlugin } from './types'; import { useTypeData } from './useType'; import { getPublicTypeData } from './utils'; @@ -30,13 +31,13 @@ export const createQueryKeyFunction = ({ }, name: 'createQueryKey', }), - selector: plugin.api.getSelector('createQueryKey'), + selector: plugin.api.selector('createQueryKey'), }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const symbolJsonValue = plugin.referenceSymbol( - plugin.api.getSelector('_JSONValue'), + plugin.api.selector('_JSONValue'), ); const returnType = tsc.indexedAccessTypeNode({ @@ -53,14 +54,14 @@ export const createQueryKeyFunction = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; @@ -289,7 +290,7 @@ const createQueryKeyLiteral = ({ } const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); const createQueryKeyCallExpression = tsc.callExpression({ functionName: symbolCreateQueryKey.placeholder, @@ -304,7 +305,7 @@ export const createQueryKeyType = ({ plugin: PiniaColadaPlugin['Instance']; }) => { const symbolJsonValue = plugin.referenceSymbol( - plugin.api.getSelector('_JSONValue'), + plugin.api.selector('_JSONValue'), ); const properties: Array = [ @@ -333,13 +334,13 @@ export const createQueryKeyType = ({ const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolQueryKeyType = plugin.registerSymbol({ exported: true, meta: { kind: 'type' }, name: 'QueryKey', - selector: plugin.api.getSelector('QueryKey'), + selector: plugin.api.selector('QueryKey'), }); const queryKeyType = tsc.typeAliasDeclaration({ exportType: symbolQueryKeyType.exported, diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts b/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts index 33a696d28..804f168f7 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/queryOptions.ts @@ -1,12 +1,13 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; import { hasOperationSse, isOperationOptionsRequired, -} from '../../shared/utils/operation'; +} from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; + import { handleMeta } from './meta'; import { createQueryKeyFunction, @@ -37,7 +38,7 @@ export const createQueryOptions = ({ context: plugin.context, operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } @@ -63,7 +64,7 @@ export const createQueryOptions = ({ }); } else { const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); // Optionally include tags when configured let tagsExpr: ts.Expression | undefined; @@ -155,12 +156,12 @@ export const createQueryOptions = ({ config: plugin.config.queryOptions, name: operation.id, }), - selector: plugin.api.getSelector('queryOptionsFn', operation.id), + selector: plugin.api.selector('queryOptionsFn', operation.id), }); const symbolDefineQueryOptions = plugin.registerSymbol({ external: plugin.name, name: 'defineQueryOptions', - selector: plugin.api.getSelector('defineQueryOptions'), + selector: plugin.api.selector('defineQueryOptions'), }); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/types.d.ts b/packages/openapi-ts/src/plugins/@pinia/colada/types.d.ts index a69bd0445..efc2d3fd4 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/types.d.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@pinia/colada'> & diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts b/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts index 245051b8a..e225c2dc8 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/useType.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import { getClientPlugin } from '../../@hey-api/client-core/utils'; -import { operationOptionsType } from '../../@hey-api/sdk/operation'; +import type { IR } from '~/ir/types'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { operationOptionsType } from '~/plugins/@hey-api/sdk/operation'; + import type { PiniaColadaPlugin } from './types'; export const useTypeData = ({ @@ -26,7 +27,7 @@ export const useTypeError = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('error', operation.id), + pluginTypeScript.api.selector('error', operation.id), ); let typeErrorName: string | undefined = symbolErrorType?.placeholder; @@ -34,7 +35,7 @@ export const useTypeError = ({ typeErrorName = 'Error'; } if (client.name === '@hey-api/client-axios') { - const symbol = plugin.referenceSymbol(plugin.api.getSelector('AxiosError')); + const symbol = plugin.referenceSymbol(plugin.api.selector('AxiosError')); typeErrorName = `${symbol.placeholder}<${typeErrorName}>`; } return typeErrorName; @@ -49,7 +50,7 @@ export const useTypeResponse = ({ }): string => { const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); return symbolResponseType?.placeholder || 'unknown'; }; diff --git a/packages/openapi-ts/src/plugins/@pinia/colada/utils.ts b/packages/openapi-ts/src/plugins/@pinia/colada/utils.ts index a5757b680..9164b3ee7 100644 --- a/packages/openapi-ts/src/plugins/@pinia/colada/utils.ts +++ b/packages/openapi-ts/src/plugins/@pinia/colada/utils.ts @@ -1,4 +1,5 @@ -import { getClientPlugin } from '../../@hey-api/client-core/utils'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; + import type { PiniaColadaPlugin } from './types'; export const getPublicTypeData = ({ diff --git a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts index 9086df45f..450cafabc 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'AxiosError' @@ -32,7 +32,7 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { @@ -40,7 +40,7 @@ export class Api implements IApi { public meta: Plugin.Name<'@tanstack/angular-query-experimental'>, ) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/config.ts b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/config.ts index 42a1d2f05..873364817 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/config.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/config.ts @@ -1,6 +1,7 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { handler } from '../query-core/plugin'; -import { handlerLegacy } from '../query-core/plugin-legacy'; +import { handler } from '~/plugins/@tanstack/query-core/plugin'; +import { handlerLegacy } from '~/plugins/@tanstack/query-core/plugin-legacy'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { TanStackAngularQueryPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts index 76d319b8f..80334a301 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@tanstack/angular-query-experimental'> & diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts index 66d566b4a..db1cbe65b 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts @@ -1,11 +1,12 @@ import ts from 'typescript'; -import { operationPagination } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { tsNodeToString } from '../../../tsc/utils'; -import { isOperationOptionsRequired } from '../../shared/utils/operation'; +import { operationPagination } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { isOperationOptionsRequired } from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; +import { tsNodeToString } from '~/tsc/utils'; + import { handleMeta } from './meta'; import { createQueryKeyFunction, @@ -27,7 +28,7 @@ const createInfiniteParamsFunction = ({ }, name: 'createInfiniteParams', }), - selector: plugin.api.getSelector('createInfiniteParams'), + selector: plugin.api.selector('createInfiniteParams'), }); const fn = tsc.constVariable({ @@ -238,20 +239,20 @@ export const createInfiniteQueryOptions = ({ operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } - if (!plugin.getSymbol(plugin.api.getSelector('createInfiniteParams'))) { + if (!plugin.getSymbol(plugin.api.selector('createInfiniteParams'))) { createInfiniteParamsFunction({ plugin }); } const symbolInfiniteQueryOptions = plugin.referenceSymbol( - plugin.api.getSelector('infiniteQueryOptions'), + plugin.api.selector('infiniteQueryOptions'), ); const symbolInfiniteDataType = plugin.referenceSymbol( - plugin.api.getSelector('InfiniteData'), + plugin.api.selector('InfiniteData'), ); const typeData = useTypeData({ operation, plugin }); @@ -259,7 +260,7 @@ export const createInfiniteQueryOptions = ({ const typeResponse = useTypeResponse({ operation, plugin }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const typeQueryKey = `${symbolQueryKeyType.placeholder}<${typeData}>`; const typePageObjectParam = `Pick<${typeQueryKey}[0], 'body' | 'headers' | 'path' | 'query'>`; @@ -322,7 +323,7 @@ export const createInfiniteQueryOptions = ({ }); const symbolCreateInfiniteParams = plugin.referenceSymbol( - plugin.api.getSelector('createInfiniteParams'), + plugin.api.selector('createInfiniteParams'), ); const statements: Array = [ diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/meta.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/meta.ts index f51c356ff..a5bbb901e 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/meta.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/meta.ts @@ -1,7 +1,8 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { tsc } from '~/tsc'; + import type { PluginInstance } from './types'; export const handleMeta = ( diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts index 6084b516c..0d9269f8c 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/mutationOptions.ts @@ -1,8 +1,9 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { tsc } from '~/tsc'; + import { handleMeta } from './meta'; import type { PluginInstance } from './types'; import { useTypeData, useTypeError, useTypeResponse } from './useType'; @@ -17,7 +18,7 @@ export const createMutationOptions = ({ queryFn: string; }): void => { const symbolMutationOptionsType = plugin.referenceSymbol( - plugin.api.getSelector('MutationOptions'), + plugin.api.selector('MutationOptions'), ); const typeData = useTypeData({ operation, plugin }); diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin-legacy.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin-legacy.ts index 5825d1a46..977ca4334 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin-legacy.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin-legacy.ts @@ -1,29 +1,15 @@ import ts from 'typescript'; -import { clientModulePath } from '../../../generate/client'; -import { relativeModulePath } from '../../../generate/utils'; -import { createOperationKey } from '../../../ir/operation'; -import { getPaginationKeywordsRegExp } from '../../../ir/pagination'; -import type { IR } from '../../../ir/types'; -import { isOperationParameterRequired } from '../../../openApi'; -import { type Property, tsc } from '../../../tsc'; -import type { ImportExportItem } from '../../../tsc/module'; -import type { ImportExportItemObject } from '../../../tsc/utils'; -import type { - Client, - Method, - Model, - Operation, - OperationParameter, -} from '../../../types/client'; -import type { Config } from '../../../types/config'; -import type { Files } from '../../../types/utils'; -import { getConfig, isLegacyClient } from '../../../utils/config'; -import { transformClassName } from '../../../utils/transform'; +import { clientModulePath } from '~/generate/client'; +import { relativeModulePath } from '~/generate/utils'; +import { createOperationKey } from '~/ir/operation'; +import { getPaginationKeywordsRegExp } from '~/ir/pagination'; +import type { IR } from '~/ir/types'; +import { isOperationParameterRequired } from '~/openApi'; import { getClientBaseUrlKey, getClientPlugin, -} from '../../@hey-api/client-core/utils'; +} from '~/plugins/@hey-api/client-core/utils'; import { generateImport, operationDataTypeName, @@ -31,12 +17,26 @@ import { operationOptionsLegacyParserType, operationResponseTypeName, serviceFunctionIdentifier, -} from '../../@hey-api/sdk/plugin-legacy'; -import type { TanStackAngularQueryPlugin } from '../angular-query-experimental'; -import type { TanStackReactQueryPlugin } from '../react-query'; -import type { TanStackSolidQueryPlugin } from '../solid-query'; -import type { TanStackSvelteQueryPlugin } from '../svelte-query'; -import type { TanStackVueQueryPlugin } from '../vue-query'; +} from '~/plugins/@hey-api/sdk/plugin-legacy'; +import type { TanStackAngularQueryPlugin } from '~/plugins/@tanstack/angular-query-experimental'; +import type { TanStackReactQueryPlugin } from '~/plugins/@tanstack/react-query'; +import type { TanStackSolidQueryPlugin } from '~/plugins/@tanstack/solid-query'; +import type { TanStackSvelteQueryPlugin } from '~/plugins/@tanstack/svelte-query'; +import type { TanStackVueQueryPlugin } from '~/plugins/@tanstack/vue-query'; +import { type Property, tsc } from '~/tsc'; +import type { ImportExportItem } from '~/tsc/module'; +import type { ImportExportItemObject } from '~/tsc/utils'; +import type { + Client, + Method, + Model, + Operation, + OperationParameter, +} from '~/types/client'; +import type { Config } from '~/types/config'; +import type { Files } from '~/types/utils'; +import { getConfig, isLegacyClient } from '~/utils/config'; +import { transformClassName } from '~/utils/transform'; const toInfiniteQueryOptionsName = (operation: Operation) => `${serviceFunctionIdentifier({ diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts index 73baee276..4c773dfc2 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/plugin.ts @@ -1,5 +1,6 @@ -import { stringCase } from '../../../utils/stringCase'; -import { operationClasses } from '../../@hey-api/sdk/operation'; +import { operationClasses } from '~/plugins/@hey-api/sdk/operation'; +import { stringCase } from '~/utils/stringCase'; + import { createInfiniteQueryOptions } from './infiniteQueryOptions'; import { createMutationOptions } from './mutationOptions'; import { createQueryOptions } from './queryOptions'; @@ -13,7 +14,7 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'DefaultError', - selector: plugin.api.getSelector('DefaultError'), + selector: plugin.api.selector('DefaultError'), }); plugin.registerSymbol({ external: plugin.name, @@ -21,7 +22,7 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'InfiniteData', - selector: plugin.api.getSelector('InfiniteData'), + selector: plugin.api.selector('InfiniteData'), }); const mutationsType = plugin.name === '@tanstack/angular-query-experimental' || @@ -35,22 +36,22 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: mutationsType, - selector: plugin.api.getSelector('MutationOptions'), + selector: plugin.api.selector('MutationOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'infiniteQueryOptions', - selector: plugin.api.getSelector('infiniteQueryOptions'), + selector: plugin.api.selector('infiniteQueryOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'queryOptions', - selector: plugin.api.getSelector('queryOptions'), + selector: plugin.api.selector('queryOptions'), }); plugin.registerSymbol({ external: plugin.name, name: 'useQuery', - selector: plugin.api.getSelector('useQuery'), + selector: plugin.api.selector('useQuery'), }); plugin.registerSymbol({ external: 'axios', @@ -58,60 +59,66 @@ export const handler: PluginHandler = ({ plugin }) => { kind: 'type', }, name: 'AxiosError', - selector: plugin.api.getSelector('AxiosError'), + selector: plugin.api.selector('AxiosError'), }); const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); - plugin.forEach('operation', ({ operation }) => { - const classes = sdkPlugin.config.asClass - ? operationClasses({ - context: plugin.context, - operation, - plugin: sdkPlugin, - }) - : undefined; - const entry = classes ? classes.values().next().value : undefined; - const queryFn = - // TODO: this should use class graph to determine correct path string - // as it's really easy to break once we change the class casing - entry - ? [ - plugin.referenceSymbol( - sdkPlugin.api.getSelector('class', entry.path[0]), - ).placeholder, - ...entry.path.slice(1).map((className) => - stringCase({ - case: 'camelCase', - value: className, - }), - ), - entry.methodName, - ] - .filter(Boolean) - .join('.') - : plugin.referenceSymbol( - sdkPlugin.api.getSelector('function', operation.id), - ).placeholder; + plugin.forEach( + 'operation', + ({ operation }) => { + const classes = sdkPlugin.config.asClass + ? operationClasses({ + context: plugin.context, + operation, + plugin: sdkPlugin, + }) + : undefined; + const entry = classes ? classes.values().next().value : undefined; + const queryFn = + // TODO: this should use class graph to determine correct path string + // as it's really easy to break once we change the class casing + entry + ? [ + plugin.referenceSymbol( + sdkPlugin.api.selector('class', entry.path[0]), + ).placeholder, + ...entry.path.slice(1).map((className) => + stringCase({ + case: 'camelCase', + value: className, + }), + ), + entry.methodName, + ] + .filter(Boolean) + .join('.') + : plugin.referenceSymbol( + sdkPlugin.api.selector('function', operation.id), + ).placeholder; - if (plugin.hooks.operation.isQuery(operation)) { - if (plugin.config.queryOptions.enabled) { - createQueryOptions({ operation, plugin, queryFn }); - } + if (plugin.hooks.operation.isQuery(operation)) { + if (plugin.config.queryOptions.enabled) { + createQueryOptions({ operation, plugin, queryFn }); + } - if (plugin.config.infiniteQueryOptions.enabled) { - createInfiniteQueryOptions({ operation, plugin, queryFn }); - } + if (plugin.config.infiniteQueryOptions.enabled) { + createInfiniteQueryOptions({ operation, plugin, queryFn }); + } - if ('useQuery' in plugin.config && plugin.config.useQuery.enabled) { - createUseQuery({ operation, plugin }); + if ('useQuery' in plugin.config && plugin.config.useQuery.enabled) { + createUseQuery({ operation, plugin }); + } } - } - if (plugin.hooks.operation.isMutation(operation)) { - if (plugin.config.mutationOptions.enabled) { - createMutationOptions({ operation, plugin, queryFn }); + if (plugin.hooks.operation.isMutation(operation)) { + if (plugin.config.mutationOptions.enabled) { + createMutationOptions({ operation, plugin, queryFn }); + } } - } - }); + }, + { + order: 'declarations', + }, + ); }; diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts index 08a80d370..434bddbaa 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts @@ -1,14 +1,15 @@ import type { Symbol } from '@hey-api/codegen-core'; import type { Expression } from 'typescript'; -import { hasOperationDataRequired } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { type Property, tsc } from '../../../tsc'; +import { hasOperationDataRequired } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; import { getClientBaseUrlKey, getClientPlugin, -} from '../../@hey-api/client-core/utils'; +} from '~/plugins/@hey-api/client-core/utils'; +import { type Property, tsc } from '~/tsc'; + import type { PluginInstance } from './types'; import { useTypeData } from './useType'; @@ -29,10 +30,10 @@ export const createQueryKeyFunction = ({ }, name: 'createQueryKey', }), - selector: plugin.api.getSelector('createQueryKey'), + selector: plugin.api.selector('createQueryKey'), }); const symbolQueryKeyType = plugin.referenceSymbol( - plugin.api.getSelector('QueryKey'), + plugin.api.selector('QueryKey'), ); const returnType = tsc.indexedAccessTypeNode({ @@ -49,16 +50,16 @@ export const createQueryKeyFunction = ({ const client = getClientPlugin(plugin.context.config); const symbolClient = - client.api && 'getSelector' in client.api + client.api && 'selector' in client.api ? plugin.getSymbol( // @ts-expect-error - client.api.getSelector('client'), + client.api.selector('client'), ) : undefined; const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const fn = tsc.constVariable({ @@ -275,7 +276,7 @@ const createQueryKeyLiteral = ({ } const symbolCreateQueryKey = plugin.referenceSymbol( - plugin.api.getSelector('createQueryKey'), + plugin.api.selector('createQueryKey'), ); const createQueryKeyCallExpression = tsc.callExpression({ functionName: symbolCreateQueryKey.placeholder, @@ -311,7 +312,7 @@ export const createQueryKeyType = ({ plugin }: { plugin: PluginInstance }) => { const sdkPlugin = plugin.getPluginOrThrow('@hey-api/sdk'); const symbolOptions = plugin.referenceSymbol( - sdkPlugin.api.getSelector('Options'), + sdkPlugin.api.selector('Options'), ); const symbolQueryKeyType = plugin.registerSymbol({ exported: true, @@ -319,7 +320,7 @@ export const createQueryKeyType = ({ plugin }: { plugin: PluginInstance }) => { kind: 'type', }, name: 'QueryKey', - selector: plugin.api.getSelector('QueryKey'), + selector: plugin.api.selector('QueryKey'), }); const queryKeyType = tsc.typeAliasDeclaration({ exportType: symbolQueryKeyType.exported, diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts index de68661af..d9392010f 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryOptions.ts @@ -1,12 +1,13 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; import { hasOperationSse, isOperationOptionsRequired, -} from '../../shared/utils/operation'; +} from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; + import { handleMeta } from './meta'; import { createQueryKeyFunction, @@ -36,13 +37,13 @@ export const createQueryOptions = ({ operation, }); - if (!plugin.getSymbol(plugin.api.getSelector('createQueryKey'))) { + if (!plugin.getSymbol(plugin.api.selector('createQueryKey'))) { createQueryKeyType({ plugin }); createQueryKeyFunction({ plugin }); } const symbolQueryOptions = plugin.referenceSymbol( - plugin.api.getSelector('queryOptions'), + plugin.api.selector('queryOptions'), ); const symbolQueryKey = plugin.registerSymbol({ @@ -160,7 +161,7 @@ export const createQueryOptions = ({ config: plugin.config.queryOptions, name: operation.id, }), - selector: plugin.api.getSelector('queryOptionsFn', operation.id), + selector: plugin.api.selector('queryOptionsFn', operation.id), }); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts index d765a8545..10c381ed2 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts @@ -1,8 +1,8 @@ -import type { TanStackAngularQueryPlugin } from '../angular-query-experimental/types'; -import type { TanStackReactQueryPlugin } from '../react-query/types'; -import type { TanStackSolidQueryPlugin } from '../solid-query/types'; -import type { TanStackSvelteQueryPlugin } from '../svelte-query/types'; -import type { TanStackVueQueryPlugin } from '../vue-query/types'; +import type { TanStackAngularQueryPlugin } from '~/plugins/@tanstack/angular-query-experimental/types'; +import type { TanStackReactQueryPlugin } from '~/plugins/@tanstack/react-query/types'; +import type { TanStackSolidQueryPlugin } from '~/plugins/@tanstack/solid-query/types'; +import type { TanStackSvelteQueryPlugin } from '~/plugins/@tanstack/svelte-query/types'; +import type { TanStackVueQueryPlugin } from '~/plugins/@tanstack/vue-query/types'; export interface PluginHandler { (...args: Parameters): void; diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts index 085e72052..d9ee1d191 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/useQuery.ts @@ -1,10 +1,11 @@ -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; import { hasOperationSse, isOperationOptionsRequired, -} from '../../shared/utils/operation'; +} from '~/plugins/shared/utils/operation'; +import { tsc } from '~/tsc'; + import type { PluginInstance } from './types'; import { useTypeData } from './useType'; @@ -36,7 +37,7 @@ export const createUseQuery = ({ }); const symbolUseQuery = plugin.referenceSymbol( - plugin.api.getSelector('useQuery'), + plugin.api.selector('useQuery'), ); const isRequiredOptions = isOperationOptionsRequired({ @@ -46,7 +47,7 @@ export const createUseQuery = ({ const typeData = useTypeData({ operation, plugin }); const symbolQueryOptionsFn = plugin.referenceSymbol( - plugin.api.getSelector('queryOptionsFn', operation.id), + plugin.api.selector('queryOptionsFn', operation.id), ); const statement = tsc.constVariable({ comment: plugin.config.comments diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts index 41f0b6624..573f9b704 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/useType.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import { getClientPlugin } from '../../@hey-api/client-core/utils'; -import { operationOptionsType } from '../../@hey-api/sdk/operation'; +import type { IR } from '~/ir/types'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import { operationOptionsType } from '~/plugins/@hey-api/sdk/operation'; + import type { PluginInstance } from './types'; export const useTypeData = ({ @@ -26,18 +27,16 @@ export const useTypeError = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('error', operation.id), + pluginTypeScript.api.selector('error', operation.id), ); let typeErrorName: string | undefined = symbolErrorType?.placeholder; if (!typeErrorName) { - const symbol = plugin.referenceSymbol( - plugin.api.getSelector('DefaultError'), - ); + const symbol = plugin.referenceSymbol(plugin.api.selector('DefaultError')); typeErrorName = symbol.placeholder; } if (client.name === '@hey-api/client-axios') { - const symbol = plugin.referenceSymbol(plugin.api.getSelector('AxiosError')); + const symbol = plugin.referenceSymbol(plugin.api.selector('AxiosError')); typeErrorName = `${symbol.placeholder}<${typeErrorName}>`; } return typeErrorName; @@ -52,7 +51,7 @@ export const useTypeResponse = ({ }): string => { const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('response', operation.id), + pluginTypeScript.api.selector('response', operation.id), ); return symbolResponseType?.placeholder || 'unknown'; }; diff --git a/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts index 33357cb95..3d70e15b9 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/react-query/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'AxiosError' @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/react-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts b/packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts index 6e6f1b1bf..87135dd47 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts @@ -1,6 +1,7 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { handler } from '../query-core/plugin'; -import { handlerLegacy } from '../query-core/plugin-legacy'; +import { handler } from '~/plugins/@tanstack/query-core/plugin'; +import { handlerLegacy } from '~/plugins/@tanstack/query-core/plugin-legacy'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { TanStackReactQueryPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts index 4a0de3251..4ac7abd75 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@tanstack/react-query'> & diff --git a/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts index b21c391db..ae14ba2b5 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/solid-query/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'AxiosError' @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/solid-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/solid-query/config.ts b/packages/openapi-ts/src/plugins/@tanstack/solid-query/config.ts index a1e6915c3..d62a3c6bf 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/solid-query/config.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/solid-query/config.ts @@ -1,6 +1,7 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { handler } from '../query-core/plugin'; -import { handlerLegacy } from '../query-core/plugin-legacy'; +import { handler } from '~/plugins/@tanstack/query-core/plugin'; +import { handlerLegacy } from '~/plugins/@tanstack/query-core/plugin-legacy'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { TanStackSolidQueryPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts index 2758857b1..3f2bdbb40 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@tanstack/solid-query'> & diff --git a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts index db6b292e6..8be5ab3fa 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'AxiosError' @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/svelte-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/config.ts b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/config.ts index f4c400bae..dfa0b2f4f 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/config.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/config.ts @@ -1,6 +1,7 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { handler } from '../query-core/plugin'; -import { handlerLegacy } from '../query-core/plugin-legacy'; +import { handler } from '~/plugins/@tanstack/query-core/plugin'; +import { handlerLegacy } from '~/plugins/@tanstack/query-core/plugin-legacy'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { TanStackSvelteQueryPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts index 2ff589aec..c6442afb2 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@tanstack/svelte-query'> & diff --git a/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts b/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts index 72a6c77b0..bb4cd391c 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/vue-query/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = | 'AxiosError' @@ -32,13 +32,13 @@ export type IApi = { * - `useQuery`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'@tanstack/vue-query'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/@tanstack/vue-query/config.ts b/packages/openapi-ts/src/plugins/@tanstack/vue-query/config.ts index c07182142..fbff14dea 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/vue-query/config.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/vue-query/config.ts @@ -1,6 +1,7 @@ -import { definePluginConfig } from '../../shared/utils/config'; -import { handler } from '../query-core/plugin'; -import { handlerLegacy } from '../query-core/plugin-legacy'; +import { handler } from '~/plugins/@tanstack/query-core/plugin'; +import { handlerLegacy } from '~/plugins/@tanstack/query-core/plugin-legacy'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import type { TanStackVueQueryPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts index 99562511e..1c546c59b 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts @@ -1,6 +1,7 @@ -import type { IR } from '../../../ir/types'; -import type { StringCase, StringName } from '../../../types/case'; -import type { DefinePlugin, Plugin } from '../../types'; +import type { IR } from '~/ir/types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'@tanstack/vue-query'> & diff --git a/packages/openapi-ts/src/plugins/arktype/api.ts b/packages/openapi-ts/src/plugins/arktype/api.ts new file mode 100644 index 000000000..48d65b53d --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/api.ts @@ -0,0 +1,56 @@ +import type { Selector } from '@hey-api/codegen-core'; +import type ts from 'typescript'; + +import type { Plugin } from '~/plugins/types'; + +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV2, createResponseValidatorV2 } from './v2/api'; + +type SelectorType = + | 'data' + | 'external' + | 'ref' + | 'responses' + | 'type-infer-data' + | 'type-infer-ref' + | 'type-infer-responses' + | 'type-infer-webhook-request' + | 'webhook-request'; + +export type IApi = { + createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; + createResponseValidator: ( + args: ValidatorArgs, + ) => ts.ArrowFunction | undefined; + /** + * @param type Selector type. + * @param value Depends on `type`: + * - `data`: `operation.id` string + * - `external`: external modules + * - `ref`: `$ref` JSON pointer + * - `responses`: `operation.id` string + * - `type-infer-data`: `operation.id` string + * - `type-infer-ref`: `$ref` JSON pointer + * - `type-infer-responses`: `operation.id` string + * - `type-infer-webhook-request`: `operation.id` string + * - `webhook-request`: `operation.id` string + * @returns Selector array + */ + selector: (type: SelectorType, value?: string) => Selector; +}; + +export class Api implements IApi { + constructor(public meta: Plugin.Name<'arktype'>) {} + + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createRequestValidatorV2(args); + } + + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createResponseValidatorV2(args); + } + + selector(...args: ReadonlyArray): Selector { + return [this.meta.name, ...(args as Selector)]; + } +} diff --git a/packages/openapi-ts/src/plugins/arktype/config.ts b/packages/openapi-ts/src/plugins/arktype/config.ts new file mode 100644 index 000000000..36a2107d1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/config.ts @@ -0,0 +1,272 @@ +import { definePluginConfig, mappers } from '~/plugins/shared/utils/config'; + +import { Api } from './api'; +import { handler } from './plugin'; +import type { ArktypePlugin } from './types'; + +export const defaultConfig: ArktypePlugin['Config'] = { + api: new Api({ + name: 'arktype', + }), + config: { + case: 'PascalCase', + comments: true, + exportFromIndex: false, + metadata: false, + }, + handler, + name: 'arktype', + resolveConfig: (plugin, context) => { + plugin.config.types = context.valueToObject({ + defaultValue: { + infer: { + case: 'PascalCase', + enabled: false, + }, + }, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: plugin.config.types, + }); + + plugin.config.definitions = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.definitions, + }); + + plugin.config.requests = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}Data', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}Data', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.requests, + }); + + plugin.config.responses = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}Response', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}Response', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.responses, + }); + + plugin.config.webhooks = context.valueToObject({ + defaultValue: { + case: plugin.config.case ?? 'PascalCase', + enabled: true, + name: '{{name}}WebhookRequest', + types: { + ...plugin.config.types, + infer: { + ...(plugin.config.types.infer as Extract< + typeof plugin.config.types.infer, + Record + >), + name: '{{name}}WebhookRequest', + }, + }, + }, + mappers: { + ...mappers, + object: (fields, defaultValue) => ({ + ...fields, + types: context.valueToObject({ + defaultValue: defaultValue.types!, + mappers: { + object: (fields, defaultValue) => ({ + ...fields, + infer: context.valueToObject({ + defaultValue: { + ...(defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + >), + enabled: + fields.infer !== undefined + ? Boolean(fields.infer) + : ( + defaultValue.infer as Extract< + typeof defaultValue.infer, + Record + > + ).enabled, + }, + mappers, + value: fields.infer, + }), + }), + }, + value: fields.types, + }), + }), + }, + value: plugin.config.webhooks, + }); + }, + tags: ['validator'], +}; + +/** + * Type helper for Arktype plugin, returns {@link Plugin.Config} object + */ +export const defineConfig = definePluginConfig(defaultConfig); diff --git a/packages/openapi-ts/src/plugins/arktype/constants.ts b/packages/openapi-ts/src/plugins/arktype/constants.ts new file mode 100644 index 000000000..4663f2f05 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/constants.ts @@ -0,0 +1,115 @@ +import { tsc } from '~/tsc'; + +export const identifiers = { + keywords: { + false: tsc.identifier({ text: 'false' }), + true: tsc.identifier({ text: 'true' }), + }, + /** + * {@link https://arktype.io/docs/primitives#number Number} + */ + number: { + Infinity: tsc.identifier({ text: 'Infinity' }), + NaN: tsc.identifier({ text: 'NaN' }), + NegativeInfinity: tsc.identifier({ text: 'NegativeInfinity' }), + epoch: tsc.identifier({ text: 'epoch' }), + integer: tsc.identifier({ text: 'integer' }), + safe: tsc.identifier({ text: 'safe' }), + }, + /** + * {@link https://arktype.io/docs/primitives Primitives} + */ + primitives: { + bigint: tsc.identifier({ text: 'bigint' }), + boolean: tsc.identifier({ text: 'boolean' }), + keywords: tsc.identifier({ text: 'keywords' }), + null: 'null', + number: tsc.identifier({ text: 'number' }), + string: 'string', + symbol: tsc.identifier({ text: 'symbol' }), + undefined: tsc.identifier({ text: 'undefined' }), + unit: tsc.identifier({ text: 'unit' }), + }, + /** + * {@link https://arktype.io/docs/primitives#string String} + */ + string: { + NFC: tsc.identifier({ text: 'NFC' }), + NFD: tsc.identifier({ text: 'NFD' }), + NFKC: tsc.identifier({ text: 'NFKC' }), + NFKD: tsc.identifier({ text: 'NFKD' }), + alpha: tsc.identifier({ text: 'alpha' }), + alphanumeric: tsc.identifier({ text: 'alphanumeric' }), + base64: tsc.identifier({ text: 'base64' }), + capitalize: tsc.identifier({ text: 'capitalize' }), + creditCard: tsc.identifier({ text: 'creditCard' }), + date: 'date', + digits: tsc.identifier({ text: 'digits' }), + email: 'email', + epoch: tsc.identifier({ text: 'epoch' }), + hex: tsc.identifier({ text: 'hex' }), + integer: tsc.identifier({ text: 'integer' }), + ip: 'ip', + iso: 'iso', + json: tsc.identifier({ text: 'json' }), + lower: tsc.identifier({ text: 'lower' }), + normalize: tsc.identifier({ text: 'normalize' }), + numeric: tsc.identifier({ text: 'numeric' }), + parse: tsc.identifier({ text: 'parse' }), + preformatted: tsc.identifier({ text: 'preformatted' }), + regex: tsc.identifier({ text: 'regex' }), + semver: tsc.identifier({ text: 'semver' }), + trim: tsc.identifier({ text: 'trim' }), + upper: tsc.identifier({ text: 'upper' }), + url: 'url', + uuid: 'uuid', + v1: 'v1', + v2: 'v2', + v3: 'v3', + v4: 'v4', + v5: 'v5', + v6: 'v6', + v7: 'v7', + v8: 'v8', + }, + /** + * {@link https://arktype.io/docs/type-api Type API} + */ + type: { + $: tsc.identifier({ text: '$' }), + allows: tsc.identifier({ text: 'allows' }), + and: tsc.identifier({ text: 'and' }), + array: tsc.identifier({ text: 'array' }), + as: tsc.identifier({ text: 'as' }), + assert: tsc.identifier({ text: 'assert' }), + brand: tsc.identifier({ text: 'brand' }), + configure: tsc.identifier({ text: 'configure' }), + default: tsc.identifier({ text: 'default' }), + describe: tsc.identifier({ text: 'describe' }), + description: tsc.identifier({ text: 'description' }), + equals: tsc.identifier({ text: 'equals' }), + exclude: tsc.identifier({ text: 'exclude' }), + expression: tsc.identifier({ text: 'expression' }), + extends: tsc.identifier({ text: 'extends' }), + extract: tsc.identifier({ text: 'extract' }), + filter: tsc.identifier({ text: 'filter' }), + from: tsc.identifier({ text: 'from' }), + ifEquals: tsc.identifier({ text: 'ifEquals' }), + ifExtends: tsc.identifier({ text: 'ifExtends' }), + infer: tsc.identifier({ text: 'infer' }), + inferIn: tsc.identifier({ text: 'inferIn' }), + intersect: tsc.identifier({ text: 'intersect' }), + json: tsc.identifier({ text: 'json' }), + meta: tsc.identifier({ text: 'meta' }), + narrow: tsc.identifier({ text: 'narrow' }), + onDeepUndeclaredKey: tsc.identifier({ text: 'onDeepUndeclaredKey' }), + onUndeclaredKey: tsc.identifier({ text: 'onUndeclaredKey' }), + optional: tsc.identifier({ text: 'optional' }), + or: tsc.identifier({ text: 'or' }), + overlaps: tsc.identifier({ text: 'overlaps' }), + pipe: tsc.identifier({ text: 'pipe' }), + select: tsc.identifier({ text: 'select' }), + to: tsc.identifier({ text: 'to' }), + toJsonSchema: tsc.identifier({ text: 'toJsonSchema' }), + }, +}; diff --git a/packages/openapi-ts/src/plugins/arktype/index.ts b/packages/openapi-ts/src/plugins/arktype/index.ts new file mode 100644 index 000000000..3325c5553 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/index.ts @@ -0,0 +1,2 @@ +export { defaultConfig, defineConfig } from './config'; +export type { ArktypePlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/arktype/plugin.ts b/packages/openapi-ts/src/plugins/arktype/plugin.ts new file mode 100644 index 000000000..205edde7c --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/plugin.ts @@ -0,0 +1,4 @@ +import type { ArktypePlugin } from './types'; +import { handlerV2 } from './v2/plugin'; + +export const handler: ArktypePlugin['Handler'] = (args) => handlerV2(args); diff --git a/packages/openapi-ts/src/plugins/arktype/shared/export.ts b/packages/openapi-ts/src/plugins/arktype/shared/export.ts new file mode 100644 index 000000000..76349c01b --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/shared/export.ts @@ -0,0 +1,63 @@ +import type { Symbol } from '@hey-api/codegen-core'; +import ts from 'typescript'; + +import type { IR } from '~/ir/types'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../constants'; +import type { ArktypePlugin } from '../types'; +import type { Ast } from './types'; + +export const exportAst = ({ + ast, + plugin, + schema, + symbol, + typeInferSymbol, +}: { + ast: Ast; + plugin: ArktypePlugin['Instance']; + schema: IR.SchemaObject; + symbol: Symbol; + typeInferSymbol: Symbol | undefined; +}): void => { + const type = plugin.referenceSymbol( + plugin.api.selector('external', 'arktype.type'), + ); + + const statement = tsc.constVariable({ + comment: plugin.config.comments + ? createSchemaComment({ schema }) + : undefined, + exportConst: symbol.exported, + expression: tsc.callExpression({ + functionName: type.placeholder, + parameters: [ + ast.def ? tsc.stringLiteral({ text: ast.def }) : ast.expression, + ], + }), + name: symbol.placeholder, + // typeName: ast.typeName + // ? (tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: ast.typeName, + // }) as unknown as ts.TypeNode) + // : undefined, + }); + plugin.setSymbolValue(symbol, statement); + + if (typeInferSymbol) { + const inferType = tsc.typeAliasDeclaration({ + exportType: typeInferSymbol.exported, + name: typeInferSymbol.placeholder, + type: ts.factory.createTypeQueryNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(symbol.placeholder), + identifiers.type.infer, + ), + ), + }); + plugin.setSymbolValue(typeInferSymbol, inferType); + } +}; diff --git a/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts new file mode 100644 index 000000000..dfa46bcff --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/shared/types.d.ts @@ -0,0 +1,31 @@ +import type ts from 'typescript'; + +import type { IR } from '~/ir/types'; +import type { ToRefs } from '~/plugins/shared/types/refs'; + +import type { ArktypePlugin } from '../types'; + +export type Ast = { + def: string; + expression: ts.Expression; + hasLazyExpression?: boolean; + typeName?: string | ts.Identifier; +}; + +export type IrSchemaToAstOptions = { + plugin: ArktypePlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { + /** + * Path to the schema in the intermediary representation. + */ + _path: ReadonlyArray; + hasLazyExpression: boolean; +}; + +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ArktypePlugin['Instance']; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/types.d.ts b/packages/openapi-ts/src/plugins/arktype/types.d.ts new file mode 100644 index 000000000..6f78cded3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/types.d.ts @@ -0,0 +1,666 @@ +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + +import type { IApi } from './api'; + +export type UserConfig = Plugin.Name<'arktype'> & + Plugin.Hooks & { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Add comments from input to the generated Arktype schemas? + * + * @default true + */ + comments?: boolean; + /** + * Configuration for reusable schema definitions. + * + * Controls generation of shared Arktype schemas that can be referenced + * across requests and responses. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + */ + definitions?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for reusable definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the schema name. + * + * @default '{{name}}' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}' + */ + name?: StringName; + }; + }; + }; + /** + * Should the exports from the generated files be re-exported in the index + * barrel file? + * + * @default false + */ + exportFromIndex?: boolean; + /** + * Enable Arktype metadata support? It's often useful to associate a schema + * with some additional metadata for documentation, code generation, AI + * structured outputs, form validation, and other purposes. + * + * @default false + */ + metadata?: boolean; + /** + * Configuration for request-specific Arktype schemas. + * + * Controls generation of Arktype schemas for request bodies, query parameters, path + * parameters, and headers. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + requests?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for request definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the operation name. + * + * @default '{{name}}Data' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Data' + */ + name?: StringName; + }; + }; + }; + /** + * Configuration for response-specific Arktype schemas. + * + * Controls generation of Arktype schemas for response bodies, error responses, + * and status codes. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + responses?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for response definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the operation name. + * + * @default '{{name}}Response' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Response' + */ + name?: StringName; + }; + }; + }; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + }; + }; + /** + * Configuration for webhook-specific Arktype schemas. + * + * Controls generation of Arktype schemas for webhook payloads. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default true + */ + webhooks?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate Arktype schemas for webhook definitions. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated schema names. The name variable + * is obtained from the webhook key. + * + * @default '{{name}}WebhookRequest' + */ + name?: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types?: { + /** + * Configuration for `infer` types. + * + * Can be: + * - `boolean`: Shorthand for `{ enabled: boolean }` + * - `string` or `function`: Shorthand for `{ name: string | function }` + * - `object`: Full configuration object + * + * @default false + */ + infer?: + | boolean + | StringName + | { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case?: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled?: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}WebhookRequest' + */ + name?: StringName; + }; + }; + }; + }; + +export type Config = Plugin.Name<'arktype'> & + Plugin.Hooks & { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Add comments from input to the generated Arktype schemas? + * + * @default true + */ + comments: boolean; + /** + * Configuration for reusable schema definitions. + * + * Controls generation of shared Arktype schemas that can be referenced across + * requests and responses. + */ + definitions: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for reusable definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the schema name. + * + * @default '{{name}}' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}' + */ + name: StringName; + }; + }; + }; + /** + * Should the exports from the generated files be re-exported in the index + * barrel file? + * + * @default false + */ + exportFromIndex: boolean; + /** + * Enable Arktype metadata support? It's often useful to associate a schema with + * some additional metadata for documentation, code generation, AI + * structured outputs, form validation, and other purposes. + * + * @default false + */ + metadata: boolean; + /** + * Configuration for request-specific Arktype schemas. + * + * Controls generation of Arktype schemas for request bodies, query parameters, path + * parameters, and headers. + */ + requests: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for request definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the operation name. + * + * @default '{{name}}Data' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Data' + */ + name: StringName; + }; + }; + }; + /** + * Configuration for response-specific Arktype schemas. + * + * Controls generation of Arktype schemas for response bodies, error responses, + * and status codes. + */ + responses: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for response definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * obtained from the operation name. + * + * @default '{{name}}Response' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}Response' + */ + name: StringName; + }; + }; + }; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + }; + }; + /** + * Configuration for webhook-specific Arktype schemas. + * + * Controls generation of Arktype schemas for webhook payloads. + */ + webhooks: { + /** + * The casing convention to use for generated names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate Arktype schemas for webhook definitions. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated schema names. The name variable is + * is obtained from the webhook key. + * + * @default '{{name}}WebhookRequest' + */ + name: StringName; + /** + * Configuration for TypeScript type generation from Arktype schemas. + * + * Controls generation of TypeScript types based on the generated Arktype schemas. + */ + types: { + /** + * Configuration for `infer` types. + */ + infer: { + /** + * The casing convention to use for generated type names. + * + * @default 'PascalCase' + */ + case: StringCase; + /** + * Whether to generate TypeScript types from Arktype schemas. + * + * @default true + */ + enabled: boolean; + /** + * Custom naming pattern for generated type names. The name variable is + * obtained from the Arktype schema name. + * + * @default '{{name}}WebhookRequest' + */ + name: StringName; + }; + }; + }; + }; + +export type ArktypePlugin = DefinePlugin; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/api.ts b/packages/openapi-ts/src/plugins/arktype/v2/api.ts new file mode 100644 index 000000000..5a679ad3c --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/api.ts @@ -0,0 +1,88 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +// import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV2 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + // const out = User({ + // name: "Alan Turing", + // device: { + // platform: "enigma", + // versions: [0, "1", 0n] + // } + // }) + // if (out instanceof type.errors) { + // // hover out.summary to see validation errors + // console.error(out.summary) + // } else { + // // hover out to see your validated data + // console.log(`Hello, ${out.name}`) + // } + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: 'parseAsync', + // name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV2 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: 'parseAsync', + // name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts new file mode 100644 index 000000000..d1faa5fac --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/plugin.ts @@ -0,0 +1,364 @@ +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRefs } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; + +import { exportAst } from '../shared/export'; +import type { Ast, IrSchemaToAstOptions } from '../shared/types'; +import type { ArktypePlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; + +export const irSchemaToAst = ({ + // optional, + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + /** + * Accept `optional` to handle optional object properties. We can't handle + * this inside the object function because `.optional()` must come before + * `.default()` which is handled in this function. + */ + optional?: boolean; + schema: IR.SchemaObject; +}): Ast => { + let ast: Partial = {}; + + // const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (schema.$ref) { + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; + } else { + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + expression: 'TODO', + name: 'TODO', + // name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; + } + } else if (schema.type) { + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.def = typeAst.def; + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; + + if (plugin.config.metadata && schema.description) { + // TODO: add description + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.register, + // }), + // parameters: [ + // tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.globalRegistry, + // }), + // tsc.objectExpression({ + // obj: [ + // { + // key: 'description', + // value: tsc.stringLiteral({ text: schema.description }), + // }, + // ], + // }), + // ], + // }); + } + } else if (schema.items) { + schema = deduplicateSchema({ schema }); + + if (schema.items) { + // const itemSchemas = schema.items.map((item, index) => + // irSchemaToAst({ + // plugin, + // schema: item, + // state: { + // ...state, + // _path: [...state._path, 'items', index], + // }, + // }), + // ); + // if (schema.logicalOperator === 'and') { + // const firstSchema = schema.items[0]!; + // // we want to add an intersection, but not every schema can use the same API. + // // if the first item contains another array or not an object, we cannot use + // // `.merge()` as that does not exist on `.union()` and non-object schemas. + // if ( + // firstSchema.logicalOperator === 'or' || + // (firstSchema.type && firstSchema.type !== 'object') + // ) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.intersection, + // }), + // parameters: itemSchemas.map((schema) => schema.expression), + // }); + // } else { + // ast.expression = itemSchemas[0]!.expression; + // itemSchemas.slice(1).forEach((schema) => { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression!, + // name: identifiers.and, + // }), + // parameters: [ + // schema.hasCircularReference + // ? tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.lazy, + // }), + // parameters: [ + // tsc.arrowFunction({ + // statements: [ + // tsc.returnStatement({ + // expression: schema.expression, + // }), + // ], + // }), + // ], + // }) + // : schema.expression, + // ], + // }); + // }); + // } + // } else { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.union, + // }), + // parameters: [ + // tsc.arrayLiteralExpression({ + // elements: itemSchemas.map((schema) => schema.expression), + // }), + // ], + // }); + // } + } else { + ast = irSchemaToAst({ plugin, schema, state }); + } + } else { + // catch-all fallback for failed schemas + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + ast.def = typeAst.def; + ast.expression = typeAst.expression; + } + + // TODO: remove later + if (!ast.expression) { + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + ast.expression = typeAst.expression; + } + // END TODO: remove later + + // if (ast.expression) { + // if (schema.accessScope === 'read') { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.readonly, + // }), + // }); + // } + + // if (optional) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: z.placeholder, + // name: identifiers.optional, + // }), + // parameters: [ast.expression], + // }); + // ast.typeName = identifiers.ZodOptional; + // } + + // if (schema.default !== undefined) { + // const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + // const callParameter = numberParameter({ + // isBigInt, + // value: schema.default, + // }); + // if (callParameter) { + // ast.expression = tsc.callExpression({ + // functionName: tsc.propertyAccessExpression({ + // expression: ast.expression, + // name: identifiers.default, + // }), + // parameters: [callParameter], + // }); + // } + // } + // } + + return ast as Ast; +}; + +const handleComponent = ({ + $ref, + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + $ref: string; + schema: IR.SchemaObject; +}): void => { + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ + exported: true, + meta: { + resourceType, + }, + name: buildName({ + config: plugin.config.definitions, + name: baseName, + }), + selector: plugin.api.selector('ref', $ref), + }); + const typeInferSymbol = plugin.config.definitions.types.infer.enabled + ? plugin.registerSymbol({ + exported: true, + meta: { + kind: 'type', + resourceType, + }, + name: buildName({ + config: plugin.config.definitions.types.infer, + name: baseName, + }), + selector: plugin.api.selector('type-infer-ref', $ref), + }) + : undefined; + exportAst({ + ast, + plugin, + schema, + symbol, + typeInferSymbol, + }); +}; + +export const handlerV2: ArktypePlugin['Handler'] = ({ plugin }) => { + plugin.registerSymbol({ + external: 'arktype', + name: 'type', + selector: plugin.api.selector('external', 'arktype.type'), + }); + + plugin.forEach( + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'webhook', + (event) => { + switch (event.type) { + // case 'operation': + // operationToZodSchema({ + // getZodSchema: (schema) => { + // const state: State = { + // circularReferenceTracker: [], + // currentReferenceTracker: [], + // hasCircularReference: false, + // }; + // return schemaToZodSchema({ plugin, schema, state }); + // }, + // operation: event.operation, + // plugin, + // }); + // break; + case 'parameter': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + case 'requestBody': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + case 'schema': + handleComponent({ + $ref: event.$ref, + plugin, + schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), + }); + break; + // case 'webhook': + // webhookToZodSchema({ + // getZodSchema: (schema) => { + // const state: State = { + // circularReferenceTracker: [], + // currentReferenceTracker: [], + // hasCircularReference: false, + // }; + // return schemaToZodSchema({ plugin, schema, state }); + // }, + // operation: event.operation, + // plugin, + // }); + // break; + } + }, + ); +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts new file mode 100644 index 000000000..7d977d64c --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts @@ -0,0 +1,124 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { nullToAst } from './null'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +// import { arrayToAst } from "./array"; +// import { booleanToAst } from "./boolean"; +// import { enumToAst } from "./enum"; +// import { neverToAst } from "./never"; +// import { numberToAst } from "./number"; +// import { tupleToAst } from "./tuple"; +// import { undefinedToAst } from "./undefined"; +// import { unknownToAst } from "./unknown"; +// import { voidToAst } from "./void"; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + // case 'array': + // return arrayToAst({ + // ...args, + // schema: schema as SchemaWithType<'array'>, + // }); + // case 'boolean': + // return booleanToAst({ + // ...args, + // schema: schema as SchemaWithType<'boolean'>, + // }); + // case 'enum': + // return enumToAst({ + // ...args, + // schema: schema as SchemaWithType<'enum'>, + // }); + // case 'integer': + // case 'number': + // return numberToAst({ + // ...args, + // schema: schema as SchemaWithType<'integer' | 'number'>, + // }); + // case 'never': + // return neverToAst({ + // ...args, + // schema: schema as SchemaWithType<'never'>, + // }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + // case 'tuple': + // return tupleToAst({ + // ...args, + // schema: schema as SchemaWithType<'tuple'>, + // }); + // case 'undefined': + // return undefinedToAst({ + // ...args, + // schema: schema as SchemaWithType<'undefined'>, + // }); + // case 'unknown': + // return unknownToAst({ + // ...args, + // schema: schema as SchemaWithType<'unknown'>, + // }); + // case 'void': + // return voidToAst({ + // ...args, + // schema: schema as SchemaWithType<'void'>, + // }); + } + + const type = args.plugin.referenceSymbol( + args.plugin.api.selector('external', 'arktype.type'), + ); + + const expression = ts.factory.createCallExpression( + ts.factory.createIdentifier(type.placeholder), + undefined, + [ + ts.factory.createObjectLiteralExpression( + [ + ts.factory.createPropertyAssignment( + 'name', + ts.factory.createStringLiteral('string'), + ), + ts.factory.createPropertyAssignment( + 'platform', + ts.factory.createStringLiteral("'android' | 'ios'"), + ), + ts.factory.createPropertyAssignment( + ts.factory.createComputedPropertyName( + ts.factory.createStringLiteral('versions?'), + ), + ts.factory.createStringLiteral('(number | string)[]'), + ), + ], + true, + ), + ], + ); + + return { + def: '', + expression, + hasLazyExpression: false, + }; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts new file mode 100644 index 000000000..26c6cdf89 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts @@ -0,0 +1,15 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _args: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; + }, +): Omit => { + const result: Partial> = {}; + result.def = identifiers.primitives.null; + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts new file mode 100644 index 000000000..3e974f785 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts @@ -0,0 +1,172 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; + +// import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + let propertyName: + | ts.ComputedPropertyName + | ts.StringLiteral + | ts.NumericLiteral + | string = isRequired ? name : `${name}?`; + + // if (propertyAst.hasCircularReference) { + // properties.push( + // tsc.getAccessorDeclaration({ + // name: propertyName, + // // @ts-expect-error + // returnType: propertyAst.typeName + // ? tsc.propertyAccessExpression({ + // expression: 'TODO', + // name: propertyAst.typeName, + // }) + // : undefined, + // statements: [ + // tsc.returnStatement({ + // expression: propertyAst.expression, + // }), + // ], + // }), + // ); + // } else { + // properties.push( + // tsc.propertyAssignment({ + // initializer: propertyAst.expression, + // name: ts.factory.createComputedPropertyName( + // ts.factory.createStringLiteral(`${propertyName}?`), + // ), + // }), + // ); + // } + + if (propertyName.endsWith('?')) { + propertyName = ts.factory.createComputedPropertyName( + tsc.stringLiteral({ text: propertyName }), + ); + } else { + // TODO: parser - abstract safe property name logic + if ( + ((propertyName.match(/^[0-9]/) && propertyName.match(/\D+/g)) || + propertyName.match(/\W/g)) && + !propertyName.startsWith("'") && + !propertyName.endsWith("'") + ) { + propertyName = `'${propertyName}'`; + } + + numberRegExp.lastIndex = 0; + if (numberRegExp.test(propertyName)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = propertyName.startsWith('-') + ? tsc.stringLiteral({ text: name }) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + } + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: 'TODO', + name: 'record', + // name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: 'TODO', + name: 'string', + // name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + // Return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'TODO', + } as Ast; + } + + return result as Omit; + } + + result.expression = ts.factory.createObjectLiteralExpression( + properties, + true, + ); + + // return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'TODO', + } as Ast; + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts b/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts new file mode 100644 index 000000000..8c498d0d8 --- /dev/null +++ b/packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts @@ -0,0 +1,66 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const result: Partial> = {}; + + if (typeof schema.const === 'string') { + result.def = schema.const; + return result as Omit; + } + + let def = identifiers.primitives.string; + + if (schema.format) { + switch (schema.format) { + case 'date': + case 'date-time': + case 'time': + def = `${def}.${identifiers.string.date}.${identifiers.string.iso}`; + break; + case 'email': + def = `${def}.${identifiers.string.email}`; + break; + case 'ipv4': + def = `${def}.${identifiers.string.ip}.${identifiers.string.v4}`; + break; + case 'ipv6': + def = `${def}.${identifiers.string.ip}.${identifiers.string.v6}`; + break; + case 'uri': + def = `${def}.${identifiers.string.url}`; + break; + case 'uuid': + def = `${def}.${identifiers.string.uuid}`; + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + def = `${schema.minLength} <= ${def} <= ${schema.maxLength}`; + } else { + if (schema.maxLength !== undefined) { + def = `${def} <= ${schema.maxLength}`; + + if (schema.minLength !== undefined) { + def = `${schema.minLength} <= ${def}`; + } + } else if (schema.minLength !== undefined) { + def = `${def} >= ${schema.minLength}`; + } + } + + if (schema.pattern) { + def = `/${schema.pattern}/`; + } + + result.def = def; + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/config.ts b/packages/openapi-ts/src/plugins/config.ts index d8460ecec..e72265ae7 100644 --- a/packages/openapi-ts/src/plugins/config.ts +++ b/packages/openapi-ts/src/plugins/config.ts @@ -1,54 +1,56 @@ -import type { AngularCommonPlugin } from './@angular/common'; -import { defaultConfig as angularCommon } from './@angular/common'; -import type { HeyApiClientAngularPlugin } from './@hey-api/client-angular'; -import { defaultConfig as heyApiClientAngular } from './@hey-api/client-angular'; -import type { HeyApiClientAxiosPlugin } from './@hey-api/client-axios'; -import { defaultConfig as heyApiClientAxios } from './@hey-api/client-axios'; -import type { HeyApiClientFetchPlugin } from './@hey-api/client-fetch'; -import { defaultConfig as heyApiClientFetch } from './@hey-api/client-fetch'; -import type { HeyApiClientNextPlugin } from './@hey-api/client-next'; -import { defaultConfig as heyApiClientNext } from './@hey-api/client-next'; -import type { HeyApiClientNuxtPlugin } from './@hey-api/client-nuxt'; -import { defaultConfig as heyApiClientNuxt } from './@hey-api/client-nuxt'; -import type { HeyApiClientOfetchPlugin } from './@hey-api/client-ofetch'; -import { defaultConfig as heyApiClientOfetch } from './@hey-api/client-ofetch'; -import type { HeyApiClientLegacyAngularPlugin } from './@hey-api/legacy-angular'; -import { defaultConfig as heyApiLegacyAngular } from './@hey-api/legacy-angular'; -import type { HeyApiClientLegacyAxiosPlugin } from './@hey-api/legacy-axios'; -import { defaultConfig as heyApiLegacyAxios } from './@hey-api/legacy-axios'; -import type { HeyApiClientLegacyFetchPlugin } from './@hey-api/legacy-fetch'; -import { defaultConfig as heyApiLegacyFetch } from './@hey-api/legacy-fetch'; -import type { HeyApiClientLegacyNodePlugin } from './@hey-api/legacy-node'; -import { defaultConfig as heyApiLegacyNode } from './@hey-api/legacy-node'; -import type { HeyApiClientLegacyXhrPlugin } from './@hey-api/legacy-xhr'; -import { defaultConfig as heyApiLegacyXhr } from './@hey-api/legacy-xhr'; -import type { HeyApiSchemasPlugin } from './@hey-api/schemas'; -import { defaultConfig as heyApiSchemas } from './@hey-api/schemas'; -import type { HeyApiSdkPlugin } from './@hey-api/sdk'; -import { defaultConfig as heyApiSdk } from './@hey-api/sdk'; -import type { HeyApiTransformersPlugin } from './@hey-api/transformers'; -import { defaultConfig as heyApiTransformers } from './@hey-api/transformers'; -import type { HeyApiTypeScriptPlugin } from './@hey-api/typescript'; -import { defaultConfig as heyApiTypeScript } from './@hey-api/typescript'; -import type { PiniaColadaPlugin } from './@pinia/colada'; -import { defaultConfig as piniaColada } from './@pinia/colada'; -import type { TanStackAngularQueryPlugin } from './@tanstack/angular-query-experimental'; -import { defaultConfig as tanStackAngularQuery } from './@tanstack/angular-query-experimental'; -import type { TanStackReactQueryPlugin } from './@tanstack/react-query'; -import { defaultConfig as tanStackReactQuery } from './@tanstack/react-query'; -import type { TanStackSolidQueryPlugin } from './@tanstack/solid-query'; -import { defaultConfig as tanStackSolidQuery } from './@tanstack/solid-query'; -import type { TanStackSvelteQueryPlugin } from './@tanstack/svelte-query'; -import { defaultConfig as tanStackSvelteQuery } from './@tanstack/svelte-query'; -import type { TanStackVueQueryPlugin } from './@tanstack/vue-query'; -import { defaultConfig as tanStackVueQuery } from './@tanstack/vue-query'; -import type { FastifyPlugin } from './fastify'; -import { defaultConfig as fastify } from './fastify'; -import type { Plugin, PluginNames } from './types'; -import type { ValibotPlugin } from './valibot'; -import { defaultConfig as valibot } from './valibot'; -import type { ZodPlugin } from './zod'; -import { defaultConfig as zod } from './zod'; +import type { AngularCommonPlugin } from '~/plugins/@angular/common'; +import { defaultConfig as angularCommon } from '~/plugins/@angular/common'; +import type { HeyApiClientAngularPlugin } from '~/plugins/@hey-api/client-angular'; +import { defaultConfig as heyApiClientAngular } from '~/plugins/@hey-api/client-angular'; +import type { HeyApiClientAxiosPlugin } from '~/plugins/@hey-api/client-axios'; +import { defaultConfig as heyApiClientAxios } from '~/plugins/@hey-api/client-axios'; +import type { HeyApiClientFetchPlugin } from '~/plugins/@hey-api/client-fetch'; +import { defaultConfig as heyApiClientFetch } from '~/plugins/@hey-api/client-fetch'; +import type { HeyApiClientNextPlugin } from '~/plugins/@hey-api/client-next'; +import { defaultConfig as heyApiClientNext } from '~/plugins/@hey-api/client-next'; +import type { HeyApiClientNuxtPlugin } from '~/plugins/@hey-api/client-nuxt'; +import { defaultConfig as heyApiClientNuxt } from '~/plugins/@hey-api/client-nuxt'; +import type { HeyApiClientOfetchPlugin } from '~/plugins/@hey-api/client-ofetch'; +import { defaultConfig as heyApiClientOfetch } from '~/plugins/@hey-api/client-ofetch'; +import type { HeyApiClientLegacyAngularPlugin } from '~/plugins/@hey-api/legacy-angular'; +import { defaultConfig as heyApiLegacyAngular } from '~/plugins/@hey-api/legacy-angular'; +import type { HeyApiClientLegacyAxiosPlugin } from '~/plugins/@hey-api/legacy-axios'; +import { defaultConfig as heyApiLegacyAxios } from '~/plugins/@hey-api/legacy-axios'; +import type { HeyApiClientLegacyFetchPlugin } from '~/plugins/@hey-api/legacy-fetch'; +import { defaultConfig as heyApiLegacyFetch } from '~/plugins/@hey-api/legacy-fetch'; +import type { HeyApiClientLegacyNodePlugin } from '~/plugins/@hey-api/legacy-node'; +import { defaultConfig as heyApiLegacyNode } from '~/plugins/@hey-api/legacy-node'; +import type { HeyApiClientLegacyXhrPlugin } from '~/plugins/@hey-api/legacy-xhr'; +import { defaultConfig as heyApiLegacyXhr } from '~/plugins/@hey-api/legacy-xhr'; +import type { HeyApiSchemasPlugin } from '~/plugins/@hey-api/schemas'; +import { defaultConfig as heyApiSchemas } from '~/plugins/@hey-api/schemas'; +import type { HeyApiSdkPlugin } from '~/plugins/@hey-api/sdk'; +import { defaultConfig as heyApiSdk } from '~/plugins/@hey-api/sdk'; +import type { HeyApiTransformersPlugin } from '~/plugins/@hey-api/transformers'; +import { defaultConfig as heyApiTransformers } from '~/plugins/@hey-api/transformers'; +import type { HeyApiTypeScriptPlugin } from '~/plugins/@hey-api/typescript'; +import { defaultConfig as heyApiTypeScript } from '~/plugins/@hey-api/typescript'; +import type { PiniaColadaPlugin } from '~/plugins/@pinia/colada'; +import { defaultConfig as piniaColada } from '~/plugins/@pinia/colada'; +import type { TanStackAngularQueryPlugin } from '~/plugins/@tanstack/angular-query-experimental'; +import { defaultConfig as tanStackAngularQuery } from '~/plugins/@tanstack/angular-query-experimental'; +import type { TanStackReactQueryPlugin } from '~/plugins/@tanstack/react-query'; +import { defaultConfig as tanStackReactQuery } from '~/plugins/@tanstack/react-query'; +import type { TanStackSolidQueryPlugin } from '~/plugins/@tanstack/solid-query'; +import { defaultConfig as tanStackSolidQuery } from '~/plugins/@tanstack/solid-query'; +import type { TanStackSvelteQueryPlugin } from '~/plugins/@tanstack/svelte-query'; +import { defaultConfig as tanStackSvelteQuery } from '~/plugins/@tanstack/svelte-query'; +import type { TanStackVueQueryPlugin } from '~/plugins/@tanstack/vue-query'; +import { defaultConfig as tanStackVueQuery } from '~/plugins/@tanstack/vue-query'; +import type { ArktypePlugin } from '~/plugins/arktype'; +import { defaultConfig as arktype } from '~/plugins/arktype'; +import type { FastifyPlugin } from '~/plugins/fastify'; +import { defaultConfig as fastify } from '~/plugins/fastify'; +import type { Plugin, PluginNames } from '~/plugins/types'; +import type { ValibotPlugin } from '~/plugins/valibot'; +import { defaultConfig as valibot } from '~/plugins/valibot'; +import type { ZodPlugin } from '~/plugins/zod'; +import { defaultConfig as zod } from '~/plugins/zod'; export interface PluginConfigMap { '@angular/common': AngularCommonPlugin['Types']; @@ -68,6 +70,7 @@ export interface PluginConfigMap { '@tanstack/solid-query': TanStackSolidQueryPlugin['Types']; '@tanstack/svelte-query': TanStackSvelteQueryPlugin['Types']; '@tanstack/vue-query': TanStackVueQueryPlugin['Types']; + arktype: ArktypePlugin['Types']; fastify: FastifyPlugin['Types']; 'legacy/angular': HeyApiClientLegacyAngularPlugin['Types']; 'legacy/axios': HeyApiClientLegacyAxiosPlugin['Types']; @@ -98,6 +101,7 @@ export const defaultPluginConfigs: { '@tanstack/solid-query': tanStackSolidQuery, '@tanstack/svelte-query': tanStackSvelteQuery, '@tanstack/vue-query': tanStackVueQuery, + arktype, fastify, 'legacy/angular': heyApiLegacyAngular, 'legacy/axios': heyApiLegacyAxios, diff --git a/packages/openapi-ts/src/plugins/fastify/api.ts b/packages/openapi-ts/src/plugins/fastify/api.ts index b8b2cd8f7..98130c3db 100644 --- a/packages/openapi-ts/src/plugins/fastify/api.ts +++ b/packages/openapi-ts/src/plugins/fastify/api.ts @@ -1,6 +1,6 @@ import type { Selector } from '@hey-api/codegen-core'; -import type { Plugin } from '../types'; +import type { Plugin } from '~/plugins/types'; type SelectorType = 'RouteHandler'; @@ -11,13 +11,13 @@ export type IApi = { * - `RouteHandler`: never * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'fastify'>) {} - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/fastify/config.ts b/packages/openapi-ts/src/plugins/fastify/config.ts index 6db188294..b094d056a 100644 --- a/packages/openapi-ts/src/plugins/fastify/config.ts +++ b/packages/openapi-ts/src/plugins/fastify/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import type { FastifyPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/fastify/plugin.ts b/packages/openapi-ts/src/plugins/fastify/plugin.ts index 7c79bcbdc..1f1ec72a1 100644 --- a/packages/openapi-ts/src/plugins/fastify/plugin.ts +++ b/packages/openapi-ts/src/plugins/fastify/plugin.ts @@ -1,9 +1,10 @@ import type ts from 'typescript'; -import { operationResponsesMap } from '../../ir/operation'; -import { hasParameterGroupObjectRequired } from '../../ir/parameter'; -import type { IR } from '../../ir/types'; -import { type Property, tsc } from '../../tsc'; +import { operationResponsesMap } from '~/ir/operation'; +import { hasParameterGroupObjectRequired } from '~/ir/parameter'; +import type { IR } from '~/ir/types'; +import { type Property, tsc } from '~/tsc'; + import type { FastifyPlugin } from './types'; const operationToRouteHandler = ({ @@ -17,7 +18,7 @@ const operationToRouteHandler = ({ const pluginTypeScript = plugin.getPluginOrThrow('@hey-api/typescript'); const symbolDataType = plugin.getSymbol( - pluginTypeScript.api.getSelector('data', operation.id), + pluginTypeScript.api.selector('data', operation.id), ); if (symbolDataType) { if (operation.body) { @@ -65,7 +66,7 @@ const operationToRouteHandler = ({ let errorsTypeReference: ts.TypeReferenceNode | undefined = undefined; const symbolErrorType = plugin.getSymbol( - pluginTypeScript.api.getSelector('errors', operation.id), + pluginTypeScript.api.selector('errors', operation.id), ); if (symbolErrorType && errors && errors.properties) { const keys = Object.keys(errors.properties); @@ -92,7 +93,7 @@ const operationToRouteHandler = ({ let responsesTypeReference: ts.TypeReferenceNode | undefined = undefined; const symbolResponseType = plugin.getSymbol( - pluginTypeScript.api.getSelector('responses', operation.id), + pluginTypeScript.api.selector('responses', operation.id), ); if (symbolResponseType && responses && responses.properties) { const keys = Object.keys(responses.properties); @@ -134,7 +135,7 @@ const operationToRouteHandler = ({ } const symbolRouteHandler = plugin.referenceSymbol( - plugin.api.getSelector('RouteHandler'), + plugin.api.selector('RouteHandler'), ); const routeHandler: Property = { name: operation.id, @@ -158,7 +159,7 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { kind: 'type', }, name: 'RouteHandler', - selector: plugin.api.getSelector('RouteHandler'), + selector: plugin.api.selector('RouteHandler'), }); const symbolRouteHandlers = plugin.registerSymbol({ @@ -171,12 +172,18 @@ export const handler: FastifyPlugin['Handler'] = ({ plugin }) => { const routeHandlers: Array = []; - plugin.forEach('operation', ({ operation }) => { - const routeHandler = operationToRouteHandler({ operation, plugin }); - if (routeHandler) { - routeHandlers.push(routeHandler); - } - }); + plugin.forEach( + 'operation', + ({ operation }) => { + const routeHandler = operationToRouteHandler({ operation, plugin }); + if (routeHandler) { + routeHandlers.push(routeHandler); + } + }, + { + order: 'declarations', + }, + ); const node = tsc.typeAliasDeclaration({ exportType: symbolRouteHandlers.exported, diff --git a/packages/openapi-ts/src/plugins/fastify/types.d.ts b/packages/openapi-ts/src/plugins/fastify/types.d.ts index 541c5c848..2f409a6ca 100644 --- a/packages/openapi-ts/src/plugins/fastify/types.d.ts +++ b/packages/openapi-ts/src/plugins/fastify/types.d.ts @@ -1,4 +1,5 @@ -import type { DefinePlugin, Plugin } from '../types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'fastify'> & diff --git a/packages/openapi-ts/src/plugins/shared/types/instance.d.ts b/packages/openapi-ts/src/plugins/shared/types/instance.d.ts index 7527eb57b..38047f139 100644 --- a/packages/openapi-ts/src/plugins/shared/types/instance.d.ts +++ b/packages/openapi-ts/src/plugins/shared/types/instance.d.ts @@ -1,4 +1,5 @@ -import type { IR } from '../../../ir/types'; +import type { IrTopLevelKind } from '~/ir/graph'; +import type { IR } from '~/ir/types'; type WalkEvents = | { @@ -6,44 +7,62 @@ type WalkEvents = method: keyof IR.PathItemObject; operation: IR.OperationObject; path: string; - type: 'operation'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; parameter: IR.ParameterObject; - type: 'parameter'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; requestBody: IR.RequestBodyObject; - type: 'requestBody'; + type: Extract; } | { $ref: string; _path: ReadonlyArray; name: string; schema: IR.SchemaObject; - type: 'schema'; + type: Extract; } | { _path: ReadonlyArray; server: IR.ServerObject; - type: 'server'; + type: Extract; } | { _path: ReadonlyArray; key: string; method: keyof IR.PathItemObject; operation: IR.OperationObject; - type: 'webhook'; + type: Extract; }; -export type WalkEventType = WalkEvents['type']; -export type WalkEvent = Extract< +export type WalkEvent = Extract< WalkEvents, { type: T } >; + +export type WalkOptions = { + /** + * Order of walking schemas. + * + * The "declarations" option ensures that schemas are walked in the order + * they are declared in the input document. This is useful for scenarios where + * the order of declaration matters, such as when generating code that relies + * on the sequence of schema definitions. + * + * The "topological" option ensures that schemas are walked in an order + * where dependencies are visited before the schemas that depend on them. + * This is useful for scenarios where you need to process or generate + * schemas in a way that respects their interdependencies. + * + * @default 'topological' + */ + order?: 'declarations' | 'topological'; +}; diff --git a/packages/openapi-ts/src/plugins/shared/types/refs.d.ts b/packages/openapi-ts/src/plugins/shared/types/refs.d.ts new file mode 100644 index 000000000..79c839311 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/types/refs.d.ts @@ -0,0 +1,59 @@ +/** + * Ref wrapper which ensures a stable reference for a value. + * + * @example + * ```ts + * type NumRef = Ref; // { value: number } + * const num: NumRef = { value: 42 }; + * console.log(num.value); // 42 + * ``` + */ +type Ref = { value: T }; + +/** + * Utility type: wraps a value in a Ref. + * + * @example + * ```ts + * type R = ToRef; // { value: number } + * ``` + */ +export type ToRef = Ref; + +/** + * Utility type: unwraps a Ref to its value type. + * @example + * ```ts + * type N = FromRef<{ value: number }>; // number + * ``` + */ +export type FromRef = T extends Ref ? V : T; + +/** + * Maps every property of a Ref-wrapped object back to its plain value. + * + * @example + * ```ts + * type Foo = { a: number; b: string }; + * type Refs = ToRefs; // { a: Ref; b: Ref } + * type Foo2 = FromRefs; // { a: number; b: string } + * ``` + */ +export type FromRefs = { + [K in keyof T]: T[K] extends Ref ? V : T[K]; +}; + +/** + * Maps every property of `T` to a `Ref` of that property. + * + * @example + * ```ts + * type Foo = { a: number; b: string }; + * type Refs = ToRefs; // { a: Ref; b: Ref } + * const refs: Refs = { a: { value: 1 }, b: { value: 'x' } }; + * console.log(refs.a.value, refs.b.value); // 1 'x' + * ``` + */ +export type ToRefs = { + [K in keyof T]: Ref; +}; diff --git a/packages/openapi-ts/src/plugins/shared/types/schema.d.ts b/packages/openapi-ts/src/plugins/shared/types/schema.d.ts new file mode 100644 index 000000000..fcef09883 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/types/schema.d.ts @@ -0,0 +1,8 @@ +import type { IR } from '~/ir/types'; + +export interface SchemaWithType< + T extends + Required['type'] = Required['type'], +> extends Omit { + type: Extract['type'], T>; +} diff --git a/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts b/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts new file mode 100644 index 000000000..afc221d63 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/utils/__tests__/refs.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; + +import { fromRef, fromRefs, toRef, toRefs } from '../refs'; + +describe('toRef', () => { + it('wraps a primitive value', () => { + expect(toRef(42)).toEqual({ value: 42 }); + expect(toRef('x')).toEqual({ value: 'x' }); + }); + + it('wraps an object', () => { + const obj = { foo: 1 }; + expect(toRef(obj)).toEqual({ value: obj }); + }); +}); + +describe('fromRef', () => { + it('unwraps a primitive value', () => { + expect(fromRef({ value: 42 })).toBe(42); + expect(fromRef({ value: 'x' })).toBe('x'); + }); + + it('unwraps an object', () => { + const obj = { foo: 1 }; + expect(fromRef({ value: obj })).toBe(obj); + }); +}); + +describe('toRef <-> fromRef roundtrip', () => { + it('roundtrips value -> ref -> value', () => { + expect(fromRef(toRef(123))).toBe(123); + const obj = { foo: 'bar' }; + expect(fromRef(toRef(obj))).toBe(obj); + }); +}); + +describe('toRefs', () => { + it('wraps primitives', () => { + expect(toRefs({ a: 1, b: 'x' })).toEqual({ + a: { value: 1 }, + b: { value: 'x' }, + }); + }); + + it('wraps empty object', () => { + expect(toRefs({})).toEqual({}); + }); + + it('wraps nested objects shallowly', () => { + const input = { a: { foo: 1 }, b: [1, 2] }; + const refs = toRefs(input); + expect(refs.a.value).toEqual({ foo: 1 }); + expect(refs.b.value).toEqual([1, 2]); + }); +}); + +describe('fromRefs', () => { + it('unwraps primitives', () => { + expect(fromRefs({ a: { value: 1 }, b: { value: 'x' } })).toEqual({ + a: 1, + b: 'x', + }); + }); + + it('unwraps empty object', () => { + expect(fromRefs({})).toEqual({}); + }); + + it('unwraps nested objects shallowly', () => { + const input = { a: { value: { foo: 1 } }, b: { value: [1, 2] } }; + expect(fromRefs(input)).toEqual({ a: { foo: 1 }, b: [1, 2] }); + }); +}); + +describe('toRefs <-> fromRefs roundtrip', () => { + it('roundtrips plain -> refs -> plain', () => { + const obj = { a: 1, b: 'x', c: [1, 2], d: { foo: 2 } }; + expect(fromRefs(toRefs(obj))).toEqual(obj); + }); + + it('roundtrips refs -> plain -> refs', () => { + const refs = { a: { value: 1 }, b: { value: 'x' }, c: { value: [1, 2] } }; + expect(toRefs(fromRefs(refs))).toEqual(refs); + }); +}); diff --git a/packages/openapi-ts/src/plugins/shared/utils/case.ts b/packages/openapi-ts/src/plugins/shared/utils/case.ts index 7bdd24404..d3253d28c 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/case.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/case.ts @@ -1,8 +1,8 @@ import ts from 'typescript'; -import type { IR } from '../../../ir/types'; -import { numberRegExp } from '../../../utils/regexp'; -import { stringCase } from '../../../utils/stringCase'; +import type { IR } from '~/ir/types'; +import { numberRegExp } from '~/utils/regexp'; +import { stringCase } from '~/utils/stringCase'; /** * Returns final field name for object properties. This might differ from the diff --git a/packages/openapi-ts/src/plugins/shared/utils/config.ts b/packages/openapi-ts/src/plugins/shared/utils/config.ts index 215095712..44f085eb7 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/config.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/config.ts @@ -1,4 +1,4 @@ -import type { Plugin } from '../../types'; +import type { Plugin } from '~/plugins/types'; export const definePluginConfig = (defaultConfig: Plugin.Config) => diff --git a/packages/openapi-ts/src/plugins/shared/utils/instance.ts b/packages/openapi-ts/src/plugins/shared/utils/instance.ts index ae77a6e30..16f25ae63 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/instance.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/instance.ts @@ -7,12 +7,20 @@ import type { SymbolIn, } from '@hey-api/codegen-core'; -import { HeyApiError } from '../../../error'; -import type { IR } from '../../../ir/types'; -import type { OpenApi } from '../../../openApi/types'; -import type { PluginConfigMap } from '../../config'; -import type { Plugin } from '../../types'; -import type { WalkEvent, WalkEventType } from '../types/instance'; +import { HeyApiError } from '~/error'; +import type { IrTopLevelKind } from '~/ir/graph'; +import { + irTopLevelKinds, + matchIrTopLevelPointer, + walkTopological, +} from '~/ir/graph'; +import type { IR } from '~/ir/types'; +import type { OpenApi } from '~/openApi/types'; +import type { PluginConfigMap } from '~/plugins/config'; +import type { Plugin } from '~/plugins/types'; +import { jsonPointerToPath } from '~/utils/ref'; + +import type { WalkEvent, WalkOptions } from '../types/instance'; const defaultGetFilePath = (symbol: Symbol): string | undefined => { if (!symbol.meta?.pluginName || typeof symbol.meta.pluginName !== 'string') { @@ -108,108 +116,105 @@ export class PluginInstance { * } * }); */ - forEach( + forEach( + ...args: [ + ...events: ReadonlyArray, + callback: (event: WalkEvent) => void, + ] + ): void; + forEach( + ...args: [ + ...events: ReadonlyArray, + callback: (event: WalkEvent) => void, + options: WalkOptions, + ] + ): void; + forEach( ...args: [ ...events: ReadonlyArray, callback: (event: WalkEvent) => void, + options: any, ] ): void { - const events = args.slice(0, -1) as ReadonlyArray; - const callback = args[args.length - 1] as (event: WalkEvent) => void; - const eventSet = new Set( - events.length - ? events - : ([ - 'operation', - 'parameter', - 'requestBody', - 'schema', - 'server', - 'webhook', - ] as ReadonlyArray), - ); - - if (eventSet.has('server') && this.context.ir.servers) { - for (const server of this.context.ir.servers) { - const event: WalkEvent<'server'> = { - _path: ['servers', String(this.context.ir.servers.indexOf(server))], - server, - type: 'server', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); - } - } + let callback: (event: WalkEvent) => void; + let events: ReadonlyArray; + let options: Required = { + order: 'topological', + }; + if (typeof args[args.length - 1] === 'function') { + events = args.slice(0, -1); + callback = args[args.length - 1]; + } else { + events = args.slice(0, -2); + callback = args[args.length - 2]; + options = { + ...options, + ...args[args.length - 1], + }; } + const eventSet = new Set(events.length ? events : irTopLevelKinds); - if (eventSet.has('schema') && this.context.ir.components?.schemas) { - for (const name in this.context.ir.components.schemas) { - const event: WalkEvent<'schema'> = { - $ref: `#/components/schemas/${name}`, - _path: ['components', 'schemas', name], - name, - schema: this.context.ir.components.schemas[name]!, - type: 'schema', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (options.order === 'declarations') { + if (eventSet.has('server') && this.context.ir.servers) { + for (const server of this.context.ir.servers) { + const event: WalkEvent<'server'> = { + _path: ['servers', String(this.context.ir.servers.indexOf(server))], + server, + type: 'server', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if (eventSet.has('parameter') && this.context.ir.components?.parameters) { - for (const name in this.context.ir.components.parameters) { - const event: WalkEvent<'parameter'> = { - $ref: `#/components/parameters/${name}`, - _path: ['components', 'parameters', name], - name, - parameter: this.context.ir.components.parameters[name]!, - type: 'parameter', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (eventSet.has('schema') && this.context.ir.components?.schemas) { + for (const name in this.context.ir.components.schemas) { + const event: WalkEvent<'schema'> = { + $ref: `#/components/schemas/${name}`, + _path: ['components', 'schemas', name], + name, + schema: this.context.ir.components.schemas[name]!, + type: 'schema', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if ( - eventSet.has('requestBody') && - this.context.ir.components?.requestBodies - ) { - for (const name in this.context.ir.components.requestBodies) { - const event: WalkEvent<'requestBody'> = { - $ref: `#/components/requestBodies/${name}`, - _path: ['components', 'requestBodies', name], - name, - requestBody: this.context.ir.components.requestBodies[name]!, - type: 'requestBody', - }; - try { - callback(event as WalkEvent); - } catch (error) { - this.forEachError(error, event); + if (eventSet.has('parameter') && this.context.ir.components?.parameters) { + for (const name in this.context.ir.components.parameters) { + const event: WalkEvent<'parameter'> = { + $ref: `#/components/parameters/${name}`, + _path: ['components', 'parameters', name], + name, + parameter: this.context.ir.components.parameters[name]!, + type: 'parameter', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } } } - } - if (eventSet.has('operation') && this.context.ir.paths) { - for (const path in this.context.ir.paths) { - const pathItem = - this.context.ir.paths[path as keyof typeof this.context.ir.paths]; - for (const _method in pathItem) { - const method = _method as keyof typeof pathItem; - const event: WalkEvent<'operation'> = { - _path: ['paths', path, method], - method, - operation: pathItem[method]!, - path, - type: 'operation', + if ( + eventSet.has('requestBody') && + this.context.ir.components?.requestBodies + ) { + for (const name in this.context.ir.components.requestBodies) { + const event: WalkEvent<'requestBody'> = { + $ref: `#/components/requestBodies/${name}`, + _path: ['components', 'requestBodies', name], + name, + requestBody: this.context.ir.components.requestBodies[name]!, + type: 'requestBody', }; try { callback(event as WalkEvent); @@ -218,27 +223,116 @@ export class PluginInstance { } } } - } - if (eventSet.has('webhook') && this.context.ir.webhooks) { - for (const key in this.context.ir.webhooks) { - const webhook = this.context.ir.webhooks[key]; - for (const _method in webhook) { - const method = _method as keyof typeof webhook; - const event: WalkEvent<'webhook'> = { - _path: ['webhooks', key, method], - key, - method, - operation: webhook[method]!, - type: 'webhook', - }; + if (eventSet.has('operation') && this.context.ir.paths) { + for (const path in this.context.ir.paths) { + const pathItem = + this.context.ir.paths[path as keyof typeof this.context.ir.paths]; + for (const _method in pathItem) { + const method = _method as keyof typeof pathItem; + const event: WalkEvent<'operation'> = { + _path: ['paths', path, method], + method, + operation: pathItem[method]!, + path, + type: 'operation', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } + } + } + } + + if (eventSet.has('webhook') && this.context.ir.webhooks) { + for (const key in this.context.ir.webhooks) { + const webhook = this.context.ir.webhooks[key]; + for (const _method in webhook) { + const method = _method as keyof typeof webhook; + const event: WalkEvent<'webhook'> = { + _path: ['webhooks', key, method], + key, + method, + operation: webhook[method]!, + type: 'webhook', + }; + try { + callback(event as WalkEvent); + } catch (error) { + this.forEachError(error, event); + } + } + } + } + } else if (options.order === 'topological' && this.context.graph) { + walkTopological(this.context.graph, (pointer, nodeInfo) => { + const result = matchIrTopLevelPointer(pointer); + if (!result.matched || !eventSet.has(result.kind)) return; + let event: WalkEvent | undefined; + switch (result.kind) { + case 'operation': + event = { + _path: jsonPointerToPath(pointer), + method: nodeInfo.key as keyof IR.PathItemObject, + operation: nodeInfo.node as IR.OperationObject, + path: jsonPointerToPath(pointer)[1]!, + type: result.kind, + } satisfies WalkEvent<'operation'>; + break; + case 'parameter': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + parameter: nodeInfo.node as IR.ParameterObject, + type: result.kind, + } satisfies WalkEvent<'parameter'>; + break; + case 'requestBody': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + requestBody: nodeInfo.node as IR.RequestBodyObject, + type: result.kind, + } satisfies WalkEvent<'requestBody'>; + break; + case 'schema': + event = { + $ref: pointer, + _path: jsonPointerToPath(pointer), + name: nodeInfo.key as string, + schema: nodeInfo.node as IR.SchemaObject, + type: result.kind, + } satisfies WalkEvent<'schema'>; + break; + case 'server': + event = { + _path: jsonPointerToPath(pointer), + server: nodeInfo.node as IR.ServerObject, + type: result.kind, + } satisfies WalkEvent<'server'>; + break; + case 'webhook': + event = { + _path: jsonPointerToPath(pointer), + key: jsonPointerToPath(pointer)[1]!, + method: nodeInfo.key as keyof IR.PathItemObject, + operation: nodeInfo.node as IR.OperationObject, + type: result.kind, + } satisfies WalkEvent<'webhook'>; + break; + } + if (event) { try { callback(event as WalkEvent); } catch (error) { this.forEachError(error, event); } } - } + }); } } @@ -354,6 +448,10 @@ export class PluginInstance { return (defaultGetKind(operation) ?? []).includes(kind); } + isSymbolRegistered(symbolIdOrSelector: number | Selector): boolean { + return this.gen.symbols.isRegistered(symbolIdOrSelector); + } + referenceSymbol(symbolIdOrSelector: number | Selector): Symbol { return this.gen.symbols.reference(symbolIdOrSelector); } diff --git a/packages/openapi-ts/src/plugins/shared/utils/operation.ts b/packages/openapi-ts/src/plugins/shared/utils/operation.ts index ab6e145e5..e5b66b0c1 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/operation.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/operation.ts @@ -1,6 +1,6 @@ -import { hasOperationDataRequired } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { getClientPlugin } from '../../@hey-api/client-core/utils'; +import { hasOperationDataRequired } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; export const isOperationOptionsRequired = ({ context, diff --git a/packages/openapi-ts/src/plugins/shared/utils/refs.ts b/packages/openapi-ts/src/plugins/shared/utils/refs.ts new file mode 100644 index 000000000..616136906 --- /dev/null +++ b/packages/openapi-ts/src/plugins/shared/utils/refs.ts @@ -0,0 +1,66 @@ +import type { FromRefs, ToRefs } from '../types/refs'; + +/** + * Wraps a single value in a Ref object. + * + * @example + * ```ts + * const r = toRef(123); // { value: 123 } + * console.log(r.value); // 123 + * ``` + */ +export const toRef = (value: T): { value: T } => ({ value }); + +/** + * Unwraps a single Ref object to its value. + * + * @example + * ```ts + * const r = { value: 42 }; + * const n = fromRef(r); // 42 + * console.log(n); // 42 + * ``` + */ +export const fromRef = (ref: { value: T }): T => ref.value; + +/** + * Converts an object of Refs back to a plain object (unwraps all refs). + * + * @example + * ```ts + * const refs = { a: { value: 1 }, b: { value: "x" } }; + * const plain = fromRefs(refs); // { a: 1, b: "x" } + * ``` + */ +export const fromRefs = >>( + obj: T, +): FromRefs => { + const result = {} as FromRefs; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = fromRef(obj[key]!) as (typeof result)[typeof key]; + } + } + return result; +}; + +/** + * Converts a plain object to an object of Refs (deep, per property). + * + * @example + * ```ts + * const obj = { a: 1, b: "x" }; + * const refs = toRefs(obj); // { a: { value: 1 }, b: { value: "x" } } + * ``` + */ +export const toRefs = >( + obj: T, +): ToRefs => { + const result = {} as ToRefs; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + result[key] = toRef(obj[key]); + } + } + return result; +}; diff --git a/packages/openapi-ts/src/plugins/shared/utils/schema.ts b/packages/openapi-ts/src/plugins/shared/utils/schema.ts index 5902fd550..93e814d04 100644 --- a/packages/openapi-ts/src/plugins/shared/utils/schema.ts +++ b/packages/openapi-ts/src/plugins/shared/utils/schema.ts @@ -1,6 +1,6 @@ -import type { IR } from '../../../ir/types'; -import type { Comments } from '../../../tsc'; -import { escapeComment } from '../../../utils/escape'; +import type { IR } from '~/ir/types'; +import type { Comments } from '~/tsc'; +import { escapeComment } from '~/utils/escape'; export const createSchemaComment = ({ schema, diff --git a/packages/openapi-ts/src/plugins/types.d.ts b/packages/openapi-ts/src/plugins/types.d.ts index 79f92e220..a0876813b 100644 --- a/packages/openapi-ts/src/plugins/types.d.ts +++ b/packages/openapi-ts/src/plugins/types.d.ts @@ -1,10 +1,10 @@ -import type { ValueToObject } from '../config/utils/config'; -import type { Package } from '../config/utils/package'; -import type { IR } from '../ir/types'; -import type { OpenApi as LegacyOpenApi } from '../openApi'; -import type { Client as LegacyClient } from '../types/client'; -import type { Files } from '../types/utils'; -import type { PluginInstance } from './shared/utils/instance'; +import type { ValueToObject } from '~/config/utils/config'; +import type { Package } from '~/config/utils/package'; +import type { IR } from '~/ir/types'; +import type { OpenApi as LegacyOpenApi } from '~/openApi'; +import type { PluginInstance } from '~/plugins/shared/utils/instance'; +import type { Client as LegacyClient } from '~/types/client'; +import type { Files } from '~/types/utils'; export type PluginClientNames = | '@hey-api/client-angular' @@ -19,7 +19,7 @@ export type PluginClientNames = | 'legacy/node' | 'legacy/xhr'; -export type PluginValidatorNames = 'valibot' | 'zod'; +export type PluginValidatorNames = 'arktype' | 'valibot' | 'zod'; export type PluginNames = | PluginClientNames diff --git a/packages/openapi-ts/src/plugins/valibot/api.ts b/packages/openapi-ts/src/plugins/valibot/api.ts index be4b955e8..4eb3bf2ce 100644 --- a/packages/openapi-ts/src/plugins/valibot/api.ts +++ b/packages/openapi-ts/src/plugins/valibot/api.ts @@ -1,18 +1,17 @@ import type { Selector } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; -import type { Plugin } from '../types'; -import { identifiers } from './constants'; -import type { ValibotPlugin } from './types'; +import type { Plugin } from '~/plugins/types'; -type SelectorType = 'data' | 'import' | 'ref' | 'responses' | 'webhook-request'; +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV1, createResponseValidatorV1 } from './v1/api'; -type ValidatorArgs = { - operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; -}; +type SelectorType = + | 'data' + | 'external' + | 'ref' + | 'responses' + | 'webhook-request'; export type IApi = { createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; @@ -23,101 +22,27 @@ export type IApi = { * @param type Selector type. * @param value Depends on `type`: * - `data`: `operation.id` string - * - `import`: headless symbols representing module imports + * - `external`: external modules * - `ref`: `$ref` JSON pointer * - `responses`: `operation.id` string * - `webhook-request`: `operation.id` string * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'valibot'>) {} - createRequestValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('data', operation.id), - ); - if (!symbol) return; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.async.parseAsync, - }), - parameters: [ - tsc.identifier({ text: symbol.placeholder }), - tsc.identifier({ text: dataParameterName }), - ], - }), - }), - }), - ], - }); + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createRequestValidatorV1(args); } - createResponseValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('responses', operation.id), - ); - if (!symbol) return; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.async.parseAsync, - }), - parameters: [ - tsc.identifier({ text: symbol.placeholder }), - tsc.identifier({ text: dataParameterName }), - ], - }), - }), - }), - ], - }); + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + return createResponseValidatorV1(args); } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/valibot/config.ts b/packages/openapi-ts/src/plugins/valibot/config.ts index a6f4d85f6..96af788c3 100644 --- a/packages/openapi-ts/src/plugins/valibot/config.ts +++ b/packages/openapi-ts/src/plugins/valibot/config.ts @@ -1,4 +1,5 @@ -import { definePluginConfig } from '../shared/utils/config'; +import { definePluginConfig } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import type { ValibotPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/valibot/plugin.ts b/packages/openapi-ts/src/plugins/valibot/plugin.ts index 00cc6814d..4a72ae614 100644 --- a/packages/openapi-ts/src/plugins/valibot/plugin.ts +++ b/packages/openapi-ts/src/plugins/valibot/plugin.ts @@ -1,1375 +1,4 @@ -import type { Symbol } from '@hey-api/codegen-core'; -import ts from 'typescript'; - -import { deduplicateSchema } from '../../ir/schema'; -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { tsc } from '../../tsc'; -import type { StringCase, StringName } from '../../types/case'; -import { jsonPointerToPath, refToName } from '../../utils/ref'; -import { numberRegExp } from '../../utils/regexp'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { createSchemaComment } from '../shared/utils/schema'; -import type { SchemaWithType } from '../zod/shared/types'; -import { identifiers } from './constants'; -import { - INTEGER_FORMATS, - isIntegerFormat, - needsBigIntForFormat, - numberParameter, -} from './number-helpers'; -import { operationToValibotSchema } from './operation'; import type { ValibotPlugin } from './types'; -import { webhookToValibotSchema } from './webhook'; - -export interface State { - circularReferenceTracker: Set; - hasCircularReference: boolean; - nameCase: StringCase; - nameTransformer: StringName; -} - -type SchemaToValibotSchemaOptions = { - /** - * Path to the schema in the intermediary representation. - */ - _path: ReadonlyArray; - plugin: ValibotPlugin['Instance']; -}; - -const pipesToExpression = ({ - pipes, - plugin, -}: { - pipes: Array; - plugin: ValibotPlugin['Instance']; -}) => { - if (pipes.length === 1) { - return pipes[0]!; - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.methods.pipe, - }), - parameters: pipes, - }); - return expression; -}; - -const arrayTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'array'>; - state: State; -}): ts.Expression => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const functionName = tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.array, - }); - - const pipes: Array = []; - - if (!schema.items) { - const expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - pipes.push(expression); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - - if (itemExpressions.length === 1) { - const expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - pipes.push(expression); - } else { - if (schema.logicalOperator === 'and') { - // TODO: parser - handle intersection - // return tsc.typeArrayNode( - // tsc.typeIntersectionNode({ types: itemExpressions }), - // ); - } - - // TODO: parser - handle union - // return tsc.typeArrayNode(tsc.typeUnionNode({ types: itemExpressions })); - - const expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - pipes.push(expression); - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - pipes.push(expression); - } else { - if (schema.minItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - pipes.push(expression); - } - - if (schema.maxItems !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - pipes.push(expression); - } - } - - return pipesToExpression({ pipes, plugin }); -}; - -const booleanTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'boolean'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.boolean, - }), - }); - return expression; -}; - -const enumTypeToValibotSchema = ({ - _path, - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'enum'>; -}): ts.CallExpression => { - const enumMembers: Array = []; - - let isNullable = false; - - for (const item of schema.items ?? []) { - // Zod supports only string enums - if (item.type === 'string' && typeof item.const === 'string') { - enumMembers.push( - tsc.stringLiteral({ - text: item.const, - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!enumMembers.length) { - return unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }); - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - let resultExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.picklist, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - - if (isNullable) { - resultExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.nullable, - }), - parameters: [resultExpression], - }); - } - - return resultExpression; -}; - -const neverTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'never'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.never, - }), - }); - return expression; -}; - -const nullTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'null'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.null, - }), - }); - return expression; -}; - -const numberTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'integer' | 'number'>; -}) => { - const format = schema.format; - const isInteger = schema.type === 'integer'; - const isBigInt = needsBigIntForFormat(format); - const formatInfo = isIntegerFormat(format) ? INTEGER_FORMATS[format] : null; - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - // Return early if const is defined since we can create a literal type directly without additional validation - if (schema.const !== undefined && schema.const !== null) { - const constValue = schema.const; - let literalValue; - - // Case 1: Number with no format -> generate literal with the number - if (typeof constValue === 'number' && !format) { - literalValue = tsc.ots.number(constValue); - } - // Case 2: Number with format -> check if format needs BigInt, generate appropriate literal - else if (typeof constValue === 'number' && format) { - if (isBigInt) { - // Format requires BigInt, convert number to BigInt - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(constValue.toString())], - }); - } else { - // Regular format, use number as-is - literalValue = tsc.ots.number(constValue); - } - } - // Case 3: Format that allows string -> generate BigInt literal (for int64/uint64 formats) - else if (typeof constValue === 'string' && isBigInt) { - // Remove 'n' suffix if present in string - const cleanString = constValue.endsWith('n') - ? constValue.slice(0, -1) - : constValue; - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(cleanString)], - }); - } - // Case 4: Const is typeof bigint (literal) -> transform from literal to BigInt() - else if (typeof constValue === 'bigint') { - // Convert BigInt to string and remove 'n' suffix that toString() adds - const bigintString = constValue.toString(); - const cleanString = bigintString.endsWith('n') - ? bigintString.slice(0, -1) - : bigintString; - literalValue = tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(cleanString)], - }); - } - // Default case: use value as-is for other types - else { - literalValue = tsc.valueToExpression({ value: constValue }); - } - - return tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [literalValue], - }); - } - - const pipes: Array = []; - - // For bigint formats (int64, uint64), create union of number, string, and bigint with transform - if (isBigInt) { - const unionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.number, - }), - }), - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - }), - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.bigInt, - }), - }), - ], - multiLine: false, - }), - ], - }); - pipes.push(unionExpression); - - // Add transform to convert to BigInt - const transformExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.transform, - }), - parameters: [ - tsc.arrowFunction({ - parameters: [{ name: 'x' }], - statements: tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.identifier({ text: 'x' })], - }), - }), - ], - }); - pipes.push(transformExpression); - } else { - // For regular number formats, use number schema - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.number, - }), - }); - pipes.push(expression); - } - - // Add integer validation for integer types (except when using bigint union) - if (!isBigInt && isInteger) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.integer, - }), - }); - pipes.push(expression); - } - - // Add format-specific range validations - if (formatInfo) { - const minValue = formatInfo.min; - const maxValue = formatInfo.max; - const minErrorMessage = formatInfo.minError; - const maxErrorMessage = formatInfo.maxError; - - // Add minimum value validation - const minExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minValue, - }), - parameters: [ - isBigInt - ? tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(minValue.toString())], - }) - : tsc.ots.number(minValue as number), - tsc.ots.string(minErrorMessage), - ], - }); - pipes.push(minExpression); - - // Add maximum value validation - const maxExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxValue, - }), - parameters: [ - isBigInt - ? tsc.callExpression({ - functionName: 'BigInt', - parameters: [tsc.ots.string(maxValue.toString())], - }) - : tsc.ots.number(maxValue as number), - tsc.ots.string(maxErrorMessage), - ], - }); - pipes.push(maxExpression); - } - - if (schema.exclusiveMinimum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.gtValue, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - pipes.push(expression); - } else if (schema.minimum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minValue, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - pipes.push(expression); - } - - if (schema.exclusiveMaximum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.ltValue, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - pipes.push(expression); - } else if (schema.maximum !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxValue, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - pipes.push(expression); - } - - return pipesToExpression({ pipes, plugin }); -}; - -const objectTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'object'>; - state: State; -}): { - anyType: string; - expression: ts.CallExpression; -} => { - // TODO: parser - handle constants - const properties: Array = []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'properties', name], - optional: !isRequired, - plugin, - schema: property, - state, - }); - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = name; - } - // TODO: parser - abstract safe property name logic - if ( - ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && - !name.startsWith("'") && - !name.endsWith("'") - ) { - propertyName = `'${name}'`; - } - properties.push( - tsc.propertyAssignment({ - initializer: pipesToExpression({ pipes: schemaPipes, plugin }), - name: propertyName, - }), - ); - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if ( - schema.additionalProperties && - schema.additionalProperties.type === 'object' && - !Object.keys(properties).length - ) { - const pipes = schemaToValibotSchema({ - _path: [..._path, 'additionalProperties'], - plugin, - schema: schema.additionalProperties, - state, - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - parameters: [], - }), - pipesToExpression({ pipes, plugin }), - ], - }); - return { - anyType: 'AnyZodObject', - expression, - }; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - return { - // Zod uses AnyZodObject here, maybe we want to be more specific too - anyType: identifiers.types.GenericSchema.text, - expression, - }; -}; - -const stringTypeToValibotSchema = ({ - plugin, - schema, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'string'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; - } - - const pipes: Array = []; - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.string, - }), - }); - pipes.push(expression); - - if (schema.format) { - switch (schema.format) { - case 'date': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.isoDate, - }), - }), - ); - break; - case 'date-time': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.isoTimestamp, - }), - }), - ); - break; - case 'ipv4': - case 'ipv6': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.ip, - }), - }), - ); - break; - case 'uri': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.url, - }), - }), - ); - break; - case 'email': - case 'time': - case 'uuid': - pipes.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: tsc.identifier({ text: schema.format }), - }), - }), - ); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); - } else { - if (schema.minLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - pipes.push(expression); - } - - if (schema.maxLength !== undefined) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - pipes.push(expression); - } - } - - if (schema.pattern) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - pipes.push(expression); - } - - return pipesToExpression({ pipes, plugin }); -}; - -const tupleTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'tuple'>; - state: State; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return expression; - } - - if (schema.items) { - const tupleElements = schema.items.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return expression; - } - - return unknownTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - }); -}; - -const undefinedTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'undefined'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.undefined, - }), - }); - return expression; -}; - -const unknownTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'unknown'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.unknown, - }), - }); - return expression; -}; - -const voidTypeToValibotSchema = ({ - plugin, -}: SchemaToValibotSchemaOptions & { - schema: SchemaWithType<'void'>; -}) => { - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.void, - }), - }); - return expression; -}; - -const schemaTypeToValibotSchema = ({ - _path, - plugin, - schema, - state, -}: SchemaToValibotSchemaOptions & { - schema: IR.SchemaObject; - state: State; -}): { - anyType?: string; - expression: ts.Expression; -} => { - switch (schema.type as Required['type']) { - case 'array': - return { - expression: arrayTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }), - }; - case 'boolean': - return { - expression: booleanTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'boolean'>, - }), - }; - case 'enum': - return { - expression: enumTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'enum'>, - }), - }; - case 'integer': - case 'number': - return { - expression: numberTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - case 'never': - return { - expression: neverTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'never'>, - }), - }; - case 'null': - return { - expression: nullTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'null'>, - }), - }; - case 'object': - return objectTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - // For string schemas with int64/uint64 formats, use number handler to generate union with transform - if (schema.format === 'int64' || schema.format === 'uint64') { - return { - expression: numberTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - } - return { - expression: stringTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'string'>, - }), - }; - case 'tuple': - return { - expression: tupleTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }), - }; - case 'undefined': - return { - expression: undefinedTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'undefined'>, - }), - }; - case 'unknown': - return { - expression: unknownTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'unknown'>, - }), - }; - case 'void': - return { - expression: voidTypeToValibotSchema({ - _path, - plugin, - schema: schema as SchemaWithType<'void'>, - }), - }; - } -}; - -export const schemaToValibotSchema = ({ - $ref, - _path = [], - optional, - plugin, - schema, - state, - symbol, -}: SchemaToValibotSchemaOptions & { - /** - * When $ref is supplied, a node will be emitted to the file. - */ - $ref?: string; - /** - * Accept `optional` to handle optional object properties. We can't handle - * this inside the object function because `.optional()` must come before - * `.default()` which is handled in this function. - */ - optional?: boolean; - schema: IR.SchemaObject; - state: State; - symbol?: Symbol; -}): Array => { - let anyType: string | undefined; - let pipes: Array = []; - - if ($ref) { - state.circularReferenceTracker.add($ref); - - if (!symbol) { - const selector = plugin.api.getSelector('ref', $ref); - if (!plugin.getSymbol(selector)) { - symbol = plugin.referenceSymbol(selector); - } - } - } - - const vSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'valibot'), - ); - - if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.has(schema.$ref); - - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const selector = plugin.api.getSelector('ref', schema.$ref); - let refSymbol = plugin.getSymbol(selector); - if (!refSymbol) { - const ref = plugin.context.resolveIrRef(schema.$ref); - const schemaPipes = schemaToValibotSchema({ - $ref: schema.$ref, - _path: jsonPointerToPath(schema.$ref), - plugin, - schema: ref, - state, - }); - pipes.push(...schemaPipes); - - refSymbol = plugin.getSymbol(selector); - } - - if (refSymbol) { - const refIdentifier = tsc.identifier({ text: refSymbol.placeholder }); - if (isCircularReference) { - const lazyExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.lazy, - }), - parameters: [ - tsc.arrowFunction({ - statements: [ - tsc.returnStatement({ - expression: refIdentifier, - }), - ], - }), - ], - }); - pipes.push(lazyExpression); - state.hasCircularReference = true; - } else { - pipes.push(refIdentifier); - } - } - } else if (schema.type) { - const valibotSchema = schemaTypeToValibotSchema({ - _path, - plugin, - schema, - state, - }); - anyType = valibotSchema.anyType; - pipes.push(valibotSchema.expression); - - if (plugin.config.metadata && schema.description) { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.metadata, - }), - parameters: [ - tsc.objectExpression({ - obj: [ - { - key: 'description', - value: tsc.stringLiteral({ text: schema.description }), - }, - ], - }), - ], - }); - pipes.push(expression); - } - } else if (schema.items) { - schema = deduplicateSchema({ schema }); - - if (schema.items) { - const itemTypes = schema.items.map((item, index) => { - const schemaPipes = schemaToValibotSchema({ - _path: [..._path, 'items', index], - plugin, - schema: item, - state, - }); - return pipesToExpression({ pipes: schemaPipes, plugin }); - }); - - if (schema.logicalOperator === 'and') { - const intersectExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.intersect, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemTypes, - }), - ], - }); - pipes.push(intersectExpression); - } else { - const unionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemTypes, - }), - ], - }); - pipes.push(unionExpression); - } - } else { - const schemaPipes = schemaToValibotSchema({ - _path, - plugin, - schema, - state, - }); - pipes.push(...schemaPipes); - } - } else { - // catch-all fallback for failed schemas - const valibotSchema = schemaTypeToValibotSchema({ - _path, - plugin, - schema: { - type: 'unknown', - }, - state, - }); - anyType = valibotSchema.anyType; - pipes.push(valibotSchema.expression); - } - - if ($ref) { - state.circularReferenceTracker.delete($ref); - } - - if (pipes.length) { - if (schema.accessScope === 'read') { - const readonlyExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.actions.readonly, - }), - }); - pipes.push(readonlyExpression); - } - } - - if (pipes.length) { - let callParameter: ts.Expression | undefined; - - if (schema.default !== undefined) { - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - callParameter = numberParameter({ isBigInt, value: schema.default }); - if (callParameter) { - pipes = [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.optional, - }), - parameters: [pipesToExpression({ pipes, plugin }), callParameter], - }), - ]; - } - } - - if (optional && !callParameter) { - pipes = [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: identifiers.schemas.optional, - }), - parameters: [pipesToExpression({ pipes, plugin })], - }), - ]; - } - } - - if (symbol) { - if ($ref) { - symbol = plugin.registerSymbol({ - exported: true, - meta: { - resourceType: pathToSymbolResourceType(_path), - }, - name: buildName({ - config: { - case: state.nameCase, - name: state.nameTransformer, - }, - name: refToName($ref), - }), - selector: plugin.api.getSelector('ref', $ref), - }); - } - const statement = tsc.constVariable({ - comment: plugin.config.comments - ? createSchemaComment({ schema }) - : undefined, - exportConst: symbol.exported, - expression: pipesToExpression({ pipes, plugin }), - name: symbol.placeholder, - typeName: state.hasCircularReference - ? (tsc.propertyAccessExpression({ - expression: vSymbol.placeholder, - name: anyType || identifiers.types.GenericSchema.text, - }) as unknown as ts.TypeNode) - : undefined, - }); - plugin.setSymbolValue(symbol, statement); - return []; - } - - return pipes; -}; - -export const handler: ValibotPlugin['Handler'] = ({ plugin }) => { - plugin.registerSymbol({ - external: 'valibot', - meta: { importKind: 'namespace' }, - name: 'v', - selector: plugin.api.getSelector('import', 'valibot'), - }); - - plugin.forEach( - 'operation', - 'parameter', - 'requestBody', - 'schema', - 'webhook', - (event) => { - const state: State = { - circularReferenceTracker: new Set(), - hasCircularReference: false, - nameCase: plugin.config.definitions.case, - nameTransformer: plugin.config.definitions.name, - }; +import { handlerV1 } from './v1/plugin'; - switch (event.type) { - case 'operation': - operationToValibotSchema({ - _path: event._path, - operation: event.operation, - plugin, - state, - }); - break; - case 'parameter': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.parameter.schema, - state, - }); - break; - case 'requestBody': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.requestBody.schema, - state, - }); - break; - case 'schema': - schemaToValibotSchema({ - $ref: event.$ref, - _path: event._path, - plugin, - schema: event.schema, - state, - }); - break; - case 'webhook': - webhookToValibotSchema({ - _path: event._path, - operation: event.operation, - plugin, - state, - }); - break; - } - }, - ); -}; +export const handler: ValibotPlugin['Handler'] = (args) => handlerV1(args); diff --git a/packages/openapi-ts/src/plugins/valibot/number-helpers.ts b/packages/openapi-ts/src/plugins/valibot/shared/numbers.ts similarity index 97% rename from packages/openapi-ts/src/plugins/valibot/number-helpers.ts rename to packages/openapi-ts/src/plugins/valibot/shared/numbers.ts index 8fa532a72..18105e917 100644 --- a/packages/openapi-ts/src/plugins/valibot/number-helpers.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/numbers.ts @@ -1,6 +1,5 @@ -import { tsc } from '../../tsc'; +import { tsc } from '~/tsc'; -// Integer format ranges and properties export const INTEGER_FORMATS = { int16: { max: 32767, diff --git a/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts b/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts new file mode 100644 index 000000000..bac6dfec1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/shared/types.d.ts @@ -0,0 +1,25 @@ +import type { IR } from '~/ir/types'; +import type { ToRefs } from '~/plugins/shared/types/refs'; +import type { StringCase, StringName } from '~/types/case'; + +import type { ValibotPlugin } from '../types'; + +export type IrSchemaToAstOptions = { + plugin: ValibotPlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { + /** + * Path to the schema in the intermediary representation. + */ + _path: ReadonlyArray; + hasLazyExpression: boolean; + nameCase: StringCase; + nameTransformer: StringName; +}; + +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ValibotPlugin['Instance']; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/types.d.ts b/packages/openapi-ts/src/plugins/valibot/types.d.ts index 2d560d4ea..c6e5a80be 100644 --- a/packages/openapi-ts/src/plugins/valibot/types.d.ts +++ b/packages/openapi-ts/src/plugins/valibot/types.d.ts @@ -1,5 +1,6 @@ -import type { StringCase, StringName } from '../../types/case'; -import type { DefinePlugin, Plugin } from '../types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'valibot'> & diff --git a/packages/openapi-ts/src/plugins/valibot/v1/api.ts b/packages/openapi-ts/src/plugins/valibot/v1/api.ts new file mode 100644 index 000000000..f0f40109f --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/api.ts @@ -0,0 +1,86 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +import type { ValidatorArgs } from '../shared/types'; +import { identifiers } from './constants'; + +export const createRequestValidatorV1 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.async.parseAsync, + }), + parameters: [ + tsc.identifier({ text: symbol.placeholder }), + tsc.identifier({ text: dataParameterName }), + ], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV1 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.async.parseAsync, + }), + parameters: [ + tsc.identifier({ text: symbol.placeholder }), + tsc.identifier({ text: dataParameterName }), + ], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/constants.ts b/packages/openapi-ts/src/plugins/valibot/v1/constants.ts similarity index 99% rename from packages/openapi-ts/src/plugins/valibot/constants.ts rename to packages/openapi-ts/src/plugins/valibot/v1/constants.ts index 5f1790715..e49e120a4 100644 --- a/packages/openapi-ts/src/plugins/valibot/constants.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/constants.ts @@ -1,4 +1,4 @@ -import { tsc } from '../../tsc'; +import { tsc } from '~/tsc'; export const identifiers = { /** diff --git a/packages/openapi-ts/src/plugins/valibot/operation.ts b/packages/openapi-ts/src/plugins/valibot/v1/operation.ts similarity index 81% rename from packages/openapi-ts/src/plugins/valibot/operation.ts rename to packages/openapi-ts/src/plugins/valibot/v1/operation.ts index b3e9cfd2e..a4c1ce784 100644 --- a/packages/openapi-ts/src/plugins/valibot/operation.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/operation.ts @@ -1,20 +1,18 @@ -import { operationResponsesMap } from '../../ir/operation'; -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { schemaToValibotSchema, type State } from './plugin'; -import type { ValibotPlugin } from './types'; - -export const operationToValibotSchema = ({ - _path, +import { operationResponsesMap } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRef } from '~/plugins/shared/utils/refs'; + +import type { IrSchemaToAstOptions } from '../shared/types'; +import { irSchemaToAst } from './plugin'; + +export const irOperationToAst = ({ operation, plugin, state, -}: { - _path: ReadonlyArray; +}: IrSchemaToAstOptions & { operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; - state: State; }) => { if (plugin.config.requests.enabled) { const requiredProperties = new Set(); @@ -117,16 +115,15 @@ export const operationToValibotSchema = ({ const symbol = plugin.registerSymbol({ exported: true, meta: { - resourceType: pathToSymbolResourceType(_path), + resourceType: pathToSymbolResourceType(state._path.value), }, name: buildName({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); - schemaToValibotSchema({ - _path, + irSchemaToAst({ plugin, schema: schemaData, state, @@ -139,7 +136,7 @@ export const operationToValibotSchema = ({ const { response } = operationResponsesMap(operation); if (response) { - const path = [..._path, 'responses']; + const path = [...state._path.value, 'responses']; const symbol = plugin.registerSymbol({ exported: true, meta: { @@ -149,13 +146,15 @@ export const operationToValibotSchema = ({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); - schemaToValibotSchema({ - _path: path, + irSchemaToAst({ plugin, schema: response, - state, + state: { + ...state, + _path: toRef(path), + }, symbol, }); } diff --git a/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts b/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts new file mode 100644 index 000000000..bca4920f7 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/pipesToAst.ts @@ -0,0 +1,30 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +import type { ValibotPlugin } from '../types'; +import { identifiers } from './constants'; + +export const pipesToAst = ({ + pipes, + plugin, +}: { + pipes: Array; + plugin: ValibotPlugin['Instance']; +}): ts.Expression => { + if (pipes.length === 1) { + return pipes[0]!; + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.methods.pipe, + }), + parameters: pipes, + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts new file mode 100644 index 000000000..c3e5ea5ac --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts @@ -0,0 +1,315 @@ +import type { Symbol } from '@hey-api/codegen-core'; +import type ts from 'typescript'; + +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRef, toRefs } from '~/plugins/shared/utils/refs'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; + +import { numberParameter } from '../shared/numbers'; +import type { IrSchemaToAstOptions, PluginState } from '../shared/types'; +import type { ValibotPlugin } from '../types'; +import { identifiers } from './constants'; +import { irOperationToAst } from './operation'; +import { pipesToAst } from './pipesToAst'; +import { irSchemaWithTypeToAst } from './toAst'; +import { irWebhookToAst } from './webhook'; + +export const irSchemaToAst = ({ + $ref, + optional, + plugin, + schema, + state, + symbol, +}: IrSchemaToAstOptions & { + /** + * When $ref is supplied, a node will be emitted to the file. + */ + $ref?: string; + /** + * Accept `optional` to handle optional object properties. We can't handle + * this inside the object function because `.optional()` must come before + * `.default()` which is handled in this function. + */ + optional?: boolean; + schema: IR.SchemaObject; + /** + * When symbol is supplied, the AST node will be set as its value. + */ + symbol?: Symbol; +}): Array => { + if ($ref && !symbol) { + const selector = plugin.api.selector('ref', $ref); + symbol = plugin.getSymbol(selector) || plugin.referenceSymbol(selector); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + let anyType: string | undefined; + let pipes: Array = []; + + if (schema.$ref) { + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + pipes.push(ref); + } else { + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.lazy, + }), + parameters: [ + tsc.arrowFunction({ + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + pipes.push(lazyExpression); + state.hasLazyExpression.value = true; + } + } else if (schema.type) { + const ast = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + anyType = ast.anyType; + pipes.push(ast.expression); + + if (plugin.config.metadata && schema.description) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.metadata, + }), + parameters: [ + tsc.objectExpression({ + obj: [ + { + key: 'description', + value: tsc.stringLiteral({ text: schema.description }), + }, + ], + }), + ], + }); + pipes.push(expression); + } + } else if (schema.items) { + schema = deduplicateSchema({ schema }); + + if (schema.items) { + const itemsAst = schema.items.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: itemAst, plugin }); + }); + + if (schema.logicalOperator === 'and') { + const intersectExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.intersect, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemsAst, + }), + ], + }); + pipes.push(intersectExpression); + } else { + const unionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemsAst, + }), + ], + }); + pipes.push(unionExpression); + } + } else { + const schemaPipes = irSchemaToAst({ plugin, schema, state }); + pipes.push(...schemaPipes); + } + } else { + // catch-all fallback for failed schemas + const ast = irSchemaWithTypeToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + anyType = ast.anyType; + pipes.push(ast.expression); + } + + if (pipes.length) { + if (schema.accessScope === 'read') { + const readonlyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.readonly, + }), + }); + pipes.push(readonlyExpression); + } + + let callParameter: ts.Expression | undefined; + + if (schema.default !== undefined) { + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + callParameter = numberParameter({ isBigInt, value: schema.default }); + if (callParameter) { + pipes = [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.optional, + }), + parameters: [pipesToAst({ pipes, plugin }), callParameter], + }), + ]; + } + } + + if (optional && !callParameter) { + pipes = [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.optional, + }), + parameters: [pipesToAst({ pipes, plugin })], + }), + ]; + } + } + + if (symbol) { + if ($ref) { + symbol = plugin.registerSymbol({ + exported: true, + meta: { + resourceType: pathToSymbolResourceType(state._path.value), + }, + name: buildName({ + config: { + case: state.nameCase.value, + name: state.nameTransformer.value, + }, + name: refToName($ref), + }), + selector: plugin.api.selector('ref', $ref), + }); + } + const statement = tsc.constVariable({ + comment: plugin.config.comments + ? createSchemaComment({ schema }) + : undefined, + exportConst: symbol.exported, + expression: pipesToAst({ pipes, plugin }), + name: symbol.placeholder, + typeName: state.hasLazyExpression.value + ? (tsc.propertyAccessExpression({ + expression: v.placeholder, + name: anyType || identifiers.types.GenericSchema.text, + }) as unknown as ts.TypeNode) + : undefined, + }); + plugin.setSymbolValue(symbol, statement); + return []; + } + + return pipes; +}; + +export const handlerV1: ValibotPlugin['Handler'] = ({ plugin }) => { + plugin.registerSymbol({ + external: 'valibot', + meta: { importKind: 'namespace' }, + name: 'v', + selector: plugin.api.selector('external', 'valibot.v'), + }); + + plugin.forEach( + 'operation', + 'parameter', + 'requestBody', + 'schema', + 'webhook', + (event) => { + const state = toRefs({ + _path: event._path, + hasLazyExpression: false, + nameCase: plugin.config.definitions.case, + nameTransformer: plugin.config.definitions.name, + }); + + switch (event.type) { + case 'operation': + irOperationToAst({ + operation: event.operation, + plugin, + state, + }); + break; + case 'parameter': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.parameter.schema, + state, + }); + break; + case 'requestBody': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.requestBody.schema, + state, + }); + break; + case 'schema': + irSchemaToAst({ + $ref: event.$ref, + plugin, + schema: event.schema, + state, + }); + break; + case 'webhook': + irWebhookToAst({ + operation: event.operation, + plugin, + state, + }); + break; + } + }, + ); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts new file mode 100644 index 000000000..981863a26 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts @@ -0,0 +1,128 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '~/ir/schema'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): ts.Expression => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const functionName = tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.array, + }); + + const pipes: Array = []; + + if (!schema.items) { + const expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + pipes.push(expression); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const schemaPipes = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: schemaPipes, plugin }); + }); + + if (itemExpressions.length === 1) { + const expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + pipes.push(expression); + } else { + if (schema.logicalOperator === 'and') { + // TODO: parser - handle intersection + // return tsc.typeArrayNode( + // tsc.typeIntersectionNode({ types: itemExpressions }), + // ); + } + + // TODO: parser - handle union + // return tsc.typeArrayNode(tsc.typeUnionNode({ types: itemExpressions })); + + const expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + pipes.push(expression); + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + pipes.push(expression); + } else { + if (schema.minItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + pipes.push(expression); + } + + if (schema.maxItems !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + pipes.push(expression); + } + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts new file mode 100644 index 000000000..d38f3b99e --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts @@ -0,0 +1,35 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (typeof schema.const === 'boolean') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return expression; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.boolean, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts new file mode 100644 index 000000000..233a0762e --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts @@ -0,0 +1,72 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): ts.CallExpression => { + const enumMembers: Array = []; + + let isNullable = false; + + for (const item of schema.items ?? []) { + // Zod supports only string enums + if (item.type === 'string' && typeof item.const === 'string') { + enumMembers.push( + tsc.stringLiteral({ + text: item.const, + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!enumMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + let resultExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.picklist, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + + if (isNullable) { + resultExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.nullable, + }), + parameters: [resultExpression], + }); + } + + return resultExpression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts new file mode 100644 index 000000000..c05801d8b --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts @@ -0,0 +1,122 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): { + anyType?: string; + expression: ts.Expression; +} => { + switch (schema.type) { + case 'array': + return { + expression: arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }), + }; + case 'boolean': + return { + expression: booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }), + }; + case 'enum': + return { + expression: enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }), + }; + case 'integer': + case 'number': + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + case 'never': + return { + expression: neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }), + }; + case 'null': + return { + expression: nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }), + }; + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + // For string schemas with int64/uint64 formats, use number handler to generate union with transform + if (schema.format === 'int64' || schema.format === 'uint64') { + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + } + return { + expression: stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }), + }; + case 'tuple': + return { + expression: tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }), + }; + case 'undefined': + return { + expression: undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }), + }; + case 'unknown': + return { + expression: unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }), + }; + case 'void': + return { + expression: voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }), + }; + } +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts new file mode 100644 index 000000000..a58bafb33 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts @@ -0,0 +1,22 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.never, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts new file mode 100644 index 000000000..952f604c5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts @@ -0,0 +1,22 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.null, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts new file mode 100644 index 000000000..c0898efd3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts @@ -0,0 +1,255 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { + INTEGER_FORMATS, + isIntegerFormat, + needsBigIntForFormat, + numberParameter, +} from '../../shared/numbers'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}) => { + const format = schema.format; + const isInteger = schema.type === 'integer'; + const isBigInt = needsBigIntForFormat(format); + const formatInfo = isIntegerFormat(format) ? INTEGER_FORMATS[format] : null; + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + // Return early if const is defined since we can create a literal type directly without additional validation + if (schema.const !== undefined && schema.const !== null) { + const constValue = schema.const; + let literalValue; + + // Case 1: Number with no format -> generate literal with the number + if (typeof constValue === 'number' && !format) { + literalValue = tsc.ots.number(constValue); + } + // Case 2: Number with format -> check if format needs BigInt, generate appropriate literal + else if (typeof constValue === 'number' && format) { + if (isBigInt) { + // Format requires BigInt, convert number to BigInt + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(constValue.toString())], + }); + } else { + // Regular format, use number as-is + literalValue = tsc.ots.number(constValue); + } + } + // Case 3: Format that allows string -> generate BigInt literal (for int64/uint64 formats) + else if (typeof constValue === 'string' && isBigInt) { + // Remove 'n' suffix if present in string + const cleanString = constValue.endsWith('n') + ? constValue.slice(0, -1) + : constValue; + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(cleanString)], + }); + } + // Case 4: Const is typeof bigint (literal) -> transform from literal to BigInt() + else if (typeof constValue === 'bigint') { + // Convert BigInt to string and remove 'n' suffix that toString() adds + const bigintString = constValue.toString(); + const cleanString = bigintString.endsWith('n') + ? bigintString.slice(0, -1) + : bigintString; + literalValue = tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(cleanString)], + }); + } + // Default case: use value as-is for other types + else { + literalValue = tsc.valueToExpression({ value: constValue }); + } + + return tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [literalValue], + }); + } + + const pipes: Array = []; + + // For bigint formats (int64, uint64), create union of number, string, and bigint with transform + if (isBigInt) { + const unionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.number, + }), + }), + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + }), + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.bigInt, + }), + }), + ], + multiLine: false, + }), + ], + }); + pipes.push(unionExpression); + + // Add transform to convert to BigInt + const transformExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.transform, + }), + parameters: [ + tsc.arrowFunction({ + parameters: [{ name: 'x' }], + statements: tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.identifier({ text: 'x' })], + }), + }), + ], + }); + pipes.push(transformExpression); + } else { + // For regular number formats, use number schema + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.number, + }), + }); + pipes.push(expression); + } + + // Add integer validation for integer types (except when using bigint union) + if (!isBigInt && isInteger) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.integer, + }), + }); + pipes.push(expression); + } + + // Add format-specific range validations + if (formatInfo) { + const minValue = formatInfo.min; + const maxValue = formatInfo.max; + const minErrorMessage = formatInfo.minError; + const maxErrorMessage = formatInfo.maxError; + + // Add minimum value validation + const minExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minValue, + }), + parameters: [ + isBigInt + ? tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(minValue.toString())], + }) + : tsc.ots.number(minValue as number), + tsc.ots.string(minErrorMessage), + ], + }); + pipes.push(minExpression); + + // Add maximum value validation + const maxExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxValue, + }), + parameters: [ + isBigInt + ? tsc.callExpression({ + functionName: 'BigInt', + parameters: [tsc.ots.string(maxValue.toString())], + }) + : tsc.ots.number(maxValue as number), + tsc.ots.string(maxErrorMessage), + ], + }); + pipes.push(maxExpression); + } + + if (schema.exclusiveMinimum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.gtValue, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + pipes.push(expression); + } else if (schema.minimum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minValue, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + pipes.push(expression); + } + + if (schema.exclusiveMaximum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.ltValue, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + pipes.push(expression); + } else if (schema.maximum !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxValue, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + pipes.push(expression); + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts new file mode 100644 index 000000000..423ee7881 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts @@ -0,0 +1,120 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): { + anyType: string; + expression: ts.CallExpression; +} => { + // TODO: parser - handle constants + const properties: Array = []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const schemaPipes = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + // TODO: parser - abstract safe property name logic + if ( + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && + !name.startsWith("'") && + !name.endsWith("'") + ) { + propertyName = `'${name}'`; + } + properties.push( + tsc.propertyAssignment({ + initializer: pipesToAst({ pipes: schemaPipes, plugin }), + name: propertyName, + }), + ); + } + + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if ( + schema.additionalProperties && + schema.additionalProperties.type === 'object' && + !Object.keys(properties).length + ) { + const pipes = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + parameters: [], + }), + pipesToAst({ pipes, plugin }), + ], + }); + return { + anyType: 'AnyZodObject', + expression, + }; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + return { + // Zod uses AnyZodObject here, maybe we want to be more specific too + anyType: identifiers.types.GenericSchema.text, + expression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts new file mode 100644 index 000000000..7a77c9c1b --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts @@ -0,0 +1,144 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (typeof schema.const === 'string') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return expression; + } + + const pipes: Array = []; + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.string, + }), + }); + pipes.push(expression); + + if (schema.format) { + switch (schema.format) { + case 'date': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.isoDate, + }), + }), + ); + break; + case 'date-time': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.isoTimestamp, + }), + }), + ); + break; + case 'ipv4': + case 'ipv6': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.ip, + }), + }), + ); + break; + case 'uri': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.url, + }), + }), + ); + break; + case 'email': + case 'time': + case 'uuid': + pipes.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: tsc.identifier({ text: schema.format }), + }), + }), + ); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + pipes.push(expression); + } else { + if (schema.minLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + pipes.push(expression); + } + + if (schema.maxLength !== undefined) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + pipes.push(expression); + } + } + + if (schema.pattern) { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.actions.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + pipes.push(expression); + } + + return pipesToAst({ pipes, plugin }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts new file mode 100644 index 000000000..d100c5bdc --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts @@ -0,0 +1,79 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; +import { pipesToAst } from '../pipesToAst'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return expression; + } + + if (schema.items) { + const tupleElements = schema.items.map((item, index) => { + const schemaPipes = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + return pipesToAst({ pipes: schemaPipes, plugin }); + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return expression; + } + + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts new file mode 100644 index 000000000..3cc866bf5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts @@ -0,0 +1,23 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.undefined, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts new file mode 100644 index 000000000..9df535c73 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts @@ -0,0 +1,23 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.unknown, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts new file mode 100644 index 000000000..55c11dbdb --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts @@ -0,0 +1,23 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { identifiers } from '../constants'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}) => { + const v = plugin.referenceSymbol( + plugin.api.selector('external', 'valibot.v'), + ); + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: v.placeholder, + name: identifiers.schemas.void, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/valibot/webhook.ts b/packages/openapi-ts/src/plugins/valibot/v1/webhook.ts similarity index 83% rename from packages/openapi-ts/src/plugins/valibot/webhook.ts rename to packages/openapi-ts/src/plugins/valibot/v1/webhook.ts index 6400d46b7..398398e23 100644 --- a/packages/openapi-ts/src/plugins/valibot/webhook.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/webhook.ts @@ -1,19 +1,16 @@ -import type { IR } from '../../ir/types'; -import { buildName } from '../../openApi/shared/utils/name'; -import { pathToSymbolResourceType } from '../shared/utils/meta'; -import { schemaToValibotSchema, type State } from './plugin'; -import type { ValibotPlugin } from './types'; - -export const webhookToValibotSchema = ({ - _path, +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; + +import type { IrSchemaToAstOptions } from '../shared/types'; +import { irSchemaToAst } from './plugin'; + +export const irWebhookToAst = ({ operation, plugin, state, -}: { - _path: ReadonlyArray; +}: IrSchemaToAstOptions & { operation: IR.OperationObject; - plugin: ValibotPlugin['Instance']; - state: State; }) => { if (plugin.config.webhooks.enabled) { const requiredProperties = new Set(); @@ -116,16 +113,15 @@ export const webhookToValibotSchema = ({ const symbol = plugin.registerSymbol({ exported: true, meta: { - resourceType: pathToSymbolResourceType(_path), + resourceType: pathToSymbolResourceType(state._path.value), }, name: buildName({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); - schemaToValibotSchema({ - _path, + irSchemaToAst({ plugin, schema: schemaData, state, diff --git a/packages/openapi-ts/src/plugins/zod/api.ts b/packages/openapi-ts/src/plugins/zod/api.ts index 0c78dc943..95b8aa298 100644 --- a/packages/openapi-ts/src/plugins/zod/api.ts +++ b/packages/openapi-ts/src/plugins/zod/api.ts @@ -1,15 +1,19 @@ import type { Selector } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; -import type { Plugin } from '../types'; -import { identifiers } from './constants'; -import type { ZodPlugin } from './types'; +import type { Plugin } from '~/plugins/types'; + +import { + createRequestValidatorMini, + createResponseValidatorMini, +} from './mini/api'; +import type { ValidatorArgs } from './shared/types'; +import { createRequestValidatorV3, createResponseValidatorV3 } from './v3/api'; +import { createRequestValidatorV4, createResponseValidatorV4 } from './v4/api'; type SelectorType = | 'data' - | 'import' + | 'external' | 'ref' | 'responses' | 'type-infer-data' @@ -18,11 +22,6 @@ type SelectorType = | 'type-infer-webhook-request' | 'webhook-request'; -type ValidatorArgs = { - operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; -}; - export type IApi = { createRequestValidator: (args: ValidatorArgs) => ts.ArrowFunction | undefined; createResponseValidator: ( @@ -32,7 +31,7 @@ export type IApi = { * @param type Selector type. * @param value Depends on `type`: * - `data`: `operation.id` string - * - `import`: headless symbols representing module imports + * - `external`: external modules * - `ref`: `$ref` JSON pointer * - `responses`: `operation.id` string * - `type-infer-data`: `operation.id` string @@ -42,81 +41,39 @@ export type IApi = { * - `webhook-request`: `operation.id` string * @returns Selector array */ - getSelector: (type: SelectorType, value?: string) => Selector; + selector: (type: SelectorType, value?: string) => Selector; }; export class Api implements IApi { constructor(public meta: Plugin.Name<'zod'>) {} - createRequestValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('data', operation.id), - ); - if (!symbol) return; - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: symbol.placeholder, - name: identifiers.parseAsync, - }), - parameters: [tsc.identifier({ text: dataParameterName })], - }), - }), - }), - ], - }); + createRequestValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + const { plugin } = args; + switch (plugin.config.compatibilityVersion) { + case 3: + return createRequestValidatorV3(args); + case 'mini': + return createRequestValidatorMini(args); + case 4: + default: + return createRequestValidatorV4(args); + } } - createResponseValidator({ - operation, - plugin, - }: ValidatorArgs): ts.ArrowFunction | undefined { - const symbol = plugin.getSymbol( - plugin.api.getSelector('responses', operation.id), - ); - if (!symbol) return; - - const dataParameterName = 'data'; - - return tsc.arrowFunction({ - async: true, - parameters: [ - { - name: dataParameterName, - }, - ], - statements: [ - tsc.returnStatement({ - expression: tsc.awaitExpression({ - expression: tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: symbol.placeholder, - name: identifiers.parseAsync, - }), - parameters: [tsc.identifier({ text: dataParameterName })], - }), - }), - }), - ], - }); + createResponseValidator(args: ValidatorArgs): ts.ArrowFunction | undefined { + const { plugin } = args; + switch (plugin.config.compatibilityVersion) { + case 3: + return createResponseValidatorV3(args); + case 'mini': + return createResponseValidatorMini(args); + case 4: + default: + return createResponseValidatorV4(args); + } } - getSelector(...args: ReadonlyArray): Selector { + selector(...args: ReadonlyArray): Selector { return [this.meta.name, ...(args as Selector)]; } } diff --git a/packages/openapi-ts/src/plugins/zod/config.ts b/packages/openapi-ts/src/plugins/zod/config.ts index a025ed339..bf2680141 100644 --- a/packages/openapi-ts/src/plugins/zod/config.ts +++ b/packages/openapi-ts/src/plugins/zod/config.ts @@ -1,6 +1,7 @@ import colors from 'ansi-colors'; -import { definePluginConfig, mappers } from '../shared/utils/config'; +import { definePluginConfig, mappers } from '~/plugins/shared/utils/config'; + import { Api } from './api'; import { handler } from './plugin'; import type { ZodPlugin } from './types'; diff --git a/packages/openapi-ts/src/plugins/zod/constants.ts b/packages/openapi-ts/src/plugins/zod/constants.ts index 5fbf81c8e..fe79a3555 100644 --- a/packages/openapi-ts/src/plugins/zod/constants.ts +++ b/packages/openapi-ts/src/plugins/zod/constants.ts @@ -1,5 +1,6 @@ -import { tsc } from '../../tsc'; +import { tsc } from '~/tsc'; +// TODO: this is inaccurate, it combines identifiers for all supported versions export const identifiers = { ZodMiniOptional: tsc.identifier({ text: 'ZodMiniOptional' }), ZodOptional: tsc.identifier({ text: 'ZodOptional' }), diff --git a/packages/openapi-ts/src/plugins/zod/mini/api.ts b/packages/openapi-ts/src/plugins/zod/mini/api.ts new file mode 100644 index 000000000..0b560fbb3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/api.ts @@ -0,0 +1,72 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorMini = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorMini = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts index 4c03374eb..6583b6d3c 100644 --- a/packages/openapi-ts/src/plugins/zod/mini/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/mini/plugin.ts @@ -1,1125 +1,85 @@ -import ts from 'typescript'; +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRef, toRefs } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; -import { deduplicateSchema } from '../../../ir/schema'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - if (!schema.items) { - result.expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }).expression, - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - result.expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - if (schema.logicalOperator === 'and') { - const firstSchema = schema.items![0]!; - // we want to add an intersection, but not every schema can use the same API. - // if the first item contains another array or not an object, we cannot use - // `.intersection()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: [intersectionExpression, itemExpressions[i]!], - }); - } - } - - result.expression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - const checks: Array = []; - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }), - ); - } else { - if (schema.minItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }), - ); - } - - if (schema.maxItems !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }), - ); - } - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return result as Omit; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - if (allStrings && enumMembers.length > 0) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - result.expression = literalMembers[0]; - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.nullable, - }), - parameters: [result.expression], - }); - } - - return result as Omit; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return result as Omit; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return result as Omit; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}): ts.Expression | undefined => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.int, - }), - }); - } - - const checks: Array = []; - - if (schema.exclusiveMinimum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }), - ); - } else if (schema.minimum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }), - ); - } - - if (schema.exclusiveMaximum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }), - ); - } else if (schema.maximum !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }), - ); - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - // TODO: parser - handle constants - const properties: Array = - []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertySchema = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - if (propertySchema.hasCircularReference) { - result.hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = name; - } - // TODO: parser - abstract safe property name logic - if ( - ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && - !name.startsWith("'") && - !name.endsWith("'") - ) { - propertyName = `'${name}'`; - } - - if (propertySchema.hasCircularReference) { - properties.push( - tsc.getAccessorDeclaration({ - name: propertyName, - // @ts-expect-error - returnType: propertySchema.typeName - ? tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: propertySchema.typeName, - }) - : undefined, - statements: [ - tsc.returnStatement({ - expression: propertySchema.expression, - }), - ], - }), - ); - } else { - properties.push( - tsc.propertyAssignment({ - initializer: propertySchema.expression, - name: propertyName, - }), - ); - } - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - parameters: [], - }), - zodSchema.expression, - ], - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - - return result as Omit; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.uuid, - }), - }); - break; - } - } - - const checks: Array = []; - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), - ); - } else { - if (schema.minLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.minLength, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }), - ); - } - - if (schema.maxLength !== undefined) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.maxLength, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }), - ); - } - } - - if (schema.pattern) { - checks.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }), - ); - } - - if (checks.length) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.check, - }), - parameters: checks, - }); - } - - return result as Omit; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const result: Partial> = {}; - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return result as Omit; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const itemSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - tupleElements.push(itemSchema.expression); - - if (itemSchema.hasCircularReference) { - result.hasCircularReference = true; - } - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - - return result as Omit; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return result as Omit; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return result as Omit; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}): Omit => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const result: Partial> = {}; - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return result as Omit; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }); - case 'enum': - return enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }); - case 'integer': - case 'number': - return numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }); - case 'never': - return neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }); - case 'null': - return nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }); - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }); - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }); - case 'unknown': - return unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }); - case 'void': - return voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }); - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - const isSelfReference = state.currentReferenceTracker.includes(schema.$ref); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - if (isSelfReference) { - zodSchema.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lazy, - }), - parameters: [ - tsc.arrowFunction({ - returnType: tsc.keywordTypeNode({ keyword: 'any' }), - statements: [ - tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), - }), - ], - }), - ], - }); - } else { - zodSchema.expression = tsc.identifier({ text: symbol.placeholder }); - } - zodSchema.hasCircularReference = schema.circular; + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.register, }), parameters: [ tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.globalRegistry, }), tsc.objectExpression({ @@ -1137,11 +97,14 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemSchemas = schema.items.map((item) => - schemaToZodSchema({ + const itemSchemas = schema.items.map((item, index) => + irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }), ); @@ -1154,27 +117,27 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemSchemas.map((schema) => schema.expression), }); } else { - zodSchema.expression = itemSchemas[0]!.expression; + ast.expression = itemSchemas[0]!.expression; itemSchemas.slice(1).forEach((schema) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: [ - zodSchema.expression, - schema.hasCircularReference + ast.expression, + schema.hasLazyExpression ? tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ @@ -1193,9 +156,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1206,40 +169,40 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ plugin, schema, state }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; + ast.expression = typeAst.expression; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.readonly, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.optional, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); - zodSchema.typeName = identifiers.ZodMiniOptional; + ast.typeName = identifiers.ZodMiniOptional; } if (schema.default !== undefined) { @@ -1249,71 +212,63 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers._default, }), - parameters: [zodSchema.expression, callParameter], + parameters: [ast.expression, callParameter], }); } } } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, - state: _state, -}: { - id: string; - plugin: ZodPlugin['Instance']; + state, +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: Omit; }): void => { - const state: State = { - circularReferenceTracker: [id], - hasCircularReference: false, - ..._state, - currentReferenceTracker: [id], - }; - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol && !plugin.getSymbolValue(symbol)) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1322,7 +277,7 @@ export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { external: getZodModule({ plugin }), meta: { importKind: 'namespace' }, name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1334,52 +289,68 @@ export const handlerMini: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts new file mode 100644 index 000000000..49374643f --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/array.ts @@ -0,0 +1,174 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '~/ir/schema'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + if (!schema.items) { + result.expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }).expression, + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + result.expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + if (schema.logicalOperator === 'and') { + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.intersection()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: [intersectionExpression, itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + const checks: Array = []; + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }), + ); + } else { + if (schema.minItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }), + ); + } + + if (schema.maxItems !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }), + ); + } + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts new file mode 100644 index 000000000..f555df28c --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/boolean.ts @@ -0,0 +1,35 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (typeof schema.const === 'boolean') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts new file mode 100644 index 000000000..97baffe9f --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/enum.ts @@ -0,0 +1,128 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + if (allStrings && enumMembers.length > 0) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + result.expression = literalMembers[0]; + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.nullable, + }), + parameters: [result.expression], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts new file mode 100644 index 000000000..b7400b68a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/index.ts @@ -0,0 +1,86 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }); + case 'enum': + return enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }); + case 'integer': + case 'number': + return numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }); + case 'never': + return neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }); + case 'unknown': + return unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }); + case 'void': + return voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }); + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts new file mode 100644 index 000000000..a7dcc91f1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/never.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts new file mode 100644 index 000000000..082534607 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/null.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts new file mode 100644 index 000000000..53919affd --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/number.ts @@ -0,0 +1,119 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.int, + }), + }); + } + + const checks: Array = []; + + if (schema.exclusiveMinimum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }), + ); + } else if (schema.minimum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }), + ); + } + + if (schema.exclusiveMaximum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }), + ); + } else if (schema.maximum !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }), + ); + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts new file mode 100644 index 000000000..46c8a14cd --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/object.ts @@ -0,0 +1,130 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + // TODO: parser - abstract safe property name logic + if ( + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && + !name.startsWith("'") && + !name.endsWith("'") + ) { + propertyName = `'${name}'`; + } + + if (propertyAst.hasLazyExpression) { + properties.push( + tsc.getAccessorDeclaration({ + name: propertyName, + statements: [ + tsc.returnStatement({ + expression: propertyAst.expression, + }), + ], + }), + ); + } else { + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts new file mode 100644 index 000000000..0cad6e696 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/string.ts @@ -0,0 +1,193 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (typeof schema.const === 'string') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.date, + }), + }); + break; + case 'date-time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv4, + }), + }); + break; + case 'ipv6': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv6, + }), + }); + break; + case 'time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.time, + }), + }); + break; + case 'uri': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.url, + }), + }); + break; + case 'uuid': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.uuid, + }), + }); + break; + } + } + + const checks: Array = []; + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }), + ); + } else { + if (schema.minLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.minLength, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }), + ); + } + + if (schema.maxLength !== undefined) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.maxLength, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }), + ); + } + } + + if (schema.pattern) { + checks.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }), + ); + } + + if (checks.length) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.check, + }), + parameters: checks, + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts new file mode 100644 index 000000000..068597dd3 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/tuple.ts @@ -0,0 +1,78 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const result: Partial> = {}; + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return result as Omit; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + result.hasLazyExpression = true; + } + }); + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts new file mode 100644 index 000000000..bf6640610 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/undefined.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts new file mode 100644 index 000000000..73d1909cd --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/unknown.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts new file mode 100644 index 000000000..c26bf8c33 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/mini/toAst/void.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}): Omit => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const result: Partial> = {}; + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/plugin.ts b/packages/openapi-ts/src/plugins/zod/plugin.ts index fbd86a5a3..78e159140 100644 --- a/packages/openapi-ts/src/plugins/zod/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/plugin.ts @@ -8,10 +8,9 @@ export const handler: ZodPlugin['Handler'] = (args) => { switch (plugin.config.compatibilityVersion) { case 3: return handlerV3(args); - case 4: - return handlerV4(args); case 'mini': return handlerMini(args); + case 4: default: return handlerV4(args); } diff --git a/packages/openapi-ts/src/plugins/zod/export.ts b/packages/openapi-ts/src/plugins/zod/shared/export.ts similarity index 64% rename from packages/openapi-ts/src/plugins/zod/export.ts rename to packages/openapi-ts/src/plugins/zod/shared/export.ts index 21c836849..f2a5732ef 100644 --- a/packages/openapi-ts/src/plugins/zod/export.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/export.ts @@ -1,41 +1,40 @@ import type { Symbol } from '@hey-api/codegen-core'; import type ts from 'typescript'; -import type { IR } from '../../ir/types'; -import { tsc } from '../../tsc'; -import { createSchemaComment } from '../shared/utils/schema'; -import { identifiers } from './constants'; -import type { ZodSchema } from './shared/types'; -import type { ZodPlugin } from './types'; +import type { IR } from '~/ir/types'; +import { createSchemaComment } from '~/plugins/shared/utils/schema'; +import { tsc } from '~/tsc'; -export const exportZodSchema = ({ +import { identifiers } from '../constants'; +import type { ZodPlugin } from '../types'; +import type { Ast } from './types'; + +export const exportAst = ({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }: { + ast: Ast; plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; symbol: Symbol; typeInferSymbol: Symbol | undefined; - zodSchema: ZodSchema; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); +}): void => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); const statement = tsc.constVariable({ comment: plugin.config.comments ? createSchemaComment({ schema }) : undefined, exportConst: symbol.exported, - expression: zodSchema.expression, + expression: ast.expression, name: symbol.placeholder, - typeName: zodSchema.typeName + typeName: ast.typeName ? (tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: zodSchema.typeName, + expression: z.placeholder, + name: ast.typeName, }) as unknown as ts.TypeNode) : undefined, }); @@ -52,7 +51,7 @@ export const exportZodSchema = ({ }) as unknown as ts.TypeNode, ], typeName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.infer, }) as unknown as string, }), diff --git a/packages/openapi-ts/src/plugins/zod/shared/numbers.ts b/packages/openapi-ts/src/plugins/zod/shared/numbers.ts new file mode 100644 index 000000000..2da75b3bc --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/shared/numbers.ts @@ -0,0 +1,28 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +export const numberParameter = ({ + isBigInt, + value, +}: { + isBigInt: boolean; + value: unknown; +}): ts.Expression | undefined => { + const expression = tsc.valueToExpression({ value }); + + if ( + isBigInt && + (typeof value === 'bigint' || + typeof value === 'number' || + typeof value === 'string' || + typeof value === 'boolean') + ) { + return tsc.callExpression({ + functionName: 'BigInt', + parameters: [expression], + }); + } + + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/shared/operation.ts b/packages/openapi-ts/src/plugins/zod/shared/operation.ts index b4b4f7dd1..04bcfd747 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/operation.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/operation.ts @@ -1,19 +1,24 @@ -import { operationResponsesMap } from '../../../ir/operation'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { exportZodSchema } from '../export'; -import type { ZodPlugin } from '../types'; -import type { ZodSchema } from './types'; - -export const operationToZodSchema = ({ - getZodSchema, +import { operationResponsesMap } from '~/ir/operation'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; + +import { exportAst } from './export'; +import type { Ast, IrSchemaToAstOptions } from './types'; + +export const irOperationToAst = ({ + getAst, operation, plugin, -}: { - getZodSchema: (schema: IR.SchemaObject) => ZodSchema; + state, +}: Omit & { + getAst: ( + schema: IR.SchemaObject, + path: ReadonlyArray, + ) => Ast; operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; -}) => { + state: Partial; +}): void => { if (plugin.config.requests.enabled) { const requiredProperties = new Set(); @@ -112,34 +117,40 @@ export const operationToZodSchema = ({ schemaData.required = [...requiredProperties]; - const zodSchema = getZodSchema(schemaData); + const path = state._path?.value || []; + const ast = getAst(schemaData, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.requests, name: operation.id, }), - selector: plugin.api.getSelector('data', operation.id), + selector: plugin.api.selector('data', operation.id), }); const typeInferSymbol = plugin.config.requests.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.requests.types.infer, name: operation.id, }), - selector: plugin.api.getSelector('type-infer-data', operation.id), + selector: plugin.api.selector('type-infer-data', operation.id), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: schemaData, symbol, typeInferSymbol, - zodSchema, }); } @@ -148,37 +159,43 @@ export const operationToZodSchema = ({ const { response } = operationResponsesMap(operation); if (response) { - const zodSchema = getZodSchema(response); + const path = [...(state._path?.value || []), 'responses']; + const ast = getAst(response, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.responses, name: operation.id, }), - selector: plugin.api.getSelector('responses', operation.id), + selector: plugin.api.selector('responses', operation.id), }); const typeInferSymbol = plugin.config.responses.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.responses.types.infer, name: operation.id, }), - selector: plugin.api.getSelector( + selector: plugin.api.selector( 'type-infer-responses', operation.id, ), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: response, symbol, typeInferSymbol, - zodSchema, }); } } diff --git a/packages/openapi-ts/src/plugins/zod/shared/types.d.ts b/packages/openapi-ts/src/plugins/zod/shared/types.d.ts index 6f352346f..90fcf86f1 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/types.d.ts @@ -1,25 +1,30 @@ import type ts from 'typescript'; -import type { IR } from '../../../ir/types'; +import type { IR } from '~/ir/types'; +import type { ToRefs } from '~/plugins/shared/types/refs'; -export interface SchemaWithType['type']> - extends Omit { - type: Extract['type'], T>; -} +import type { ZodPlugin } from '../types'; -export type State = { - circularReferenceTracker: Array; +export type Ast = { + expression: ts.Expression; + hasLazyExpression?: boolean; + typeName?: string | ts.Identifier; +}; + +export type IrSchemaToAstOptions = { + plugin: ZodPlugin['Instance']; + state: ToRefs; +}; + +export type PluginState = { /** - * Works the same as `circularReferenceTracker`, but it resets whenever we - * walk inside another schema. This can be used to detect if a schema - * directly references itself. + * Path to the schema in the intermediary representation. */ - currentReferenceTracker: Array; - hasCircularReference: boolean; + _path: ReadonlyArray; + hasLazyExpression: boolean; }; -export type ZodSchema = { - expression: ts.Expression; - hasCircularReference?: boolean; - typeName?: string | ts.Identifier; +export type ValidatorArgs = { + operation: IR.OperationObject; + plugin: ZodPlugin['Instance']; }; diff --git a/packages/openapi-ts/src/plugins/zod/shared/webhook.ts b/packages/openapi-ts/src/plugins/zod/shared/webhook.ts index fc753cffb..62ab05468 100644 --- a/packages/openapi-ts/src/plugins/zod/shared/webhook.ts +++ b/packages/openapi-ts/src/plugins/zod/shared/webhook.ts @@ -1,17 +1,22 @@ -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { exportZodSchema } from '../export'; -import type { ZodPlugin } from '../types'; -import type { ZodSchema } from './types'; - -export const webhookToZodSchema = ({ - getZodSchema, +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; + +import { exportAst } from './export'; +import type { Ast, IrSchemaToAstOptions } from './types'; + +export const irWebhookToAst = ({ + getAst, operation, plugin, -}: { - getZodSchema: (schema: IR.SchemaObject) => ZodSchema; + state, +}: Omit & { + getAst: ( + schema: IR.SchemaObject, + path: ReadonlyArray, + ) => Ast; operation: IR.OperationObject; - plugin: ZodPlugin['Instance']; + state: Partial; }) => { if (plugin.config.webhooks.enabled) { const requiredProperties = new Set(); @@ -111,37 +116,43 @@ export const webhookToZodSchema = ({ schemaData.required = [...requiredProperties]; - const zodSchema = getZodSchema(schemaData); + const path = state._path?.value || []; + const ast = getAst(schemaData, path); + const resourceType = pathToSymbolResourceType(path); const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.webhooks, name: operation.id, }), - selector: plugin.api.getSelector('webhook-request', operation.id), + selector: plugin.api.selector('webhook-request', operation.id), }); const typeInferSymbol = plugin.config.webhooks.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.webhooks.types.infer, name: operation.id, }), - selector: plugin.api.getSelector( + selector: plugin.api.selector( 'type-infer-webhook-request', operation.id, ), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema: schemaData, symbol, typeInferSymbol, - zodSchema, }); } }; diff --git a/packages/openapi-ts/src/plugins/zod/types.d.ts b/packages/openapi-ts/src/plugins/zod/types.d.ts index e5bc03a25..6d82aff4e 100644 --- a/packages/openapi-ts/src/plugins/zod/types.d.ts +++ b/packages/openapi-ts/src/plugins/zod/types.d.ts @@ -1,5 +1,6 @@ -import type { StringCase, StringName } from '../../types/case'; -import type { DefinePlugin, Plugin } from '../types'; +import type { DefinePlugin, Plugin } from '~/plugins/types'; +import type { StringCase, StringName } from '~/types/case'; + import type { IApi } from './api'; export type UserConfig = Plugin.Name<'zod'> & @@ -96,7 +97,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -189,7 +190,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -267,7 +268,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -309,7 +310,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -378,7 +379,7 @@ export type UserConfig = Plugin.Name<'zod'> & */ types?: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. * * Can be: * - `boolean`: Shorthand for `{ enabled: boolean }` @@ -499,7 +500,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -572,7 +573,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -630,7 +631,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -662,7 +663,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** @@ -711,7 +712,7 @@ export type Config = Plugin.Name<'zod'> & */ types: { /** - * Configuration for `z.infer` types. + * Configuration for `infer` types. */ infer: { /** diff --git a/packages/openapi-ts/src/plugins/zod/v3/api.ts b/packages/openapi-ts/src/plugins/zod/v3/api.ts new file mode 100644 index 000000000..e6b089f93 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/api.ts @@ -0,0 +1,72 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV3 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV3 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts index ab7fc5535..a625aa8ba 100644 --- a/packages/openapi-ts/src/plugins/zod/v3/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v3/plugin.ts @@ -1,1039 +1,79 @@ -import ts from 'typescript'; +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRef, toRefs } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; -import { deduplicateSchema } from '../../../ir/schema'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - let arrayExpression: ts.CallExpression | undefined; - let hasCircularReference = false; - - if (!schema.items) { - arrayExpression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }), - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - arrayExpression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - if (schema.logicalOperator === 'and') { - const firstSchema = schema.items![0]!; - // we want to add an intersection, but not every schema can use the same API. - // if the first item contains another array or not an object, we cannot use - // `.and()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: intersectionExpression, - name: identifiers.and, - }), - parameters: [itemExpressions[i]!], - }); - } - } - - arrayExpression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } else { - if (schema.minItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } - - if (schema.maxItems !== undefined) { - arrayExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: arrayExpression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - } - } - - return { - expression: arrayExpression, - hasCircularReference, - }; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'boolean') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return expression; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return expression; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): ts.CallExpression => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - let enumExpression: ts.CallExpression; - if (allStrings && enumMembers.length > 0) { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - enumExpression = literalMembers[0]!; - } else { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - enumExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: enumExpression, - name: identifiers.nullable, - }), - }); - } - - return enumExpression; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return expression; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return expression; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}) => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return expression; - } - - let numberExpression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.int, - }), - }); - } - - if (schema.exclusiveMinimum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - } else if (schema.minimum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - } - - if (schema.exclusiveMaximum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - } else if (schema.maximum !== undefined) { - numberExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: numberExpression, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - } - - return numberExpression; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - let hasCircularReference = false; - - // TODO: parser - handle constants - const properties: Array = []; - - const required = schema.required ?? []; - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertyExpression = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - - if (propertyExpression.hasCircularReference) { - hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = name; - } - // TODO: parser - abstract safe property name logic - if ( - ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && - !name.startsWith("'") && - !name.endsWith("'") - ) { - propertyName = `'${name}'`; - } - properties.push( - tsc.propertyAssignment({ - initializer: propertyExpression.expression, - name: propertyName, - }), - ); - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [zodSchema.expression], - }); - return { - anyType: 'AnyZodObject', - expression, - hasCircularReference: zodSchema.hasCircularReference, - }; - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - return { - anyType: 'AnyZodObject', - expression, - hasCircularReference, - }; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'string') { - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return expression; - } - - let stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.date, - }), - }); - break; - case 'date-time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - case 'ipv6': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.ip, - }), - }); - break; - case 'time': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.time, - }), - }); - break; - case 'uri': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.url, - }), - }); - break; - case 'uuid': - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.uuid, - }), - }); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } else { - if (schema.minLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } - - if (schema.maxLength !== undefined) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - } - } - - if (schema.pattern) { - stringExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: stringExpression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - } - - return stringExpression; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit & { - anyType?: string; -} => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - let hasCircularReference = false; - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return { - expression, - hasCircularReference, - }; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - hasCircularReference = true; - } - tupleElements.push(zodSchema.expression); - } - - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return { - expression, - hasCircularReference, - }; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return expression; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return expression; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}) => { - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - const expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return expression; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit & { - anyType?: string; -} => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return { - expression: booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }), - }; - case 'enum': - return { - expression: enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }), - }; - case 'integer': - case 'number': - return { - expression: numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - case 'never': - return { - expression: neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }), - }; - case 'null': - return { - expression: nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }), - }; - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return { - expression: stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }), - }; - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return { - expression: undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }), - }; - case 'unknown': - return { - expression: unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }), - }; - case 'void': - return { - expression: voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }), - }; - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - zodSchema.expression = tsc.callExpression({ + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; + } else { + const lazyExpression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ tsc.arrowFunction({ statements: [ tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), + expression: tsc.identifier({ text: refSymbol.placeholder }), }), ], }), ], }); - zodSchema.hasCircularReference = schema.circular; - } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state: { - ...state, - currentReferenceTracker: [schema.$ref], - }, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; - zodSchema.typeName = zSchema.anyType; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.typeName = typeAst.anyType; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.describe, }), parameters: [tsc.stringLiteral({ text: schema.description })], @@ -1043,16 +83,16 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemTypes = schema.items.map((item) => { - const zSchema = schemaToZodSchema({ + const itemTypes = schema.items.map((item, index) => { + const typeAst = irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }); - if (zSchema.hasCircularReference) { - zodSchema.hasCircularReference = true; - } - return zSchema.expression; + return typeAst.expression; }); if (schema.logicalOperator === 'and') { @@ -1064,19 +104,19 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemTypes, }); } else { - zodSchema.expression = itemTypes[0]; + ast.expression = itemTypes[0]; itemTypes.slice(1).forEach((item) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression!, + expression: ast.expression!, name: identifiers.and, }), parameters: [item], @@ -1084,9 +124,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1097,36 +137,35 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ plugin, schema, state }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; - zodSchema.typeName = zSchema.anyType; + ast.expression = typeAst.expression; + ast.typeName = typeAst.anyType; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.readonly, }), }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.optional, }), }); @@ -1139,9 +178,9 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.default, }), parameters: [callParameter], @@ -1150,69 +189,60 @@ const schemaToZodSchema = ({ } } - if (zodSchema.hasCircularReference) { - if (!zodSchema.typeName) { - zodSchema.typeName = 'ZodTypeAny'; + if (state.hasLazyExpression.value) { + if (!ast.typeName) { + ast.typeName = 'ZodTypeAny'; } - } else if (zodSchema.typeName) { - zodSchema.typeName = undefined; + } else if (ast.typeName) { + ast.typeName = undefined; } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, state, -}: { - id: string; - plugin: ZodPlugin['Instance']; +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: State; }): void => { - if (!state) { - state = { - circularReferenceTracker: [id], - currentReferenceTracker: [id], - hasCircularReference: false, - }; - } - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1220,7 +250,7 @@ export const handlerV3: ZodPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: getZodModule({ plugin }), name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1232,52 +262,68 @@ export const handlerV3: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts new file mode 100644 index 000000000..895d83465 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/array.ts @@ -0,0 +1,162 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '~/ir/schema'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + let arrayExpression: ts.CallExpression | undefined; + let hasLazyExpression = false; + + if (!schema.items) { + arrayExpression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }), + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + arrayExpression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + if (schema.logicalOperator === 'and') { + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, + }), + parameters: [itemExpressions[i]!], + }); + } + } + + arrayExpression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } else { + if (schema.minItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } + + if (schema.maxItems !== undefined) { + arrayExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: arrayExpression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + } + } + + return { + expression: arrayExpression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts new file mode 100644 index 000000000..b818fcd46 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/boolean.ts @@ -0,0 +1,33 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'boolean') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return expression; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts new file mode 100644 index 000000000..282274e5c --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/enum.ts @@ -0,0 +1,126 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): ts.CallExpression => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + let enumExpression: ts.CallExpression; + if (allStrings && enumMembers.length > 0) { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + enumExpression = literalMembers[0]!; + } else { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + enumExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: enumExpression, + name: identifiers.nullable, + }), + }); + } + + return enumExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts new file mode 100644 index 000000000..41c535f9f --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/index.ts @@ -0,0 +1,106 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit & { + anyType?: string; +} => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return { + expression: booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }), + }; + case 'enum': + return { + expression: enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }), + }; + case 'integer': + case 'number': + return { + expression: numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }), + }; + case 'never': + return { + expression: neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }), + }; + case 'null': + return { + expression: nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }), + }; + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return { + expression: stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }), + }; + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return { + expression: undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }), + }; + case 'unknown': + return { + expression: unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }), + }; + case 'void': + return { + expression: voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }), + }; + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts new file mode 100644 index 000000000..6f31ae90d --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/never.ts @@ -0,0 +1,20 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts new file mode 100644 index 000000000..5247a6663 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/null.ts @@ -0,0 +1,20 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts new file mode 100644 index 000000000..3d39bb940 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/number.ts @@ -0,0 +1,95 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return expression; + } + + let numberExpression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.int, + }), + }); + } + + if (schema.exclusiveMinimum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + } else if (schema.minimum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + } + + if (schema.exclusiveMaximum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + } else if (schema.maximum !== undefined) { + numberExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: numberExpression, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + } + + return numberExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts new file mode 100644 index 000000000..24721e70a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/object.ts @@ -0,0 +1,113 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + let hasLazyExpression = false; + + // TODO: parser - handle constants + const properties: Array = []; + + const required = schema.required ?? []; + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyExpression = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + + if (propertyExpression.hasLazyExpression) { + hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + // TODO: parser - abstract safe property name logic + if ( + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && + !name.startsWith("'") && + !name.endsWith("'") + ) { + propertyName = `'${name}'`; + } + properties.push( + tsc.propertyAssignment({ + initializer: propertyExpression.expression, + name: propertyName, + }), + ); + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [additionalAst.expression], + }); + return { + anyType: 'AnyZodObject', + expression, + hasLazyExpression: additionalAst.hasLazyExpression, + }; + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + return { + anyType: 'AnyZodObject', + expression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts new file mode 100644 index 000000000..b45ae09fe --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/string.ts @@ -0,0 +1,153 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'string') { + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return expression; + } + + let stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.date, + }), + }); + break; + case 'date-time': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + case 'ipv6': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.ip, + }), + }); + break; + case 'time': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.time, + }), + }); + break; + case 'uri': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.url, + }), + }); + break; + case 'uuid': + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.uuid, + }), + }); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } else { + if (schema.minLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } + + if (schema.maxLength !== undefined) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + } + } + + if (schema.pattern) { + stringExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: stringExpression, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + } + + return stringExpression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts new file mode 100644 index 000000000..ec6e8f0db --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/tuple.ts @@ -0,0 +1,85 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit & { + anyType?: string; +} => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + let hasLazyExpression = false; + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return { + expression, + hasLazyExpression, + }; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + hasLazyExpression = true; + } + }); + } + + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return { + expression, + hasLazyExpression, + }; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts new file mode 100644 index 000000000..381cd89c0 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/undefined.ts @@ -0,0 +1,20 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts new file mode 100644 index 000000000..5a534a02c --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/unknown.ts @@ -0,0 +1,20 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts new file mode 100644 index 000000000..28abe5dc1 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v3/toAst/void.ts @@ -0,0 +1,20 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}) => { + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + const expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return expression; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/api.ts b/packages/openapi-ts/src/plugins/zod/v4/api.ts new file mode 100644 index 000000000..01663c8e0 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/api.ts @@ -0,0 +1,72 @@ +import type ts from 'typescript'; + +import { tsc } from '~/tsc'; + +import { identifiers } from '../constants'; +import type { ValidatorArgs } from '../shared/types'; + +export const createRequestValidatorV4 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol(plugin.api.selector('data', operation.id)); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; + +export const createResponseValidatorV4 = ({ + operation, + plugin, +}: ValidatorArgs): ts.ArrowFunction | undefined => { + const symbol = plugin.getSymbol( + plugin.api.selector('responses', operation.id), + ); + if (!symbol) return; + + const dataParameterName = 'data'; + + return tsc.arrowFunction({ + async: true, + parameters: [ + { + name: dataParameterName, + }, + ], + statements: [ + tsc.returnStatement({ + expression: tsc.awaitExpression({ + expression: tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: symbol.placeholder, + name: identifiers.parseAsync, + }), + parameters: [tsc.identifier({ text: dataParameterName })], + }), + }), + }), + ], + }); +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts index 27ce9fe2b..fcb072a6a 100644 --- a/packages/openapi-ts/src/plugins/zod/v4/plugin.ts +++ b/packages/openapi-ts/src/plugins/zod/v4/plugin.ts @@ -1,1084 +1,85 @@ -import ts from 'typescript'; +import { deduplicateSchema } from '~/ir/schema'; +import type { IR } from '~/ir/types'; +import { buildName } from '~/openApi/shared/utils/name'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { pathToSymbolResourceType } from '~/plugins/shared/utils/meta'; +import { toRef, toRefs } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { refToName } from '~/utils/ref'; -import { deduplicateSchema } from '../../../ir/schema'; -import type { IR } from '../../../ir/types'; -import { buildName } from '../../../openApi/shared/utils/name'; -import { tsc } from '../../../tsc'; -import { refToName } from '../../../utils/ref'; -import { numberRegExp } from '../../../utils/regexp'; import { identifiers } from '../constants'; -import { exportZodSchema } from '../export'; +import { exportAst } from '../shared/export'; import { getZodModule } from '../shared/module'; -import { operationToZodSchema } from '../shared/operation'; -import type { SchemaWithType, State, ZodSchema } from '../shared/types'; -import { webhookToZodSchema } from '../shared/webhook'; +import { numberParameter } from '../shared/numbers'; +import { irOperationToAst } from '../shared/operation'; +import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import { irWebhookToAst } from '../shared/webhook'; import type { ZodPlugin } from '../types'; +import { irSchemaWithTypeToAst } from './toAst'; -const arrayTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'array'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const functionName = tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }); - - if (!schema.items) { - result.expression = tsc.callExpression({ - functionName, - parameters: [ - unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }).expression, - ], - }); - } else { - schema = deduplicateSchema({ schema }); - - // at least one item is guaranteed - const itemExpressions = schema.items!.map((item) => { - const zodSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - return zodSchema.expression; - }); - - if (itemExpressions.length === 1) { - result.expression = tsc.callExpression({ - functionName, - parameters: itemExpressions, - }); - } else { - if (schema.logicalOperator === 'and') { - const firstSchema = schema.items![0]!; - // we want to add an intersection, but not every schema can use the same API. - // if the first item contains another array or not an object, we cannot use - // `.and()` as that does not exist on `.union()` and non-object schemas. - let intersectionExpression: ts.Expression; - if ( - firstSchema.logicalOperator === 'or' || - (firstSchema.type && firstSchema.type !== 'object') - ) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.intersection, - }), - parameters: itemExpressions, - }); - } else { - intersectionExpression = itemExpressions[0]!; - for (let i = 1; i < itemExpressions.length; i++) { - intersectionExpression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: intersectionExpression, - name: identifiers.and, - }), - parameters: [itemExpressions[i]!], - }); - } - } - - result.expression = tsc.callExpression({ - functionName, - parameters: [intersectionExpression], - }); - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.array, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: itemExpressions, - }), - ], - }), - ], - }); - } - } - } - - if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } else { - if (schema.minItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minItems })], - }); - } - - if (schema.maxItems !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxItems })], - }); - } - } - - return result as Omit; -}; - -const booleanTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'boolean'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'boolean') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.boolean(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.boolean, - }), - }); - return result as Omit; -}; - -const enumTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'enum'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - const enumMembers: Array = []; - const literalMembers: Array = []; - - let isNullable = false; - let allStrings = true; - - for (const item of schema.items ?? []) { - // Zod supports string, number, and boolean enums - if (item.type === 'string' && typeof item.const === 'string') { - const stringLiteral = tsc.stringLiteral({ - text: item.const, - }); - enumMembers.push(stringLiteral); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [stringLiteral], - }), - ); - } else if ( - (item.type === 'number' || item.type === 'integer') && - typeof item.const === 'number' - ) { - allStrings = false; - const numberLiteral = tsc.ots.number(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [numberLiteral], - }), - ); - } else if (item.type === 'boolean' && typeof item.const === 'boolean') { - allStrings = false; - const booleanLiteral = tsc.ots.boolean(item.const); - literalMembers.push( - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [booleanLiteral], - }), - ); - } else if (item.type === 'null' || item.const === null) { - isNullable = true; - } - } - - if (!literalMembers.length) { - return unknownTypeToZodSchema({ - plugin, - schema: { - type: 'unknown', - }, - }); - } - - // Use z.enum() for pure string enums, z.union() for mixed or non-string types - if (allStrings && enumMembers.length > 0) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.enum, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: enumMembers, - multiLine: false, - }), - ], - }); - } else if (literalMembers.length === 1) { - // For single-member unions, use the member directly instead of wrapping in z.union() - result.expression = literalMembers[0]; - } else { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.union, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: literalMembers, - multiLine: literalMembers.length > 3, - }), - ], - }); - } - - if (isNullable) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.nullable, - }), - parameters: [result.expression], - }); - } - - return result as Omit; -}; - -const neverTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'never'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.never, - }), - }); - return result as Omit; -}; - -const nullTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'null'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.null, - }), - }); - return result as Omit; -}; - -const numberParameter = ({ - isBigInt, - value, -}: { - isBigInt: boolean; - value: unknown; -}): ts.Expression | undefined => { - const expression = tsc.valueToExpression({ value }); - - if ( - isBigInt && - (typeof value === 'bigint' || - typeof value === 'number' || - typeof value === 'string' || - typeof value === 'boolean') - ) { - return tsc.callExpression({ - functionName: 'BigInt', - parameters: [expression], - }); - } - - return expression; -}; - -const numberTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'integer' | 'number'>; -}): Omit => { - const result: Partial> = {}; - - const isBigInt = schema.type === 'integer' && schema.format === 'int64'; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'number') { - // TODO: parser - handle bigint constants - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.number(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: isBigInt - ? tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.coerce, - }), - name: identifiers.bigint, - }) - : tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.number, - }), - }); - - if (!isBigInt && schema.type === 'integer') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.int, - }), - }); - } - - if (schema.exclusiveMinimum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.gt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMinimum }), - ], - }); - } else if (schema.minimum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.gte, - }), - parameters: [numberParameter({ isBigInt, value: schema.minimum })], - }); - } - - if (schema.exclusiveMaximum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.lt, - }), - parameters: [ - numberParameter({ isBigInt, value: schema.exclusiveMaximum }), - ], - }); - } else if (schema.maximum !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.lte, - }), - parameters: [numberParameter({ isBigInt, value: schema.maximum })], - }); - } - - return result as Omit; -}; - -const objectTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'object'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - // TODO: parser - handle constants - const properties: Array = - []; - - const required = schema.required ?? []; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - for (const name in schema.properties) { - const property = schema.properties[name]!; - const isRequired = required.includes(name); - - const propertySchema = schemaToZodSchema({ - optional: !isRequired, - plugin, - schema: property, - state, - }); - if (propertySchema.hasCircularReference) { - result.hasCircularReference = true; - } - - numberRegExp.lastIndex = 0; - let propertyName; - if (numberRegExp.test(name)) { - // For numeric literals, we'll handle negative numbers by using a string literal - // instead of trying to use a PrefixUnaryExpression - propertyName = name.startsWith('-') - ? ts.factory.createStringLiteral(name) - : ts.factory.createNumericLiteral(name); - } else { - propertyName = name; - } - // TODO: parser - abstract safe property name logic - if ( - ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && - !name.startsWith("'") && - !name.endsWith("'") - ) { - propertyName = `'${name}'`; - } - - if (propertySchema.hasCircularReference) { - properties.push( - tsc.getAccessorDeclaration({ - name: propertyName, - // @ts-expect-error - returnType: propertySchema.typeName - ? tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: propertySchema.typeName, - }) - : undefined, - statements: [ - tsc.returnStatement({ - expression: propertySchema.expression, - }), - ], - }), - ); - } else { - properties.push( - tsc.propertyAssignment({ - initializer: propertySchema.expression, - name: propertyName, - }), - ); - } - } - - if ( - schema.additionalProperties && - (!schema.properties || !Object.keys(schema.properties).length) - ) { - const zodSchema = schemaToZodSchema({ - plugin, - schema: schema.additionalProperties, - state, - }); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.record, - }), - parameters: [ - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - parameters: [], - }), - zodSchema.expression, - ], - }); - if (zodSchema.hasCircularReference) { - result.hasCircularReference = true; - } - - // Return with typeName for circular references - if (result.hasCircularReference) { - return { - ...result, - typeName: 'ZodType', - } as ZodSchema; - } - - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.object, - }), - parameters: [ts.factory.createObjectLiteralExpression(properties, true)], - }); - - // Return with typeName for circular references (AnyZodObject doesn't exist in Zod v4, use ZodType) - if (result.hasCircularReference) { - return { - ...result, - typeName: 'ZodType', - } as ZodSchema; - } - - return result as Omit; -}; - -const stringTypeToZodSchema = ({ - plugin, - schema, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'string'>; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (typeof schema.const === 'string') { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.ots.string(schema.const)], - }); - return result as Omit; - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.string, - }), - }); - - const dateTimeOptions: { key: string; value: boolean }[] = []; - - if (plugin.config.dates.offset) { - dateTimeOptions.push({ key: 'offset', value: true }); - } - if (plugin.config.dates.local) { - dateTimeOptions.push({ key: 'local', value: true }); - } - - if (schema.format) { - switch (schema.format) { - case 'date': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.date, - }), - }); - break; - case 'date-time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.datetime, - }), - parameters: - dateTimeOptions.length > 0 - ? [ - tsc.objectExpression({ - obj: dateTimeOptions, - }), - ] - : [], - }); - break; - case 'email': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.email, - }), - }); - break; - case 'ipv4': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv4, - }), - }); - break; - case 'ipv6': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.ipv6, - }), - }); - break; - case 'time': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.iso, - }), - name: identifiers.time, - }), - }); - break; - case 'uri': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.url, - }), - }); - break; - case 'uuid': - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.uuid, - }), - }); - break; - } - } - - if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.length, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } else { - if (schema.minLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.min, - }), - parameters: [tsc.valueToExpression({ value: schema.minLength })], - }); - } - - if (schema.maxLength !== undefined) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.max, - }), - parameters: [tsc.valueToExpression({ value: schema.maxLength })], - }); - } - } - - if (schema.pattern) { - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: result.expression, - name: identifiers.regex, - }), - parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], - }); - } - - return result as Omit; -}; - -const tupleTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'tuple'>; - state: State; -}): Omit => { - const result: Partial> = {}; - - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - - if (schema.const && Array.isArray(schema.const)) { - const tupleElements = schema.const.map((value) => - tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.literal, - }), - parameters: [tsc.valueToExpression({ value })], - }), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - return result as Omit; - } - - const tupleElements: Array = []; - - for (const item of schema.items ?? []) { - const itemSchema = schemaToZodSchema({ - plugin, - schema: item, - state, - }); - tupleElements.push(itemSchema.expression); - - if (itemSchema.hasCircularReference) { - result.hasCircularReference = true; - } - } - - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.tuple, - }), - parameters: [ - tsc.arrayLiteralExpression({ - elements: tupleElements, - }), - ], - }); - - return result as Omit; -}; - -const undefinedTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'undefined'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.undefined, - }), - }); - return result as Omit; -}; - -const unknownTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'unknown'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.unknown, - }), - }); - return result as Omit; -}; - -const voidTypeToZodSchema = ({ - plugin, -}: { - plugin: ZodPlugin['Instance']; - schema: SchemaWithType<'void'>; -}): Omit => { - const result: Partial> = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); - result.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.void, - }), - }); - return result as Omit; -}; - -const schemaTypeToZodSchema = ({ - plugin, - schema, - state, -}: { - plugin: ZodPlugin['Instance']; - schema: IR.SchemaObject; - state: State; -}): Omit => { - switch (schema.type as Required['type']) { - case 'array': - return arrayTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'array'>, - state, - }); - case 'boolean': - return booleanTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'boolean'>, - }); - case 'enum': - return enumTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'enum'>, - }); - case 'integer': - case 'number': - return numberTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'integer' | 'number'>, - }); - case 'never': - return neverTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'never'>, - }); - case 'null': - return nullTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'null'>, - }); - case 'object': - return objectTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'object'>, - state, - }); - case 'string': - return stringTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'string'>, - }); - case 'tuple': - return tupleTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'tuple'>, - state, - }); - case 'undefined': - return undefinedTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'undefined'>, - }); - case 'unknown': - return unknownTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'unknown'>, - }); - case 'void': - return voidTypeToZodSchema({ - plugin, - schema: schema as SchemaWithType<'void'>, - }); - } -}; - -const schemaToZodSchema = ({ +export const irSchemaToAst = ({ optional, plugin, schema, state, -}: { +}: IrSchemaToAstOptions & { /** * Accept `optional` to handle optional object properties. We can't handle * this inside the object function because `.optional()` must come before * `.default()` which is handled in this function. */ optional?: boolean; - plugin: ZodPlugin['Instance']; schema: IR.SchemaObject; - state: State; -}): ZodSchema => { - let zodSchema: Partial = {}; +}): Ast => { + let ast: Partial = {}; - const zSymbol = plugin.referenceSymbol( - plugin.api.getSelector('import', 'zod'), - ); + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); if (schema.$ref) { - const isCircularReference = state.circularReferenceTracker.includes( - schema.$ref, - ); - const isSelfReference = state.currentReferenceTracker.includes(schema.$ref); - state.circularReferenceTracker.push(schema.$ref); - state.currentReferenceTracker.push(schema.$ref); - - const selector = plugin.api.getSelector('ref', schema.$ref); - let symbol = plugin.getSymbol(selector); - - if (isCircularReference) { - if (!symbol) { - symbol = plugin.referenceSymbol(selector); - } - - if (isSelfReference) { - zodSchema.expression = tsc.callExpression({ - functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, - name: identifiers.lazy, - }), - parameters: [ - tsc.arrowFunction({ - returnType: tsc.keywordTypeNode({ keyword: 'any' }), - statements: [ - tsc.returnStatement({ - expression: tsc.identifier({ text: symbol.placeholder }), - }), - ], - }), - ], - }); - } else { - zodSchema.expression = tsc.identifier({ text: symbol.placeholder }); - } - zodSchema.hasCircularReference = schema.circular; + const selector = plugin.api.selector('ref', schema.$ref); + const refSymbol = plugin.referenceSymbol(selector); + if (plugin.isSymbolRegistered(selector)) { + const ref = tsc.identifier({ text: refSymbol.placeholder }); + ast.expression = ref; } else { - if (!symbol) { - // if $ref hasn't been processed yet, inline it to avoid the - // "Block-scoped variable used before its declaration." error - // this could be (maybe?) fixed by reshuffling the generation order - const ref = plugin.context.resolveIrRef(schema.$ref); - handleComponent({ - id: schema.$ref, - plugin, - schema: ref, - state, - }); - } else { - zodSchema.hasCircularReference = schema.circular; - } - - const refSymbol = plugin.referenceSymbol(selector); - zodSchema.expression = tsc.identifier({ text: refSymbol.placeholder }); + const lazyExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.lazy, + }), + parameters: [ + tsc.arrowFunction({ + returnType: tsc.keywordTypeNode({ keyword: 'any' }), + statements: [ + tsc.returnStatement({ + expression: tsc.identifier({ text: refSymbol.placeholder }), + }), + ], + }), + ], + }); + ast.expression = lazyExpression; + ast.hasLazyExpression = true; + state.hasLazyExpression.value = true; } - - state.circularReferenceTracker.pop(); - state.currentReferenceTracker.pop(); } else if (schema.type) { - const zSchema = schemaTypeToZodSchema({ plugin, schema, state }); - zodSchema.expression = zSchema.expression; - zodSchema.hasCircularReference = zSchema.hasCircularReference; + const typeAst = irSchemaWithTypeToAst({ + plugin, + schema: schema as SchemaWithType, + state, + }); + ast.expression = typeAst.expression; + ast.hasLazyExpression = typeAst.hasLazyExpression; if (plugin.config.metadata && schema.description) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.register, }), parameters: [ tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.globalRegistry, }), tsc.objectExpression({ @@ -1096,11 +97,14 @@ const schemaToZodSchema = ({ schema = deduplicateSchema({ schema }); if (schema.items) { - const itemSchemas = schema.items.map((item) => - schemaToZodSchema({ + const itemSchemas = schema.items.map((item, index) => + irSchemaToAst({ plugin, schema: item, - state, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, }), ); @@ -1113,26 +117,26 @@ const schemaToZodSchema = ({ firstSchema.logicalOperator === 'or' || (firstSchema.type && firstSchema.type !== 'object') ) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.intersection, }), parameters: itemSchemas.map((schema) => schema.expression), }); } else { - zodSchema.expression = itemSchemas[0]!.expression; + ast.expression = itemSchemas[0]!.expression; itemSchemas.slice(1).forEach((schema) => { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression!, + expression: ast.expression!, name: identifiers.and, }), parameters: [ - schema.hasCircularReference + schema.hasLazyExpression ? tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.lazy, }), parameters: [ @@ -1151,9 +155,9 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.union, }), parameters: [ @@ -1164,39 +168,43 @@ const schemaToZodSchema = ({ }); } } else { - zodSchema = schemaToZodSchema({ plugin, schema, state }); + ast = irSchemaToAst({ + plugin, + schema, + state, + }); } } else { // catch-all fallback for failed schemas - const zSchema = schemaTypeToZodSchema({ + const typeAst = irSchemaWithTypeToAst({ plugin, schema: { type: 'unknown', }, state, }); - zodSchema.expression = zSchema.expression; + ast.expression = typeAst.expression; } - if (zodSchema.expression) { + if (ast.expression) { if (schema.accessScope === 'read') { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.readonly, }), }); } if (optional) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zSymbol.placeholder, + expression: z.placeholder, name: identifiers.optional, }), - parameters: [zodSchema.expression], + parameters: [ast.expression], }); - zodSchema.typeName = identifiers.ZodOptional; + ast.typeName = identifiers.ZodOptional; } if (schema.default !== undefined) { @@ -1206,9 +214,9 @@ const schemaToZodSchema = ({ value: schema.default, }); if (callParameter) { - zodSchema.expression = tsc.callExpression({ + ast.expression = tsc.callExpression({ functionName: tsc.propertyAccessExpression({ - expression: zodSchema.expression, + expression: ast.expression, name: identifiers.default, }), parameters: [callParameter], @@ -1217,60 +225,52 @@ const schemaToZodSchema = ({ } } - return zodSchema as ZodSchema; + return ast as Ast; }; const handleComponent = ({ - id, + $ref, plugin, schema, - state: _state, -}: { - id: string; - plugin: ZodPlugin['Instance']; + state, +}: IrSchemaToAstOptions & { + $ref: string; schema: IR.SchemaObject; - state?: Omit; }): void => { - const state: State = { - circularReferenceTracker: [id], - hasCircularReference: false, - ..._state, - currentReferenceTracker: [id], - }; - - const selector = plugin.api.getSelector('ref', id); - let symbol = plugin.getSymbol(selector); - if (symbol && !plugin.getSymbolValue(symbol)) return; - - const zodSchema = schemaToZodSchema({ plugin, schema, state }); - const baseName = refToName(id); - symbol = plugin.registerSymbol({ + const ast = irSchemaToAst({ plugin, schema, state }); + const baseName = refToName($ref); + const resourceType = pathToSymbolResourceType(state._path.value); + const symbol = plugin.registerSymbol({ exported: true, + meta: { + resourceType, + }, name: buildName({ config: plugin.config.definitions, name: baseName, }), - selector, + selector: plugin.api.selector('ref', $ref), }); const typeInferSymbol = plugin.config.definitions.types.infer.enabled ? plugin.registerSymbol({ exported: true, meta: { kind: 'type', + resourceType, }, name: buildName({ config: plugin.config.definitions.types.infer, name: baseName, }), - selector: plugin.api.getSelector('type-infer-ref', id), + selector: plugin.api.selector('type-infer-ref', $ref), }) : undefined; - exportZodSchema({ + exportAst({ + ast, plugin, schema, symbol, typeInferSymbol, - zodSchema, }); }; @@ -1278,7 +278,7 @@ export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { plugin.registerSymbol({ external: getZodModule({ plugin }), name: 'z', - selector: plugin.api.getSelector('import', 'zod'), + selector: plugin.api.selector('external', 'zod.z'), }); plugin.forEach( @@ -1290,52 +290,68 @@ export const handlerV4: ZodPlugin['Handler'] = ({ plugin }) => { (event) => { switch (event.type) { case 'operation': - operationToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irOperationToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; case 'parameter': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.parameter.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'requestBody': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.requestBody.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'schema': handleComponent({ - id: event.$ref, + $ref: event.$ref, plugin, schema: event.schema, + state: toRefs({ + _path: event._path, + hasLazyExpression: false, + }), }); break; case 'webhook': - webhookToZodSchema({ - getZodSchema: (schema) => { - const state: State = { - circularReferenceTracker: [], - currentReferenceTracker: [], - hasCircularReference: false, - }; - return schemaToZodSchema({ plugin, schema, state }); + irWebhookToAst({ + getAst: (schema, path) => { + const state = toRefs({ + _path: path, + hasLazyExpression: false, + }); + return irSchemaToAst({ plugin, schema, state }); }, operation: event.operation, plugin, + state: toRefs({ + _path: event._path, + }), }); break; } diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts new file mode 100644 index 000000000..0d78f2635 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/array.ts @@ -0,0 +1,156 @@ +import type ts from 'typescript'; + +import { deduplicateSchema } from '~/ir/schema'; +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; +import { unknownToAst } from './unknown'; + +export const arrayToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'array'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const functionName = tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }); + + if (!schema.items) { + result.expression = tsc.callExpression({ + functionName, + parameters: [ + unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }).expression, + ], + }); + } else { + schema = deduplicateSchema({ schema }); + + // at least one item is guaranteed + const itemExpressions = schema.items!.map((item, index) => { + const itemAst = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + if (itemAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + return itemAst.expression; + }); + + if (itemExpressions.length === 1) { + result.expression = tsc.callExpression({ + functionName, + parameters: itemExpressions, + }); + } else { + if (schema.logicalOperator === 'and') { + const firstSchema = schema.items![0]!; + // we want to add an intersection, but not every schema can use the same API. + // if the first item contains another array or not an object, we cannot use + // `.and()` as that does not exist on `.union()` and non-object schemas. + let intersectionExpression: ts.Expression; + if ( + firstSchema.logicalOperator === 'or' || + (firstSchema.type && firstSchema.type !== 'object') + ) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.intersection, + }), + parameters: itemExpressions, + }); + } else { + intersectionExpression = itemExpressions[0]!; + for (let i = 1; i < itemExpressions.length; i++) { + intersectionExpression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: intersectionExpression, + name: identifiers.and, + }), + parameters: [itemExpressions[i]!], + }); + } + } + + result.expression = tsc.callExpression({ + functionName, + parameters: [intersectionExpression], + }); + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.array, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: itemExpressions, + }), + ], + }), + ], + }); + } + } + } + + if (schema.minItems === schema.maxItems && schema.minItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } else { + if (schema.minItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minItems })], + }); + } + + if (schema.maxItems !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxItems })], + }); + } + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts new file mode 100644 index 000000000..c6a23106c --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/boolean.ts @@ -0,0 +1,35 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const booleanToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'boolean'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'boolean') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.boolean(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.boolean, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts new file mode 100644 index 000000000..6882759a6 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/enum.ts @@ -0,0 +1,128 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { unknownToAst } from './unknown'; + +export const enumToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'enum'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + const enumMembers: Array = []; + const literalMembers: Array = []; + + let isNullable = false; + let allStrings = true; + + for (const item of schema.items ?? []) { + // Zod supports string, number, and boolean enums + if (item.type === 'string' && typeof item.const === 'string') { + const stringLiteral = tsc.stringLiteral({ + text: item.const, + }); + enumMembers.push(stringLiteral); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [stringLiteral], + }), + ); + } else if ( + (item.type === 'number' || item.type === 'integer') && + typeof item.const === 'number' + ) { + allStrings = false; + const numberLiteral = tsc.ots.number(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [numberLiteral], + }), + ); + } else if (item.type === 'boolean' && typeof item.const === 'boolean') { + allStrings = false; + const booleanLiteral = tsc.ots.boolean(item.const); + literalMembers.push( + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [booleanLiteral], + }), + ); + } else if (item.type === 'null' || item.const === null) { + isNullable = true; + } + } + + if (!literalMembers.length) { + return unknownToAst({ + plugin, + schema: { + type: 'unknown', + }, + state, + }); + } + + // Use z.enum() for pure string enums, z.union() for mixed or non-string types + if (allStrings && enumMembers.length > 0) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.enum, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: enumMembers, + multiLine: false, + }), + ], + }); + } else if (literalMembers.length === 1) { + // For single-member unions, use the member directly instead of wrapping in z.union() + result.expression = literalMembers[0]; + } else { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.union, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: literalMembers, + multiLine: literalMembers.length > 3, + }), + ], + }); + } + + if (isNullable) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.nullable, + }), + parameters: [result.expression], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts new file mode 100644 index 000000000..b7400b68a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/index.ts @@ -0,0 +1,86 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; + +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { arrayToAst } from './array'; +import { booleanToAst } from './boolean'; +import { enumToAst } from './enum'; +import { neverToAst } from './never'; +import { nullToAst } from './null'; +import { numberToAst } from './number'; +import { objectToAst } from './object'; +import { stringToAst } from './string'; +import { tupleToAst } from './tuple'; +import { undefinedToAst } from './undefined'; +import { unknownToAst } from './unknown'; +import { voidToAst } from './void'; + +export const irSchemaWithTypeToAst = ({ + schema, + ...args +}: IrSchemaToAstOptions & { + schema: SchemaWithType; +}): Omit => { + switch (schema.type) { + case 'array': + return arrayToAst({ + ...args, + schema: schema as SchemaWithType<'array'>, + }); + case 'boolean': + return booleanToAst({ + ...args, + schema: schema as SchemaWithType<'boolean'>, + }); + case 'enum': + return enumToAst({ + ...args, + schema: schema as SchemaWithType<'enum'>, + }); + case 'integer': + case 'number': + return numberToAst({ + ...args, + schema: schema as SchemaWithType<'integer' | 'number'>, + }); + case 'never': + return neverToAst({ + ...args, + schema: schema as SchemaWithType<'never'>, + }); + case 'null': + return nullToAst({ + ...args, + schema: schema as SchemaWithType<'null'>, + }); + case 'object': + return objectToAst({ + ...args, + schema: schema as SchemaWithType<'object'>, + }); + case 'string': + return stringToAst({ + ...args, + schema: schema as SchemaWithType<'string'>, + }); + case 'tuple': + return tupleToAst({ + ...args, + schema: schema as SchemaWithType<'tuple'>, + }); + case 'undefined': + return undefinedToAst({ + ...args, + schema: schema as SchemaWithType<'undefined'>, + }); + case 'unknown': + return unknownToAst({ + ...args, + schema: schema as SchemaWithType<'unknown'>, + }); + case 'void': + return voidToAst({ + ...args, + schema: schema as SchemaWithType<'void'>, + }); + } +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts new file mode 100644 index 000000000..9a6239563 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/never.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const neverToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'never'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.never, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts new file mode 100644 index 000000000..ec4ca5e22 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/null.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const nullToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'null'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.null, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts new file mode 100644 index 000000000..800b55cbd --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/number.ts @@ -0,0 +1,97 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import { numberParameter } from '../../shared/numbers'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const numberToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'integer' | 'number'>; +}): Omit => { + const result: Partial> = {}; + + const isBigInt = schema.type === 'integer' && schema.format === 'int64'; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'number') { + // TODO: parser - handle bigint constants + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.number(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: isBigInt + ? tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.coerce, + }), + name: identifiers.bigint, + }) + : tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.number, + }), + }); + + if (!isBigInt && schema.type === 'integer') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.int, + }), + }); + } + + if (schema.exclusiveMinimum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.gt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMinimum }), + ], + }); + } else if (schema.minimum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.gte, + }), + parameters: [numberParameter({ isBigInt, value: schema.minimum })], + }); + } + + if (schema.exclusiveMaximum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.lt, + }), + parameters: [ + numberParameter({ isBigInt, value: schema.exclusiveMaximum }), + ], + }); + } else if (schema.maximum !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.lte, + }), + parameters: [numberParameter({ isBigInt, value: schema.maximum })], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts new file mode 100644 index 000000000..607f0ea3e --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/object.ts @@ -0,0 +1,147 @@ +import ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; +import { numberRegExp } from '~/utils/regexp'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const objectToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'object'>; +}): Omit => { + const result: Partial> = {}; + + // TODO: parser - handle constants + const properties: Array = + []; + + const required = schema.required ?? []; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + for (const name in schema.properties) { + const property = schema.properties[name]!; + const isRequired = required.includes(name); + + const propertyAst = irSchemaToAst({ + optional: !isRequired, + plugin, + schema: property, + state: { + ...state, + _path: toRef([...state._path.value, 'properties', name]), + }, + }); + if (propertyAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + numberRegExp.lastIndex = 0; + let propertyName; + if (numberRegExp.test(name)) { + // For numeric literals, we'll handle negative numbers by using a string literal + // instead of trying to use a PrefixUnaryExpression + propertyName = name.startsWith('-') + ? ts.factory.createStringLiteral(name) + : ts.factory.createNumericLiteral(name); + } else { + propertyName = name; + } + // TODO: parser - abstract safe property name logic + if ( + ((name.match(/^[0-9]/) && name.match(/\D+/g)) || name.match(/\W/g)) && + !name.startsWith("'") && + !name.endsWith("'") + ) { + propertyName = `'${name}'`; + } + + if (propertyAst.hasLazyExpression) { + properties.push( + tsc.getAccessorDeclaration({ + name: propertyName, + statements: [ + tsc.returnStatement({ + expression: propertyAst.expression, + }), + ], + }), + ); + } else { + properties.push( + tsc.propertyAssignment({ + initializer: propertyAst.expression, + name: propertyName, + }), + ); + } + } + + if ( + schema.additionalProperties && + (!schema.properties || !Object.keys(schema.properties).length) + ) { + const additionalAst = irSchemaToAst({ + plugin, + schema: schema.additionalProperties, + state: { + ...state, + _path: toRef([...state._path.value, 'additionalProperties']), + }, + }); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.record, + }), + parameters: [ + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + parameters: [], + }), + additionalAst.expression, + ], + }); + if (additionalAst.hasLazyExpression) { + result.hasLazyExpression = true; + } + + // Return with typeName for circular references + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'ZodType', + } as Ast; + } + + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.object, + }), + parameters: [ts.factory.createObjectLiteralExpression(properties, true)], + }); + + // Return with typeName for circular references (AnyZodObject doesn't exist in Zod v4, use ZodType) + if (result.hasLazyExpression) { + return { + ...result, + typeName: 'ZodType', + } as Ast; + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts new file mode 100644 index 000000000..559d33354 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/string.ts @@ -0,0 +1,171 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const stringToAst = ({ + plugin, + schema, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'string'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (typeof schema.const === 'string') { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.ots.string(schema.const)], + }); + return result as Omit; + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.string, + }), + }); + + const dateTimeOptions: { key: string; value: boolean }[] = []; + + if (plugin.config.dates.offset) { + dateTimeOptions.push({ key: 'offset', value: true }); + } + if (plugin.config.dates.local) { + dateTimeOptions.push({ key: 'local', value: true }); + } + + if (schema.format) { + switch (schema.format) { + case 'date': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.date, + }), + }); + break; + case 'date-time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.datetime, + }), + parameters: + dateTimeOptions.length > 0 + ? [ + tsc.objectExpression({ + obj: dateTimeOptions, + }), + ] + : [], + }); + break; + case 'email': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.email, + }), + }); + break; + case 'ipv4': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv4, + }), + }); + break; + case 'ipv6': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.ipv6, + }), + }); + break; + case 'time': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.iso, + }), + name: identifiers.time, + }), + }); + break; + case 'uri': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.url, + }), + }); + break; + case 'uuid': + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.uuid, + }), + }); + break; + } + } + + if (schema.minLength === schema.maxLength && schema.minLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.length, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } else { + if (schema.minLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.min, + }), + parameters: [tsc.valueToExpression({ value: schema.minLength })], + }); + } + + if (schema.maxLength !== undefined) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.max, + }), + parameters: [tsc.valueToExpression({ value: schema.maxLength })], + }); + } + } + + if (schema.pattern) { + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: result.expression, + name: identifiers.regex, + }), + parameters: [tsc.regularExpressionLiteral({ text: schema.pattern })], + }); + } + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts new file mode 100644 index 000000000..f875ed7b9 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/tuple.ts @@ -0,0 +1,78 @@ +import type ts from 'typescript'; + +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { toRef } from '~/plugins/shared/utils/refs'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import { irSchemaToAst } from '../plugin'; + +export const tupleToAst = ({ + plugin, + schema, + state, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'tuple'>; +}): Omit => { + const result: Partial> = {}; + + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + + if (schema.const && Array.isArray(schema.const)) { + const tupleElements = schema.const.map((value) => + tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.literal, + }), + parameters: [tsc.valueToExpression({ value })], + }), + ); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + return result as Omit; + } + + const tupleElements: Array = []; + + if (schema.items) { + schema.items.forEach((item, index) => { + const itemSchema = irSchemaToAst({ + plugin, + schema: item, + state: { + ...state, + _path: toRef([...state._path.value, 'items', index]), + }, + }); + tupleElements.push(itemSchema.expression); + if (itemSchema.hasLazyExpression) { + result.hasLazyExpression = true; + } + }); + } + + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.tuple, + }), + parameters: [ + tsc.arrayLiteralExpression({ + elements: tupleElements, + }), + ], + }); + + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts new file mode 100644 index 000000000..ebdf778b5 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/undefined.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const undefinedToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'undefined'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.undefined, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts new file mode 100644 index 000000000..e0f991b92 --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/unknown.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const unknownToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'unknown'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.unknown, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts b/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts new file mode 100644 index 000000000..b30aff23a --- /dev/null +++ b/packages/openapi-ts/src/plugins/zod/v4/toAst/void.ts @@ -0,0 +1,21 @@ +import type { SchemaWithType } from '~/plugins/shared/types/schema'; +import { tsc } from '~/tsc'; + +import { identifiers } from '../../constants'; +import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; + +export const voidToAst = ({ + plugin, +}: IrSchemaToAstOptions & { + schema: SchemaWithType<'void'>; +}): Omit => { + const result: Partial> = {}; + const z = plugin.referenceSymbol(plugin.api.selector('external', 'zod.z')); + result.expression = tsc.callExpression({ + functionName: tsc.propertyAccessExpression({ + expression: z.placeholder, + name: identifiers.void, + }), + }); + return result as Omit; +}; diff --git a/packages/openapi-ts/src/processOutput.ts b/packages/openapi-ts/src/processOutput.ts index 7e35f0a9e..f870dbb3e 100644 --- a/packages/openapi-ts/src/processOutput.ts +++ b/packages/openapi-ts/src/processOutput.ts @@ -1,7 +1,7 @@ import { sync } from 'cross-spawn'; -import type { Config } from './types/config'; -import type { Formatters, Linters } from './types/output'; +import type { Config } from '~/types/config'; +import type { Formatters, Linters } from '~/types/output'; type OutputProcessor = { args: (path: string) => ReadonlyArray; diff --git a/packages/openapi-ts/src/tsc/transform.ts b/packages/openapi-ts/src/tsc/transform.ts index f6f3253e8..a73e0523a 100644 --- a/packages/openapi-ts/src/tsc/transform.ts +++ b/packages/openapi-ts/src/tsc/transform.ts @@ -1,6 +1,7 @@ import ts from 'typescript'; -import { validTypescriptIdentifierRegExp } from '../utils/regexp'; +import { validTypescriptIdentifierRegExp } from '~/utils/regexp'; + import { expressionToStatement } from './convert'; import { createCallExpression } from './module'; import { diff --git a/packages/openapi-ts/src/tsc/typedef.ts b/packages/openapi-ts/src/tsc/typedef.ts index 600e1179c..4313ce04e 100644 --- a/packages/openapi-ts/src/tsc/typedef.ts +++ b/packages/openapi-ts/src/tsc/typedef.ts @@ -1,6 +1,7 @@ import ts from 'typescript'; -import { validTypescriptIdentifierRegExp } from '../utils/regexp'; +import { validTypescriptIdentifierRegExp } from '~/utils/regexp'; + import { createKeywordTypeNode, createMappedTypeNode, diff --git a/packages/openapi-ts/src/tsc/types.ts b/packages/openapi-ts/src/tsc/types.ts index af869048b..9a585442b 100644 --- a/packages/openapi-ts/src/tsc/types.ts +++ b/packages/openapi-ts/src/tsc/types.ts @@ -1,7 +1,8 @@ import ts from 'typescript'; -import { escapeName } from '../utils/escape'; -import { validTypescriptIdentifierRegExp } from '../utils/regexp'; +import { escapeName } from '~/utils/escape'; +import { validTypescriptIdentifierRegExp } from '~/utils/regexp'; + import { addLeadingComments, type Comments, diff --git a/packages/openapi-ts/src/tsc/utils.ts b/packages/openapi-ts/src/tsc/utils.ts index 5ed283029..37606547e 100644 --- a/packages/openapi-ts/src/tsc/utils.ts +++ b/packages/openapi-ts/src/tsc/utils.ts @@ -1,7 +1,8 @@ import ts from 'typescript'; -import { getConfig } from '../utils/config'; -import { unescapeName } from '../utils/escape'; +import { getConfig } from '~/utils/config'; +import { unescapeName } from '~/utils/escape'; + import type { AccessLevel } from './types'; import { createStringLiteral, syntaxKindKeyword } from './types'; diff --git a/packages/openapi-ts/src/types/client.d.ts b/packages/openapi-ts/src/types/client.d.ts index 0d64908c7..54b47e7e0 100644 --- a/packages/openapi-ts/src/types/client.d.ts +++ b/packages/openapi-ts/src/types/client.d.ts @@ -6,7 +6,7 @@ import type { Operation as ParserOperation, OperationParameter, OperationResponse, -} from '../openApi'; +} from '~/openApi'; export type { Method, Model, ModelMeta, OperationParameter, OperationResponse }; diff --git a/packages/openapi-ts/src/types/config.d.ts b/packages/openapi-ts/src/types/config.d.ts index 2d585e68d..58198a637 100644 --- a/packages/openapi-ts/src/types/config.d.ts +++ b/packages/openapi-ts/src/types/config.d.ts @@ -1,5 +1,6 @@ -import type { PluginConfigMap } from '../plugins/config'; -import type { Plugin, PluginNames } from '../plugins/types'; +import type { PluginConfigMap } from '~/plugins/config'; +import type { Plugin, PluginNames } from '~/plugins/types'; + import type { Input, UserInput, Watch } from './input'; import type { Logs } from './logs'; import type { Output, UserOutput } from './output'; diff --git a/packages/openapi-ts/src/types/parser.d.ts b/packages/openapi-ts/src/types/parser.d.ts index ad4d2e05f..eff50d325 100644 --- a/packages/openapi-ts/src/types/parser.d.ts +++ b/packages/openapi-ts/src/types/parser.d.ts @@ -1,4 +1,4 @@ -import type { IR } from '../ir/types'; +import type { IR } from '~/ir/types'; import type { OpenApiMetaObject, OpenApiOperationObject, @@ -6,7 +6,8 @@ import type { OpenApiRequestBodyObject, OpenApiResponseObject, OpenApiSchemaObject, -} from '../openApi/types'; +} from '~/openApi/types'; + import type { StringCase, StringName } from './case'; type EnumsMode = 'inline' | 'root'; diff --git a/packages/openapi-ts/src/types/utils.d.ts b/packages/openapi-ts/src/types/utils.d.ts index ef9b7ae81..c73a2f710 100644 --- a/packages/openapi-ts/src/types/utils.d.ts +++ b/packages/openapi-ts/src/types/utils.d.ts @@ -1,4 +1,4 @@ -import type { GeneratedFile } from '../generate/file'; +import type { GeneratedFile } from '~/generate/file'; /** * Converts all top-level ReadonlyArray properties to Array (shallow). diff --git a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts index 79cbf7c05..e9d378053 100644 --- a/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/handlebars.test.ts @@ -82,7 +82,7 @@ describe('registerHandlebarHelpers', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -94,7 +94,7 @@ describe('registerHandlebarHelpers', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -105,8 +105,8 @@ describe('registerHandlebarHelpers', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', @@ -213,7 +213,7 @@ describe('registerHandlebarTemplates', () => { plugins: { '@hey-api/schemas': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/schemas', @@ -225,7 +225,7 @@ describe('registerHandlebarTemplates', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -236,8 +236,8 @@ describe('registerHandlebarTemplates', () => { }, '@hey-api/typescript': { api: { - getSelector: () => [], schemaToType: () => ({}) as ts.TypeNode, + selector: () => [], }, config: { enums: 'javascript', diff --git a/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts b/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts new file mode 100644 index 000000000..63fb3bcd1 --- /dev/null +++ b/packages/openapi-ts/src/utils/__tests__/minHeap.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest'; + +import { MinHeap } from '../minHeap'; + +describe('MinHeap', () => { + it('pops items in increasing declIndex order', () => { + const idx = new Map([ + ['a', 10], + ['b', 5], + ['c', 20], + ]); + const h = new MinHeap(idx); + h.push('a'); + h.push('b'); + h.push('c'); + + expect(h.pop()).toBe('b'); + expect(h.pop()).toBe('a'); + expect(h.pop()).toBe('c'); + expect(h.pop()).toBeUndefined(); + }); + + it('supports interleaved push/pop and maintains order', () => { + const idx = new Map([ + ['x', 0], + ['y', 1], + ['z', 2], + ]); + const h = new MinHeap(idx); + h.push('y'); + expect(h.pop()).toBe('y'); + + h.push('z'); + h.push('x'); + expect(h.pop()).toBe('x'); + expect(h.pop()).toBe('z'); + }); + + it('handles duplicates (same id pushed multiple times)', () => { + const idx = new Map([['dup', 1]]); + const h = new MinHeap(idx); + h.push('dup'); + h.push('dup'); + expect(h.pop()).toBe('dup'); + // second duplicate still present + expect(h.pop()).toBe('dup'); + expect(h.pop()).toBeUndefined(); + }); + + it('isEmpty returns true when empty and false when not', () => { + const idx = new Map([['a', 0]]); + const h = new MinHeap(idx); + expect(h.isEmpty()).toBe(true); + h.push('a'); + expect(h.isEmpty()).toBe(false); + h.pop(); + expect(h.isEmpty()).toBe(true); + }); +}); diff --git a/packages/openapi-ts/src/utils/__tests__/parse.test.ts b/packages/openapi-ts/src/utils/__tests__/parse.test.ts index 90cc82667..5b28c5897 100644 --- a/packages/openapi-ts/src/utils/__tests__/parse.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/parse.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { operationNameFn } from '../../openApi/common/parser/operation'; +import { operationNameFn } from '~/openApi/common/parser/operation'; + import { setConfig } from '../config'; describe('operationNameFn', () => { @@ -72,7 +73,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -103,7 +104,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -124,7 +125,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -144,7 +145,7 @@ describe('operationNameFn', () => { plugins: { '@hey-api/client-fetch': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/client-fetch', @@ -157,7 +158,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', @@ -177,7 +178,7 @@ describe('operationNameFn', () => { plugins: { '@hey-api/client-fetch': { api: { - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/client-fetch', @@ -190,7 +191,7 @@ describe('operationNameFn', () => { '@hey-api/sdk': { api: { createOperationComment: () => undefined, - getSelector: () => [], + selector: () => [], }, config: { name: '@hey-api/sdk', diff --git a/packages/openapi-ts/src/utils/__tests__/postprocess.test.ts b/packages/openapi-ts/src/utils/__tests__/postprocess.test.ts index 52a78a988..44faf4cb8 100644 --- a/packages/openapi-ts/src/utils/__tests__/postprocess.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/postprocess.test.ts @@ -1,7 +1,8 @@ import { describe, expect, it, vi } from 'vitest'; -import { parseLegacy } from '../../openApi'; -import type { Config } from '../../types/config'; +import { parseLegacy } from '~/openApi'; +import type { Config } from '~/types/config'; + import { getServiceName, postProcessClient } from '../postprocess'; vi.mock('../config', () => { diff --git a/packages/openapi-ts/src/utils/__tests__/sort.test.ts b/packages/openapi-ts/src/utils/__tests__/sort.test.ts index cca8cb614..04bb1f33d 100644 --- a/packages/openapi-ts/src/utils/__tests__/sort.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/sort.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { Model, Service } from '../../types/client'; +import type { Model, Service } from '~/types/client'; + import { sort, sortByName } from '../sort'; describe('sort', () => { diff --git a/packages/openapi-ts/src/utils/__tests__/stringCase.test.ts b/packages/openapi-ts/src/utils/__tests__/stringCase.test.ts index c44e24b2d..3e7f3da87 100644 --- a/packages/openapi-ts/src/utils/__tests__/stringCase.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/stringCase.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import type { StringCase } from '../../types/case'; +import type { StringCase } from '~/types/case'; + import { stringCase } from '../stringCase'; const cases: ReadonlyArray = [ diff --git a/packages/openapi-ts/src/utils/__tests__/type.test.ts b/packages/openapi-ts/src/utils/__tests__/type.test.ts index 5999ff15e..2474d75fe 100644 --- a/packages/openapi-ts/src/utils/__tests__/type.test.ts +++ b/packages/openapi-ts/src/utils/__tests__/type.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it, type MockedFunction, vi } from 'vitest'; -import type { Config } from '../../types/config'; +import type { Config } from '~/types/config'; + import { isLegacyClient } from '../config'; import { transformTypeKeyName } from '../type'; diff --git a/packages/openapi-ts/src/utils/cli.ts b/packages/openapi-ts/src/utils/cli.ts index af10deda1..d65a5b82d 100644 --- a/packages/openapi-ts/src/utils/cli.ts +++ b/packages/openapi-ts/src/utils/cli.ts @@ -1,6 +1,6 @@ import colors from 'ansi-colors'; -import { loadPackageJson } from '../generate/tsConfig'; +import { loadPackageJson } from '~/generate/tsConfig'; const textAscii = ` 888 | e 888~-_ 888 diff --git a/packages/openapi-ts/src/utils/config.ts b/packages/openapi-ts/src/utils/config.ts index dba4fa5af..304753734 100644 --- a/packages/openapi-ts/src/utils/config.ts +++ b/packages/openapi-ts/src/utils/config.ts @@ -1,5 +1,5 @@ -import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; -import type { Config } from '../types/config'; +import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; +import type { Config } from '~/types/config'; let _config: Config; diff --git a/packages/openapi-ts/src/utils/enum.ts b/packages/openapi-ts/src/utils/enum.ts index 5d863b033..eef7f66df 100644 --- a/packages/openapi-ts/src/utils/enum.ts +++ b/packages/openapi-ts/src/utils/enum.ts @@ -1,5 +1,6 @@ -import type { Enum, Model } from '../openApi'; -import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi'; +import type { Enum, Model } from '~/openApi'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '~/openApi'; + import { unescapeName } from './escape'; import { sort } from './sort'; import { unique } from './unique'; diff --git a/packages/openapi-ts/src/utils/escape.ts b/packages/openapi-ts/src/utils/escape.ts index 35728b37c..cf0aa9392 100644 --- a/packages/openapi-ts/src/utils/escape.ts +++ b/packages/openapi-ts/src/utils/escape.ts @@ -1,4 +1,4 @@ -import { EOL } from 'os'; +import { EOL } from 'node:os'; import { validTypescriptIdentifierRegExp } from './regexp'; diff --git a/packages/openapi-ts/src/utils/getHttpRequestName.ts b/packages/openapi-ts/src/utils/getHttpRequestName.ts index c90fb64ce..8b4e74aa5 100644 --- a/packages/openapi-ts/src/utils/getHttpRequestName.ts +++ b/packages/openapi-ts/src/utils/getHttpRequestName.ts @@ -1,4 +1,4 @@ -import type { PluginClientNames } from '../plugins/types'; +import type { PluginClientNames } from '~/plugins/types'; /** * Generate the HttpRequest filename based on the selected client diff --git a/packages/openapi-ts/src/utils/handlebars.ts b/packages/openapi-ts/src/utils/handlebars.ts index c7d37ac24..b8eb52dc3 100644 --- a/packages/openapi-ts/src/utils/handlebars.ts +++ b/packages/openapi-ts/src/utils/handlebars.ts @@ -1,93 +1,94 @@ import Handlebars from 'handlebars'; // @ts-expect-error -import templateClient from '../legacy/handlebars/compiled/client.js'; +import templateClient from '~/legacy/handlebars/compiled/client'; // @ts-expect-error -import angularGetHeaders from '../legacy/handlebars/compiled/core/angular/getHeaders.js'; +import angularGetHeaders from '~/legacy/handlebars/compiled/core/angular/getHeaders'; // @ts-expect-error -import angularGetRequestBody from '../legacy/handlebars/compiled/core/angular/getRequestBody.js'; +import angularGetRequestBody from '~/legacy/handlebars/compiled/core/angular/getRequestBody'; // @ts-expect-error -import angularGetResponseBody from '../legacy/handlebars/compiled/core/angular/getResponseBody.js'; +import angularGetResponseBody from '~/legacy/handlebars/compiled/core/angular/getResponseBody'; // @ts-expect-error -import angularGetResponseHeader from '../legacy/handlebars/compiled/core/angular/getResponseHeader.js'; +import angularGetResponseHeader from '~/legacy/handlebars/compiled/core/angular/getResponseHeader'; // @ts-expect-error -import angularRequest from '../legacy/handlebars/compiled/core/angular/request.js'; +import angularRequest from '~/legacy/handlebars/compiled/core/angular/request'; // @ts-expect-error -import angularSendRequest from '../legacy/handlebars/compiled/core/angular/sendRequest.js'; +import angularSendRequest from '~/legacy/handlebars/compiled/core/angular/sendRequest'; // @ts-expect-error -import templateCoreApiError from '../legacy/handlebars/compiled/core/ApiError.js'; +import templateCoreApiError from '~/legacy/handlebars/compiled/core/ApiError'; // @ts-expect-error -import templateCoreApiRequestOptions from '../legacy/handlebars/compiled/core/ApiRequestOptions.js'; +import templateCoreApiRequestOptions from '~/legacy/handlebars/compiled/core/ApiRequestOptions'; // @ts-expect-error -import templateCoreApiResult from '../legacy/handlebars/compiled/core/ApiResult.js'; +import templateCoreApiResult from '~/legacy/handlebars/compiled/core/ApiResult'; // @ts-expect-error -import axiosGetHeaders from '../legacy/handlebars/compiled/core/axios/getHeaders.js'; +import axiosGetHeaders from '~/legacy/handlebars/compiled/core/axios/getHeaders'; // @ts-expect-error -import axiosGetRequestBody from '../legacy/handlebars/compiled/core/axios/getRequestBody.js'; +import axiosGetRequestBody from '~/legacy/handlebars/compiled/core/axios/getRequestBody'; // @ts-expect-error -import axiosGetResponseBody from '../legacy/handlebars/compiled/core/axios/getResponseBody.js'; +import axiosGetResponseBody from '~/legacy/handlebars/compiled/core/axios/getResponseBody'; // @ts-expect-error -import axiosGetResponseHeader from '../legacy/handlebars/compiled/core/axios/getResponseHeader.js'; +import axiosGetResponseHeader from '~/legacy/handlebars/compiled/core/axios/getResponseHeader'; // @ts-expect-error -import axiosRequest from '../legacy/handlebars/compiled/core/axios/request.js'; +import axiosRequest from '~/legacy/handlebars/compiled/core/axios/request'; // @ts-expect-error -import axiosSendRequest from '../legacy/handlebars/compiled/core/axios/sendRequest.js'; +import axiosSendRequest from '~/legacy/handlebars/compiled/core/axios/sendRequest'; // @ts-expect-error -import templateCoreBaseHttpRequest from '../legacy/handlebars/compiled/core/BaseHttpRequest.js'; +import templateCoreBaseHttpRequest from '~/legacy/handlebars/compiled/core/BaseHttpRequest'; // @ts-expect-error -import templateCancelablePromise from '../legacy/handlebars/compiled/core/CancelablePromise.js'; +import templateCancelablePromise from '~/legacy/handlebars/compiled/core/CancelablePromise'; // @ts-expect-error -import fetchGetHeaders from '../legacy/handlebars/compiled/core/fetch/getHeaders.js'; +import fetchGetHeaders from '~/legacy/handlebars/compiled/core/fetch/getHeaders'; // @ts-expect-error -import fetchGetRequestBody from '../legacy/handlebars/compiled/core/fetch/getRequestBody.js'; +import fetchGetRequestBody from '~/legacy/handlebars/compiled/core/fetch/getRequestBody'; // @ts-expect-error -import fetchGetResponseBody from '../legacy/handlebars/compiled/core/fetch/getResponseBody.js'; +import fetchGetResponseBody from '~/legacy/handlebars/compiled/core/fetch/getResponseBody'; // @ts-expect-error -import fetchGetResponseHeader from '../legacy/handlebars/compiled/core/fetch/getResponseHeader.js'; +import fetchGetResponseHeader from '~/legacy/handlebars/compiled/core/fetch/getResponseHeader'; // @ts-expect-error -import fetchRequest from '../legacy/handlebars/compiled/core/fetch/request.js'; +import fetchRequest from '~/legacy/handlebars/compiled/core/fetch/request'; // @ts-expect-error -import fetchSendRequest from '../legacy/handlebars/compiled/core/fetch/sendRequest.js'; +import fetchSendRequest from '~/legacy/handlebars/compiled/core/fetch/sendRequest'; // @ts-expect-error -import functionBase64 from '../legacy/handlebars/compiled/core/functions/base64.js'; +import functionBase64 from '~/legacy/handlebars/compiled/core/functions/base64'; // @ts-expect-error -import functionCatchErrorCodes from '../legacy/handlebars/compiled/core/functions/catchErrorCodes.js'; +import functionCatchErrorCodes from '~/legacy/handlebars/compiled/core/functions/catchErrorCodes'; // @ts-expect-error -import functionGetFormData from '../legacy/handlebars/compiled/core/functions/getFormData.js'; +import functionGetFormData from '~/legacy/handlebars/compiled/core/functions/getFormData'; // @ts-expect-error -import functionGetQueryString from '../legacy/handlebars/compiled/core/functions/getQueryString.js'; +import functionGetQueryString from '~/legacy/handlebars/compiled/core/functions/getQueryString'; // @ts-expect-error -import functionGetUrl from '../legacy/handlebars/compiled/core/functions/getUrl.js'; +import functionGetUrl from '~/legacy/handlebars/compiled/core/functions/getUrl'; // @ts-expect-error -import functionIsBlob from '../legacy/handlebars/compiled/core/functions/isBlob.js'; +import functionIsBlob from '~/legacy/handlebars/compiled/core/functions/isBlob'; // @ts-expect-error -import functionIsFormData from '../legacy/handlebars/compiled/core/functions/isFormData.js'; +import functionIsFormData from '~/legacy/handlebars/compiled/core/functions/isFormData'; // @ts-expect-error -import functionIsString from '../legacy/handlebars/compiled/core/functions/isString.js'; +import functionIsString from '~/legacy/handlebars/compiled/core/functions/isString'; // @ts-expect-error -import functionIsStringWithValue from '../legacy/handlebars/compiled/core/functions/isStringWithValue.js'; +import functionIsStringWithValue from '~/legacy/handlebars/compiled/core/functions/isStringWithValue'; // @ts-expect-error -import functionIsSuccess from '../legacy/handlebars/compiled/core/functions/isSuccess.js'; +import functionIsSuccess from '~/legacy/handlebars/compiled/core/functions/isSuccess'; // @ts-expect-error -import functionResolve from '../legacy/handlebars/compiled/core/functions/resolve.js'; +import functionResolve from '~/legacy/handlebars/compiled/core/functions/resolve'; // @ts-expect-error -import templateCoreHttpRequest from '../legacy/handlebars/compiled/core/HttpRequest.js'; +import templateCoreHttpRequest from '~/legacy/handlebars/compiled/core/HttpRequest'; // @ts-expect-error -import templateCoreSettings from '../legacy/handlebars/compiled/core/OpenAPI.js'; +import templateCoreSettings from '~/legacy/handlebars/compiled/core/OpenAPI'; // @ts-expect-error -import templateCoreRequest from '../legacy/handlebars/compiled/core/request.js'; +import templateCoreRequest from '~/legacy/handlebars/compiled/core/request'; // @ts-expect-error -import xhrGetHeaders from '../legacy/handlebars/compiled/core/xhr/getHeaders.js'; +import xhrGetHeaders from '~/legacy/handlebars/compiled/core/xhr/getHeaders'; // @ts-expect-error -import xhrGetRequestBody from '../legacy/handlebars/compiled/core/xhr/getRequestBody.js'; +import xhrGetRequestBody from '~/legacy/handlebars/compiled/core/xhr/getRequestBody'; // @ts-expect-error -import xhrGetResponseBody from '../legacy/handlebars/compiled/core/xhr/getResponseBody.js'; +import xhrGetResponseBody from '~/legacy/handlebars/compiled/core/xhr/getResponseBody'; // @ts-expect-error -import xhrGetResponseHeader from '../legacy/handlebars/compiled/core/xhr/getResponseHeader.js'; +import xhrGetResponseHeader from '~/legacy/handlebars/compiled/core/xhr/getResponseHeader'; // @ts-expect-error -import xhrRequest from '../legacy/handlebars/compiled/core/xhr/request.js'; +import xhrRequest from '~/legacy/handlebars/compiled/core/xhr/request'; // @ts-expect-error -import xhrSendRequest from '../legacy/handlebars/compiled/core/xhr/sendRequest.js'; +import xhrSendRequest from '~/legacy/handlebars/compiled/core/xhr/sendRequest'; + import { getConfig } from './config'; import { stringCase } from './stringCase'; import { transformClassName } from './transform'; diff --git a/packages/openapi-ts/src/utils/input/heyApi.ts b/packages/openapi-ts/src/utils/input/heyApi.ts index 08af7efc7..2f5b1ea7f 100644 --- a/packages/openapi-ts/src/utils/input/heyApi.ts +++ b/packages/openapi-ts/src/utils/input/heyApi.ts @@ -1,4 +1,4 @@ -import type { Input } from '../../types/input'; +import type { Input } from '~/types/input'; // Regular expression to match Hey API Registry input formats: // - {organization}/{project}?{queryParams} diff --git a/packages/openapi-ts/src/utils/input/index.ts b/packages/openapi-ts/src/utils/input/index.ts index c0b96b5c1..78bd3ced1 100644 --- a/packages/openapi-ts/src/utils/input/index.ts +++ b/packages/openapi-ts/src/utils/input/index.ts @@ -1,4 +1,5 @@ -import type { Input } from '../../types/input'; +import type { Input } from '~/types/input'; + import { heyApiRegistryBaseUrl, inputToHeyApiPath } from './heyApi'; import { inputToReadmePath } from './readme'; import { inputToScalarPath } from './scalar'; diff --git a/packages/openapi-ts/src/utils/input/readme.ts b/packages/openapi-ts/src/utils/input/readme.ts index 3ed5a5cdf..d5f10bdee 100644 --- a/packages/openapi-ts/src/utils/input/readme.ts +++ b/packages/openapi-ts/src/utils/input/readme.ts @@ -1,4 +1,4 @@ -import type { Input } from '../../types/input'; +import type { Input } from '~/types/input'; // Regular expression to match ReadMe API Registry input formats: // - @{organization}/{project}#{uuid} diff --git a/packages/openapi-ts/src/utils/input/scalar.ts b/packages/openapi-ts/src/utils/input/scalar.ts index 5686ae114..8e00aafb4 100644 --- a/packages/openapi-ts/src/utils/input/scalar.ts +++ b/packages/openapi-ts/src/utils/input/scalar.ts @@ -1,4 +1,4 @@ -import type { Input } from '../../types/input'; +import type { Input } from '~/types/input'; // Regular expression to match Scalar API Registry input formats: // - @{organization}/{project} diff --git a/packages/openapi-ts/src/utils/meta.ts b/packages/openapi-ts/src/utils/meta.ts index 5110278b1..af055a89e 100644 --- a/packages/openapi-ts/src/utils/meta.ts +++ b/packages/openapi-ts/src/utils/meta.ts @@ -1,4 +1,5 @@ -import { getType } from '../openApi'; +import { getType } from '~/openApi'; + import { refParametersPartial, refSchemasPartial } from './const'; import { reservedJavaScriptKeywordsRegExp } from './regexp'; import { cleanAndTransformTypeName } from './transform'; diff --git a/packages/openapi-ts/src/utils/minHeap.ts b/packages/openapi-ts/src/utils/minHeap.ts new file mode 100644 index 000000000..0c502a411 --- /dev/null +++ b/packages/openapi-ts/src/utils/minHeap.ts @@ -0,0 +1,64 @@ +export class MinHeap { + private heap: Array = []; + + constructor(public declIndex: Map) {} + + isEmpty(): boolean { + return !this.heap.length; + } + + pop(): string | undefined { + const [top] = this.heap; + if (!this.heap.length) return; + const last = this.heap.pop()!; + if (!this.heap.length) return top; + this.heap[0] = last; + this.sinkDown(0); + return top; + } + + push(item: string): void { + this.heap.push(item); + this.bubbleUp(this.heap.length - 1); + } + + private bubbleUp(index: number): void { + const heap = this.heap; + while (index > 0) { + const parent = Math.floor((index - 1) / 2); + const parentVal = heap[parent]!; + const curVal = heap[index]!; + if (this.declIndex.get(parentVal)! <= this.declIndex.get(curVal)!) break; + heap[parent] = curVal; + heap[index] = parentVal; + index = parent; + } + } + + private sinkDown(index: number): void { + const heap = this.heap; + const len = heap.length; + while (true) { + const left = 2 * index + 1; + const right = 2 * index + 2; + let smallest = index; + if (left < len) { + const leftVal = heap[left]!; + const smallestVal = heap[smallest]!; + if (this.declIndex.get(leftVal)! < this.declIndex.get(smallestVal)!) + smallest = left; + } + if (right < len) { + const rightVal = heap[right]!; + const smallestVal = heap[smallest]!; + if (this.declIndex.get(rightVal)! < this.declIndex.get(smallestVal)!) + smallest = right; + } + if (smallest === index) break; + const tmp = heap[smallest]!; + heap[smallest] = heap[index]!; + heap[index] = tmp; + index = smallest; + } + } +} diff --git a/packages/openapi-ts/src/utils/postprocess.ts b/packages/openapi-ts/src/utils/postprocess.ts index c19937a2e..6e6b6b9aa 100644 --- a/packages/openapi-ts/src/utils/postprocess.ts +++ b/packages/openapi-ts/src/utils/postprocess.ts @@ -1,7 +1,8 @@ -import type { Client as ParserClient, Model } from '../openApi'; -import { sanitizeNamespaceIdentifier } from '../openApi'; -import type { Client, Operation, Service } from '../types/client'; -import type { Config } from '../types/config'; +import type { Client as ParserClient, Model } from '~/openApi'; +import { sanitizeNamespaceIdentifier } from '~/openApi'; +import type { Client, Operation, Service } from '~/types/client'; +import type { Config } from '~/types/config'; + import { getConfig, legacyNameFromConfig } from './config'; import { sort } from './sort'; import { stringCase } from './stringCase'; diff --git a/packages/openapi-ts/src/utils/stringCase.ts b/packages/openapi-ts/src/utils/stringCase.ts index 9b279a35a..d5acb3ba1 100644 --- a/packages/openapi-ts/src/utils/stringCase.ts +++ b/packages/openapi-ts/src/utils/stringCase.ts @@ -1,4 +1,4 @@ -import type { StringCase } from '../types/case'; +import type { StringCase } from '~/types/case'; const uppercaseRegExp = /[\p{Lu}]/u; const lowercaseRegExp = /[\p{Ll}]/u; diff --git a/packages/openapi-ts/src/utils/transform.ts b/packages/openapi-ts/src/utils/transform.ts index 8b68a18f1..a766161e6 100644 --- a/packages/openapi-ts/src/utils/transform.ts +++ b/packages/openapi-ts/src/utils/transform.ts @@ -1,5 +1,6 @@ -import { ensureValidTypeScriptJavaScriptIdentifier } from '../openApi'; -import type { Config } from '../types/config'; +import { ensureValidTypeScriptJavaScriptIdentifier } from '~/openApi'; +import type { Config } from '~/types/config'; + import { getConfig } from './config'; import { reservedJavaScriptKeywordsRegExp } from './regexp'; import { stringCase } from './stringCase'; diff --git a/packages/openapi-ts/src/utils/type.ts b/packages/openapi-ts/src/utils/type.ts index 351817a0e..04767ebb8 100644 --- a/packages/openapi-ts/src/utils/type.ts +++ b/packages/openapi-ts/src/utils/type.ts @@ -1,9 +1,10 @@ import type ts from 'typescript'; -import type { Model } from '../openApi'; -import { sanitizeOperationParameterName } from '../openApi'; -import { type Property, tsc } from '../tsc'; -import type { Client } from '../types/client'; +import type { Model } from '~/openApi'; +import { sanitizeOperationParameterName } from '~/openApi'; +import { type Property, tsc } from '~/tsc'; +import type { Client } from '~/types/client'; + import { getConfig, isLegacyClient } from './config'; import { refSchemasPartial } from './const'; import { enumValue } from './enum'; diff --git a/packages/openapi-ts/tsconfig.base.json b/packages/openapi-ts/tsconfig.base.json index 5afc381fb..43a2b88eb 100644 --- a/packages/openapi-ts/tsconfig.base.json +++ b/packages/openapi-ts/tsconfig.base.json @@ -11,6 +11,10 @@ "noUnusedParameters": true, "strict": true, "target": "ES2022", - "useUnknownInCatchVariables": false + "useUnknownInCatchVariables": false, + "baseUrl": "./", + "paths": { + "~/*": ["src/*"] + } } } diff --git a/packages/openapi-ts/vitest.config.ts b/packages/openapi-ts/vitest.config.ts index 1ae312607..21c403f8e 100644 --- a/packages/openapi-ts/vitest.config.ts +++ b/packages/openapi-ts/vitest.config.ts @@ -1,12 +1,19 @@ +import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createVitestConfig } from '@config/vite-base'; -export default createVitestConfig( - fileURLToPath(new URL('./', import.meta.url)), - { - test: { - setupFiles: './setupTests.ts', - }, +const rootDir = fileURLToPath(new URL('./', import.meta.url)); + +export default createVitestConfig(rootDir, { + resolve: { + alias: [ + // Support imports like `~/foo` and `~` -> maps to /src + { find: /^~\/(.*)/, replacement: path.resolve(rootDir, 'src/$1') }, + { find: '~', replacement: path.resolve(rootDir, 'src') }, + ], + }, + test: { + setupFiles: './setupTests.ts', }, -); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7821c7962..7c4621e47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,8 +53,8 @@ importers: specifier: 9.17.0 version: 9.17.0(jiti@2.6.1) eslint-config-prettier: - specifier: 9.1.0 - version: 9.1.0(eslint@9.17.0(jiti@2.6.1)) + specifier: 9.1.2 + version: 9.1.2(eslint@9.17.0(jiti@2.6.1)) eslint-plugin-simple-import-sort: specifier: 12.1.1 version: 12.1.1(eslint@9.17.0(jiti@2.6.1)) @@ -529,7 +529,7 @@ importers: version: link:../../packages/nuxt nuxt: specifier: 3.14.1592 - version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) vue: specifier: 3.5.13 version: 3.5.13(typescript@5.9.3) @@ -1235,7 +1235,7 @@ importers: version: 1.7.4 nuxt: specifier: '>=3.0.0' - version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) vue: specifier: '>=3.5.13' version: 3.5.13(typescript@5.9.3) @@ -1262,11 +1262,11 @@ importers: packages/openapi-ts: dependencies: '@hey-api/codegen-core': - specifier: workspace:^0.3.0 + specifier: workspace:^0.3.1 version: link:../codegen-core '@hey-api/json-schema-ref-parser': - specifier: 1.2.0 - version: 1.2.0 + specifier: 1.2.1 + version: 1.2.1 ansi-colors: specifier: 4.1.3 version: 4.1.3 @@ -1336,7 +1336,7 @@ importers: version: 10.4.3 nuxt: specifier: 3.14.1592 - version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) ofetch: specifier: 1.4.1 version: 1.4.1 @@ -1362,14 +1362,14 @@ importers: packages/openapi-ts-tests/main: devDependencies: '@angular-devkit/build-angular': - specifier: 19.2.17 - version: 19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0) + specifier: 19.2.18 + version: 19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0) '@angular/animations': specifier: 19.2.15 version: 19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)) '@angular/cli': - specifier: 19.2.17 - version: 19.2.17(@types/node@22.10.5)(chokidar@4.0.3) + specifier: 19.2.18 + version: 19.2.18(@types/node@22.10.5)(chokidar@4.0.3) '@angular/common': specifier: 19.2.15 version: 19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1) @@ -1427,6 +1427,9 @@ importers: '@types/cross-spawn': specifier: 6.0.6 version: 6.0.6 + arktype: + specifier: 2.1.23 + version: 2.1.23 axios: specifier: 1.8.2 version: 1.8.2 @@ -1447,7 +1450,7 @@ importers: version: 3.3.2 nuxt: specifier: 3.14.1592 - version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + version: 3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) ofetch: specifier: 1.4.1 version: 1.4.1 @@ -1539,8 +1542,8 @@ packages: resolution: {integrity: sha512-RbqhStc6ZoRv57ZqLB36VOkBkAdU3nNezCvIs0AJV5V4+vLPMrb0hpIB0sF+9yMlMjWsolnRsj0/Fil+zQG3bw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@angular-devkit/architect@0.1902.17': - resolution: {integrity: sha512-/LV8lXi6/SqevyI9ZAk2uAqlnN/pUwNwD6SyjotCqU55FBhBW8vM3/GucFXawJqTOzNmBXuMx1YVvQN5H0v5LQ==} + '@angular-devkit/architect@0.1902.18': + resolution: {integrity: sha512-3AyIlxbJWmWJm/CPS6S57kWBydMdYUPtF+SK8tqzwcBnyRbLwXoI7UbxstZ/C9J1hAY8QdZrDYGotwlHwhiC8g==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@angular-devkit/build-angular@19.2.0': @@ -1631,15 +1634,15 @@ packages: tailwindcss: optional: true - '@angular-devkit/build-angular@19.2.17': - resolution: {integrity: sha512-lbvzNoSjHlhP6bcHtFMlEQHG/Zxc1tTdwoelm4+AWPuQH4rGfoty4SXH4rr50SXVBUg9Zb4xZuChOYZmYKpGLQ==} + '@angular-devkit/build-angular@19.2.18': + resolution: {integrity: sha512-OFrNsWT7GovW2MX4nDx2U9bEsAhOhJUbTm2qibMUSnVjm/iJhdcZXRB7qmDjetVp68nAXvcHCPPIo+2WlfLdig==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler-cli': ^19.0.0 || ^19.2.0-next.0 '@angular/localize': ^19.0.0 || ^19.2.0-next.0 '@angular/platform-server': ^19.0.0 || ^19.2.0-next.0 '@angular/service-worker': ^19.0.0 || ^19.2.0-next.0 - '@angular/ssr': ^19.2.17 + '@angular/ssr': ^19.2.18 '@web/test-runner': ^0.20.0 browser-sync: ^3.0.2 jest: ^29.5.0 @@ -1689,8 +1692,8 @@ packages: webpack: ^5.30.0 webpack-dev-server: ^5.0.2 - '@angular-devkit/build-webpack@0.1902.17': - resolution: {integrity: sha512-8NVJL7ujeTYKR1LgErkc5UN3EEoGYasqtu5AACXraFf9NLOw2p9N0+QY4cfjIwip1nyBp0RRzlBS4omGEymJCw==} + '@angular-devkit/build-webpack@0.1902.18': + resolution: {integrity: sha512-pOo8HFEE92MdL6BwBvvWb8wiZ8Y3KHRPewG3pwHdJfL3pCVP3oNThJnFVLxrA/LxJK7kFRzKexhqd89OteZiYw==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: webpack: ^5.30.0 @@ -1714,8 +1717,8 @@ packages: chokidar: optional: true - '@angular-devkit/core@19.2.17': - resolution: {integrity: sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==} + '@angular-devkit/core@19.2.18': + resolution: {integrity: sha512-D/JbeM3yAZ6Cnk/3ez8MvoTjx1pgUnkJHvDkuMhRuelCi3m0b0Qt/3548ie7CU+oLHdzAzjFhEvCPNssdevTRQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^4.0.0 @@ -1731,8 +1734,8 @@ packages: resolution: {integrity: sha512-kNOJ+3vekJJCQKWihNmxBkarJzNW09kP5a9E1SRNiQVNOUEeSwcRR0qYotM65nx821gNzjjhJXnAZ8OazWldrg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@angular-devkit/schematics@19.2.17': - resolution: {integrity: sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==} + '@angular-devkit/schematics@19.2.18': + resolution: {integrity: sha512-DYiQDKv2jnT0j+d8SeWynCCGERWIYDkdS6bQKiO7rSc7ChXby2fFZZ7VpcEHGv7l2K2/I+q9mZTG0i/g5mSzCg==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@angular/animations@19.2.0': @@ -1827,8 +1830,8 @@ packages: tailwindcss: optional: true - '@angular/build@19.2.17': - resolution: {integrity: sha512-JrF9dSrsMip2xJzSz3zNoozBXu/OYg0bHuKfuPA/usPhz5AomJ2SQ2unvl6sDF00pTlgJohJMQ6SUHjylybn2g==} + '@angular/build@19.2.18': + resolution: {integrity: sha512-dUpcqiryCunKdIF3FnVeO0tkxXctOk1JTaVoIA9vTW1fV8zdtRMVduq5yikE3Z3ec3AIC/2F+afD6BcF2fqliQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler': ^19.0.0 || ^19.2.0-next.0 @@ -1836,7 +1839,7 @@ packages: '@angular/localize': ^19.0.0 || ^19.2.0-next.0 '@angular/platform-server': ^19.0.0 || ^19.2.0-next.0 '@angular/service-worker': ^19.0.0 || ^19.2.0-next.0 - '@angular/ssr': ^19.2.17 + '@angular/ssr': ^19.2.18 karma: ^6.4.0 less: ^4.2.0 ng-packagr: ^19.0.0 || ^19.2.0-next.0 @@ -1880,8 +1883,8 @@ packages: engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular/cli@19.2.17': - resolution: {integrity: sha512-BIdFAPbDOASSVUvtJ2sFRlsmkSyX9VgdAs631RoVMlw/BskIW1q/MUJtwzEsOsybVpSH7vGHYJa6JFqDDriiRg==} + '@angular/cli@19.2.18': + resolution: {integrity: sha512-TwqS0+4k28EepFNRalQJs4qj4axLCfFSJJAWP+mZlVUyCgYL6L7Kw851f7tfG6wTuSV1xI8ysJtRtycAEqooJA==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true @@ -2124,6 +2127,15 @@ packages: resolution: {integrity: sha512-Izvir8iIoU+X4SKtDAa5kpb+9cpifclzsbA8x/AZY0k0gIfXYQ1fa1B6Epfe6vNA2YfDX8VtrZFgvnXB6aPEoQ==} engines: {node: '>=18'} + '@ark/regex@0.0.0': + resolution: {integrity: sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg==} + + '@ark/schema@0.50.0': + resolution: {integrity: sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ==} + + '@ark/util@0.50.0': + resolution: {integrity: sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -3947,8 +3959,8 @@ packages: '@fontsource/fira-mono@5.0.0': resolution: {integrity: sha512-IsinH/oLYJyv/sQv7SbKmjoAXZsSjm6Q1Tz5GBBXCXi3Jg9MzXmKvWm9bSLC8lFI6CDsi8GkH/DAgZ98t8bhTQ==} - '@hey-api/json-schema-ref-parser@1.2.0': - resolution: {integrity: sha512-BMnIuhVgNmSudadw1GcTsP18Yk5l8FrYrg/OSYNxz0D2E0vf4D5e4j5nUbuY8MU6p1vp7ev0xrfP6A/NWazkzQ==} + '@hey-api/json-schema-ref-parser@1.2.1': + resolution: {integrity: sha512-inPeksRLq+j3ArnuGOzQPQE//YrhezQG0+9Y9yizScBN2qatJ78fIByhEgKdNAbtguDCn4RPxmEhcrePwHxs4A==} engines: {node: '>= 16'} '@humanfs/core@0.19.1': @@ -4619,8 +4631,8 @@ packages: typescript: '>=5.5 <5.9' webpack: ^5.54.0 - '@ngtools/webpack@19.2.17': - resolution: {integrity: sha512-HpbOLwS8tIW041UXcMqwfySqpZ9ztObH8U4NWKwjPBe0S5UDnF6doW2rS3GQm71hkiuB8sqbxOWz5I/NNvZFNQ==} + '@ngtools/webpack@19.2.18': + resolution: {integrity: sha512-UHwrNcrq1TfiGbQ04xw6rmsijWqTjsh7gTizrJILMDF4U6xVvSth8FolCyLhAjheT5e9zKfIHXeGxUS0Qgrp9A==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler-cli': ^19.0.0 || ^19.2.0-next.0 @@ -4775,8 +4787,8 @@ packages: '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - '@oxc-project/types@0.94.0': - resolution: {integrity: sha512-+UgQT/4o59cZfH6Cp7G0hwmqEQ0wE+AdIwhikdwnhWI9Dp8CgSY081+Q3O67/wq3VJu8mgUEB93J9EHHn70fOw==} + '@oxc-project/types@0.95.0': + resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -5572,85 +5584,85 @@ packages: '@types/react-dom': optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.43': - resolution: {integrity: sha512-TP8bcPOb1s6UmY5syhXrDn9k0XkYcw+XaoylTN4cJxf0JOVS2j682I3aTcpfT51hOFGr2bRwNKN9RZ19XxeQbA==} + '@rolldown/binding-android-arm64@1.0.0-beta.44': + resolution: {integrity: sha512-g9ejDOehJFhxC1DIXQuZQ9bKv4lRDioOTL42cJjFjqKPl1L7DVb9QQQE1FxokGEIMr6FezLipxwnzOXWe7DNPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-beta.43': - resolution: {integrity: sha512-kuVWnZsE4vEjMF/10SbSUyzucIW2zmdsqFghYMqy+fsjXnRHg0luTU6qWF8IqJf4Cbpm9NEZRnjIEPpAbdiSNQ==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.44': + resolution: {integrity: sha512-PxAW1PXLPmCzfhfKIS53kwpjLGTUdIfX4Ht+l9mj05C3lYCGaGowcNsYi2rdxWH24vSTmeK+ajDNRmmmrK0M7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.43': - resolution: {integrity: sha512-u9Ps4sh6lcmJ3vgLtyEg/x4jlhI64U0mM93Ew+tlfFdLDe7yKyA+Fe80cpr2n1mNCeZXrvTSbZluKpXQ0GxLjw==} + '@rolldown/binding-darwin-x64@1.0.0-beta.44': + resolution: {integrity: sha512-/CtQqs1oO9uSb5Ju60rZvsdjE7Pzn8EK2ISAdl2jedjMzeD/4neNyCbwyJOAPzU+GIQTZVyrFZJX+t7HXR1R/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.43': - resolution: {integrity: sha512-h9lUtVtXgfbk/tnicMpbFfZ3DJvk5Zn2IvmlC1/e0+nUfwoc/TFqpfrRRqcNBXk/e+xiWMSKv6b0MF8N+Rtvlg==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.44': + resolution: {integrity: sha512-V5Q5W9c4+2GJ4QabmjmVV6alY97zhC/MZBaLkDtHwGy3qwzbM4DYgXUbun/0a8AH5hGhuU27tUIlYz6ZBlvgOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.43': - resolution: {integrity: sha512-IX2C6bA6wM2rX/RvD75ko+ix9yxPKjKGGq7pOhB8wGI4Z4fqX5B1nDHga/qMDmAdCAR1m9ymzxkmqhm/AFYf7A==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.44': + resolution: {integrity: sha512-X6adjkHeFqKsTU0FXdNN9HY4LDozPqIfHcnXovE5RkYLWIjMWuc489mIZ6iyhrMbCqMUla9IOsh5dvXSGT9o9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.43': - resolution: {integrity: sha512-mcjd57vEj+CEQbZAzUiaxNzNgwwgOpFtZBWcINm8DNscvkXl5b/s622Z1dqGNWSdrZmdjdC6LWMvu8iHM6v9sQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.44': + resolution: {integrity: sha512-kRRKGZI4DXWa6ANFr3dLA85aSVkwPdgXaRjfanwY84tfc3LncDiIjyWCb042e3ckPzYhHSZ3LmisO+cdOIYL6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.43': - resolution: {integrity: sha512-Pa8QMwlkrztTo/1mVjZmPIQ44tCSci10TBqxzVBvXVA5CFh5EpiEi99fPSll2dHG2uT4dCOMeC6fIhyDdb0zXA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.44': + resolution: {integrity: sha512-hMtiN9xX1NhxXBa2U3Up4XkVcsVp2h73yYtMDY59z9CDLEZLrik9RVLhBL5QtoX4zZKJ8HZKJtWuGYvtmkCbIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.43': - resolution: {integrity: sha512-BgynXKMjeaX4AfWLARhOKDetBOOghnSiVRjAHVvhiAaDXgdQN8e65mSmXRiVoVtD3cHXx/cfU8Gw0p0K+qYKVQ==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.44': + resolution: {integrity: sha512-rd1LzbpXQuR8MTG43JB9VyXDjG7ogSJbIkBpZEHJ8oMKzL6j47kQT5BpIXrg3b5UVygW9QCI2fpFdMocT5Kudg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.43': - resolution: {integrity: sha512-VIsoPlOB/tDSAw9CySckBYysoIBqLeps1/umNSYUD8pMtalJyzMTneAVI1HrUdf4ceFmQ5vARoLIXSsPwVFxNg==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.44': + resolution: {integrity: sha512-qI2IiPqmPRW25exXkuQr3TlweCDc05YvvbSDRPCuPsWkwb70dTiSoXn8iFxT4PWqTi71wWHg1Wyta9PlVhX5VA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.43': - resolution: {integrity: sha512-YDXTxVJG67PqTQMKyjVJSddoPbSWJ4yRz/E3xzTLHqNrTDGY0UuhG8EMr8zsYnfH/0cPFJ3wjQd/hJWHuR6nkA==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.44': + resolution: {integrity: sha512-+vHvEc1pL5iJRFlldLC8mjm6P4Qciyfh2bh5ZI6yxDQKbYhCHRKNURaKz1mFcwxhVL5YMYsLyaqM3qizVif9MQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.43': - resolution: {integrity: sha512-3M+2DmorXvDuAIGYQ9Z93Oy1G9ETkejLwdXXb1uRTgKN9pMcu7N+KG2zDrJwqyxeeLIFE22AZGtSJm3PJbNu9Q==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.44': + resolution: {integrity: sha512-XSgLxRrtFj6RpTeMYmmQDAwHjKseYGKUn5LPiIdW4Cq+f5SBSStL2ToBDxkbdxKPEbCZptnLPQ/nfKcAxrC8Xg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.43': - resolution: {integrity: sha512-/B1j1pJs33y9ywtslOMxryUPHq8zIGu/OGEc2gyed0slimJ8fX2uR/SaJVhB4+NEgCFIeYDR4CX6jynAkeRuCA==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.44': + resolution: {integrity: sha512-cF1LJdDIX02cJrFrX3wwQ6IzFM7I74BYeKFkzdcIA4QZ0+2WA7/NsKIgjvrunupepWb1Y6PFWdRlHSaz5AW1Wg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.43': - resolution: {integrity: sha512-29oG1swCz7hNP+CQYrsM4EtylsKwuYzM8ljqbqC5TsQwmKat7P8ouDpImsqg/GZxFSXcPP9ezQm0Q0wQwGM3JA==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.44': + resolution: {integrity: sha512-5uaJonDafhHiMn+iEh7qUp3QQ4Gihv3lEOxKfN8Vwadpy0e+5o28DWI42DpJ9YBYMrVy4JOWJ/3etB/sptpUwA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.43': - resolution: {integrity: sha512-eWBV1Ef3gfGNehxVGCyXs7wLayRIgCmyItuCZwYYXW5bsk4EvR4n2GP5m3ohjnx7wdiY3nLmwQfH2Knb5gbNZw==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.44': + resolution: {integrity: sha512-vsqhWAFJkkmgfBN/lkLCWTXF1PuPhMjfnAyru48KvF7mVh2+K7WkKYHezF3Fjz4X/mPScOcIv+g6cf6wnI6eWg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -5658,8 +5670,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.29': resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} - '@rolldown/pluginutils@1.0.0-beta.43': - resolution: {integrity: sha512-5Uxg7fQUCmfhax7FJke2+8B6cqgeUJUD9o2uXIKXhD+mG0mL6NObmVoi9wXEU1tY89mZKgAYA6fTbftx3q2ZPQ==} + '@rolldown/pluginutils@1.0.0-beta.44': + resolution: {integrity: sha512-g6eW7Zwnr2c5RADIoqziHoVs6b3W5QTQ4+qbpfjbkMJ9x+8Og211VW/oot2dj9dVwaK/UyC6Yo+02gV+wWQVNg==} '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} @@ -6074,8 +6086,8 @@ packages: resolution: {integrity: sha512-dz/eoFQKG09POSygpEDdlCehFIMo35HUM2rVV8lx9PfQEibpbGwl1NNQYEbqwVjTyCyD/ILyIXCWPE+EfTnG4g==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} - '@schematics/angular@19.2.17': - resolution: {integrity: sha512-FGNDJXjeCoYz3JMze3JReNOGkPsxLGs5LmLf5Zj3+BYYMsFoDUJD9BMRRf+tmc/epBkiZZtv80c8gmfhqGv4dA==} + '@schematics/angular@19.2.18': + resolution: {integrity: sha512-GUR+7RIXm91nq4EZ+Ofg/RccHNyd6S/vPTMd1Q4nCtkgbEgjqFM3F//JVJJDwmwai7+hHJWlsCILz/hHCQOCHQ==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@sec-ant/readable-stream@0.4.1': @@ -6268,9 +6280,6 @@ packages: resolution: {integrity: sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==} engines: {node: ^18.17.0 || >=20.5.0} - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -7268,6 +7277,9 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + arktype@2.1.23: + resolution: {integrity: sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ==} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -8578,6 +8590,12 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-config-prettier@9.1.2: + resolution: {integrity: sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} @@ -11942,8 +11960,8 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-beta.43: - resolution: {integrity: sha512-6RcqyRx0tY1MlRLnjXPp/849Rl/CPFhzpGGwNPEPjKwqBMqPq/Rbbkxasa8s0x+IkUk46ty4jazb5skZ/Vgdhw==} + rolldown@1.0.0-beta.44: + resolution: {integrity: sha512-gcqgyCi3g93Fhr49PKvymE8PoaGS0sf6ajQrsYaQ8o5de6aUEbD6rJZiJbhOfpcqOnycgsAsUNPYri1h25NgsQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -14203,9 +14221,9 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/architect@0.1902.17(chokidar@4.0.3)': + '@angular-devkit/architect@0.1902.18(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/core': 19.2.18(chokidar@4.0.3) rxjs: 7.8.1 transitivePeerDependencies: - chokidar @@ -14474,13 +14492,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0)': + '@angular-devkit/build-angular@19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(yaml@2.8.0)': dependencies: '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.1902.17(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.17(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0) - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) - '@angular/build': 19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(terser@5.39.0)(typescript@5.9.3)(yaml@2.8.0) + '@angular-devkit/architect': 0.1902.18(chokidar@4.0.3) + '@angular-devkit/build-webpack': 0.1902.18(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0) + '@angular-devkit/core': 19.2.18(chokidar@4.0.3) + '@angular/build': 19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(terser@5.39.0)(typescript@5.9.3)(yaml@2.8.0) '@angular/compiler-cli': 19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3) '@babel/core': 7.26.10 '@babel/generator': 7.26.10 @@ -14492,7 +14510,7 @@ snapshots: '@babel/preset-env': 7.26.9(@babel/core@7.26.10) '@babel/runtime': 7.26.10 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.98.0) + '@ngtools/webpack': 19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.98.0) '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.2) @@ -14579,9 +14597,9 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-webpack@0.1902.17(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0)': + '@angular-devkit/build-webpack@0.1902.18(chokidar@4.0.3)(webpack-dev-server@5.2.2(webpack@5.98.0))(webpack@5.98.0)': dependencies: - '@angular-devkit/architect': 0.1902.17(chokidar@4.0.3) + '@angular-devkit/architect': 0.1902.18(chokidar@4.0.3) rxjs: 7.8.1 webpack: 5.98.0(esbuild@0.25.0) webpack-dev-server: 5.2.2(webpack@5.98.0) @@ -14610,7 +14628,7 @@ snapshots: optionalDependencies: chokidar: 4.0.3 - '@angular-devkit/core@19.2.17(chokidar@4.0.3)': + '@angular-devkit/core@19.2.18(chokidar@4.0.3)': dependencies: ajv: 8.17.1 ajv-formats: 3.0.1(ajv@8.17.1) @@ -14641,9 +14659,9 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/schematics@19.2.17(chokidar@4.0.3)': + '@angular-devkit/schematics@19.2.18(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) + '@angular-devkit/core': 19.2.18(chokidar@4.0.3) jsonc-parser: 3.3.1 magic-string: 0.30.17 ora: 5.4.1 @@ -14773,10 +14791,10 @@ snapshots: - tsx - yaml - '@angular/build@19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(terser@5.39.0)(typescript@5.9.3)(yaml@2.8.0)': + '@angular/build@19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(@angular/compiler@19.2.15)(@angular/platform-server@19.2.0(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/compiler@19.2.15)(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(@angular/platform-browser@19.2.15(@angular/animations@19.2.15(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1)))(@angular/common@19.2.15(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))(rxjs@7.8.1))(@angular/core@19.2.15(rxjs@7.8.1)(zone.js@0.15.1))))(@types/node@22.10.5)(chokidar@4.0.3)(jiti@2.6.1)(karma@6.4.4)(less@4.2.2)(postcss@8.5.2)(tailwindcss@3.4.14(ts-node@10.9.2(@types/node@22.10.5)(typescript@5.9.3)))(terser@5.39.0)(typescript@5.9.3)(yaml@2.8.0)': dependencies: '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.1902.17(chokidar@4.0.3) + '@angular-devkit/architect': 0.1902.18(chokidar@4.0.3) '@angular/compiler': 19.2.15 '@angular/compiler-cli': 19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3) '@babel/core': 7.26.10 @@ -14880,14 +14898,14 @@ snapshots: - chokidar - supports-color - '@angular/cli@19.2.17(@types/node@22.10.5)(chokidar@4.0.3)': + '@angular/cli@19.2.18(@types/node@22.10.5)(chokidar@4.0.3)': dependencies: - '@angular-devkit/architect': 0.1902.17(chokidar@4.0.3) - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + '@angular-devkit/architect': 0.1902.18(chokidar@4.0.3) + '@angular-devkit/core': 19.2.18(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.18(chokidar@4.0.3) '@inquirer/prompts': 7.3.2(@types/node@22.10.5) '@listr2/prompt-adapter-inquirer': 2.0.18(@inquirer/prompts@7.3.2(@types/node@22.10.5)) - '@schematics/angular': 19.2.17(chokidar@4.0.3) + '@schematics/angular': 19.2.18(chokidar@4.0.3) '@yarnpkg/lockfile': 1.1.0 ini: 5.0.0 jsonc-parser: 3.3.1 @@ -15176,6 +15194,16 @@ snapshots: typescript: 5.6.1-rc validate-npm-package-name: 5.0.1 + '@ark/regex@0.0.0': + dependencies: + '@ark/util': 0.50.0 + + '@ark/schema@0.50.0': + dependencies: + '@ark/util': 0.50.0 + + '@ark/util@0.50.0': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.27.1 @@ -17355,7 +17383,7 @@ snapshots: '@fontsource/fira-mono@5.0.0': {} - '@hey-api/json-schema-ref-parser@1.2.0': + '@hey-api/json-schema-ref-parser@1.2.1': dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 @@ -17839,7 +17867,7 @@ snapshots: dependencies: '@emnapi/core': 1.5.0 '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.0 + '@tybys/wasm-util': 0.10.1 optional: true '@napi-rs/wasm-runtime@1.0.7': @@ -17981,7 +18009,7 @@ snapshots: typescript: 5.8.3 webpack: 5.98.0(esbuild@0.25.0) - '@ngtools/webpack@19.2.17(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.98.0)': + '@ngtools/webpack@19.2.18(@angular/compiler-cli@19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3))(typescript@5.9.3)(webpack@5.98.0)': dependencies: '@angular/compiler-cli': 19.2.15(@angular/compiler@19.2.15)(typescript@5.9.3) typescript: 5.9.3 @@ -18062,32 +18090,32 @@ snapshots: '@nuxt/devalue@2.0.2': {} - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) transitivePeerDependencies: - magicast - supports-color - '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))': + '@nuxt/devtools-kit@1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))': dependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) '@nuxt/schema': 3.16.2 execa: 7.2.0 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - magicast - supports-color @@ -18152,13 +18180,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18187,9 +18215,9 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) - vite-plugin-vue-inspector: 5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18199,13 +18227,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18234,9 +18262,9 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18246,13 +18274,13 @@ snapshots: - utf-8-validate - vue - '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': + '@nuxt/devtools@1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@antfu/utils': 0.7.10 - '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + '@nuxt/devtools-kit': 1.7.0(magicast@0.3.5)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) '@nuxt/devtools-wizard': 1.7.0 '@nuxt/kit': 3.15.4(magicast@0.3.5) - '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) + '@vue/devtools-core': 7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@vue/devtools-kit': 7.6.8 birpc: 0.2.19 consola: 3.4.2 @@ -18281,9 +18309,9 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 unimport: 3.14.6(rollup@4.50.0) - vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) - vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) - vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite-plugin-inspect: 0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) which: 3.0.1 ws: 8.18.3 transitivePeerDependencies: @@ -18531,7 +18559,7 @@ snapshots: - typescript - yaml - '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': + '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': dependencies: '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@3.29.5) '@rollup/plugin-replace': 6.0.2(rollup@3.29.5) @@ -18557,7 +18585,7 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 1.3.1 postcss: 8.5.6 - rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.43)(rollup@3.29.5) + rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.44)(rollup@3.29.5) std-env: 3.9.0 strip-literal: 2.1.1 ufo: 1.6.1 @@ -18591,7 +18619,7 @@ snapshots: - vti - vue-tsc - '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': + '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': dependencies: '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@rollup/plugin-replace': 6.0.2(rollup@4.50.0) @@ -18617,7 +18645,7 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 1.3.1 postcss: 8.5.6 - rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.43)(rollup@4.50.0) + rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.44)(rollup@4.50.0) std-env: 3.9.0 strip-literal: 2.1.1 ufo: 1.6.1 @@ -18651,67 +18679,7 @@ snapshots: - vti - vue-tsc - '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': - dependencies: - '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) - '@rollup/plugin-replace': 6.0.2(rollup@4.50.0) - '@vitejs/plugin-vue': 5.2.1(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3)) - '@vitejs/plugin-vue-jsx': 4.1.1(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3)) - autoprefixer: 10.4.20(postcss@8.5.6) - clear: 0.1.0 - consola: 3.4.2 - cssnano: 7.1.1(postcss@8.5.6) - defu: 6.1.4 - esbuild: 0.24.2 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - externality: 1.0.2 - get-port-please: 3.2.0 - h3: 1.15.4 - jiti: 2.5.1 - knitwork: 1.2.0 - magic-string: 0.30.18 - mlly: 1.7.4 - ohash: 1.1.6 - pathe: 1.1.2 - perfect-debounce: 1.0.0 - pkg-types: 1.3.1 - postcss: 8.5.6 - rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.43)(rollup@4.50.0) - std-env: 3.9.0 - strip-literal: 2.1.1 - ufo: 1.6.1 - unenv: 1.10.0 - unplugin: 1.16.1 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) - vite-node: 2.1.9(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) - vite-plugin-checker: 0.8.0(eslint@9.17.0(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) - vue: 3.5.13(typescript@5.9.3) - vue-bundle-renderer: 2.1.2 - transitivePeerDependencies: - - '@biomejs/biome' - - '@types/node' - - eslint - - less - - lightningcss - - magicast - - meow - - optionator - - rolldown - - rollup - - sass - - sass-embedded - - stylelint - - stylus - - sugarss - - supports-color - - terser - - typescript - - vls - - vti - - vue-tsc - - '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': + '@nuxt/vite-builder@3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))': dependencies: '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@rollup/plugin-replace': 6.0.2(rollup@4.50.0) @@ -18737,7 +18705,7 @@ snapshots: perfect-debounce: 1.0.0 pkg-types: 1.3.1 postcss: 8.5.6 - rollup-plugin-visualizer: 5.14.0(rollup@4.50.0) + rollup-plugin-visualizer: 5.14.0(rolldown@1.0.0-beta.44)(rollup@4.50.0) std-env: 3.9.0 strip-literal: 2.1.1 ufo: 1.6.1 @@ -18773,7 +18741,7 @@ snapshots: '@one-ini/wasm@0.1.1': {} - '@oxc-project/types@0.94.0': {} + '@oxc-project/types@0.95.0': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -19584,53 +19552,53 @@ snapshots: '@types/react': 19.0.1 '@types/react-dom': 19.0.1 - '@rolldown/binding-android-arm64@1.0.0-beta.43': + '@rolldown/binding-android-arm64@1.0.0-beta.44': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.43': + '@rolldown/binding-darwin-arm64@1.0.0-beta.44': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.43': + '@rolldown/binding-darwin-x64@1.0.0-beta.44': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.43': + '@rolldown/binding-freebsd-x64@1.0.0-beta.44': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.43': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.44': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.43': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.44': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.43': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.44': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.43': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.44': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.43': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.44': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.43': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.44': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.43': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.44': dependencies: '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.43': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.44': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.43': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.44': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.43': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.44': optional: true '@rolldown/pluginutils@1.0.0-beta.29': {} - '@rolldown/pluginutils@1.0.0-beta.43': {} + '@rolldown/pluginutils@1.0.0-beta.44': {} '@rollup/plugin-alias@5.1.1(rollup@3.29.5)': optionalDependencies: @@ -19948,10 +19916,10 @@ snapshots: transitivePeerDependencies: - chokidar - '@schematics/angular@19.2.17(chokidar@4.0.3)': + '@schematics/angular@19.2.18(chokidar@4.0.3)': dependencies: - '@angular-devkit/core': 19.2.17(chokidar@4.0.3) - '@angular-devkit/schematics': 19.2.17(chokidar@4.0.3) + '@angular-devkit/core': 19.2.18(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.18(chokidar@4.0.3) jsonc-parser: 3.3.1 transitivePeerDependencies: - chokidar @@ -20181,11 +20149,6 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.5 - '@tybys/wasm-util@0.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -21096,38 +21059,38 @@ snapshots: dependencies: '@vue/devtools-kit': 8.0.2 - '@vue/devtools-core@7.6.8(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)) + vite-hot-client: 0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) vue: 3.5.13(typescript@5.9.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) + vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) vue: 3.5.13(typescript@5.9.3) transitivePeerDependencies: - vite - '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': + '@vue/devtools-core@7.6.8(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.7 '@vue/devtools-shared': 7.7.7 mitt: 3.0.1 nanoid: 5.1.5 pathe: 1.1.2 - vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)) + vite-hot-client: 0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) vue: 3.5.13(typescript@5.9.3) transitivePeerDependencies: - vite @@ -21579,6 +21542,12 @@ snapshots: aria-query@5.3.2: {} + arktype@2.1.23: + dependencies: + '@ark/regex': 0.0.0 + '@ark/schema': 0.50.0 + '@ark/util': 0.50.0 + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -21734,7 +21703,7 @@ snapshots: axios@1.8.2: dependencies: - follow-redirects: 1.15.11 + follow-redirects: 1.15.11(debug@4.4.1) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -23199,6 +23168,10 @@ snapshots: dependencies: eslint: 9.17.0(jiti@2.6.1) + eslint-config-prettier@9.1.2(eslint@9.17.0(jiti@2.6.1)): + dependencies: + eslint: 9.17.0(jiti@2.6.1) + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -23837,8 +23810,6 @@ snapshots: dependencies: tabbable: 6.2.0 - follow-redirects@1.15.11: {} - follow-redirects@1.15.11(debug@4.4.1): optionalDependencies: debug: 4.4.1 @@ -25735,108 +25706,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nitropack@2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13): - dependencies: - '@cloudflare/kv-asset-handler': 0.4.0 - '@netlify/functions': 3.1.10(encoding@0.1.13)(rollup@4.50.0) - '@rollup/plugin-alias': 5.1.1(rollup@4.50.0) - '@rollup/plugin-commonjs': 28.0.6(rollup@4.50.0) - '@rollup/plugin-inject': 5.0.5(rollup@4.50.0) - '@rollup/plugin-json': 6.1.0(rollup@4.50.0) - '@rollup/plugin-node-resolve': 16.0.1(rollup@4.50.0) - '@rollup/plugin-replace': 6.0.2(rollup@4.50.0) - '@rollup/plugin-terser': 0.4.4(rollup@4.50.0) - '@vercel/nft': 0.29.4(encoding@0.1.13)(rollup@4.50.0) - archiver: 7.0.1 - c12: 3.3.1(magicast@0.3.5) - chokidar: 4.0.3 - citty: 0.1.6 - compatx: 0.2.0 - confbox: 0.2.2 - consola: 3.4.2 - cookie-es: 2.0.0 - croner: 9.1.0 - crossws: 0.3.5 - db0: 0.3.2 - defu: 6.1.4 - destr: 2.0.5 - dot-prop: 9.0.0 - esbuild: 0.25.9 - escape-string-regexp: 5.0.0 - etag: 1.8.1 - exsolve: 1.0.7 - globby: 14.1.0 - gzip-size: 7.0.0 - h3: 1.15.4 - hookable: 5.5.3 - httpxy: 0.1.7 - ioredis: 5.7.0 - jiti: 2.5.1 - klona: 2.0.6 - knitwork: 1.2.0 - listhen: 1.9.0 - magic-string: 0.30.18 - magicast: 0.3.5 - mime: 4.0.7 - mlly: 1.7.4 - node-fetch-native: 1.6.7 - node-mock-http: 1.0.2 - ofetch: 1.4.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 1.0.0 - pkg-types: 2.3.0 - pretty-bytes: 6.1.1 - radix3: 1.1.2 - rollup: 4.50.0 - rollup-plugin-visualizer: 6.0.3(rollup@4.50.0) - scule: 1.3.0 - semver: 7.7.2 - serve-placeholder: 2.0.2 - serve-static: 2.2.0 - source-map: 0.7.6 - std-env: 3.9.0 - ufo: 1.6.1 - ultrahtml: 1.6.0 - uncrypto: 0.1.3 - unctx: 2.4.1 - unenv: 2.0.0-rc.19 - unimport: 5.2.0 - unplugin-utils: 0.2.5 - unstorage: 1.17.0(@netlify/blobs@9.1.2)(db0@0.3.2)(ioredis@5.7.0) - untyped: 2.0.0 - unwasm: 0.3.11 - youch: 4.1.0-beta.8 - youch-core: 0.3.3 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - mysql2 - - rolldown - - sqlite3 - - supports-color - - uploadthing - - nitropack@2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.43): + nitropack@2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.44): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@netlify/functions': 3.1.10(encoding@0.1.13)(rollup@4.50.0) @@ -25890,7 +25760,7 @@ snapshots: pretty-bytes: 6.1.1 radix3: 1.1.2 rollup: 4.50.0 - rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.43)(rollup@4.50.0) + rollup-plugin-visualizer: 6.0.3(rolldown@1.0.0-beta.44)(rollup@4.50.0) scule: 1.3.0 semver: 7.7.2 serve-placeholder: 2.0.2 @@ -26091,14 +25961,14 @@ snapshots: nuxi@3.28.0: {} - nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 1.7.0(rollup@3.29.5)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@3.29.5) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@3.29.5) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@3.29.5)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@unhead/dom': 1.11.20 '@unhead/shared': 1.11.20 '@unhead/ssr': 1.11.20 @@ -26128,7 +25998,7 @@ snapshots: magic-string: 0.30.18 mlly: 1.7.4 nanotar: 0.1.1 - nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.43) + nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.44) nuxi: 3.28.0 nypm: 0.3.12 ofetch: 1.4.1 @@ -26212,14 +26082,14 @@ snapshots: - vue-tsc - xml2js - nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): + nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.39.0)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@unhead/dom': 1.11.20 '@unhead/shared': 1.11.20 '@unhead/ssr': 1.11.20 @@ -26249,7 +26119,7 @@ snapshots: magic-string: 0.30.18 mlly: 1.7.4 nanotar: 0.1.1 - nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.43) + nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.44) nuxi: 3.28.0 nypm: 0.3.12 ofetch: 1.4.1 @@ -26333,14 +26203,14 @@ snapshots: - vue-tsc - xml2js - nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.43)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@unhead/dom': 1.11.20 '@unhead/shared': 1.11.20 '@unhead/ssr': 1.11.20 @@ -26370,7 +26240,7 @@ snapshots: magic-string: 0.30.18 mlly: 1.7.4 nanotar: 0.1.1 - nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.43) + nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.44) nuxi: 3.28.0 nypm: 0.3.12 ofetch: 1.4.1 @@ -26454,14 +26324,14 @@ snapshots: - vue-tsc - xml2js - nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): + nuxt@3.14.1592(@netlify/blobs@9.1.2)(@parcel/watcher@2.5.1)(@types/node@22.10.5)(db0@0.3.2)(encoding@0.1.13)(eslint@9.17.0(jiti@2.6.1))(ioredis@5.7.0)(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@nuxt/devalue': 2.0.2 - '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1))(vue@3.5.13(typescript@5.9.3)) + '@nuxt/devtools': 1.7.0(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0))(vue@3.5.13(typescript@5.9.3)) '@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.50.0) '@nuxt/telemetry': 2.6.6(magicast@0.3.5) - '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) + '@nuxt/vite-builder': 3.14.1592(@types/node@22.10.5)(eslint@9.17.0(jiti@2.6.1))(less@4.2.2)(magicast@0.3.5)(optionator@0.9.4)(rolldown@1.0.0-beta.44)(rollup@4.50.0)(sass@1.85.0)(terser@5.43.1)(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3)) '@unhead/dom': 1.11.20 '@unhead/shared': 1.11.20 '@unhead/ssr': 1.11.20 @@ -26491,7 +26361,7 @@ snapshots: magic-string: 0.30.18 mlly: 1.7.4 nanotar: 0.1.1 - nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13) + nitropack: 2.12.4(@netlify/blobs@9.1.2)(encoding@0.1.13)(rolldown@1.0.0-beta.44) nuxi: 3.28.0 nypm: 0.3.12 ofetch: 1.4.1 @@ -27719,7 +27589,7 @@ snapshots: dependencies: glob: 7.2.3 - rolldown-plugin-dts@0.16.12(rolldown@1.0.0-beta.43)(typescript@5.9.3): + rolldown-plugin-dts@0.16.12(rolldown@1.0.0-beta.44)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.4 @@ -27730,33 +27600,32 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.12.0 magic-string: 0.30.19 - rolldown: 1.0.0-beta.43 + rolldown: 1.0.0-beta.44 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: - oxc-resolver - supports-color - rolldown@1.0.0-beta.43: + rolldown@1.0.0-beta.44: dependencies: - '@oxc-project/types': 0.94.0 - '@rolldown/pluginutils': 1.0.0-beta.43 - ansis: 4.2.0 + '@oxc-project/types': 0.95.0 + '@rolldown/pluginutils': 1.0.0-beta.44 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.43 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.43 - '@rolldown/binding-darwin-x64': 1.0.0-beta.43 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.43 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.43 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.43 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.43 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.43 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.43 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.43 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.43 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.43 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.43 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.43 + '@rolldown/binding-android-arm64': 1.0.0-beta.44 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.44 + '@rolldown/binding-darwin-x64': 1.0.0-beta.44 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.44 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.44 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.44 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.44 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.44 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.44 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.44 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.44 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.44 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.44 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.44 rollup-plugin-dts@6.1.1(rollup@3.29.5)(typescript@5.9.3): dependencies: @@ -27774,52 +27643,34 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.27.1 - rollup-plugin-visualizer@5.14.0(rolldown@1.0.0-beta.43)(rollup@3.29.5): + rollup-plugin-visualizer@5.14.0(rolldown@1.0.0-beta.44)(rollup@3.29.5): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: - rolldown: 1.0.0-beta.43 + rolldown: 1.0.0-beta.44 rollup: 3.29.5 - rollup-plugin-visualizer@5.14.0(rolldown@1.0.0-beta.43)(rollup@4.50.0): - dependencies: - open: 8.4.2 - picomatch: 4.0.3 - source-map: 0.7.6 - yargs: 17.7.2 - optionalDependencies: - rolldown: 1.0.0-beta.43 - rollup: 4.50.0 - - rollup-plugin-visualizer@5.14.0(rollup@4.50.0): + rollup-plugin-visualizer@5.14.0(rolldown@1.0.0-beta.44)(rollup@4.50.0): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: + rolldown: 1.0.0-beta.44 rollup: 4.50.0 - rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.43)(rollup@4.50.0): - dependencies: - open: 8.4.2 - picomatch: 4.0.3 - source-map: 0.7.6 - yargs: 17.7.2 - optionalDependencies: - rolldown: 1.0.0-beta.43 - rollup: 4.50.0 - - rollup-plugin-visualizer@6.0.3(rollup@4.50.0): + rollup-plugin-visualizer@6.0.3(rolldown@1.0.0-beta.44)(rollup@4.50.0): dependencies: open: 8.4.2 picomatch: 4.0.3 source-map: 0.7.6 yargs: 17.7.2 optionalDependencies: + rolldown: 1.0.0-beta.44 rollup: 4.50.0 rollup@3.29.5: @@ -28961,8 +28812,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.43 - rolldown-plugin-dts: 0.16.12(rolldown@1.0.0-beta.43)(typescript@5.9.3) + rolldown: 1.0.0-beta.44 + rolldown-plugin-dts: 0.16.12(rolldown@1.0.0-beta.44)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.15 @@ -29572,10 +29423,6 @@ snapshots: vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) vite-hot-client: 2.1.0(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)) - vite-hot-client@0.2.4(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): - dependencies: - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) - vite-hot-client@0.2.4(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) @@ -29584,6 +29431,10 @@ snapshots: dependencies: vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite-hot-client@0.2.4(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + dependencies: + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite-hot-client@2.1.0(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) @@ -29728,7 +29579,7 @@ snapshots: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29739,14 +29590,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29757,14 +29608,14 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: - rollup - supports-color - vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): + vite-plugin-inspect@0.8.9(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.50.0)(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.2.0(rollup@4.50.0) @@ -29775,7 +29626,7 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.1 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) optionalDependencies: '@nuxt/kit': 3.15.4(magicast@0.3.5) transitivePeerDependencies: @@ -29812,7 +29663,7 @@ snapshots: - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29823,11 +29674,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 5.4.19(@types/node@22.10.5)(less@4.2.2)(sass@1.85.0)(terser@5.43.1) + vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(vite@7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29838,11 +29689,11 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color - vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0)): + vite-plugin-vue-inspector@5.3.2(vite@7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0)): dependencies: '@babel/core': 7.28.3 '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.3) @@ -29853,7 +29704,7 @@ snapshots: '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 - vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.0) + vite: 7.1.5(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.0) transitivePeerDependencies: - supports-color diff --git a/scripts/example.sh b/scripts/example.sh deleted file mode 100644 index 622dacd55..000000000 --- a/scripts/example.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -pnpm --filter @example/openapi-ts-$1 -- $2 diff --git a/scripts/examples-check.sh b/scripts/examples-check.sh new file mode 100755 index 000000000..30cb827e8 --- /dev/null +++ b/scripts/examples-check.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Check if generated client code for all examples is up-to-date +# This script is used in CI to ensure examples are kept in sync with the codebase + +set -e + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "Checking if generated code is up-to-date..." + +# Generate fresh code +"$SCRIPT_DIR/examples-generate.sh" + +# Check if there are any changes +if ! git diff --quiet; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "❌ ERROR: Generated code is out of sync!" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "The following files have changed:" + git diff --name-only + echo "" + echo "To fix this, run:" + echo " pnpm examples:generate" + echo "" + echo "Then commit the changes." + exit 1 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "✅ All generated code is up-to-date!" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/scripts/examples-generate.sh b/scripts/examples-generate.sh new file mode 100755 index 000000000..55cda530e --- /dev/null +++ b/scripts/examples-generate.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash + +# Generate client code for all examples that have openapi-ts script +# This script is used to ensure examples are up-to-date with the latest code + +set -e + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "⏳ Generating client code for all examples..." +# Find all examples with openapi-ts script and generate code in parallel +# Concurrency control: adjust this number depending on CI machine resources +CONCURRENCY=${CONCURRENCY:-4} +tmpdir=$(mktemp -d) +# Use a simple space-separated list of pids and per-pid files for metadata +PIDS="" + +wait_for_slot() { + # Wait until number of background jobs is less than CONCURRENCY + while [ "$(jobs -rp | wc -l)" -ge "$CONCURRENCY" ]; do + sleep 0.2 + done +} + +for dir in "$ROOT_DIR"/examples/*/; do + package_json="$dir/package.json" + if [ ! -f "$package_json" ]; then + continue + fi + + if ! grep -q "\"openapi-ts\":" "$package_json"; then + continue + fi + + example_name=$(basename "$dir") + echo "📦 Scheduling: $example_name" + + wait_for_slot + + log="$tmpdir/${example_name}.log" + ( + echo "Generating: $example_name" + set -e + cd "$dir" + echo "-> Running pnpm openapi-ts" + pnpm openapi-ts + + # Format generated files in this example only to keep the step fast + if command -v pnpm >/dev/null 2>&1 && pnpm -w -s --version >/dev/null 2>&1; then + pnpm -s exec prettier --write "src/**/*.{ts,tsx,js,jsx,json,md}" || true + pnpm -s exec eslint --fix "src/**/*.{ts,tsx,js,jsx,json,md}" || true + else + if [ -x "node_modules/.bin/prettier" ]; then + ./node_modules/.bin/prettier --write "src/**/*.{ts,tsx,js,jsx,json,md}" || true + fi + if [ -x "node_modules/.bin/eslint" ]; then + ./node_modules/.bin/eslint --fix "src/**/*.{ts,tsx,js,jsx,json,md}" || true + fi + fi + + echo "Completed: $example_name" + ) >"$log" 2>&1 & + + pid=$! + PIDS="$PIDS $pid" + printf '%s' "$example_name" >"$tmpdir/$pid.name" + printf '%s' "$log" >"$tmpdir/$pid.log" +done + +failed=0 +for pid in $PIDS; do + if wait "$pid"; then + name=$(cat "$tmpdir/$pid.name" 2>/dev/null || echo "$pid") + echo "✅ $name succeeded" + else + name=$(cat "$tmpdir/$pid.name" 2>/dev/null || echo "$pid") + log=$(cat "$tmpdir/$pid.log" 2>/dev/null || echo "(no log)") + echo "❌ $name failed — see log: $tmpdir/$pid.log" + failed=1 + fi +done + +if [ "$failed" -ne 0 ]; then + echo "One or more examples failed to generate. Logs are in: $tmpdir" + exit 1 +fi + +echo "✨ All examples generated successfully!" -- 2.51.2