genoc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/analyzer/naming.d.ts +24 -0
  4. package/dist/analyzer/naming.js +122 -0
  5. package/dist/analyzer/path-analyzer.d.ts +53 -0
  6. package/dist/analyzer/path-analyzer.js +222 -0
  7. package/dist/analyzer/schema-mapper.d.ts +48 -0
  8. package/dist/analyzer/schema-mapper.js +435 -0
  9. package/dist/cli/app.d.ts +9 -0
  10. package/dist/cli/app.js +60 -0
  11. package/dist/cli/errors.d.ts +3 -0
  12. package/dist/cli/errors.js +6 -0
  13. package/dist/cli/impl.d.ts +3 -0
  14. package/dist/cli/impl.js +45 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.js +5 -0
  17. package/dist/generator/client-generator.d.ts +21 -0
  18. package/dist/generator/client-generator.js +287 -0
  19. package/dist/generator/contracts-generator.d.ts +16 -0
  20. package/dist/generator/contracts-generator.js +525 -0
  21. package/dist/generator/error-types.d.ts +24 -0
  22. package/dist/generator/error-types.js +94 -0
  23. package/dist/generator/method-generator.d.ts +9 -0
  24. package/dist/generator/method-generator.js +249 -0
  25. package/dist/index.d.ts +7 -0
  26. package/dist/index.js +8 -0
  27. package/dist/parser/ref-resolver.d.ts +24 -0
  28. package/dist/parser/ref-resolver.js +119 -0
  29. package/dist/parser/spec-reader.d.ts +4 -0
  30. package/dist/parser/spec-reader.js +116 -0
  31. package/dist/parser/validators.d.ts +7 -0
  32. package/dist/parser/validators.js +79 -0
  33. package/dist/parser/version/index.d.ts +18 -0
  34. package/dist/parser/version/index.js +16 -0
  35. package/dist/parser/version/normalized-spec.d.ts +199 -0
  36. package/dist/parser/version/normalized-spec.js +1 -0
  37. package/dist/parser/version/registry.d.ts +28 -0
  38. package/dist/parser/version/registry.js +44 -0
  39. package/dist/parser/version/v3.0/index.d.ts +3 -0
  40. package/dist/parser/version/v3.0/index.js +3 -0
  41. package/dist/parser/version/v3.0/normalizer.d.ts +15 -0
  42. package/dist/parser/version/v3.0/normalizer.js +389 -0
  43. package/dist/parser/version/v3.0/strategy.d.ts +27 -0
  44. package/dist/parser/version/v3.0/strategy.js +96 -0
  45. package/dist/parser/version/v3.0/validator.d.ts +13 -0
  46. package/dist/parser/version/v3.0/validator.js +117 -0
  47. package/dist/parser/version/v3.1/index.d.ts +1 -0
  48. package/dist/parser/version/v3.1/index.js +1 -0
  49. package/dist/parser/version/v3.1/strategy.d.ts +42 -0
  50. package/dist/parser/version/v3.1/strategy.js +513 -0
  51. package/dist/parser/version/v3.2/index.d.ts +4 -0
  52. package/dist/parser/version/v3.2/index.js +4 -0
  53. package/dist/parser/version/v3.2/strategy.d.ts +39 -0
  54. package/dist/parser/version/v3.2/strategy.js +57 -0
  55. package/dist/parser/version/version-detector.d.ts +4 -0
  56. package/dist/parser/version/version-detector.js +34 -0
  57. package/dist/parser/version/version-strategy.d.ts +31 -0
  58. package/dist/parser/version/version-strategy.js +1 -0
  59. package/dist/types/client.d.ts +25 -0
  60. package/dist/types/client.js +1 -0
  61. package/dist/types/contracts.d.ts +13 -0
  62. package/dist/types/contracts.js +1 -0
  63. package/dist/types/openapi.d.ts +173 -0
  64. package/dist/types/openapi.js +1 -0
  65. package/dist/utils/case.d.ts +5 -0
  66. package/dist/utils/case.js +51 -0
  67. package/dist/utils/generator-helpers.d.ts +23 -0
  68. package/dist/utils/generator-helpers.js +66 -0
  69. package/dist/utils/string.d.ts +34 -0
  70. package/dist/utils/string.js +182 -0
  71. package/dist/utils/url.d.ts +10 -0
  72. package/dist/utils/url.js +40 -0
  73. package/package.json +60 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrey Kiselev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # genoc
