fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add py-dsl

Lubos cd44523b a8d0bf79

+6866 -757
+5
.changeset/blue-loops-fix.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **config**: rename `exportFromIndex` option to `includeInEntry`
+9
.changeset/cold-ties-return.md
··· 1 + --- 2 + "@hey-api/openapi-ts": minor 3 + --- 4 + 5 + **BREAKING:** **symbol**: replace `exportFrom` array with `getExportFromFilePath()` function 6 + 7 + ### Updated Symbol interface 8 + 9 + The `exportFrom` property has been replaced with the `getExportFromFilePath()` function. This allows you to dynamically determine export paths based on symbol properties. This is a low-level feature, so you're most likely unaffected.
+5
.changeset/deep-buttons-repeat.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **config**: `includeInEntry` accepts function in addition to primitive value
+5
.changeset/forty-shoes-fetch.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **parser**: add `getExportFromFilePath()` hook
+5
.changeset/fresh-peaches-thank.md
··· 1 + --- 2 + "@hey-api/shared": patch 3 + --- 4 + 5 + **parser**: add `getExportFromFilePath()` hook
+5
.changeset/heavy-waves-kneel.md
··· 1 + --- 2 + "@hey-api/shared": patch 3 + --- 4 + 5 + **config**: `includeInEntry` accepts function in addition to primitive value
+9
.changeset/itchy-beds-start.md
··· 1 + --- 2 + "@hey-api/codegen-core": minor 3 + --- 4 + 5 + **BREAKING:** **symbol**: replace `exportFrom` array with `getExportFromFilePath()` function 6 + 7 + ### Updated Symbol interface 8 + 9 + The `exportFrom` property has been replaced with the `getExportFromFilePath()` function. This allows you to dynamically determine export paths based on symbol properties. This is a low-level feature, so you're most likely unaffected.
+5
.changeset/slick-olives-doubt.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **types**: improve `defineConfig()` types
+5
.changeset/swift-rooms-see.md
··· 1 + --- 2 + "@hey-api/openapi-ts": patch 3 + --- 4 + 5 + **config**: rename `output.indexFile` to `output.entryFile`
+1
.gitignore
··· 1 1 .DS_Store 2 2 .cache 3 + .mypy_cache 3 4 .idea 4 5 .tmp 5 6 junit.xml
+113
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + OpenAPI TypeScript is a CLI tool and library for generating TypeScript clients, SDKs, validators, and schemas from OpenAPI specifications. This is a monorepo built with pnpm workspaces, Turbo build orchestration, and TypeScript. 4 + 5 + ## Quick Reference 6 + 7 + ```bash 8 + pnpm install # Install dependencies 9 + pnpm build --filter="@hey-api/**" # Build packages only 10 + pnpm build # Build everything (packages + examples + docs) 11 + pnpm test # Run all tests 12 + pnpm typecheck # Type check all packages 13 + pnpm lint # Check formatting (oxfmt) + linting (eslint) 14 + pnpm lint:fix # Auto-fix formatting and linting 15 + pnpm format # Format with oxfmt 16 + ``` 17 + 18 + ### Shortcuts 19 + 20 + ```bash 21 + pnpm tt -- @hey-api/openapi-ts # Test specific package 22 + pnpm tw -- @hey-api/openapi-ts # Test watch specific package 23 + pnpm tu -- @hey-api/openapi-ts # Update test snapshots 24 + pnpm tb -- @hey-api/openapi-ts # Build specific package 25 + pnpm ty -- @hey-api/openapi-ts # Typecheck specific package 26 + ``` 27 + 28 + ### Development 29 + 30 + ```bash 31 + pnpm dev:ts # Watch mode for openapi-ts (runs from dev/) 32 + pnpm dev:py # Watch mode for openapi-python (runs from dev/) 33 + ``` 34 + 35 + ## Build Timing 36 + 37 + **Do not cancel build commands** - they take significant time: 38 + 39 + - `pnpm install`: ~1m 20s 40 + - `pnpm build --filter="@hey-api/**"`: ~2m 15s 41 + - `pnpm build` (full): ~5+ minutes 42 + - `pnpm test`: ~1m 5s 43 + - `pnpm typecheck`: ~1m 20s 44 + - `pnpm lint`: ~35s 45 + 46 + Set timeouts accordingly (180s+ for builds, 120s+ for tests/typecheck). 47 + 48 + ## Repository Structure 49 + 50 + ``` 51 + packages/ 52 + openapi-ts/ # Main CLI tool and library 53 + openapi-python/ # Python DSL generation 54 + codegen-core/ # Core code generation utilities 55 + shared/ # Cross-package utilities (migrating out) 56 + types/ # Shared type definitions 57 + custom-client/ # Custom HTTP client implementations 58 + nuxt/ # Nuxt.js integration 59 + vite-plugin/ # Vite plugin 60 + config-vite-base/ # Shared Vite base configuration 61 + openapi-ts-tests/ # Test utilities and snapshots 62 + examples/ # 16+ framework-specific examples 63 + docs/ # VitePress documentation site 64 + dev/ # Development environment (CLI testing configs) 65 + specs/ # OpenAPI test specifications 66 + scripts/ # Build and test scripts 67 + ``` 68 + 69 + ## Tooling 70 + 71 + - **Package manager**: pnpm 10.28.2 (strict engine, exact versions) 72 + - **Node**: >=20.19.0 (see .nvmrc for exact version) 73 + - **Build**: Turbo 2.8.0 + tsdown + Rollup 74 + - **Language**: TypeScript 5.9.3, ESM only 75 + - **Formatter**: oxfmt 0.27.0 (single quotes via .oxfmtrc.json) 76 + - **Linter**: ESLint 9 flat config with typescript-eslint, simple-import-sort, sort-destructure-keys, typescript-sort-keys 77 + - **Tests**: Vitest 3.2.4 78 + - **Pre-commit**: Husky + lint-staged (runs `pnpm format` + `pnpm lint:fix`) 79 + - **Python** (for openapi-python): Python >=3.10, mypy, ruff, line length 120 80 + - **Releases**: Changesets 81 + 82 + ## Code Conventions 83 + 84 + - ESM modules only (`.mts`/`.mjs` extensions in builds) 85 + - UTF-8, LF line endings, 2-space indentation 86 + - Single quotes (enforced by oxfmt) 87 + - Imports sorted by eslint-plugin-simple-import-sort 88 + - Object/interface keys sorted alphabetically 89 + - Destructured keys sorted alphabetically 90 + 91 + ## Pre-commit Checklist 92 + 93 + Run before committing (Husky runs format + lint automatically, but also verify): 94 + 95 + ```bash 96 + pnpm lint:fix # Auto-fix formatting and linting 97 + pnpm typecheck # Type check 98 + pnpm test # Run tests 99 + ``` 100 + 101 + Some linting warnings in `.gen/snapshots/` directories are expected for generated code. 102 + 103 + ## Git Conventions 104 + 105 + - **Branch naming**: `feat/`, `fix/`, `chore/`, `refactor/`, `docs/` prefixes 106 + - **Commit messages**: Conventional Commits (`feat:`, `fix:`, `chore:`, `refactor:`, `ci:`, `docs:`) 107 + - **Releases**: Changesets-based, auto-publish on merge to main 108 + 109 + ## Known Issues 110 + 111 + - Docs build may fail due to pnpm version mismatch in VitePress - use `--filter="@hey-api/**"` to skip 112 + - Some tests may fail in sandboxed environments due to network restrictions (OpenAPI spec downloads) 113 + - Generated test files in `packages/openapi-ts-tests/` may have expected linting warnings
+10 -1
dev/python/presets.ts
··· 3 3 export const presets = { 4 4 sdk: () => [ 5 5 /** SDK */ 6 - sdk(), 6 + sdk({ 7 + '~hooks': { 8 + symbols: { 9 + // getExportFromFilePath(symbol) { 10 + // console.log(symbol.toString()); 11 + // return undefined; 12 + // }, 13 + }, 14 + }, 15 + }), 7 16 ], 8 17 } as const; 9 18
+6 -1
dev/typescript/presets.ts
··· 16 16 sdk: () => [ 17 17 /** SDK with types */ 18 18 typescript(), 19 - sdk(), 19 + sdk({ 20 + operations: { 21 + containerName: 'OpenCode', 22 + strategy: 'single', 23 + }, 24 + }), 20 25 ], 21 26 tanstack: () => [ 22 27 /** SDK + TanStack Query */
+1 -1
docs/CHANGELOG.md
··· 1 - # @docs/openapi-ts 1 + # docs 2 2 3 3 ## 0.10.4 4 4
+6
docs/openapi-ts/migrating.md
··· 7 7 8 8 While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features. This page lists changes that require updates to your code. If you run into a problem with migration, please [open an issue](https://github.com/hey-api/openapi-ts/issues). 9 9 10 + ## v0.92.0 11 + 12 + ### Updated Symbol interface 13 + 14 + The `exportFrom` property has been replaced with the `getExportFromFilePath()` function. This allows you to dynamically determine export paths based on symbol properties. This is a low-level feature, so you're most likely unaffected. 15 + 10 16 ## v0.91.0 11 17 12 18 ### Removed CommonJS (CJS) support
+46 -8
docs/openapi-ts/output.md
··· 58 58 59 59 You can learn more on the [SDK](/openapi-ts/plugins/sdk) page. 60 60 61 - ## Barrel File 61 + ## Entry File 62 62 63 63 `index.ts` is not generated by any specific plugin. It's meant for convenience and by default, it re-exports every artifact generated by default plugins (TypeScript and SDK). 64 64 ··· 71 71 72 72 ::: 73 73 74 - ### Disable index file 74 + ### Disable entry file 75 75 76 76 We recommend importing artifacts from their respective files to avoid ambiguity, but we leave this choice up to you. 77 77 ··· 81 81 import type { Pet } from './client/types.gen'; 82 82 ``` 83 83 84 - If you're not importing artifacts from the index file, you can skip generating it altogether by setting the `output.indexFile` option to `false`. 84 + If you're not importing artifacts from the entry file, you can skip generating it altogether by setting the `output.entryFile` option to `false`. 85 85 86 86 ```js 87 87 export default { 88 88 input: 'hey-api/backend', // sign up at app.heyapi.dev 89 89 output: { 90 - indexFile: false, // [!code ++] 90 + entryFile: false, // [!code ++] 91 91 path: 'src/client', 92 92 }, 93 93 }; 94 94 ``` 95 95 96 - ### Re-export more files 96 + ### Re-export artifacts 97 + 98 + You can choose which artifacts should be re-exported from the entry file using the `includeInEntry` option on any plugin. For example, we can re-export all [Zod](/openapi-ts/plugins/zod) plugin artifacts by setting `includeInEntry` to `true`: 99 + 100 + ::: code-group 97 101 98 - You can choose which files should be re-exported by setting the `exportFromIndex` option to `true` on any plugin. For example, here's how you would re-export [Zod](/openapi-ts/plugins/zod) plugin exports. 102 + ```ts [example] 103 + export { 104 + zAddPetData, 105 + zAddPetResponse, 106 + zApiResponse, 107 + zCategory, 108 + // ...more exports 109 + } from './zod.gen'; 110 + ``` 99 111 100 - ```js 112 + ```js [config] 101 113 export default { 102 114 input: 'hey-api/backend', // sign up at app.heyapi.dev 103 115 output: 'src/client', 104 116 plugins: [ 105 117 // ...other plugins 106 118 { 107 - exportFromIndex: true, // [!code ++] 119 + includeInEntry: true, // [!code ++] 108 120 name: 'zod', 109 121 }, 110 122 ], 111 123 }; 112 124 ``` 125 + 126 + ::: 127 + 128 + Or we can re-export only specific artifacts by providing a predicate function: 129 + 130 + ::: code-group 131 + 132 + ```ts [example] 133 + export { zTag } from './zod.gen'; 134 + ``` 135 + 136 + ```js [config] 137 + export default { 138 + input: 'hey-api/backend', // sign up at app.heyapi.dev 139 + output: 'src/client', 140 + plugins: [ 141 + // ...other plugins 142 + { 143 + includeInEntry: (symbol) => symbol.name === 'zTag', // [!code ++] 144 + name: 'zod', 145 + }, 146 + ], 147 + }; 148 + ``` 149 + 150 + ::: 113 151 114 152 <!--@include: ../partials/examples.md--> 115 153 <!--@include: ../partials/sponsors.md-->
+1 -1
docs/openapi-ts/plugins/custom.md
··· 92 92 93 93 In the file above, we define a `my-plugin` plugin which will generate a `my-plugin.gen.ts` file. We also demonstrate declaring `@hey-api/typescript` as a dependency for our plugin, so we can safely import artifacts from `types.gen.ts`. 94 94 95 - By default, your plugin output won't be re-exported from the `index.ts` file. To enable this feature, add `exportFromIndex: true` to your `config.ts` file. 95 + By default, your plugin output won't be re-exported from the `index.ts` file. To enable this feature, add `includeInEntry: true` to your `config.ts` file. 96 96 97 97 ::: warning 98 98 Re-exporting your plugin from index file may result in broken output due to naming conflicts. Ensure your exported identifiers are unique.
+1 -1
docs/package.json
··· 1 1 { 2 - "name": "@docs/openapi-ts", 2 + "name": "docs", 3 3 "version": "0.10.4", 4 4 "private": true, 5 5 "description": "Documentation for OpenAPI TypeScript.",
+4 -1
examples/openapi-ts-angular-common/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'], ··· 13 16 throwOnError: true, 14 17 }, 15 18 { 16 - exportFromIndex: true, 17 19 httpRequests: true, 18 20 httpResources: { 19 21 containerName: '{{name}}ServiceResources', 20 22 strategy: 'byTags', 21 23 }, 24 + includeInEntry: true, 22 25 name: '@angular/common', 23 26 }, 24 27 '@hey-api/schemas',
+3
examples/openapi-ts-angular/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-axios/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-fastify/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://gist.githubusercontent.com/seriousme/55bd4c8ba2e598e416bb5543dcd362dc/raw/cf0b86ba37bb54bf1a6bf047c0ecf2a0ce4c62e0/petstore-v3.1.json', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-fetch/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-ky/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-next/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-ofetch/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-openai/openapi-ts.config.ts
··· 4 4 5 5 export default defineConfig({ 6 6 input: path.resolve('..', '..', 'specs', '3.1.x', 'openai.yaml'), 7 + logs: { 8 + path: './logs', 9 + }, 7 10 output: { 8 11 path: './src/client', 9 12 postProcess: ['oxfmt'],
+4 -1
examples/openapi-ts-pinia-colada/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'], ··· 16 19 name: '@hey-api/typescript', 17 20 }, 18 21 { 19 - exportFromIndex: true, 22 + includeInEntry: true, 20 23 name: '@pinia/colada', 21 24 queryKeys: false, 22 25 },
+3
examples/openapi-ts-tanstack-angular-query-experimental/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-tanstack-react-query/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-tanstack-svelte-query/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+3
examples/openapi-ts-tanstack-vue-query/openapi-ts.config.ts
··· 3 3 export default defineConfig({ 4 4 input: 5 5 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 6 + logs: { 7 + path: './logs', 8 + }, 6 9 output: { 7 10 path: './src/client', 8 11 postProcess: ['oxfmt', 'eslint'],
+1
package.json
··· 38 38 "test:watch": "turbo run test:watch", 39 39 "test": "turbo run test", 40 40 "typecheck": "turbo run typecheck", 41 + "td": "turbo run dev --filter", 41 42 "tt": "turbo run test --filter", 42 43 "tw": "turbo run test:watch --filter", 43 44 "tu": "turbo run test:update --filter",
+2
packages/codegen-core/src/__tests__/exports.test.ts
··· 2 2 3 3 const constExports = [ 4 4 'defaultExtensions', 5 + 'defaultModuleEntryNames', 5 6 'defaultNameConflictResolvers', 6 7 'File', 7 8 'fromRef', ··· 41 42 index.ImportModule, 42 43 index.IProject, 43 44 index.Language, 45 + index.ModuleEntryNames, 44 46 index.NameConflictResolver, 45 47 index.NameConflictResolvers, 46 48 index.Node,
+7 -1
packages/codegen-core/src/index.ts
··· 11 11 export type { IFileIn as FileIn } from './files/types'; 12 12 export { isNode, isNodeRef, isSymbol, isSymbolRef } from './guards'; 13 13 export { defaultExtensions } from './languages/extensions'; 14 + export { defaultModuleEntryNames } from './languages/modules'; 14 15 export { defaultNameConflictResolvers } from './languages/resolvers'; 15 - export type { Extensions, Language, NameConflictResolvers } from './languages/types'; 16 + export type { 17 + Extensions, 18 + Language, 19 + ModuleEntryNames, 20 + NameConflictResolvers, 21 + } from './languages/types'; 16 22 export { log } from './log'; 17 23 export { Logger } from './logger'; 18 24 export type {
+7
packages/codegen-core/src/languages/modules.ts
··· 1 + import type { ModuleEntryNames } from './types'; 2 + 3 + export const defaultModuleEntryNames: ModuleEntryNames = { 4 + javascript: 'index', 5 + python: '__init__', 6 + typescript: 'index', 7 + };
+24
packages/codegen-core/src/languages/types.ts
··· 2 2 3 3 import type { NameConflictResolver } from '../planner/types'; 4 4 5 + /** 6 + * Map of extensions for each language. 7 + * 8 + * @example 9 + * ```ts 10 + * const exts: Extensions = { 11 + * typescript: ['.ts', '.tsx'], 12 + * python: ['.py'], 13 + * }; 14 + * ``` 15 + */ 5 16 export type Extensions = Partial<Record<Language, ReadonlyArray<string>>>; 6 17 7 18 export type Language = ··· 33 44 | 'typescript' 34 45 | 'yaml' 35 46 | AnyString; // other/custom language 47 + 48 + /** 49 + * Map of module entry names for each language. 50 + * 51 + * @example 52 + * ```ts 53 + * const entries: ModuleEntryNames = { 54 + * typescript: 'index', 55 + * python: '__init__', 56 + * }; 57 + * ``` 58 + */ 59 + export type ModuleEntryNames = Partial<Record<Language, string>>; 36 60 37 61 export type NameConflictResolvers = Partial<Record<Language, NameConflictResolver>>;
+4 -4
packages/codegen-core/src/planner/planner.ts
··· 54 54 }); 55 55 file.addNode(node); 56 56 symbol.setFile(file); 57 - for (const exportFrom of symbol.exportFrom) { 57 + for (const logicalFilePath of symbol.getExportFromFilePath?.(symbol) ?? []) { 58 58 this.project.files.register({ 59 59 external: false, 60 60 language: file.language, 61 - logicalFilePath: exportFrom, 61 + logicalFilePath, 62 62 }); 63 63 } 64 64 ctx.walkScopes((dependency) => { ··· 151 151 const file = node.file; 152 152 if (!file) return; 153 153 154 - for (const exportFrom of symbol.exportFrom) { 154 + for (const logicalFilePath of symbol.getExportFromFilePath?.(symbol) ?? []) { 155 155 const target = this.project.files.register({ 156 156 external: false, 157 157 language: node.language, 158 - logicalFilePath: exportFrom, 158 + logicalFilePath, 159 159 }); 160 160 if (target.id === file.id) continue; 161 161
+8 -1
packages/codegen-core/src/project/project.ts
··· 3 3 import type { IProjectRenderMeta } from '../extensions'; 4 4 import { FileRegistry } from '../files/registry'; 5 5 import { defaultExtensions } from '../languages/extensions'; 6 + import { defaultModuleEntryNames } from '../languages/modules'; 6 7 import { defaultNameConflictResolvers } from '../languages/resolvers'; 7 - import type { Extensions, NameConflictResolvers } from '../languages/types'; 8 + import type { Extensions, ModuleEntryNames, NameConflictResolvers } from '../languages/types'; 8 9 import { NodeRegistry } from '../nodes/registry'; 9 10 import type { IOutput } from '../output'; 10 11 import { Planner } from '../planner/planner'; ··· 25 26 readonly defaultNameConflictResolver: NameConflictResolver; 26 27 readonly extensions: Extensions; 27 28 readonly fileName?: (name: string) => string; 29 + readonly moduleEntryNames: ModuleEntryNames; 28 30 readonly nameConflictResolvers: NameConflictResolvers; 29 31 readonly renderers: ReadonlyArray<Renderer>; 30 32 readonly root: string; ··· 36 38 | 'defaultNameConflictResolver' 37 39 | 'extensions' 38 40 | 'fileName' 41 + | 'moduleEntryNames' 39 42 | 'nameConflictResolvers' 40 43 | 'renderers' 41 44 > & ··· 51 54 }; 52 55 this.fileName = typeof fileName === 'string' ? () => fileName : fileName; 53 56 this.files = new FileRegistry(this); 57 + this.moduleEntryNames = { 58 + ...defaultModuleEntryNames, 59 + ...args.moduleEntryNames, 60 + }; 54 61 this.nameConflictResolvers = { 55 62 ...defaultNameConflictResolvers, 56 63 ...args.nameConflictResolvers,
+35 -3
packages/codegen-core/src/project/types.ts
··· 1 1 import type { IProjectRenderMeta } from '../extensions'; 2 2 import type { IFileRegistry } from '../files/types'; 3 - import type { Extensions, NameConflictResolvers } from '../languages/types'; 3 + import type { Extensions, ModuleEntryNames, NameConflictResolvers } from '../languages/types'; 4 4 import type { INodeRegistry } from '../nodes/types'; 5 5 import type { IOutput } from '../output'; 6 6 import type { NameConflictResolver } from '../planner/types'; ··· 21 21 readonly defaultFileName: string; 22 22 /** Default name conflict resolver used when a file has no specific resolver. */ 23 23 readonly defaultNameConflictResolver: NameConflictResolver; 24 - /** Maps language to array of extensions. First element is used by default. */ 24 + /** 25 + * Maps language to array of extensions. First element is used by default. 26 + * 27 + * @example 28 + * ```ts 29 + * const exts: Extensions = { 30 + * typescript: ['.ts', '.tsx'], 31 + * python: ['.py'], 32 + * }; 33 + * ``` 34 + */ 25 35 readonly extensions: Extensions; 26 36 /** 27 37 * Function to transform file names before they are used. ··· 32 42 readonly fileName?: (name: string) => string; 33 43 /** Centralized file registry for the project. */ 34 44 readonly files: IFileRegistry; 35 - /** Map of language-specific name conflict resolvers for files in the project. */ 45 + /** 46 + * Map of module entry names for each language. 47 + * 48 + * @example 49 + * ```ts 50 + * const entries: ModuleEntryNames = { 51 + * typescript: 'index', 52 + * python: '__init__', 53 + * }; 54 + * ``` 55 + */ 56 + readonly moduleEntryNames: ModuleEntryNames; 57 + /** 58 + * Map of language-specific name conflict resolvers for files in the project. 59 + * 60 + * @example 61 + * ```ts 62 + * const resolvers: NameConflictResolvers = { 63 + * typescript: myTypeScriptResolver, 64 + * python: myPythonResolver, 65 + * }; 66 + * ``` 67 + */ 36 68 readonly nameConflictResolvers: NameConflictResolvers; 37 69 /** Centralized node registry for the project. */ 38 70 readonly nodes: INodeRegistry;
+14 -24
packages/codegen-core/src/symbols/symbol.ts
··· 21 21 */ 22 22 private _exported: boolean; 23 23 /** 24 - * Names of files (without extension) from which this symbol is re-exported. 25 - * 26 - * @default [] 27 - */ 28 - private _exportFrom: ReadonlyArray<string>; 29 - /** 30 24 * External module name if this symbol is imported from a module not managed 31 25 * by the project (e.g. "zod", "lodash"). 32 26 * ··· 44 38 */ 45 39 private _finalName?: string; 46 40 /** 41 + * Custom strategy to determine from which file path(s) this symbol is re-exported. 42 + * 43 + * @returns The file path(s) that re-export this symbol, or undefined if none. 44 + */ 45 + private _getExportFromFilePath?: (symbol: Symbol) => ReadonlyArray<string> | undefined; 46 + /** 47 47 * Custom strategy to determine file output path. 48 48 * 49 49 * @returns The file path to output the symbol to, or undefined to fallback to default behavior. ··· 85 85 86 86 constructor(input: ISymbolIn, id: number) { 87 87 this._exported = input.exported ?? false; 88 - this._exportFrom = input.exportFrom ?? []; 89 88 this._external = input.external; 89 + this._getExportFromFilePath = input.getExportFromFilePath; 90 90 this._getFilePath = input.getFilePath; 91 91 this.id = id; 92 92 this._importKind = input.importKind ?? 'named'; ··· 114 114 } 115 115 116 116 /** 117 - * Names of files (without extension) that re-export this symbol. 118 - */ 119 - get exportFrom(): ReadonlyArray<string> { 120 - return this.canonical._exportFrom; 121 - } 122 - 123 - /** 124 117 * External module from which this symbol originates, if any. 125 118 */ 126 119 get external(): string | undefined { ··· 146 139 throw new Error(message); 147 140 } 148 141 return this.canonical._finalName; 142 + } 143 + 144 + /** 145 + * Custom re-export file path resolver, if provided. 146 + */ 147 + get getExportFromFilePath(): ((symbol: Symbol) => ReadonlyArray<string> | undefined) | undefined { 148 + return this.canonical._getExportFromFilePath; 149 149 } 150 150 151 151 /** ··· 217 217 setExported(exported: boolean): void { 218 218 this.assertCanonical(); 219 219 this._exported = exported; 220 - } 221 - 222 - /** 223 - * Records file names that re‑export this symbol. 224 - * 225 - * @param list — Source files re‑exporting this symbol. 226 - */ 227 - setExportFrom(list: ReadonlyArray<string>): void { 228 - this.assertCanonical(); 229 - this._exportFrom = list; 230 220 } 231 221 232 222 /**
+6 -6
packages/codegen-core/src/symbols/types.ts
··· 9 9 10 10 export type ISymbolIn = { 11 11 /** 12 - * Array of file names (without extensions) from which this symbol is re-exported. 13 - * 14 - * @default undefined 15 - */ 16 - exportFrom?: ReadonlyArray<string>; 17 - /** 18 12 * Whether this symbol is exported from its own file. 19 13 * 20 14 * @default false ··· 27 21 * @default undefined 28 22 */ 29 23 external?: string; 24 + /** 25 + * Optional output strategy to override default behavior. 26 + * 27 + * @returns The file path(s) that re-export this symbol, or undefined if none. 28 + */ 29 + getExportFromFilePath?: Symbol['getExportFromFilePath']; 30 30 /** 31 31 * Optional output strategy to override default behavior. 32 32 *
+3 -3
packages/openapi-python/src/config/output/config.ts
··· 18 18 const output = valueToObject({ 19 19 defaultValue: { 20 20 clean: true, 21 + entryFile: true, 21 22 fileName: { 22 23 case: 'preserve', 23 24 name: '{{name}}', 24 - suffix: '.gen', 25 + suffix: '_gen', 25 26 }, 26 - header: '// This file is auto-generated by @hey-api/openapi-python', 27 - indexFile: true, 27 + header: '# This file is auto-generated by @hey-api/openapi-python', 28 28 path: '', 29 29 postProcess: [], 30 30 preferExportAll: false,
+12
packages/openapi-python/src/config/utils.ts
··· 1 + import type { Context, PluginInstance } from '@hey-api/shared'; 2 + 3 + import type { Config } from './types'; 4 + 5 + export function getTypedConfig( 6 + plugin: Pick<PluginInstance, 'context'> | Pick<Context, 'config'>, 7 + ): Config { 8 + if ('context' in plugin) { 9 + return plugin.context.config as Config; 10 + } 11 + return plugin.config as Config; 12 + }
+9 -10
packages/openapi-python/src/createClient.ts
··· 21 21 import { postProcessors } from './config/output/postprocess'; 22 22 import type { Config } from './config/types'; 23 23 import { generateOutput } from './generate/output'; 24 - // import { TypeScriptRenderer } from './py-dsl'; 24 + import { PythonRenderer } from './py-dsl'; 25 25 26 26 export async function createClient({ 27 27 config, ··· 106 106 const eventParser = logger.timeEvent('parser'); 107 107 // TODO: allow overriding via config 108 108 const project = new Project({ 109 - defaultFileName: 'index', 109 + defaultFileName: '__init__', 110 110 fileName: (base) => { 111 111 const name = applyNaming(base, config.output.fileName); 112 112 const { suffix } = config.output.fileName; 113 113 if (!suffix) { 114 114 return name; 115 115 } 116 - return name === 'index' || name.endsWith(suffix) ? name : `${name}${suffix}`; 116 + return name === '__init__' || name.endsWith(suffix) ? name : `${name}${suffix}`; 117 117 }, 118 118 nameConflictResolvers: config.output.nameConflictResolver 119 119 ? { ··· 121 121 } 122 122 : undefined, 123 123 renderers: [ 124 - // TODO: attach renderer 125 - // new TypeScriptRenderer({ 126 - // header: config.output.header, 127 - // preferExportAll: config.output.preferExportAll, 128 - // preferFileExtension: config.output.importFileExtension || undefined, 129 - // resolveModuleName: config.output.resolveModuleName, 130 - // }), 124 + new PythonRenderer({ 125 + header: config.output.header, 126 + preferExportAll: config.output.preferExportAll, 127 + preferFileExtension: config.output.importFileExtension || undefined, 128 + resolveModuleName: config.output.resolveModuleName, 129 + }), 131 130 ], 132 131 root: config.output.path, 133 132 });
+131
packages/openapi-python/src/generate/__tests__/client.test.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { describe, expect, it } from 'vitest'; 4 + 5 + describe('isDevMode logic', () => { 6 + const scenarios: ReadonlyArray<{ 7 + description: string; 8 + expected: boolean; 9 + mockPath: string; 10 + }> = [ 11 + { 12 + description: 'returns true in dev mode (src/generate)', 13 + expected: true, 14 + mockPath: ['', 'home', 'user', 'packages', 'openapi-python', 'src', 'generate'].join( 15 + path.sep, 16 + ), 17 + }, 18 + { 19 + description: 'returns false in prod mode (dist/generate)', 20 + expected: false, 21 + mockPath: ['', 'home', 'user', 'packages', 'openapi-python', 'dist', 'generate'].join( 22 + path.sep, 23 + ), 24 + }, 25 + { 26 + description: 'returns false when path contains /src/ but not in correct position', 27 + expected: false, 28 + mockPath: [ 29 + '', 30 + 'home', 31 + 'user', 32 + 'src', 33 + 'project', 34 + 'node_modules', 35 + '@hey-api', 36 + 'openapi-python', 37 + 'dist', 38 + 'generate', 39 + ].join(path.sep), 40 + }, 41 + { 42 + description: 'returns false when src is in project path (pnpm case from issue)', 43 + expected: false, 44 + mockPath: [ 45 + '', 46 + 'home', 47 + 'user', 48 + 'src', 49 + 'thcdb', 50 + 'worktree', 51 + 'schema-gen', 52 + 'web', 53 + 'node_modules', 54 + '.pnpm', 55 + '@hey-api+openapi-python@0.91.1', 56 + 'node_modules', 57 + '@hey-api', 58 + 'openapi-python', 59 + 'dist', 60 + 'generate', 61 + ].join(path.sep), 62 + }, 63 + { 64 + description: 65 + 'returns false when src is in project path with plugins path (exact error from issue)', 66 + expected: false, 67 + mockPath: [ 68 + '', 69 + 'home', 70 + 'user', 71 + 'src', 72 + 'thcdb', 73 + 'worktree', 74 + 'schema-gen', 75 + 'web', 76 + 'node_modules', 77 + '.pnpm', 78 + '@hey-api+openapi-python@0.91.1_magicast@0.5.1_typescript@5.9.3', 79 + 'node_modules', 80 + '@hey-api', 81 + 'openapi-python', 82 + 'plugins', 83 + '@hey-api', 84 + 'client-core', 85 + 'bundle', 86 + ].join(path.sep), 87 + }, 88 + { 89 + description: 'returns true only when ending with src/generate', 90 + expected: true, 91 + mockPath: [ 92 + '', 93 + 'home', 94 + 'user', 95 + 'src', 96 + 'backup', 97 + 'packages', 98 + 'openapi-python', 99 + 'src', 100 + 'generate', 101 + ].join(path.sep), 102 + }, 103 + { 104 + description: 'returns false when not ending with generate', 105 + expected: false, 106 + mockPath: ['', 'home', 'user', 'packages', 'openapi-python', 'src', 'plugins'].join(path.sep), 107 + }, 108 + { 109 + description: 'returns false when src exists but dist is later', 110 + expected: false, 111 + mockPath: ['', 'home', 'user', 'src', 'project', 'openapi-python', 'dist', 'generate'].join( 112 + path.sep, 113 + ), 114 + }, 115 + ]; 116 + 117 + it.each(scenarios)('$description', ({ expected, mockPath }) => { 118 + // Test the isDevMode logic 119 + const normalized = mockPath.split(path.sep); 120 + const srcIndex = normalized.lastIndexOf('src'); 121 + const distIndex = normalized.lastIndexOf('dist'); 122 + 123 + const result = 124 + srcIndex !== -1 && 125 + srcIndex > distIndex && 126 + srcIndex === normalized.length - 2 && 127 + normalized[srcIndex + 1] === 'generate'; 128 + 129 + expect(result).toBe(expected); 130 + }); 131 + });
+259
packages/openapi-python/src/generate/client.ts
··· 1 + import fs from 'node:fs'; 2 + import path from 'node:path'; 3 + import { fileURLToPath } from 'node:url'; 4 + 5 + import type { IProject, ProjectRenderMeta } from '@hey-api/codegen-core'; 6 + import type { DefinePlugin } from '@hey-api/shared'; 7 + import { ensureDirSync } from '@hey-api/shared'; 8 + 9 + import type { Config } from '../config/types'; 10 + import type { Client } from '../plugins/@hey-api/client-core/types'; 11 + import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; 12 + 13 + const __filename = fileURLToPath(import.meta.url); 14 + const __dirname = path.dirname(__filename); 15 + 16 + /** 17 + * Dev mode: 'src' appears after 'dist' (or dist doesn't exist), and 'generate' follows 'src' 18 + */ 19 + function isDevMode(): boolean { 20 + const normalized = __dirname.split(path.sep); 21 + const srcIndex = normalized.lastIndexOf('src'); 22 + const distIndex = normalized.lastIndexOf('dist'); 23 + return ( 24 + srcIndex !== -1 && 25 + srcIndex > distIndex && 26 + srcIndex === normalized.length - 2 && 27 + normalized[srcIndex + 1] === 'generate' 28 + ); 29 + } 30 + 31 + /** 32 + * Returns paths to client bundle files based on execution context 33 + */ 34 + function getClientBundlePaths(pluginName: string): { 35 + clientPath: string; 36 + corePath: string; 37 + } { 38 + const clientName = pluginName.slice('@hey-api/client-'.length); 39 + 40 + if (isDevMode()) { 41 + // Dev: source bundle folders at src/plugins/@hey-api/{client}/bundle 42 + const pluginsDir = path.resolve(__dirname, '..', 'plugins', '@hey-api'); 43 + return { 44 + clientPath: path.resolve(pluginsDir, `client-${clientName}`, 'bundle'), 45 + corePath: path.resolve(pluginsDir, 'client-core', 'bundle'), 46 + }; 47 + } 48 + 49 + // Prod: copied to dist/clients/{clientName} 50 + return { 51 + clientPath: path.resolve(__dirname, 'clients', clientName), 52 + corePath: path.resolve(__dirname, 'clients', 'core'), 53 + }; 54 + } 55 + 56 + /** 57 + * Returns absolute path to the client folder. This is hard-coded for now. 58 + */ 59 + export function clientFolderAbsolutePath(config: Config): string { 60 + const client = getClientPlugin(config); 61 + 62 + if ('bundle' in client.config && client.config.bundle) { 63 + // not proud of this one 64 + const renamed: Map<string, string> | undefined = 65 + // @ts-expect-error 66 + config._FRAGILE_CLIENT_BUNDLE_RENAMED; 67 + return path.resolve(config.output.path, 'client', `${renamed?.get('index') ?? 'index'}.py`); 68 + } 69 + 70 + return client.name; 71 + } 72 + 73 + /** 74 + * Recursively copies files and directories. 75 + * This is a PnP-compatible alternative to fs.cpSync that works with Yarn PnP's 76 + * virtualized filesystem. 77 + */ 78 + function copyRecursivePnP(src: string, dest: string): void { 79 + const stat = fs.statSync(src); 80 + 81 + if (stat.isDirectory()) { 82 + if (!fs.existsSync(dest)) { 83 + fs.mkdirSync(dest, { recursive: true }); 84 + } 85 + 86 + const files = fs.readdirSync(src); 87 + for (const file of files) { 88 + copyRecursivePnP(path.join(src, file), path.join(dest, file)); 89 + } 90 + } else { 91 + const content = fs.readFileSync(src); 92 + fs.writeFileSync(dest, content); 93 + } 94 + } 95 + 96 + function renameFile({ 97 + filePath, 98 + project, 99 + renamed, 100 + }: { 101 + filePath: string; 102 + project: IProject; 103 + renamed: Map<string, string>; 104 + }): void { 105 + const extension = path.extname(filePath); 106 + const name = path.basename(filePath, extension); 107 + const renamedName = project.fileName?.(name) || name; 108 + if (renamedName !== name) { 109 + const outputPath = path.dirname(filePath); 110 + fs.renameSync(filePath, path.resolve(outputPath, `${renamedName}${extension}`)); 111 + renamed.set(name, renamedName); 112 + } 113 + } 114 + 115 + function replaceImports({ 116 + filePath, 117 + isDevMode, 118 + meta, 119 + renamed, 120 + }: { 121 + filePath: string; 122 + isDevMode?: boolean; 123 + meta: ProjectRenderMeta; 124 + renamed: Map<string, string>; 125 + }): void { 126 + let content = fs.readFileSync(filePath, 'utf8'); 127 + 128 + // Dev mode: rewrite source bundle imports to match output structure 129 + if (isDevMode) { 130 + // ../../client-core/bundle/foo -> ../core/foo 131 + content = content.replace(/from\s+['"]\.\.\/\.\.\/client-core\/bundle\//g, "from '../core/"); 132 + // ../../client-core/bundle' (index import) 133 + content = content.replace(/from\s+['"]\.\.\/\.\.\/client-core\/bundle['"]/g, "from '../core'"); 134 + } 135 + 136 + content = content.replace(/from\s+['"](\.\.?\/[^'"]*?)['"]/g, (match, importPath) => { 137 + const importIndex = match.indexOf(importPath); 138 + const extension = path.extname(importPath); 139 + const fileName = path.basename(importPath, extension); 140 + const importDir = path.dirname(importPath); 141 + const replacedName = 142 + (renamed.get(fileName) ?? fileName) + 143 + (meta.importFileExtension ? meta.importFileExtension : extension); 144 + const replacedMatch = 145 + match.slice(0, importIndex) + 146 + [importDir, replacedName].filter(Boolean).join('/') + 147 + match.slice(importIndex + importPath.length); 148 + return replacedMatch; 149 + }); 150 + 151 + const header = '# This file is auto-generated by @hey-api/openapi-python\n\n'; 152 + 153 + content = `${header}${content}`; 154 + 155 + fs.writeFileSync(filePath, content, 'utf8'); 156 + } 157 + 158 + /** 159 + * Creates a `client` folder containing the same modules as the client package. 160 + */ 161 + export function generateClientBundle({ 162 + meta, 163 + outputPath, 164 + plugin, 165 + project, 166 + }: { 167 + meta: ProjectRenderMeta; 168 + outputPath: string; 169 + plugin: DefinePlugin<Client.Config & { name: string }>['Config']; 170 + project?: IProject; 171 + }): Map<string, string> | undefined { 172 + const renamed = new Map<string, string>(); 173 + const devMode = isDevMode(); 174 + 175 + // copy Hey API clients to output 176 + const isHeyApiClientPlugin = plugin.name.startsWith('@hey-api/client-'); 177 + if (isHeyApiClientPlugin) { 178 + const { clientPath } = getClientBundlePaths(plugin.name); 179 + // const { clientPath, corePath } = getClientBundlePaths(plugin.name); 180 + 181 + // copy client core 182 + // const coreOutputPath = path.resolve(outputPath, 'core'); 183 + // ensureDirSync(coreOutputPath); 184 + // copyRecursivePnP(corePath, coreOutputPath); 185 + 186 + // copy client bundle 187 + const clientOutputPath = path.resolve(outputPath, 'client'); 188 + ensureDirSync(clientOutputPath); 189 + copyRecursivePnP(clientPath, clientOutputPath); 190 + 191 + if (project) { 192 + // const copiedCoreFiles = fs.readdirSync(coreOutputPath); 193 + // for (const file of copiedCoreFiles) { 194 + // renameFile({ 195 + // filePath: path.resolve(coreOutputPath, file), 196 + // project, 197 + // renamed, 198 + // }); 199 + // } 200 + 201 + const copiedClientFiles = fs.readdirSync(clientOutputPath); 202 + for (const file of copiedClientFiles) { 203 + renameFile({ 204 + filePath: path.resolve(clientOutputPath, file), 205 + project, 206 + renamed, 207 + }); 208 + } 209 + } 210 + 211 + // const coreFiles = fs.readdirSync(coreOutputPath); 212 + // for (const file of coreFiles) { 213 + // replaceImports({ 214 + // filePath: path.resolve(coreOutputPath, file), 215 + // isDevMode: devMode, 216 + // meta, 217 + // renamed, 218 + // }); 219 + // } 220 + 221 + const clientFiles = fs.readdirSync(clientOutputPath); 222 + for (const file of clientFiles) { 223 + replaceImports({ 224 + filePath: path.resolve(clientOutputPath, file), 225 + isDevMode: devMode, 226 + meta, 227 + renamed, 228 + }); 229 + } 230 + return renamed; 231 + } 232 + 233 + const clientSrcPath = path.isAbsolute(plugin.name) ? path.dirname(plugin.name) : undefined; 234 + 235 + // copy custom local client to output 236 + if (clientSrcPath) { 237 + const dirPath = path.resolve(outputPath, 'client'); 238 + ensureDirSync(dirPath); 239 + copyRecursivePnP(clientSrcPath, dirPath); 240 + return; 241 + } 242 + 243 + // copy third-party client to output 244 + const clientModulePath = path.normalize(require.resolve(plugin.name)); 245 + const clientModulePathComponents = clientModulePath.split(path.sep); 246 + const clientDistPath = clientModulePathComponents 247 + .slice(0, clientModulePathComponents.indexOf('dist') + 1) 248 + .join(path.sep); 249 + 250 + const indexJsFile = clientModulePathComponents[clientModulePathComponents.length - 1]; 251 + const distFiles = [indexJsFile!, 'index.d.mts', 'index.d.cts']; 252 + const dirPath = path.resolve(outputPath, 'client'); 253 + ensureDirSync(dirPath); 254 + for (const file of distFiles) { 255 + fs.copyFileSync(path.resolve(clientDistPath, file), path.resolve(dirPath, file)); 256 + } 257 + 258 + return; 259 + }
+18 -18
packages/openapi-python/src/generate/output.ts
··· 4 4 import type { Context } from '@hey-api/shared'; 5 5 import { IntentContext } from '@hey-api/shared'; 6 6 7 - // import { getTypedConfig } from '../config/utils'; 8 - // import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; 9 - // import { generateClientBundle } from './client'; 7 + import { getTypedConfig } from '../config/utils'; 8 + import { getClientPlugin } from '../plugins/@hey-api/client-core/utils'; 9 + import { generateClientBundle } from './client'; 10 10 11 11 export async function generateOutput(context: Context): Promise<void> { 12 12 const outputPath = path.resolve(context.config.output.path); ··· 17 17 } 18 18 } 19 19 20 - // const config = getTypedConfig(context); 20 + const config = getTypedConfig(context); 21 21 22 - // const client = getClientPlugin(config); 23 - // if ('bundle' in client.config && client.config.bundle && !config.dryRun) { 24 - // not proud of this one 25 - // // @ts-expect-error 26 - // config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({ 27 - // meta: { 28 - // importFileExtension: config.output.importFileExtension, 29 - // }, 30 - // outputPath, 31 - // // @ts-expect-error 32 - // plugin: client, 33 - // project: context.gen, 34 - // }); 35 - // } 22 + const client = getClientPlugin(config); 23 + if ('bundle' in client.config && client.config.bundle && !config.dryRun) { 24 + // not proud of this one 25 + // @ts-expect-error 26 + config._FRAGILE_CLIENT_BUNDLE_RENAMED = generateClientBundle({ 27 + meta: { 28 + importFileExtension: config.output.importFileExtension, 29 + }, 30 + outputPath, 31 + // @ts-expect-error 32 + plugin: client, 33 + project: context.gen, 34 + }); 35 + } 36 36 37 37 for (const plugin of context.registerPlugins()) { 38 38 await plugin.run();
+6
packages/openapi-python/src/index.ts
··· 57 57 58 58 declare module '@hey-api/shared' { 59 59 interface PluginConfigMap { 60 + '@hey-api/client-httpx': HeyApiClientHttpxPlugin['Types']; 60 61 '@hey-api/python-sdk': HeyApiSdkPlugin['Types']; 61 62 } 62 63 } ··· 68 69 import colorSupport from 'color-support'; 69 70 70 71 import type { UserConfig } from './config/types'; 72 + import type { HeyApiClientHttpxPlugin } from './plugins/@hey-api/client-httpx'; 71 73 import type { HeyApiSdkPlugin } from './plugins/@hey-api/sdk'; 72 74 73 75 colors.enabled = colorSupport().hasBasic; ··· 77 79 /** 78 80 * Type helper for configuration object, returns {@link MaybeArray<UserConfig>} object(s) 79 81 */ 82 + export function defineConfig( 83 + config: LazyOrAsync<ReadonlyArray<UserConfig>>, 84 + ): Promise<ReadonlyArray<UserConfig>>; 85 + export function defineConfig(config: LazyOrAsync<UserConfig>): Promise<UserConfig>; 80 86 export async function defineConfig<T extends MaybeArray<UserConfig>>( 81 87 config: LazyOrAsync<T>, 82 88 ): Promise<T> {
+9
packages/openapi-python/src/plugins/@hey-api/client-core/config.ts
··· 1 + export const clientDefaultConfig = { 2 + baseUrl: true, 3 + bundle: true, 4 + includeInEntry: false, 5 + } as const; 6 + 7 + export const clientDefaultMeta = { 8 + tags: ['client'], 9 + } as const;
+60
packages/openapi-python/src/plugins/@hey-api/client-core/types.ts
··· 1 + /* eslint-disable @typescript-eslint/no-namespace */ 2 + import type { Plugin } from '@hey-api/shared'; 3 + 4 + import type { HeyApiClientHttpxPlugin } from '../../../plugins/@hey-api/client-httpx'; 5 + 6 + export interface PluginHandler { 7 + (...args: Parameters<HeyApiClientHttpxPlugin['Handler']>): void; 8 + } 9 + 10 + /** 11 + * Public Client API. 12 + */ 13 + export namespace Client { 14 + export type Config = Plugin.Hooks & 15 + Plugin.UserExports & { 16 + /** 17 + * Set a default base URL when creating the client? You can set `baseUrl` 18 + * to a string which will be used as the base URL. If your input defines 19 + * server(s), you can set `baseUrl` to a number to pick a specific server 20 + * to use as the base URL. You can disable setting the base URL by setting 21 + * `baseUrl` to `false`. By default, `baseUrl` is `true` and it will try to 22 + * use the first defined server value. If there's none, we won't set a 23 + * base URL. 24 + * 25 + * If the matched URL contains template literals, it will be ignored. 26 + * 27 + * @default true 28 + */ 29 + baseUrl?: string | number | boolean; 30 + /** 31 + * Bundle the client module? When `true`, the client module will be copied 32 + * from the client plugin and bundled with the generated output. 33 + * 34 + * @default true 35 + */ 36 + bundle?: boolean; 37 + /** 38 + * Relative path to the runtime configuration file. This file must export 39 + * a `createClientConfig()` function. The `createClientConfig()` function 40 + * will be called on client initialization and the returned object will 41 + * become the client's initial configuration. 42 + * 43 + * You may want to initialize your client this way instead of calling 44 + * `setConfig()`. This is useful for example if you're using Next.js 45 + * to ensure your client always has the correct values. 46 + */ 47 + runtimeConfigPath?: string; 48 + /** 49 + * Should the type helper for base URL allow only values matching the 50 + * server(s) defined in the input? By default, `strictBaseUrl` is `false` 51 + * which will provide type hints and allow you to pass any string. 52 + * 53 + * Note that setting `strictBaseUrl` to `true` can produce an invalid 54 + * build if you specify `baseUrl` which doesn't conform to the type helper. 55 + * 56 + * @default false 57 + */ 58 + strictBaseUrl?: boolean; 59 + }; 60 + }
+24
packages/openapi-python/src/plugins/@hey-api/client-core/utils.ts
··· 1 + import type { Config } from '../../../config/types'; 2 + import type { PluginClientNames } from '../../../plugins/types'; 3 + 4 + export function getClientPlugin( 5 + config: Config, 6 + ): Config['plugins'][PluginClientNames] & { name: PluginClientNames } { 7 + for (const name of config.pluginOrder) { 8 + const plugin = config.plugins[name]; 9 + if (plugin?.tags?.includes('client')) { 10 + return plugin as Config['plugins'][PluginClientNames] & { 11 + name: PluginClientNames; 12 + }; 13 + } 14 + } 15 + 16 + return { 17 + config: { 18 + // @ts-expect-error 19 + name: '', 20 + }, 21 + // @ts-expect-error 22 + name: '', 23 + }; 24 + }
+3
packages/openapi-python/src/plugins/@hey-api/client-httpx/bundle/__init__.py
··· 1 + from .client import Client, create_client 2 + 3 + __all__ = ["Client", "create_client"]
+62
packages/openapi-python/src/plugins/@hey-api/client-httpx/bundle/client.py
··· 1 + from typing import Optional 2 + import httpx 3 + 4 + 5 + class BaseClient: 6 + """Base HTTP client using httpx that SDK classes extend.""" 7 + 8 + def __init__(self, client: Optional[httpx.Client] = None, base_url: Optional[str] = None, **kwargs): 9 + if client is not None: 10 + self._client = client 11 + else: 12 + self._client = httpx.Client(base_url=base_url or "", **kwargs) 13 + 14 + @property 15 + def client(self) -> httpx.Client: 16 + """Get the httpx client instance.""" 17 + return self._client 18 + 19 + def request(self, method: str, url: str, **kwargs) -> httpx.Response: 20 + """Make an HTTP request.""" 21 + return self._client.request(method, url, **kwargs) 22 + 23 + def get(self, url: str, **kwargs) -> httpx.Response: 24 + """Make a GET request.""" 25 + return self._client.get(url, **kwargs) 26 + 27 + def post(self, url: str, **kwargs) -> httpx.Response: 28 + """Make a POST request.""" 29 + return self._client.post(url, **kwargs) 30 + 31 + def put(self, url: str, **kwargs) -> httpx.Response: 32 + """Make a PUT request.""" 33 + return self._client.put(url, **kwargs) 34 + 35 + def patch(self, url: str, **kwargs) -> httpx.Response: 36 + """Make a PATCH request.""" 37 + return self._client.patch(url, **kwargs) 38 + 39 + def delete(self, url: str, **kwargs) -> httpx.Response: 40 + """Make a DELETE request.""" 41 + return self._client.delete(url, **kwargs) 42 + 43 + def close(self): 44 + """Close the client.""" 45 + self._client.close() 46 + 47 + def __enter__(self): 48 + return self 49 + 50 + def __exit__(self, *args): 51 + self.close() 52 + 53 + 54 + class Client(BaseClient): 55 + """HTTP client using httpx (alias for BaseClient).""" 56 + 57 + pass 58 + 59 + 60 + def create_client(base_url: Optional[str] = None, **kwargs) -> Client: 61 + """Create a new HTTP client instance.""" 62 + return Client(base_url=base_url, **kwargs)
+18
packages/openapi-python/src/plugins/@hey-api/client-httpx/config.ts
··· 1 + import { definePluginConfig } from '@hey-api/shared'; 2 + 3 + import { clientDefaultConfig, clientDefaultMeta } from '../client-core/config'; 4 + import type { HeyApiClientHttpxPlugin } from './types'; 5 + 6 + export const defaultConfig: HeyApiClientHttpxPlugin['Config'] = { 7 + ...clientDefaultMeta, 8 + config: clientDefaultConfig, 9 + handler() { 10 + // TODO: handler 11 + }, 12 + name: '@hey-api/client-httpx', 13 + }; 14 + 15 + /** 16 + * Type helper for `@hey-api/client-httpx` plugin, returns {@link Plugin.Config} object 17 + */ 18 + export const defineConfig = definePluginConfig(defaultConfig);
+2
packages/openapi-python/src/plugins/@hey-api/client-httpx/index.ts
··· 1 + export { defaultConfig, defineConfig } from './config'; 2 + export type { HeyApiClientHttpxPlugin } from './types';
+24
packages/openapi-python/src/plugins/@hey-api/client-httpx/types.ts
··· 1 + import type { DefinePlugin, Plugin } from '@hey-api/shared'; 2 + 3 + export type UserConfig = Plugin.Name<'@hey-api/client-httpx'> & { 4 + /** 5 + * Set a default base URL when creating the client? You can set `baseUrl` 6 + * to a string which will be used as the base URL. If your input defines 7 + * server(s), you can set `baseUrl` to a number to pick a specific server 8 + * to use as the base URL. You can disable setting the base URL by setting 9 + * `baseUrl` to `false`. By default, `baseUrl` is `true` and it will try to 10 + * use the first defined server value. If there's none, we won't set a 11 + * base URL. 12 + * 13 + * If the matched URL contains template literals, it will be ignored. 14 + * 15 + * @default true 16 + */ 17 + baseUrl?: string | number | boolean; 18 + }; 19 + 20 + export type Config = Plugin.Name<'@hey-api/client-httpx'> & { 21 + baseUrl: string | number | boolean; 22 + }; 23 + 24 + export type HeyApiClientHttpxPlugin = DefinePlugin<UserConfig, Config>;
+18 -24
packages/openapi-python/src/plugins/@hey-api/sdk/config.ts
··· 1 1 import { definePluginConfig } from '@hey-api/shared'; 2 2 3 - // import { resolveExamples } from './examples'; 4 - // import { resolveOperations } from './operations'; 3 + import { resolveOperations } from './operations'; 5 4 import { handler } from './plugin'; 6 5 import type { HeyApiSdkPlugin } from './types'; 7 6 8 7 export const defaultConfig: HeyApiSdkPlugin['Config'] = { 9 8 config: { 10 - auth: true, 9 + // auth: true, 11 10 client: true, 12 - exportFromIndex: true, 11 + includeInEntry: true, 13 12 paramsStructure: 'grouped', 14 - responseStyle: 'fields', 15 - transformer: false, 16 - validator: false, 17 - 18 - // Deprecated - kept for backward compatibility 19 - // eslint-disable-next-line sort-keys-fix/sort-keys-fix 20 - response: 'body', 13 + // responseStyle: 'fields', 14 + // transformer: false, 15 + // validator: false, 21 16 }, 22 17 handler, 23 18 name: '@hey-api/python-sdk', 24 - // resolveConfig: (plugin, context) => { 25 - resolveConfig: () => { 26 - // if (plugin.config.client) { 27 - // if (typeof plugin.config.client === 'boolean') { 28 - // plugin.config.client = context.pluginByTag('client', { 29 - // defaultPlugin: '@hey-api/client-httpx', 30 - // }); 31 - // } 32 - // plugin.dependencies.add(plugin.config.client!); 33 - // } else { 34 - // plugin.config.client = false; 35 - // } 19 + resolveConfig: (plugin, context) => { 20 + if (plugin.config.client) { 21 + if (typeof plugin.config.client === 'boolean') { 22 + plugin.config.client = context.pluginByTag('client', { 23 + defaultPlugin: '@hey-api/client-httpx', 24 + }); 25 + } 26 + plugin.dependencies.add(plugin.config.client!); 27 + } else { 28 + plugin.config.client = false; 29 + } 36 30 // if (plugin.config.transformer) { 37 31 // if (typeof plugin.config.transformer === 'boolean') { 38 32 // plugin.config.transformer = context.pluginByTag('transformer'); ··· 64 58 // plugin.config.validator.response = false; 65 59 // } 66 60 // plugin.config.examples = resolveExamples(plugin.config, context); 67 - // plugin.config.operations = resolveOperations(plugin.config, context); 61 + plugin.config.operations = resolveOperations(plugin.config, context); 68 62 }, 69 63 }; 70 64
+19
packages/openapi-python/src/plugins/@hey-api/sdk/examples/config.ts
··· 1 + import type { PluginContext } from '@hey-api/shared'; 2 + 3 + import type { UserConfig } from '../types'; 4 + import type { ExamplesConfig } from './types'; 5 + 6 + type Config = Omit<UserConfig, 'name'>; 7 + 8 + export function resolveExamples(config: Config, context: PluginContext): ExamplesConfig { 9 + return context.valueToObject({ 10 + defaultValue: { 11 + enabled: Boolean(config.examples), 12 + language: 'Python', 13 + }, 14 + mappers: { 15 + boolean: (enabled) => ({ enabled }), 16 + }, 17 + value: config.examples, 18 + }) as ExamplesConfig; 19 + }
+2
packages/openapi-python/src/plugins/@hey-api/sdk/examples/index.ts
··· 1 + export { resolveExamples } from './config'; 2 + export type { ExamplesConfig, UserExamplesConfig } from './types';
+61
packages/openapi-python/src/plugins/@hey-api/sdk/examples/types.ts
··· 1 + import type { FeatureToggle, IR, LinguistLanguages } from '@hey-api/shared'; 2 + import type { MaybeFunc } from '@hey-api/types'; 3 + 4 + import type { CallArgs, DollarPyDsl, ExampleOptions } from '../../../../py-dsl'; 5 + 6 + export type UserExamplesConfig = Omit<ExampleOptions, 'payload'> & { 7 + /** 8 + * Whether this feature is enabled. 9 + * 10 + * @default true 11 + */ 12 + enabled?: boolean; 13 + /** 14 + * The programming language for the generated examples. 15 + * 16 + * This is used to display the language label in code blocks in 17 + * documentation UIs. 18 + * 19 + * @default 'Python' 20 + */ 21 + language?: LinguistLanguages; 22 + /** 23 + * Example request payload. 24 + */ 25 + payload?: MaybeFunc< 26 + (operation: IR.OperationObject, ctx: DollarPyDsl) => CallArgs | CallArgs[number] 27 + >; 28 + /** 29 + * Transform the generated example string. 30 + * 31 + * @param example The generated example string. 32 + * @param operation The operation the example was generated for. 33 + * @returns The final example string. 34 + */ 35 + transform?: (example: string, operation: IR.OperationObject) => string; 36 + }; 37 + 38 + export type ExamplesConfig = Omit<ExampleOptions, 'payload'> & 39 + FeatureToggle & { 40 + /** 41 + * The programming language for the generated examples. 42 + * 43 + * This is used to display the language label in code blocks in 44 + * documentation UIs. 45 + */ 46 + language: LinguistLanguages; 47 + /** 48 + * Example request payload. 49 + */ 50 + payload?: MaybeFunc< 51 + (operation: IR.OperationObject, ctx: DollarPyDsl) => CallArgs | CallArgs[number] 52 + >; 53 + /** 54 + * Transform the generated example string. 55 + * 56 + * @param example The generated example string. 57 + * @param operation The operation the example was generated for. 58 + * @returns The final example string. 59 + */ 60 + transform?: (example: string, operation: IR.OperationObject) => string; 61 + };
+67
packages/openapi-python/src/plugins/@hey-api/sdk/operations/config.ts
··· 1 + import type { OperationsStrategy, PluginContext } from '@hey-api/shared'; 2 + 3 + import type { UserConfig } from '../types'; 4 + import type { OperationsConfig, UserOperationsConfig } from './types'; 5 + 6 + type Config = Omit<UserConfig, 'name'>; 7 + 8 + export function resolveOperations(config: Config, context: PluginContext): OperationsConfig { 9 + return normalizeConfig(config.operations, context); 10 + } 11 + 12 + function normalizeConfig( 13 + input: Exclude<OperationsStrategy, 'flat'> | UserOperationsConfig | undefined, 14 + context: PluginContext, 15 + ): OperationsConfig { 16 + if (!input || typeof input === 'string' || typeof input === 'function') { 17 + input = { strategy: input }; 18 + } 19 + 20 + const strategy = input.strategy ?? 'single'; 21 + 22 + return context.valueToObject({ 23 + defaultValue: { 24 + container: 'class', 25 + methods: 'instance', 26 + nesting: 'operationId', 27 + nestingDelimiters: /[./]/, 28 + strategy, 29 + strategyDefaultTag: 'default', 30 + }, 31 + mappers: { 32 + object(value) { 33 + value.containerName = context.valueToObject({ 34 + defaultValue: 35 + strategy === 'single' 36 + ? { casing: 'PascalCase', name: 'Sdk' } 37 + : { casing: 'PascalCase' }, 38 + mappers: { 39 + function: (name) => ({ name }), 40 + string: (name) => ({ name }), 41 + }, 42 + value: value.containerName, 43 + }); 44 + value.methodName = context.valueToObject({ 45 + defaultValue: { casing: 'snake_case' }, 46 + mappers: { 47 + function: (name) => ({ name }), 48 + string: (name) => ({ name }), 49 + }, 50 + value: value.methodName, 51 + }); 52 + value.segmentName = context.valueToObject({ 53 + defaultValue: { casing: 'PascalCase' }, 54 + mappers: { 55 + function: (name) => ({ name }), 56 + string: (name) => ({ name }), 57 + }, 58 + value: value.segmentName, 59 + }); 60 + return value; 61 + }, 62 + }, 63 + value: { 64 + ...input, 65 + } as UserOperationsConfig, 66 + }) as OperationsConfig; 67 + }
+3
packages/openapi-python/src/plugins/@hey-api/sdk/operations/index.ts
··· 1 + export { resolveOperations } from './config'; 2 + export { resolveStrategy } from './resolve'; 3 + export type { OperationsConfig, UserOperationsConfig } from './types';
+44
packages/openapi-python/src/plugins/@hey-api/sdk/operations/resolve.ts
··· 1 + import type { OperationPathStrategy, OperationStructureStrategy } from '@hey-api/shared'; 2 + import { OperationPath, OperationStrategy } from '@hey-api/shared'; 3 + 4 + import type { HeyApiSdkPlugin } from '../types'; 5 + 6 + function resolvePath(plugin: HeyApiSdkPlugin['Instance']): OperationPathStrategy { 7 + if (plugin.config.operations.nesting === 'id') { 8 + return OperationPath.id(); 9 + } 10 + 11 + if (plugin.config.operations.nesting === 'operationId') { 12 + return OperationPath.fromOperationId({ 13 + delimiters: plugin.config.operations.nestingDelimiters, 14 + fallback: OperationPath.id(), 15 + }); 16 + } 17 + 18 + return plugin.config.operations.nesting; 19 + } 20 + 21 + export function resolveStrategy(plugin: HeyApiSdkPlugin['Instance']): OperationStructureStrategy { 22 + // if (plugin.config.operations.strategy === 'flat') { 23 + // return OperationStrategy.flat({ 24 + // path: (operation) => [resolvePath(plugin)(operation).join('.')], 25 + // }); 26 + // } 27 + 28 + if (plugin.config.operations.strategy === 'single') { 29 + const root = plugin.config.operations.containerName; 30 + return OperationStrategy.single({ 31 + path: resolvePath(plugin), 32 + root: typeof root.name === 'string' ? root.name : (root.name?.('') ?? ''), 33 + }); 34 + } 35 + 36 + if (plugin.config.operations.strategy === 'byTags') { 37 + return OperationStrategy.byTags({ 38 + fallback: plugin.config.operations.strategyDefaultTag, 39 + path: resolvePath(plugin), 40 + }); 41 + } 42 + 43 + return plugin.config.operations.strategy; 44 + }
+178
packages/openapi-python/src/plugins/@hey-api/sdk/operations/types.ts
··· 1 + import type { 2 + NamingConfig, 3 + NamingRule, 4 + OperationPathStrategy, 5 + OperationsStrategy, 6 + } from '@hey-api/shared'; 7 + 8 + export interface UserOperationsConfig { 9 + /** 10 + * Type of container for grouped operations. 11 + * 12 + * Ignored when `strategy` is `'flat'`. 13 + * 14 + * - `'class'` - Class with methods 15 + * 16 + * @default 'class' 17 + */ 18 + container?: 'class'; 19 + /** 20 + * Customize container names. 21 + * 22 + * For `'single'` strategy, this sets the root container name. 23 + * For `'byTags'` strategy, this transforms tag names. 24 + * 25 + * @default 'Sdk' for `'single'` strategy 26 + * 27 + * @example 28 + * // Set root name for single strategy 29 + * containerName: 'MyApi' 30 + * 31 + * @example 32 + * // Transform tag names with suffix 33 + * containerName: '{{name}}Service' 34 + * 35 + * @example 36 + * // With casing 37 + * containerName: { name: '{{name}}Service', casing: 'PascalCase' } 38 + */ 39 + containerName?: NamingRule; 40 + /** 41 + * Customize method/function names. 42 + * 43 + * Applied to the final segment of the path (the method name). 44 + */ 45 + methodName?: NamingRule; 46 + /** 47 + * How methods are attached to class containers. 48 + * 49 + * Only applies when `container` is `'class'`. 50 + * 51 + * - `'instance'` - Instance methods, requires `new ClassName(config)` 52 + * 53 + * @default 'instance' 54 + */ 55 + methods?: 'instance'; 56 + /** 57 + * How to derive nesting structure from operations. 58 + * 59 + * - `'operationId'` - Split operationId by delimiters (e.g., `users.list` → `Users.list()`) 60 + * - `'id'` - Use operation id as-is, no nesting 61 + * - Custom function for full control 62 + * 63 + * @default 'operationId' 64 + */ 65 + nesting?: 'operationId' | 'id' | OperationPathStrategy; 66 + /** 67 + * Delimiters for splitting operationId. 68 + * 69 + * Only applies when `nesting` is `'operationId'`. 70 + * 71 + * @default /[./]/ 72 + */ 73 + nestingDelimiters?: RegExp; 74 + /** 75 + * Customize nesting segment names. 76 + * 77 + * Applied to intermediate path segments (not the method name). 78 + */ 79 + segmentName?: NamingRule; 80 + /** 81 + * Grouping strategy. 82 + * 83 + * - `'byTags'` - One container per operation tag 84 + * - `'single'` - All operations in one container 85 + * - Custom function for full control 86 + * 87 + * @default 'single' 88 + */ 89 + strategy?: Exclude<OperationsStrategy, 'flat'>; 90 + /** 91 + * Default container name for operations without tags. 92 + * 93 + * Only applies when `strategy` is `'byTags'`. 94 + * 95 + * @default 'default' 96 + */ 97 + strategyDefaultTag?: string; 98 + } 99 + 100 + export interface OperationsConfig { 101 + /** 102 + * Type of container for grouped operations. 103 + * 104 + * Ignored when `strategy` is `'flat'`. 105 + * 106 + * - `'class'` - Class with methods 107 + */ 108 + container: 'class'; 109 + /** 110 + * Customize container names. 111 + * 112 + * For `'single'` strategy, this sets the root container name. 113 + * For `'byTags'` strategy, this transforms tag names. 114 + * 115 + * @default 'Sdk' for `'single'` strategy 116 + * 117 + * @example 118 + * // Set root name for single strategy 119 + * containerName: 'MyApi' 120 + * 121 + * @example 122 + * // Transform tag names with suffix 123 + * containerName: '{{name}}Service' 124 + * 125 + * @example 126 + * // With casing 127 + * containerName: { name: '{{name}}Service', case: 'PascalCase' } 128 + */ 129 + containerName: NamingConfig; 130 + /** 131 + * Customize method/function names. 132 + * 133 + * Applied to the final segment of the path (the method name). 134 + */ 135 + methodName: NamingConfig; 136 + /** 137 + * How methods are attached to class containers. 138 + * 139 + * Only applies when `container` is `'class'`. 140 + * 141 + * - `'instance'` - Instance methods, requires `new ClassName(config)` 142 + */ 143 + methods: 'instance'; 144 + /** 145 + * How to derive nesting structure from operations. 146 + * 147 + * - `'operationId'` - Split operationId by delimiters (e.g., `users.list` → `Users.list()`) 148 + * - `'id'` - Use operation id as-is, no nesting 149 + * - Custom function for full control 150 + */ 151 + nesting: 'operationId' | 'id' | OperationPathStrategy; 152 + /** 153 + * Delimiters for splitting operationId. 154 + * 155 + * Only applies when `nesting` is `'operationId'`. 156 + */ 157 + nestingDelimiters: RegExp; 158 + /** 159 + * Customize nesting segment names. 160 + * 161 + * Applied to intermediate path segments (not the method name). 162 + */ 163 + segmentName: NamingConfig; 164 + /** 165 + * Grouping strategy. 166 + * 167 + * - `'byTags'` - One container per operation tag 168 + * - `'single'` - All operations in one container 169 + * - Custom function for full control 170 + */ 171 + strategy: Exclude<OperationsStrategy, 'flat'>; 172 + /** 173 + * Default container name for operations without tags. 174 + * 175 + * Only applies when `strategy` is `'byTags'`. 176 + */ 177 + strategyDefaultTag: string; 178 + }
+2
packages/openapi-python/src/plugins/@hey-api/sdk/shared/class.ts
··· 1 + export const createRegistryClass = () => {}; 2 + export const createClientClass = () => {};
+58 -142
packages/openapi-python/src/plugins/@hey-api/sdk/types.ts
··· 1 - import type { IndexExportOption, NameTransformer } from '@hey-api/shared'; 2 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 1 + import type { DefinePlugin, OperationsStrategy, Plugin } from '@hey-api/shared'; 3 2 4 - // import type { OperationsStrategy } from '@hey-api/shared'; 5 - import type { PluginClientNames, PluginValidatorNames } from '../../../plugins/types'; 6 - 7 - // import type { ExamplesConfig, UserExamplesConfig } from './examples'; 8 - // import type { OperationsConfig, UserOperationsConfig } from './operations'; 3 + import type { PluginClientNames } from '../../../plugins/types'; 4 + // import type { PluginClientNames, PluginValidatorNames } from '../../../plugins/types'; 5 + import type { ExamplesConfig, UserExamplesConfig } from './examples'; 6 + import type { OperationsConfig, UserOperationsConfig } from './operations'; 9 7 10 8 export type UserConfig = Plugin.Name<'@hey-api/python-sdk'> & 11 - Plugin.Hooks & { 9 + Plugin.Hooks & 10 + Plugin.UserExports & { 12 11 /** 13 12 * Should the generated functions contain auth mechanisms? You may want to 14 13 * disable this option if you're handling auth yourself or defining it ··· 16 15 * 17 16 * @default true 18 17 */ 19 - auth?: boolean; 18 + // auth?: boolean; 20 19 /** 21 20 * Use an internal client instance to send HTTP requests? This is useful if 22 21 * you don't want to manually pass the client to each SDK function. ··· 24 23 * You can customize the selected client output through its plugin. You can 25 24 * also set `client` to `true` to automatically choose the client from your 26 25 * defined plugins. If we can't detect a client plugin when using `true`, we 27 - * will default to `@hey-api/client-fetch`. 26 + * will default to `@hey-api/client-httpx`. 28 27 * 29 28 * @default true 30 29 */ ··· 38 37 * 39 38 * @default false 40 39 */ 41 - // examples?: boolean | UserExamplesConfig; 42 - /** 43 - * Whether exports should be re-exported in the index file. 44 - * 45 - * @default true 46 - */ 47 - exportFromIndex?: boolean; 40 + examples?: boolean | UserExamplesConfig; 48 41 /** 49 42 * Define the structure of generated SDK operations. 50 43 * 51 44 * String shorthand: 52 45 * - `'byTags'` – one container per operation tag 53 - * - `'flat'` – standalone functions, no container 54 46 * - `'single'` – all operations in a single container 55 47 * - custom function for full control 56 48 * 57 49 * Use the object form for advanced configuration. 58 50 * 59 - * @default 'flat' 51 + * @default 'single' 60 52 */ 61 - // operations?: OperationsStrategy | UserOperationsConfig; 53 + operations?: Exclude<OperationsStrategy, 'flat'> | UserOperationsConfig; 62 54 /** 63 55 * Define how request parameters are structured in generated SDK methods. 64 56 * ··· 77 69 * 78 70 * @default 'fields' 79 71 */ 80 - responseStyle?: 'data' | 'fields'; 72 + // responseStyle?: 'data' | 'fields'; 81 73 /** 82 74 * Transform response data before returning. This is useful if you want to 83 75 * convert for example ISO strings into Date objects. However, transformation ··· 89 81 * 90 82 * @default false 91 83 */ 92 - transformer?: '@hey-api/transformers' | boolean; 84 + // transformer?: PluginTransformerNames | boolean; 93 85 /** 94 86 * Validate request and/or response data against schema before returning. 95 87 * This is useful if you want to ensure the request and/or response conforms ··· 108 100 * 109 101 * @default false 110 102 */ 111 - validator?: 112 - | PluginValidatorNames 113 - | boolean 114 - | { 115 - /** 116 - * Validate request data against schema before sending. 117 - * 118 - * Can be a validator plugin name or boolean (true to auto-select, false 119 - * to disable). 120 - * 121 - * @default false 122 - */ 123 - request?: PluginValidatorNames | boolean; 124 - /** 125 - * Validate response data against schema before returning. 126 - * 127 - * Can be a validator plugin name or boolean (true to auto-select, false 128 - * to disable). 129 - * 130 - * @default false 131 - */ 132 - response?: PluginValidatorNames | boolean; 133 - }; 134 - 135 - // DEPRECATED OPTIONS BELOW 136 - 137 - /** 138 - * Group operation methods into classes? When enabled, you can select which 139 - * classes to export with `sdk.include` and/or transform their names with 140 - * `sdk.classNameBuilder`. 141 - * 142 - * Note that by enabling this option, your SDKs will **NOT** 143 - * support {@link https://developer.mozilla.org/docs/Glossary/Tree_shaking tree-shaking}. 144 - * For this reason, it is disabled by default. 145 - * 146 - * @deprecated Use `operations: { strategy: "byTags" }` or `operations: { strategy: "single" }` instead. 147 - * @default false 148 - */ 149 - // eslint-disable-next-line typescript-sort-keys/interface 150 - asClass?: boolean; 151 - /** 152 - * Customize the generated class names. The name variable is obtained from 153 - * your OpenAPI specification tags or `instance` value. 154 - * 155 - * This option has no effect if `sdk.asClass` is `false`. 156 - * 157 - * @deprecated Use `operations: { containerName: "..." }` instead. 158 - */ 159 - classNameBuilder?: NameTransformer; 160 - /** 161 - * How should we structure your SDK? By default, we try to infer the ideal 162 - * structure using `operationId` keywords. If you prefer a flatter structure, 163 - * you can set `classStructure` to `off` to disable this behavior. 164 - * 165 - * @deprecated Use `operations: { nesting: "operationId" }` or `operations: { nesting: "id" }` instead. 166 - * @default 'auto' 167 - */ 168 - classStructure?: 'auto' | 'off'; 169 - /** 170 - * Set `instance` to create an instantiable SDK. Using `true` will use the 171 - * default instance name; in practice, you want to define your own by passing 172 - * a string value. 173 - * 174 - * @deprecated Use `operations: { strategy: "single", containerName: "Name", methods: "instance" }` instead. 175 - * @default false 176 - */ 177 - instance?: string | boolean; 178 - /** 179 - * Customise the name of methods within the service. By default, 180 - * `operation.id` is used. 181 - * 182 - * @deprecated Use `operations: { methodName: "..." }` instead. 183 - */ 184 - methodNameBuilder?: NameTransformer; 185 - /** 186 - * Use operation ID to generate operation names? 187 - * 188 - * @deprecated Use `operations: { nesting: "operationId" }` or `operations: { nesting: "id" }` instead. 189 - * @default true 190 - */ 191 - operationId?: boolean; 192 - /** 193 - * Define shape of returned value from service calls 194 - * 195 - * @deprecated 196 - * @default 'body' 197 - */ 198 - response?: 'body' | 'response'; 103 + // validator?: 104 + // | PluginValidatorNames 105 + // | boolean 106 + // | { 107 + // /** 108 + // * Validate request data against schema before sending. 109 + // * 110 + // * Can be a validator plugin name or boolean (true to auto-select, false 111 + // * to disable). 112 + // * 113 + // * @default false 114 + // */ 115 + // request?: PluginValidatorNames | boolean; 116 + // /** 117 + // * Validate response data against schema before returning. 118 + // * 119 + // * Can be a validator plugin name or boolean (true to auto-select, false 120 + // * to disable). 121 + // * 122 + // * @default false 123 + // */ 124 + // response?: PluginValidatorNames | boolean; 125 + // }; 199 126 }; 200 127 201 128 export type Config = Plugin.Name<'@hey-api/python-sdk'> & 202 129 Plugin.Hooks & 203 - IndexExportOption & { 130 + Plugin.Exports & { 204 131 /** 205 132 * Should the generated functions contain auth mechanisms? You may want to 206 133 * disable this option if you're handling auth yourself or defining it ··· 208 135 * 209 136 * @default true 210 137 */ 211 - auth: boolean; 138 + // auth: boolean; 212 139 /** 213 140 * Use an internal client instance to send HTTP requests? This is useful if 214 141 * you don't want to manually pass the client to each SDK function. ··· 216 143 * You can customize the selected client output through its plugin. You can 217 144 * also set `client` to `true` to automatically choose the client from your 218 145 * defined plugins. If we can't detect a client plugin when using `true`, we 219 - * will default to `@hey-api/client-fetch`. 146 + * will default to `@hey-api/client-httpx`. 220 147 * 221 148 * @default true 222 149 */ ··· 224 151 /** 225 152 * Configuration for generating SDK code examples. 226 153 */ 227 - // examples: ExamplesConfig; 154 + examples: ExamplesConfig; 228 155 /** 229 156 * Define the structure of generated SDK operations. 230 157 */ 231 - // operations: OperationsConfig; 158 + operations: OperationsConfig; 232 159 /** 233 160 * Define how request parameters are structured in generated SDK methods. 234 161 * ··· 247 174 * 248 175 * @default 'fields' 249 176 */ 250 - responseStyle: 'data' | 'fields'; 177 + // responseStyle: 'data' | 'fields'; 251 178 /** 252 179 * Transform response data before returning. This is useful if you want to 253 180 * convert for example ISO strings into Date objects. However, transformation ··· 259 186 * 260 187 * @default false 261 188 */ 262 - transformer: '@hey-api/transformers' | false; 189 + // transformer: PluginTransformerNames | false; 263 190 /** 264 191 * Validate request and/or response data against schema before returning. 265 192 * This is useful if you want to ensure the request and/or response conforms 266 193 * to a desired shape. However, validation adds runtime overhead, so it's 267 194 * not recommended to use unless absolutely necessary. 268 195 */ 269 - validator: { 270 - /** 271 - * The validator plugin to use for request validation, or false to disable. 272 - * 273 - * @default false 274 - */ 275 - request: PluginValidatorNames | false; 276 - /** 277 - * The validator plugin to use for response validation, or false to disable. 278 - * 279 - * @default false 280 - */ 281 - response: PluginValidatorNames | false; 282 - }; 283 - 284 - // DEPRECATED OPTIONS BELOW 285 - 286 - /** 287 - * Define shape of returned value from service calls 288 - * 289 - * @deprecated 290 - * @default 'body' 291 - */ 292 - // eslint-disable-next-line typescript-sort-keys/interface 293 - response: 'body' | 'response'; 196 + // validator: { 197 + // /** 198 + // * The validator plugin to use for request validation, or false to disable. 199 + // * 200 + // * @default false 201 + // */ 202 + // request: PluginValidatorNames | false; 203 + // /** 204 + // * The validator plugin to use for response validation, or false to disable. 205 + // * 206 + // * @default false 207 + // */ 208 + // response: PluginValidatorNames | false; 209 + // }; 294 210 }; 295 211 296 212 export type HeyApiSdkPlugin = DefinePlugin<UserConfig, Config>;
+203
packages/openapi-python/src/plugins/@hey-api/sdk/v1/node.ts
··· 1 + import type { 2 + StructureItem, 3 + StructureNode, 4 + StructureShell, 5 + Symbol, 6 + SymbolMeta, 7 + } from '@hey-api/codegen-core'; 8 + import type { IR } from '@hey-api/shared'; 9 + import { applyNaming, toCase } from '@hey-api/shared'; 10 + 11 + import { $ } from '../../../../py-dsl'; 12 + import { createOperationComment } from '../../../shared/utils/operation'; 13 + import type { HeyApiSdkPlugin } from '../types'; 14 + 15 + export interface OperationItem { 16 + operation: IR.OperationObject; 17 + path: ReadonlyArray<string | number>; 18 + tags: ReadonlyArray<string> | undefined; 19 + } 20 + 21 + export const source = globalThis.Symbol('@hey-api/python-sdk'); 22 + 23 + function attachComment<T extends ReturnType<typeof $.func>>(args: { 24 + node: T; 25 + operation: IR.OperationObject; 26 + }): T { 27 + const { node, operation } = args; 28 + return node.$if(createOperationComment(operation), (n, v) => n.doc(v)) as T; 29 + } 30 + 31 + function createShellMeta(node: StructureNode): SymbolMeta { 32 + return { 33 + category: 'utility', 34 + resource: 'class', 35 + resourceId: node.getPath().join('.'), 36 + tool: 'sdk', 37 + }; 38 + } 39 + 40 + function createFnSymbol( 41 + plugin: HeyApiSdkPlugin['Instance'], 42 + item: StructureItem & { data: OperationItem }, 43 + ): Symbol { 44 + const { operation, path, tags } = item.data; 45 + const name = item.location[item.location.length - 1]!; 46 + return plugin.symbol(applyNaming(name, plugin.config.operations.methodName), { 47 + meta: { 48 + category: 'sdk', 49 + path, 50 + resource: 'operation', 51 + resourceId: operation.id, 52 + tags, 53 + tool: 'sdk', 54 + }, 55 + }); 56 + } 57 + 58 + function childToNode( 59 + resource: StructureNode, 60 + plugin: HeyApiSdkPlugin['Instance'], 61 + ): ReadonlyArray<ReturnType<typeof $.func>> { 62 + const refChild = plugin.referenceSymbol(createShellMeta(resource)); 63 + const memberNameStr = toCase( 64 + refChild.name, 65 + plugin.config.operations.methodName.casing ?? 'camelCase', 66 + ); 67 + const memberName = plugin.symbol(memberNameStr); 68 + return [ 69 + $.func( 70 + memberName, 71 + (f) => f.returns('None'), 72 + // f.returns(refChild).do( 73 + // $('this') 74 + // .attr(privateName) 75 + // .nullishAssign( 76 + // $.new(refChild).args($.object().prop('client', $('this').attr('client'))), 77 + // ) 78 + // .return(), 79 + // ), 80 + ), 81 + ]; 82 + } 83 + 84 + export function createShell(plugin: HeyApiSdkPlugin['Instance']): StructureShell { 85 + return { 86 + define: (node) => { 87 + const symbol = plugin.symbol( 88 + applyNaming( 89 + node.name, 90 + node.isRoot 91 + ? plugin.config.operations.containerName 92 + : plugin.config.operations.segmentName, 93 + ), 94 + { 95 + meta: createShellMeta(node), 96 + }, 97 + ); 98 + 99 + const symbolClient = plugin.external('client.Client'); 100 + 101 + const c = $.class(symbol).extends(symbolClient); 102 + 103 + const dependencies: Array<ReturnType<typeof $.class>> = []; 104 + 105 + return { dependencies, node: c }; 106 + }, 107 + }; 108 + } 109 + 110 + function implementFn<T extends ReturnType<typeof $.func>>(args: { 111 + node: T; 112 + operation: IR.OperationObject; 113 + plugin: HeyApiSdkPlugin['Instance']; 114 + }): T { 115 + const { node, operation } = args; 116 + 117 + // Build the URL path 118 + const path = operation.path; 119 + 120 + // Get the HTTP method (default to GET) 121 + const method = operation.method?.toLowerCase() || 'get'; 122 + 123 + // Create the client call expression: self.client.get(path) 124 + // self is the Python equivalent of 'this' for instance methods 125 + const selfExpr = $.id('self'); 126 + const clientExpr = $.attr(selfExpr, 'client'); 127 + const methodExpr = $.attr(clientExpr, method); 128 + const clientCall = $.call(methodExpr, $.literal(path)); 129 + 130 + // Return the client call 131 + node.do($.return(clientCall)); 132 + 133 + return node; 134 + } 135 + 136 + export function toNode( 137 + model: StructureNode, 138 + plugin: HeyApiSdkPlugin['Instance'], 139 + ): { 140 + dependencies?: Array<ReturnType<typeof $.class | typeof $.func>>; 141 + nodes: ReadonlyArray<ReturnType<typeof $.class | typeof $.func>>; 142 + } { 143 + if (model.virtual) { 144 + const nodes: Array<ReturnType<typeof $.func>> = []; 145 + for (const item of model.itemsFrom<OperationItem>(source)) { 146 + const fnName = applyNaming( 147 + String(item.location[item.location.length - 1]), 148 + plugin.config.operations.methodName, 149 + ); 150 + const node = $.func(fnName); 151 + node.do($.return($.id('None'))); 152 + nodes.push(node); 153 + } 154 + return { nodes }; 155 + } 156 + 157 + if (!model.shell) { 158 + return { nodes: [] }; 159 + } 160 + 161 + const nodes: Array<ReturnType<typeof $.class | typeof $.func>> = []; 162 + const shell = model.shell.define(model); 163 + const node = shell.node as ReturnType<typeof $.class | typeof $.func>; 164 + 165 + let index = 0; 166 + for (const item of model.itemsFrom<OperationItem>(source)) { 167 + const { operation } = item.data; 168 + if (node['~dsl'] === 'FuncPyDsl') { 169 + // TODO: function? 170 + } else { 171 + if (index > 0 || node.hasBody) node.newline(); 172 + const method = implementFn({ 173 + node: $.func(createFnSymbol(plugin, item), (m) => 174 + attachComment({ 175 + node: m, 176 + operation, 177 + }), 178 + ), 179 + operation, 180 + plugin, 181 + }); 182 + node.do(method); 183 + // exampleIntent(method, operation, plugin); 184 + } 185 + index += 1; 186 + } 187 + 188 + for (const child of model.children.values()) { 189 + if (node['~dsl'] === 'FuncPyDsl') { 190 + // TODO: function? 191 + } else { 192 + if (node.hasBody) node.newline(); 193 + node.do(...childToNode(child, plugin)); 194 + } 195 + } 196 + 197 + nodes.push(node); 198 + 199 + return { 200 + dependencies: shell.dependencies as Array<ReturnType<typeof $.class | typeof $.func>>, 201 + nodes, 202 + }; 203 + }
+55 -2
packages/openapi-python/src/plugins/@hey-api/sdk/v1/plugin.ts
··· 1 + import { StructureModel } from '@hey-api/codegen-core'; 2 + 3 + import { getTypedConfig } from '../../../../config/utils'; 4 + import { clientFolderAbsolutePath } from '../../../../generate/client'; 5 + import type { $ } from '../../../../py-dsl'; 6 + import { getClientPlugin } from '../../client-core/utils'; 7 + import { resolveStrategy } from '../operations'; 1 8 import type { HeyApiSdkPlugin } from '../types'; 9 + import { createShell, type OperationItem, source, toNode } from './node'; 2 10 3 11 export const handlerV1: HeyApiSdkPlugin['Handler'] = ({ plugin }) => { 4 - console.log(plugin); 5 - // plugin.node() 12 + const clientModule = clientFolderAbsolutePath(getTypedConfig(plugin)); 13 + const client = getClientPlugin(getTypedConfig(plugin)); 14 + plugin.symbol('Client', { 15 + external: clientModule, 16 + meta: { 17 + category: 'external', 18 + resource: 'client.Client', 19 + tool: client.name, 20 + }, 21 + }); 22 + 23 + const structure = new StructureModel(); 24 + const shell = createShell(plugin); 25 + const strategy = resolveStrategy(plugin); 26 + 27 + plugin.forEach( 28 + 'operation', 29 + (event) => { 30 + structure.insert({ 31 + data: { 32 + operation: event.operation, 33 + path: event._path, 34 + tags: event.tags, 35 + } satisfies OperationItem, 36 + locations: strategy(event.operation).map((path) => ({ path, shell })), 37 + source, 38 + }); 39 + }, 40 + { order: 'declarations' }, 41 + ); 42 + 43 + const allDependencies: Array<ReturnType<typeof $.class | typeof $.func>> = []; 44 + const allNodes: Array<ReturnType<typeof $.class | typeof $.func>> = []; 45 + 46 + for (const node of structure.walk()) { 47 + const { dependencies, nodes } = toNode(node, plugin); 48 + allDependencies.push(...(dependencies ?? [])); 49 + allNodes.push(...nodes); 50 + } 51 + 52 + for (const dep of allDependencies) { 53 + plugin.node(dep); 54 + } 55 + 56 + for (const node of allNodes) { 57 + plugin.node(node); 58 + } 6 59 };
+2
packages/openapi-python/src/plugins/config.ts
··· 1 1 import type { Plugin, PluginConfigMap, PluginNames } from '@hey-api/shared'; 2 2 3 + import { defaultConfig as heyApiClientHttpx } from '../plugins/@hey-api/client-httpx'; 3 4 import { defaultConfig as heyApiSdk } from '../plugins/@hey-api/sdk'; 4 5 5 6 export const defaultPluginConfigs: { 6 7 [K in PluginNames]: Plugin.Config<PluginConfigMap[K]>; 7 8 } = { 9 + '@hey-api/client-httpx': heyApiClientHttpx, 8 10 '@hey-api/python-sdk': heyApiSdk, 9 11 };
+31
packages/openapi-python/src/plugins/shared/utils/operation.ts
··· 1 + import type { IR } from '@hey-api/shared'; 2 + import { escapeComment } from '@hey-api/shared'; 3 + 4 + export function createOperationComment( 5 + operation: IR.OperationObject, 6 + ): ReadonlyArray<string> | undefined { 7 + const comments: Array<string> = []; 8 + 9 + if (operation.summary) { 10 + comments.push(escapeComment(operation.summary)); 11 + } 12 + 13 + if (operation.description) { 14 + if (comments.length) { 15 + comments.push(''); // Add an empty line between summary and description 16 + } 17 + 18 + comments.push(escapeComment(operation.description)); 19 + } 20 + 21 + if (operation.deprecated) { 22 + if (comments.length) { 23 + comments.push(''); // Add an empty line before deprecated 24 + } 25 + 26 + // TODO: smarter deprecation message 27 + comments.push('Deprecated.'); 28 + } 29 + 30 + return comments.length ? comments : undefined; 31 + }
+2
packages/openapi-python/src/plugins/types.ts
··· 6 6 7 7 export type PluginMockNames = 'factory_boy' | 'faker' | 'mimesis'; 8 8 9 + export type PluginTransformerNames = never; 10 + 9 11 export type PluginValidatorNames = 'attrs' | 'dataclasses' | 'marshmallow' | 'pydantic';
+270
packages/openapi-python/src/py-dsl/base.ts
··· 1 + // TODO: symbol should be protected, but needs to be public to satisfy types 2 + import type { 3 + AnalysisContext, 4 + File, 5 + FromRef, 6 + Language, 7 + Node, 8 + NodeName, 9 + NodeNameSanitizer, 10 + NodeRelationship, 11 + NodeScope, 12 + Ref, 13 + Symbol, 14 + } from '@hey-api/codegen-core'; 15 + import { fromRef, isNode, isRef, isSymbol, nodeBrand, ref } from '@hey-api/codegen-core'; 16 + import type { AnyString } from '@hey-api/types'; 17 + 18 + import { py } from '../ts-python'; 19 + import type { AccessOptions } from './utils/context'; 20 + 21 + export abstract class PyDsl<T extends py.Node = py.Node> implements Node<T> { 22 + // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 + analyze(_: AnalysisContext): void {} 24 + clone(): this { 25 + const cloned = Object.create(Object.getPrototypeOf(this)); 26 + Object.assign(cloned, this); 27 + return cloned; 28 + } 29 + exported?: boolean; 30 + file?: File; 31 + get name(): Node['name'] { 32 + return { 33 + ...this._name, 34 + set: (value) => { 35 + this._name = ref(value); 36 + if (isSymbol(value)) { 37 + value.setNode(this); 38 + } 39 + }, 40 + toString: () => (this._name ? this.$name(this._name) : ''), 41 + } as Node['name']; 42 + } 43 + readonly nameSanitizer?: NodeNameSanitizer; 44 + language: Language = 'python'; 45 + parent?: Node; 46 + root: boolean = false; 47 + scope?: NodeScope = 'value'; 48 + structuralChildren?: Map<PyDsl, NodeRelationship>; 49 + structuralParents?: Map<PyDsl, NodeRelationship>; 50 + symbol?: Symbol; 51 + toAst(): T { 52 + return undefined as unknown as T; 53 + } 54 + readonly '~brand' = nodeBrand; 55 + 56 + /** Branding property to identify the DSL class at runtime. */ 57 + abstract readonly '~dsl': AnyString; 58 + 59 + /** Conditionally applies a callback to this builder. */ 60 + $if<T extends PyDsl, V, R extends PyDsl = T>( 61 + this: T, 62 + value: V, 63 + ifTrue: (self: T, v: Exclude<V, false | null | undefined>) => R | void, 64 + ifFalse?: (self: T, v: Extract<V, false | null | undefined>) => R | void, 65 + ): R | T; 66 + $if<T extends PyDsl, V, R extends PyDsl = T>( 67 + this: T, 68 + value: V, 69 + ifTrue: (v: Exclude<V, false | null | undefined>) => R | void, 70 + ifFalse?: (v: Extract<V, false | null | undefined>) => R | void, 71 + ): R | T; 72 + $if<T extends PyDsl, V, R extends PyDsl = T>( 73 + this: T, 74 + value: V, 75 + ifTrue: () => R | void, 76 + ifFalse?: () => R | void, 77 + ): R | T; 78 + $if<T extends PyDsl, V, R extends PyDsl = T>( 79 + this: T, 80 + value: V, 81 + ifTrue: any, 82 + ifFalse?: any, 83 + ): R | T { 84 + if (value) { 85 + // Try calling with (self, value), then (value), then () 86 + let result: R | void | undefined; 87 + try { 88 + result = ifTrue?.(this, value as Exclude<V, false | null | undefined>); 89 + } catch { 90 + // ignore and try other signatures 91 + } 92 + if (result === undefined) { 93 + try { 94 + result = ifTrue?.(value as Exclude<V, false | null | undefined>); 95 + } catch { 96 + // ignore and try zero-arg 97 + } 98 + } 99 + if (result === undefined) { 100 + try { 101 + result = ifTrue?.(); 102 + } catch { 103 + // swallow 104 + } 105 + } 106 + return (result ?? this) as R | T; 107 + } 108 + if (ifFalse) { 109 + let result: R | void | undefined; 110 + try { 111 + result = ifFalse?.(this, value as Extract<V, false | null | undefined>); 112 + } catch { 113 + // ignore 114 + } 115 + if (result === undefined) { 116 + try { 117 + result = ifFalse?.(value as Extract<V, false | null | undefined>); 118 + } catch { 119 + // ignore 120 + } 121 + } 122 + if (result === undefined) { 123 + try { 124 + result = ifFalse?.(); 125 + } catch { 126 + // ignore 127 + } 128 + } 129 + return (result ?? this) as R | T; 130 + } 131 + return this; 132 + } 133 + 134 + /** Access patterns for this node. */ 135 + toAccessNode?( 136 + node: this, 137 + options: AccessOptions, 138 + ctx: { 139 + /** The full chain. */ 140 + chain: ReadonlyArray<PyDsl>; 141 + /** Position in the chain (0 = root). */ 142 + index: number; 143 + /** Is this the leaf node? */ 144 + isLeaf: boolean; 145 + /** Is this the root node? */ 146 + isRoot: boolean; 147 + /** Total length of the chain. */ 148 + length: number; 149 + }, 150 + ): PyDsl | undefined; 151 + 152 + protected $maybeId<T extends string | py.Expression>( 153 + expr: T, 154 + ): T extends string ? py.Identifier : T { 155 + return (typeof expr === 'string' ? py.factory.createIdentifier(expr) : expr) as T extends string 156 + ? py.Identifier 157 + : T; 158 + } 159 + 160 + protected $name(name: Ref<NodeName>): string { 161 + const value = fromRef(name); 162 + if (isSymbol(value)) { 163 + try { 164 + return value.finalName; 165 + } catch { 166 + return value.name; 167 + } 168 + } 169 + return String(value); 170 + } 171 + 172 + protected $node<I>(value: I): NodeOfMaybe<I> { 173 + if (value === undefined) { 174 + return undefined as NodeOfMaybe<I>; 175 + } 176 + // @ts-expect-error 177 + if (isRef(value)) value = fromRef(value); 178 + if (isSymbol(value)) { 179 + return this.$maybeId(value.finalName) as NodeOfMaybe<I>; 180 + } 181 + if (typeof value === 'string') { 182 + return this.$maybeId(value) as NodeOfMaybe<I>; 183 + } 184 + if (value instanceof Array) { 185 + return value.map((item) => { 186 + if (isRef(item)) item = fromRef(item); 187 + return this.unwrap(item); 188 + }) as NodeOfMaybe<I>; 189 + } 190 + return this.unwrap(value as any) as NodeOfMaybe<I>; 191 + } 192 + 193 + // protected $type<I>(value: I, args?: ReadonlyArray<ts.TypeNode>): TypeOfMaybe<I> { 194 + // if (value === undefined) { 195 + // return undefined as TypeOfMaybe<I>; 196 + // } 197 + // // @ts-expect-error 198 + // if (isRef(value)) value = fromRef(value); 199 + // if (isSymbol(value)) { 200 + // return ts.factory.createTypeReferenceNode(value.finalName, args) as TypeOfMaybe<I>; 201 + // } 202 + // if (typeof value === 'string') { 203 + // return ts.factory.createTypeReferenceNode(value, args) as TypeOfMaybe<I>; 204 + // } 205 + // if (typeof value === 'boolean') { 206 + // const literal = value ? ts.factory.createTrue() : ts.factory.createFalse(); 207 + // return ts.factory.createLiteralTypeNode(literal) as TypeOfMaybe<I>; 208 + // } 209 + // if (typeof value === 'number') { 210 + // return ts.factory.createLiteralTypeNode( 211 + // ts.factory.createNumericLiteral(value), 212 + // ) as TypeOfMaybe<I>; 213 + // } 214 + // if (value instanceof Array) { 215 + // return value.map((item) => this.$type(item, args)) as TypeOfMaybe<I>; 216 + // } 217 + // return this.unwrap(value as any) as TypeOfMaybe<I>; 218 + // } 219 + 220 + private _name?: Ref<NodeName>; 221 + 222 + /** Unwraps nested nodes into raw Python AST. */ 223 + private unwrap<I>(value: I): I extends PyDsl<infer N> ? N : I { 224 + return (isNode(value) ? value.toAst() : value) as I extends PyDsl<infer N> ? N : I; 225 + } 226 + } 227 + 228 + type NodeOfMaybe<I> = undefined extends I 229 + ? NodeOf<NonNullable<FromRef<I>>> | undefined 230 + : NodeOf<FromRef<I>>; 231 + 232 + type NodeOf<I> = 233 + I extends ReadonlyArray<infer U> 234 + ? ReadonlyArray<U extends PyDsl<infer N> ? N : U> 235 + : I extends string 236 + ? py.Expression 237 + : I extends PyDsl<infer N> 238 + ? N 239 + : I extends py.Node 240 + ? I 241 + : never; 242 + 243 + export type MaybePyDsl<T> = 244 + T extends PyDsl<infer U> ? U | PyDsl<U> : T extends py.Node ? T | PyDsl<T> : never; 245 + 246 + // export abstract class TypePyDsl< 247 + // T extends 248 + // | ts.LiteralTypeNode 249 + // | ts.QualifiedName 250 + // | ts.TypeElement 251 + // | ts.TypeNode 252 + // | ts.TypeParameterDeclaration = ts.TypeNode, 253 + // > extends PyDsl<T> {} 254 + 255 + // type TypeOfMaybe<I> = undefined extends I 256 + // ? TypeOf<NonNullable<FromRef<I>>> | undefined 257 + // : TypeOf<FromRef<I>>; 258 + 259 + // type TypeOf<I> = 260 + // I extends ReadonlyArray<infer U> 261 + // ? ReadonlyArray<TypeOf<U>> 262 + // : I extends string 263 + // ? ts.TypeNode 264 + // : I extends boolean 265 + // ? ts.LiteralTypeNode 266 + // : I extends PyDsl<infer N> 267 + // ? N 268 + // : I extends ts.TypeNode 269 + // ? I 270 + // : never;
+118
packages/openapi-python/src/py-dsl/decl/class.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + import { isSymbol } from '@hey-api/codegen-core'; 3 + 4 + import { py } from '../../ts-python'; 5 + import { type MaybePyDsl, PyDsl } from '../base'; 6 + import { NewlinePyDsl } from '../layout/newline'; 7 + import { DecoratorMixin } from '../mixins/decorator'; 8 + import { DocMixin } from '../mixins/doc'; 9 + import { LayoutMixin } from '../mixins/layout'; 10 + import { safeRuntimeName } from '../utils/name'; 11 + 12 + type Body = Array<MaybePyDsl<py.Statement>>; 13 + 14 + const Mixed = DecoratorMixin(DocMixin(LayoutMixin(PyDsl<py.ClassDeclaration>))); 15 + 16 + export class ClassPyDsl extends Mixed { 17 + readonly '~dsl' = 'ClassPyDsl'; 18 + override readonly nameSanitizer = safeRuntimeName; 19 + 20 + protected baseClasses: Array<NodeName> = []; 21 + protected body: Body = []; 22 + 23 + constructor(name: NodeName) { 24 + super(); 25 + this.name.set(name); 26 + if (isSymbol(name)) { 27 + name.setKind('class'); 28 + } 29 + } 30 + 31 + override analyze(ctx: AnalysisContext): void { 32 + super.analyze(ctx); 33 + for (const baseClass of this.baseClasses) { 34 + ctx.analyze(baseClass); 35 + } 36 + ctx.analyze(this.name); 37 + ctx.pushScope(); 38 + try { 39 + for (const item of this.body) { 40 + ctx.analyze(item); 41 + } 42 + } finally { 43 + ctx.popScope(); 44 + } 45 + } 46 + 47 + /** Returns true when all required builder calls are present. */ 48 + get isValid(): boolean { 49 + return this.missingRequiredCalls().length === 0; 50 + } 51 + 52 + /** Returns true if the class has any members. */ 53 + get hasBody(): boolean { 54 + return this.body.length > 0; 55 + } 56 + 57 + /** Adds one or more class members (fields, methods, etc.). */ 58 + do(...items: Body): this { 59 + this.body.push(...items); 60 + return this; 61 + } 62 + 63 + /** Records base classes to extend from. */ 64 + extends(...baseClass: ReadonlyArray<NodeName>): this { 65 + this.baseClasses.push(...baseClass); 66 + return this; 67 + } 68 + 69 + /** Inserts an empty line between members for formatting. */ 70 + newline(): this { 71 + this.body.push(new NewlinePyDsl()); 72 + return this; 73 + } 74 + 75 + override toAst(): py.ClassDeclaration { 76 + this.$validate(); 77 + // const uniqueClasses: Array<py.Expression> = []; 78 + 79 + // for (const base of baseClass) { 80 + // let expr: py.Expression; 81 + // if (typeof base === 'string' || base instanceof PyDsl) { 82 + // expr = this.$node(base) as py.Expression; 83 + // } else if (isSymbol(base)) { 84 + // expr = py.factory.createIdentifier(base.finalName); 85 + // } 86 + 87 + // // Avoid duplicates by checking if already added 88 + // const exists = uniqueClasses.some((existing) => { 89 + // const existingExpr = this.$node(existing) as py.Expression; 90 + // return existingExpr === expr; 91 + // }); 92 + 93 + // if (!exists) { 94 + // uniqueClasses.push(expr); 95 + // } 96 + // } 97 + 98 + return py.factory.createClassDeclaration( 99 + this.name.toString(), 100 + this.$node(this.body) as ReadonlyArray<py.Statement>, 101 + this.$decorators(), 102 + this.baseClasses.map((c) => this.$node(c)), 103 + this.$docs(), 104 + ); 105 + } 106 + 107 + $validate(): asserts this { 108 + const missing = this.missingRequiredCalls(); 109 + if (missing.length === 0) return; 110 + throw new Error(`Class declaration missing ${missing.join(' and ')}`); 111 + } 112 + 113 + private missingRequiredCalls(): ReadonlyArray<string> { 114 + const missing: Array<string> = []; 115 + if (!this.name.toString()) missing.push('name'); 116 + return missing; 117 + } 118 + }
+89
packages/openapi-python/src/py-dsl/decl/func.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + import { isSymbol } from '@hey-api/codegen-core'; 3 + 4 + import { py } from '../../ts-python'; 5 + import { PyDsl } from '../base'; 6 + import { DecoratorMixin } from '../mixins/decorator'; 7 + import { DoMixin } from '../mixins/do'; 8 + import { DocMixin } from '../mixins/doc'; 9 + import { LayoutMixin } from '../mixins/layout'; 10 + import { AsyncMixin, ModifiersMixin } from '../mixins/modifiers'; 11 + import { safeRuntimeName } from '../utils/name'; 12 + 13 + const Mixed = DecoratorMixin( 14 + DocMixin(DoMixin(LayoutMixin(AsyncMixin(ModifiersMixin(PyDsl<py.FunctionDeclaration>))))), 15 + ); 16 + 17 + export class FuncPyDsl extends Mixed { 18 + readonly '~dsl' = 'FuncPyDsl'; 19 + override readonly nameSanitizer = safeRuntimeName; 20 + 21 + protected _parameters: Array<py.FunctionParameter> = []; 22 + protected _returnType?: py.Expression; 23 + 24 + constructor(name: NodeName, fn?: (f: FuncPyDsl) => void) { 25 + super(); 26 + this.name.set(name); 27 + if (isSymbol(name)) { 28 + name.setKind('function'); 29 + } 30 + fn?.(this); 31 + } 32 + 33 + override analyze(ctx: AnalysisContext): void { 34 + ctx.pushScope(); 35 + try { 36 + super.analyze(ctx); 37 + ctx.analyze(this.name); 38 + } finally { 39 + ctx.popScope(); 40 + } 41 + for (const param of this._parameters) { 42 + ctx.analyze(param); 43 + } 44 + ctx.analyze(this._returnType); 45 + } 46 + 47 + /** Returns true when all required builder calls are present. */ 48 + get isValid(): boolean { 49 + return this.missingRequiredCalls().length === 0; 50 + } 51 + 52 + param(name: string, configure?: (p: py.FunctionParameter) => void): this { 53 + const param = py.factory.createFunctionParameter(name, undefined, undefined, undefined); 54 + if (configure) configure(param); 55 + this._parameters.push(param); 56 + return this; 57 + } 58 + 59 + returns(returnType: string | py.Expression): this { 60 + this._returnType = 61 + typeof returnType === 'string' ? py.factory.createIdentifier(returnType) : returnType; 62 + return this; 63 + } 64 + 65 + override toAst(): py.FunctionDeclaration { 66 + this.$validate(); 67 + return py.factory.createFunctionDeclaration( 68 + this.name.toString(), 69 + this._parameters, 70 + this._returnType ? (this.$node(this._returnType) as py.Expression) : undefined, 71 + this.$do(), 72 + this.$decorators(), 73 + this.$docs(), 74 + this.modifiers, 75 + ); 76 + } 77 + 78 + $validate(): asserts this { 79 + const missing = this.missingRequiredCalls(); 80 + if (missing.length === 0) return; 81 + throw new Error(`Function declaration missing ${missing.join(' and ')}`); 82 + } 83 + 84 + private missingRequiredCalls(): ReadonlyArray<string> { 85 + const missing: Array<string> = []; 86 + if (!this.name.toString()) missing.push('name'); 87 + return missing; 88 + } 89 + }
+168
packages/openapi-python/src/py-dsl/expr/binary.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + 7 + export type PyBinaryOperator = 8 + | '+' 9 + | '-' 10 + | '*' 11 + | '/' 12 + | '//' 13 + | '%' 14 + | '**' 15 + | '==' 16 + | '!=' 17 + | '>' 18 + | '>=' 19 + | '<' 20 + | '<=' 21 + | 'is' 22 + | 'is not' 23 + | 'in' 24 + | 'not in' 25 + | 'and' 26 + | 'or'; 27 + 28 + const Mixed = PyDsl<py.BinaryExpression>; 29 + 30 + export class BinaryPyDsl extends Mixed { 31 + readonly '~dsl' = 'BinaryPyDsl'; 32 + 33 + protected _left?: MaybePyDsl<py.Expression>; 34 + protected _op?: PyBinaryOperator; 35 + protected _right?: MaybePyDsl<py.Expression>; 36 + 37 + constructor( 38 + left: MaybePyDsl<py.Expression>, 39 + op: PyBinaryOperator, 40 + right: MaybePyDsl<py.Expression>, 41 + ) { 42 + super(); 43 + this._left = left; 44 + this._op = op; 45 + this._right = right; 46 + } 47 + 48 + override analyze(ctx: AnalysisContext): void { 49 + super.analyze(ctx); 50 + ctx.analyze(this._left); 51 + ctx.analyze(this._right); 52 + } 53 + 54 + /** Returns true when all required builder calls are present. */ 55 + get isValid(): boolean { 56 + return this.missingRequiredCalls().length === 0; 57 + } 58 + 59 + and(right: MaybePyDsl<py.Expression>): this { 60 + return this.opAndExpr('and', right); 61 + } 62 + 63 + div(right: MaybePyDsl<py.Expression>): this { 64 + return this.opAndExpr('/', right); 65 + } 66 + 67 + eq(right: MaybePyDsl<py.Expression>): this { 68 + return this.opAndExpr('==', right); 69 + } 70 + 71 + floordiv(right: MaybePyDsl<py.Expression>): this { 72 + return this.opAndExpr('//', right); 73 + } 74 + 75 + gt(right: MaybePyDsl<py.Expression>): this { 76 + return this.opAndExpr('>', right); 77 + } 78 + 79 + gte(right: MaybePyDsl<py.Expression>): this { 80 + return this.opAndExpr('>=', right); 81 + } 82 + 83 + in_(right: MaybePyDsl<py.Expression>): this { 84 + return this.opAndExpr('in', right); 85 + } 86 + 87 + is(right: MaybePyDsl<py.Expression>): this { 88 + return this.opAndExpr('is', right); 89 + } 90 + 91 + isNot(right: MaybePyDsl<py.Expression>): this { 92 + return this.opAndExpr('is not', right); 93 + } 94 + 95 + lt(right: MaybePyDsl<py.Expression>): this { 96 + return this.opAndExpr('<', right); 97 + } 98 + 99 + lte(right: MaybePyDsl<py.Expression>): this { 100 + return this.opAndExpr('<=', right); 101 + } 102 + 103 + minus(right: MaybePyDsl<py.Expression>): this { 104 + return this.opAndExpr('-', right); 105 + } 106 + 107 + mod(right: MaybePyDsl<py.Expression>): this { 108 + return this.opAndExpr('%', right); 109 + } 110 + 111 + neq(right: MaybePyDsl<py.Expression>): this { 112 + return this.opAndExpr('!=', right); 113 + } 114 + 115 + notIn(right: MaybePyDsl<py.Expression>): this { 116 + return this.opAndExpr('not in', right); 117 + } 118 + 119 + or(right: MaybePyDsl<py.Expression>): this { 120 + return this.opAndExpr('or', right); 121 + } 122 + 123 + plus(right: MaybePyDsl<py.Expression>): this { 124 + return this.opAndExpr('+', right); 125 + } 126 + 127 + pow(right: MaybePyDsl<py.Expression>): this { 128 + return this.opAndExpr('**', right); 129 + } 130 + 131 + times(right: MaybePyDsl<py.Expression>): this { 132 + return this.opAndExpr('*', right); 133 + } 134 + 135 + override toAst(): py.BinaryExpression { 136 + this.$validate(); 137 + 138 + return py.factory.createBinaryExpression( 139 + this.$node(this._left!) as py.Expression, 140 + this._op!, 141 + this.$node(this._right!) as py.Expression, 142 + ); 143 + } 144 + 145 + $validate(): asserts this is this & { 146 + _left: MaybePyDsl<py.Expression>; 147 + _op: PyBinaryOperator; 148 + _right: MaybePyDsl<py.Expression>; 149 + } { 150 + const missing = this.missingRequiredCalls(); 151 + if (missing.length === 0) return; 152 + throw new Error(`Binary expression missing ${missing.join(' and ')}`); 153 + } 154 + 155 + private missingRequiredCalls(): ReadonlyArray<string> { 156 + const missing: Array<string> = []; 157 + if (!this._left) missing.push('left operand'); 158 + if (!this._op) missing.push('operator'); 159 + if (!this._right) missing.push('right operand'); 160 + return missing; 161 + } 162 + 163 + private opAndExpr(op: PyBinaryOperator, right: MaybePyDsl<py.Expression>): this { 164 + this._right = right; 165 + this._op = op; 166 + return this; 167 + } 168 + }
+69
packages/openapi-python/src/py-dsl/expr/call.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + 7 + export type CallArgs = ReadonlyArray<MaybePyDsl<py.Expression> | undefined>; 8 + 9 + const Mixed = PyDsl<py.CallExpression>; 10 + 11 + export class CallPyDsl extends Mixed { 12 + readonly '~dsl' = 'CallPyDsl'; 13 + 14 + protected _args: Array<MaybePyDsl<py.Expression> | undefined> = []; 15 + protected _callee?: MaybePyDsl<py.Expression>; 16 + 17 + constructor(callee: MaybePyDsl<py.Expression>, ...args: CallArgs) { 18 + super(); 19 + this._callee = callee; 20 + this._args = [...args]; 21 + } 22 + 23 + override analyze(ctx: AnalysisContext): void { 24 + super.analyze(ctx); 25 + ctx.analyze(this._callee); 26 + for (const arg of this._args) { 27 + if (arg) ctx.analyze(arg); 28 + } 29 + } 30 + 31 + /** Returns true when all required builder calls are present. */ 32 + get isValid(): boolean { 33 + return this.missingRequiredCalls().length === 0; 34 + } 35 + 36 + arg(arg: MaybePyDsl<py.Expression>): this { 37 + this._args.push(arg); 38 + return this; 39 + } 40 + 41 + args(...args: CallArgs): this { 42 + this._args.push(...args); 43 + return this; 44 + } 45 + 46 + override toAst(): py.CallExpression { 47 + this.$validate(); 48 + 49 + const astArgs = this._args 50 + .filter((a): a is MaybePyDsl<py.Expression> => a !== undefined) 51 + .map((arg) => this.$node(arg) as py.Expression); 52 + 53 + return py.factory.createCallExpression(this.$node(this._callee!) as py.Expression, astArgs); 54 + } 55 + 56 + $validate(): asserts this is this & { 57 + _callee: MaybePyDsl<py.Expression>; 58 + } { 59 + const missing = this.missingRequiredCalls(); 60 + if (missing.length === 0) return; 61 + throw new Error(`Call expression missing ${missing.join(' and ')}`); 62 + } 63 + 64 + private missingRequiredCalls(): ReadonlyArray<string> { 65 + const missing: Array<string> = []; 66 + if (!this._callee) missing.push('callee'); 67 + return missing; 68 + } 69 + }
+52
packages/openapi-python/src/py-dsl/expr/dict.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import { LayoutMixin } from '../mixins/layout'; 7 + 8 + const Mixed = LayoutMixin(PyDsl<py.DictExpression>); 9 + 10 + export class DictPyDsl extends Mixed { 11 + readonly '~dsl' = 'DictPyDsl'; 12 + 13 + protected _entries: Array<{ 14 + key: MaybePyDsl<py.Expression>; 15 + value: MaybePyDsl<py.Expression>; 16 + }> = []; 17 + 18 + constructor( 19 + ...entries: Array<{ key: MaybePyDsl<py.Expression>; value: MaybePyDsl<py.Expression> }> 20 + ) { 21 + super(); 22 + this._entries = entries; 23 + } 24 + 25 + override analyze(ctx: AnalysisContext): void { 26 + super.analyze(ctx); 27 + for (const entry of this._entries) { 28 + ctx.analyze(entry.key); 29 + ctx.analyze(entry.value); 30 + } 31 + } 32 + 33 + entry(key: MaybePyDsl<py.Expression>, value: MaybePyDsl<py.Expression>): this { 34 + this._entries.push({ key, value }); 35 + return this; 36 + } 37 + 38 + entries( 39 + ...entries: ReadonlyArray<{ key: MaybePyDsl<py.Expression>; value: MaybePyDsl<py.Expression> }> 40 + ): this { 41 + this._entries.push(...entries); 42 + return this; 43 + } 44 + 45 + override toAst(): py.DictExpression { 46 + const astEntries = this._entries.map((entry) => ({ 47 + key: this.$node(entry.key) as py.Expression, 48 + value: this.$node(entry.value) as py.Expression, 49 + })); 50 + return py.factory.createDictExpression(astEntries); 51 + } 52 + }
+40
packages/openapi-python/src/py-dsl/expr/expr.ts
··· 1 + import type { AnalysisContext, NodeName, Ref } from '@hey-api/codegen-core'; 2 + import { isNode, isSymbol, ref } from '@hey-api/codegen-core'; 3 + 4 + import type { py } from '../../ts-python'; 5 + import type { MaybePyDsl } from '../base'; 6 + import { PyDsl } from '../base'; 7 + // import { AsMixin } from '../mixins/as'; 8 + // import { ExprMixin } from '../mixins/expr'; 9 + // import { OperatorMixin } from '../mixins/operator'; 10 + // import { TypeExprMixin } from '../mixins/type-expr'; 11 + 12 + type Id = NodeName | MaybePyDsl<py.Expression>; 13 + 14 + const Mixed = PyDsl<py.Expression>; 15 + // const Mixed = AsMixin(ExprMixin(OperatorMixin(TypeExprMixin(PyDsl<PyExpression>)))); 16 + 17 + export class ExprPyDsl extends Mixed { 18 + readonly '~dsl' = 'ExprPyDsl'; 19 + 20 + protected _exprInput: Ref<Id>; 21 + 22 + constructor(id: Id) { 23 + super(); 24 + this._exprInput = ref(id); 25 + if (typeof id === 'string' || isSymbol(id)) { 26 + this.name.set(id); 27 + } else if (isNode(id)) { 28 + this.name.set(id.name); 29 + } 30 + } 31 + 32 + override analyze(ctx: AnalysisContext): void { 33 + super.analyze(ctx); 34 + ctx.analyze(this._exprInput); 35 + } 36 + 37 + override toAst() { 38 + return this.$node(this._exprInput); 39 + } 40 + }
+24
packages/openapi-python/src/py-dsl/expr/identifier.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + const Mixed = PyDsl<py.Identifier>; 7 + 8 + export class IdPyDsl extends Mixed { 9 + readonly '~dsl' = 'IdPyDsl'; 10 + 11 + constructor(name: NodeName) { 12 + super(); 13 + this.name.set(name); 14 + } 15 + 16 + override analyze(ctx: AnalysisContext): void { 17 + super.analyze(ctx); 18 + ctx.analyze(this.name); 19 + } 20 + 21 + override toAst(): py.Identifier { 22 + return py.factory.createIdentifier(this.name.toString()); 23 + } 24 + }
+41
packages/openapi-python/src/py-dsl/expr/list.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import { LayoutMixin } from '../mixins/layout'; 7 + 8 + const Mixed = LayoutMixin(PyDsl<py.ListExpression>); 9 + 10 + export class ListPyDsl extends Mixed { 11 + readonly '~dsl' = 'ListPyDsl'; 12 + 13 + protected _elements: Array<MaybePyDsl<py.Expression>> = []; 14 + 15 + constructor(...elements: Array<MaybePyDsl<py.Expression>>) { 16 + super(); 17 + this._elements = elements; 18 + } 19 + 20 + override analyze(ctx: AnalysisContext): void { 21 + super.analyze(ctx); 22 + for (const el of this._elements) { 23 + ctx.analyze(el); 24 + } 25 + } 26 + 27 + element(expr: MaybePyDsl<py.Expression>): this { 28 + this._elements.push(expr); 29 + return this; 30 + } 31 + 32 + elements(...exprs: ReadonlyArray<MaybePyDsl<py.Expression>>): this { 33 + this._elements.push(...exprs); 34 + return this; 35 + } 36 + 37 + override toAst(): py.ListExpression { 38 + const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 39 + return py.factory.createListExpression(astElements); 40 + } 41 + }
+27
packages/openapi-python/src/py-dsl/expr/literal.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + export type LiteralValue = string | number | boolean | null; 7 + 8 + const Mixed = PyDsl<py.Literal>; 9 + 10 + export class LiteralPyDsl extends Mixed { 11 + readonly '~dsl' = 'LiteralPyDsl'; 12 + 13 + protected value: LiteralValue; 14 + 15 + constructor(value: LiteralValue) { 16 + super(); 17 + this.value = value; 18 + } 19 + 20 + override analyze(_ctx: AnalysisContext): void { 21 + super.analyze(_ctx); 22 + } 23 + 24 + override toAst(): py.Literal { 25 + return py.factory.createLiteral(this.value); 26 + } 27 + }
+59
packages/openapi-python/src/py-dsl/expr/member.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + 7 + const Mixed = PyDsl<py.MemberExpression>; 8 + 9 + export class AttrPyDsl extends Mixed { 10 + readonly '~dsl' = 'AttrPyDsl'; 11 + 12 + protected object?: MaybePyDsl<py.Expression>; 13 + protected prop?: NodeName; 14 + 15 + constructor(object: MaybePyDsl<py.Expression>, prop: NodeName) { 16 + super(); 17 + this.object = object; 18 + this.name.set(prop); 19 + } 20 + 21 + static attr(object: MaybePyDsl<py.Expression>, prop: NodeName): AttrPyDsl { 22 + return new AttrPyDsl(object, prop); 23 + } 24 + 25 + override analyze(ctx: AnalysisContext): void { 26 + super.analyze(ctx); 27 + ctx.analyze(this.object); 28 + ctx.analyze(this.name); 29 + } 30 + 31 + /** Returns true when all required builder calls are present. */ 32 + get isValid(): boolean { 33 + return this.missingRequiredCalls().length === 0; 34 + } 35 + 36 + override toAst(): py.MemberExpression { 37 + this.$validate(); 38 + 39 + return py.factory.createMemberExpression( 40 + this.$node(this.object!) as py.Expression, 41 + py.factory.createIdentifier(this.name.toString()), 42 + ); 43 + } 44 + 45 + $validate(): asserts this is this & { 46 + object: MaybePyDsl<py.Expression>; 47 + } { 48 + const missing = this.missingRequiredCalls(); 49 + if (missing.length === 0) return; 50 + throw new Error(`Attribute access missing ${missing.join(' and ')}`); 51 + } 52 + 53 + private missingRequiredCalls(): ReadonlyArray<string> { 54 + const missing: Array<string> = []; 55 + if (!this.object) missing.push('object'); 56 + if (!this.name.toString()) missing.push('property name'); 57 + return missing; 58 + } 59 + }
+41
packages/openapi-python/src/py-dsl/expr/set.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import { LayoutMixin } from '../mixins/layout'; 7 + 8 + const Mixed = LayoutMixin(PyDsl<py.SetExpression>); 9 + 10 + export class SetPyDsl extends Mixed { 11 + readonly '~dsl' = 'SetPyDsl'; 12 + 13 + protected _elements: Array<MaybePyDsl<py.Expression>> = []; 14 + 15 + constructor(...elements: Array<MaybePyDsl<py.Expression>>) { 16 + super(); 17 + this._elements = elements; 18 + } 19 + 20 + override analyze(ctx: AnalysisContext): void { 21 + super.analyze(ctx); 22 + for (const el of this._elements) { 23 + ctx.analyze(el); 24 + } 25 + } 26 + 27 + element(expr: MaybePyDsl<py.Expression>): this { 28 + this._elements.push(expr); 29 + return this; 30 + } 31 + 32 + elements(...exprs: ReadonlyArray<MaybePyDsl<py.Expression>>): this { 33 + this._elements.push(...exprs); 34 + return this; 35 + } 36 + 37 + override toAst(): py.SetExpression { 38 + const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 39 + return py.factory.createSetExpression(astElements); 40 + } 41 + }
+41
packages/openapi-python/src/py-dsl/expr/tuple.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import { LayoutMixin } from '../mixins/layout'; 7 + 8 + const Mixed = LayoutMixin(PyDsl<py.TupleExpression>); 9 + 10 + export class TuplePyDsl extends Mixed { 11 + readonly '~dsl' = 'TuplePyDsl'; 12 + 13 + protected _elements: Array<MaybePyDsl<py.Expression>> = []; 14 + 15 + constructor(...elements: Array<MaybePyDsl<py.Expression>>) { 16 + super(); 17 + this._elements = elements; 18 + } 19 + 20 + override analyze(ctx: AnalysisContext): void { 21 + super.analyze(ctx); 22 + for (const el of this._elements) { 23 + ctx.analyze(el); 24 + } 25 + } 26 + 27 + element(expr: MaybePyDsl<py.Expression>): this { 28 + this._elements.push(expr); 29 + return this; 30 + } 31 + 32 + elements(...exprs: ReadonlyArray<MaybePyDsl<py.Expression>>): this { 33 + this._elements.push(...exprs); 34 + return this; 35 + } 36 + 37 + override toAst(): py.TupleExpression { 38 + const astElements = this._elements.map((el) => this.$node(el) as py.Expression); 39 + return py.factory.createTupleExpression(astElements); 40 + } 41 + }
+320
packages/openapi-python/src/py-dsl/index.ts
··· 1 + import type { NodeName } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../ts-python'; 4 + import { ClassPyDsl } from './decl/class'; 5 + // import { DecoratorPyDsl } from './decl/decorator'; 6 + // import { EnumPyDsl } from './decl/enum'; 7 + // import { FieldPyDsl } from './decl/field'; 8 + import { FuncPyDsl } from './decl/func'; 9 + // import { GetterPyDsl } from './decl/getter'; 10 + // import { InitPyDsl } from './decl/init'; 11 + // import { EnumMemberPyDsl } from './decl/member'; 12 + // import { MethodPyDsl } from './decl/method'; 13 + // import { ParamPyDsl } from './decl/param'; 14 + // import { PatternPyDsl } from './decl/pattern'; 15 + // import { SetterPyDsl } from './decl/setter'; 16 + // import { ArrayPyDsl } from './expr/array'; 17 + // import { AsPyDsl } from './expr/as'; 18 + // import { AwaitPyDsl } from './expr/await'; 19 + import { BinaryPyDsl } from './expr/binary'; 20 + import { CallPyDsl } from './expr/call'; 21 + import { DictPyDsl } from './expr/dict'; 22 + import { ExprPyDsl } from './expr/expr'; 23 + // import { fromValue as exprValue } from './expr/fromValue'; 24 + import { IdPyDsl } from './expr/identifier'; 25 + import { ListPyDsl } from './expr/list'; 26 + import { LiteralPyDsl } from './expr/literal'; 27 + import { AttrPyDsl } from './expr/member'; 28 + // import { NewPyDsl } from './expr/new'; 29 + // import { ObjectPyDsl } from './expr/object'; 30 + // import { PrefixPyDsl } from './expr/prefix'; 31 + // import { ObjectPropPyDsl } from './expr/prop'; 32 + // import { RegExpPyDsl } from './expr/regexp'; 33 + import { SetPyDsl } from './expr/set'; 34 + // import { TemplatePyDsl } from './expr/template'; 35 + // import { TernaryPyDsl } from './expr/ternary'; 36 + import { TuplePyDsl } from './expr/tuple'; 37 + // import { TypeOfExprPyDsl } from './expr/typeof'; 38 + import { DocPyDsl } from './layout/doc'; 39 + import { HintPyDsl } from './layout/hint'; 40 + import { NewlinePyDsl } from './layout/newline'; 41 + import { BlockPyDsl } from './stmt/block'; 42 + import { BreakPyDsl } from './stmt/break'; 43 + import { ContinuePyDsl } from './stmt/continue'; 44 + import { ForPyDsl } from './stmt/for'; 45 + import { IfPyDsl } from './stmt/if'; 46 + import { ImportPyDsl } from './stmt/import'; 47 + import { RaisePyDsl } from './stmt/raise'; 48 + import { ReturnPyDsl } from './stmt/return'; 49 + import { StmtPyDsl } from './stmt/stmt'; 50 + import { TryPyDsl } from './stmt/try'; 51 + import { VarPyDsl } from './stmt/var'; 52 + import { WhilePyDsl } from './stmt/while'; 53 + import { WithPyDsl } from './stmt/with'; 54 + // import { TokenPyDsl } from './token'; 55 + // import { TypeAliasPyDsl } from './type/alias'; 56 + // import { TypeAndPyDsl } from './type/and'; 57 + // import { TypeAttrPyDsl } from './type/attr'; 58 + // import { TypeExprPyDsl } from './type/expr'; 59 + // import { fromValue as typeValue } from './type/fromValue'; 60 + // import { TypeFuncPyDsl } from './type/func'; 61 + // import { TypeIdxPyDsl } from './type/idx'; 62 + // import { TypeLiteralPyDsl } from './type/literal'; 63 + // import { TypeMappedPyDsl } from './type/mapped'; 64 + // import { TypeObjectPyDsl } from './type/object'; 65 + // import { TypeOperatorPyDsl } from './type/operator'; 66 + // import { TypeOrPyDsl } from './type/or'; 67 + // import { TypeParamPyDsl } from './type/param'; 68 + // import { TypeQueryPyDsl } from './type/query'; 69 + // import { TypeTemplatePyDsl } from './type/template'; 70 + // import { TypeTuplePyDsl } from './type/tuple'; 71 + import { LazyPyDsl } from './utils/lazy'; 72 + 73 + const pyDsl = { 74 + /** Creates an array literal expression (e.g. `[1, 2, 3]`). */ 75 + // array: (...args: ConstructorParameters<typeof ArrayTsDsl>) => new ArrayTsDsl(...args), 76 + /** Creates an `as` type assertion expression (e.g. `value as Type`). */ 77 + // as: (...args: ConstructorParameters<typeof AsTsDsl>) => new AsTsDsl(...args), 78 + 79 + /** Creates a property access expression (e.g. `obj.foo`). */ 80 + attr: (...args: ConstructorParameters<typeof AttrPyDsl>) => new AttrPyDsl(...args), 81 + 82 + /** Creates an await expression (e.g. `await promise`). */ 83 + // await: (...args: ConstructorParameters<typeof AwaitTsDsl>) => new AwaitTsDsl(...args), 84 + 85 + /** Creates a binary expression (e.g. `a + b`). */ 86 + binary: (...args: ConstructorParameters<typeof BinaryPyDsl>) => new BinaryPyDsl(...args), 87 + 88 + /** Creates a statement block. */ 89 + block: (...args: ConstructorParameters<typeof BlockPyDsl>) => new BlockPyDsl(...args), 90 + 91 + /** Creates a break statement. */ 92 + break: (...args: ConstructorParameters<typeof BreakPyDsl>) => new BreakPyDsl(...args), 93 + 94 + /** Creates a function or method call expression (e.g. `fn(arg)`). */ 95 + call: (...args: ConstructorParameters<typeof CallPyDsl>) => new CallPyDsl(...args), 96 + 97 + /** Creates a class declaration or expression. */ 98 + class: (...args: ConstructorParameters<typeof ClassPyDsl>) => new ClassPyDsl(...args), 99 + 100 + /** Creates a continue statement. */ 101 + continue: (...args: ConstructorParameters<typeof ContinuePyDsl>) => new ContinuePyDsl(...args), 102 + 103 + /** Creates a decorator expression (e.g. `@decorator`). */ 104 + // decorator: (...args: ConstructorParameters<typeof DecoratorTsDsl>) => new DecoratorTsDsl(...args), 105 + 106 + /** Creates a dictionary expression (e.g. `{ 'a': 1 }`). */ 107 + dict: (...args: ConstructorParameters<typeof DictPyDsl>) => new DictPyDsl(...args), 108 + 109 + /** Creates a Python docstring (`"""..."""`). */ 110 + doc: (...args: ConstructorParameters<typeof DocPyDsl>) => new DocPyDsl(...args), 111 + 112 + /** Creates an enum declaration. */ 113 + // enum: (...args: ConstructorParameters<typeof EnumTsDsl>) => new EnumTsDsl(...args), 114 + 115 + /** Creates a general expression node. */ 116 + expr: (...args: ConstructorParameters<typeof ExprPyDsl>) => new ExprPyDsl(...args), 117 + 118 + /** Creates a field declaration in a class or object. */ 119 + // field: (...args: ConstructorParameters<typeof FieldTsDsl>) => new FieldTsDsl(...args), 120 + 121 + /** Creates a for statement (e.g. `for x in items:`). */ 122 + for: (...args: ConstructorParameters<typeof ForPyDsl>) => new ForPyDsl(...args), 123 + 124 + /** Converts a runtime value into a corresponding expression node. */ 125 + // fromValue: (...args: Parameters<typeof exprValue>) => exprValue(...args), 126 + 127 + /** Creates a function declaration. */ 128 + func: ((name: NodeName, fn?: (f: FuncPyDsl) => void) => new FuncPyDsl(name, fn)) as { 129 + (name: NodeName): FuncPyDsl; 130 + (name: NodeName, fn: (f: FuncPyDsl) => void): FuncPyDsl; 131 + }, 132 + 133 + /** Creates a getter method declaration. */ 134 + // getter: (...args: ConstructorParameters<typeof GetterTsDsl>) => new GetterTsDsl(...args), 135 + 136 + /** Creates a Python comment (`# ...`). */ 137 + hint: (...args: ConstructorParameters<typeof HintPyDsl>) => new HintPyDsl(...args), 138 + 139 + /** Creates an identifier (e.g. `foo`). */ 140 + id: (...args: ConstructorParameters<typeof IdPyDsl>) => new IdPyDsl(...args), 141 + 142 + /** Creates an if statement. */ 143 + if: (...args: ConstructorParameters<typeof IfPyDsl>) => new IfPyDsl(...args), 144 + 145 + /** Creates an import statement. */ 146 + import: (...args: ConstructorParameters<typeof ImportPyDsl>) => new ImportPyDsl(...args), 147 + 148 + /** Creates an initialization block or statement. */ 149 + // init: (...args: ConstructorParameters<typeof InitTsDsl>) => new InitTsDsl(...args), 150 + 151 + /** Creates a lazy, context-aware node with deferred evaluation. */ 152 + lazy: <T extends py.Node>(...args: ConstructorParameters<typeof LazyPyDsl<T>>) => 153 + new LazyPyDsl<T>(...args), 154 + 155 + /** Creates a list expression (e.g. `[1, 2, 3]`). */ 156 + list: (...args: ConstructorParameters<typeof ListPyDsl>) => new ListPyDsl(...args), 157 + 158 + /** Creates a literal value (e.g. string, number, boolean). */ 159 + literal: (...args: ConstructorParameters<typeof LiteralPyDsl>) => new LiteralPyDsl(...args), 160 + 161 + /** Creates an enum member declaration. */ 162 + // member: (...args: ConstructorParameters<typeof EnumMemberTsDsl>) => new EnumMemberTsDsl(...args), 163 + 164 + /** Creates a method declaration inside a class or object. */ 165 + // method: (...args: ConstructorParameters<typeof MethodTsDsl>) => new MethodTsDsl(...args), 166 + 167 + /** Creates a negation expression (`-x`). */ 168 + // neg: (...args: ConstructorParameters<typeof PrefixTsDsl>) => new PrefixTsDsl(...args).neg(), 169 + 170 + /** Creates a new expression (e.g. `new ClassName()`). */ 171 + // new: (...args: ConstructorParameters<typeof NewTsDsl>) => new NewTsDsl(...args), 172 + 173 + /** Creates a newline (for formatting purposes). */ 174 + newline: (...args: ConstructorParameters<typeof NewlinePyDsl>) => new NewlinePyDsl(...args), 175 + 176 + /** Creates a logical NOT expression (`!x`). */ 177 + // not: (...args: ConstructorParameters<typeof PrefixTsDsl>) => new PrefixTsDsl(...args).not(), 178 + 179 + /** Creates an object literal expression. */ 180 + // object: (...args: ConstructorParameters<typeof ObjectTsDsl>) => new ObjectTsDsl(...args), 181 + 182 + /** Creates a parameter declaration for functions or methods. */ 183 + // param: (...args: ConstructorParameters<typeof ParamTsDsl>) => new ParamTsDsl(...args), 184 + 185 + /** Creates a pattern for destructuring or matching. */ 186 + // pattern: (...args: ConstructorParameters<typeof PatternTsDsl>) => new PatternTsDsl(...args), 187 + 188 + /** Creates a prefix unary expression (e.g. `-x`, `!x`, `~x`). */ 189 + // prefix: (...args: ConstructorParameters<typeof PrefixTsDsl>) => new PrefixTsDsl(...args), 190 + 191 + /** Creates an object literal property (e.g. `{ foo: bar }`). */ 192 + // prop: (...args: ConstructorParameters<typeof ObjectPropTsDsl>) => new ObjectPropTsDsl(...args), 193 + 194 + /** Creates a raise statement. */ 195 + raise: (...args: ConstructorParameters<typeof RaisePyDsl>) => new RaisePyDsl(...args), 196 + 197 + /** Creates a regular expression literal (e.g. `/foo/gi`). */ 198 + // regexp: (...args: ConstructorParameters<typeof RegExpTsDsl>) => new RegExpTsDsl(...args), 199 + 200 + /** Creates a return statement. */ 201 + return: (...args: ConstructorParameters<typeof ReturnPyDsl>) => new ReturnPyDsl(...args), 202 + 203 + /** Creates a set expression (e.g. `{1, 2, 3}`). */ 204 + set: (...args: ConstructorParameters<typeof SetPyDsl>) => new SetPyDsl(...args), 205 + 206 + /** Creates a setter method declaration. */ 207 + // setter: (...args: ConstructorParameters<typeof SetterTsDsl>) => new SetterTsDsl(...args), 208 + 209 + /** Wraps an expression or statement-like value into a `StmtPyDsl`. */ 210 + stmt: (...args: ConstructorParameters<typeof StmtPyDsl>) => new StmtPyDsl(...args), 211 + 212 + /** Creates a template literal expression. */ 213 + // template: (...args: ConstructorParameters<typeof TemplateTsDsl>) => new TemplateTsDsl(...args), 214 + 215 + /** Creates a ternary conditional expression (if ? then : else). */ 216 + // ternary: (...args: ConstructorParameters<typeof TernaryTsDsl>) => new TernaryTsDsl(...args), 217 + 218 + // /** Creates a throw statement. */ 219 + // throw: (...args: ConstructorParameters<typeof ThrowTsDsl>) => new ThrowTsDsl(...args), 220 + 221 + /** Creates a syntax token (e.g. `?`, `readonly`, `+`, `-`). */ 222 + // token: (...args: ConstructorParameters<typeof TokenTsDsl>) => new TokenTsDsl(...args), 223 + 224 + /** Creates a try/except/finally statement. */ 225 + try: (...args: ConstructorParameters<typeof TryPyDsl>) => new TryPyDsl(...args), 226 + 227 + /** Creates a tuple expression (e.g. `(1, 2, 3)`). */ 228 + tuple: (...args: ConstructorParameters<typeof TuplePyDsl>) => new TuplePyDsl(...args), 229 + 230 + /** Creates a basic type reference or type expression (e.g. Foo or Foo<T>). */ 231 + // type: Object.assign( 232 + // (...args: ConstructorParameters<typeof TypeExprTsDsl>) => new TypeExprTsDsl(...args), 233 + // { 234 + /** Creates a type alias declaration (e.g. `type Foo = Bar`). */ 235 + // alias: (...args: ConstructorParameters<typeof TypeAliasTsDsl>) => new TypeAliasTsDsl(...args), 236 + /** Creates an intersection type (e.g. `A & B`). */ 237 + // and: (...args: ConstructorParameters<typeof TypeAndTsDsl>) => new TypeAndTsDsl(...args), 238 + /** Creates a qualified type reference (e.g. Foo.Bar). */ 239 + // attr: (...args: ConstructorParameters<typeof TypeAttrTsDsl>) => new TypeAttrTsDsl(...args), 240 + /** Creates a basic type reference or type expression (e.g. Foo or Foo<T>). */ 241 + // expr: (...args: ConstructorParameters<typeof TypeExprTsDsl>) => new TypeExprTsDsl(...args), 242 + /** Converts a runtime value into a corresponding type expression node. */ 243 + // fromValue: (...args: Parameters<typeof typeValue>) => typeValue(...args), 244 + /** Creates a function type node (e.g. `(a: string) => number`). */ 245 + // func: (...args: ConstructorParameters<typeof TypeFuncTsDsl>) => new TypeFuncTsDsl(...args), 246 + /** Creates an indexed-access type (e.g. `Foo<T>[K]`). */ 247 + // idx: (...args: ConstructorParameters<typeof TypeIdxTsDsl>) => new TypeIdxTsDsl(...args), 248 + /** Creates a literal type node (e.g. 'foo', 42, or true). */ 249 + // literal: (...args: ConstructorParameters<typeof TypeLiteralTsDsl>) => 250 + // new TypeLiteralTsDsl(...args), 251 + /** Creates a mapped type (e.g. `{ [K in keyof T]: U }`). */ 252 + // mapped: (...args: ConstructorParameters<typeof TypeMappedTsDsl>) => 253 + // new TypeMappedTsDsl(...args), 254 + /** Creates a type literal node (e.g. { foo: string }). */ 255 + // object: (...args: ConstructorParameters<typeof TypeObjectTsDsl>) => 256 + // new TypeObjectTsDsl(...args), 257 + /** Creates a type operator node (e.g. `readonly T`, `keyof T`, `unique T`). */ 258 + // operator: (...args: ConstructorParameters<typeof TypeOperatorTsDsl>) => 259 + // new TypeOperatorTsDsl(...args), 260 + /** Represents a union type (e.g. `A | B | C`). */ 261 + // or: (...args: ConstructorParameters<typeof TypeOrTsDsl>) => new TypeOrTsDsl(...args), 262 + /** Creates a type parameter (e.g. `<T>`). */ 263 + // param: (...args: ConstructorParameters<typeof TypeParamTsDsl>) => new TypeParamTsDsl(...args), 264 + /** Creates a type query node (e.g. `typeof Foo`). */ 265 + // query: (...args: ConstructorParameters<typeof TypeQueryTsDsl>) => new TypeQueryTsDsl(...args), 266 + /** Builds a TypeScript template literal *type* (e.g. `${Foo}-${Bar}` as a type). */ 267 + // template: (...args: ConstructorParameters<typeof TypeTemplateTsDsl>) => 268 + // new TypeTemplateTsDsl(...args), 269 + /** Creates a tuple type (e.g. [A, B, C]). */ 270 + // tuple: (...args: ConstructorParameters<typeof TypeTupleTsDsl>) => new TypeTupleTsDsl(...args), 271 + // }, 272 + // ), 273 + /** Creates a `typeof` expression (e.g. `typeof value`). */ 274 + // typeofExpr: (...args: ConstructorParameters<typeof TypeOfExprTsDsl>) => 275 + // new TypeOfExprTsDsl(...args), 276 + 277 + /** Creates a variable assignment. */ 278 + var: (...args: ConstructorParameters<typeof VarPyDsl>) => new VarPyDsl(...args), 279 + 280 + /** Creates a while statement (e.g. `while x:`). */ 281 + while: (...args: ConstructorParameters<typeof WhilePyDsl>) => new WhilePyDsl(...args), 282 + 283 + /** Creates a with statement (e.g. `with open(f) as file:`). */ 284 + with: (...args: ConstructorParameters<typeof WithPyDsl>) => new WithPyDsl(...args), 285 + }; 286 + 287 + export const $ = Object.assign( 288 + (...args: ConstructorParameters<typeof ExprPyDsl>) => new ExprPyDsl(...args), 289 + pyDsl, 290 + ); 291 + 292 + export type DollarPyDsl = { 293 + /** 294 + * Entry point to the Python DSL. 295 + * 296 + * `$` creates a general expression node by default, but also exposes 297 + * builders for all other constructs. 298 + * 299 + * Example: 300 + * ```ts 301 + * const node = $('console').attr('log').call($.literal('Hello')); 302 + * ``` 303 + * 304 + * Returns: 305 + * - A new `ExprPyDsl` instance when called directly. 306 + * - The `pyDsl` object for constructing more specific nodes. 307 + */ 308 + $: typeof $; 309 + }; 310 + 311 + export type { MaybePyDsl } from './base'; 312 + // export type { MaybePyDsl, TypePyDsl } from './base'; 313 + export { PyDsl } from './base'; 314 + export type { CallArgs } from './expr/call'; 315 + export type { ExampleOptions } from './utils/context'; 316 + export { ctx, PyDslContext } from './utils/context'; 317 + export { keywords } from './utils/keywords'; 318 + export { regexp } from './utils/regexp'; 319 + export { PythonRenderer } from './utils/render'; 320 + export { reserved } from './utils/reserved';
+50
packages/openapi-python/src/py-dsl/layout/doc.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + import type { MaybeArray } from '@hey-api/types'; 3 + 4 + import { py } from '../../ts-python'; 5 + import { PyDsl } from '../base'; 6 + import type { PyDslContext } from '../utils/context'; 7 + import { ctx } from '../utils/context'; 8 + 9 + type DocMaybeLazy<T> = ((ctx: PyDslContext) => T) | T; 10 + export type DocFn = (d: DocPyDsl) => void; 11 + export type DocLines = DocMaybeLazy<MaybeArray<string>>; 12 + 13 + export class DocPyDsl extends PyDsl<py.Comment> { 14 + readonly '~dsl' = 'DocPyDsl'; 15 + 16 + protected _lines: Array<DocLines> = []; 17 + 18 + constructor(lines?: DocLines, fn?: DocFn) { 19 + super(); 20 + if (lines) this.add(lines); 21 + fn?.(this); 22 + } 23 + 24 + override analyze(ctx: AnalysisContext): void { 25 + super.analyze(ctx); 26 + } 27 + 28 + add(lines: DocLines): this { 29 + this._lines.push(lines); 30 + return this; 31 + } 32 + 33 + resolve(): string | undefined { 34 + const lines = this._lines.reduce((lines: Array<string>, line: DocLines) => { 35 + if (typeof line === 'function') line = line(ctx); 36 + for (const l of typeof line === 'string' ? [line] : line) { 37 + if (l || l === '') lines.push(l); 38 + } 39 + return lines; 40 + }, []); 41 + if (!lines.length) return undefined; 42 + return lines.join('\n'); 43 + } 44 + 45 + override toAst(): py.Comment { 46 + // Return a dummy comment node for compliance. 47 + return py.factory.createComment(this.resolve() ?? ''); 48 + // return this.$node(new IdTsDsl('')); 49 + } 50 + }
+58
packages/openapi-python/src/py-dsl/layout/hint.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + import type { MaybeArray } from '@hey-api/types'; 3 + 4 + import { py } from '../../ts-python'; 5 + import { PyDsl } from '../base'; 6 + import type { PyDslContext } from '../utils/context'; 7 + import { ctx } from '../utils/context'; 8 + 9 + type HintMaybeLazy<T> = ((ctx: PyDslContext) => T) | T; 10 + export type HintFn = (d: HintPyDsl) => void; 11 + export type HintLines = HintMaybeLazy<MaybeArray<string>>; 12 + 13 + export class HintPyDsl extends PyDsl<py.Comment> { 14 + readonly '~dsl' = 'HintPyDsl'; 15 + 16 + protected _lines: Array<HintLines> = []; 17 + 18 + constructor(lines?: HintLines, fn?: HintFn) { 19 + super(); 20 + if (lines) this.add(lines); 21 + fn?.(this); 22 + } 23 + 24 + override analyze(ctx: AnalysisContext): void { 25 + super.analyze(ctx); 26 + } 27 + 28 + add(lines: HintLines): this { 29 + this._lines.push(lines); 30 + return this; 31 + } 32 + 33 + apply<T extends py.Node>(node: T): T { 34 + const lines = this._resolveLines(); 35 + if (!lines.length) return node; 36 + 37 + const existing = node.leadingComments ? [...node.leadingComments] : []; 38 + existing.push(...lines); 39 + node.leadingComments = existing; 40 + return node; 41 + } 42 + 43 + override toAst(): py.Comment { 44 + // Return a dummy comment node for compliance. 45 + const lines = this._resolveLines(); 46 + return py.factory.createComment(lines.join('\n')); 47 + } 48 + 49 + private _resolveLines(): Array<string> { 50 + return this._lines.reduce((lines: Array<string>, line: HintLines) => { 51 + if (typeof line === 'function') line = line(ctx); 52 + for (const l of typeof line === 'string' ? [line] : line) { 53 + if (l || l === '') lines.push(l); 54 + } 55 + return lines; 56 + }, []); 57 + } 58 + }
+16
packages/openapi-python/src/py-dsl/layout/newline.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + export class NewlinePyDsl extends PyDsl<py.EmptyStatement> { 7 + readonly '~dsl' = 'NewlinePyDsl'; 8 + 9 + override analyze(ctx: AnalysisContext): void { 10 + super.analyze(ctx); 11 + } 12 + 13 + override toAst(): py.EmptyStatement { 14 + return py.factory.createEmptyStatement(); 15 + } 16 + }
+47
packages/openapi-python/src/py-dsl/mixins/args.ts
··· 1 + import type { AnalysisContext, Node, NodeName, Ref } from '@hey-api/codegen-core'; 2 + import { ref } from '@hey-api/codegen-core'; 3 + 4 + import type { py } from '../../ts-python'; 5 + import type { MaybePyDsl } from '../base'; 6 + import type { BaseCtor, MixinCtor } from './types'; 7 + 8 + type Arg = NodeName | MaybePyDsl<py.Expression>; 9 + 10 + export interface ArgsMethods extends Node { 11 + $args(): ReadonlyArray<py.Expression>; 12 + arg(arg: Arg | undefined): this; 13 + args(...args: ReadonlyArray<Arg | undefined>): this; 14 + } 15 + 16 + export function ArgsMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 17 + abstract class Args extends Base { 18 + protected _args: Array<Ref<Arg>> = []; 19 + 20 + override analyze(ctx: AnalysisContext): void { 21 + super.analyze(ctx); 22 + for (const arg of this._args) { 23 + ctx.analyze(arg); 24 + } 25 + } 26 + 27 + protected arg(arg: Arg | undefined): this { 28 + if (arg !== undefined) this._args.push(ref(arg)); 29 + return this; 30 + } 31 + 32 + protected args(...args: ReadonlyArray<Arg | undefined>): this { 33 + this._args.push( 34 + ...args 35 + .filter((a): a is NonNullable<typeof a> => a !== undefined) 36 + .map((a) => ref(a as Arg)), 37 + ); 38 + return this; 39 + } 40 + 41 + protected $args(): ReadonlyArray<py.Expression> { 42 + return this.$node(this._args).map((arg) => this.$node(arg) as py.Expression); 43 + } 44 + } 45 + 46 + return Args as unknown as MixinCtor<TBase, ArgsMethods>; 47 + }
+54
packages/openapi-python/src/py-dsl/mixins/decorator.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import type { BaseCtor, MixinCtor } from './types'; 6 + 7 + export interface DecoratorMethods extends Node { 8 + $decorators(): ReadonlyArray<py.Expression>; 9 + decorator( 10 + name: NodeName | MaybePyDsl<py.Expression>, 11 + ...args: ReadonlyArray<MaybePyDsl<py.Expression>> 12 + ): this; 13 + } 14 + 15 + export function DecoratorMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 16 + abstract class Decorator extends Base { 17 + protected decorators: Array<py.Expression> = []; 18 + 19 + override analyze(ctx: AnalysisContext): void { 20 + super.analyze(ctx); 21 + for (const decorator of this.decorators) { 22 + ctx.analyze(decorator); 23 + } 24 + } 25 + 26 + protected decorator( 27 + name: NodeName | MaybePyDsl<py.Expression>, 28 + ...args: ReadonlyArray<MaybePyDsl<py.Expression>> 29 + ): this { 30 + const nameNode = 31 + typeof name === 'string' || isPyNode(name) 32 + ? name 33 + : py.factory.createIdentifier(String(name)); 34 + const decoratorExpr = args.length 35 + ? py.factory.createCallExpression( 36 + nameNode as py.Expression, 37 + args.map((a) => this.$node(a) as py.Expression), 38 + ) 39 + : (nameNode as py.Expression); 40 + this.decorators.push(decoratorExpr); 41 + return this; 42 + } 43 + 44 + protected $decorators(): ReadonlyArray<py.Expression> { 45 + return this.decorators; 46 + } 47 + } 48 + 49 + return Decorator as unknown as MixinCtor<TBase, DecoratorMethods>; 50 + } 51 + 52 + function isPyNode(value: unknown): value is { toAst(): unknown } { 53 + return typeof value === 'object' && value !== null && 'toAst' in value; 54 + }
+45
packages/openapi-python/src/py-dsl/mixins/do.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { StmtPyDsl } from '../stmt/stmt'; 6 + import type { BaseCtor, MixinCtor } from './types'; 7 + 8 + export type DoExpr = MaybePyDsl<py.Expression | py.Statement>; 9 + 10 + export interface DoMethods extends Node { 11 + /** Renders the collected `.do()` calls into an array of `py.Statement` nodes. */ 12 + $do(): ReadonlyArray<py.Statement>; 13 + _do: Array<DoExpr>; 14 + /** Adds one or more expressions/statements to the body. */ 15 + do(...items: ReadonlyArray<DoExpr>): this; 16 + } 17 + 18 + export function DoMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 19 + abstract class Do extends Base { 20 + protected _do: Array<DoExpr> = []; 21 + 22 + override analyze(ctx: AnalysisContext): void { 23 + super.analyze(ctx); 24 + ctx.pushScope(); 25 + try { 26 + for (const item of this._do) { 27 + ctx.analyze(item); 28 + } 29 + } finally { 30 + ctx.popScope(); 31 + } 32 + } 33 + 34 + protected do(...items: ReadonlyArray<DoExpr>): this { 35 + this._do.push(...items); 36 + return this; 37 + } 38 + 39 + protected $do(): ReadonlyArray<py.Statement> { 40 + return this.$node(this._do.map((item) => new StmtPyDsl(item))); 41 + } 42 + } 43 + 44 + return Do as unknown as MixinCtor<TBase, DoMethods>; 45 + }
+32
packages/openapi-python/src/py-dsl/mixins/doc.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { DocFn, DocLines } from '../layout/doc'; 5 + import { DocPyDsl } from '../layout/doc'; 6 + import type { BaseCtor, MixinCtor } from './types'; 7 + 8 + export interface DocMethods extends Node { 9 + $docs(): string | undefined; 10 + doc(lines?: DocLines, fn?: DocFn): this; 11 + } 12 + 13 + export function DocMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 14 + abstract class Doc extends Base { 15 + private _doc?: DocPyDsl; 16 + 17 + override analyze(ctx: AnalysisContext): void { 18 + super.analyze(ctx); 19 + } 20 + 21 + protected doc(lines?: DocLines, fn?: DocFn): this { 22 + this._doc = new DocPyDsl(lines, fn); 23 + return this; 24 + } 25 + 26 + protected $docs(): string | undefined { 27 + return this._doc ? this._doc.resolve() : undefined; 28 + } 29 + } 30 + 31 + return Doc as unknown as MixinCtor<TBase, DocMethods>; 32 + }
+33
packages/openapi-python/src/py-dsl/mixins/expr.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { CallPyDsl } from '../expr/call'; 6 + import { AttrPyDsl } from '../expr/member'; 7 + import type { BaseCtor, MixinCtor } from './types'; 8 + 9 + export interface ExprMethods extends Node { 10 + attr(object: MaybePyDsl<py.Expression>, prop: string): AttrPyDsl; 11 + call(callee: MaybePyDsl<py.Expression>, ...args: Array<MaybePyDsl<py.Expression>>): CallPyDsl; 12 + } 13 + 14 + export function ExprMixin<T extends py.Expression, TBase extends BaseCtor<T>>(Base: TBase) { 15 + abstract class Expr extends Base { 16 + override analyze(ctx: AnalysisContext): void { 17 + super.analyze(ctx); 18 + } 19 + 20 + protected attr(object: MaybePyDsl<py.Expression>, prop: string): AttrPyDsl { 21 + return AttrPyDsl.attr(object, prop); 22 + } 23 + 24 + protected call( 25 + callee: MaybePyDsl<py.Expression>, 26 + ...args: Array<MaybePyDsl<py.Expression>> 27 + ): CallPyDsl { 28 + return new CallPyDsl(callee, ...args); 29 + } 30 + } 31 + 32 + return Expr as unknown as MixinCtor<TBase, ExprMethods>; 33 + }
+32
packages/openapi-python/src/py-dsl/mixins/hint.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { HintFn, HintLines } from '../layout/hint'; 5 + import { HintPyDsl } from '../layout/hint'; 6 + import type { BaseCtor, MixinCtor } from './types'; 7 + 8 + export interface HintMethods extends Node { 9 + $hint<T extends py.Node>(node: T): T; 10 + hint(lines?: HintLines, fn?: HintFn): this; 11 + } 12 + 13 + export function HintMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 14 + abstract class Hint extends Base { 15 + private _hint?: HintPyDsl; 16 + 17 + override analyze(ctx: AnalysisContext): void { 18 + super.analyze(ctx); 19 + } 20 + 21 + protected hint(lines?: HintLines, fn?: HintFn): this { 22 + this._hint = new HintPyDsl(lines, fn); 23 + return this; 24 + } 25 + 26 + protected $hint<T extends py.Node>(node: T): T { 27 + return this._hint ? this._hint.apply(node) : node; 28 + } 29 + } 30 + 31 + return Hint as unknown as MixinCtor<TBase, HintMethods>; 32 + }
+51
packages/openapi-python/src/py-dsl/mixins/layout.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { BaseCtor, MixinCtor } from './types'; 5 + 6 + export interface LayoutMethods extends Node { 7 + /** Computes whether output should be multiline based on layout setting and element count. */ 8 + $multiline(count: number): boolean; 9 + /** Sets automatic line output with optional threshold (default: 3). */ 10 + auto(threshold?: number): this; 11 + /** Sets single line output. */ 12 + inline(): this; 13 + /** Sets multi line output. */ 14 + pretty(): this; 15 + } 16 + 17 + export function LayoutMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 18 + abstract class Layout extends Base { 19 + protected static readonly DEFAULT_THRESHOLD = 3; 20 + protected layout: boolean | number | undefined; 21 + 22 + override analyze(ctx: AnalysisContext): void { 23 + super.analyze(ctx); 24 + } 25 + 26 + protected auto(threshold: number = Layout.DEFAULT_THRESHOLD): this { 27 + this.layout = threshold; 28 + return this; 29 + } 30 + 31 + protected inline(): this { 32 + this.layout = false; 33 + return this; 34 + } 35 + 36 + protected pretty(): this { 37 + this.layout = true; 38 + return this; 39 + } 40 + 41 + protected $multiline(count: number): boolean { 42 + if (this.layout === undefined) { 43 + this.layout = Layout.DEFAULT_THRESHOLD; 44 + } 45 + if (count === 0) return false; 46 + return typeof this.layout === 'number' ? count >= this.layout : this.layout; 47 + } 48 + } 49 + 50 + return Layout as unknown as MixinCtor<TBase, LayoutMethods>; 51 + }
+86
packages/openapi-python/src/py-dsl/mixins/modifiers.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { BaseCtor, MixinCtor } from './types'; 5 + 6 + export type Modifiers = { 7 + /** 8 + * Checks if specified modifier is present. 9 + * 10 + * @param modifier - The modifier to check. 11 + * @returns True if modifier is present, false otherwise. 12 + */ 13 + hasModifier(modifier: Modifier): boolean; 14 + modifiers: Array<py.Expression>; 15 + }; 16 + 17 + type Modifier = 'async'; 18 + 19 + export interface ModifierMethods extends Modifiers { 20 + /** 21 + * Adds a modifier of specified kind to modifiers list if condition is true. 22 + * 23 + * @param modifier - The modifier to add. 24 + * @param condition - Whether to add modifier. 25 + * @returns The parent node for chaining. 26 + */ 27 + _m(modifier: Modifier, condition: boolean): this; 28 + } 29 + 30 + function modifierToKind(modifier: Modifier): py.Expression { 31 + switch (modifier) { 32 + case 'async': 33 + return py.factory.createIdentifier('async'); 34 + } 35 + } 36 + 37 + export function ModifiersMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 38 + abstract class Modifiers extends Base { 39 + protected modifiers: Array<py.Expression> = []; 40 + 41 + override analyze(ctx: AnalysisContext): void { 42 + super.analyze(ctx); 43 + } 44 + 45 + protected hasModifier(modifier: Modifier): boolean { 46 + const kind = modifierToKind(modifier); 47 + return Boolean(this.modifiers.find((mod) => mod === kind)); 48 + } 49 + 50 + protected _m(modifier: Modifier, condition: boolean): this { 51 + if (condition) { 52 + const kind = modifierToKind(modifier); 53 + this.modifiers.push(kind); 54 + } 55 + return this; 56 + } 57 + } 58 + 59 + return Modifiers as unknown as MixinCtor<TBase, ModifierMethods>; 60 + } 61 + 62 + export interface AsyncMethods extends Modifiers { 63 + /** 64 + * Adds an `async` keyword modifier if condition is true. 65 + * 66 + * @param condition - Whether to add modifier (default: true). 67 + * @returns The target object for chaining. 68 + */ 69 + async(condition?: boolean): this; 70 + } 71 + 72 + /** 73 + * Mixin that adds an `async` modifier to a node. 74 + */ 75 + export function AsyncMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 76 + const Mixed = ModifiersMixin(Base as BaseCtor<T>); 77 + 78 + abstract class Async extends Mixed { 79 + protected async(condition?: boolean): this { 80 + const cond = arguments.length === 0 ? true : Boolean(condition); 81 + return this._m('async', cond); 82 + } 83 + } 84 + 85 + return Async as unknown as MixinCtor<TBase, AsyncMethods>; 86 + }
+192
packages/openapi-python/src/py-dsl/mixins/operator.ts
··· 1 + import type { AnalysisContext, Node, NodeName } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { BinaryPyDsl } from '../expr/binary'; 6 + import type { BaseCtor, MixinCtor } from './types'; 7 + 8 + type Expr = NodeName | MaybePyDsl<py.Expression>; 9 + 10 + export interface OperatorMethods extends Node { 11 + and(expr: Expr): BinaryPyDsl; 12 + div(expr: Expr): BinaryPyDsl; 13 + eq(expr: Expr): BinaryPyDsl; 14 + floordiv(expr: Expr): BinaryPyDsl; 15 + gt(expr: Expr): BinaryPyDsl; 16 + gte(expr: Expr): BinaryPyDsl; 17 + in_(expr: Expr): BinaryPyDsl; 18 + is(expr: Expr): BinaryPyDsl; 19 + isNot(expr: Expr): BinaryPyDsl; 20 + lt(expr: Expr): BinaryPyDsl; 21 + lte(expr: Expr): BinaryPyDsl; 22 + minus(expr: Expr): BinaryPyDsl; 23 + mod(expr: Expr): BinaryPyDsl; 24 + neq(expr: Expr): BinaryPyDsl; 25 + notIn(expr: Expr): BinaryPyDsl; 26 + or(expr: Expr): BinaryPyDsl; 27 + plus(expr: Expr): BinaryPyDsl; 28 + pow(expr: Expr): BinaryPyDsl; 29 + times(expr: Expr): BinaryPyDsl; 30 + } 31 + 32 + export function OperatorMixin<T extends py.Expression, TBase extends BaseCtor<T>>(Base: TBase) { 33 + abstract class Operator extends Base { 34 + override analyze(ctx: AnalysisContext): void { 35 + super.analyze(ctx); 36 + } 37 + 38 + protected and(expr: Expr): BinaryPyDsl { 39 + return new BinaryPyDsl( 40 + this as unknown as MaybePyDsl<py.Expression>, 41 + 'and', 42 + expr as MaybePyDsl<py.Expression>, 43 + ); 44 + } 45 + 46 + protected div(expr: Expr): BinaryPyDsl { 47 + return new BinaryPyDsl( 48 + this as unknown as MaybePyDsl<py.Expression>, 49 + '/', 50 + expr as MaybePyDsl<py.Expression>, 51 + ); 52 + } 53 + 54 + protected eq(expr: Expr): BinaryPyDsl { 55 + return new BinaryPyDsl( 56 + this as unknown as MaybePyDsl<py.Expression>, 57 + '==', 58 + expr as MaybePyDsl<py.Expression>, 59 + ); 60 + } 61 + 62 + protected floordiv(expr: Expr): BinaryPyDsl { 63 + return new BinaryPyDsl( 64 + this as unknown as MaybePyDsl<py.Expression>, 65 + '//', 66 + expr as MaybePyDsl<py.Expression>, 67 + ); 68 + } 69 + 70 + protected gt(expr: Expr): BinaryPyDsl { 71 + return new BinaryPyDsl( 72 + this as unknown as MaybePyDsl<py.Expression>, 73 + '>', 74 + expr as MaybePyDsl<py.Expression>, 75 + ); 76 + } 77 + 78 + protected gte(expr: Expr): BinaryPyDsl { 79 + return new BinaryPyDsl( 80 + this as unknown as MaybePyDsl<py.Expression>, 81 + '>=', 82 + expr as MaybePyDsl<py.Expression>, 83 + ); 84 + } 85 + 86 + protected in_(expr: Expr): BinaryPyDsl { 87 + return new BinaryPyDsl( 88 + this as unknown as MaybePyDsl<py.Expression>, 89 + 'in', 90 + expr as MaybePyDsl<py.Expression>, 91 + ); 92 + } 93 + 94 + protected is(expr: Expr): BinaryPyDsl { 95 + return new BinaryPyDsl( 96 + this as unknown as MaybePyDsl<py.Expression>, 97 + 'is', 98 + expr as MaybePyDsl<py.Expression>, 99 + ); 100 + } 101 + 102 + protected isNot(expr: Expr): BinaryPyDsl { 103 + return new BinaryPyDsl( 104 + this as unknown as MaybePyDsl<py.Expression>, 105 + 'is not', 106 + expr as MaybePyDsl<py.Expression>, 107 + ); 108 + } 109 + 110 + protected lt(expr: Expr): BinaryPyDsl { 111 + return new BinaryPyDsl( 112 + this as unknown as MaybePyDsl<py.Expression>, 113 + '<', 114 + expr as MaybePyDsl<py.Expression>, 115 + ); 116 + } 117 + 118 + protected lte(expr: Expr): BinaryPyDsl { 119 + return new BinaryPyDsl( 120 + this as unknown as MaybePyDsl<py.Expression>, 121 + '<=', 122 + expr as MaybePyDsl<py.Expression>, 123 + ); 124 + } 125 + 126 + protected minus(expr: Expr): BinaryPyDsl { 127 + return new BinaryPyDsl( 128 + this as unknown as MaybePyDsl<py.Expression>, 129 + '-', 130 + expr as MaybePyDsl<py.Expression>, 131 + ); 132 + } 133 + 134 + protected mod(expr: Expr): BinaryPyDsl { 135 + return new BinaryPyDsl( 136 + this as unknown as MaybePyDsl<py.Expression>, 137 + '%', 138 + expr as MaybePyDsl<py.Expression>, 139 + ); 140 + } 141 + 142 + protected neq(expr: Expr): BinaryPyDsl { 143 + return new BinaryPyDsl( 144 + this as unknown as MaybePyDsl<py.Expression>, 145 + '!=', 146 + expr as MaybePyDsl<py.Expression>, 147 + ); 148 + } 149 + 150 + protected notIn(expr: Expr): BinaryPyDsl { 151 + return new BinaryPyDsl( 152 + this as unknown as MaybePyDsl<py.Expression>, 153 + 'not in', 154 + expr as MaybePyDsl<py.Expression>, 155 + ); 156 + } 157 + 158 + protected or(expr: Expr): BinaryPyDsl { 159 + return new BinaryPyDsl( 160 + this as unknown as MaybePyDsl<py.Expression>, 161 + 'or', 162 + expr as MaybePyDsl<py.Expression>, 163 + ); 164 + } 165 + 166 + protected plus(expr: Expr): BinaryPyDsl { 167 + return new BinaryPyDsl( 168 + this as unknown as MaybePyDsl<py.Expression>, 169 + '+', 170 + expr as MaybePyDsl<py.Expression>, 171 + ); 172 + } 173 + 174 + protected pow(expr: Expr): BinaryPyDsl { 175 + return new BinaryPyDsl( 176 + this as unknown as MaybePyDsl<py.Expression>, 177 + '**', 178 + expr as MaybePyDsl<py.Expression>, 179 + ); 180 + } 181 + 182 + protected times(expr: Expr): BinaryPyDsl { 183 + return new BinaryPyDsl( 184 + this as unknown as MaybePyDsl<py.Expression>, 185 + '*', 186 + expr as MaybePyDsl<py.Expression>, 187 + ); 188 + } 189 + } 190 + 191 + return Operator as unknown as MixinCtor<TBase, OperatorMethods>; 192 + }
+10
packages/openapi-python/src/py-dsl/mixins/types.ts
··· 1 + import type { py } from '../../ts-python'; 2 + import type { PyDsl } from '../base'; 3 + 4 + export type BaseCtor<T extends py.Node> = abstract new (...args: Array<any>) => PyDsl<T>; 5 + 6 + export type DropFirst<T extends Array<any>> = T extends [any, ...infer Rest] ? Rest : never; 7 + 8 + export type MixinCtor<T extends BaseCtor<any>, K> = abstract new ( 9 + ...args: Array<any> 10 + ) => InstanceType<T> & K;
+35
packages/openapi-python/src/py-dsl/mixins/value.ts
··· 1 + import type { AnalysisContext, Node } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import type { BaseCtor, MixinCtor } from './types'; 6 + 7 + export type ValueExpr = string | MaybePyDsl<py.Expression>; 8 + 9 + export interface ValueMethods extends Node { 10 + $value(): py.Expression | undefined; 11 + /** Sets the initializer expression (e.g. `= expr`). */ 12 + assign(expr: ValueExpr): this; 13 + } 14 + 15 + export function ValueMixin<T extends py.Node, TBase extends BaseCtor<T>>(Base: TBase) { 16 + abstract class Value extends Base { 17 + protected value?: ValueExpr; 18 + 19 + override analyze(ctx: AnalysisContext): void { 20 + super.analyze(ctx); 21 + ctx.analyze(this.value); 22 + } 23 + 24 + protected assign(expr: ValueExpr): this { 25 + this.value = expr; 26 + return this; 27 + } 28 + 29 + protected $value(): py.Expression | undefined { 30 + return this.$node(this.value); 31 + } 32 + } 33 + 34 + return Value as unknown as MixinCtor<TBase, ValueMethods>; 35 + }
+27
packages/openapi-python/src/py-dsl/stmt/block.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + import type { DoExpr } from '../mixins/do'; 6 + import { DoMixin } from '../mixins/do'; 7 + import { LayoutMixin } from '../mixins/layout'; 8 + 9 + const Mixed = DoMixin(LayoutMixin(PyDsl<py.Block>)); 10 + 11 + export class BlockPyDsl extends Mixed { 12 + readonly '~dsl' = 'BlockPyDsl'; 13 + 14 + constructor(...items: Array<DoExpr>) { 15 + super(); 16 + this.do(...items); 17 + } 18 + 19 + override analyze(ctx: AnalysisContext) { 20 + super.analyze(ctx); 21 + } 22 + 23 + override toAst() { 24 + const statements = this.$do(); 25 + return py.factory.createBlock(statements); 26 + } 27 + }
+18
packages/openapi-python/src/py-dsl/stmt/break.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + const Mixed = PyDsl<py.BreakStatement>; 7 + 8 + export class BreakPyDsl extends Mixed { 9 + readonly '~dsl' = 'BreakPyDsl'; 10 + 11 + override analyze(_ctx: AnalysisContext): void { 12 + super.analyze(_ctx); 13 + } 14 + 15 + override toAst(): py.BreakStatement { 16 + return py.factory.createBreakStatement(); 17 + } 18 + }
+18
packages/openapi-python/src/py-dsl/stmt/continue.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + const Mixed = PyDsl<py.ContinueStatement>; 7 + 8 + export class ContinuePyDsl extends Mixed { 9 + readonly '~dsl' = 'ContinuePyDsl'; 10 + 11 + override analyze(_ctx: AnalysisContext): void { 12 + super.analyze(_ctx); 13 + } 14 + 15 + override toAst(): py.ContinueStatement { 16 + return py.factory.createContinueStatement(); 17 + } 18 + }
+101
packages/openapi-python/src/py-dsl/stmt/for.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import type { DoExpr } from '../mixins/do'; 7 + import { BlockPyDsl } from './block'; 8 + 9 + const Mixed = PyDsl<py.ForStatement>; 10 + 11 + export class ForPyDsl extends Mixed { 12 + readonly '~dsl' = 'ForPyDsl'; 13 + 14 + protected _body?: Array<DoExpr>; 15 + protected _else?: Array<DoExpr>; 16 + protected _iterable?: MaybePyDsl<py.Expression>; 17 + protected _target?: MaybePyDsl<py.Expression>; 18 + 19 + constructor( 20 + target: MaybePyDsl<py.Expression>, 21 + iterable: MaybePyDsl<py.Expression>, 22 + ...body: Array<DoExpr> 23 + ) { 24 + super(); 25 + this._target = target; 26 + this._iterable = iterable; 27 + this._body = body; 28 + } 29 + 30 + override analyze(ctx: AnalysisContext): void { 31 + super.analyze(ctx); 32 + 33 + if (this._target) ctx.analyze(this._target); 34 + if (this._iterable) ctx.analyze(this._iterable); 35 + 36 + if (this._body) { 37 + ctx.pushScope(); 38 + try { 39 + for (const stmt of this._body) ctx.analyze(stmt); 40 + } finally { 41 + ctx.popScope(); 42 + } 43 + } 44 + 45 + if (this._else) { 46 + ctx.pushScope(); 47 + try { 48 + for (const stmt of this._else) ctx.analyze(stmt); 49 + } finally { 50 + ctx.popScope(); 51 + } 52 + } 53 + } 54 + 55 + /** Returns true when all required builder calls are present. */ 56 + get isValid(): boolean { 57 + return this.missingRequiredCalls().length === 0; 58 + } 59 + 60 + body(...items: Array<DoExpr>): this { 61 + this._body = items; 62 + return this; 63 + } 64 + 65 + else(...items: Array<DoExpr>): this { 66 + this._else = items; 67 + return this; 68 + } 69 + 70 + override toAst(): py.ForStatement { 71 + this.$validate(); 72 + 73 + const body = new BlockPyDsl(...this._body!).$do(); 74 + const elseBlock = this._else ? new BlockPyDsl(...this._else).$do() : undefined; 75 + 76 + return py.factory.createForStatement( 77 + this.$node(this._target!) as py.Expression, 78 + this.$node(this._iterable!) as py.Expression, 79 + [...body], 80 + elseBlock ? [...elseBlock] : undefined, 81 + ); 82 + } 83 + 84 + $validate(): asserts this is this & { 85 + _body: Array<DoExpr>; 86 + _iterable: MaybePyDsl<py.Expression>; 87 + _target: MaybePyDsl<py.Expression>; 88 + } { 89 + const missing = this.missingRequiredCalls(); 90 + if (missing.length === 0) return; 91 + throw new Error(`For statement missing ${missing.join(' and ')}`); 92 + } 93 + 94 + private missingRequiredCalls(): ReadonlyArray<string> { 95 + const missing: Array<string> = []; 96 + if (!this._target) missing.push('target'); 97 + if (!this._iterable) missing.push('iterable'); 98 + if (!this._body || this._body.length === 0) missing.push('body'); 99 + return missing; 100 + } 101 + }
+82
packages/openapi-python/src/py-dsl/stmt/if.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import type { DoExpr } from '../mixins/do'; 7 + import { DoMixin } from '../mixins/do'; 8 + import { BlockPyDsl } from './block'; 9 + 10 + export type IfCondition = string | MaybePyDsl<py.Expression>; 11 + 12 + const Mixed = DoMixin(PyDsl<py.IfStatement>); 13 + 14 + export class IfPyDsl extends Mixed { 15 + readonly '~dsl' = 'IfPyDsl'; 16 + 17 + protected _condition?: IfCondition; 18 + protected _else?: Array<DoExpr>; 19 + 20 + constructor(condition?: IfCondition) { 21 + super(); 22 + if (condition) this.condition(condition); 23 + } 24 + 25 + override analyze(ctx: AnalysisContext): void { 26 + super.analyze(ctx); 27 + ctx.analyze(this._condition); 28 + if (this._else) { 29 + ctx.pushScope(); 30 + try { 31 + for (const stmt of this._else) { 32 + ctx.analyze(stmt); 33 + } 34 + } finally { 35 + ctx.popScope(); 36 + } 37 + } 38 + } 39 + 40 + /** Returns true when all required builder calls are present. */ 41 + get isValid(): boolean { 42 + return this.missingRequiredCalls().length === 0; 43 + } 44 + 45 + condition(condition: IfCondition): this { 46 + this._condition = condition; 47 + return this; 48 + } 49 + 50 + otherwise(...items: Array<DoExpr>): this { 51 + this._else = items; 52 + return this; 53 + } 54 + 55 + override toAst() { 56 + this.$validate(); 57 + 58 + const thenStatements = this.$do(); 59 + const elseStatements = this._else ? new BlockPyDsl(...this._else).$do() : undefined; 60 + 61 + return py.factory.createIfStatement( 62 + this.$node(this._condition!), 63 + [...thenStatements], 64 + elseStatements ? [...elseStatements] : undefined, 65 + ); 66 + } 67 + 68 + $validate(): asserts this is this & { 69 + _condition: IfCondition; 70 + } { 71 + const missing = this.missingRequiredCalls(); 72 + if (missing.length === 0) return; 73 + throw new Error(`If statement missing ${missing.join(' and ')}`); 74 + } 75 + 76 + private missingRequiredCalls(): ReadonlyArray<string> { 77 + const missing: Array<string> = []; 78 + if (!this._condition) missing.push('.condition()'); 79 + if (this._do.length === 0) missing.push('.do()'); 80 + return missing; 81 + } 82 + }
+57
packages/openapi-python/src/py-dsl/stmt/import.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + type ImportName = { alias?: string; name: string }; 7 + 8 + const Mixed = PyDsl<py.ImportStatement>; 9 + 10 + export class ImportPyDsl extends Mixed { 11 + readonly '~dsl' = 'ImportPyDsl'; 12 + 13 + protected isFrom: boolean = true; 14 + protected module: string = ''; 15 + protected names?: ReadonlyArray<ImportName>; 16 + 17 + constructor(module: string); 18 + constructor(module: string, isFrom: boolean); 19 + constructor(module: string, names: ReadonlyArray<ImportName>, isFrom: boolean); 20 + constructor( 21 + module: string, 22 + namesOrIsFrom?: ReadonlyArray<ImportName> | boolean, 23 + isFrom?: boolean, 24 + ) { 25 + super(); 26 + this.module = module; 27 + if (typeof namesOrIsFrom === 'boolean') { 28 + this.isFrom = namesOrIsFrom; 29 + } else if (Array.isArray(namesOrIsFrom)) { 30 + this.names = namesOrIsFrom; 31 + this.isFrom = isFrom ?? true; 32 + } else { 33 + this.isFrom = true; 34 + } 35 + } 36 + 37 + static from(module: string, names?: ReadonlyArray<ImportName>): ImportPyDsl { 38 + return names ? new ImportPyDsl(module, names, true) : new ImportPyDsl(module, true); 39 + } 40 + 41 + static direct(module: string): ImportPyDsl { 42 + return new ImportPyDsl(module, false); 43 + } 44 + 45 + override analyze(_ctx: AnalysisContext): void { 46 + super.analyze(_ctx); 47 + } 48 + 49 + override toAst(): py.ImportStatement { 50 + return { 51 + isFrom: this.isFrom, 52 + kind: py.PyNodeKind.ImportStatement, 53 + module: this.module, 54 + names: this.names, 55 + }; 56 + } 57 + }
+40
packages/openapi-python/src/py-dsl/stmt/raise.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + // import { LiteralPyDsl } from '../expr/literal'; 7 + 8 + const Mixed = PyDsl<py.RaiseStatement>; 9 + 10 + export class RaisePyDsl extends Mixed { 11 + readonly '~dsl' = 'RaisePyDsl'; 12 + 13 + protected _error?: string | MaybePyDsl<py.Expression>; 14 + // protected msg?: string | MaybePyDsl<py.Expression>; 15 + 16 + constructor(error?: string | MaybePyDsl<py.Expression>) { 17 + super(); 18 + this._error = error; 19 + } 20 + 21 + override analyze(ctx: AnalysisContext): void { 22 + super.analyze(ctx); 23 + ctx.analyze(this._error); 24 + // ctx.analyze(this.msg); 25 + } 26 + 27 + // /** Sets the message argument for the exception (e.g. `raise ValueError('msg')`). */ 28 + // message(value: string | MaybePyDsl<py.Expression>): this { 29 + // this.msg = value; 30 + // return this; 31 + // } 32 + 33 + override toAst() { 34 + // Python's `raise` can be bare (re-raise), or `raise <expr>`. 35 + // Unlike JS `throw new Error(msg)`, Python uses `raise ErrorType(msg)` directly, 36 + // so the caller constructs the call expression themselves. 37 + const errorNode = this._error ? this.$node(this._error) : undefined; 38 + return py.factory.createRaiseStatement(errorNode); 39 + } 40 + }
+34
packages/openapi-python/src/py-dsl/stmt/return.ts
··· 1 + import type { AnalysisContext, NodeName, Ref } from '@hey-api/codegen-core'; 2 + import { ref } from '@hey-api/codegen-core'; 3 + 4 + import { py } from '../../ts-python'; 5 + import type { MaybePyDsl } from '../base'; 6 + import { PyDsl } from '../base'; 7 + // import { f } from '../utils/factories'; 8 + 9 + export type ReturnExpr = NodeName | MaybePyDsl<py.Expression>; 10 + export type ReturnCtor = (expr?: ReturnExpr) => ReturnPyDsl; 11 + 12 + const Mixed = PyDsl<py.ReturnStatement>; 13 + 14 + export class ReturnPyDsl extends Mixed { 15 + readonly '~dsl' = 'ReturnPyDsl'; 16 + 17 + protected _returnExpr?: Ref<ReturnExpr>; 18 + 19 + constructor(expr?: ReturnExpr) { 20 + super(); 21 + if (expr) this._returnExpr = ref(expr); 22 + } 23 + 24 + override analyze(ctx: AnalysisContext): void { 25 + super.analyze(ctx); 26 + ctx.analyze(this._returnExpr); 27 + } 28 + 29 + override toAst() { 30 + return py.factory.createReturnStatement(this.$node(this._returnExpr)); 31 + } 32 + } 33 + 34 + // f.return.set((...args) => new ReturnPyDsl(...args));
+53
packages/openapi-python/src/py-dsl/stmt/stmt.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + 6 + const Mixed = PyDsl<py.Statement>; 7 + 8 + export class StmtPyDsl extends Mixed { 9 + readonly '~dsl' = 'StmtPyDsl'; 10 + 11 + protected _inner: py.Expression | py.Statement | PyDsl<any>; 12 + 13 + constructor(inner: py.Expression | py.Statement | PyDsl<any>) { 14 + super(); 15 + this._inner = inner; 16 + } 17 + 18 + override analyze(ctx: AnalysisContext): void { 19 + super.analyze(ctx); 20 + ctx.analyze(this._inner); 21 + } 22 + 23 + override toAst() { 24 + const node = this.$node(this._inner); 25 + if (isStatement(node)) return node; 26 + return py.factory.createExpressionStatement(node as py.Expression); 27 + } 28 + } 29 + 30 + /** Checks whether a Python AST node is a statement. */ 31 + function isStatement(node: py.Expression | py.Statement): node is py.Statement { 32 + const statementKinds = new Set([ 33 + py.PyNodeKind.Assignment, 34 + py.PyNodeKind.AugmentedAssignment, 35 + py.PyNodeKind.Block, 36 + py.PyNodeKind.BreakStatement, 37 + py.PyNodeKind.ClassDeclaration, 38 + py.PyNodeKind.Comment, 39 + py.PyNodeKind.ContinueStatement, 40 + py.PyNodeKind.EmptyStatement, 41 + py.PyNodeKind.ExpressionStatement, 42 + py.PyNodeKind.ForStatement, 43 + py.PyNodeKind.FunctionDeclaration, 44 + py.PyNodeKind.IfStatement, 45 + py.PyNodeKind.ImportStatement, 46 + py.PyNodeKind.RaiseStatement, 47 + py.PyNodeKind.ReturnStatement, 48 + py.PyNodeKind.TryStatement, 49 + py.PyNodeKind.WhileStatement, 50 + py.PyNodeKind.WithStatement, 51 + ]); 52 + return statementKinds.has(node.kind); 53 + }
+231
packages/openapi-python/src/py-dsl/stmt/try.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + import type { MaybeArray } from '@hey-api/types'; 3 + 4 + import { py } from '../../ts-python'; 5 + import type { MaybePyDsl } from '../base'; 6 + import { PyDsl } from '../base'; 7 + import type { DoExpr } from '../mixins/do'; 8 + import { BlockPyDsl } from './block'; 9 + 10 + const Mixed = PyDsl<py.TryStatement>; 11 + 12 + type ExceptType = string | MaybePyDsl<py.Expression>; 13 + 14 + interface ExceptEntry { 15 + body: Array<DoExpr>; 16 + name?: NodeName; 17 + types: Array<ExceptType>; 18 + } 19 + 20 + function exceptKey(types: Array<ExceptType>): string { 21 + return types 22 + .map((t) => (typeof t === 'string' ? t : '<<expr>>')) 23 + .sort() 24 + .join(','); 25 + } 26 + 27 + export class TryPyDsl extends Mixed { 28 + readonly '~dsl' = 'TryPyDsl'; 29 + 30 + /** 31 + * Ordered list of except clauses. We also keep a lookup map 32 + * (`_exceptIndex`) keyed by the normalised type key so that 33 + * repeated `.except()` calls with the same type set merge their 34 + * body statements instead of creating duplicate clauses. 35 + */ 36 + protected _excepts: Array<ExceptEntry> = []; 37 + protected _exceptIndex: Map<string, number> = new Map(); 38 + 39 + protected _finally?: Array<DoExpr>; 40 + protected _try?: Array<DoExpr>; 41 + 42 + constructor(...tryBlock: Array<DoExpr>) { 43 + super(); 44 + this.try(...tryBlock); 45 + } 46 + 47 + override analyze(ctx: AnalysisContext): void { 48 + super.analyze(ctx); 49 + 50 + if (this._try) { 51 + ctx.pushScope(); 52 + try { 53 + for (const stmt of this._try) ctx.analyze(stmt); 54 + } finally { 55 + ctx.popScope(); 56 + } 57 + } 58 + 59 + for (const entry of this._excepts) { 60 + ctx.pushScope(); 61 + try { 62 + ctx.analyze(entry.name); 63 + for (const t of entry.types) ctx.analyze(t); 64 + for (const stmt of entry.body) ctx.analyze(stmt); 65 + } finally { 66 + ctx.popScope(); 67 + } 68 + } 69 + 70 + if (this._finally) { 71 + ctx.pushScope(); 72 + try { 73 + for (const stmt of this._finally) ctx.analyze(stmt); 74 + } finally { 75 + ctx.popScope(); 76 + } 77 + } 78 + } 79 + 80 + /** Returns true when all required builder calls are present. */ 81 + get isValid(): boolean { 82 + return this.missingRequiredCalls().length === 0; 83 + } 84 + 85 + /** 86 + * Add (or merge into) an except clause. 87 + * 88 + * ```ts 89 + * $.try(...) 90 + * .except('ValueError', 'e', body1, body2) // except ValueError as e: 91 + * .except(['TypeError', 'KeyError'], 'e', ...) // except (TypeError, KeyError) as e: 92 + * .except('ValueError', moreBody) // merges into first clause 93 + * ``` 94 + * 95 + * @param types Single exception type or array of types. 96 + * @param nameOrBody Either the `as` variable name (`NodeName`) or the 97 + * first body expression. If it looks like a `NodeName` (string that 98 + * is a valid Python identifier and is *not* a DSL node), it is treated 99 + * as the name; pass body items after it. 100 + * @param body Remaining body statements. 101 + */ 102 + except( 103 + types: MaybeArray<ExceptType>, 104 + nameOrBody?: NodeName | DoExpr, 105 + ...body: Array<DoExpr> 106 + ): this { 107 + const typeArr = Array.isArray(types) ? types : [types]; 108 + const key = exceptKey(typeArr); 109 + 110 + let name: NodeName | undefined; 111 + let bodyItems: Array<DoExpr>; 112 + 113 + // Disambiguate: if the second arg is a plain string that looks like 114 + // an identifier (no dots, no spaces, not a DSL node) treat it as 115 + // the `as` name. Otherwise it's the first body expression. 116 + if (nameOrBody !== undefined && this._isNodeName(nameOrBody)) { 117 + name = nameOrBody as NodeName; 118 + bodyItems = body; 119 + } else if (nameOrBody !== undefined) { 120 + bodyItems = [nameOrBody as DoExpr, ...body]; 121 + } else { 122 + bodyItems = body; 123 + } 124 + 125 + const existing = this._exceptIndex.get(key); 126 + if (existing !== undefined) { 127 + const entry = this._excepts[existing]!; 128 + entry.body.push(...bodyItems); 129 + if (name !== undefined) entry.name = name; 130 + } else { 131 + this._exceptIndex.set(key, this._excepts.length); 132 + this._excepts.push({ body: bodyItems, name, types: typeArr }); 133 + } 134 + 135 + return this; 136 + } 137 + 138 + /** Add a bare `except:` clause (catches everything). */ 139 + exceptAll(...body: Array<DoExpr>): this { 140 + const key = ''; 141 + const existing = this._exceptIndex.get(key); 142 + if (existing !== undefined) { 143 + this._excepts[existing]!.body.push(...body); 144 + } else { 145 + this._exceptIndex.set(key, this._excepts.length); 146 + this._excepts.push({ body, types: [] }); 147 + } 148 + return this; 149 + } 150 + 151 + finally(...items: Array<DoExpr>): this { 152 + this._finally = items; 153 + return this; 154 + } 155 + 156 + try(...items: Array<DoExpr>): this { 157 + this._try = items; 158 + return this; 159 + } 160 + 161 + override toAst() { 162 + this.$validate(); 163 + 164 + const tryStatements = new BlockPyDsl(...this._try!).$do(); 165 + 166 + let exceptClauses: Array<py.ExceptClause> | undefined; 167 + if (this._excepts.length) { 168 + exceptClauses = this._excepts.map((entry) => { 169 + const bodyStatements = new BlockPyDsl(...entry.body).$do(); 170 + 171 + let exceptionType: py.Expression | undefined; 172 + if (entry.types.length === 1) { 173 + exceptionType = this.$node(entry.types[0]!); 174 + } else if (entry.types.length > 1) { 175 + exceptionType = py.factory.createTupleExpression( 176 + entry.types.map((t) => this.$node(t) as py.Expression), 177 + ); 178 + } 179 + 180 + const exceptionName = entry.name 181 + ? py.factory.createIdentifier( 182 + this.$name({ current: entry.name } as any) || String(entry.name), 183 + ) 184 + : undefined; 185 + 186 + return py.factory.createExceptClause([...bodyStatements], exceptionType, exceptionName); 187 + }); 188 + } 189 + 190 + const finallyStatements = this._finally 191 + ? [...new BlockPyDsl(...this._finally).$do()] 192 + : undefined; 193 + 194 + return py.factory.createTryStatement( 195 + [...tryStatements], 196 + exceptClauses, 197 + undefined, 198 + finallyStatements, 199 + ); 200 + } 201 + 202 + $validate(): asserts this is this & { 203 + _try: Array<DoExpr>; 204 + } { 205 + const missing = this.missingRequiredCalls(); 206 + if (missing.length === 0) return; 207 + throw new Error(`Try statement missing ${missing.join(' and ')}`); 208 + } 209 + 210 + private missingRequiredCalls(): ReadonlyArray<string> { 211 + const missing: Array<string> = []; 212 + if (!this._try || this._try.length === 0) missing.push('.try()'); 213 + return missing; 214 + } 215 + 216 + /** 217 + * Heuristic: a value is a `NodeName` (intended as the `as` variable) 218 + * if it is a plain string matching a Python identifier pattern, or a 219 + * Symbol. 220 + */ 221 + private _isNodeName(value: unknown): boolean { 222 + if (typeof value === 'string') { 223 + return /^[A-Za-z_]\w*$/.test(value); 224 + } 225 + // Symbols from codegen-core have `~brand` 226 + if (value && typeof value === 'object' && '~brand' in value) { 227 + return true; 228 + } 229 + return false; 230 + } 231 + }
+68
packages/openapi-python/src/py-dsl/stmt/var.ts
··· 1 + import type { AnalysisContext, NodeName } from '@hey-api/codegen-core'; 2 + import { isSymbol } from '@hey-api/codegen-core'; 3 + 4 + import { py } from '../../ts-python'; 5 + // import { TypePyDsl } from '../base'; 6 + import { PyDsl } from '../base'; 7 + // import { DocMixin } from '../mixins/doc'; 8 + // import { HintMixin } from '../mixins/hint'; 9 + // import { DefaultMixin, ExportMixin } from '../mixins/modifiers'; 10 + // import { PatternMixin } from '../mixins/pattern'; 11 + import { ValueMixin } from '../mixins/value'; 12 + // import { TypeExprPyDsl } from '../type/expr'; 13 + import { safeRuntimeName } from '../utils/name'; 14 + 15 + // const Mixed = DefaultMixin( 16 + // DocMixin(ExportMixin(HintMixin(PatternMixin(ValueMixin(PyDsl<py.Assignment>))))), 17 + // ); 18 + const Mixed = ValueMixin(PyDsl<py.Assignment>); 19 + 20 + export class VarPyDsl extends Mixed { 21 + readonly '~dsl' = 'VarPyDsl'; 22 + override readonly nameSanitizer = safeRuntimeName; 23 + 24 + // protected _type?: TypePyDsl; 25 + 26 + constructor(name?: NodeName) { 27 + super(); 28 + if (name) this.name.set(name); 29 + if (isSymbol(name)) { 30 + name.setKind('var'); 31 + } 32 + } 33 + 34 + override analyze(ctx: AnalysisContext): void { 35 + super.analyze(ctx); 36 + ctx.analyze(this.name); 37 + // ctx.analyze(this._type); 38 + } 39 + 40 + /** Returns true when all required builder calls are present. */ 41 + get isValid(): boolean { 42 + return this.missingRequiredCalls().length === 0; 43 + } 44 + 45 + // /** Sets the variable type annotation. */ 46 + // type(type: string | TypePyDsl): this { 47 + // this._type = type instanceof TypePyDsl ? type : new TypeExprPyDsl(type); 48 + // return this; 49 + // } 50 + 51 + override toAst() { 52 + this.$validate(); 53 + return py.factory.createAssignment(this.$node(this.name)!, this.$value()!); 54 + } 55 + 56 + $validate(): asserts this { 57 + const missing = this.missingRequiredCalls(); 58 + if (missing.length === 0) return; 59 + throw new Error(`Variable assignment missing ${missing.join(' and ')}`); 60 + } 61 + 62 + private missingRequiredCalls(): ReadonlyArray<string> { 63 + const missing: Array<string> = []; 64 + if (!this.$node(this.name)) missing.push('name'); 65 + if (!this.$value()) missing.push('.value()'); 66 + return missing; 67 + } 68 + }
+91
packages/openapi-python/src/py-dsl/stmt/while.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import type { DoExpr } from '../mixins/do'; 7 + import { BlockPyDsl } from './block'; 8 + 9 + const Mixed = PyDsl<py.WhileStatement>; 10 + 11 + export class WhilePyDsl extends Mixed { 12 + readonly '~dsl' = 'WhilePyDsl'; 13 + 14 + protected _body?: Array<DoExpr>; 15 + protected _condition?: MaybePyDsl<py.Expression>; 16 + protected _else?: Array<DoExpr>; 17 + 18 + constructor(condition: MaybePyDsl<py.Expression>, ...body: Array<DoExpr>) { 19 + super(); 20 + this._condition = condition; 21 + this._body = body; 22 + } 23 + 24 + override analyze(ctx: AnalysisContext): void { 25 + super.analyze(ctx); 26 + 27 + if (this._condition) ctx.analyze(this._condition); 28 + 29 + if (this._body) { 30 + ctx.pushScope(); 31 + try { 32 + for (const stmt of this._body) ctx.analyze(stmt); 33 + } finally { 34 + ctx.popScope(); 35 + } 36 + } 37 + 38 + if (this._else) { 39 + ctx.pushScope(); 40 + try { 41 + for (const stmt of this._else) ctx.analyze(stmt); 42 + } finally { 43 + ctx.popScope(); 44 + } 45 + } 46 + } 47 + 48 + /** Returns true when all required builder calls are present. */ 49 + get isValid(): boolean { 50 + return this.missingRequiredCalls().length === 0; 51 + } 52 + 53 + body(...items: Array<DoExpr>): this { 54 + this._body = items; 55 + return this; 56 + } 57 + 58 + else(...items: Array<DoExpr>): this { 59 + this._else = items; 60 + return this; 61 + } 62 + 63 + override toAst(): py.WhileStatement { 64 + this.$validate(); 65 + 66 + const body = new BlockPyDsl(...this._body!).$do(); 67 + const elseBlock = this._else ? new BlockPyDsl(...this._else).$do() : undefined; 68 + 69 + return py.factory.createWhileStatement( 70 + this.$node(this._condition!) as py.Expression, 71 + [...body], 72 + elseBlock ? [...elseBlock] : undefined, 73 + ); 74 + } 75 + 76 + $validate(): asserts this is this & { 77 + _body: Array<DoExpr>; 78 + _condition: MaybePyDsl<py.Expression>; 79 + } { 80 + const missing = this.missingRequiredCalls(); 81 + if (missing.length === 0) return; 82 + throw new Error(`While statement missing ${missing.join(' and ')}`); 83 + } 84 + 85 + private missingRequiredCalls(): ReadonlyArray<string> { 86 + const missing: Array<string> = []; 87 + if (!this._condition) missing.push('condition'); 88 + if (!this._body || this._body.length === 0) missing.push('body'); 89 + return missing; 90 + } 91 + }
+111
packages/openapi-python/src/py-dsl/stmt/with.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import { py } from '../../ts-python'; 4 + import type { MaybePyDsl } from '../base'; 5 + import { PyDsl } from '../base'; 6 + import type { DoExpr } from '../mixins/do'; 7 + import { BlockPyDsl } from './block'; 8 + 9 + const Mixed = PyDsl<py.WithStatement>; 10 + 11 + export type WithItemInput = 12 + | MaybePyDsl<py.Expression> 13 + | { alias?: MaybePyDsl<py.Expression>; context: MaybePyDsl<py.Expression> }; 14 + 15 + export class WithPyDsl extends Mixed { 16 + readonly '~dsl' = 'WithPyDsl'; 17 + 18 + protected _body?: Array<DoExpr>; 19 + protected _items: Array<WithItemInput> = []; 20 + protected _modifier?: MaybePyDsl<py.Expression>; 21 + 22 + constructor(...items: Array<WithItemInput>) { 23 + super(); 24 + this._items = items; 25 + } 26 + 27 + override analyze(ctx: AnalysisContext): void { 28 + super.analyze(ctx); 29 + 30 + for (const item of this._items) { 31 + if (typeof item === 'object' && 'context' in item) { 32 + ctx.analyze(item.context); 33 + ctx.analyze(item.alias); 34 + } else { 35 + ctx.analyze(item); 36 + } 37 + } 38 + if (this._modifier) ctx.analyze(this._modifier); 39 + 40 + if (this._body) { 41 + ctx.pushScope(); 42 + try { 43 + for (const stmt of this._body) ctx.analyze(stmt); 44 + } finally { 45 + ctx.popScope(); 46 + } 47 + } 48 + } 49 + 50 + /** Returns true when all required builder calls are present. */ 51 + get isValid(): boolean { 52 + return this.missingRequiredCalls().length === 0; 53 + } 54 + 55 + body(...items: Array<DoExpr>): this { 56 + this._body = items; 57 + return this; 58 + } 59 + 60 + item(item: WithItemInput): this { 61 + this._items.push(item); 62 + return this; 63 + } 64 + 65 + modifier(expr: MaybePyDsl<py.Expression>): this { 66 + this._modifier = expr; 67 + return this; 68 + } 69 + 70 + async(): this { 71 + this._modifier = py.factory.createIdentifier('async'); 72 + return this; 73 + } 74 + 75 + override toAst(): py.WithStatement { 76 + this.$validate(); 77 + 78 + const astItems = this._items.map((item) => { 79 + if (typeof item === 'object' && 'context' in item) { 80 + return py.factory.createWithItem( 81 + this.$node(item.context) as py.Expression, 82 + item.alias ? (this.$node(item.alias) as py.Expression) : undefined, 83 + ); 84 + } 85 + return py.factory.createWithItem(this.$node(item) as py.Expression, undefined); 86 + }); 87 + 88 + const body = new BlockPyDsl(...this._body!).$do(); 89 + 90 + return py.factory.createWithStatement( 91 + astItems, 92 + [...body], 93 + this._modifier ? [this.$node(this._modifier) as py.Expression] : undefined, 94 + ); 95 + } 96 + 97 + $validate(): asserts this is this & { 98 + _body: Array<DoExpr>; 99 + } { 100 + const missing = this.missingRequiredCalls(); 101 + if (missing.length === 0) return; 102 + throw new Error(`With statement missing ${missing.join(' and ')}`); 103 + } 104 + 105 + private missingRequiredCalls(): ReadonlyArray<string> { 106 + const missing: Array<string> = []; 107 + if (!this._items.length) missing.push('items'); 108 + if (!this._body || this._body.length === 0) missing.push('.body()'); 109 + return missing; 110 + } 111 + }
+207
packages/openapi-python/src/py-dsl/utils/__tests__/render-utils.test.ts
··· 1 + import { moduleSortKey } from '../render-utils'; 2 + 3 + const createFile = (finalPath: string, external = false) => { 4 + const parts = finalPath.split('/'); 5 + const filename = parts.at(-1)!; 6 + const dotIndex = filename.lastIndexOf('.'); 7 + return { 8 + extension: dotIndex > 0 ? filename.slice(dotIndex) : undefined, 9 + external, 10 + finalPath, 11 + name: dotIndex > 0 ? filename.slice(0, dotIndex) : filename, 12 + }; 13 + }; 14 + 15 + const root = '/project/src'; 16 + 17 + describe('moduleSortKey', () => { 18 + describe('external imports (group 0)', () => { 19 + it('returns external module path unchanged', () => { 20 + const file = createFile('/project/src/client.py'); 21 + const fromFile = createFile('httpx', true); 22 + 23 + const [group, distance, modulePath] = moduleSortKey({ 24 + file, 25 + fromFile, 26 + preferFileExtension: '.py', 27 + root, 28 + }); 29 + 30 + expect(group).toBe(0); 31 + expect(distance).toBe(0); 32 + expect(modulePath).toBe('httpx'); 33 + }); 34 + }); 35 + 36 + describe('local imports (group 2)', () => { 37 + describe('same directory', () => { 38 + it('converts sibling file to relative import', () => { 39 + const file = createFile('/project/src/api/client.py'); 40 + const fromFile = createFile('/project/src/api/types.py'); 41 + 42 + const [group, distance, modulePath] = moduleSortKey({ 43 + file, 44 + fromFile, 45 + preferFileExtension: '.py', 46 + root, 47 + }); 48 + 49 + expect(group).toBe(2); 50 + expect(distance).toBe(0); 51 + expect(modulePath).toBe('.types'); 52 + }); 53 + 54 + it('handles index.py as implicit module', () => { 55 + const file = createFile('/project/src/api/client.py'); 56 + const fromFile = createFile('/project/src/api/index.py'); 57 + 58 + const [, , modulePath] = moduleSortKey({ 59 + file, 60 + fromFile, 61 + preferFileExtension: '.py', 62 + root, 63 + }); 64 + 65 + expect(modulePath).toBe('.'); 66 + }); 67 + 68 + it('handles __init__.py as implicit module', () => { 69 + const file = createFile('/project/src/api/client.py'); 70 + const fromFile = createFile('/project/src/api/__init__.py'); 71 + 72 + const [, , modulePath] = moduleSortKey({ 73 + file, 74 + fromFile, 75 + preferFileExtension: '.py', 76 + root, 77 + }); 78 + 79 + expect(modulePath).toBe('.'); 80 + }); 81 + }); 82 + 83 + describe('child directory', () => { 84 + it('converts nested path to dotted module', () => { 85 + const file = createFile('/project/src/client.py'); 86 + const fromFile = createFile('/project/src/models/user.py'); 87 + 88 + const [group, distance, modulePath] = moduleSortKey({ 89 + file, 90 + fromFile, 91 + preferFileExtension: '.py', 92 + root, 93 + }); 94 + 95 + expect(group).toBe(2); 96 + expect(distance).toBe(0); 97 + expect(modulePath).toBe('.models.user'); 98 + }); 99 + 100 + it('handles deeply nested paths', () => { 101 + const file = createFile('/project/src/client.py'); 102 + const fromFile = createFile('/project/src/api/v1/endpoints/users.py'); 103 + 104 + const [, , modulePath] = moduleSortKey({ 105 + file, 106 + fromFile, 107 + preferFileExtension: '.py', 108 + root, 109 + }); 110 + 111 + expect(modulePath).toBe('.api.v1.endpoints.users'); 112 + }); 113 + 114 + it('handles index.py in child directory', () => { 115 + const file = createFile('/project/src/client.py'); 116 + const fromFile = createFile('/project/src/models/index.py'); 117 + 118 + const [, , modulePath] = moduleSortKey({ 119 + file, 120 + fromFile, 121 + preferFileExtension: '.py', 122 + root, 123 + }); 124 + 125 + expect(modulePath).toBe('.models'); 126 + }); 127 + }); 128 + 129 + describe('parent directory', () => { 130 + it('converts single parent traversal', () => { 131 + const file = createFile('/project/src/api/client.py'); 132 + const fromFile = createFile('/project/src/types.py'); 133 + 134 + const [group, distance, modulePath] = moduleSortKey({ 135 + file, 136 + fromFile, 137 + preferFileExtension: '.py', 138 + root, 139 + }); 140 + 141 + expect(group).toBe(2); 142 + expect(distance).toBe(1); 143 + expect(modulePath).toBe('..types'); 144 + }); 145 + 146 + it('converts double parent traversal', () => { 147 + const file = createFile('/project/src/api/v1/client.py'); 148 + const fromFile = createFile('/project/src/types.py'); 149 + 150 + const [, distance, modulePath] = moduleSortKey({ 151 + file, 152 + fromFile, 153 + preferFileExtension: '.py', 154 + root, 155 + }); 156 + 157 + expect(distance).toBe(2); 158 + expect(modulePath).toBe('...types'); 159 + }); 160 + 161 + it('handles index.py in parent directory', () => { 162 + const file = createFile('/project/src/api/client.py'); 163 + const fromFile = createFile('/project/src/index.py'); 164 + 165 + const [, , modulePath] = moduleSortKey({ 166 + file, 167 + fromFile, 168 + preferFileExtension: '.py', 169 + root, 170 + }); 171 + 172 + expect(modulePath).toBe('..'); 173 + }); 174 + }); 175 + 176 + describe('sibling directory', () => { 177 + it('converts sibling directory path', () => { 178 + const file = createFile('/project/src/api/client.py'); 179 + const fromFile = createFile('/project/src/models/user.py'); 180 + 181 + const [, distance, modulePath] = moduleSortKey({ 182 + file, 183 + fromFile, 184 + preferFileExtension: '.py', 185 + root, 186 + }); 187 + 188 + expect(distance).toBe(1); 189 + expect(modulePath).toBe('..models.user'); 190 + }); 191 + 192 + it('handles index.py in sibling directory', () => { 193 + const file = createFile('/project/src/api/client.py'); 194 + const fromFile = createFile('/project/src/models/index.py'); 195 + 196 + const [, , modulePath] = moduleSortKey({ 197 + file, 198 + fromFile, 199 + preferFileExtension: '.py', 200 + root, 201 + }); 202 + 203 + expect(modulePath).toBe('..models'); 204 + }); 205 + }); 206 + }); 207 + });
+249
packages/openapi-python/src/py-dsl/utils/context.ts
··· 1 + import type { BindingKind } from '@hey-api/codegen-core'; 2 + // import type { BindingKind, NodeScope, Symbol } from '@hey-api/codegen-core'; 3 + // import { isSymbol } from '@hey-api/codegen-core'; 4 + import type { MaybeFunc } from '@hey-api/types'; 5 + 6 + import type { DollarPyDsl } from '../../py-dsl'; 7 + import type { py } from '../../ts-python'; 8 + // import { $, PythonRenderer } from '../../py-dsl'; 9 + import type { PyDsl } from '../base'; 10 + import type { CallArgs } from '../expr/call'; 11 + 12 + export type NodeChain = ReadonlyArray<PyDsl>; 13 + 14 + export interface AccessOptions { 15 + /** The access context. */ 16 + context?: 'example'; 17 + /** Enable debug mode. */ 18 + debug?: boolean; 19 + /** Transform function for each node in the access chain. */ 20 + transform?: (node: PyDsl, index: number, chain: NodeChain) => PyDsl; 21 + } 22 + 23 + // export type AccessResult = ReturnType<typeof $.expr | typeof $.attr | typeof $.call | typeof $.new>; 24 + 25 + export interface ExampleOptions { 26 + /** Import kind for the root node. */ 27 + importKind?: BindingKind; 28 + /** Import name for the root node. */ 29 + importName?: string; 30 + /** Setup to run before calling the example. */ 31 + importSetup?: MaybeFunc< 32 + ( 33 + ctx: DollarPyDsl & { 34 + /** The imported expression. */ 35 + node: PyDsl<py.Expression>; 36 + }, 37 + ) => PyDsl<py.Expression> 38 + >; 39 + /** Module to import from. */ 40 + moduleName?: string; 41 + /** Example request payload. */ 42 + payload?: MaybeFunc<(ctx: DollarPyDsl) => CallArgs | CallArgs[number]>; 43 + /** Variable name for setup node. */ 44 + setupName?: string; 45 + } 46 + 47 + // function accessChainToNode<T = AccessResult>(accessChain: NodeChain): T { 48 + // let result!: AccessResult; 49 + // accessChain.forEach((node, index) => { 50 + // if (index === 0) { 51 + // // assume correct node 52 + // result = node as typeof result; 53 + // } else { 54 + // result = result.attr(node.name); 55 + // } 56 + // }); 57 + // return result as T; 58 + // } 59 + 60 + // function getAccessChainForNode(node: PyDsl): NodeChain { 61 + // const structuralChain = [...getStructuralChainForNode(node, new Set())]; 62 + // const accessChain = structuralToAccessChain(structuralChain); 63 + // if (accessChain.length === 0) { 64 + // // I _think_ this should not happen, but it does and this fix works for now. 65 + // // I assume this will cause issues with imports in some cases, investigate 66 + // // when it actually happens. 67 + // return [node.clone()]; 68 + // } 69 + // return accessChain.map((node) => node.clone()); 70 + // } 71 + 72 + // function getScope(node: PyDsl): NodeScope { 73 + // return node.scope ?? 'value'; 74 + // } 75 + 76 + // function getStructuralChainForNode(node: PyDsl, visited: Set<PyDsl>): NodeChain { 77 + // if (visited.has(node)) return []; 78 + // visited.add(node); 79 + 80 + // if (isStopNode(node)) return []; 81 + 82 + // if (node.structuralParents) { 83 + // for (const [parent] of node.structuralParents) { 84 + // if (getScope(parent) !== getScope(node)) continue; 85 + 86 + // const chain = getStructuralChainForNode(parent, visited); 87 + // if (chain.length > 0) return [...chain, node]; 88 + // } 89 + // } 90 + 91 + // if (!node.root) return []; 92 + 93 + // return [node]; 94 + // } 95 + 96 + // function isAccessorNode(node: PyDsl): boolean { 97 + // return ( 98 + // node['~dsl'] === 'FieldPyDsl' || 99 + // node['~dsl'] === 'GetterPyDsl' || 100 + // node['~dsl'] === 'MethodPyDsl' 101 + // ); 102 + // } 103 + 104 + // function isStopNode(node: PyDsl): boolean { 105 + // return node['~dsl'] === 'FuncPyDsl' || node['~dsl'] === 'TemplatePyDsl'; 106 + // } 107 + 108 + /** 109 + * Fold a structural chain to an access chain by removing 110 + * non-accessor nodes. 111 + */ 112 + // function structuralToAccessChain(structuralChain: NodeChain): NodeChain { 113 + // const accessChain: Array<PyDsl> = []; 114 + // structuralChain.forEach((node, index) => { 115 + // // assume first node is always included 116 + // if (index === 0) { 117 + // accessChain.push(node); 118 + // } else if (isAccessorNode(node)) { 119 + // accessChain.push(node); 120 + // } 121 + // }); 122 + // return accessChain; 123 + // } 124 + 125 + // function transformAccessChain(accessChain: NodeChain, options: AccessOptions = {}): NodeChain { 126 + // return accessChain.map((node, index) => { 127 + // const transformedNode = options.transform?.(node, index, accessChain); 128 + // if (transformedNode) return transformedNode; 129 + // const accessNode = node.toAccessNode?.(node, options, { 130 + // chain: accessChain, 131 + // index, 132 + // isLeaf: index === accessChain.length - 1, 133 + // isRoot: index === 0, 134 + // length: accessChain.length, 135 + // }); 136 + // if (accessNode) return accessNode; 137 + // if (index === 0) { 138 + // if (node['~dsl'] === 'ClassPyDsl') { 139 + // const nextNode = accessChain[index + 1]; 140 + // if (nextNode && isAccessorNode(nextNode)) { 141 + // if ((nextNode as ReturnType<typeof $.field>).hasModifier('static')) { 142 + // return $(node.name); 143 + // } 144 + // } 145 + // return $.new(node.name).args(); 146 + // } 147 + // return $(node.name); 148 + // } 149 + // return node; 150 + // }); 151 + // } 152 + 153 + export class PyDslContext { 154 + /** 155 + * Build an expression for accessing the node. 156 + * 157 + * @param node - The node or symbol to build access for 158 + * @param options - Access options 159 + * @returns Expression for accessing the node 160 + * 161 + * @example 162 + * ```ts 163 + * ctx.access(node); // → Expression for accessing the node 164 + * ``` 165 + */ 166 + // access<T = AccessResult>(node: PyDsl | Symbol<PyDsl>, options?: AccessOptions): T { 167 + // const n = isSymbol(node) ? node.node : node; 168 + // if (!n) { 169 + // throw new Error(`Symbol ${node.name} is not resolved to a node.`); 170 + // } 171 + // const accessChain = getAccessChainForNode(n); 172 + // const finalChain = transformAccessChain(accessChain, options); 173 + // return accessChainToNode<T>(finalChain); 174 + // } 175 + /** 176 + * Build an example. 177 + * 178 + * @param node - The node to generate an example for 179 + * @param options - Example options 180 + * @returns Full example string 181 + * 182 + * @example 183 + * ```ts 184 + * ctx.example(node, { moduleName: 'my-sdk' }); // → Full example string 185 + * ``` 186 + */ 187 + // example( 188 + // node: PyDsl, 189 + // options?: ExampleOptions, 190 + // astOptions?: Parameters<typeof PythonRenderer.astToString>[0], 191 + // ): string { 192 + // if (astOptions) { 193 + // return PythonRenderer.astToString(astOptions); 194 + // } 195 + // options ||= {}; 196 + // const accessChain = getAccessChainForNode(node); 197 + // if (options.importName) { 198 + // accessChain[0]!.name.set(options.importName); 199 + // } 200 + // const importNode = $(accessChain[0]!.name.toString()); // must store name before transform 201 + // const finalChain = transformAccessChain(accessChain, { 202 + // context: 'example', 203 + // }); 204 + // const setupNode = options.importSetup 205 + // ? typeof options.importSetup === 'function' 206 + // ? options.importSetup({ $, node: importNode }) 207 + // : options.importSetup 208 + // : (finalChain[0]! as PyDsl<py.Expression>); 209 + // const setupName = options.setupName; 210 + // let payload = typeof options.payload === 'function' ? options.payload({ $ }) : options.payload; 211 + // payload = payload instanceof Array ? payload : payload ? [payload] : []; 212 + // let nodes: Array<PyDsl> = []; 213 + // if (setupName) { 214 + // nodes = [ 215 + // $.const(setupName).assign(setupNode), 216 + // $.await(accessChainToNode([$(setupName), ...finalChain.slice(1)]).call(...payload)), 217 + // ]; 218 + // } else { 219 + // nodes = [$.await(accessChainToNode([setupNode, ...finalChain.slice(1)]).call(...payload))]; 220 + // } 221 + // const localName = importNode.name.toString(); 222 + // return PythonRenderer.astToString({ 223 + // imports: [ 224 + // [ 225 + // { 226 + // imports: 227 + // !options.importKind || options.importKind === 'named' 228 + // ? [ 229 + // { 230 + // isTypeOnly: false, 231 + // localName, 232 + // sourceName: localName, 233 + // }, 234 + // ] 235 + // : [], 236 + // isTypeOnly: false, 237 + // kind: options.importKind ?? 'named', 238 + // localName: options.importKind !== 'named' ? localName : undefined, 239 + // modulePath: options.moduleName ?? 'your-package', 240 + // }, 241 + // ], 242 + // ], 243 + // nodes, 244 + // trailingNewline: false, 245 + // }); 246 + // } 247 + } 248 + 249 + export const ctx = new PyDslContext();
+115
packages/openapi-python/src/py-dsl/utils/keywords.ts
··· 1 + const pythonKeywords = [ 2 + 'and', 3 + 'as', 4 + 'assert', 5 + 'async', 6 + 'await', 7 + 'break', 8 + 'class', 9 + 'continue', 10 + 'def', 11 + 'del', 12 + 'elif', 13 + 'else', 14 + 'except', 15 + 'False', 16 + 'finally', 17 + 'for', 18 + 'from', 19 + 'global', 20 + 'if', 21 + 'import', 22 + 'in', 23 + 'is', 24 + 'lambda', 25 + 'None', 26 + 'nonlocal', 27 + 'not', 28 + 'or', 29 + 'pass', 30 + 'raise', 31 + 'return', 32 + 'True', 33 + 'try', 34 + 'while', 35 + 'with', 36 + 'yield', 37 + ]; 38 + 39 + const pythonBuiltins = [ 40 + 'abs', 41 + 'aiter', 42 + 'all', 43 + 'anext', 44 + 'any', 45 + 'ascii', 46 + 'bin', 47 + 'bool', 48 + 'breakpoint', 49 + 'bytearray', 50 + 'bytes', 51 + 'callable', 52 + 'chr', 53 + 'classmethod', 54 + 'compile', 55 + 'complex', 56 + 'delattr', 57 + 'dict', 58 + 'dir', 59 + 'divmod', 60 + 'enumerate', 61 + 'eval', 62 + 'exec', 63 + 'filter', 64 + 'float', 65 + 'format', 66 + 'frozenset', 67 + 'getattr', 68 + 'globals', 69 + 'hasattr', 70 + 'hash', 71 + 'help', 72 + 'hex', 73 + 'id', 74 + 'input', 75 + 'int', 76 + 'isinstance', 77 + 'issubclass', 78 + 'iter', 79 + 'len', 80 + 'list', 81 + 'locals', 82 + 'map', 83 + 'max', 84 + 'memoryview', 85 + 'min', 86 + 'next', 87 + 'object', 88 + 'oct', 89 + 'open', 90 + 'ord', 91 + 'pow', 92 + 'print', 93 + 'property', 94 + 'range', 95 + 'repr', 96 + 'reversed', 97 + 'round', 98 + 'set', 99 + 'setattr', 100 + 'slice', 101 + 'sorted', 102 + 'staticmethod', 103 + 'str', 104 + 'sum', 105 + 'super', 106 + 'tuple', 107 + 'type', 108 + 'vars', 109 + 'zip', 110 + ]; 111 + 112 + export const keywords = { 113 + pythonBuiltins, 114 + pythonKeywords, 115 + };
+32
packages/openapi-python/src/py-dsl/utils/lazy.ts
··· 1 + import type { AnalysisContext } from '@hey-api/codegen-core'; 2 + 3 + import type { py } from '../../ts-python'; 4 + import { PyDsl } from '../base'; 5 + import type { PyDslContext } from './context'; 6 + import { ctx } from './context'; 7 + 8 + export type LazyThunk<T extends py.Node> = (ctx: PyDslContext) => PyDsl<T>; 9 + 10 + export class LazyPyDsl<T extends py.Node = py.Node> extends PyDsl<T> { 11 + readonly '~dsl' = 'LazyPyDsl'; 12 + 13 + private _thunk: LazyThunk<T>; 14 + 15 + constructor(thunk: LazyThunk<T>) { 16 + super(); 17 + this._thunk = thunk; 18 + } 19 + 20 + override analyze(ctx: AnalysisContext): void { 21 + super.analyze(ctx); 22 + ctx.analyze(this.toResult()); 23 + } 24 + 25 + toResult(): PyDsl<T> { 26 + return this._thunk(ctx); 27 + } 28 + 29 + override toAst(): T { 30 + return this.toResult().toAst(); 31 + } 32 + }
+45
packages/openapi-python/src/py-dsl/utils/name.ts
··· 1 + import { regexp } from './regexp'; 2 + import type { ReservedList } from './reserved'; 3 + import { reserved } from './reserved'; 4 + 5 + export const safeAccessorName = (name: string): string => { 6 + regexp.number.lastIndex = 0; 7 + if (regexp.number.test(name)) { 8 + return name.startsWith('-') ? `'${name}'` : name; 9 + } 10 + 11 + regexp.pythonIdentifier.lastIndex = 0; 12 + if (regexp.pythonIdentifier.test(name)) { 13 + return name; 14 + } 15 + return `'${name}'`; 16 + }; 17 + 18 + const safeName = (name: string, reserved: ReservedList): string => { 19 + let sanitized = ''; 20 + let index: number; 21 + 22 + const first = name[0] ?? ''; 23 + regexp.illegalStartCharacters.lastIndex = 0; 24 + if (regexp.illegalStartCharacters.test(first)) { 25 + sanitized += '_'; 26 + index = 0; 27 + } else { 28 + sanitized += first; 29 + index = 1; 30 + } 31 + 32 + while (index < name.length) { 33 + const char = name[index] ?? ''; 34 + sanitized += /^[a-zA-Z0-9_]$/.test(char) ? char : '_'; 35 + index += 1; 36 + } 37 + 38 + if (reserved['~values'].has(sanitized)) { 39 + sanitized = `${sanitized}_`; 40 + } 41 + 42 + return sanitized || '_'; 43 + }; 44 + 45 + export const safeRuntimeName = (name: string): string => safeName(name, reserved.runtime);
+41
packages/openapi-python/src/py-dsl/utils/regexp.ts
··· 1 + /** 2 + * Matches characters from the start that are not valid Python identifier starts. 3 + * Python identifiers: starts with letter/underscore, followed by letters/digits/underscores. 4 + */ 5 + const illegalStartCharactersRegExp = /^[^a-zA-Z_]+/; 6 + 7 + /** 8 + * Matches if string contains only digits and optionally decimal point or leading minus. 9 + */ 10 + const numberRegExp = /^-?\d+(\.\d+)?$/; 11 + 12 + /** 13 + * Python identifier pattern: starts with letter or underscore, 14 + * followed by letters, digits, or underscores. 15 + * Uses Unicode categories for full Python 3 compliance. 16 + */ 17 + const validPythonIdentifierRegExp = /^[a-zA-Z_][a-zA-Z0-9_]*$/u; 18 + 19 + /** 20 + * Matches if a string looks like a valid Python identifier. 21 + */ 22 + const looksLikeIdentifierRegExp = /^[a-zA-Z_][a-zA-Z0-9_]*$/; 23 + 24 + export const regexp = { 25 + /** 26 + * Matches characters from the start that are not valid Python identifier starts. 27 + */ 28 + illegalStartCharacters: illegalStartCharactersRegExp, 29 + /** 30 + * Simpler pattern for quick identifier checks. 31 + */ 32 + looksLikeIdentifier: looksLikeIdentifierRegExp, 33 + /** 34 + * Matches if string contains only digits and optionally decimal point or leading minus. 35 + */ 36 + number: numberRegExp, 37 + /** 38 + * Python identifier pattern for validation. 39 + */ 40 + pythonIdentifier: validPythonIdentifierRegExp, 41 + };
+88
packages/openapi-python/src/py-dsl/utils/render-utils.ts
··· 1 + import path from 'node:path'; 2 + 3 + import type { ExportModule, File, ImportModule } from '@hey-api/codegen-core'; 4 + 5 + import { py } from '../../ts-python'; 6 + 7 + const printer = py.createPrinter({ 8 + indentSize: 4, 9 + }); 10 + 11 + /** Print a Python node to a string. */ 12 + export function astToString(node: py.Node): string { 13 + const result = printer.printFile(node); 14 + return result; 15 + } 16 + 17 + export type SortGroup = number; 18 + export type SortDistance = number; 19 + export type SortModule = string; 20 + export type SortKey = [SortGroup, SortDistance, SortModule]; 21 + 22 + export type ModuleExport = Omit<ExportModule, 'from'> & { 23 + /** Module specifier for re-exports, e.g. `./foo`. */ 24 + modulePath: string; 25 + }; 26 + 27 + export type ModuleImport = Omit<ImportModule, 'from'> & { 28 + /** Module specifier for imports, e.g. `./foo`. */ 29 + modulePath: string; 30 + }; 31 + 32 + export function moduleSortKey({ 33 + file, 34 + fromFile, 35 + root, 36 + }: { 37 + file: Pick<File, 'finalPath'>; 38 + fromFile: Pick<File, 'finalPath' | 'extension' | 'external' | 'name'>; 39 + preferFileExtension: string; 40 + root: string; 41 + }): SortKey { 42 + const filePath = file.finalPath!.split(path.sep).join('/'); 43 + let modulePath = fromFile.finalPath!.split(path.sep).join('/'); 44 + 45 + // built-ins 46 + // TODO 47 + 48 + // external 49 + if (fromFile.external && !path.isAbsolute(modulePath)) { 50 + return [0, 0, modulePath]; 51 + } 52 + 53 + // outside project root 54 + if (!modulePath.startsWith(root.split(path.sep).join('/'))) { 55 + return [1, 0, modulePath]; 56 + } 57 + 58 + // local 59 + const rel = path 60 + .relative(path.dirname(filePath), path.dirname(modulePath)) 61 + .split(path.sep) 62 + .join('/'); 63 + 64 + const segments = rel ? rel.split('/') : []; 65 + const parentCount = segments.filter((s) => s === '..').length; 66 + 67 + const leadingDots = '.'.repeat(parentCount + 1); 68 + 69 + const pathSegments = segments.filter((s) => s !== '..' && s !== '.'); 70 + 71 + const filename = modulePath.split('/').at(-1)!; 72 + // TODO: replace with extension check, there's an issue with external files 73 + // not having extension set 74 + const moduleName = filename.replace(/\.[^.]+$/, ''); 75 + // const moduleName = fromFile.extension 76 + // ? filename.slice(0, -fromFile.extension.length) 77 + // : filename; 78 + 79 + // index/__init__ are implicit 80 + const isImplicitModule = moduleName === 'index' || moduleName === '__init__'; 81 + if (!isImplicitModule) { 82 + pathSegments.push(moduleName); 83 + } 84 + 85 + modulePath = pathSegments.length > 0 ? leadingDots + pathSegments.join('.') : leadingDots; 86 + 87 + return [2, parentCount, modulePath]; 88 + }
+311
packages/openapi-python/src/py-dsl/utils/render.ts
··· 1 + import type { RenderContext, Renderer } from '@hey-api/codegen-core'; 2 + import type { MaybeArray, MaybeFunc } from '@hey-api/types'; 3 + 4 + import type { PyDsl } from '../../py-dsl'; 5 + import { py } from '../../ts-python'; 6 + import type { ModuleExport, ModuleImport, SortGroup, SortKey, SortModule } from './render-utils'; 7 + import { astToString, moduleSortKey } from './render-utils'; 8 + 9 + type Exports = ReadonlyArray<ReadonlyArray<ModuleExport>>; 10 + type ExportsOptions = { 11 + preferExportAll?: boolean; 12 + }; 13 + type Header = MaybeArray<string> | null | undefined; 14 + type Imports = ReadonlyArray<ReadonlyArray<ModuleImport>>; 15 + 16 + function headerToLines(header: Header): ReadonlyArray<string> { 17 + if (!header) return []; 18 + const lines: Array<string> = []; 19 + if (typeof header === 'string') { 20 + lines.push(...header.split(/\r?\n/)); 21 + return lines; 22 + } 23 + for (const line of header) { 24 + lines.push(...line.split(/\r?\n/)); 25 + } 26 + return lines; 27 + } 28 + 29 + export class PythonRenderer implements Renderer { 30 + /** 31 + * Function to generate a file header. 32 + * 33 + * @private 34 + */ 35 + private _header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>; 36 + /** 37 + * Whether `export * from 'module'` should be used when possible instead of named exports. 38 + * 39 + * @private 40 + */ 41 + private _preferExportAll: boolean; 42 + /** 43 + * Controls whether imports/exports include a file extension (e.g., '.ts' or '.js'). 44 + * 45 + * @private 46 + */ 47 + private _preferFileExtension: string; 48 + /** 49 + * Optional function to transform module specifiers. 50 + * 51 + * @private 52 + */ 53 + private _resolveModuleName?: (moduleName: string) => string | undefined; 54 + 55 + constructor( 56 + args: { 57 + header?: MaybeFunc<(ctx: RenderContext<PyDsl>) => Header>; 58 + preferExportAll?: boolean; 59 + preferFileExtension?: string; 60 + resolveModuleName?: (moduleName: string) => string | undefined; 61 + } = {}, 62 + ) { 63 + this._header = args.header; 64 + this._preferExportAll = args.preferExportAll ?? false; 65 + this._preferFileExtension = args.preferFileExtension ?? ''; 66 + this._resolveModuleName = args.resolveModuleName; 67 + } 68 + 69 + render(ctx: RenderContext<PyDsl>): string { 70 + const header = typeof this._header === 'function' ? this._header(ctx) : this._header; 71 + return PythonRenderer.astToString({ 72 + // exports: this.getExports(ctx), 73 + exportsOptions: { 74 + preferExportAll: this._preferExportAll, 75 + }, 76 + header, 77 + imports: this.getImports(ctx), 78 + nodes: ctx.file.nodes, 79 + }); 80 + } 81 + 82 + supports(ctx: RenderContext): boolean { 83 + return ctx.file.language === 'python'; 84 + } 85 + 86 + static astToString(args: { 87 + exports?: Exports; 88 + exportsOptions?: ExportsOptions; 89 + header?: Header; 90 + imports?: Imports; 91 + nodes?: ReadonlyArray<PyDsl>; 92 + /** 93 + * Whether to include a trailing newline at the end of the file. 94 + * 95 + * @default true 96 + */ 97 + trailingNewline?: boolean; 98 + }): string { 99 + let text = ''; 100 + for (const header of headerToLines(args.header)) { 101 + text += `${header}\n`; 102 + } 103 + 104 + let imports = ''; 105 + for (const group of args.imports ?? []) { 106 + if (imports) imports += '\n'; 107 + for (const imp of group) { 108 + imports += `${astToString(PythonRenderer.toImportAst(imp))}\n`; 109 + } 110 + } 111 + text = `${text}${text && imports ? '\n' : ''}${imports}`; 112 + 113 + let nodes = ''; 114 + for (const node of args.nodes ?? []) { 115 + if (nodes) nodes += '\n'; 116 + nodes += `${astToString(node.toAst())}\n`; 117 + } 118 + text = `${text}${text && nodes ? '\n' : ''}${nodes}`; 119 + 120 + const exports = ''; 121 + // let exports = ''; 122 + // for (const group of args.exports ?? []) { 123 + // if ((!exports && nodes) || exports) exports += '\n'; 124 + // for (const exp of group) { 125 + // exports += `${astToString(PythonRenderer.toExportAst(exp, args.exportsOptions))}\n`; 126 + // } 127 + // } 128 + text = `${text}${text && exports ? '\n' : ''}${exports}`; 129 + 130 + if (args.trailingNewline === false && text.endsWith('\n')) { 131 + text = text.slice(0, -1); 132 + } 133 + 134 + return text; 135 + } 136 + 137 + // static toExportAst(group: ModuleExport, options?: ExportsOptions): ts.ExportDeclaration { 138 + // const specifiers = group.exports.map((exp) => { 139 + // const specifier = ts.factory.createExportSpecifier( 140 + // exp.isTypeOnly, 141 + // exp.sourceName !== exp.exportedName ? $.id(exp.sourceName).toAst() : undefined, 142 + // $.id(exp.exportedName).toAst(), 143 + // ); 144 + // return specifier; 145 + // }); 146 + // const exportClause = group.namespaceExport 147 + // ? ts.factory.createNamespaceExport($.id(group.namespaceExport).toAst()) 148 + // : (!group.canExportAll || !options?.preferExportAll) && specifiers.length 149 + // ? ts.factory.createNamedExports(specifiers) 150 + // : undefined; 151 + // return ts.factory.createExportDeclaration( 152 + // undefined, 153 + // group.isTypeOnly, 154 + // exportClause, 155 + // $.literal(group.modulePath).toAst(), 156 + // ); 157 + // } 158 + 159 + static toImportAst(group: ModuleImport): py.ImportStatement { 160 + const names: Array<{ 161 + alias?: string; 162 + name: string; 163 + }> = group.imports.map((imp) => ({ 164 + alias: imp.localName !== imp.sourceName ? imp.localName : undefined, 165 + name: imp.sourceName, 166 + })); 167 + return py.factory.createImportStatement(group.modulePath, names, group.imports.length > 0); 168 + } 169 + 170 + // private getExports(ctx: RenderContext): Exports { 171 + // type ModuleEntry = { 172 + // group: ModuleExport; 173 + // sortKey: SortKey; 174 + // }; 175 + 176 + // const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>(); 177 + 178 + // for (const exp of ctx.file.exports) { 179 + // const sortKey = moduleSortKey({ 180 + // file: ctx.file, 181 + // fromFile: exp.from, 182 + // preferFileExtension: this._preferFileExtension, 183 + // root: ctx.project.root, 184 + // }); 185 + // const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2]; 186 + // const [groupIndex] = sortKey; 187 + 188 + // if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); 189 + // const moduleMap = groups.get(groupIndex)!; 190 + 191 + // if (!moduleMap.has(modulePath)) { 192 + // moduleMap.set(modulePath, { 193 + // group: { 194 + // canExportAll: exp.canExportAll, 195 + // exports: exp.exports, 196 + // isTypeOnly: exp.isTypeOnly, 197 + // modulePath, 198 + // namespaceExport: exp.namespaceExport, 199 + // }, 200 + // sortKey, 201 + // }); 202 + // } 203 + // } 204 + 205 + // const exports: Array<Array<ModuleExport>> = Array.from(groups.entries()) 206 + // .sort((a, b) => a[0] - b[0]) 207 + // .map(([, moduleMap]) => { 208 + // const entries = Array.from(moduleMap.values()); 209 + 210 + // entries.sort((a, b) => { 211 + // const d = a.sortKey[1] - b.sortKey[1]; 212 + // return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath); 213 + // }); 214 + 215 + // return entries.map((e) => { 216 + // const group = e.group; 217 + // if (group.namespaceExport) { 218 + // group.exports = []; 219 + // } else { 220 + // const isTypeOnly = !group.exports.find((exp) => !exp.isTypeOnly); 221 + // if (isTypeOnly) { 222 + // group.isTypeOnly = true; 223 + // for (const exp of group.exports) { 224 + // exp.isTypeOnly = false; 225 + // } 226 + // } 227 + // group.exports.sort((a, b) => a.exportedName.localeCompare(b.exportedName)); 228 + // } 229 + // return group; 230 + // }); 231 + // }); 232 + 233 + // return exports; 234 + // } 235 + 236 + private getImports(ctx: RenderContext): Imports { 237 + type ModuleEntry = { 238 + group: ModuleImport; 239 + sortKey: SortKey; 240 + }; 241 + 242 + const groups = new Map<SortGroup, Map<SortModule, ModuleEntry>>(); 243 + 244 + for (const imp of ctx.file.imports) { 245 + const sortKey = moduleSortKey({ 246 + file: ctx.file, 247 + fromFile: imp.from, 248 + preferFileExtension: this._preferFileExtension, 249 + root: ctx.project.root, 250 + }); 251 + const modulePath = this._resolveModuleName?.(sortKey[2]) ?? sortKey[2]; 252 + const [groupIndex] = sortKey; 253 + 254 + if (!groups.has(groupIndex)) groups.set(groupIndex, new Map()); 255 + const moduleMap = groups.get(groupIndex)!; 256 + 257 + if (!moduleMap.has(modulePath)) { 258 + moduleMap.set(modulePath, { 259 + group: { 260 + imports: [], 261 + isTypeOnly: false, 262 + kind: imp.kind, 263 + modulePath, 264 + }, 265 + sortKey, 266 + }); 267 + } 268 + 269 + const entry = moduleMap.get(modulePath)!; 270 + const group = entry.group; 271 + 272 + if (imp.kind !== 'named') { 273 + group.isTypeOnly = imp.isTypeOnly; 274 + group.kind = imp.kind; 275 + group.localName = imp.localName; 276 + } else { 277 + group.imports.push(...imp.imports); 278 + } 279 + } 280 + 281 + const imports: Array<Array<ModuleImport>> = Array.from(groups.entries()) 282 + .sort((a, b) => a[0] - b[0]) 283 + .map(([, moduleMap]) => { 284 + const entries = Array.from(moduleMap.values()); 285 + 286 + entries.sort((a, b) => { 287 + const d = a.sortKey[1] - b.sortKey[1]; 288 + return d !== 0 ? d : a.group.modulePath.localeCompare(b.group.modulePath); 289 + }); 290 + 291 + return entries.map((e) => { 292 + const group = e.group; 293 + if (group.kind === 'namespace') { 294 + group.imports = []; 295 + } else { 296 + const isTypeOnly = !group.imports.find((imp) => !imp.isTypeOnly); 297 + if (isTypeOnly) { 298 + group.isTypeOnly = true; 299 + for (const imp of group.imports) { 300 + imp.isTypeOnly = false; 301 + } 302 + } 303 + group.imports.sort((a, b) => a.localName.localeCompare(b.localName)); 304 + } 305 + return group; 306 + }); 307 + }); 308 + 309 + return imports; 310 + } 311 + }
+44
packages/openapi-python/src/py-dsl/utils/reserved.ts
··· 1 + import { keywords } from './keywords'; 2 + 3 + type List = ReadonlyArray<string>; 4 + 5 + export class ReservedList { 6 + private _array: List; 7 + private _set: Set<string>; 8 + 9 + constructor(values: List) { 10 + this._array = values; 11 + this._set = new Set(values); 12 + } 13 + 14 + get '~values'() { 15 + return this._set; 16 + } 17 + 18 + /** 19 + * Updates the reserved list with new values. 20 + * 21 + * @param values New reserved values or a function that receives the previous 22 + * reserved values and returns the new ones. 23 + */ 24 + set(values: List | ((prev: List) => List)): void { 25 + const vals = typeof values === 'function' ? values(this._array) : values; 26 + this._array = vals; 27 + this._set = new Set(vals); 28 + } 29 + } 30 + 31 + const runtimeReserved = new ReservedList([...keywords.pythonKeywords, ...keywords.pythonBuiltins]); 32 + 33 + /** 34 + * Reserved names for identifiers. These names will not be used 35 + * for variables, functions, classes, or other identifiers in generated code. 36 + */ 37 + export const reserved = { 38 + /** 39 + * Reserved names for runtime identifiers. These names will not be used 40 + * for variables, functions, classes, or other runtime identifiers in 41 + * generated code. 42 + */ 43 + runtime: runtimeReserved, 44 + };
-2
packages/openapi-python/src/ts-python/__tests__/nodes/declarations/class.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/declarations/function.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/await.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/binary.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/call.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/dict.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/list.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/nested.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/comprehensions/set.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/dict.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/fString.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/generator.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/identifier.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/lambda.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/list.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/literal.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/set.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/tuple.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/expressions/yield.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/assignment.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/augmentedAssignment.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/block.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../..'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/break.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/continue.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/expression.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/for.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/if.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/import.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/raise.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/return.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/try.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/while.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/statements/with.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/structure/comment.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
-2
packages/openapi-python/src/ts-python/__tests__/nodes/structure/sourceFile.test.ts
··· 1 - import { describe, it } from 'vitest'; 2 - 3 1 import { py } from '../../../index'; 4 2 import { assertPrintedMatchesSnapshot } from '../utils'; 5 3
+1 -4
packages/openapi-python/src/ts-python/__tests__/nodes/utils.ts
··· 1 1 import fs from 'node:fs'; 2 2 import path from 'node:path'; 3 3 4 - import { expect } from 'vitest'; 5 - 6 4 import { py } from '../../index'; 7 - import type { PySourceFile } from '../../nodes/structure/sourceFile'; 8 5 import { snapshotsDir, tmpDir } from '../constants'; 9 6 10 7 function getCallerFile(): string { ··· 38 35 } 39 36 40 37 export async function assertPrintedMatchesSnapshot( 41 - file: PySourceFile, 38 + file: py.SourceFile, 42 39 filename: string, 43 40 ): Promise<void> { 44 41 const result = py.createPrinter().printFile(file);
+123
packages/openapi-python/src/ts-python/index.ts
··· 1 + import type { PyNode as _PyNode, PyNodeBase as _PyNodeBase } from './nodes/base'; 2 + import type { 3 + PyComprehension as _PyComprehension, 4 + PyComprehensionNode as _PyComprehensionNode, 5 + } from './nodes/comprehension'; 6 + import type { PyClassDeclaration as _PyClassDeclaration } from './nodes/declarations/class'; 7 + import type { PyFunctionDeclaration as _PyFunctionDeclaration } from './nodes/declarations/function'; 8 + import type { PyFunctionParameter as _PyFunctionParameter } from './nodes/declarations/functionParameter'; 9 + import type { PyExpression as _PyExpression } from './nodes/expression'; 10 + import type { PyAsyncExpression as _PyAsyncExpression } from './nodes/expressions/async'; 11 + import type { PyAwaitExpression as _PyAwaitExpression } from './nodes/expressions/await'; 12 + import type { 13 + PyBinaryExpression as _PyBinaryExpression, 14 + PyBinaryOperator as _PyBinaryOperator, 15 + } from './nodes/expressions/binary'; 16 + import type { PyCallExpression as _PyCallExpression } from './nodes/expressions/call'; 17 + import type { PyDictComprehension as _PyDictComprehension } from './nodes/expressions/comprehensions/dict'; 18 + import type { PyListComprehension as _PyListComprehension } from './nodes/expressions/comprehensions/list'; 19 + import type { PySetComprehension as _PySetComprehension } from './nodes/expressions/comprehensions/set'; 20 + import type { PyDictExpression as _PyDictExpression } from './nodes/expressions/dict'; 21 + import type { PyFStringExpression as _PyFStringExpression } from './nodes/expressions/fString'; 22 + import type { PyGeneratorExpression as _PyGeneratorExpression } from './nodes/expressions/generator'; 23 + import type { PyIdentifier as _PyIdentifier } from './nodes/expressions/identifier'; 24 + import type { PyLambdaExpression as _PyLambdaExpression } from './nodes/expressions/lambda'; 25 + import type { PyListExpression as _PyListExpression } from './nodes/expressions/list'; 26 + import type { PyLiteral as _PyLiteral } from './nodes/expressions/literal'; 27 + import type { PyMemberExpression as _PyMemberExpression } from './nodes/expressions/member'; 28 + import type { PySetExpression as _PySetExpression } from './nodes/expressions/set'; 29 + import type { PyTupleExpression as _PyTupleExpression } from './nodes/expressions/tuple'; 30 + import type { PyYieldExpression as _PyYieldExpression } from './nodes/expressions/yield'; 31 + import type { PyYieldFromExpression as _PyYieldFromExpression } from './nodes/expressions/yieldFrom'; 1 32 import { factory } from './nodes/factory'; 2 33 import { PyNodeKind } from './nodes/kinds'; 34 + import type { PyStatement as _PyStatement } from './nodes/statement'; 35 + import type { PyAssignment as _PyAssignment } from './nodes/statements/assignment'; 36 + import type { 37 + PyAugmentedAssignment as _PyAugmentedAssignment, 38 + PyAugmentedOperator as _PyAugmentedOperator, 39 + } from './nodes/statements/augmentedAssignment'; 40 + import type { PyBlock as _PyBlock } from './nodes/statements/block'; 41 + import type { PyBreakStatement as _PyBreakStatement } from './nodes/statements/break'; 42 + import type { PyContinueStatement as _PyContinueStatement } from './nodes/statements/continue'; 43 + import type { PyEmptyStatement as _PyEmptyStatement } from './nodes/statements/empty'; 44 + import type { PyExceptClause as _PyExceptClause } from './nodes/statements/except'; 45 + import type { PyExpressionStatement as _PyExpressionStatement } from './nodes/statements/expression'; 46 + import type { PyForStatement as _PyForStatement } from './nodes/statements/for'; 47 + import type { PyIfStatement as _PyIfStatement } from './nodes/statements/if'; 48 + import type { PyImportStatement as _PyImportStatement } from './nodes/statements/import'; 49 + import type { PyRaiseStatement as _PyRaiseStatement } from './nodes/statements/raise'; 50 + import type { PyReturnStatement as _PyReturnStatement } from './nodes/statements/return'; 51 + import type { PyTryStatement as _PyTryStatement } from './nodes/statements/try'; 52 + import type { PyWhileStatement as _PyWhileStatement } from './nodes/statements/while'; 53 + import type { PyWithStatement as _PyWithStatement } from './nodes/statements/with'; 54 + import type { PyWithItem as _PyWithItem } from './nodes/statements/withItem'; 55 + import type { PyComment as _PyComment } from './nodes/structure/comment'; 56 + import type { PySourceFile as _PySourceFile } from './nodes/structure/sourceFile'; 57 + import type { PyPrinterOptions as _PyPrinterOptions } from './printer'; 3 58 import { createPrinter, printAst } from './printer'; 59 + 60 + // eslint-disable-next-line @typescript-eslint/no-namespace 61 + export namespace py { 62 + // Base / Core 63 + export type Node = _PyNode; 64 + export type NodeBase = _PyNodeBase; 65 + export type NodeKind = PyNodeKind; 66 + export type Expression = _PyExpression; 67 + export type Statement = _PyStatement; 68 + 69 + // Structure 70 + export type SourceFile = _PySourceFile; 71 + export type Comment = _PyComment; 72 + 73 + // Declarations 74 + export type ClassDeclaration = _PyClassDeclaration; 75 + export type FunctionDeclaration = _PyFunctionDeclaration; 76 + export type FunctionParameter = _PyFunctionParameter; 77 + 78 + // Statements 79 + export type Assignment = _PyAssignment; 80 + export type AugmentedAssignment = _PyAugmentedAssignment; 81 + export type AugmentedOperator = _PyAugmentedOperator; 82 + export type Block = _PyBlock; 83 + export type BreakStatement = _PyBreakStatement; 84 + export type ContinueStatement = _PyContinueStatement; 85 + export type EmptyStatement = _PyEmptyStatement; 86 + export type ExceptClause = _PyExceptClause; 87 + export type ExpressionStatement = _PyExpressionStatement; 88 + export type ForStatement = _PyForStatement; 89 + export type IfStatement = _PyIfStatement; 90 + export type ImportStatement = _PyImportStatement; 91 + export type RaiseStatement = _PyRaiseStatement; 92 + export type ReturnStatement = _PyReturnStatement; 93 + export type TryStatement = _PyTryStatement; 94 + export type WhileStatement = _PyWhileStatement; 95 + export type WithItem = _PyWithItem; 96 + export type WithStatement = _PyWithStatement; 97 + 98 + // Expressions 99 + export type AsyncExpression = _PyAsyncExpression; 100 + export type AwaitExpression = _PyAwaitExpression; 101 + export type BinaryExpression = _PyBinaryExpression; 102 + export type BinaryOperator = _PyBinaryOperator; 103 + export type CallExpression = _PyCallExpression; 104 + export type DictExpression = _PyDictExpression; 105 + export type FStringExpression = _PyFStringExpression; 106 + export type GeneratorExpression = _PyGeneratorExpression; 107 + export type Identifier = _PyIdentifier; 108 + export type LambdaExpression = _PyLambdaExpression; 109 + export type ListExpression = _PyListExpression; 110 + export type Literal = _PyLiteral; 111 + export type MemberExpression = _PyMemberExpression; 112 + export type SetExpression = _PySetExpression; 113 + export type TupleExpression = _PyTupleExpression; 114 + export type YieldExpression = _PyYieldExpression; 115 + export type YieldFromExpression = _PyYieldFromExpression; 116 + 117 + // Comprehensions 118 + export type Comprehension = _PyComprehension; 119 + export type ComprehensionNode = _PyComprehensionNode; 120 + export type DictComprehension = _PyDictComprehension; 121 + export type ListComprehension = _PyListComprehension; 122 + export type SetComprehension = _PySetComprehension; 123 + 124 + // Printer 125 + export type PrinterOptions = _PyPrinterOptions; 126 + } 4 127 5 128 export const py = { 6 129 PyNodeKind,
+20 -6
packages/openapi-python/src/ts-python/printer.ts
··· 10 10 11 11 let indentLevel = 0; 12 12 13 - function processComments( 13 + function printComments( 14 14 parts: Array<string>, 15 15 lines: ReadonlyArray<string>, 16 16 indent?: boolean, ··· 20 20 if (indent) indentLevel -= 1; 21 21 } 22 22 23 + function printDocstring(docstring: string): Array<string> { 24 + const lines = docstring.split('\n'); 25 + const parts: Array<string> = []; 26 + if (lines.length === 1) { 27 + parts.push(printLine(`"""${lines[0]}"""`), ''); 28 + } else { 29 + parts.push(printLine(`"""`)); 30 + parts.push(...lines.map((line) => printLine(line))); 31 + parts.push(printLine(`"""`), ''); 32 + } 33 + return parts; 34 + } 35 + 23 36 function printLine(line: string): string { 37 + if (line === '') return ''; 24 38 return ' '.repeat(indentLevel * indentSize) + line; 25 39 } 26 40 ··· 28 42 const parts: Array<string> = []; 29 43 30 44 if (node.leadingComments) { 31 - processComments(parts, node.leadingComments); 45 + printComments(parts, node.leadingComments); 32 46 } 33 47 34 48 let indentTrailingComments = false; ··· 85 99 parts.push(printLine(`class ${node.name}${bases}:`)); 86 100 if (node.docstring) { 87 101 indentLevel += 1; 88 - parts.push(printLine(`"""${node.docstring}"""`), ''); 102 + parts.push(...printDocstring(node.docstring)); 89 103 indentLevel -= 1; 90 104 } 91 105 parts.push(printNode(node.body)); ··· 165 179 ); 166 180 if (node.docstring) { 167 181 indentLevel += 1; 168 - parts.push(printLine(`"""${node.docstring}"""`), ''); 182 + parts.push(...printDocstring(node.docstring)); 169 183 indentLevel -= 1; 170 184 } 171 185 parts.push(printNode(node.body)); ··· 309 323 310 324 case PyNodeKind.SourceFile: 311 325 if (node.docstring) { 312 - parts.push(printLine(`"""${node.docstring}"""`), ''); 326 + parts.push(...printDocstring(node.docstring)); 313 327 } 314 328 parts.push(...node.statements.map(printNode)); 315 329 break; ··· 382 396 } 383 397 384 398 if (node.trailingComments) { 385 - processComments(parts, node.trailingComments, indentTrailingComments); 399 + printComments(parts, node.trailingComments, indentTrailingComments); 386 400 } 387 401 388 402 return parts.join('\n');
+32 -3
packages/openapi-python/tsdown.config.ts
··· 1 + import fs from 'node:fs'; 2 + import path from 'node:path'; 3 + import { fileURLToPath } from 'node:url'; 4 + 1 5 import { defineConfig } from 'tsdown'; 2 6 7 + const __dirname = path.dirname(fileURLToPath(import.meta.url)); 8 + 3 9 export default defineConfig({ 4 10 clean: true, 5 - dts: { 6 - build: true, 7 - }, 11 + dts: true, 8 12 entry: ['./src/{index,run}.ts'], 9 13 format: ['esm'], 10 14 minify: false, 15 + onSuccess: async () => { 16 + // Copy client files to dist folder for runtime access 17 + const pluginNames = ['client-httpx']; 18 + 19 + for (const pluginName of pluginNames) { 20 + const srcPath = path.resolve(__dirname, 'src', 'plugins', '@hey-api', pluginName, 'bundle'); 21 + const destPath = path.resolve( 22 + __dirname, 23 + 'dist', 24 + 'clients', 25 + pluginName.slice('client-'.length), 26 + ); 27 + 28 + if (fs.existsSync(srcPath)) { 29 + fs.mkdirSync(path.dirname(destPath), { recursive: true }); 30 + fs.cpSync(srcPath, destPath, { recursive: true }); 31 + 32 + // replace core imports in client bundle 33 + // const clientFiles = fs.readdirSync(destPath); 34 + // for (const file of clientFiles) { 35 + // replaceCoreImports(path.resolve(destPath, file)); 36 + // } 37 + } 38 + } 39 + }, 11 40 sourcemap: true, 12 41 treeshake: true, 13 42 });
+3 -3
packages/openapi-ts-tests/sdks/test/method-class-conflict.test.ts
··· 23 23 config: createConfig({ 24 24 input: specPath, 25 25 output: { 26 - indexFile: false, 26 + entryFile: false, 27 27 path: 'class', 28 28 }, 29 29 plugins: [ ··· 40 40 config: createConfig({ 41 41 input: specPath, 42 42 output: { 43 - indexFile: false, 43 + entryFile: false, 44 44 path: 'flat', 45 45 }, 46 46 plugins: [ ··· 57 57 config: createConfig({ 58 58 input: specPath, 59 59 output: { 60 - indexFile: false, 60 + entryFile: false, 61 61 path: 'instance', 62 62 }, 63 63 plugins: [
+1 -1
packages/openapi-ts/src/config/output/config.ts
··· 28 28 const output = valueToObject({ 29 29 defaultValue: { 30 30 clean: true, 31 + entryFile: true, 31 32 fileName: { 32 33 case: 'preserve', 33 34 name: '{{name}}', ··· 35 36 }, 36 37 format: null, 37 38 header: '// This file is auto-generated by @hey-api/openapi-ts', 38 - indexFile: true, 39 39 lint: null, 40 40 path: '', 41 41 postProcess: [],
+3 -1
packages/openapi-ts/src/config/utils.ts
··· 2 2 3 3 import type { Config } from './types'; 4 4 5 - export function getTypedConfig(plugin: PluginInstance | Context): Config { 5 + export function getTypedConfig( 6 + plugin: Pick<PluginInstance, 'context'> | Pick<Context, 'config'>, 7 + ): Config { 6 8 if ('context' in plugin) { 7 9 return plugin.context.config as Config; 8 10 }
+4
packages/openapi-ts/src/index.ts
··· 128 128 /** 129 129 * Type helper for configuration object, returns {@link MaybeArray<UserConfig>} object(s) 130 130 */ 131 + export function defineConfig( 132 + config: LazyOrAsync<ReadonlyArray<UserConfig>>, 133 + ): Promise<ReadonlyArray<UserConfig>>; 134 + export function defineConfig(config: LazyOrAsync<UserConfig>): Promise<UserConfig>; 131 135 export async function defineConfig<T extends MaybeArray<UserConfig>>( 132 136 config: LazyOrAsync<T>, 133 137 ): Promise<T> {
+1 -1
packages/openapi-ts/src/plugins/@angular/common/config.ts
··· 7 7 8 8 export const defaultConfig: AngularCommonPlugin['Config'] = { 9 9 config: { 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/client-angular', '@hey-api/sdk'], 13 13 handler,
+4 -11
packages/openapi-ts/src/plugins/@angular/common/types.ts
··· 1 - import type { IndexExportOption } from '@hey-api/shared'; 2 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 3 - import type { OperationsStrategy } from '@hey-api/shared'; 1 + import type { DefinePlugin, OperationsStrategy, Plugin } from '@hey-api/shared'; 4 2 5 3 import type { HttpRequestsConfig, UserHttpRequestsConfig } from './httpRequests'; 6 4 import type { HttpResourcesConfig, UserHttpResourcesConfig } from './httpResources'; 7 5 8 6 export type UserConfig = Plugin.Name<'@angular/common'> & 9 - Plugin.Hooks & { 10 - /** 11 - * Whether exports should be re-exported in the index file. 12 - * 13 - * @default false 14 - */ 15 - exportFromIndex?: boolean; 7 + Plugin.Hooks & 8 + Plugin.UserExports & { 16 9 /** 17 10 * Options for generating HTTP Request instances. 18 11 * ··· 29 22 30 23 export type Config = Plugin.Name<'@angular/common'> & 31 24 Plugin.Hooks & 32 - IndexExportOption & { 25 + Plugin.Exports & { 33 26 /** 34 27 * Options for generating HTTP Request instances. 35 28 */
+1 -1
packages/openapi-ts/src/plugins/@faker-js/faker/config.ts
··· 8 8 api: new Api(), 9 9 config: { 10 10 case: 'camelCase', 11 - exportFromIndex: false, 11 + includeInEntry: false, 12 12 }, 13 13 // handler, 14 14 handler: () => {},
+1 -1
packages/openapi-ts/src/plugins/@faker-js/faker/resolvers/types.ts
··· 1 1 // TODO: later 2 2 import type { Symbol } from '@hey-api/codegen-core'; 3 + import type { Plugin, SchemaWithType } from '@hey-api/shared'; 3 4 4 - import type { Plugin, SchemaWithType } from '../../../../plugins'; 5 5 import type { $, DollarTsDsl } from '../../../../ts-dsl'; 6 6 import type { FakerJsFakerPlugin } from '../types'; 7 7
+4 -15
packages/openapi-ts/src/plugins/@faker-js/faker/types.ts
··· 1 - import type { 2 - Casing, 3 - FeatureToggle, 4 - IndexExportOption, 5 - NameTransformer, 6 - NamingOptions, 7 - } from '@hey-api/shared'; 1 + import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey-api/shared'; 8 2 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 9 3 10 4 import type { IApi } from './api'; 11 5 12 6 export type UserConfig = Plugin.Name<'@faker-js/faker'> & 13 - Plugin.Hooks & { 7 + Plugin.Hooks & 8 + Plugin.UserExports & { 14 9 // Resolvers & { 15 10 /** 16 11 * Casing convention for generated names. ··· 50 45 name?: NameTransformer; 51 46 }; 52 47 /** 53 - * Whether exports should be re-exported in the index file. 54 - * 55 - * @default false 56 - */ 57 - exportFromIndex?: boolean; 58 - /** 59 48 * Faker locale for generated data. 60 49 * 61 50 * @default 'en' ··· 70 59 71 60 export type Config = Plugin.Name<'@faker-js/faker'> & 72 61 Plugin.Hooks & 73 - IndexExportOption & { 62 + Plugin.Exports & { 74 63 // Resolvers & { 75 64 /** 76 65 * Casing convention for generated names.
+1 -1
packages/openapi-ts/src/plugins/@hey-api/client-core/config.ts
··· 1 1 export const clientDefaultConfig = { 2 2 baseUrl: true, 3 3 bundle: true, 4 - exportFromIndex: false, 4 + includeInEntry: false, 5 5 } as const; 6 6 7 7 export const clientDefaultMeta = {
+48 -52
packages/openapi-ts/src/plugins/@hey-api/client-core/types.ts
··· 1 1 /* eslint-disable @typescript-eslint/no-namespace */ 2 - import type { Plugin } from '../../../plugins'; 2 + import type { Plugin } from '@hey-api/shared'; 3 + 3 4 import type { HeyApiClientAngularPlugin } from '../../../plugins/@hey-api/client-angular'; 4 5 import type { HeyApiClientAxiosPlugin } from '../../../plugins/@hey-api/client-axios'; 5 6 import type { HeyApiClientFetchPlugin } from '../../../plugins/@hey-api/client-fetch'; ··· 20 21 * Public Client API. 21 22 */ 22 23 export namespace Client { 23 - export type Config = Plugin.Hooks & { 24 - /** 25 - * Set a default base URL when creating the client? You can set `baseUrl` 26 - * to a string which will be used as the base URL. If your input defines 27 - * server(s), you can set `baseUrl` to a number to pick a specific server 28 - * to use as the base URL. You can disable setting the base URL by setting 29 - * `baseUrl` to `false`. By default, `baseUrl` is `true` and it will try to 30 - * use the first defined server value. If there's none, we won't set a 31 - * base URL. 32 - * 33 - * If the matched URL contains template literals, it will be ignored. 34 - * 35 - * @default true 36 - */ 37 - baseUrl?: string | number | boolean; 38 - /** 39 - * Bundle the client module? When `true`, the client module will be copied 40 - * from the client plugin and bundled with the generated output. 41 - * 42 - * @default true 43 - */ 44 - bundle?: boolean; 45 - /** 46 - * Whether exports should be re-exported in the index file. 47 - * 48 - * @default false 49 - */ 50 - exportFromIndex?: boolean; 51 - /** 52 - * Relative path to the runtime configuration file. This file must export 53 - * a `createClientConfig()` function. The `createClientConfig()` function 54 - * will be called on client initialization and the returned object will 55 - * become the client's initial configuration. 56 - * 57 - * You may want to initialize your client this way instead of calling 58 - * `setConfig()`. This is useful for example if you're using Next.js 59 - * to ensure your client always has the correct values. 60 - */ 61 - runtimeConfigPath?: string; 62 - /** 63 - * Should the type helper for base URL allow only values matching the 64 - * server(s) defined in the input? By default, `strictBaseUrl` is `false` 65 - * which will provide type hints and allow you to pass any string. 66 - * 67 - * Note that setting `strictBaseUrl` to `true` can produce an invalid 68 - * build if you specify `baseUrl` which doesn't conform to the type helper. 69 - * 70 - * @default false 71 - */ 72 - strictBaseUrl?: boolean; 73 - }; 24 + export type Config = Plugin.Hooks & 25 + Plugin.UserExports & { 26 + /** 27 + * Set a default base URL when creating the client? You can set `baseUrl` 28 + * to a string which will be used as the base URL. If your input defines 29 + * server(s), you can set `baseUrl` to a number to pick a specific server 30 + * to use as the base URL. You can disable setting the base URL by setting 31 + * `baseUrl` to `false`. By default, `baseUrl` is `true` and it will try to 32 + * use the first defined server value. If there's none, we won't set a 33 + * base URL. 34 + * 35 + * If the matched URL contains template literals, it will be ignored. 36 + * 37 + * @default true 38 + */ 39 + baseUrl?: string | number | boolean; 40 + /** 41 + * Bundle the client module? When `true`, the client module will be copied 42 + * from the client plugin and bundled with the generated output. 43 + * 44 + * @default true 45 + */ 46 + bundle?: boolean; 47 + /** 48 + * Relative path to the runtime configuration file. This file must export 49 + * a `createClientConfig()` function. The `createClientConfig()` function 50 + * will be called on client initialization and the returned object will 51 + * become the client's initial configuration. 52 + * 53 + * You may want to initialize your client this way instead of calling 54 + * `setConfig()`. This is useful for example if you're using Next.js 55 + * to ensure your client always has the correct values. 56 + */ 57 + runtimeConfigPath?: string; 58 + /** 59 + * Should the type helper for base URL allow only values matching the 60 + * server(s) defined in the input? By default, `strictBaseUrl` is `false` 61 + * which will provide type hints and allow you to pass any string. 62 + * 63 + * Note that setting `strictBaseUrl` to `true` can produce an invalid 64 + * build if you specify `baseUrl` which doesn't conform to the type helper. 65 + * 66 + * @default false 67 + */ 68 + strictBaseUrl?: boolean; 69 + }; 74 70 }
+5 -5
packages/openapi-ts/src/plugins/@hey-api/client-core/utils.ts
··· 1 1 import type { Config } from '../../../config/types'; 2 2 import type { PluginClientNames } from '../../../plugins/types'; 3 3 4 - export const getClientBaseUrlKey = (config: Config) => { 4 + export function getClientBaseUrlKey(config: Config) { 5 5 const client = getClientPlugin(config); 6 6 if (client.name === '@hey-api/client-axios' || client.name === '@hey-api/client-nuxt') { 7 7 return 'baseURL'; 8 8 } 9 9 return 'baseUrl'; 10 - }; 10 + } 11 11 12 - export const getClientPlugin = ( 12 + export function getClientPlugin( 13 13 config: Config, 14 - ): Config['plugins'][PluginClientNames] & { name: PluginClientNames } => { 14 + ): Config['plugins'][PluginClientNames] & { name: PluginClientNames } { 15 15 for (const name of config.pluginOrder) { 16 16 const plugin = config.plugins[name]; 17 17 if (plugin?.tags?.includes('client')) { ··· 29 29 // @ts-expect-error 30 30 name: '', 31 31 }; 32 - }; 32 + }
+1 -1
packages/openapi-ts/src/plugins/@hey-api/schemas/config.ts
··· 5 5 6 6 export const defaultConfig: HeyApiSchemasPlugin['Config'] = { 7 7 config: { 8 - exportFromIndex: false, 8 + includeInEntry: false, 9 9 nameBuilder: (name) => `${name}Schema`, 10 10 type: 'json', 11 11 },
+2 -7
packages/openapi-ts/src/plugins/@hey-api/schemas/types.ts
··· 7 7 } from '@hey-api/shared'; 8 8 9 9 export type UserConfig = Plugin.Name<'@hey-api/schemas'> & 10 - Plugin.Hooks & { 11 - /** 12 - * Whether exports should be re-exported in the index file. 13 - * 14 - * @default false 15 - */ 16 - exportFromIndex?: boolean; 10 + Plugin.Hooks & 11 + Plugin.UserExports & { 17 12 /** 18 13 * Customise the schema name. By default, `{{name}}Schema` is used. `name` is a 19 14 * valid JavaScript/TypeScript identifier, e.g. if your schema name is
+1 -1
packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts
··· 9 9 config: { 10 10 auth: true, 11 11 client: true, 12 - exportFromIndex: true, 12 + includeInEntry: true, 13 13 paramsStructure: 'grouped', 14 14 responseStyle: 'fields', 15 15 transformer: false,
+11 -14
packages/openapi-ts/src/plugins/@hey-api/sdk/types.ts
··· 1 - import type { IndexExportOption, NameTransformer } from '@hey-api/shared'; 2 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 3 - import type { OperationsStrategy } from '@hey-api/shared'; 1 + import type { DefinePlugin, NameTransformer, OperationsStrategy, Plugin } from '@hey-api/shared'; 4 2 5 - import type { PluginClientNames, PluginValidatorNames } from '../../../plugins/types'; 3 + import type { 4 + PluginClientNames, 5 + PluginTransformerNames, 6 + PluginValidatorNames, 7 + } from '../../../plugins/types'; 6 8 import type { ExamplesConfig, UserExamplesConfig } from './examples'; 7 9 import type { OperationsConfig, UserOperationsConfig } from './operations'; 8 10 9 11 export type UserConfig = Plugin.Name<'@hey-api/sdk'> & 10 - Plugin.Hooks & { 12 + Plugin.Hooks & 13 + Plugin.UserExports & { 11 14 /** 12 15 * Should the generated functions contain auth mechanisms? You may want to 13 16 * disable this option if you're handling auth yourself or defining it ··· 39 42 */ 40 43 examples?: boolean | UserExamplesConfig; 41 44 /** 42 - * Whether exports should be re-exported in the index file. 43 - * 44 - * @default true 45 - */ 46 - exportFromIndex?: boolean; 47 - /** 48 45 * Define the structure of generated SDK operations. 49 46 * 50 47 * String shorthand: ··· 88 85 * 89 86 * @default false 90 87 */ 91 - transformer?: '@hey-api/transformers' | boolean; 88 + transformer?: PluginTransformerNames | boolean; 92 89 /** 93 90 * Validate request and/or response data against schema before returning. 94 91 * This is useful if you want to ensure the request and/or response conforms ··· 199 196 200 197 export type Config = Plugin.Name<'@hey-api/sdk'> & 201 198 Plugin.Hooks & 202 - IndexExportOption & { 199 + Plugin.Exports & { 203 200 /** 204 201 * Should the generated functions contain auth mechanisms? You may want to 205 202 * disable this option if you're handling auth yourself or defining it ··· 258 255 * 259 256 * @default false 260 257 */ 261 - transformer: '@hey-api/transformers' | false; 258 + transformer: PluginTransformerNames | false; 262 259 /** 263 260 * Validate request and/or response data against schema before returning. 264 261 * This is useful if you want to ensure the request and/or response conforms
+1 -1
packages/openapi-ts/src/plugins/@hey-api/transformers/config.ts
··· 8 8 config: { 9 9 bigInt: true, 10 10 dates: true, 11 - exportFromIndex: false, 11 + includeInEntry: false, 12 12 transformers: [], 13 13 typeTransformers: [], 14 14 },
+3 -9
packages/openapi-ts/src/plugins/@hey-api/transformers/types.ts
··· 1 - import type { IndexExportOption } from '@hey-api/shared'; 2 1 import type { IR } from '@hey-api/shared'; 3 2 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 4 3 import type ts from 'typescript'; ··· 12 11 export type TypeTransformer = ({ schema }: { schema: IR.SchemaObject }) => ts.TypeNode | undefined; 13 12 14 13 export type UserConfig = Plugin.Name<'@hey-api/transformers'> & 15 - Plugin.Hooks & { 14 + Plugin.Hooks & 15 + Plugin.UserExports & { 16 16 /** 17 17 * Convert long integers into BigInt values? 18 18 * ··· 26 26 */ 27 27 dates?: boolean; 28 28 /** 29 - * Whether exports should be re-exported in the index file. 30 - * 31 - * @default false 32 - */ 33 - exportFromIndex?: boolean; 34 - /** 35 29 * Custom transforms to apply to the generated code. 36 30 */ 37 31 transformers?: ReadonlyArray<ExpressionTransformer>; ··· 43 37 44 38 export type Config = Plugin.Name<'@hey-api/transformers'> & 45 39 Plugin.Hooks & 46 - IndexExportOption & { 40 + Plugin.Exports & { 47 41 /** 48 42 * Convert long integers into BigInt values? 49 43 *
+1 -1
packages/openapi-ts/src/plugins/@hey-api/typescript/config.ts
··· 8 8 api: new Api(), 9 9 config: { 10 10 case: 'PascalCase', 11 - exportFromIndex: true, 11 + includeInEntry: true, 12 12 topType: 'unknown', 13 13 }, 14 14 handler,
+4 -15
packages/openapi-ts/src/plugins/@hey-api/typescript/types.ts
··· 1 - import type { 2 - Casing, 3 - FeatureToggle, 4 - IndexExportOption, 5 - NameTransformer, 6 - NamingOptions, 7 - } from '@hey-api/shared'; 1 + import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey-api/shared'; 8 2 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 9 3 10 4 import type { IApi } from './api'; ··· 12 6 export type EnumsType = 'javascript' | 'typescript' | 'typescript-const'; 13 7 14 8 export type UserConfig = Plugin.Name<'@hey-api/typescript'> & 15 - Plugin.Hooks & { 9 + Plugin.Hooks & 10 + Plugin.UserExports & { 16 11 /** 17 12 * Casing convention for generated names. 18 13 * ··· 128 123 name?: NameTransformer; 129 124 }; 130 125 /** 131 - * Whether exports should be re-exported in the index file. 132 - * 133 - * @default true 134 - */ 135 - exportFromIndex?: boolean; 136 - /** 137 126 * Configuration for request-specific types. 138 127 * 139 128 * Controls generation of types for request bodies, query parameters, path ··· 241 230 242 231 export type Config = Plugin.Name<'@hey-api/typescript'> & 243 232 Plugin.Hooks & 244 - IndexExportOption & { 233 + Plugin.Exports & { 245 234 /** 246 235 * Casing convention for generated names. 247 236 */
+1 -1
packages/openapi-ts/src/plugins/@pinia/colada/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/typescript', '@hey-api/sdk'], 13 13 handler: handler as PiniaColadaPlugin['Handler'],
+4 -15
packages/openapi-ts/src/plugins/@pinia/colada/types.ts
··· 1 - import type { 2 - Casing, 3 - FeatureToggle, 4 - IndexExportOption, 5 - NameTransformer, 6 - NamingOptions, 7 - } from '@hey-api/shared'; 1 + import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey-api/shared'; 8 2 import type { IR } from '@hey-api/shared'; 9 3 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 4 11 5 export type UserConfig = Plugin.Name<'@pinia/colada'> & 12 - Plugin.Hooks & { 6 + Plugin.Hooks & 7 + Plugin.UserExports & { 13 8 /** 14 9 * Casing convention for generated names. 15 10 * ··· 27 22 * @default true 28 23 */ 29 24 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 25 /** 37 26 * Configuration for generated mutation options helpers. 38 27 * ··· 195 184 196 185 export type Config = Plugin.Name<'@pinia/colada'> & 197 186 Plugin.Hooks & 198 - IndexExportOption & { 187 + Plugin.Exports & { 199 188 /** 200 189 * Casing convention for generated names. 201 190 */
+1 -1
packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as TanStackAngularQueryPlugin['Handler'],
+4 -15
packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.ts
··· 1 - import type { 2 - Casing, 3 - FeatureToggle, 4 - IndexExportOption, 5 - NameTransformer, 6 - NamingOptions, 7 - } from '@hey-api/shared'; 1 + import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey-api/shared'; 8 2 import type { IR } from '@hey-api/shared'; 9 3 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 4 11 5 export type UserConfig = Plugin.Name<'@tanstack/angular-query-experimental'> & 12 - Plugin.Hooks & { 6 + Plugin.Hooks & 7 + Plugin.UserExports & { 13 8 /** 14 9 * Casing convention for generated names. 15 10 * ··· 27 22 * @default true 28 23 */ 29 24 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 25 /** 37 26 * Configuration for generated infinite query key helpers. 38 27 * ··· 303 292 304 293 export type Config = Plugin.Name<'@tanstack/angular-query-experimental'> & 305 294 Plugin.Hooks & 306 - IndexExportOption & { 295 + Plugin.Exports & { 307 296 /** 308 297 * Casing convention for generated names. 309 298 */
+1 -1
packages/openapi-ts/src/plugins/@tanstack/react-query/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as TanStackReactQueryPlugin['Handler'],
+4 -15
packages/openapi-ts/src/plugins/@tanstack/react-query/types.ts
··· 1 - import type { 2 - Casing, 3 - FeatureToggle, 4 - IndexExportOption, 5 - NameTransformer, 6 - NamingOptions, 7 - } from '@hey-api/shared'; 1 + import type { Casing, FeatureToggle, NameTransformer, NamingOptions } from '@hey-api/shared'; 8 2 import type { IR } from '@hey-api/shared'; 9 3 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 4 11 5 export type UserConfig = Plugin.Name<'@tanstack/react-query'> & 12 - Plugin.Hooks & { 6 + Plugin.Hooks & 7 + Plugin.UserExports & { 13 8 /** 14 9 * Casing convention for generated names. 15 10 * ··· 27 22 * @default true 28 23 */ 29 24 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 25 /** 37 26 * Configuration for generated infinite query key helpers. 38 27 * ··· 348 337 349 338 export type Config = Plugin.Name<'@tanstack/react-query'> & 350 339 Plugin.Hooks & 351 - IndexExportOption & { 340 + Plugin.Exports & { 352 341 /** 353 342 * Casing convention for generated names. 354 343 */
+1 -1
packages/openapi-ts/src/plugins/@tanstack/solid-query/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as TanStackSolidQueryPlugin['Handler'],
+6 -11
packages/openapi-ts/src/plugins/@tanstack/solid-query/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 8 + Plugin, 7 9 } from '@hey-api/shared'; 8 - import type { IR } from '@hey-api/shared'; 9 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 10 11 11 export type UserConfig = Plugin.Name<'@tanstack/solid-query'> & 12 - Plugin.Hooks & { 12 + Plugin.Hooks & 13 + Plugin.UserExports & { 13 14 /** 14 15 * Casing convention for generated names. 15 16 * ··· 27 28 * @default true 28 29 */ 29 30 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 31 /** 37 32 * Configuration for generated infinite query key helpers. 38 33 * ··· 304 299 305 300 export type Config = Plugin.Name<'@tanstack/solid-query'> & 306 301 Plugin.Hooks & 307 - IndexExportOption & { 302 + Plugin.Exports & { 308 303 /** 309 304 * Casing convention for generated names. 310 305 */
+1 -1
packages/openapi-ts/src/plugins/@tanstack/svelte-query/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as TanStackSvelteQueryPlugin['Handler'],
+6 -11
packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 8 + Plugin, 7 9 } from '@hey-api/shared'; 8 - import type { IR } from '@hey-api/shared'; 9 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 10 11 11 export type UserConfig = Plugin.Name<'@tanstack/svelte-query'> & 12 - Plugin.Hooks & { 12 + Plugin.Hooks & 13 + Plugin.UserExports & { 13 14 /** 14 15 * Casing convention for generated names. 15 16 * ··· 27 28 * @default true 28 29 */ 29 30 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 31 /** 37 32 * Configuration for generated infinite query key helpers. 38 33 * ··· 303 298 304 299 export type Config = Plugin.Name<'@tanstack/svelte-query'> & 305 300 Plugin.Hooks & 306 - IndexExportOption & { 301 + Plugin.Exports & { 307 302 /** 308 303 * Casing convention for generated names. 309 304 */
+1 -1
packages/openapi-ts/src/plugins/@tanstack/vue-query/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as TanStackVueQueryPlugin['Handler'],
+6 -11
packages/openapi-ts/src/plugins/@tanstack/vue-query/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 8 + Plugin, 7 9 } from '@hey-api/shared'; 8 - import type { IR } from '@hey-api/shared'; 9 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 10 11 11 export type UserConfig = Plugin.Name<'@tanstack/vue-query'> & 12 - Plugin.Hooks & { 12 + Plugin.Hooks & 13 + Plugin.UserExports & { 13 14 /** 14 15 * Casing convention for generated names. 15 16 * ··· 27 28 * @default true 28 29 */ 29 30 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 31 /** 37 32 * Configuration for generated infinite query key helpers. 38 33 * ··· 306 301 307 302 export type Config = Plugin.Name<'@tanstack/vue-query'> & 308 303 Plugin.Hooks & 309 - IndexExportOption & { 304 + Plugin.Exports & { 310 305 /** 311 306 * Casing convention for generated names. 312 307 */
+1 -1
packages/openapi-ts/src/plugins/arktype/config.ts
··· 9 9 config: { 10 10 case: 'PascalCase', 11 11 comments: true, 12 - exportFromIndex: false, 12 + includeInEntry: false, 13 13 metadata: false, 14 14 }, 15 15 handler,
+5 -10
packages/openapi-ts/src/plugins/arktype/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 5 NameTransformer, 6 6 NamingOptions, 7 + Plugin, 7 8 } from '@hey-api/shared'; 8 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 9 9 10 10 import type { IApi } from './api'; 11 11 12 12 export type UserConfig = Plugin.Name<'arktype'> & 13 - Plugin.Hooks & { 13 + Plugin.Hooks & 14 + Plugin.UserExports & { 14 15 /** 15 16 * Casing convention for generated names. 16 17 * ··· 97 98 }; 98 99 }; 99 100 }; 100 - /** 101 - * Whether exports should be re-exported in the index file. 102 - * 103 - * @default false 104 - */ 105 - exportFromIndex?: boolean; 106 101 /** 107 102 * Enable Arktype metadata support? It's often useful to associate a schema 108 103 * with some additional metadata for documentation, code generation, AI ··· 376 371 377 372 export type Config = Plugin.Name<'arktype'> & 378 373 Plugin.Hooks & 379 - IndexExportOption & { 374 + Plugin.Exports & { 380 375 /** 381 376 * Casing convention for generated names. 382 377 */
+2 -5
packages/openapi-ts/src/plugins/arktype/v2/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef, refs } from '@hey-api/codegen-core'; 3 - import type { IR } from '@hey-api/shared'; 4 - import { applyNaming } from '@hey-api/shared'; 5 - import { deduplicateSchema } from '@hey-api/shared'; 6 - import { pathToJsonPointer, refToName } from '@hey-api/shared'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 7 5 8 - import type { SchemaWithType } from '../../../plugins'; 9 6 import { $ } from '../../../ts-dsl'; 10 7 import { exportAst } from '../shared/export'; 11 8 import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types';
+2 -1
packages/openapi-ts/src/plugins/arktype/v2/toAst/index.ts
··· 1 - import type { SchemaWithType } from '../../../../plugins'; 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 2 3 import { $ } from '../../../../ts-dsl'; 3 4 import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 4 5 import { nullToAst } from './null';
+2 -1
packages/openapi-ts/src/plugins/arktype/v2/toAst/null.ts
··· 1 - import type { SchemaWithType } from '../../../../plugins'; 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 2 3 import { identifiers } from '../../constants'; 3 4 import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 4 5
+1 -1
packages/openapi-ts/src/plugins/arktype/v2/toAst/object.ts
··· 1 1 import { fromRef, ref } from '@hey-api/codegen-core'; 2 + import type { SchemaWithType } from '@hey-api/shared'; 2 3 3 - import type { SchemaWithType } from '../../../../plugins'; 4 4 import { $ } from '../../../../ts-dsl'; 5 5 // import { identifiers } from '../../constants'; 6 6 import type { Ast, IrSchemaToAstOptions } from '../../shared/types';
+2 -1
packages/openapi-ts/src/plugins/arktype/v2/toAst/string.ts
··· 1 - import type { SchemaWithType } from '../../../../plugins'; 1 + import type { SchemaWithType } from '@hey-api/shared'; 2 + 2 3 import { identifiers } from '../../constants'; 3 4 import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; 4 5
+1 -1
packages/openapi-ts/src/plugins/fastify/config.ts
··· 5 5 6 6 export const defaultConfig: FastifyPlugin['Config'] = { 7 7 config: { 8 - exportFromIndex: false, 8 + includeInEntry: false, 9 9 }, 10 10 dependencies: ['@hey-api/typescript'], 11 11 handler,
+1 -9
packages/openapi-ts/src/plugins/fastify/types.ts
··· 1 1 import type { DefinePlugin, Plugin } from '@hey-api/shared'; 2 2 3 - export type UserConfig = Plugin.Name<'fastify'> & 4 - Plugin.Hooks & { 5 - /** 6 - * Whether exports should be re-exported in the index file. 7 - * 8 - * @default false 9 - */ 10 - exportFromIndex?: boolean; 11 - }; 3 + export type UserConfig = Plugin.Name<'fastify'> & Plugin.Hooks & Plugin.UserExports; 12 4 13 5 export type FastifyPlugin = DefinePlugin<UserConfig, UserConfig>;
-1
packages/openapi-ts/src/plugins/index.ts
··· 1 - export type { DefinePlugin, Plugin, SchemaWithType } from '@hey-api/shared';
+1 -1
packages/openapi-ts/src/plugins/swr/config.ts
··· 7 7 config: { 8 8 case: 'camelCase', 9 9 comments: true, 10 - exportFromIndex: false, 10 + includeInEntry: false, 11 11 }, 12 12 dependencies: ['@hey-api/sdk', '@hey-api/typescript'], 13 13 handler: handler as SwrPlugin['Handler'],
+6 -11
packages/openapi-ts/src/plugins/swr/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 + IR, 5 6 NameTransformer, 6 7 NamingOptions, 8 + Plugin, 7 9 } from '@hey-api/shared'; 8 - import type { IR } from '@hey-api/shared'; 9 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 10 10 11 11 export type UserConfig = Plugin.Name<'swr'> & 12 - Plugin.Hooks & { 12 + Plugin.Hooks & 13 + Plugin.UserExports & { 13 14 /** 14 15 * Casing convention for generated names. 15 16 * ··· 27 28 * @default true 28 29 */ 29 30 comments?: boolean; 30 - /** 31 - * Whether exports should be re-exported in the index file. 32 - * 33 - * @default false 34 - */ 35 - exportFromIndex?: boolean; 36 31 /** 37 32 * Configuration for generated infinite query key helpers. 38 33 * ··· 348 343 349 344 export type Config = Plugin.Name<'swr'> & 350 345 Plugin.Hooks & 351 - IndexExportOption & { 346 + Plugin.Exports & { 352 347 /** 353 348 * Casing convention for generated names. 354 349 */
+2
packages/openapi-ts/src/plugins/types.ts
··· 9 9 10 10 export type PluginMockNames = '@faker-js/faker'; 11 11 12 + export type PluginTransformerNames = '@hey-api/transformers'; 13 + 12 14 export type PluginValidatorNames = 'arktype' | 'valibot' | 'zod';
+1 -1
packages/openapi-ts/src/plugins/valibot/config.ts
··· 9 9 config: { 10 10 case: 'camelCase', 11 11 comments: true, 12 - exportFromIndex: false, 12 + includeInEntry: false, 13 13 metadata: false, 14 14 }, 15 15 handler,
+1 -2
packages/openapi-ts/src/plugins/valibot/resolvers/types.ts
··· 1 1 import type { Refs, Symbol } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { IR, Plugin, SchemaWithType } from '@hey-api/shared'; 3 3 4 - import type { Plugin, SchemaWithType } from '../../../plugins'; 5 4 import type { MaybeBigInt, ShouldCoerceToBigInt } from '../../../plugins/shared/utils/coerce'; 6 5 import type { GetIntegerLimit } from '../../../plugins/shared/utils/formats'; 7 6 import type { $, DollarTsDsl } from '../../../ts-dsl';
+1 -2
packages/openapi-ts/src/plugins/valibot/shared/operation.ts
··· 1 1 import { fromRef } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming } from '@hey-api/shared'; 4 - import { operationResponsesMap } from '@hey-api/shared'; 3 + import { applyNaming, operationResponsesMap } from '@hey-api/shared'; 5 4 6 5 import { exportAst } from './export'; 7 6 import type { Ast, IrSchemaToAstOptions } from './types';
+5 -10
packages/openapi-ts/src/plugins/valibot/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 5 NameTransformer, 6 6 NamingOptions, 7 + Plugin, 7 8 } from '@hey-api/shared'; 8 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 9 9 10 10 import type { IApi } from './api'; 11 11 import type { Resolvers } from './resolvers'; 12 12 13 13 export type UserConfig = Plugin.Name<'valibot'> & 14 14 Plugin.Hooks & 15 + Plugin.UserExports & 15 16 Resolvers & { 16 17 /** 17 18 * Casing convention for generated names. ··· 59 60 */ 60 61 name?: NameTransformer; 61 62 }; 62 - /** 63 - * Whether exports should be re-exported in the index file. 64 - * 65 - * @default false 66 - */ 67 - exportFromIndex?: boolean; 68 63 /** 69 64 * Enable Valibot metadata support? It's often useful to associate a schema 70 65 * with some additional metadata for documentation, code generation, AI ··· 180 175 181 176 export type Config = Plugin.Name<'valibot'> & 182 177 Plugin.Hooks & 183 - Resolvers & 184 - IndexExportOption & { 178 + Plugin.Exports & 179 + Resolvers & { 185 180 /** 186 181 * Casing convention for generated names. 187 182 */
+1 -1
packages/openapi-ts/src/plugins/zod/config.ts
··· 12 12 config: { 13 13 case: 'camelCase', 14 14 comments: true, 15 - exportFromIndex: false, 15 + includeInEntry: false, 16 16 metadata: false, 17 17 }, 18 18 handler,
+2 -5
packages/openapi-ts/src/plugins/zod/mini/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef, ref, refs } from '@hey-api/codegen-core'; 3 - import type { IR } from '@hey-api/shared'; 4 - import type { SchemaWithType } from '@hey-api/shared'; 5 - import { applyNaming } from '@hey-api/shared'; 6 - import { deduplicateSchema } from '@hey-api/shared'; 7 - import { pathToJsonPointer, refToName } from '@hey-api/shared'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 8 5 9 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 10 7 import { $ } from '../../../ts-dsl';
+1 -2
packages/openapi-ts/src/plugins/zod/resolvers/types.ts
··· 1 1 import type { Refs, Symbol } from '@hey-api/codegen-core'; 2 - import type { IR } from '@hey-api/shared'; 2 + import type { IR, Plugin, SchemaWithType } from '@hey-api/shared'; 3 3 import type { MaybeArray } from '@hey-api/types'; 4 4 import type ts from 'typescript'; 5 5 6 - import type { Plugin, SchemaWithType } from '../../../plugins'; 7 6 import type { MaybeBigInt, ShouldCoerceToBigInt } from '../../../plugins/shared/utils/coerce'; 8 7 import type { GetIntegerLimit } from '../../../plugins/shared/utils/formats'; 9 8 import type { $, DollarTsDsl, TsDsl } from '../../../ts-dsl';
+1 -2
packages/openapi-ts/src/plugins/zod/shared/operation.ts
··· 1 1 import { fromRef } from '@hey-api/codegen-core'; 2 2 import type { IR } from '@hey-api/shared'; 3 - import { applyNaming } from '@hey-api/shared'; 4 - import { operationResponsesMap } from '@hey-api/shared'; 3 + import { applyNaming, operationResponsesMap } from '@hey-api/shared'; 5 4 6 5 import { exportAst } from './export'; 7 6 import type { Ast, IrSchemaToAstOptions } from './types';
+5 -10
packages/openapi-ts/src/plugins/zod/types.ts
··· 1 1 import type { 2 2 Casing, 3 + DefinePlugin, 3 4 FeatureToggle, 4 - IndexExportOption, 5 5 NameTransformer, 6 6 NamingOptions, 7 + Plugin, 7 8 } from '@hey-api/shared'; 8 - import type { DefinePlugin, Plugin } from '@hey-api/shared'; 9 9 10 10 import type { IApi } from './api'; 11 11 import type { Resolvers } from './resolvers'; 12 12 13 13 export type UserConfig = Plugin.Name<'zod'> & 14 14 Plugin.Hooks & 15 + Plugin.UserExports & 15 16 Resolvers & { 16 17 /** 17 18 * Casing convention for generated names. ··· 138 139 }; 139 140 }; 140 141 }; 141 - /** 142 - * Whether exports should be re-exported in the index file. 143 - * 144 - * @default false 145 - */ 146 - exportFromIndex?: boolean; 147 142 /** 148 143 * Enable Zod metadata support? It's often useful to associate a schema with 149 144 * some additional metadata for documentation, code generation, AI ··· 417 412 418 413 export type Config = Plugin.Name<'zod'> & 419 414 Plugin.Hooks & 420 - Resolvers & 421 - IndexExportOption & { 415 + Plugin.Exports & 416 + Resolvers & { 422 417 /** 423 418 * Casing convention for generated names. 424 419 */
+2 -5
packages/openapi-ts/src/plugins/zod/v3/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef, ref, refs } from '@hey-api/codegen-core'; 3 - import type { IR } from '@hey-api/shared'; 4 - import type { SchemaWithType } from '@hey-api/shared'; 5 - import { applyNaming } from '@hey-api/shared'; 6 - import { deduplicateSchema } from '@hey-api/shared'; 7 - import { pathToJsonPointer, refToName } from '@hey-api/shared'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 8 5 9 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 10 7 import { $ } from '../../../ts-dsl';
+2 -5
packages/openapi-ts/src/plugins/zod/v4/plugin.ts
··· 1 1 import type { SymbolMeta } from '@hey-api/codegen-core'; 2 2 import { fromRef, ref, refs } from '@hey-api/codegen-core'; 3 - import type { IR } from '@hey-api/shared'; 4 - import type { SchemaWithType } from '@hey-api/shared'; 5 - import { applyNaming } from '@hey-api/shared'; 6 - import { deduplicateSchema } from '@hey-api/shared'; 7 - import { pathToJsonPointer, refToName } from '@hey-api/shared'; 3 + import type { IR, SchemaWithType } from '@hey-api/shared'; 4 + import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; 8 5 9 6 import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; 10 7 import { $ } from '../../../ts-dsl';
+18 -2
packages/openapi-ts/src/ts-dsl/decl/func.ts
··· 83 83 } 84 84 } 85 85 86 + /** Returns true when all required builder calls are present. */ 87 + get isValid(): boolean { 88 + return this.missingRequiredCalls().length === 0; 89 + } 90 + 86 91 /** Switches the function to an arrow function form. */ 87 92 arrow(): FuncTsDsl<'arrow'> { 88 93 this.mode = 'arrow'; ··· 107 112 : M extends 'expr' 108 113 ? ts.FunctionExpression 109 114 : ts.ArrowFunction { 115 + this.$validate(); 110 116 const body = this.$node(new BlockTsDsl(...this._do).pretty()); 111 117 112 118 if (this.mode === 'decl') { 113 - const name = this.name.toString(); 114 - if (!name) throw new Error('Function declaration requires a name'); 115 119 const node = ts.factory.createFunctionDeclaration( 116 120 [...this.$decorators(), ...this.modifiers], 117 121 undefined, ··· 150 154 : body, 151 155 ) as any; 152 156 return this.$docs(node); 157 + } 158 + 159 + $validate(): asserts this { 160 + const missing = this.missingRequiredCalls(); 161 + if (missing.length === 0) return; 162 + throw new Error(`Function ${this.mode} missing ${missing.join(' and ')}`); 163 + } 164 + 165 + private missingRequiredCalls(): ReadonlyArray<string> { 166 + const missing: Array<string> = []; 167 + if (this.mode === 'decl' && !this.name.toString()) missing.push('name'); 168 + return missing; 153 169 } 154 170 } 155 171
+20 -5
packages/openapi-ts/src/ts-dsl/decl/param.ts
··· 39 39 ctx.analyze(this._type); 40 40 } 41 41 42 + /** Returns true when all required builder calls are present. */ 43 + get isValid(): boolean { 44 + return this.missingRequiredCalls().length === 0; 45 + } 46 + 42 47 /** Sets the parameter type. */ 43 48 type(type: string | TypeTsDsl): this { 44 49 this._type = type instanceof TypeTsDsl ? type : new TypeExprTsDsl(type); ··· 46 51 } 47 52 48 53 override toAst() { 49 - const name = this.$pattern() || this.name.toString(); 50 - if (!name) { 51 - throw new Error('Param must have either a name or a destructuring pattern'); 52 - } 54 + this.$validate(); 53 55 return ts.factory.createParameterDeclaration( 54 56 this.$decorators(), 55 57 undefined, 56 - name, 58 + this.$pattern() ?? this.name.toString(), 57 59 this._optional ? this.$node(new TokenTsDsl().optional()) : undefined, 58 60 this.$type(this._type), 59 61 this.$value(), 60 62 ); 63 + } 64 + 65 + $validate(): asserts this { 66 + const missing = this.missingRequiredCalls(); 67 + if (missing.length === 0) return; 68 + throw new Error(`Parameter missing ${missing.join(' and ')}`); 69 + } 70 + 71 + private missingRequiredCalls(): ReadonlyArray<string> { 72 + const missing: Array<string> = []; 73 + if (!this.$pattern() && !this.name.toString()) 74 + missing.push('name or pattern (.array()/.object())'); 75 + return missing; 61 76 } 62 77 }
+22 -3
packages/openapi-ts/src/ts-dsl/decl/pattern.ts
··· 23 23 super.analyze(ctx); 24 24 } 25 25 26 + /** Returns true when all required builder calls are present. */ 27 + get isValid(): boolean { 28 + return this.missingRequiredCalls().length === 0; 29 + } 30 + 26 31 /** Defines an array pattern (e.g. `[a, b, c]`). */ 27 32 array(...props: ReadonlyArray<string> | [ReadonlyArray<string>]): this { 28 33 const values = props[0] instanceof Array ? [...props[0]] : (props as ReadonlyArray<string>); ··· 49 54 } 50 55 51 56 override toAst() { 52 - if (!this.pattern) { 53 - throw new Error('PatternTsDsl requires object() or array() pattern'); 54 - } 57 + this.$validate(); 55 58 56 59 if (this.pattern.kind === 'object') { 57 60 const elements = Object.entries(this.pattern.values).map(([key, alias]) => ··· 74 77 } 75 78 76 79 throw new Error('PatternTsDsl requires object() or array() pattern'); 80 + } 81 + 82 + $validate(): asserts this is this & { 83 + pattern: 84 + | { kind: 'array'; values: ReadonlyArray<string> } 85 + | { kind: 'object'; values: Record<string, string> }; 86 + } { 87 + const missing = this.missingRequiredCalls(); 88 + if (missing.length === 0) return; 89 + throw new Error(`Binding pattern missing ${missing.join(' and ')}`); 90 + } 91 + 92 + private missingRequiredCalls(): ReadonlyArray<string> { 93 + const missing: Array<string> = []; 94 + if (!this.pattern) missing.push('.array() or .object()'); 95 + return missing; 77 96 } 78 97 79 98 private createSpread(): ts.BindingElement | undefined {
+23 -8
packages/openapi-ts/src/ts-dsl/expr/binary.ts
··· 50 50 ctx.analyze(this._expr); 51 51 } 52 52 53 + /** Returns true when all required builder calls are present. */ 54 + get isValid(): boolean { 55 + return this.missingRequiredCalls().length === 0; 56 + } 57 + 53 58 /** Logical AND — `this && expr` */ 54 59 and(expr: Expr): this { 55 60 return this.opAndExpr('&&', expr); ··· 136 141 } 137 142 138 143 override toAst() { 139 - if (!this._op) { 140 - throw new Error('BinaryTsDsl: missing operator'); 141 - } 142 - const expr = this.$node(this._expr); 143 - if (!expr) { 144 - throw new Error('BinaryTsDsl: missing right-hand expression'); 145 - } 144 + this.$validate(); 146 145 const base = this.$node(this._base); 147 146 const operator = typeof this._op === 'string' ? this.opToToken(this._op) : this._op; 148 - return ts.factory.createBinaryExpression(base, operator, expr); 147 + return ts.factory.createBinaryExpression(base, operator, this.$node(this._expr)); 148 + } 149 + 150 + $validate(): asserts this is this & { 151 + _expr: Ref<Expr>; 152 + _op: Op; 153 + } { 154 + const missing = this.missingRequiredCalls(); 155 + if (missing.length === 0) return; 156 + throw new Error(`Binary expression missing ${missing.join(' and ')}`); 157 + } 158 + 159 + private missingRequiredCalls(): ReadonlyArray<string> { 160 + const missing: Array<string> = []; 161 + if (!this._op) missing.push('operator (e.g., .eq(), .plus())'); 162 + if (!this._expr) missing.push('right-hand expression'); 163 + return missing; 149 164 } 150 165 151 166 /** Sets the binary operator and right-hand operand for this expression. */
+22 -6
packages/openapi-ts/src/ts-dsl/expr/prefix.ts
··· 23 23 ctx.analyze(this._expr); 24 24 } 25 25 26 + /** Returns true when all required builder calls are present. */ 27 + get isValid(): boolean { 28 + return this.missingRequiredCalls().length === 0; 29 + } 30 + 26 31 /** Sets the operand (the expression being prefixed). */ 27 32 expr(expr: string | MaybeTsDsl<ts.Expression>): this { 28 33 this._expr = expr; ··· 48 53 } 49 54 50 55 override toAst() { 51 - if (!this._expr) { 52 - throw new Error('Missing expression for prefix unary expression'); 53 - } 54 - if (!this._op) { 55 - throw new Error('Missing operator for prefix unary expression'); 56 - } 56 + this.$validate(); 57 57 return ts.factory.createPrefixUnaryExpression(this._op, this.$node(this._expr)); 58 + } 59 + 60 + $validate(): asserts this is this & { 61 + _expr: string | MaybeTsDsl<ts.Expression>; 62 + _op: ts.PrefixUnaryOperator; 63 + } { 64 + const missing = this.missingRequiredCalls(); 65 + if (missing.length === 0) return; 66 + throw new Error(`Prefix unary expression missing ${missing.join(' and ')}`); 67 + } 68 + 69 + private missingRequiredCalls(): ReadonlyArray<string> { 70 + const missing: Array<string> = []; 71 + if (!this._expr) missing.push('.expr()'); 72 + if (!this._op) missing.push('operator (e.g., .not(), .neg())'); 73 + return missing; 58 74 } 59 75 }
+24 -4
packages/openapi-ts/src/ts-dsl/expr/ternary.ts
··· 25 25 ctx.analyze(this._else); 26 26 } 27 27 28 + /** Returns true when all required builder calls are present. */ 29 + get isValid(): boolean { 30 + return this.missingRequiredCalls().length === 0; 31 + } 32 + 28 33 condition(condition: string | MaybeTsDsl<ts.Expression>) { 29 34 this._condition = condition; 30 35 return this; ··· 41 46 } 42 47 43 48 override toAst() { 44 - if (!this._condition) throw new Error('Missing condition in ternary'); 45 - if (!this._then) throw new Error('Missing then expression in ternary'); 46 - if (!this._else) throw new Error('Missing else expression in ternary'); 47 - 49 + this.$validate(); 48 50 return ts.factory.createConditionalExpression( 49 51 this.$node(this._condition), 50 52 undefined, ··· 52 54 undefined, 53 55 this.$node(this._else), 54 56 ); 57 + } 58 + 59 + $validate(): asserts this is this & { 60 + _condition: string | MaybeTsDsl<ts.Expression>; 61 + _else: string | MaybeTsDsl<ts.Expression>; 62 + _then: string | MaybeTsDsl<ts.Expression>; 63 + } { 64 + const missing = this.missingRequiredCalls(); 65 + if (missing.length === 0) return; 66 + throw new Error(`Ternary expression missing ${missing.join(' and ')}`); 67 + } 68 + 69 + private missingRequiredCalls(): ReadonlyArray<string> { 70 + const missing: Array<string> = []; 71 + if (!this._condition) missing.push('.condition()'); 72 + if (!this._then) missing.push('.do()'); 73 + if (!this._else) missing.push('.otherwise()'); 74 + return missing; 55 75 } 56 76 }
+21 -3
packages/openapi-ts/src/ts-dsl/stmt/if.ts
··· 37 37 } 38 38 } 39 39 40 + /** Returns true when all required builder calls are present. */ 41 + get isValid(): boolean { 42 + return this.missingRequiredCalls().length === 0; 43 + } 44 + 40 45 condition(condition: IfCondition): this { 41 46 this._condition = condition; 42 47 return this; ··· 48 53 } 49 54 50 55 override toAst() { 51 - if (!this._condition) throw new Error('Missing condition in if'); 52 - if (!this._do) throw new Error('Missing then block in if'); 53 - 56 + this.$validate(); 54 57 return ts.factory.createIfStatement( 55 58 this.$node(this._condition), 56 59 this.$node(new BlockTsDsl(...this._do).pretty()), 57 60 this._else ? this.$node(new BlockTsDsl(...this._else).pretty()) : undefined, 58 61 ); 62 + } 63 + 64 + $validate(): asserts this is this & { 65 + _condition: IfCondition; 66 + } { 67 + const missing = this.missingRequiredCalls(); 68 + if (missing.length === 0) return; 69 + throw new Error(`If statement missing ${missing.join(' and ')}`); 70 + } 71 + 72 + private missingRequiredCalls(): ReadonlyArray<string> { 73 + const missing: Array<string> = []; 74 + if (!this._condition) missing.push('.condition()'); 75 + if (this._do.length === 0) missing.push('.do()'); 76 + return missing; 59 77 } 60 78 }
+20 -2
packages/openapi-ts/src/ts-dsl/stmt/try.ts
··· 54 54 } 55 55 } 56 56 57 + /** Returns true when all required builder calls are present. */ 58 + get isValid(): boolean { 59 + return this.missingRequiredCalls().length === 0; 60 + } 61 + 57 62 catch(...items: Array<DoExpr>): this { 58 63 this._catch = items; 59 64 return this; ··· 75 80 } 76 81 77 82 override toAst() { 78 - if (!this._try?.length) throw new Error('Missing try block'); 79 - 83 + this.$validate(); 80 84 const catchParam = this._catchArg ? (this.$node(this._catchArg) as ts.BindingName) : undefined; 81 85 82 86 return ts.factory.createTryStatement( ··· 87 91 ), 88 92 this._finally ? this.$node(new BlockTsDsl(...this._finally).pretty()) : undefined, 89 93 ); 94 + } 95 + 96 + $validate(): asserts this is this & { 97 + _try: Array<DoExpr>; 98 + } { 99 + const missing = this.missingRequiredCalls(); 100 + if (missing.length === 0) return; 101 + throw new Error(`Try statement missing ${missing.join(' and ')}`); 102 + } 103 + 104 + private missingRequiredCalls(): ReadonlyArray<string> { 105 + const missing: Array<string> = []; 106 + if (!this._try || this._try.length === 0) missing.push('.try()'); 107 + return missing; 90 108 } 91 109 }
+20 -3
packages/openapi-ts/src/ts-dsl/stmt/var.ts
··· 36 36 ctx.analyze(this._type); 37 37 } 38 38 39 + /** Returns true when all required builder calls are present. */ 40 + get isValid(): boolean { 41 + return this.missingRequiredCalls().length === 0; 42 + } 43 + 39 44 const(): this { 40 45 this.kind = ts.NodeFlags.Const; 41 46 return this; ··· 58 63 } 59 64 60 65 override toAst() { 61 - const name = this.$pattern() ?? this.$node(this.name); 62 - if (!name) throw new Error('Var must have either a name or a destructuring pattern'); 66 + this.$validate(); 63 67 const node = ts.factory.createVariableStatement( 64 68 this.modifiers, 65 69 ts.factory.createVariableDeclarationList( 66 70 [ 67 71 ts.factory.createVariableDeclaration( 68 - name as ts.BindingName, 72 + this.$pattern() ?? (this.$node(this.name) as ts.BindingName), 69 73 undefined, 70 74 this.$type(this._type), 71 75 this.$value(), ··· 75 79 ), 76 80 ); 77 81 return this.$docs(this.$hint(node)); 82 + } 83 + 84 + $validate(): asserts this { 85 + const missing = this.missingRequiredCalls(); 86 + if (missing.length === 0) return; 87 + throw new Error(`Variable declaration missing ${missing.join(' and ')}`); 88 + } 89 + 90 + private missingRequiredCalls(): ReadonlyArray<string> { 91 + const missing: Array<string> = []; 92 + if (!this.$pattern() && !this.name.toString()) 93 + missing.push('name or pattern (.array()/.object())'); 94 + return missing; 78 95 } 79 96 }
+20 -3
packages/openapi-ts/src/ts-dsl/token.ts
··· 46 46 } 47 47 48 48 override toAst(): ts.Token<K> { 49 - if (!this._kind) { 50 - throw new Error(`Token missing \`.kind(kind)\``); 51 - } 49 + this.$validate(); 52 50 // @ts-expect-error 53 51 return ts.factory.createToken(this._kind); 52 + } 53 + 54 + $validate(): asserts this is this & { 55 + _kind: K; 56 + } { 57 + const missing = this.missingRequiredCalls(); 58 + if (missing.length === 0) return; 59 + throw new Error(`Token missing ${missing.join(' and ')}`); 60 + } 61 + 62 + private missingRequiredCalls(): ReadonlyArray<string> { 63 + const missing: Array<string> = []; 64 + if (!this._kind) missing.push('.kind()'); 65 + return missing; 66 + } 67 + 68 + /** Returns true when all required builder calls are present. */ 69 + get isValid(): boolean { 70 + return this.missingRequiredCalls().length === 0; 54 71 } 55 72 }
+21 -2
packages/openapi-ts/src/ts-dsl/type/alias.ts
··· 35 35 ctx.analyze(this.value); 36 36 } 37 37 38 + /** Returns true when all required builder calls are present. */ 39 + get isValid(): boolean { 40 + return this.missingRequiredCalls().length === 0; 41 + } 42 + 38 43 /** Sets the type expression on the right-hand side of `= ...`. */ 39 44 type(node: Value): this { 40 45 this.value = node; ··· 42 47 } 43 48 44 49 override toAst() { 45 - if (!this.value) 46 - throw new Error(`Type alias '${this.name.toString()}' is missing a type definition`); 50 + this.$validate(); 47 51 const node = ts.factory.createTypeAliasDeclaration( 48 52 this.modifiers, 49 53 this.$node(this.name) as ts.Identifier, ··· 51 55 this.$type(this.value), 52 56 ); 53 57 return this.$docs(node); 58 + } 59 + 60 + $validate(): asserts this is this & { 61 + value: Value; 62 + } { 63 + const missing = this.missingRequiredCalls(); 64 + if (missing.length === 0) return; 65 + const name = this.name.toString(); 66 + throw new Error(`Type alias${name ? ` "${name}"` : ''} missing ${missing.join(' and ')}`); 67 + } 68 + 69 + private missingRequiredCalls(): ReadonlyArray<string> { 70 + const missing: Array<string> = []; 71 + if (!this.value) missing.push('.type()'); 72 + return missing; 54 73 } 55 74 }
+22 -3
packages/openapi-ts/src/ts-dsl/type/attr.ts
··· 37 37 ctx.analyze(this._right); 38 38 } 39 39 40 + /** Returns true when all required builder calls are present. */ 41 + get isValid(): boolean { 42 + return this.missingRequiredCalls().length === 0; 43 + } 44 + 40 45 base(base?: Base | Ref<Base>): this { 41 46 if (isRef(base)) { 42 47 this._base = base; ··· 52 57 } 53 58 54 59 override toAst() { 55 - if (!this._base) { 56 - throw new Error('TypeAttrTsDsl: missing base for qualified name'); 57 - } 60 + this.$validate(); 58 61 const left = this.$node(this._base); 59 62 if (!ts.isEntityName(left)) { 60 63 throw new Error('TypeAttrTsDsl: base must be an EntityName'); 61 64 } 62 65 return ts.factory.createQualifiedName(left, this.$node(this._right) as ts.Identifier); 66 + } 67 + 68 + $validate(): asserts this is this & { 69 + _base: Ref<Base>; 70 + _right: Ref<Right>; 71 + } { 72 + const missing = this.missingRequiredCalls(); 73 + if (missing.length === 0) return; 74 + throw new Error(`Type attribute missing ${missing.join(' and ')}`); 75 + } 76 + 77 + private missingRequiredCalls(): ReadonlyArray<string> { 78 + const missing: Array<string> = []; 79 + if (!this._base) missing.push('.base()'); 80 + if (!this._right) missing.push('.right()'); 81 + return missing; 63 82 } 64 83 }
+20 -1
packages/openapi-ts/src/ts-dsl/type/expr.ts
··· 39 39 ctx.analyze(this._exprInput); 40 40 } 41 41 42 + /** Returns true when all required builder calls are present. */ 43 + get isValid(): boolean { 44 + return this.missingRequiredCalls().length === 0; 45 + } 46 + 42 47 /** Accesses a nested type (e.g. `Foo.Bar`). */ 43 48 attr(right: string | ts.Identifier | TypeAttrTsDsl): this { 44 49 this._exprInput = isNode(right) ··· 48 53 } 49 54 50 55 override toAst() { 51 - if (!this._exprInput) throw new Error('TypeExpr must have an expression'); 56 + this.$validate(); 52 57 return ts.factory.createTypeReferenceNode( 53 58 this.$type(this._exprInput) as ts.EntityName, 54 59 this.$generics(), 55 60 ); 61 + } 62 + 63 + $validate(): asserts this is this & { 64 + _exprInput: Ref<TypeExprExpr>; 65 + } { 66 + const missing = this.missingRequiredCalls(); 67 + if (missing.length === 0) return; 68 + throw new Error(`Type expression missing ${missing.join(' and ')}`); 69 + } 70 + 71 + private missingRequiredCalls(): ReadonlyArray<string> { 72 + const missing: Array<string> = []; 73 + if (!this._exprInput) missing.push('name or .attr()'); 74 + return missing; 56 75 } 57 76 } 58 77
+23 -5
packages/openapi-ts/src/ts-dsl/type/func.ts
··· 17 17 super.analyze(ctx); 18 18 } 19 19 20 + /** Returns true when all required builder calls are present. */ 21 + get isValid(): boolean { 22 + return this.missingRequiredCalls().length === 0; 23 + } 24 + 20 25 override toAst() { 21 - const returns = this.$returns(); 22 - if (returns === undefined) { 23 - throw new Error('Missing return type in function type DSL'); 24 - } 25 - const node = ts.factory.createFunctionTypeNode(this.$generics(), this.$params(), returns); 26 + this.$validate(); 27 + const node = ts.factory.createFunctionTypeNode( 28 + this.$generics(), 29 + this.$params(), 30 + this.$returns()!, 31 + ); 26 32 return this.$docs(node); 33 + } 34 + 35 + $validate(): asserts this { 36 + const missing = this.missingRequiredCalls(); 37 + if (missing.length === 0) return; 38 + throw new Error(`Function type missing ${missing.join(' and ')}`); 39 + } 40 + 41 + private missingRequiredCalls(): ReadonlyArray<string> { 42 + const missing: Array<string> = []; 43 + if (this.$returns() === undefined) missing.push('.returns()'); 44 + return missing; 27 45 } 28 46 }
+22
packages/openapi-ts/src/ts-dsl/type/idx.ts
··· 31 31 ctx.analyze(this._index); 32 32 } 33 33 34 + /** Returns true when all required builder calls are present. */ 35 + get isValid(): boolean { 36 + return this.missingRequiredCalls().length === 0; 37 + } 38 + 34 39 base(base: Base): this { 35 40 this._base = base; 36 41 return this; ··· 42 47 } 43 48 44 49 override toAst() { 50 + this.$validate(); 45 51 return ts.factory.createIndexedAccessTypeNode(this.$type(this._base), this.$type(this._index)); 52 + } 53 + 54 + $validate(): asserts this is this & { 55 + _base: Base; 56 + _index: Index; 57 + } { 58 + const missing = this.missingRequiredCalls(); 59 + if (missing.length === 0) return; 60 + throw new Error(`Indexed access type missing ${missing.join(' and ')}`); 61 + } 62 + 63 + private missingRequiredCalls(): ReadonlyArray<string> { 64 + const missing: Array<string> = []; 65 + if (this._base === undefined) missing.push('.base()'); 66 + if (this._index === undefined) missing.push('.index()'); 67 + return missing; 46 68 } 47 69 } 48 70
+21 -3
packages/openapi-ts/src/ts-dsl/type/prop.ts
··· 42 42 ctx.analyze(this._type); 43 43 } 44 44 45 + /** Returns true when all required builder calls are present. */ 46 + get isValid(): boolean { 47 + return this.missingRequiredCalls().length === 0; 48 + } 49 + 45 50 /** Sets the property type. */ 46 51 type(type: TypePropType): this { 47 52 this._type = ref(type); ··· 49 54 } 50 55 51 56 override toAst() { 57 + this.$validate(); 52 58 const name = this.name.toString(); 53 - if (!this._type || !name) { 54 - throw new Error(`Type not specified for property '${name}'`); 55 - } 56 59 const node = ts.factory.createPropertySignature( 57 60 this.modifiers, 58 61 this.$node(safePropName(name)), ··· 60 63 this.$type(this._type), 61 64 ); 62 65 return this.$docs(node); 66 + } 67 + 68 + $validate(): asserts this is this & { 69 + _type: Ref<TypePropType>; 70 + } { 71 + const missing = this.missingRequiredCalls(); 72 + if (missing.length === 0) return; 73 + const name = this.name.toString(); 74 + throw new Error(`Type property${name ? ` "${name}"` : ''} missing ${missing.join(' and ')}`); 75 + } 76 + 77 + private missingRequiredCalls(): ReadonlyArray<string> { 78 + const missing: Array<string> = []; 79 + if (!this._type) missing.push('.type()'); 80 + return missing; 63 81 } 64 82 }
+2 -2
packages/openapi-ts/src/ts-dsl/utils/name.ts
··· 50 50 let sanitized = ''; 51 51 let index: number; 52 52 53 - const first = name[0]!; 53 + const first = name[0] ?? ''; 54 54 regexp.illegalStartCharacters.lastIndex = 0; 55 55 if (regexp.illegalStartCharacters.test(first)) { 56 56 sanitized += '_'; ··· 61 61 } 62 62 63 63 while (index < name.length) { 64 - const char = name[index]!; 64 + const char = name[index] ?? ''; 65 65 sanitized += /^[\u200c\u200d\p{ID_Continue}]$/u.test(char) ? char : '_'; 66 66 index += 1; 67 67 }
+5 -5
packages/openapi-ts/src/ts-dsl/utils/render-utils.ts
··· 80 80 modulePath: string; 81 81 }; 82 82 83 - export const moduleSortKey = ({ 83 + export function moduleSortKey({ 84 84 file, 85 85 fromFile, 86 86 preferFileExtension, 87 87 root, 88 88 }: { 89 - file: File; 90 - fromFile: File; 89 + file: Pick<File, 'finalPath'>; 90 + fromFile: Pick<File, 'finalPath' | 'extension' | 'external' | 'name'>; 91 91 preferFileExtension: string; 92 92 root: string; 93 - }): SortKey => { 93 + }): SortKey { 94 94 const filePath = file.finalPath!.split(path.sep).join('/'); 95 95 let modulePath = fromFile.finalPath!.split(path.sep).join('/'); 96 96 ··· 133 133 } 134 134 135 135 return [2, parentCount, modulePath]; 136 - }; 136 + }
+1 -3
packages/openapi-ts/tsdown.config.ts
··· 15 15 16 16 export default defineConfig({ 17 17 clean: true, 18 - dts: { 19 - build: true, 20 - }, 18 + dts: true, 21 19 entry: ['./src/{index,internal,run}.ts'], 22 20 format: ['esm'], 23 21 minify: false,
+51 -9
packages/shared/src/config/shared.ts
··· 1 - import type { NameConflictResolver } from '@hey-api/codegen-core'; 1 + import type { NameConflictResolver, Symbol } from '@hey-api/codegen-core'; 2 2 import type { MaybeArray } from '@hey-api/types'; 3 3 4 4 import type { Plugin } from '../plugins/types'; ··· 17 17 enabled: boolean; 18 18 }; 19 19 20 + export type UserIndexExportOption = { 21 + /** 22 + * Whether exports should be re-exported from the entry file. 23 + * 24 + * - `true` — include all exports 25 + * - `false` — exclude all exports 26 + * - `(symbol) => boolean` — include exports matching the predicate 27 + * 28 + * @default false 29 + * @deprecated use `includeInEntry` instead 30 + */ 31 + exportFromIndex?: boolean | ((symbol: Symbol) => boolean); 32 + /** 33 + * Whether exports should be re-exported from the entry file. 34 + * 35 + * - `true` — include all exports 36 + * - `false` — exclude all exports 37 + * - `(symbol) => boolean` — include exports matching the predicate 38 + * 39 + * @default false 40 + */ 41 + includeInEntry?: boolean | ((symbol: Symbol) => boolean); 42 + }; 20 43 export type IndexExportOption = { 21 44 /** 22 - * Whether exports should be re-exported in the index file. 45 + * Whether exports should be re-exported from the entry file. 46 + * 47 + * @deprecated use `includeInEntry` instead 23 48 */ 24 - exportFromIndex: boolean; 49 + exportFromIndex: boolean | ((symbol: Symbol) => boolean); 50 + /** 51 + * Whether exports should be re-exported from the entry file. 52 + */ 53 + includeInEntry: boolean | ((symbol: Symbol) => boolean); 25 54 }; 26 55 27 56 export type NamingOptions = { ··· 56 85 */ 57 86 clean?: boolean; 58 87 /** 88 + * Whether to generate an entry file that re-exports symbols for convenient imports. 89 + * 90 + * Plugins control their inclusion via `includeInEntry`. 91 + * 92 + * @default true 93 + */ 94 + entryFile?: boolean; 95 + /** 59 96 * Optional function to transform file names before they are used. 60 97 * 61 98 * @param name The original file name. ··· 95 132 */ 96 133 header?: OutputHeader; 97 134 /** 98 - * Should the exports from plugin files be re-exported in the index 99 - * barrel file? By default, this is enabled and only default plugins 100 - * are re-exported. 135 + * Whether to generate an entry file that re-exports symbols for convenient imports. 136 + * 137 + * Plugins control their inclusion via `includeInEntry`. 101 138 * 102 139 * @default true 140 + * @deprecated use `entryFile` instead 103 141 */ 104 142 indexFile?: boolean; 105 143 /** ··· 146 184 */ 147 185 clean: boolean; 148 186 /** 187 + * Whether to generate an entry file that re-exports symbols for convenient imports. 188 + */ 189 + entryFile: boolean; 190 + /** 149 191 * Optional function to transform file names before they are used. 150 192 * 151 193 * @param name The original file name. ··· 169 211 */ 170 212 header: OutputHeader; 171 213 /** 172 - * Should the exports from plugin files be re-exported in the index 173 - * barrel file? By default, this is enabled and only default plugins 174 - * are re-exported. 214 + * Whether to generate an entry file that re-exports symbols for convenient imports. 215 + * 216 + * @deprecated use `entryFile` instead 175 217 */ 176 218 indexFile: boolean; 177 219 /**
+1
packages/shared/src/index.ts
··· 22 22 FeatureToggle, 23 23 IndexExportOption, 24 24 NamingOptions, 25 + UserIndexExportOption, 25 26 } from './config/shared'; 26 27 export type { ValueToObject } from './config/utils/config'; 27 28 export { valueToObject } from './config/utils/config';
-2
packages/shared/src/ir/context.ts
··· 98 98 private registerPlugin<T extends PluginNames>( 99 99 name: T, 100 100 ): T extends keyof PluginConfigMap ? PluginInstance<PluginConfigMap[T]> : PluginInstance { 101 - // Cast to a loose type internally - the config structure is guaranteed 102 - // by the config resolution layer, not by this method 103 101 const plugin = (this.config.plugins as Record<string, Plugin.Config<Plugin.Types>>)[name]!; 104 102 const instance = new PluginInstance({ 105 103 api: plugin.api,
+7 -7
packages/shared/src/openApi/__tests__/index.test.ts
··· 47 47 // @ts-expect-error 48 48 output: { 49 49 case: undefined, 50 - indexFile: false, 50 + entryFile: false, 51 51 path: '', 52 52 }, 53 53 // @ts-expect-error ··· 79 79 // @ts-expect-error 80 80 output: { 81 81 case: undefined, 82 - indexFile: false, 82 + entryFile: false, 83 83 path: '', 84 84 }, 85 85 // @ts-expect-error ··· 111 111 // @ts-expect-error 112 112 output: { 113 113 case: undefined, 114 - indexFile: false, 114 + entryFile: false, 115 115 path: '', 116 116 }, 117 117 // @ts-expect-error ··· 143 143 // @ts-expect-error 144 144 output: { 145 145 case: undefined, 146 - indexFile: false, 146 + entryFile: false, 147 147 path: '', 148 148 }, 149 149 // @ts-expect-error ··· 175 175 // @ts-expect-error 176 176 output: { 177 177 case: undefined, 178 - indexFile: false, 178 + entryFile: false, 179 179 path: '', 180 180 }, 181 181 // @ts-expect-error ··· 206 206 // @ts-expect-error 207 207 output: { 208 208 case: undefined, 209 - indexFile: false, 209 + entryFile: false, 210 210 path: '', 211 211 }, 212 212 // @ts-expect-error ··· 237 237 // @ts-expect-error 238 238 output: { 239 239 case: undefined, 240 - indexFile: false, 240 + entryFile: false, 241 241 path: '', 242 242 }, 243 243 // @ts-expect-error
+9 -1
packages/shared/src/parser/hooks.ts
··· 174 174 /** 175 175 * Optional output strategy to override default plugin behavior. 176 176 * 177 - * Use this to route generated symbols to specific files. 177 + * Use this to re-export symbols from specific files. 178 + * 179 + * @returns The file path(s) that re-export this symbol, or undefined to fallback to default behavior. 180 + */ 181 + getExportFromFilePath?: (symbol: Symbol) => ReadonlyArray<string> | undefined; 182 + /** 183 + * Optional output strategy to override default plugin behavior. 184 + * 185 + * Use this to route symbols to specific files. 178 186 * 179 187 * @returns The file path to output the symbol to, or undefined to fallback to default behavior. 180 188 */
+34 -6
packages/shared/src/plugins/shared/utils/instance.ts
··· 371 371 symbol(name: SymbolIn['name'], symbol?: Omit<SymbolIn, 'name'>): Symbol<ResolvedNode> { 372 372 const symbolIn: SymbolIn = { 373 373 ...symbol, 374 - exportFrom: 375 - symbol?.exportFrom ?? 376 - (!symbol?.external && this.context.config.output.indexFile && this.config.exportFromIndex 377 - ? ['index'] 378 - : undefined), 379 - getFilePath: symbol?.getFilePath ?? this.getSymbolFilePath.bind(this), 380 374 meta: { 381 375 pluginName: path.isAbsolute(this.name) ? 'custom' : this.name, 382 376 ...symbol?.meta, 383 377 }, 384 378 name, 385 379 }; 380 + if (symbolIn.getExportFromFilePath === undefined) { 381 + symbolIn.getExportFromFilePath = this.getSymbolExportFromFilePath.bind(this); 382 + } 383 + if (symbolIn.getFilePath === undefined) { 384 + symbolIn.getFilePath = this.getSymbolFilePath.bind(this); 385 + } 386 386 for (const hook of this.eventHooks['symbol:register:before']) { 387 387 hook({ plugin: this as any, symbol: symbolIn }); 388 388 } ··· 440 440 name: 'Error', 441 441 pluginName: this.name, 442 442 }); 443 + } 444 + 445 + private getSymbolExportFromFilePath(symbol: Symbol): ReadonlyArray<string> | undefined { 446 + const hooks = [this.config['~hooks']?.symbols, this.context.config.parser.hooks.symbols]; 447 + for (const hook of hooks) { 448 + const result = hook?.getExportFromFilePath?.(symbol); 449 + if (result !== undefined) return result; 450 + } 451 + 452 + // default logic below 453 + const entryFile = this.context.config.output.indexFile ?? this.context.config.output.entryFile; 454 + if (symbol.external || !entryFile) return; 455 + 456 + const includeInEntry = this.config.exportFromIndex ?? this.config.includeInEntry; 457 + if ( 458 + (typeof includeInEntry === 'boolean' && !includeInEntry) || 459 + (typeof includeInEntry === 'function' && !includeInEntry(symbol)) 460 + ) { 461 + return; 462 + } 463 + 464 + const language = symbol.node?.language; 465 + if (!language) return; 466 + 467 + const moduleEntryName = this.gen.moduleEntryNames[language]; 468 + if (!moduleEntryName) return; 469 + 470 + return [moduleEntryName]; 443 471 } 444 472 445 473 private getSymbolFilePath(symbol: Symbol): string | undefined {
+12 -12
packages/shared/src/plugins/types.ts
··· 1 1 /* eslint-disable @typescript-eslint/no-namespace */ 2 2 import type { AnyString } from '@hey-api/types'; 3 3 4 + import type { IndexExportOption, UserIndexExportOption } from '../config/shared'; 4 5 import type { ValueToObject } from '../config/utils/config'; 5 6 import type { Dependency } from '../config/utils/dependencies'; 6 - import type { Hooks } from '../parser/hooks'; 7 + import type { Hooks as ParserHooks } from '../parser/hooks'; 7 8 import type { PluginInstance } from './shared/utils/instance'; 8 9 9 10 // eslint-disable-next-line @typescript-eslint/no-empty-object-type ··· 29 30 30 31 type BaseApi = Record<string, unknown>; 31 32 32 - type BaseConfig = { 33 - /** 34 - * Whether exports should be re-exported in the index file. 35 - */ 36 - exportFromIndex?: boolean; 33 + type PluginBaseConfig = UserIndexExportOption & { 37 34 name: AnyPluginName; 38 35 /** 39 36 * Optional hooks to override default plugin behavior. ··· 41 38 * Use these to classify resources, control which outputs are generated, 42 39 * or provide custom behavior for specific resources. 43 40 */ 44 - '~hooks'?: Hooks; 41 + '~hooks'?: ParserHooks; 45 42 }; 46 43 47 44 /** ··· 75 72 tags?: ReadonlyArray<PluginTag>; 76 73 }; 77 74 75 + export type Exports = IndexExportOption; 76 + export type UserExports = UserIndexExportOption; 77 + 78 78 /** 79 79 * Generic wrapper for plugin hooks. 80 80 */ 81 - export type Hooks = Pick<BaseConfig, '~hooks'>; 81 + export type Hooks = Pick<PluginBaseConfig, '~hooks'>; 82 82 83 83 export interface Name<Name extends PluginNames> { 84 84 name: Name; ··· 102 102 }; 103 103 104 104 export type Types< 105 - Config extends BaseConfig = BaseConfig, 106 - ResolvedConfig extends BaseConfig = Config, 105 + Config extends PluginBaseConfig = PluginBaseConfig, 106 + ResolvedConfig extends PluginBaseConfig = Config, 107 107 Api extends BaseApi = never, 108 108 > = ([Api] extends [never] ? { api?: BaseApi } : { api: Api }) & { 109 109 config: Config; ··· 112 112 } 113 113 114 114 export type DefinePlugin< 115 - Config extends BaseConfig = BaseConfig, 116 - ResolvedConfig extends BaseConfig = Config, 115 + Config extends PluginBaseConfig = PluginBaseConfig, 116 + ResolvedConfig extends PluginBaseConfig = Config, 117 117 Api extends BaseApi = never, 118 118 > = { 119 119 Config: Plugin.Config<Plugin.Types<Config, ResolvedConfig, Api>>;
+3 -3
pyproject.toml
··· 1 1 [project] 2 2 name = "hey-api-dev" 3 3 version = "0.0.0" 4 - requires-python = ">=3.10" 4 + requires-python = ">=3.9" 5 5 6 6 [dependency-groups] 7 - dev = ["mypy>=1.15", "ruff>=0.11"] 7 + dev = ["httpx>=0.27", "mypy>=1.15", "ruff>=0.11"] 8 8 9 9 [tool.mypy] 10 - python_version = "3.10" 10 + python_version = "3.9" 11 11 strict = false 12 12 warn_return_any = true 13 13 warn_unused_configs = true
+1 -1
turbo.json
··· 3 3 "tasks": { 4 4 "build": { 5 5 "dependsOn": ["^build"], 6 - "inputs": ["src/**", "package.json", "tsconfig.json"], 6 + "inputs": ["src/**", "package.json", "tsdown.config.ts", "tsconfig.json"], 7 7 "outputs": [ 8 8 ".next/**", 9 9 "!.next/cache/**",
+100 -1
uv.lock
··· 1 1 version = 1 2 2 revision = 3 3 - requires-python = ">=3.10" 3 + requires-python = ">=3.9" 4 + 5 + [[package]] 6 + name = "anyio" 7 + version = "4.12.1" 8 + source = { registry = "https://pypi.org/simple" } 9 + dependencies = [ 10 + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 11 + { name = "idna" }, 12 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 13 + ] 14 + sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } 15 + wheels = [ 16 + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, 17 + ] 18 + 19 + [[package]] 20 + name = "certifi" 21 + version = "2026.1.4" 22 + source = { registry = "https://pypi.org/simple" } 23 + sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } 24 + wheels = [ 25 + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, 26 + ] 27 + 28 + [[package]] 29 + name = "exceptiongroup" 30 + version = "1.3.1" 31 + source = { registry = "https://pypi.org/simple" } 32 + dependencies = [ 33 + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, 34 + ] 35 + sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } 36 + wheels = [ 37 + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, 38 + ] 39 + 40 + [[package]] 41 + name = "h11" 42 + version = "0.16.0" 43 + source = { registry = "https://pypi.org/simple" } 44 + sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } 45 + wheels = [ 46 + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, 47 + ] 4 48 5 49 [[package]] 6 50 name = "hey-api-dev" ··· 9 53 10 54 [package.dev-dependencies] 11 55 dev = [ 56 + { name = "httpx" }, 12 57 { name = "mypy" }, 13 58 { name = "ruff" }, 14 59 ] ··· 17 62 18 63 [package.metadata.requires-dev] 19 64 dev = [ 65 + { name = "httpx", specifier = ">=0.27" }, 20 66 { name = "mypy", specifier = ">=1.15" }, 21 67 { name = "ruff", specifier = ">=0.11" }, 22 68 ] 23 69 24 70 [[package]] 71 + name = "httpcore" 72 + version = "1.0.9" 73 + source = { registry = "https://pypi.org/simple" } 74 + dependencies = [ 75 + { name = "certifi" }, 76 + { name = "h11" }, 77 + ] 78 + sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } 79 + wheels = [ 80 + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, 81 + ] 82 + 83 + [[package]] 84 + name = "httpx" 85 + version = "0.28.1" 86 + source = { registry = "https://pypi.org/simple" } 87 + dependencies = [ 88 + { name = "anyio" }, 89 + { name = "certifi" }, 90 + { name = "httpcore" }, 91 + { name = "idna" }, 92 + ] 93 + sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } 94 + wheels = [ 95 + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, 96 + ] 97 + 98 + [[package]] 99 + name = "idna" 100 + version = "3.11" 101 + source = { registry = "https://pypi.org/simple" } 102 + sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } 103 + wheels = [ 104 + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, 105 + ] 106 + 107 + [[package]] 25 108 name = "librt" 26 109 version = "0.7.8" 27 110 source = { registry = "https://pypi.org/simple" } ··· 92 175 { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" }, 93 176 { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" }, 94 177 { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" }, 178 + { url = "https://files.pythonhosted.org/packages/3b/9b/2668bb01f568bc89ace53736df950845f8adfcacdf6da087d5cef12110cb/librt-0.7.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c7e8f88f79308d86d8f39c491773cbb533d6cb7fa6476f35d711076ee04fceb6", size = 56680, upload-time = "2026-01-14T12:56:02.602Z" }, 179 + { url = "https://files.pythonhosted.org/packages/b3/d4/dbb3edf2d0ec4ba08dcaf1865833d32737ad208962d4463c022cea6e9d3c/librt-0.7.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:389bd25a0db916e1d6bcb014f11aa9676cedaa485e9ec3752dfe19f196fd377b", size = 58612, upload-time = "2026-01-14T12:56:03.616Z" }, 180 + { url = "https://files.pythonhosted.org/packages/0f/c9/64b029de4ac9901fcd47832c650a0fd050555a452bd455ce8deddddfbb9f/librt-0.7.8-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73fd300f501a052f2ba52ede721232212f3b06503fa12665408ecfc9d8fd149c", size = 163654, upload-time = "2026-01-14T12:56:04.975Z" }, 181 + { url = "https://files.pythonhosted.org/packages/81/5c/95e2abb1b48eb8f8c7fc2ae945321a6b82777947eb544cc785c3f37165b2/librt-0.7.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d772edc6a5f7835635c7562f6688e031f0b97e31d538412a852c49c9a6c92d5", size = 172477, upload-time = "2026-01-14T12:56:06.103Z" }, 182 + { url = "https://files.pythonhosted.org/packages/7e/27/9bdf12e05b0eb089dd008d9c8aabc05748aad9d40458ade5e627c9538158/librt-0.7.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde8a130bd0f239e45503ab39fab239ace094d63ee1d6b67c25a63d741c0f71", size = 186220, upload-time = "2026-01-14T12:56:09.958Z" }, 183 + { url = "https://files.pythonhosted.org/packages/53/6a/c3774f4cc95e68ed444a39f2c8bd383fd18673db7d6b98cfa709f6634b93/librt-0.7.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fdec6e2368ae4f796fc72fad7fd4bd1753715187e6d870932b0904609e7c878e", size = 183841, upload-time = "2026-01-14T12:56:11.109Z" }, 184 + { url = "https://files.pythonhosted.org/packages/58/6b/48702c61cf83e9c04ad5cec8cad7e5e22a2cde23a13db8ef341598897ddd/librt-0.7.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:00105e7d541a8f2ee5be52caacea98a005e0478cfe78c8080fbb7b5d2b340c63", size = 179751, upload-time = "2026-01-14T12:56:12.278Z" }, 185 + { url = "https://files.pythonhosted.org/packages/35/87/5f607fc73a131d4753f4db948833063c6aad18e18a4e6fbf64316c37ae65/librt-0.7.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c6f8947d3dfd7f91066c5b4385812c18be26c9d5a99ca56667547f2c39149d94", size = 199319, upload-time = "2026-01-14T12:56:13.425Z" }, 186 + { url = "https://files.pythonhosted.org/packages/6e/cc/b7c5ac28ae0f0645a9681248bae4ede665bba15d6f761c291853c5c5b78e/librt-0.7.8-cp39-cp39-win32.whl", hash = "sha256:41d7bb1e07916aeb12ae4a44e3025db3691c4149ab788d0315781b4d29b86afb", size = 43434, upload-time = "2026-01-14T12:56:14.781Z" }, 187 + { url = "https://files.pythonhosted.org/packages/e4/5d/dce0c92f786495adf2c1e6784d9c50a52fb7feb1cfb17af97a08281a6e82/librt-0.7.8-cp39-cp39-win_amd64.whl", hash = "sha256:e90a8e237753c83b8e484d478d9a996dc5e39fd5bd4c6ce32563bc8123f132be", size = 49801, upload-time = "2026-01-14T12:56:15.827Z" }, 95 188 ] 96 189 97 190 [[package]] ··· 137 230 { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, 138 231 { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, 139 232 { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, 233 + { url = "https://files.pythonhosted.org/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3", size = 13102606, upload-time = "2025-12-15T05:02:46.833Z" }, 234 + { url = "https://files.pythonhosted.org/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a", size = 12164496, upload-time = "2025-12-15T05:03:41.947Z" }, 235 + { url = "https://files.pythonhosted.org/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67", size = 12772068, upload-time = "2025-12-15T05:02:53.689Z" }, 236 + { url = "https://files.pythonhosted.org/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e", size = 13520385, upload-time = "2025-12-15T05:02:38.328Z" }, 237 + { url = "https://files.pythonhosted.org/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376", size = 13796221, upload-time = "2025-12-15T05:03:22.147Z" }, 238 + { url = "https://files.pythonhosted.org/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24", size = 10055456, upload-time = "2025-12-15T05:03:35.169Z" }, 140 239 { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, 141 240 ] 142 241
+3 -3
vercel.json
··· 1 1 { 2 2 "$schema": "https://openapi.vercel.sh/vercel.json", 3 - "buildCommand": "pnpm --filter @docs/openapi-ts build", 3 + "buildCommand": "pnpm --filter docs build", 4 4 "cleanUrls": true, 5 - "devCommand": "pnpm --filter @docs/openapi-ts dev", 5 + "devCommand": "pnpm --filter docs dev", 6 6 "framework": "vitepress", 7 - "installCommand": "pnpm install --filter @docs/openapi-ts", 7 + "installCommand": "pnpm install --filter docs", 8 8 "outputDirectory": "docs/.vitepress/dist", 9 9 "redirects": [ 10 10 {