diff --git a/src/index.ts b/src/index.ts index 40cf7ab..6a59b83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ import { isValidOpenapiSchema } from './utils/validation.js'; import { extractSchemas } from './utils/extract-schemas.js'; import { convertSchema } from './utils/json-schema-to-zod.js'; import { convertToTypedDict } from './utils/json-schema-to-typed-dict.js'; +import { toCamelCase, toPascalCase } from './utils/case.js'; import { sortSchemas } from './utils/sort-schemas.js'; import Handlebars from 'handlebars'; @@ -95,12 +96,12 @@ program const template = Handlebars.compile(templateSource); const ordered = sortSchemas(schemas); - const schemaData = ordered.map((name) => { - const schema = schemas[name]!; + const schemaData = ordered.map((rawName) => { + const schema = schemas[rawName]!; const { zodString } = convertSchema(schema); const desc = schema.description as string | undefined; return { - schemaName: name, + schemaName: `${toCamelCase(rawName)}Schema`, zodString, description: desc ? desc.replace(/\n/g, ' ') : undefined }; @@ -148,10 +149,10 @@ program const typingImports = new Set(); const ordered = sortSchemas(schemas); - const definitions = ordered.map((name) => { - const res = convertToTypedDict(name, schemas[name]!); + const definitions = ordered.map((rawName) => { + const res = convertToTypedDict(rawName, schemas[rawName]!); res.typingImports.forEach((i) => typingImports.add(i)); - return { name, definition: res.definition }; + return { name: toPascalCase(rawName), definition: res.definition }; }); const content = template({ diff --git a/src/utils/case.ts b/src/utils/case.ts new file mode 100644 index 0000000..b3e207c --- /dev/null +++ b/src/utils/case.ts @@ -0,0 +1,13 @@ +export function toPascalCase(str: string): string { + return str + .replace(/[^a-zA-Z0-9]+/g, ' ') + .split(' ') + .filter(Boolean) + .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) + .join(''); +} + +export function toCamelCase(str: string): string { + const pascal = toPascalCase(str); + return pascal.charAt(0).toLowerCase() + pascal.slice(1); +} diff --git a/src/utils/json-schema-to-typed-dict.ts b/src/utils/json-schema-to-typed-dict.ts index 635494b..3a0e94b 100644 --- a/src/utils/json-schema-to-typed-dict.ts +++ b/src/utils/json-schema-to-typed-dict.ts @@ -1,4 +1,5 @@ import type { OpenAPIV3_1 as OpenAPI } from 'openapi-types'; +import { toPascalCase } from './case.js'; export interface TypedDictResult { definition: string; @@ -6,22 +7,15 @@ export interface TypedDictResult { } export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): TypedDictResult { + name = toPascalCase(name); const typingImports = new Set(['TypedDict']); const extraDefs: string[] = []; - function toPascal(str: string): string { - return str - .replace(/[^a-zA-Z0-9]+/g, ' ') - .split(' ') - .filter(Boolean) - .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) - .join(''); - } - function flatten(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): { bases: string[]; schema: OpenAPI.SchemaObject | null } { if ('$ref' in s) { const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/); - return { bases: [match?.[1] ?? s.$ref], schema: null }; + const refName = match?.[1] ?? s.$ref; + return { bases: [toPascalCase(refName)], schema: null }; } if ('allOf' in s && Array.isArray(s.allOf)) { @@ -51,7 +45,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | const fields: string[] = []; const attrLines: string[] = []; for (const [key, value] of Object.entries(props)) { - const typeStr = toType(value as any, `${className}${toPascal(key)}`); + const typeStr = toType(value as any, `${className}${toPascalCase(key)}`); const desc = (value as any).description; if (required.has(key)) { typingImports.add('Required'); @@ -95,7 +89,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | if ('$ref' in s) { const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/); const refName = match?.[1] ?? s.$ref; - return `${refName}`; + return `${toPascalCase(refName)}`; } if ('oneOf' in s && Array.isArray(s.oneOf)) { @@ -169,7 +163,7 @@ export function convertToTypedDict(name: string, schema: OpenAPI.SchemaObject | const fields: string[] = []; const attrLines: string[] = []; for (const [key, value] of Object.entries(props)) { - const typeStr = toType(value as any, `${name}${toPascal(key)}`); + const typeStr = toType(value as any, `${name}${toPascalCase(key)}`); const desc = (value as any).description; if (required.has(key)) { typingImports.add('Required'); diff --git a/src/utils/json-schema-to-zod.ts b/src/utils/json-schema-to-zod.ts index 31079cc..01bff27 100644 --- a/src/utils/json-schema-to-zod.ts +++ b/src/utils/json-schema-to-zod.ts @@ -1,4 +1,5 @@ import type { OpenAPIV3_1 as OpenAPI } from 'openapi-types'; +import { toCamelCase } from './case.js'; export interface ZodResult { zodString: string; @@ -11,7 +12,8 @@ export function convertSchema(schema: OpenAPI.SchemaObject | OpenAPI.ReferenceOb function walk(s: OpenAPI.SchemaObject | OpenAPI.ReferenceObject): string { if ('$ref' in s) { const match = s.$ref.match(/^#\/components\/schemas\/(.+)$/); - const name = match?.[1] ?? s.$ref; + const raw = match?.[1] ?? s.$ref; + const name = `${toCamelCase(raw)}Schema`; imports.add(name); return name; } diff --git a/tests/__snapshots__/generate-zod.spec.ts.snap b/tests/__snapshots__/generate-zod.spec.ts.snap index 569684c..e5a76ef 100644 --- a/tests/__snapshots__/generate-zod.spec.ts.snap +++ b/tests/__snapshots__/generate-zod.spec.ts.snap @@ -5,7 +5,7 @@ exports[`generate-zod > generates a simple object schema 1`] = ` import { z } from 'zod/v4'; -export const User = z.object({ +export const userSchema = z.object({ id: z.string() }); " @@ -16,6 +16,6 @@ exports[`generate-zod > generates enums and nested arrays 1`] = ` import { z } from 'zod/v4'; -export const Wrapper = z.array(Status); +export const wrapperSchema = z.array(statusSchema); " `; diff --git a/tests/case.spec.ts b/tests/case.spec.ts new file mode 100644 index 0000000..58dd045 --- /dev/null +++ b/tests/case.spec.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest'; +import { toPascalCase, toCamelCase } from '../src/utils/case'; + +describe('case utils', () => { + it('converts to PascalCase', () => { + expect(toPascalCase('my_schema-name')).toBe('MySchemaName'); + }); + it('converts to camelCase', () => { + expect(toCamelCase('my_schema-name')).toBe('mySchemaName'); + }); +}); diff --git a/tests/generate-zod.spec.ts b/tests/generate-zod.spec.ts index 7734770..aaaf7bc 100644 --- a/tests/generate-zod.spec.ts +++ b/tests/generate-zod.spec.ts @@ -21,7 +21,7 @@ describe('generate-zod', () => { const schemas = extractSchemas(doc, null); const { zodString } = convertSchema(schemas.User as OpenAPI.SchemaObject); const content = schemaTemplate({ - schemas: [{ schemaName: 'User', zodString }] + schemas: [{ schemaName: 'userSchema', zodString }] }); const result = ts.transpileModule(content, { compilerOptions: { module: ts.ModuleKind.ESNext } }); expect(result.diagnostics?.length).toBe(0); @@ -43,7 +43,7 @@ describe('generate-zod', () => { const schemas = extractSchemas(doc, null); const { zodString } = convertSchema(schemas.Wrapper as OpenAPI.SchemaObject); const content = schemaTemplate({ - schemas: [{ schemaName: 'Wrapper', zodString }] + schemas: [{ schemaName: 'wrapperSchema', zodString }] }); const result = ts.transpileModule(content, { compilerOptions: { module: ts.ModuleKind.ESNext } }); expect(result.diagnostics?.length).toBe(0); diff --git a/tests/json-schema-to-zod.spec.ts b/tests/json-schema-to-zod.spec.ts index 6b07162..450af88 100644 --- a/tests/json-schema-to-zod.spec.ts +++ b/tests/json-schema-to-zod.spec.ts @@ -17,8 +17,8 @@ describe('convertSchema', () => { type: 'array', items: { $ref: '#/components/schemas/User' } } as OpenAPI.SchemaObject); - expect(zodString).toBe('z.array(User)'); - expect(imports.has('User')).toBe(true); + expect(zodString).toBe('z.array(userSchema)'); + expect(imports.has('userSchema')).toBe(true); }); it('converts objects with optional fields', () => { @@ -49,8 +49,8 @@ describe('convertSchema', () => { allOf: [{ $ref: '#/components/schemas/Base' }, { type: 'object', properties: { extra: { type: 'string' } }, required: ['extra'] }] } as OpenAPI.SchemaObject; const { zodString, imports } = convertSchema(schema); - expect(zodString).toBe('z.intersection(Base, z.object({\n extra: z.string()\n}))'); - expect(imports.has('Base')).toBe(true); + expect(zodString).toBe('z.intersection(baseSchema, z.object({\n extra: z.string()\n}))'); + expect(imports.has('baseSchema')).toBe(true); }); it('handles oneOf with refs', () => { @@ -58,9 +58,9 @@ describe('convertSchema', () => { oneOf: [{ $ref: '#/components/schemas/A' }, { $ref: '#/components/schemas/B' }] } as OpenAPI.SchemaObject; const { zodString, imports } = convertSchema(schema); - expect(zodString).toBe('z.union([A, B])'); - expect(imports.has('A')).toBe(true); - expect(imports.has('B')).toBe(true); + expect(zodString).toBe('z.union([aSchema, bSchema])'); + expect(imports.has('aSchema')).toBe(true); + expect(imports.has('bSchema')).toBe(true); }); it('handles oneOf with a single ref', () => { @@ -68,8 +68,8 @@ describe('convertSchema', () => { oneOf: [{ $ref: '#/components/schemas/A' }] } as OpenAPI.SchemaObject; const { zodString, imports } = convertSchema(schema); - expect(zodString).toBe('A'); - expect(imports.has('A')).toBe(true); + expect(zodString).toBe('aSchema'); + expect(imports.has('aSchema')).toBe(true); }); it('converts inline object properties', () => {