2
+
3
+ Generate TypeScript HTTP clients from OpenAPI 3.0 / 3.1 specifications.
4
+ The generated code has zero runtime dependencies. Full type safety. Bring your own HTTP client.
5
+
6
+ [![npm version](https://img.shields.io/npm/v/genoc)](https://www.npmjs.com/package/genoc)
7
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org/)
8
+ [![TypeScript](https://img.shields.io/badge/typescript-blue)](https://www.typescriptlang.org/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
10
+
11
+ ## Features
12
+
13
+ - Full OpenAPI 3.0 and 3.1 specification support with automatic version detection
14
+ - End-to-end type safety — requests, responses, and errors are fully typed
15
+ - HTTP-client agnostic — adapter pattern lets you plug in fetch, axios, or anything else
16
+ - Error types with per-status-code narrowing and type guards
17
+ - File and binary upload/download with stream handling
18
+ - Flexible method naming strategies (path-based, operationId, operationId-with-fallback)
19
+ - CLI and programmatic API
20
+
21
+ ## Quick Start
22
+
23
+ Install:
24
+
25
+ ```bash
26
+ npm install -D genoc
27
+ ```
28
+
29
+ Generate:
30
+
31
+ ```bash
32
+ genoc ./path/to/spec.yaml --output-dir ./src/api
33
+ ```
34
+
35
+ This creates two files in `./src/api`:
36
+
37
+ - `contracts.ts` — Type definitions, error classes, and helper types
38
+ - `client.ts` — Typed client with `createClient(requester)` factory
39
+
40
+ ## Usage
41
+
42
+ The generated client requires a `Requester` implementation — a function that
43
+ performs the actual HTTP call and returns the result. This is the type your
44
+ implementation must satisfy:
45
+
46
+ ```typescript
47
+ type Requester = <TResponse>(
48
+ method: string,
49
+ path: string,
50
+ options: {
51
+ query?: Record<string, unknown>;
52
+ body?: unknown;
53
+ headers?: Record<string, string>;
54
+ expectStream?: true;
55
+ }
56
+ ) => Promise<TResponse | StreamResponse | ErrorResponse>;
57
+ ```
58
+
59
+ ### Basic Example with `fetch`
60
+
61
+ ```typescript
62
+ import { createClient } from './client.js';
63
+ import { ApiError, RequesterFailError, ErrorResponse } from './contracts.js';
64
+
65
+ const baseUrl = 'https://api.example.com';
66
+
67
+ const requester: Requester = async (method, path, options) => {
68
+ const url = new URL(path, baseUrl);
69
+ if (options.query) {
70
+ Object.entries(options.query).forEach(([key, value]) => {
71
+ url.searchParams.set(key, String(value));
72
+ });
73
+ }
74
+
75
+ const response = await fetch(url, {
76
+ method,
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ ...options.headers,
80
+ },
81
+ body: options.body ? JSON.stringify(options.body) : undefined,
82
+ });
83
+
84
+ if (!response.ok) {
85
+ return new ErrorResponse(
86
+ response.status,
87
+ await response.json(),
88
+ response.headers,
89
+ response.statusText
90
+ );
91
+ }
92
+
93
+ return response.json();
94
+ };
95
+
96
+ const client = createClient(requester);
97
+
98
+ // Typed call — response type is inferred from the spec
99
+ const pets = await client.getPets({ limit: 10 });
100
+ ```
101
+
102
+ See [Binary / File Responses](#binary--file-responses) for handling `expectStream: true`.
103
+
104
+ ## Binary / File Responses
105
+
106
+ When your spec defines binary responses (e.g. `format: binary`,
107
+ `application/octet-stream`, `image/*`), the generated client sends
108
+ `expectStream: true` in options. Your `Requester` should return a
109
+ `StreamResponse` in that case:
110
+
111
+ ```typescript
112
+ import { StreamResponse } from './contracts.js';
113
+
114
+ // Inside your Requester implementation:
115
+ if (options.expectStream === true) {
116
+ return new StreamResponse(
117
+ response.body as ReadableStream<Uint8Array>,
118
+ getFilename(response.headers), // extract from Content-Disposition
119
+ response.headers
120
+ );
121
+ }
122
+ ```
123
+
124
+ `StreamResponse` is a simple container:
125
+
126
+ ```typescript
127
+ class StreamResponse {
128
+ data: ReadableStream<Uint8Array>;
129
+ filename?: string;
130
+ headers: Headers;
131
+ }
132
+ ```
133
+
134
+ ### Response Helpers
135
+
136
+ The generated `contracts.ts` includes helper functions for constructing
137
+ responses in your `Requester` implementation:
138
+
139
+ - `streamResponse(data, filename?, headers?)` — Creates a `StreamResponse` instance
140
+ - `errorResponse(status, data, headers?, message?)` — Creates an `ErrorResponse` instance
141
+
142
+ These are convenience wrappers around the `StreamResponse` and `ErrorResponse`
143
+ constructors.
144
+
145
+ ## CLI Reference
146
+
147
+ ```bash
148
+ genoc <spec> [flags]
149
+ ```
150
+
151
+ `<spec>` — Path or URL to an OpenAPI 3.0 / 3.1 spec (JSON or YAML).
152
+
153
+ | Flag | Default | Description |
154
+ | ------------------------ | ------------ | ---------------------------------------------------- |
155
+ | `--output-dir` | (required) | Output directory for generated files |
156
+ | `--method-name-strategy` | `path-based` | Method naming strategy |
157
+ | `--spec-version` | auto-detect | Override version detection (`"3.0"` or `"3.1"`) |
158
+ | `--strict-version` | `true` | Warn if `--spec-version` mismatches detected version |
159
+
160
+ ## Method Naming Strategies
161
+
162
+ - **`path-based`** (default) — HTTP method + path segments in PascalCase.
163
+ `GET /pets` → `getPets`, `GET /api/v1/products` → `getApiV1Products`
164
+
165
+ - **`operationId`** — Use the `operationId` field from the spec.
166
+ `GET /pets` → `findPets` (if `operationId` is `"findPets"`)
167
+
168
+ - **`operationId-with-fallback`** — Use `operationId` if present, otherwise
169
+ fall back to path-based naming.
170
+
171
+ ## Programmatic API
172
+
173
+ ```typescript
174
+ import { generateClient } from 'genoc';
175
+
176
+ await generateClient({
177
+ input: './openapi.yaml',
178
+ outputDir: './src/api',
179
+ methodNameStrategy: 'path-based',
180
+ specVersion: '3.1',
181
+ strictVersion: true,
182
+ });
183
+ ```
184
+
185
+ ## Error Handling
186
+
187
+ The generated client throws typed errors. Each method carries its own error
188
+ union, and `isDefinedError` narrows a caught error to that union:
189
+
190
+ - **`ApiError<TStatus, TData>`** — Error for a specific status code defined in the spec
191
+ - **`UnspecifiedApiError`** — Error for a status code not defined in the spec
192
+ - **`RequesterFailError`** — Wraps unexpected failures in your `Requester`
193
+ - **`isDefinedError(err, client.method)`** — Type guard that narrows to the method's defined error union
194
+
195
+ ```typescript
196
+ import { UnspecifiedApiError, RequesterFailError } from './contracts.js';
197
+ import { isDefinedError } from './client.js';
198
+
199
+ try {
200
+ const result = await client.getPets();
201
+ } catch (error) {
202
+ if (isDefinedError(error, client.getPets)) {
203
+ // error is narrowed to GetPetsErrors (ApiError<400, ...> | ApiError<500, ...>)
204
+ if (error.status === 400) {
205
+ console.error('Bad request:', error.data);
206
+ }
207
+ }
208
+
209
+ if (error instanceof UnspecifiedApiError) {
210
+ console.error('Unexpected status:', error.status, error.data);
211
+ }
212
+
213
+ if (error instanceof RequesterFailError) {
214
+ console.error('Requester failed:', error.cause);
215
+ }
216
+ }
217
+ ```
218
+
219
+ ## Feature Support
220
+
221
+ Check the detailed feature support tables to see if your OpenAPI spec features are covered:
222
+
223
+ - **[OpenAPI 3.0 Support](./docs/openapi-3.0-support.md)** — Data types, schema keywords, parameters, request bodies, file uploads, responses, error handling, `$ref` resolution, components, security schemes, servers, and path operations.
224
+ - **[OpenAPI 3.1 Support](./docs/openapi-3.1-support.md)** — All 3.0 features plus type arrays, `$ref` siblings, webhooks, JSON Schema 2020-12 alignment, and a [3.0 → 3.1 diff](./docs/openapi-3.1-support.md#differences-from-openapi-30).
225
+
226
+ ## Requirements
227
+
228
+ - Node.js >= 18
229
+ - OpenAPI 3.0.x or 3.1.x specification (JSON or YAML, file path or URL)
230
+
231
+ ## License
232
+
233
+ [MIT](./LICENSE) — Copyright © Andrey Kiselev
@@ -0,0 +1,24 @@
1
+ import type { MethodNameStrategy } from '../types/client.js';
2
+ /**
3
+ * Generate a TypeScript method name from HTTP method and path
4
+ * @param method HTTP method (get, post, put, patch, delete, options, head, trace)
5
+ * @param path URL path
6
+ * @returns Generated method name
7
+ */
8
+ export declare function generateMethodName(method: string, path: string): string;
9
+ /**
10
+ * Generate a method name from operation ID
11
+ * @param operationId Operation ID from OpenAPI spec
12
+ * @returns Generated method name in camelCase
13
+ */
14
+ export declare function generateMethodNameFromOperationId(operationId: string): string;
15
+ /**
16
+ * Get method name based on strategy
17
+ * @param method HTTP method
18
+ * @param path URL path
19
+ * @param operationId Operation ID (optional)
20
+ * @param strategy Method naming strategy
21
+ * @returns Generated method name
22
+ * @throws Error if operationId strategy is used but no operationId provided
23
+ */
24
+ export declare function getMethodName(method: string, path: string, operationId: string | undefined, strategy: MethodNameStrategy): string;
@@ -0,0 +1,122 @@
1
+ import { toPascalCaseSegment } from '../utils/case.js';
2
+ import { sanitizeIdentifier } from '../utils/string.js';
3
+ import { isPathParam, extractParamName } from '../utils/url.js';
4
+ /**
5
+ * Generate a TypeScript method name from HTTP method and path
6
+ * @param method HTTP method (get, post, put, patch, delete, options, head, trace)
7
+ * @param path URL path
8
+ * @returns Generated method name
9
+ */
10
+ export function generateMethodName(method, path) {
11
+ const lowerMethod = method.toLowerCase();
12
+ // Get segments and identify parameters
13
+ const rawSegments = path.split('/').filter((segment) => segment.length > 0);
14
+ const segments = [];
15
+ const isParam = [];
16
+ for (const part of rawSegments) {
17
+ if (isPathParam(part)) {
18
+ // Extract parameter name and mark as parameter
19
+ const paramName = extractParamName(part);
20
+ isParam.push(true);
21
+ segments.push(paramName);
22
+ }
23
+ else if (part.startsWith(':')) {
24
+ // Handle standalone :param format
25
+ const paramName = part.substring(1);
26
+ isParam.push(true);
27
+ segments.push(paramName);
28
+ }
29
+ else if (part.includes(':')) {
30
+ // Handle embedded : separators (e.g., Products:change-quantity, {id}:recall)
31
+ const subParts = part.split(':');
32
+ for (let i = 0; i < subParts.length; i++) {
33
+ const subPart = subParts[i];
34
+ if (subPart === '')
35
+ continue;
36
+ if (isPathParam(subPart)) {
37
+ isParam.push(true);
38
+ segments.push(extractParamName(subPart));
39
+ }
40
+ else {
41
+ isParam.push(false);
42
+ segments.push(subPart);
43
+ }
44
+ }
45
+ }
46
+ else {
47
+ // Regular segment
48
+ isParam.push(false);
49
+ segments.push(part);
50
+ }
51
+ }
52
+ const transformedSegments = segments.map((segment, index) => {
53
+ if (isParam[index]) {
54
+ return `By${toPascalCaseSegment(segment)}`;
55
+ }
56
+ return toPascalCaseSegment(segment);
57
+ });
58
+ return lowerMethod + transformedSegments.join('');
59
+ }
60
+ /**
61
+ * Generate a method name from operation ID
62
+ * @param operationId Operation ID from OpenAPI spec
63
+ * @returns Generated method name in camelCase
64
+ */
65
+ export function generateMethodNameFromOperationId(operationId) {
66
+ if (!operationId) {
67
+ throw new Error('Operation ID cannot be empty');
68
+ }
69
+ const sanitized = sanitizeIdentifier(operationId);
70
+ const normalizedForCamelCase = sanitized.replace(/[$]/g, '').replace(/_/g, ' ');
71
+ // Split into words and convert to camelCase
72
+ const words = normalizedForCamelCase.split(/\s+/).filter((word) => word.length > 0);
73
+ if (words.length === 0) {
74
+ return '_';
75
+ }
76
+ // Check if the first word is a reserved word
77
+ const firstWord = words[0];
78
+ const isReservedWord = ['class', 'const', 'function', 'if', 'else', 'for', 'while', 'return', 'var', 'let'].includes(firstWord.toLowerCase()) || firstWord === 'getClass';
79
+ let camelCased;
80
+ if (isReservedWord) {
81
+ camelCased = '_' + firstWord.charAt(0).toLowerCase() + firstWord.slice(1);
82
+ }
83
+ else {
84
+ camelCased = firstWord.charAt(0).toLowerCase() + firstWord.slice(1);
85
+ }
86
+ // Add remaining words with proper capitalization
87
+ camelCased += words
88
+ .slice(1)
89
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
90
+ .join('');
91
+ if (!camelCased || !/^[a-zA-Z_$]/.test(camelCased)) {
92
+ return '_' + camelCased;
93
+ }
94
+ return camelCased;
95
+ }
96
+ /**
97
+ * Get method name based on strategy
98
+ * @param method HTTP method
99
+ * @param path URL path
100
+ * @param operationId Operation ID (optional)
101
+ * @param strategy Method naming strategy
102
+ * @returns Generated method name
103
+ * @throws Error if operationId strategy is used but no operationId provided
104
+ */
105
+ export function getMethodName(method, path, operationId, strategy) {
106
+ switch (strategy) {
107
+ case 'path-based':
108
+ return generateMethodName(method, path);
109
+ case 'operationId':
110
+ if (!operationId) {
111
+ throw new Error('Operation ID is required for operationId strategy but not provided');
112
+ }
113
+ return generateMethodNameFromOperationId(operationId);
114
+ case 'operationId-with-fallback':
115
+ if (operationId) {
116
+ return generateMethodNameFromOperationId(operationId);
117
+ }
118
+ return generateMethodName(method, path);
119
+ default:
120
+ throw new Error(`Unknown method name strategy: ${strategy}`);
121
+ }
122
+ }
@@ -0,0 +1,53 @@
1
+ import { RefResolver } from '../parser/ref-resolver.js';
2
+ import type { MethodNameStrategy } from '../types/client.js';
3
+ import type { OpenAPIDocument, ReferenceObject, SchemaObject } from '../types/openapi.js';
4
+ export interface AnalyzedParameter {
5
+ name: string;
6
+ in: 'path' | 'query' | 'header' | 'cookie';
7
+ required: boolean;
8
+ schema: SchemaObject | undefined;
9
+ tsType: string;
10
+ description?: string;
11
+ deprecated?: boolean;
12
+ }
13
+ export interface AnalyzedRequestBody {
14
+ required: boolean;
15
+ contentTypes: string[];
16
+ schema: SchemaObject | ReferenceObject | undefined;
17
+ tsType: string;
18
+ isMultipart: boolean;
19
+ }
20
+ export interface AnalyzedResponse {
21
+ statusCode: string;
22
+ description?: string;
23
+ schema: SchemaObject | ReferenceObject | undefined;
24
+ tsType: string;
25
+ isSuccess: boolean;
26
+ isBinary: boolean;
27
+ }
28
+ export interface AnalyzedOperation {
29
+ method: string;
30
+ path: string;
31
+ operationId: string | undefined;
32
+ methodName: string;
33
+ summary: string | undefined;
34
+ description: string | undefined;
35
+ deprecated: boolean;
36
+ tags: string[];
37
+ pathParams: AnalyzedParameter[];
38
+ queryParams: AnalyzedParameter[];
39
+ headerParams: AnalyzedParameter[];
40
+ cookieParams: AnalyzedParameter[];
41
+ requestBody: AnalyzedRequestBody | undefined;
42
+ responses: AnalyzedResponse[];
43
+ }
44
+ /**
45
+ * Analyze all paths and operations from an OpenAPI document into structured data
46
+ * for code generation.
47
+ *
48
+ * @param doc - The parsed and validated OpenAPI document
49
+ * @param resolver - A RefResolver for resolving $ref pointers
50
+ * @param strategy - Method naming strategy (defaults to 'path-based')
51
+ * @returns Array of AnalyzedOperation objects
52
+ */
53
+ export declare function analyzePaths(doc: OpenAPIDocument, resolver: RefResolver, strategy?: MethodNameStrategy): AnalyzedOperation[];
@@ -0,0 +1,222 @@
1
+ import { RefResolver } from '../parser/ref-resolver.js';
2
+ import { getMethodName } from './naming.js';
3
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
4
+ function isRef(obj) {
5
+ return obj !== null && typeof obj === 'object' && '$ref' in obj;
6
+ }
7
+ function isBinaryContentType(ct) {
8
+ if (ct === 'application/octet-stream')
9
+ return true;
10
+ if (ct.startsWith('image/'))
11
+ return true;
12
+ if (ct.startsWith('video/'))
13
+ return true;
14
+ if (ct.startsWith('audio/'))
15
+ return true;
16
+ return false;
17
+ }
18
+ function schemaToTsType(schema, resolver) {
19
+ if (!schema)
20
+ return 'unknown';
21
+ if (isRef(schema)) {
22
+ const resolved = resolver.resolve(schema);
23
+ const refStr = schema.$ref;
24
+ const lastSegment = refStr.split('/').pop();
25
+ if (lastSegment && resolved.type) {
26
+ return lastSegment;
27
+ }
28
+ return lastSegment ?? 'unknown';
29
+ }
30
+ const s = schema;
31
+ if (s.type === undefined)
32
+ return 'unknown';
33
+ if (Array.isArray(s.type)) {
34
+ const nonNull = s.type.filter((t) => t !== 'null');
35
+ if (nonNull.length === 0)
36
+ return 'null';
37
+ return schemaToTsType({ ...s, type: nonNull[0] }, resolver);
38
+ }
39
+ switch (s.type) {
40
+ case 'string':
41
+ return 'string';
42
+ case 'integer':
43
+ case 'number':
44
+ return 'number';
45
+ case 'boolean':
46
+ return 'boolean';
47
+ case 'array':
48
+ if (s.items) {
49
+ const itemType = schemaToTsType(s.items, resolver);
50
+ return `${itemType}[]`;
51
+ }
52
+ return 'unknown[]';
53
+ case 'object':
54
+ return 'object';
55
+ case 'null':
56
+ return 'null';
57
+ default:
58
+ return 'unknown';
59
+ }
60
+ }
61
+ function resolveParameter(param, resolver) {
62
+ return resolver.resolve(param);
63
+ }
64
+ function resolveRequestBody(body, resolver) {
65
+ return resolver.resolve(body);
66
+ }
67
+ function resolveResponse(response, resolver) {
68
+ return resolver.resolve(response);
69
+ }
70
+ function analyzeParameter(param, resolver) {
71
+ const schema = param.schema ? resolver.resolve(param.schema) : undefined;
72
+ return {
73
+ name: param.name,
74
+ in: param.in,
75
+ required: param.required ?? param.in === 'path',
76
+ schema,
77
+ tsType: schemaToTsType(param.schema, resolver),
78
+ description: param.description,
79
+ deprecated: param.deprecated,
80
+ };
81
+ }
82
+ function mergeParameters(pathItemParams, operationParams, resolver) {
83
+ const resolvedPathParams = (pathItemParams ?? []).map((p) => resolveParameter(p, resolver));
84
+ const resolvedOpParams = (operationParams ?? []).map((p) => resolveParameter(p, resolver));
85
+ const opParamMap = new Map();
86
+ for (const p of resolvedOpParams) {
87
+ opParamMap.set(`${p.name}::${p.in}`, p);
88
+ }
89
+ const merged = [];
90
+ for (const p of resolvedPathParams) {
91
+ const key = `${p.name}::${p.in}`;
92
+ if (!opParamMap.has(key)) {
93
+ merged.push(p);
94
+ }
95
+ }
96
+ for (const p of resolvedOpParams) {
97
+ merged.push(p);
98
+ }
99
+ return merged;
100
+ }
101
+ function analyzeRequestBody(body, resolver) {
102
+ if (!body)
103
+ return undefined;
104
+ const resolved = resolveRequestBody(body, resolver);
105
+ const contentTypes = Object.keys(resolved.content);
106
+ let schema;
107
+ let tsType = 'unknown';
108
+ if (contentTypes.length > 0) {
109
+ const firstContent = resolved.content[contentTypes[0]];
110
+ if (firstContent?.schema) {
111
+ schema = firstContent.schema;
112
+ tsType = schemaToTsType(schema, resolver);
113
+ }
114
+ }
115
+ const isMultipart = contentTypes.length > 0 && contentTypes[0] === 'multipart/form-data';
116
+ return {
117
+ required: resolved.required ?? false,
118
+ contentTypes,
119
+ schema,
120
+ tsType,
121
+ isMultipart,
122
+ };
123
+ }
124
+ function analyzeResponses(responses, resolver) {
125
+ const result = [];
126
+ for (const [statusCode, response] of Object.entries(responses)) {
127
+ const resolved = resolveResponse(response, resolver);
128
+ let schema;
129
+ let tsType = 'unknown';
130
+ let contentTypes = [];
131
+ if (resolved.content) {
132
+ contentTypes = Object.keys(resolved.content);
133
+ if (contentTypes.length > 0) {
134
+ const firstContent = resolved.content[contentTypes[0]];
135
+ if (firstContent?.schema) {
136
+ schema = firstContent.schema;
137
+ tsType = schemaToTsType(schema, resolver);
138
+ }
139
+ }
140
+ }
141
+ // Empty-body success responses → void
142
+ if (tsType === 'unknown' &&
143
+ statusCode.startsWith('2') &&
144
+ (!resolved.content || contentTypes.length === 0)) {
145
+ tsType = 'void';
146
+ }
147
+ const isBinary = contentTypes.length > 0 && isBinaryContentType(contentTypes[0]);
148
+ result.push({
149
+ statusCode,
150
+ description: resolved.description,
151
+ schema,
152
+ tsType,
153
+ isSuccess: statusCode.startsWith('2'),
154
+ isBinary,
155
+ });
156
+ }
157
+ return result;
158
+ }
159
+ function categorizeParameters(params) {
160
+ const pathParams = [];
161
+ const queryParams = [];
162
+ const headerParams = [];
163
+ const cookieParams = [];
164
+ for (const param of params) {
165
+ switch (param.in) {
166
+ case 'path':
167
+ pathParams.push(param);
168
+ break;
169
+ case 'query':
170
+ queryParams.push(param);
171
+ break;
172
+ case 'header':
173
+ headerParams.push(param);
174
+ break;
175
+ case 'cookie':
176
+ cookieParams.push(param);
177
+ break;
178
+ }
179
+ }
180
+ return { pathParams, queryParams, headerParams, cookieParams };
181
+ }
182
+ /**
183
+ * Analyze all paths and operations from an OpenAPI document into structured data
184
+ * for code generation.
185
+ *
186
+ * @param doc - The parsed and validated OpenAPI document
187
+ * @param resolver - A RefResolver for resolving $ref pointers
188
+ * @param strategy - Method naming strategy (defaults to 'path-based')
189
+ * @returns Array of AnalyzedOperation objects
190
+ */
191
+ export function analyzePaths(doc, resolver, strategy = 'path-based') {
192
+ const operations = [];
193
+ if (!doc.paths)
194
+ return operations;
195
+ for (const [urlPath, pathItem] of Object.entries(doc.paths)) {
196
+ for (const method of HTTP_METHODS) {
197
+ const operation = pathItem[method];
198
+ if (!operation)
199
+ continue;
200
+ const mergedParams = mergeParameters(pathItem.parameters, operation.parameters, resolver);
201
+ const analyzedParams = mergedParams.map((p) => analyzeParameter(p, resolver));
202
+ const categorized = categorizeParameters(analyzedParams);
203
+ const requestBody = analyzeRequestBody(operation.requestBody, resolver);
204
+ const responses = analyzeResponses(operation.responses, resolver);
205
+ const methodName = getMethodName(method, urlPath, operation.operationId, strategy);
206
+ operations.push({
207
+ method,
208
+ path: urlPath,
209
+ operationId: operation.operationId,
210
+ methodName,
211
+ summary: operation.summary,
212
+ description: operation.description,
213
+ deprecated: operation.deprecated ?? false,
214
+ tags: operation.tags ?? [],
215
+ ...categorized,
216
+ requestBody,
217
+ responses,
218
+ });
219
+ }
220
+ }
221
+ return operations;
222
+ }