diff --git a/docs/content/docs/reference/configuration/output.mdx b/docs/content/docs/reference/configuration/output.mdx index 358de15cf7..a6fe7211e7 100644 --- a/docs/content/docs/reference/configuration/output.mdx +++ b/docs/content/docs/reference/configuration/output.mdx @@ -1697,6 +1697,64 @@ export interface Pet { } ``` +## factoryMethods + +**Type:** `Object` +**Default:** `{ generate: false }` + +Generate factory methods for DTOs (Data Transfer Objects) initialized with safe default values. Useful for testing and initializing empty state. +Functionality handles OpenAPI `readOnly` and `writeOnly` flags to generate appropriate payload structures: +- **Required properties:** Always included in the factory output, regardless of their visibility flags. +- **Optional `readOnly` properties:** Always omitted from the factory output, as they would be dropped by the server. +- **Optional `writeOnly` properties:** Always included in the factory output (even if `includeOptionalProperty` is set to `false`). + +```ts title="orval.config.ts" +export default defineConfig({ + petstore: { + output: { + factoryMethods: { + functionNamePrefix: 'create', + mode: 'split', + includeOptionalProperty: true, + outputDirectory: `#output.workspace.schemas`, + }, + }, + }, +}); +``` + +### functionNamePrefix + +**Type:** `String` +**Default:** `'create'` + +Prefix for the generated factory function names. + +### mode + +**Type:** `'single' | 'split' | 'single-split'` +**Default:** `'split'` + +Where to generate the factory methods: +- `single`: Appends the factory function to the schema file. +- `split`: Creates a `{schema}.factory.ts` with factory method. By default it is placed next to schema file. +- `single-split`: Aggregates all factory methods into a single `factoryMethods.ts` file. + +### includeOptionalProperty + +**Type:** `boolean` +**Default:** `true` + +Determines whether optional schema properties are included in the default factory output. + +### outputDirectory + +**Type:** `String` +**Default:** `#output.workspace.schemas` + +Defaults to the value configured in `#output.workspace.schemas`. +Determines where factory methods will be generated (can be used to generated methods away from schema directory). +Takes effect only when used `mode` is `split` or `single-split`. --- ## Other Options diff --git a/packages/angular/src/http-client.test.ts b/packages/angular/src/http-client.test.ts index e00689043a..e444694e14 100644 --- a/packages/angular/src/http-client.test.ts +++ b/packages/angular/src/http-client.test.ts @@ -125,6 +125,12 @@ const createOutput = ( optionsParamRequired: false, unionAddMissingProperties: false, propertySortOrder: 'Specification', + factoryMethods: { + functionNamePrefix: 'create', + mode: 'single', + outputDirectory: '', + includeOptionalProperty: false, + }, ...overrides, } satisfies NormalizedOutputOptions; diff --git a/packages/angular/src/http-resource.test.ts b/packages/angular/src/http-resource.test.ts index e30cd85be8..71922f07ba 100644 --- a/packages/angular/src/http-resource.test.ts +++ b/packages/angular/src/http-resource.test.ts @@ -135,6 +135,12 @@ const createOutput = ( optionsParamRequired: false, unionAddMissingProperties: false, propertySortOrder: 'Specification', + factoryMethods: { + functionNamePrefix: 'create', + mode: 'single', + outputDirectory: '', + includeOptionalProperty: false, + }, ...overrides, } satisfies NormalizedOutputOptions; diff --git a/packages/core/src/generators/factory.test.ts b/packages/core/src/generators/factory.test.ts new file mode 100644 index 0000000000..909de49131 --- /dev/null +++ b/packages/core/src/generators/factory.test.ts @@ -0,0 +1,587 @@ +import { describe, expect, it } from 'vitest'; + +import type { + ContextSpec, + OpenApiDocument, + OpenApiSchemaObject, +} from '../types'; +import { NamingConvention } from '../types'; +import { generateFactory } from './factory'; + +const baseFactoryMethods = { + functionNamePrefix: 'create', + mode: 'single', + outputDirectory: '', + includeOptionalProperty: false, +}; + +const baseOverride = { + useDates: false, + namingConvention: {}, + components: { + schemas: { suffix: '', itemSuffix: '' }, + responses: { suffix: '' }, + parameters: { suffix: '' }, + requestBodies: { suffix: '' }, + }, +}; + +const createMockContext = ( + overrides: Record = {}, +): ContextSpec => { + const { override: overrideOverride, ...rest } = overrides; + return { + target: 'test', + workspace: 'test', + spec: { + components: { + schemas: { + RefTarget: { + type: 'object', + properties: { id: { type: 'string' } }, + }, + CircularChild: { + type: 'object', + properties: { + parent: { $ref: '#/components/schemas/CircularParent' }, + }, + }, + CircularParent: { + type: 'object', + properties: { + child: { $ref: '#/components/schemas/CircularChild' }, + }, + }, + DeepCircularA: { + type: 'object', + properties: { b: { $ref: '#/components/schemas/DeepCircularB' } }, + }, + DeepCircularB: { + type: 'object', + properties: { c: { $ref: '#/components/schemas/DeepCircularC' } }, + }, + DeepCircularC: { + type: 'object', + properties: { a: { $ref: '#/components/schemas/DeepCircularA' } }, + }, + }, + }, + } as unknown as OpenApiDocument, + output: { + target: '', + namingConvention: NamingConvention.CAMEL_CASE, + fileExtension: '.ts', + mode: 'single' as unknown, + client: 'axios' as unknown, + httpClient: 'axios' as unknown, + clean: false, + docs: false, + prettier: false, + biome: false, + headers: false, + indexFiles: false, + allParamsOptional: false, + urlEncodeParameters: false, + unionAddMissingProperties: false, + optionsParamRequired: false, + propertySortOrder: 'Alphabetical' as unknown, + factoryMethods: baseFactoryMethods, + override: { + ...baseOverride, + ...(overrideOverride as object | undefined), + }, + ...rest, + } as unknown, + } as unknown as ContextSpec; +}; + +describe('generateFactory', () => { + it('returns undefined if schema is not an object/combination', () => { + const schema: OpenApiSchemaObject = { type: 'string' }; + expect( + generateFactory(schema, 'StringSchema', createMockContext()), + ).toBeUndefined(); + }); + + it('generates factory for basic object with omitted optionals', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['id', 'name'], + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + isActive: { type: 'boolean' }, + tags: { type: 'array', items: { type: 'string' } }, + }, + }; + + const result = generateFactory(schema, 'User', createMockContext()); + expect(result).toBeDefined(); + expect(result?.model).toContain('export function createUser(): User {'); + expect(result?.model).toContain('id: 0'); + expect(result?.model).toContain("name: ''"); + expect(result?.model).not.toContain('isActive'); + expect(result?.model).not.toContain('tags'); + }); + + it('includes optional properties when strategy is include', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + properties: { + isActive: { type: 'boolean' }, + tags: { type: 'array', items: { type: 'string' } }, + score: { type: 'number' }, + }, + }; + + const result = generateFactory( + schema, + 'User', + createMockContext({ + factoryMethods: { + ...baseFactoryMethods, + includeOptionalProperty: true, + }, + }), + ); + expect(result?.model).toContain('isActive: false'); + expect(result?.model).toContain('tags: []'); + expect(result?.model).toContain('score: 0'); + }); + + it('omits optional readOnly properties but forcefully keeps optional writeOnly properties', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['reqReadOnly'], + properties: { + normalOpt: { type: 'string' }, + readOnlyOpt: { type: 'string', readOnly: true }, + writeOnlyOpt: { type: 'string', writeOnly: true }, + reqReadOnly: { type: 'string', readOnly: true }, + }, + }; + + const result = generateFactory(schema, 'Obj', createMockContext()); + expect(result?.model).not.toContain("normalOpt: ''"); + expect(result?.model).not.toContain("readOnlyOpt: ''"); + expect(result?.model).toContain("writeOnlyOpt: ''"); + expect(result?.model).toContain("reqReadOnly: ''"); + }); + + it('handles nested objects', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['nested'], + properties: { + nested: { + type: 'object', + required: ['val'], + properties: { val: { type: 'number' } }, + }, + }, + }; + + const result = generateFactory(schema, 'NestedObj', createMockContext()); + expect(result?.model).toContain('nested: {\n val: 0\n }'); + }); + + it('handles $ref and imports correctly', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['target'], + properties: { + target: { $ref: '#/components/schemas/RefTarget' }, + }, + }; + + const result = generateFactory(schema, 'WithRef', createMockContext()); + expect(result?.model).toContain('target: createRefTarget()'); + expect(result?.imports).toContainEqual({ + name: 'createRefTarget', + importPath: './refTarget', + isConstant: true, + }); + expect(result?.imports).toContainEqual({ name: 'RefTarget' }); + }); + + it('handles mode: split import paths', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['target'], + properties: { + target: { $ref: '#/components/schemas/RefTarget' }, + }, + }; + + const result = generateFactory( + schema, + 'WithRef', + createMockContext({ + factoryMethods: { ...baseFactoryMethods, mode: 'split' }, + }), + ); + expect(result?.imports).toContainEqual({ + name: 'createRefTarget', + importPath: './refTarget.factory', + isConstant: true, + }); + }); + + it('handles mode: single-split import paths', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['target'], + properties: { + target: { $ref: '#/components/schemas/RefTarget' }, + }, + }; + + const result = generateFactory( + schema, + 'WithRefCombined', + createMockContext({ + factoryMethods: { + ...baseFactoryMethods, + mode: 'single-split', + }, + }), + ); + expect(result?.imports).not.toContainEqual({ + name: 'createRefTarget', + importPath: './factoryMethods', + isConstant: true, + }); + }); + + it('breaks cycles automatically (direct parent child)', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['child'], + properties: { + child: { $ref: '#/components/schemas/CircularChild' }, + }, + }; + + const result = generateFactory( + schema, + 'CircularParent', + createMockContext(), + ); + expect(result?.model).toContain('child: {} as CircularChild'); + }); + + it('breaks deep cycles automatically', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['b'], + properties: { + b: { $ref: '#/components/schemas/DeepCircularB' }, + }, + }; + + const result = generateFactory( + schema, + 'DeepCircularA', + createMockContext(), + ); + expect(result?.model).toContain('b: {} as DeepCircularB'); + }); + + it('uses string literal for date/date-time format when useDates is false', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['createdAt'], + properties: { + createdAt: { type: 'string', format: 'date-time' }, + }, + }; + + const result = generateFactory(schema, 'Dates', createMockContext()); + expect(result?.model).toContain("createdAt: '"); + }); + + it('uses Date object for date/date-time format when useDates is true', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['createdAt'], + properties: { + createdAt: { type: 'string', format: 'date-time' }, + }, + }; + + const result = generateFactory( + schema, + 'Dates', + createMockContext({ override: { useDates: true } }), + ); + expect(result?.model).toContain('createdAt: new Date(0)'); + }); + + it('handles date/date-time default values with useDates = true', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['dateField'], + properties: { + dateField: { type: 'string', format: 'date', default: '2026-01-01' }, + }, + }; + + const result = generateFactory( + schema, + 'DateDefaultObj', + createMockContext({ override: { useDates: true } }), + ); + expect(result?.model).toContain("dateField: new Date('2026-01-01')"); + }); + + it('handles enums with strings and numbers', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['status', 'count'], + properties: { + status: { type: 'string', enum: ['ACTIVE', 'INACTIVE'] }, + count: { type: 'number', enum: [100, 200] }, + }, + }; + + const result = generateFactory(schema, 'WithEnum', createMockContext()); + expect(result?.model).toContain('status: "ACTIVE"'); + expect(result?.model).toContain('count: 100'); + }); + + it('handles allOf appropriately', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + allOf: [ + { + type: 'object', + required: ['a'], + properties: { a: { type: 'string' } }, + }, + { + type: 'object', + required: ['b'], + properties: { b: { type: 'number' } }, + }, + ], + }; + + const result = generateFactory(schema, 'Combined', createMockContext()); + expect(result?.model).toContain( + "Object.assign({}, {\n a: ''\n }, {\n b: 0\n })", + ); + }); + + it('sorts properties alphabetically when configured', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['z', 'a', 'm'], + properties: { + z: { type: 'number' }, + a: { type: 'string' }, + m: { type: 'boolean' }, + }, + }; + + const result = generateFactory( + schema, + 'SortedObj', + createMockContext({ propertySortOrder: 'Alphabetical' }), + ); + expect(result?.model).toMatch(/a: '',[\s\S]*m: false,[\s\S]*z: 0/); + }); + + it('handles const, default, null, array type arrays, and array constraints', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: [ + 'withConst', + 'withDefault', + 'withDefaultObj', + 'nullField', + 'multiType', + 'arrayConstraints', + 'tupleArray', + ], + properties: { + withConst: { type: 'string', const: 'STATIC_VALUE' }, + withDefault: { type: 'number', default: 42 }, + withDefaultObj: { type: 'object', default: { key: 'val' } }, + nullField: { type: 'null' as unknown as string }, + multiType: { type: ['string', 'null'] as unknown as string[] }, + arrayConstraints: { + type: 'array', + minItems: 2, + items: { type: 'string' }, + }, + tupleArray: { + type: 'array', + prefixItems: [{ type: 'number' }, { type: 'boolean' }], + }, + }, + }; + + const result = generateFactory(schema, 'AdvancedObj', createMockContext()); + expect(result?.model).toContain('withConst: "STATIC_VALUE"'); + expect(result?.model).toContain('withDefault: 42'); + expect(result?.model).toContain('withDefaultObj: {"key":"val"}'); + expect(result?.model).toContain('nullField: null'); + expect(result?.model).toContain("multiType: ''"); + expect(result?.model).toContain("arrayConstraints: ['', '']"); + expect(result?.model).toContain('tupleArray: [0, false]'); + }); + + it('handles anyOf and oneOf by picking the first option', () => { + const schemaOneOf: OpenApiSchemaObject = { + type: 'object', + oneOf: [ + { + type: 'object', + required: ['a'], + properties: { a: { type: 'string' } }, + }, + { + type: 'object', + required: ['b'], + properties: { b: { type: 'number' } }, + }, + ], + }; + + const resultOneOf = generateFactory( + schemaOneOf, + 'Combined', + createMockContext(), + ); + expect(resultOneOf?.model).toContain("{\n a: ''\n }"); + + const schemaAnyOf: OpenApiSchemaObject = { + type: 'object', + anyOf: [ + { + type: 'object', + required: ['x'], + properties: { x: { type: 'boolean' } }, + }, + { + type: 'object', + required: ['y'], + properties: { y: { type: 'string' } }, + }, + ], + }; + + const resultAnyOf = generateFactory( + schemaAnyOf, + 'CombinedAny', + createMockContext(), + ); + expect(resultAnyOf?.model).toContain('{\n x: false\n }'); + }); + + it('handles root array schemas', () => { + const schema: OpenApiSchemaObject = { + type: 'array', + items: { type: 'string' }, + }; + + const result = generateFactory(schema, 'StringArray', createMockContext()); + expect(result?.model).toContain( + 'export function createStringArray(): StringArray {', + ); + expect(result?.model).toContain('return []'); + }); + + it('handles root enum schemas', () => { + const schema: OpenApiSchemaObject = { + type: 'string', + enum: ['FIRST', 'SECOND'], + }; + + const result = generateFactory(schema, 'StringEnum', createMockContext()); + expect(result?.model).toContain('return "FIRST"'); + }); + + it('infers implicit types for arrays and enums', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: [ + 'implicitArray', + 'implicitEnum', + 'implicitNumEnum', + 'implicitBoolEnum', + ], + properties: { + implicitArray: { items: { type: 'number' } }, + implicitEnum: { enum: ['A', 'B'] }, + implicitNumEnum: { enum: [1, 2] }, + implicitBoolEnum: { enum: [true, false] }, + }, + }; + + const result = generateFactory(schema, 'ImplicitObj', createMockContext()); + expect(result?.model).toContain('implicitArray: []'); + expect(result?.model).toContain('implicitEnum: "A"'); + expect(result?.model).toContain('implicitNumEnum: 1'); + expect(result?.model).toContain('implicitBoolEnum: true'); + }); + + it('returns undefined as unknown for unsupported types', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + required: ['weirdField'], + properties: { + weirdField: { type: 'weird' as unknown }, + }, + }; + + const result = generateFactory(schema, 'WeirdObj', createMockContext()); + expect(result?.model).toContain('weirdField: undefined as unknown'); + }); + + it('escapes quotes in string enums', () => { + const schema: OpenApiSchemaObject = { + type: 'string', + enum: ["O'Reilly"], + }; + + const result = generateFactory(schema, 'EscapedEnum', createMockContext()); + expect(result?.model).toContain(`return "O'Reilly"`); + }); + + it('escapes backslashes and quotes properly', () => { + const schema: OpenApiSchemaObject = { + type: 'string', + enum: [String.raw`C:\temp\folder\file's`], + }; + + const result = generateFactory( + schema, + 'EscapedBackslashEnum', + createMockContext(), + ); + expect(result?.model).toContain( + String.raw`return "C:\\temp\\folder\\file's"`, + ); + }); + + it('omits optional $ref properties when readOnly is set on the reference itself', () => { + const schema: OpenApiSchemaObject = { + type: 'object', + properties: { + user: { + $ref: '#/components/schemas/RefTarget', + readOnly: true, + } as unknown as OpenApiSchemaObject, + token: { + $ref: '#/components/schemas/RefTarget', + writeOnly: true, + } as unknown as OpenApiSchemaObject, + }, + }; + + const result = generateFactory(schema, 'AuthPayload', createMockContext()); + expect(result?.model).not.toContain('user: createRefTarget()'); + expect(result?.model).toContain('token: createRefTarget()'); + }); +}); diff --git a/packages/core/src/generators/factory.ts b/packages/core/src/generators/factory.ts new file mode 100644 index 0000000000..8b1f329cd8 --- /dev/null +++ b/packages/core/src/generators/factory.ts @@ -0,0 +1,502 @@ +import { resolveRef } from '../resolvers/ref'; +import type { + ContextSpec, + GeneratorImport, + OpenApiReferenceObject, + OpenApiSchemaObject, +} from '../types'; +import { PropertySortOrder } from '../types'; +import { + conventionName, + getFileInfo, + isString, + logWarning, + pascal, + upath, +} from '../utils'; + +const circularRefCache = new WeakMap>(); + +function getSchemasPath(context: ContextSpec): string { + const { schemas, target } = context.output; + if (schemas) { + const schemasPath = isString(schemas) ? schemas : schemas.path; + return upath.normalizeSafe(schemasPath); + } + const { dirname, filename } = getFileInfo(target); + return upath.joinSafe(dirname, filename + '.schemas'); +} + +function getSchemaImportPath( + refName: string, + context: ContextSpec, +): string | undefined { + if (context.output.factoryMethods?.mode === 'single') { + return undefined; + } + let outputDir = context.output.factoryMethods?.outputDirectory; + let schemasPath = getSchemasPath(context); + + if (context.output.workspace) { + if (outputDir && !upath.isAbsolute(outputDir)) { + outputDir = upath.resolve(context.output.workspace, outputDir); + } + if (schemasPath && !upath.isAbsolute(schemasPath)) { + schemasPath = upath.resolve(context.output.workspace, schemasPath); + } + } + + const relativePath = outputDir + ? upath.relativeSafe(outputDir, schemasPath) + : './'; + const baseName = conventionName(refName, context.output.namingConvention); + return upath.joinSafe(relativePath, baseName); +} + +type SchemaArray = (OpenApiSchemaObject | OpenApiReferenceObject)[]; + +interface ResolvedRef { + imports: GeneratorImport[]; + schema: OpenApiSchemaObject; +} + +function isReference( + schema: OpenApiSchemaObject | OpenApiReferenceObject, +): schema is OpenApiReferenceObject { + return '$ref' in schema; +} + +function getResolvedRef( + schema: OpenApiReferenceObject, + context: ContextSpec, +): ResolvedRef { + return resolveRef(schema, context) as ResolvedRef; +} + +function getProperties( + schema: OpenApiSchemaObject, +): Record { + return ( + (schema.properties as + | Record + | undefined) ?? {} + ); +} + +function getItems( + schema: OpenApiSchemaObject, +): OpenApiSchemaObject | OpenApiReferenceObject | undefined { + return schema.items as + | OpenApiSchemaObject + | OpenApiReferenceObject + | undefined; +} + +function getAdditionalProperties( + schema: OpenApiSchemaObject, +): OpenApiSchemaObject | OpenApiReferenceObject | boolean | undefined { + return schema.additionalProperties as + | OpenApiSchemaObject + | OpenApiReferenceObject + | boolean + | undefined; +} + +function getSchemas(schemas: unknown): SchemaArray | undefined { + return schemas as SchemaArray | undefined; +} + +function getExtendedProps(schema: OpenApiSchemaObject): { + constValue: unknown; + prefixItems: SchemaArray | undefined; + minItems: number | undefined; +} { + const extended = schema as OpenApiSchemaObject & { + const?: unknown; + prefixItems?: SchemaArray; + minItems?: number; + }; + return { + constValue: extended.const, + prefixItems: extended.prefixItems, + minItems: extended.minItems, + }; +} + +export function generateFactory( + schema: OpenApiSchemaObject, + name: string, + context: ContextSpec, +): { model: string; imports: GeneratorImport[] } | undefined { + if (!canGenerateSchema(schema) || !context.output.factoryMethods) + return undefined; + + const { functionNamePrefix, mode } = context.output.factoryMethods; + const factoryName = `${functionNamePrefix}${pascal(name)}`; + const imports: GeneratorImport[] = []; + const payload = buildPayload(schema, context, [name], imports); + + if (mode !== 'single') { + const schemaImportPath = getSchemaImportPath(name, context); + imports.push({ name, importPath: schemaImportPath }); + } + + return { + model: `export function ${factoryName}(): ${name} {\n return ${payload};\n}\n`, + imports, + }; +} + +function canGenerateSchema(schema: OpenApiSchemaObject): boolean { + return ( + schema.type === 'object' || + schema.type === 'array' || + !!schema.properties || + !!schema.allOf || + !!schema.oneOf || + !!schema.anyOf || + !!schema.items || + !!schema.enum + ); +} + +function hasCircularReference( + target: OpenApiSchemaObject | OpenApiReferenceObject, + sourceName: string, + context: ContextSpec, + visited = new Set(), +): boolean { + if (isReference(target)) { + const { imports, schema } = getResolvedRef(target, context); + const refName = imports[0]?.name; + if (refName === sourceName) return true; + if (refName && visited.has(refName)) return false; + if (refName) visited.add(refName); + + let cache = circularRefCache.get(context); + if (!cache) { + cache = new Map(); + circularRefCache.set(context, cache); + } + const cacheKey = refName ? `${sourceName}::${refName}` : undefined; + if (cacheKey) { + const cached = cache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + } + + const result = hasCircularReference(schema, sourceName, context, visited); + + if (cacheKey) { + cache.set(cacheKey, result); + } + return result; + } + + const check = (schemas?: SchemaArray): boolean => + schemas?.some((s) => + hasCircularReference(s, sourceName, context, visited), + ) ?? false; + + const items = getItems(target); + const additionalProperties = getAdditionalProperties(target); + + return ( + check(getSchemas(target.allOf)) || + check(getSchemas(target.oneOf)) || + check(getSchemas(target.anyOf)) || + Object.values(getProperties(target)).some((s) => + hasCircularReference(s, sourceName, context, visited), + ) || + (!!items && hasCircularReference(items, sourceName, context, visited)) || + (typeof additionalProperties === 'object' && + hasCircularReference(additionalProperties, sourceName, context, visited)) + ); +} + +function buildPayload( + target: OpenApiSchemaObject | OpenApiReferenceObject, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + if (isReference(target)) { + return buildRefPayload(target, context, parents, imports); + } + + const schema = target; + + if (schema.allOf) + return buildAllOfPayload( + getSchemas(schema.allOf) ?? [], + context, + parents, + imports, + ); + if (schema.oneOf) + return buildFirstOfPayload( + getSchemas(schema.oneOf) ?? [], + context, + parents, + imports, + ); + if (schema.anyOf) + return buildFirstOfPayload( + getSchemas(schema.anyOf) ?? [], + context, + parents, + imports, + ); + + const { constValue } = getExtendedProps(schema); + if (constValue !== undefined) return formatValue(constValue); + if (schema.default !== undefined) return buildDefaultPayload(schema, context); + + const schemaType = inferSchemaType(schema); + + if (schemaType === 'object' || schema.properties) + return buildObjectPayload(schema, context, parents, imports); + if (schemaType === 'array') + return buildArrayPayload(schema, context, parents, imports); + + return buildPrimitivePayload(schema, schemaType, context); +} + +function buildRefPayload( + schema: OpenApiReferenceObject, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + const { schema: resolved, imports: refImports } = getResolvedRef( + schema, + context, + ); + const refName = refImports[0]?.name; + + if (!refName) return '{}'; + + if ( + parents.includes(refName) || + hasCircularReference(resolved, parents[0], context) + ) { + imports.push({ + name: refName, + importPath: getSchemaImportPath(refName, context), + }); + return `{} as ${refName}`; + } + + const { functionNamePrefix = 'create', mode = 'single' } = + context.output.factoryMethods ?? {}; + const refFactoryName = `${functionNamePrefix}${pascal(refName)}`; + + if (mode !== 'single-split') { + const importPath = resolveImportPath(mode, refName, context); + imports.push({ name: refFactoryName, importPath, isConstant: true }); + } + + imports.push({ + name: refName, + importPath: getSchemaImportPath(refName, context), + }); + + return `${refFactoryName}()`; +} + +function resolveImportPath( + mode: string, + refName: string, + context: ContextSpec, +): string | undefined { + const baseName = conventionName(refName, context.output.namingConvention); + switch (mode) { + case 'split': { + return `./${baseName}.factory`; + } + case 'single-split': { + return `./${conventionName('factoryMethods', context.output.namingConvention)}`; + } + case 'single': { + return `./${baseName}`; + } + } +} + +function buildAllOfPayload( + allOf: SchemaArray, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + const payloads = allOf.map((s) => buildPayload(s, context, parents, imports)); + return payloads.length > 0 + ? `Object.assign({}, ${payloads.join(', ')})` + : '{}'; +} + +function buildFirstOfPayload( + schemas: SchemaArray, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + const first = schemas[0]; + return first ? buildPayload(first, context, parents, imports) : '{}'; +} + +function buildObjectPayload( + schema: OpenApiSchemaObject, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + const { includeOptionalProperty = false } = + context.output.factoryMethods ?? {}; + const props = getProperties(schema); + const requiredProps: string[] = + (schema.required as string[] | undefined) ?? []; + const entries = Object.entries(props); + + if (context.output.propertySortOrder === PropertySortOrder.ALPHABETICAL) { + entries.sort(([a], [b]) => a.localeCompare(b)); + } + + const includeOptional = includeOptionalProperty; + const lines: string[] = []; + + for (const [key, prop] of entries) { + const isRequired = requiredProps.includes(key); + const resolved = isReference(prop) + ? getResolvedRef(prop, context).schema + : prop; + + const isReadOnly = + !!(prop as OpenApiSchemaObject).readOnly || !!resolved.readOnly; + const isWriteOnly = + !!(prop as OpenApiSchemaObject).writeOnly || !!resolved.writeOnly; + + if (!isRequired) { + if (isReadOnly) continue; + if (!isWriteOnly && !includeOptional) continue; + } + + const payload = buildPayload(prop, context, parents, imports); + const safeKey = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) + ? key + : JSON.stringify(key); + lines.push(`${safeKey}: ${payload}`); + } + + return `{\n ${lines.join(',\n ')}\n }`; +} + +function buildArrayPayload( + schema: OpenApiSchemaObject, + context: ContextSpec, + parents: string[], + imports: GeneratorImport[], +): string { + const { prefixItems, minItems } = getExtendedProps(schema); + const items = getItems(schema); + + if (prefixItems && prefixItems.length > 0) { + const payloads = prefixItems.map((item) => + buildPayload(item, context, parents, imports), + ); + return `[${payloads.join(', ')}]`; + } + + if (minItems && items) { + const MAX_MIN_ITEMS = 50; + if (minItems > MAX_MIN_ITEMS) { + logWarning( + `Warning: minItems is ${minItems}, capping at ${MAX_MIN_ITEMS} to prevent massive payload.`, + ); + } + const count = Math.min(minItems, MAX_MIN_ITEMS); + const itemPayload = buildPayload(items, context, parents, imports); + return `[${Array.from({ length: count }) + .fill(itemPayload) + .join(', ')}]`; + } + + return '[]'; +} + +function inferSchemaType(schema: OpenApiSchemaObject): string | undefined { + let type = schema.type as string | string[] | undefined; + + if (Array.isArray(type)) { + const nonNull = type.filter((t) => t !== 'null'); + type = nonNull.length > 0 ? nonNull[0] : 'null'; + } + + if (!type && schema.items) return 'array'; + + if (!type && schema.enum) { + const first = (schema.enum as unknown[])[0]; + if (typeof first === 'number') return 'number'; + if (typeof first === 'boolean') return 'boolean'; + return 'string'; + } + + return type; +} + +function buildDefaultPayload( + schema: OpenApiSchemaObject, + context: ContextSpec, +): string { + if ( + context.output.override.useDates && + typeof schema.default === 'string' && + (schema.format === 'date' || schema.format === 'date-time') + ) { + return `new Date('${schema.default}')`; + } + return formatValue(schema.default); +} + +function buildPrimitivePayload( + schema: OpenApiSchemaObject, + schemaType: string | undefined, + context: ContextSpec, +): string { + if (schemaType === 'null') return 'null'; + + const enumValues = schema.enum as unknown[] | undefined; + + if (schemaType === 'boolean') { + return enumValues && enumValues.length > 0 + ? String(enumValues[0]) + : 'false'; + } + + if (schemaType === 'number' || schemaType === 'integer') { + return enumValues && enumValues.length > 0 ? String(enumValues[0]) : '0'; + } + + if (schemaType === 'string') { + if (enumValues && enumValues.length > 0) { + const first = enumValues[0]; + return typeof first === 'string' ? JSON.stringify(first) : String(first); + } + if (schema.format === 'date' || schema.format === 'date-time') { + return context.output.override.useDates + ? 'new Date(0)' + : `'${new Date(0).toISOString()}'`; + } + return "''"; + } + + return 'undefined as unknown'; +} + +function formatValue(val: unknown): string { + if (val === null) return 'null'; + if (typeof val === 'string') return JSON.stringify(val); + if (typeof val === 'object') return JSON.stringify(val); + return String(val as number | boolean); +} diff --git a/packages/core/src/generators/index.ts b/packages/core/src/generators/index.ts index 9cc5a65c38..c50a623b54 100644 --- a/packages/core/src/generators/index.ts +++ b/packages/core/src/generators/index.ts @@ -1,4 +1,5 @@ export * from './component-definition'; +export * from './factory'; export * from './imports'; export * from './input-filters'; export * from './models-inline'; diff --git a/packages/core/src/generators/schema-definition.test.ts b/packages/core/src/generators/schema-definition.test.ts index 7f3af5931f..6f55448d78 100644 --- a/packages/core/src/generators/schema-definition.test.ts +++ b/packages/core/src/generators/schema-definition.test.ts @@ -11,6 +11,7 @@ describe('generateSchemasDefinition', () => { const context = { output: { override: { namingConvention: {} }, + factoryMethods: undefined, }, target: 'typescript', spec: {}, @@ -138,6 +139,7 @@ describe('generateSchemasDefinition', () => { enum: 'PascalCase', }, }, + factoryMethods: undefined, }, target: 'typescript', spec: {}, @@ -235,6 +237,7 @@ describe('generateSchemasDefinition', () => { const specContext = { ...context, output: { + ...context.output, override: { enumGenerationType: 'const', namingConvention: {} }, }, spec: { @@ -280,6 +283,7 @@ describe('generateSchemasDefinition', () => { const specContext = { ...context, output: { + ...context.output, override: { enumGenerationType: 'const', namingConvention: {} }, }, spec: { @@ -337,6 +341,7 @@ describe('generateSchemasDefinition', () => { const specContext = { ...context, output: { + ...context.output, override: { enumGenerationType: 'const', namingConvention: {} }, }, spec: { diff --git a/packages/core/src/generators/schema-definition.ts b/packages/core/src/generators/schema-definition.ts index b971b60beb..3963b438d9 100644 --- a/packages/core/src/generators/schema-definition.ts +++ b/packages/core/src/generators/schema-definition.ts @@ -23,6 +23,7 @@ import { pascal, sanitize, } from '../utils'; +import { generateFactory } from './factory'; import { generateInterface } from './interface'; /** @@ -69,6 +70,31 @@ export function generateSchemasDefinition( ); if (!seenNames.has(normalizedName)) { seenNames.add(normalizedName); + + if (context.output.factoryMethods && schema.schema) { + const factoryData = generateFactory( + schema.schema, + schema.name, + context, + ); + if (factoryData) { + if (context.output.factoryMethods.mode === 'single') { + schema.model += `\n${factoryData.model}`; + for (const imp of factoryData.imports) { + if ( + !schema.imports.some((existing) => existing.name === imp.name) + ) { + schema.imports.push(imp); + } + } + } else { + schema.factory = factoryData.model; + schema.factoryImports = factoryData.imports; + schema.factoryMode = context.output.factoryMethods.mode; + } + } + } + deduplicatedModels.push(schema); } } diff --git a/packages/core/src/test-utils/context.ts b/packages/core/src/test-utils/context.ts index 136ca9912d..f447506737 100644 --- a/packages/core/src/test-utils/context.ts +++ b/packages/core/src/test-utils/context.ts @@ -42,6 +42,7 @@ export function createTestContextSpec({ unionAddMissingProperties: false, optionsParamRequired: false, propertySortOrder: PropertySortOrder.SPECIFICATION, + factoryMethods: undefined, override: { title: undefined, transformer: undefined, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 886b84890f..9362bd40bf 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -60,6 +60,7 @@ export interface NormalizedOutputOptions { unionAddMissingProperties: boolean; optionsParamRequired: boolean; propertySortOrder: PropertySortOrder; + factoryMethods?: NormalizedFactoryMethodsOptions; } export interface NormalizedParamsSerializerOptions { @@ -279,6 +280,22 @@ export type EnumGeneration = export type SchemaGenerationType = 'typescript' | 'zod'; +export type FactoryMethodsMode = 'single' | 'split' | 'single-split'; + +export interface FactoryMethodsOptions { + functionNamePrefix?: string; + mode?: FactoryMethodsMode; + outputDirectory?: string; + includeOptionalProperty?: boolean; +} + +export interface NormalizedFactoryMethodsOptions { + functionNamePrefix: string; + mode: FactoryMethodsMode; + outputDirectory: string; + includeOptionalProperty: boolean; +} + export interface SchemaOptions { path: string; type: SchemaGenerationType; @@ -323,6 +340,7 @@ export interface OutputOptions { unionAddMissingProperties?: boolean; optionsParamRequired?: boolean; propertySortOrder?: PropertySortOrder; + factoryMethods?: FactoryMethodsOptions; } export interface InputFiltersOptions { @@ -1129,6 +1147,9 @@ export interface GeneratorSchema { imports: GeneratorImport[]; dependencies?: string[]; schema?: OpenApiSchemaObject; + factory?: string; + factoryImports?: GeneratorImport[]; + factoryMode?: FactoryMethodsMode; } export interface GeneratorImport { diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts index c5f894aa84..5902fa96ed 100644 --- a/packages/core/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -2,6 +2,14 @@ import basepath from 'node:path'; import { getExtension } from './extension'; +export function isAbsolute(value: string): boolean { + return basepath.isAbsolute(value); +} + +export function resolve(...args: string[]): string { + return toUnix(basepath.resolve(...args)); +} + export function toUnix(value: string): string { value = value.replaceAll('\\', '/'); value = value.replaceAll(/(? { const canonicalByName = canonicalNameMap.get(imp.name); @@ -233,6 +236,32 @@ function normalizeCanonicalImportPaths( return { ...imp, importPath }; }); + + if (schema.factoryImports) { + schema.factoryImports = schema.factoryImports.map((imp) => { + const canonicalByName = canonicalNameMap.get(imp.name); + + const resolvedImportKey = resolveImportKey( + factoryDir, + imp.importPath ?? `./${conventionName(imp.name, namingConvention)}`, + fileExtension, + ); + const canonicalByPath = canonicalPathMap.get(resolvedImportKey); + const canonical = canonicalByName ?? canonicalByPath; + if (!canonical?.importPath) return imp; + + const relative = upath.relativeSafe( + factoryDir, + canonical.importPath.replaceAll('\\', '/'), + ); + const withoutFileExtension = relative.endsWith(fileExtension) + ? relative.slice(0, -fileExtension.length) + : relative.replace(/\.ts$/, ''); + const importPath = `${withoutFileExtension}${importExtension}`; + + return { ...imp, importPath }; + }); + } } } @@ -249,12 +278,28 @@ function mergeSchemaGroup(schemas: GeneratorSchema[]): GeneratorSchema { const mergedDependencies = [ ...new Set(schemas.flatMap((schema) => schema.dependencies ?? [])), ]; + + const mergedFactory = schemas + .map((s) => s.factory) + .filter(Boolean) + .join('\n'); + const mergedFactoryImports = [ + ...new Map( + schemas + .flatMap((schema) => schema.factoryImports ?? []) + .map((imp) => [JSON.stringify(imp), imp] as [string, GeneratorImport]), + ).values(), + ]; + return { name: baseSchemaName, schema: baseSchema, model: schemas.map((schema) => schema.model).join('\n'), imports: mergedImports, dependencies: mergedDependencies, + factory: mergedFactory || undefined, + factoryImports: mergedFactoryImports, + factoryMode: schemas[0].factoryMode, }; } @@ -363,6 +408,43 @@ interface WriteSchemasOptions { header: string; indexFiles: boolean; tsconfig?: Tsconfig; + factoryOutputDirectory?: string; +} + +async function emitFactoryForSchema( + schema: GeneratorSchema, + namingConvention: NamingConvention, + header: string, + factoryDir: string, + fileExtension: string, + helpers: { + separateFactoryNames: string[]; + combinedFactoryContent: { value: string }; + combinedFactoryImports: GeneratorImport[]; + isCombined: { value: boolean }; + }, +) { + if (schema.factory && schema.factoryMode) { + const mode = schema.factoryMode; + if (mode === 'split') { + const baseName = conventionName(schema.name, namingConvention); + const factoryName = `${baseName}.factory`; + helpers.separateFactoryNames.push(factoryName); + const factoryImportsStr = generateImports({ + imports: schema.factoryImports ?? [], + namingConvention, + }); + const factoryFile = `${header}\n${factoryImportsStr}\n\n${schema.factory}`; + await writeGeneratedFile( + getPath(factoryDir, factoryName, fileExtension), + factoryFile, + ); + } else if (mode === 'single-split') { + helpers.isCombined.value = true; + helpers.combinedFactoryContent.value += `${schema.factory}\n`; + helpers.combinedFactoryImports.push(...(schema.factoryImports ?? [])); + } + } } export async function writeSchemas({ @@ -374,6 +456,7 @@ export async function writeSchemas({ header, indexFiles, tsconfig, + factoryOutputDirectory, }: WriteSchemasOptions) { const schemaGroups = getSchemaGroups( schemaPath, @@ -397,8 +480,23 @@ export async function writeSchemas({ namingConvention, fileExtension, tsconfig, + factoryOutputDirectory, ); + const factoryDir = factoryOutputDirectory ?? schemaPath; + + const combinedFactoryContent = { value: '' }; + const combinedFactoryImports: GeneratorImport[] = []; + const isCombined = { value: false }; + const separateFactoryNames: string[] = []; + + const factoryHelpers = { + separateFactoryNames, + combinedFactoryContent, + combinedFactoryImports, + isCombined, + }; + for (const groupSchemas of Object.values(schemaGroups)) { if (groupSchemas.length === 1) { await writeSchema({ @@ -410,6 +508,16 @@ export async function writeSchemas({ header, tsconfig, }); + + const singleSchema = groupSchemas[0]; + await emitFactoryForSchema( + singleSchema, + namingConvention, + header, + factoryDir, + fileExtension, + factoryHelpers, + ); continue; } @@ -424,6 +532,28 @@ export async function writeSchemas({ header, tsconfig, }); + + await emitFactoryForSchema( + mergedSchema, + namingConvention, + header, + factoryDir, + fileExtension, + factoryHelpers, + ); + } + + if (isCombined.value) { + const factoryFileName = conventionName('factoryMethods', namingConvention); + const factoryFileImports = generateImports({ + imports: combinedFactoryImports, + namingConvention, + }); + const factoryFile = `${header}\n${factoryFileImports}\n\n${combinedFactoryContent.value}`; + await writeGeneratedFile( + getPath(factoryDir, factoryFileName, fileExtension), + factoryFile, + ); } if (indexFiles) { @@ -444,11 +574,50 @@ export async function writeSchemas({ const uniqueSchemaNames = [...conventionNamesSet]; // Create export statements - const currentExports = uniqueSchemaNames - .map((schemaName) => `export * from './${schemaName}${ext}';`) - .toSorted((a, b) => a.localeCompare(b, 'en', { numeric: true })); + const currentExports = uniqueSchemaNames.map( + (schemaName) => `export * from './${schemaName}${ext}';`, + ); + + if ( + factoryOutputDirectory && + upath.normalizeSafe(factoryOutputDirectory) !== + upath.normalizeSafe(schemaPath) && + (isCombined.value || separateFactoryNames.length > 0) + ) { + const factoryIndexFilePath = nodePath.join( + factoryOutputDirectory, + `index.ts`, + ); + await fs.ensureFile(factoryIndexFilePath); + const factoryExports: string[] = []; + if (isCombined.value) { + const factoryFileName = conventionName( + 'factoryMethods', + namingConvention, + ); + factoryExports.push(`export * from './${factoryFileName}${ext}';`); + } + for (const fName of separateFactoryNames) { + factoryExports.push(`export * from './${fName}${ext}';`); + } + const content = `${header}\n${factoryExports.join('\n')}\n`; + await writeGeneratedFile(factoryIndexFilePath, content); + } else { + if (isCombined.value) { + const factoryFileName = conventionName( + 'factoryMethods', + namingConvention, + ); + currentExports.push(`export * from './${factoryFileName}${ext}';`); + } + for (const fName of separateFactoryNames) { + currentExports.push(`export * from './${fName}${ext}';`); + } + } - const exports = currentExports.join('\n'); + const exports = [...new Set(currentExports)] + .toSorted((a, b) => a.localeCompare(b, 'en', { numeric: true })) + .join('\n'); const fileContent = `${header}\n${exports}\n`; diff --git a/packages/mock/src/faker/getters/combine.test.ts b/packages/mock/src/faker/getters/combine.test.ts index 129c666408..4ad6ffddd2 100644 --- a/packages/mock/src/faker/getters/combine.test.ts +++ b/packages/mock/src/faker/getters/combine.test.ts @@ -37,6 +37,12 @@ function createMockContext(): ContextSpec { unionAddMissingProperties: false, optionsParamRequired: false, propertySortOrder: PropertySortOrder.ALPHABETICAL, + factoryMethods: { + functionNamePrefix: 'create', + mode: 'single', + outputDirectory: '', + includeOptionalProperty: false, + }, override: { title: undefined, transformer: undefined, diff --git a/packages/orval/src/utils/options.factory.test.ts b/packages/orval/src/utils/options.factory.test.ts new file mode 100644 index 0000000000..cf0885cf51 --- /dev/null +++ b/packages/orval/src/utils/options.factory.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; + +import { normalizeOptions } from './options'; + +describe('normalizeOptions factoryMethods', () => { + const baseInput = { target: 'petstore.yaml' }; + + it('should use defaults when factoryMethods is an empty object', async () => { + const options = await normalizeOptions({ + input: baseInput, + output: { + target: 'api.ts', + factoryMethods: {}, + }, + }); + + expect(options.output.factoryMethods).toEqual({ + functionNamePrefix: 'create', + mode: 'split', + includeOptionalProperty: true, + outputDirectory: process.cwd(), + }); + }); + + it('should override specific properties', async () => { + const options = await normalizeOptions({ + input: baseInput, + output: { + target: 'api.ts', + factoryMethods: { + functionNamePrefix: 'build', + mode: 'single', + includeOptionalProperty: false, + }, + }, + }); + + expect(options.output.factoryMethods).toEqual({ + functionNamePrefix: 'build', + mode: 'single', + includeOptionalProperty: false, + outputDirectory: process.cwd(), + }); + }); + + it('should use default values when properties are omitted', async () => { + const options = await normalizeOptions({ + input: baseInput, + output: { + target: 'api.ts', + factoryMethods: { + functionNamePrefix: 'make', + }, + }, + }); + + expect(options.output.factoryMethods).toEqual({ + functionNamePrefix: 'make', + mode: 'split', + includeOptionalProperty: true, + outputDirectory: process.cwd(), + }); + }); + + it('should default to undefined if factoryMethods is not provided', async () => { + const options = await normalizeOptions({ + input: baseInput, + output: { + target: 'api.ts', + }, + }); + + expect(options.output.factoryMethods).toBeUndefined(); + }); +}); diff --git a/packages/orval/src/utils/options.ts b/packages/orval/src/utils/options.ts index 7a13ace16a..cbf12c6562 100644 --- a/packages/orval/src/utils/options.ts +++ b/packages/orval/src/utils/options.ts @@ -26,6 +26,7 @@ import { type McpServerOptions, type Mutator, NamingConvention, + type NormalizedFactoryMethodsOptions, type NormalizedHonoOptions, type NormalizedHookOptions, type NormalizedJsDocOptions, @@ -219,6 +220,28 @@ export async function normalizeOptions( const defaultFileExtension = '.ts'; + const factoryMethodsConfig = outputOptions.factoryMethods; + let factoryMethods: NormalizedFactoryMethodsOptions | undefined = undefined; + + if (factoryMethodsConfig) { + factoryMethods = { + functionNamePrefix: factoryMethodsConfig.functionNamePrefix ?? 'create', + mode: factoryMethodsConfig.mode ?? 'split', + outputDirectory: factoryMethodsConfig.outputDirectory + ? normalizePath(factoryMethodsConfig.outputDirectory, outputWorkspace) + : outputOptions.schemas + ? normalizePath( + isString(outputOptions.schemas) + ? outputOptions.schemas + : outputOptions.schemas.path, + outputWorkspace, + ) + : normalizePath(outputWorkspace, outputWorkspace), + includeOptionalProperty: + factoryMethodsConfig.includeOptionalProperty ?? true, + }; + } + // `useQuery` / `useMutation` defaults are applied per-verb in // `query-generator.ts` so we can tell "unset" from "explicit true" (#2376). const globalQueryOptions: NormalizedQueryOptions = { @@ -289,6 +312,7 @@ export async function normalizeOptions( baseUrl: outputOptions.baseUrl, unionAddMissingProperties: outputOptions.unionAddMissingProperties ?? false, + factoryMethods, override: { ...outputOptions.override, mock: { diff --git a/packages/orval/src/write-specs.ts b/packages/orval/src/write-specs.ts index 977c16fd52..358bf6d5e6 100644 --- a/packages/orval/src/write-specs.ts +++ b/packages/orval/src/write-specs.ts @@ -319,6 +319,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); } @@ -333,6 +334,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); // Add re-export from operations in the main schemas index @@ -354,6 +356,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); } } else { @@ -399,6 +402,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); } @@ -412,6 +416,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); // Add re-export from operations in the main schemas index @@ -433,6 +438,7 @@ export async function writeSpecs( header, indexFiles: output.indexFiles, tsconfig: output.tsconfig, + factoryOutputDirectory: output.factoryMethods?.outputDirectory, }); } } else { diff --git a/packages/solid-start/src/index.test.ts b/packages/solid-start/src/index.test.ts index 65375299bc..8d53cc877d 100644 --- a/packages/solid-start/src/index.test.ts +++ b/packages/solid-start/src/index.test.ts @@ -42,6 +42,12 @@ function makeOutput(useDates = false): ContextSpec['output'] { unionAddMissingProperties: false, optionsParamRequired: false, propertySortOrder: PropertySortOrder.ALPHABETICAL, + factoryMethods: { + functionNamePrefix: 'create', + mode: 'single', + outputDirectory: '', + includeOptionalProperty: false, + }, override: { title: undefined, transformer: undefined, diff --git a/tests/__snapshots__/factory-methods/combined/endpoints.ts b/tests/__snapshots__/factory-methods/combined/endpoints.ts new file mode 100644 index 0000000000..2053b43a8d --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/endpoints.ts @@ -0,0 +1,25 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import axios from 'axios'; +import type { + AxiosRequestConfig, + AxiosResponse +} from 'axios'; + +import type { + User +} from './model'; + +export const getTest = ( + options?: AxiosRequestConfig + ): Promise> => { + return axios.get( + `/test`,options + ); + } + +export type GetTestResult = AxiosResponse diff --git a/tests/__snapshots__/factory-methods/combined/model/circularChild.ts b/tests/__snapshots__/factory-methods/combined/model/circularChild.ts new file mode 100644 index 0000000000..f001787610 --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/circularChild.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; + +export interface CircularChild { + parent: CircularParent; +} diff --git a/tests/__snapshots__/factory-methods/combined/model/circularParent.ts b/tests/__snapshots__/factory-methods/combined/model/circularParent.ts new file mode 100644 index 0000000000..bfa68e87ca --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/circularParent.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularChild } from './circularChild'; + +export interface CircularParent { + child: CircularChild; +} diff --git a/tests/__snapshots__/factory-methods/combined/model/deepCircularA.ts b/tests/__snapshots__/factory-methods/combined/model/deepCircularA.ts new file mode 100644 index 0000000000..efef21128a --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/deepCircularA.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularB } from './deepCircularB'; + +export interface DeepCircularA { + b?: DeepCircularB; +} diff --git a/tests/__snapshots__/factory-methods/combined/model/deepCircularB.ts b/tests/__snapshots__/factory-methods/combined/model/deepCircularB.ts new file mode 100644 index 0000000000..a7d2c61ba9 --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/deepCircularB.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularC } from './deepCircularC'; + +export interface DeepCircularB { + c?: DeepCircularC; +} diff --git a/tests/__snapshots__/factory-methods/combined/model/deepCircularC.ts b/tests/__snapshots__/factory-methods/combined/model/deepCircularC.ts new file mode 100644 index 0000000000..9321b3d22a --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/deepCircularC.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularA } from './deepCircularA'; + +export interface DeepCircularC { + a?: DeepCircularA; +} diff --git a/tests/__snapshots__/factory-methods/combined/model/factoryMethods.ts b/tests/__snapshots__/factory-methods/combined/model/factoryMethods.ts new file mode 100644 index 0000000000..38a491c6f4 --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/factoryMethods.ts @@ -0,0 +1,61 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { CircularChild } from './circularChild'; +import type { CircularParent } from './circularParent'; +import type { DeepCircularA } from './deepCircularA'; +import type { DeepCircularB } from './deepCircularB'; +import type { DeepCircularC } from './deepCircularC'; +import type { Status } from './status'; +import type { User } from './user'; + +export function createStatus(): Status { + return "ACTIVE"; +} + +export function createCircularChild(): CircularChild { + return { + parent: {} as CircularParent + }; +} + +export function createCircularParent(): CircularParent { + return { + child: {} as CircularChild + }; +} + +export function createUser(): User { + return { + id: 0, + name: '', + status: createStatus(), + avatar: '', + createdAt: '1970-01-01T00:00:00.000Z', + tags: [], + circular: createCircularParent() + }; +} + +export function createDeepCircularC(): DeepCircularC { + return { + a: {} as DeepCircularA + }; +} + +export function createDeepCircularB(): DeepCircularB { + return { + c: {} as DeepCircularC + }; +} + +export function createDeepCircularA(): DeepCircularA { + return { + b: {} as DeepCircularB + }; +} + diff --git a/tests/__snapshots__/factory-methods/combined/model/index.ts b/tests/__snapshots__/factory-methods/combined/model/index.ts new file mode 100644 index 0000000000..547e9d917f --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/index.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export * from './circularChild'; +export * from './circularParent'; +export * from './deepCircularA'; +export * from './deepCircularB'; +export * from './deepCircularC'; +export * from './factoryMethods'; +export * from './status'; +export * from './user'; diff --git a/tests/__snapshots__/factory-methods/combined/model/status.ts b/tests/__snapshots__/factory-methods/combined/model/status.ts new file mode 100644 index 0000000000..6694ec6f8b --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/status.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export type Status = typeof Status[keyof typeof Status]; + + +export const Status = { + ACTIVE: 'ACTIVE', + INACTIVE: 'INACTIVE', + PENDING: 'PENDING', +} as const; diff --git a/tests/__snapshots__/factory-methods/combined/model/user.ts b/tests/__snapshots__/factory-methods/combined/model/user.ts new file mode 100644 index 0000000000..712ff1a956 --- /dev/null +++ b/tests/__snapshots__/factory-methods/combined/model/user.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; +import type { Status } from './status'; + +export interface User { + id: number; + name: string; + status: Status; + /** @nullable */ + avatar?: string | null; + createdAt?: string; + tags?: string[]; + circular?: CircularParent; +} diff --git a/tests/__snapshots__/factory-methods/inline/endpoints.ts b/tests/__snapshots__/factory-methods/inline/endpoints.ts new file mode 100644 index 0000000000..2053b43a8d --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/endpoints.ts @@ -0,0 +1,25 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import axios from 'axios'; +import type { + AxiosRequestConfig, + AxiosResponse +} from 'axios'; + +import type { + User +} from './model'; + +export const getTest = ( + options?: AxiosRequestConfig + ): Promise> => { + return axios.get( + `/test`,options + ); + } + +export type GetTestResult = AxiosResponse diff --git a/tests/__snapshots__/factory-methods/inline/model/circularChild.ts b/tests/__snapshots__/factory-methods/inline/model/circularChild.ts new file mode 100644 index 0000000000..c10a2ca35e --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/circularChild.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; + +export interface CircularChild { + parent: CircularParent; +} + +export function createCircularChild(): CircularChild { + return { + parent: {} as CircularParent + }; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/circularParent.ts b/tests/__snapshots__/factory-methods/inline/model/circularParent.ts new file mode 100644 index 0000000000..6977e615b9 --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/circularParent.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularChild } from './circularChild'; + +export interface CircularParent { + child: CircularChild; +} + +export function createCircularParent(): CircularParent { + return { + child: {} as CircularChild + }; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/deepCircularA.ts b/tests/__snapshots__/factory-methods/inline/model/deepCircularA.ts new file mode 100644 index 0000000000..0a0cb4b7ea --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/deepCircularA.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularB } from './deepCircularB'; + +export interface DeepCircularA { + b?: DeepCircularB; +} + +export function createDeepCircularA(): DeepCircularA { + return { + b: {} as DeepCircularB + }; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/deepCircularB.ts b/tests/__snapshots__/factory-methods/inline/model/deepCircularB.ts new file mode 100644 index 0000000000..13f593897f --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/deepCircularB.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularC } from './deepCircularC'; + +export interface DeepCircularB { + c?: DeepCircularC; +} + +export function createDeepCircularB(): DeepCircularB { + return { + c: {} as DeepCircularC + }; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/deepCircularC.ts b/tests/__snapshots__/factory-methods/inline/model/deepCircularC.ts new file mode 100644 index 0000000000..8f82d01439 --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/deepCircularC.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularA } from './deepCircularA'; + +export interface DeepCircularC { + a?: DeepCircularA; +} + +export function createDeepCircularC(): DeepCircularC { + return { + a: {} as DeepCircularA + }; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/index.ts b/tests/__snapshots__/factory-methods/inline/model/index.ts new file mode 100644 index 0000000000..8f63d15b4a --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/index.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export * from './circularChild'; +export * from './circularParent'; +export * from './deepCircularA'; +export * from './deepCircularB'; +export * from './deepCircularC'; +export * from './status'; +export * from './user'; diff --git a/tests/__snapshots__/factory-methods/inline/model/status.ts b/tests/__snapshots__/factory-methods/inline/model/status.ts new file mode 100644 index 0000000000..984d6a0cb1 --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/status.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export type Status = typeof Status[keyof typeof Status]; + + +export const Status = { + ACTIVE: 'ACTIVE', + INACTIVE: 'INACTIVE', + PENDING: 'PENDING', +} as const; + +export function createStatus(): Status { + return "ACTIVE"; +} diff --git a/tests/__snapshots__/factory-methods/inline/model/user.ts b/tests/__snapshots__/factory-methods/inline/model/user.ts new file mode 100644 index 0000000000..97eb9ff861 --- /dev/null +++ b/tests/__snapshots__/factory-methods/inline/model/user.ts @@ -0,0 +1,33 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; +import type { Status } from './status'; +import { createCircularParent } from './circularParent'; +import { createStatus } from './status'; + +export interface User { + id: number; + name: string; + status: Status; + /** @nullable */ + avatar?: string | null; + createdAt?: string; + tags?: string[]; + circular?: CircularParent; +} + +export function createUser(): User { + return { + id: 0, + name: '', + status: createStatus(), + avatar: '', + createdAt: '1970-01-01T00:00:00.000Z', + tags: [], + circular: createCircularParent() + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/endpoints.ts b/tests/__snapshots__/factory-methods/separate/endpoints.ts new file mode 100644 index 0000000000..2053b43a8d --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/endpoints.ts @@ -0,0 +1,25 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import axios from 'axios'; +import type { + AxiosRequestConfig, + AxiosResponse +} from 'axios'; + +import type { + User +} from './model'; + +export const getTest = ( + options?: AxiosRequestConfig + ): Promise> => { + return axios.get( + `/test`,options + ); + } + +export type GetTestResult = AxiosResponse diff --git a/tests/__snapshots__/factory-methods/separate/model/circularChild.factory.ts b/tests/__snapshots__/factory-methods/separate/model/circularChild.factory.ts new file mode 100644 index 0000000000..7e94da800e --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/circularChild.factory.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { CircularChild } from './circularChild'; +import type { CircularParent } from './circularParent'; + +export function createCircularChild(): CircularChild { + return { + parent: {} as CircularParent + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/circularChild.ts b/tests/__snapshots__/factory-methods/separate/model/circularChild.ts new file mode 100644 index 0000000000..f001787610 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/circularChild.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; + +export interface CircularChild { + parent: CircularParent; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/circularParent.factory.ts b/tests/__snapshots__/factory-methods/separate/model/circularParent.factory.ts new file mode 100644 index 0000000000..415c1a1bf0 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/circularParent.factory.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { CircularChild } from './circularChild'; +import type { CircularParent } from './circularParent'; + +export function createCircularParent(): CircularParent { + return { + child: {} as CircularChild + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/circularParent.ts b/tests/__snapshots__/factory-methods/separate/model/circularParent.ts new file mode 100644 index 0000000000..bfa68e87ca --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/circularParent.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularChild } from './circularChild'; + +export interface CircularParent { + child: CircularChild; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularA.factory.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularA.factory.ts new file mode 100644 index 0000000000..3d4b995b28 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularA.factory.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { DeepCircularA } from './deepCircularA'; +import type { DeepCircularB } from './deepCircularB'; + +export function createDeepCircularA(): DeepCircularA { + return { + b: {} as DeepCircularB + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularA.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularA.ts new file mode 100644 index 0000000000..efef21128a --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularA.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularB } from './deepCircularB'; + +export interface DeepCircularA { + b?: DeepCircularB; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularB.factory.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularB.factory.ts new file mode 100644 index 0000000000..42021e8c28 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularB.factory.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { DeepCircularB } from './deepCircularB'; +import type { DeepCircularC } from './deepCircularC'; + +export function createDeepCircularB(): DeepCircularB { + return { + c: {} as DeepCircularC + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularB.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularB.ts new file mode 100644 index 0000000000..a7d2c61ba9 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularB.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularC } from './deepCircularC'; + +export interface DeepCircularB { + c?: DeepCircularC; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularC.factory.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularC.factory.ts new file mode 100644 index 0000000000..341a911531 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularC.factory.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { DeepCircularA } from './deepCircularA'; +import type { DeepCircularC } from './deepCircularC'; + +export function createDeepCircularC(): DeepCircularC { + return { + a: {} as DeepCircularA + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/deepCircularC.ts b/tests/__snapshots__/factory-methods/separate/model/deepCircularC.ts new file mode 100644 index 0000000000..9321b3d22a --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/deepCircularC.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { DeepCircularA } from './deepCircularA'; + +export interface DeepCircularC { + a?: DeepCircularA; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/index.ts b/tests/__snapshots__/factory-methods/separate/model/index.ts new file mode 100644 index 0000000000..e3970c2c12 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/index.ts @@ -0,0 +1,21 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export * from './circularChild.factory'; +export * from './circularChild'; +export * from './circularParent.factory'; +export * from './circularParent'; +export * from './deepCircularA.factory'; +export * from './deepCircularA'; +export * from './deepCircularB.factory'; +export * from './deepCircularB'; +export * from './deepCircularC.factory'; +export * from './deepCircularC'; +export * from './status.factory'; +export * from './status'; +export * from './user.factory'; +export * from './user'; diff --git a/tests/__snapshots__/factory-methods/separate/model/status.factory.ts b/tests/__snapshots__/factory-methods/separate/model/status.factory.ts new file mode 100644 index 0000000000..2dc31a76a5 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/status.factory.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { Status } from './status'; + +export function createStatus(): Status { + return "ACTIVE"; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/status.ts b/tests/__snapshots__/factory-methods/separate/model/status.ts new file mode 100644 index 0000000000..6694ec6f8b --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/status.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +export type Status = typeof Status[keyof typeof Status]; + + +export const Status = { + ACTIVE: 'ACTIVE', + INACTIVE: 'INACTIVE', + PENDING: 'PENDING', +} as const; diff --git a/tests/__snapshots__/factory-methods/separate/model/user.factory.ts b/tests/__snapshots__/factory-methods/separate/model/user.factory.ts new file mode 100644 index 0000000000..fbc884062a --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/user.factory.ts @@ -0,0 +1,24 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ + +import type { CircularParent } from './circularParent'; +import type { Status } from './status'; +import type { User } from './user'; +import { createCircularParent } from './circularParent.factory'; +import { createStatus } from './status.factory'; + +export function createUser(): User { + return { + id: 0, + name: '', + status: createStatus(), + avatar: '', + createdAt: '1970-01-01T00:00:00.000Z', + tags: [], + circular: createCircularParent() + }; +} diff --git a/tests/__snapshots__/factory-methods/separate/model/user.ts b/tests/__snapshots__/factory-methods/separate/model/user.ts new file mode 100644 index 0000000000..712ff1a956 --- /dev/null +++ b/tests/__snapshots__/factory-methods/separate/model/user.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.12.3 🍺 + * Do not edit manually. + * Factory Methods Test Spec + * OpenAPI spec version: 1.0.0 + */ +import type { CircularParent } from './circularParent'; +import type { Status } from './status'; + +export interface User { + id: number; + name: string; + status: Status; + /** @nullable */ + avatar?: string | null; + createdAt?: string; + tags?: string[]; + circular?: CircularParent; +} diff --git a/tests/api-generation.spec.ts b/tests/api-generation.spec.ts index cc595f641b..8306444d86 100644 --- a/tests/api-generation.spec.ts +++ b/tests/api-generation.spec.ts @@ -13,6 +13,7 @@ await describeApiGenerationSnapshots({ generated('angular'), generated('angular-query'), generated('axios'), + generated('factory-methods'), generated('cli'), generated('default'), generated('fetch'), diff --git a/tests/configs/factory-methods.config.ts b/tests/configs/factory-methods.config.ts new file mode 100644 index 0000000000..0b226850bb --- /dev/null +++ b/tests/configs/factory-methods.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from 'orval'; + +export default defineConfig({ + inline: { + input: '../specifications/factory-methods.yaml', + output: { + target: '../generated/factory-methods/inline/endpoints.ts', + schemas: '../generated/factory-methods/inline/model', + factoryMethods: { + mode: 'single', + }, + clean: true, + prettier: true, + }, + }, + separate: { + input: '../specifications/factory-methods.yaml', + output: { + target: '../generated/factory-methods/separate/endpoints.ts', + schemas: '../generated/factory-methods/separate/model', + factoryMethods: { + mode: 'split', + }, + clean: true, + prettier: true, + }, + }, + combined: { + input: '../specifications/factory-methods.yaml', + output: { + target: '../generated/factory-methods/combined/endpoints.ts', + schemas: '../generated/factory-methods/combined/model', + factoryMethods: { + mode: 'single-split', + }, + clean: true, + prettier: true, + }, + }, +}); diff --git a/tests/package.json b/tests/package.json index 27ef63d380..8d48879761 100644 --- a/tests/package.json +++ b/tests/package.json @@ -23,6 +23,7 @@ "generate:fetch": "orval --config ./configs/fetch.config.ts", "generate:mcp": "orval --config ./configs/mcp.config.ts", "generate:hono": "orval --config ./configs/hono.config.ts", + "generate:factory-methods": "orval --config ./configs/factory-methods.config.ts", "test:snapshots": "vitest run --config vitest.snapshots.ts", "test:snapshots:update": "bun run test:snapshots --update", "build": "node ./scripts/typecheck-generated.mjs && bun run verify:mock-generated", diff --git a/tests/specifications/factory-methods.yaml b/tests/specifications/factory-methods.yaml new file mode 100644 index 0000000000..ac0fe45d56 --- /dev/null +++ b/tests/specifications/factory-methods.yaml @@ -0,0 +1,68 @@ +openapi: '3.0.0' +info: + version: 1.0.0 + title: Factory Methods Test Spec +paths: + /test: + get: + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' +components: + schemas: + User: + type: object + required: [id, name, status] + properties: + id: + type: integer + name: + type: string + status: + $ref: '#/components/schemas/Status' + avatar: + type: string + nullable: true + createdAt: + type: string + format: date-time + tags: + type: array + items: + type: string + circular: + $ref: '#/components/schemas/CircularParent' + Status: + type: string + enum: [ACTIVE, INACTIVE, PENDING] + CircularParent: + type: object + required: [child] + properties: + child: + $ref: '#/components/schemas/CircularChild' + CircularChild: + type: object + required: [parent] + properties: + parent: + $ref: '#/components/schemas/CircularParent' + DeepCircularA: + type: object + properties: + b: + $ref: '#/components/schemas/DeepCircularB' + DeepCircularB: + type: object + properties: + c: + $ref: '#/components/schemas/DeepCircularC' + DeepCircularC: + type: object + properties: + a: + $ref: '#/components/schemas/DeepCircularA'