genoc 0.1.2 → 0.1.3

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.
@@ -1,4 +1,5 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
+ import { sanitizeTypeName } from '../utils/generator-helpers.js';
2
3
  import { getMethodName } from './naming.js';
3
4
  const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];
4
5
  function isRef(obj) {
@@ -21,7 +22,7 @@ function schemaToTsType(schema, resolver) {
21
22
  if (isRef(schema)) {
22
23
  const resolved = resolver.resolve(schema);
23
24
  const refStr = schema.$ref;
24
- const lastSegment = refStr.split('/').pop();
25
+ const lastSegment = sanitizeTypeName(refStr.split('/').pop());
25
26
  if (lastSegment && resolved.type) {
26
27
  return lastSegment;
27
28
  }
@@ -1,5 +1,6 @@
1
1
  import { RefResolver } from '../parser/ref-resolver.js';
2
2
  import { formatToBrandTypeName } from '../utils/case.js';
3
+ import { sanitizeTypeName } from '../utils/generator-helpers.js';
3
4
  import { quoteKey } from '../utils/string.js';
4
5
  /**
5
6
  * Default: extracts the last segment of the JSON pointer.
@@ -7,7 +8,8 @@ import { quoteKey } from '../utils/string.js';
7
8
  */
8
9
  function defaultTypeNameGenerator(refString) {
9
10
  const segments = refString.split('/');
10
- return segments[segments.length - 1] || 'unknown';
11
+ const rawSegment = segments[segments.length - 1] || 'unknown';
12
+ return sanitizeTypeName(rawSegment);
11
13
  }
12
14
  function isRefObject(obj) {
13
15
  return (obj !== null &&
@@ -230,7 +232,7 @@ export class SchemaMapper {
230
232
  }
231
233
  }
232
234
  resolveDiscriminatorInfo(schema, refStr) {
233
- const schemaName = refStr.split('/').pop();
235
+ const schemaName = sanitizeTypeName(refStr.split('/').pop());
234
236
  const target = this.discriminatorTargets.get(schemaName);
235
237
  if (target)
236
238
  return target;
@@ -1,7 +1,7 @@
1
1
  import { analyzePaths } from '../analyzer/path-analyzer.js';
2
2
  import { SchemaMapper } from '../analyzer/schema-mapper.js';
3
3
  import { RefResolver } from '../parser/ref-resolver.js';
4
- import { toPascalCase, getOperationTypePrefix, makeHeader } from '../utils/generator-helpers.js';
4
+ import { RESERVED_TYPE_NAMES, buildSchemaRenameMap, sanitizeTypeName, toPascalCase, getOperationTypePrefix, makeHeader, } from '../utils/generator-helpers.js';
5
5
  function isBinaryContentType(ct) {
6
6
  if (ct === 'application/octet-stream')
7
7
  return true;
@@ -17,22 +17,24 @@ function isBinaryContentType(ct) {
17
17
  * If the schema is a $ref to a discriminated base type (or an array whose items
18
18
  * are), replace the type name with the {Base}Variant union type.
19
19
  */
20
- function substituteDiscriminatedType(tsType, schema, discriminatorInfo) {
20
+ function substituteDiscriminatedType(tsType, schema, discriminatorInfo, renameMap) {
21
21
  const refSchema = schema;
22
22
  if (!refSchema || typeof refSchema !== 'object')
23
23
  return tsType;
24
24
  if (typeof refSchema.$ref === 'string') {
25
- const schemaName = refSchema.$ref.split('/').pop();
25
+ const schemaName = sanitizeTypeName(refSchema.$ref.split('/').pop());
26
26
  if (schemaName && discriminatorInfo.has(schemaName)) {
27
- return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
27
+ const renamed = renameMap.get(schemaName) ?? schemaName;
28
+ return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
28
29
  }
29
30
  }
30
31
  if (refSchema.items && typeof refSchema.items === 'object') {
31
32
  const items = refSchema.items;
32
33
  if (typeof items.$ref === 'string') {
33
- const schemaName = items.$ref.split('/').pop();
34
+ const schemaName = sanitizeTypeName(items.$ref.split('/').pop());
34
35
  if (schemaName && discriminatorInfo.has(schemaName)) {
35
- return tsType.replace(new RegExp(`\\b${schemaName}\\b`, 'g'), `${schemaName}Variant`);
36
+ const renamed = renameMap.get(schemaName) ?? schemaName;
37
+ return tsType.replace(new RegExp(`\\b${renamed}\\b`, 'g'), `${renamed}Variant`);
36
38
  }
37
39
  }
38
40
  }
@@ -162,6 +164,19 @@ function topologicalSort(entries, allNames) {
162
164
  * 7b. UnspecifiedApiError class
163
165
  */
164
166
  export function generateContracts(doc, resolver) {
167
+ const schemaNameList = doc.components?.schemas ? Object.keys(doc.components.schemas) : [];
168
+ const renameMap = buildSchemaRenameMap(schemaNameList, RESERVED_TYPE_NAMES);
169
+ for (const [original, renamed] of renameMap) {
170
+ // TODO: Replace with structured logging solution
171
+ // oxlint-disable-next-line no-console
172
+ console.warn(`Warning: Schema "${original}" collides with a built-in type and was renamed to "${renamed}".`);
173
+ }
174
+ const renamingTypeGenerator = (refString) => {
175
+ const segments = refString.split('/');
176
+ const rawSegment = segments[segments.length - 1] || 'unknown';
177
+ const lastSegment = sanitizeTypeName(rawSegment);
178
+ return renameMap.get(lastSegment) ?? lastSegment;
179
+ };
165
180
  const discriminatorInfo = new Map();
166
181
  if (doc.components?.schemas) {
167
182
  for (const [name, schema] of Object.entries(doc.components.schemas)) {
@@ -170,11 +185,12 @@ export function generateContracts(doc, resolver) {
170
185
  const mapping = new Map();
171
186
  if (resolved.discriminator.mapping) {
172
187
  for (const [key, ref] of Object.entries(resolved.discriminator.mapping)) {
173
- const targetName = ref.split('/').pop() || key;
174
- mapping.set(key, targetName);
188
+ const targetName = sanitizeTypeName(ref.split('/').pop() || key);
189
+ const renamedTarget = renameMap.get(targetName) ?? targetName;
190
+ mapping.set(key, renamedTarget);
175
191
  }
176
192
  }
177
- discriminatorInfo.set(name, {
193
+ discriminatorInfo.set(sanitizeTypeName(name), {
178
194
  propertyName: resolved.discriminator.propertyName,
179
195
  mapping,
180
196
  });
@@ -190,25 +206,26 @@ export function generateContracts(doc, resolver) {
190
206
  });
191
207
  }
192
208
  }
193
- // reservedNames: prevent branded types from colliding with user-defined schema names
194
209
  const allSchemaNames = new Set();
195
210
  if (doc.components?.schemas) {
196
211
  for (const name of Object.keys(doc.components.schemas)) {
197
- allSchemaNames.add(name);
212
+ allSchemaNames.add(sanitizeTypeName(name));
198
213
  }
199
214
  }
200
- const mapper = new SchemaMapper(resolver, undefined, discriminatorTargets, allSchemaNames);
215
+ const mapper = new SchemaMapper(resolver, renamingTypeGenerator, discriminatorTargets, allSchemaNames);
201
216
  const lines = [];
202
217
  lines.push(makeHeader(doc.openapi));
203
218
  // Section 1: Schema types
204
219
  const schemaEntries = [];
205
220
  if (doc.components?.schemas) {
206
221
  for (const [name, schema] of Object.entries(doc.components.schemas)) {
207
- const result = mapper.mapSchema(schema, name);
222
+ const sanitizedName = sanitizeTypeName(name);
223
+ const renamedName = renameMap.get(sanitizedName) ?? sanitizedName;
224
+ const result = mapper.mapSchema(schema, sanitizedName);
208
225
  const resolved = resolver.resolve(schema);
209
- const definition = `export type ${name} = ${result.tsType};`;
226
+ const definition = `export type ${renamedName} = ${result.tsType};`;
210
227
  schemaEntries.push({
211
- name,
228
+ name: renamedName,
212
229
  kind: 'type',
213
230
  definition,
214
231
  jsDoc: buildJsDoc(resolved.description),
@@ -228,9 +245,10 @@ export function generateContracts(doc, resolver) {
228
245
  if (subtypeNames.length === 0)
229
246
  continue;
230
247
  const unionType = subtypeNames.join(' | ');
248
+ const renamedBase = renameMap.get(sanitizeTypeName(baseName)) ?? sanitizeTypeName(baseName);
231
249
  lines.push('');
232
- lines.push(`export type ${baseName}Variant = ${unionType};`);
233
- allSchemaNames.add(`${baseName}Variant`);
250
+ lines.push(`export type ${renamedBase}Variant = ${unionType};`);
251
+ allSchemaNames.add(`${renamedBase}Variant`);
234
252
  }
235
253
  // Section 1b: Security scheme types
236
254
  const securitySchemes = doc.components?.securitySchemes;
@@ -376,7 +394,7 @@ export function generateContracts(doc, resolver) {
376
394
  return 'StreamResponse';
377
395
  if (r.schema) {
378
396
  const result = mapper.mapSchema(r.schema, undefined, 'response').tsType;
379
- return substituteDiscriminatedType(result, r.schema, discriminatorInfo);
397
+ return substituteDiscriminatedType(result, r.schema, discriminatorInfo, renameMap);
380
398
  }
381
399
  return r.tsType;
382
400
  });
@@ -15,10 +15,9 @@ export declare class DefaultApiError<TData> extends Error {
15
15
  * Produces:
16
16
  * 1. `ApiError<TStatus, TData>` class
17
17
  * 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
18
- * 3. `isError` type guard function
19
- * 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
20
- * 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
21
- * 6. `DefaultErrorBody` type for `default` responses
22
- * 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
18
+ * 3. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
19
+ * 4. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
20
+ * 5. `DefaultErrorBody` type for `default` responses
21
+ * 6. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
23
22
  */
24
23
  export declare function generateErrorTypes(operations: AnalyzedOperation[]): string;
@@ -24,11 +24,10 @@ export class DefaultApiError extends Error {
24
24
  * Produces:
25
25
  * 1. `ApiError<TStatus, TData>` class
26
26
  * 2. `UnspecifiedApiError` class (extends ApiError, for status codes not in spec)
27
- * 3. `isError` type guard function
28
- * 4. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
29
- * 5. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
30
- * 6. `DefaultErrorBody` type for `default` responses
31
- * 7. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
27
+ * 3. Per-operation error union types (e.g. `GetApiV1ProductsErrors`)
28
+ * 4. Per-operation per-status error type aliases (e.g. `GetApiV1ProductsError400`)
29
+ * 5. `DefaultErrorBody` type for `default` responses
30
+ * 6. Catch-all `UnspecifiedApiError` in error unions for unexpected status codes
32
31
  */
33
32
  export function generateErrorTypes(operations) {
34
33
  const lines = [];
@@ -54,12 +53,6 @@ export function generateErrorTypes(operations) {
54
53
  }
55
54
  }`);
56
55
  lines.push('');
57
- lines.push(`export function isError<T extends { status: number }, S extends number>(
58
- response: T,
59
- status: S,
60
- ): response is Extract<T, { status: S }> {
61
- return response.status === status;
62
- }`);
63
56
  let needsDefaultErrorBody = false;
64
57
  for (const op of operations) {
65
58
  const errorResponses = op.responses.filter((r) => !r.isSuccess && r.statusCode !== 'default');
@@ -1,4 +1,22 @@
1
1
  import type { AnalyzedOperation } from '../analyzer/path-analyzer.js';
2
+ /**
3
+ * Reserved type names used by the generated output's built-in classes,
4
+ * functions, and types. User-defined schema names that collide with these
5
+ * are automatically renamed with a suffix to prevent duplicate identifiers.
6
+ */
7
+ export declare const RESERVED_TYPE_NAMES: ReadonlySet<string>;
8
+ /**
9
+ * Sanitize a schema type name by converting dot-separated segments into
10
+ * PascalCase. Names without dots pass through unchanged.
11
+ *
12
+ * "Api.Error" → "ApiError", "Simple" → "Simple"
13
+ */
14
+ export declare function sanitizeTypeName(name: string): string;
15
+ /**
16
+ * Build a mapping of sanitized schema names → renamed names for any that
17
+ * collide with reserved type names.
18
+ */
19
+ export declare function buildSchemaRenameMap(schemaNames: Iterable<string>, reserved: ReadonlySet<string>, suffix?: string): Map<string, string>;
2
20
  /**
3
21
  * Convert a string to PascalCase, handling camelCase, kebab-case, snake_case,
4
22
  * and colon-separated segments.
@@ -1,3 +1,55 @@
1
+ /**
2
+ * Reserved type names used by the generated output's built-in classes,
3
+ * functions, and types. User-defined schema names that collide with these
4
+ * are automatically renamed with a suffix to prevent duplicate identifiers.
5
+ */
6
+ export const RESERVED_TYPE_NAMES = new Set([
7
+ // Classes in contracts.ts
8
+ 'StreamResponse',
9
+ 'ErrorResponse',
10
+ 'ApiError',
11
+ 'UnspecifiedApiError',
12
+ 'DefaultApiError',
13
+ 'RequesterFailError',
14
+ // Functions in contracts.ts
15
+ 'streamResponse',
16
+ 'errorResponse',
17
+ // Types / functions in client.ts
18
+ 'Requester',
19
+ 'isDefinedError',
20
+ 'decorateWithErrors',
21
+ 'ApiClient',
22
+ // Interface in contracts.ts (when file uploads present)
23
+ 'FileInput',
24
+ ]);
25
+ /**
26
+ * Sanitize a schema type name by converting dot-separated segments into
27
+ * PascalCase. Names without dots pass through unchanged.
28
+ *
29
+ * "Api.Error" → "ApiError", "Simple" → "Simple"
30
+ */
31
+ export function sanitizeTypeName(name) {
32
+ if (!name.includes('.'))
33
+ return name;
34
+ return name
35
+ .split('.')
36
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
37
+ .join('');
38
+ }
39
+ /**
40
+ * Build a mapping of sanitized schema names → renamed names for any that
41
+ * collide with reserved type names.
42
+ */
43
+ export function buildSchemaRenameMap(schemaNames, reserved, suffix = 'Model') {
44
+ const renameMap = new Map();
45
+ for (const name of schemaNames) {
46
+ const sanitized = sanitizeTypeName(name);
47
+ if (reserved.has(sanitized)) {
48
+ renameMap.set(sanitized, `${sanitized}${suffix}`);
49
+ }
50
+ }
51
+ return renameMap;
52
+ }
1
53
  /**
2
54
  * Convert a string to PascalCase, handling camelCase, kebab-case, snake_case,
3
55
  * and colon-separated segments.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genoc",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Generate typed HTTP clients from OpenAPI 3.0 / 3.1 specifications",
5
5
  "keywords": [
6
6
  "client",