Skip to content

orval/core - generating factory method for schema interfaces #1334

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/generators/component-definition.ts
Original file line number Diff line number Diff line change
@@ -55,6 +55,7 @@ export const generateComponentDefinition = (
if (modelName !== type) {
acc.push({
name: modelName,
factoryMethod: '',
model,
imports,
});
14 changes: 12 additions & 2 deletions packages/core/src/generators/imports.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import {
GeneratorMutator,
GeneratorVerbOptions,
GetterPropType,
OutputModelFactoryMethodsMode,
} from '../types';
import { camel, upath } from '../utils';

@@ -14,12 +15,16 @@ export const generateImports = ({
isRootKey,
specsName,
specKey: currentSpecKey,
factoryMethodOutput,
factoryMethodPrefix
}: {
imports: GeneratorImport[];
target: string;
isRootKey: boolean;
specsName: Record<string, string>;
specKey: string;
factoryMethodOutput?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
factoryMethodPrefix?: string;
}) => {
if (!imports.length) {
return '';
@@ -47,9 +52,14 @@ export const generateImports = ({
} } from \'./${upath.join(path, camel(name))}\';`;
}

return `import ${!values ? 'type ' : ''}{ ${name}${
let mainImport = `import ${!values ? 'type ' : ''}{ ${name}${
alias ? ` as ${alias}` : ''
} } from \'./${camel(name)}\';`;
}${factoryMethodOutput == OutputModelFactoryMethodsMode.SINGLE ? `${factoryMethodPrefix}${name}` : ''} } from \'./${camel(name)}\';`;
if (factoryMethodOutput == OutputModelFactoryMethodsMode.SPLIT) {
let factoryMethodImport = `import { ${factoryMethodPrefix}${name} } from \'./${camel(name)}.factory\';`;
mainImport += '\n' + factoryMethodImport;
}
return mainImport;
})
.join('\n');
};
14 changes: 12 additions & 2 deletions packages/core/src/generators/interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchemaObject } from 'openapi3-ts/oas30';
import { getScalar } from '../getters';
import { ContextSpecs } from '../types';
import { ContextSpecs, OutputModelFactoryMethodsMode } from '../types';
import { jsDoc } from '../utils';

/**
@@ -53,17 +53,27 @@ export const generateInterface = ({
} else {
model += `export type ${name} = ${scalar.value};\n`;
}

// Filter out imports that refer to the type defined in current file (OpenAPI recursive schema definitions)
const externalModulesImportsOnly = scalar.imports.filter(
(importName) => importName.name !== name,
);

let factoryMethod: string = '';
if (context?.output.modelFactoryMethods) {
let factoryMethodPrefix = context?.output.override?.modelFactoryMethods?.factoryMethodPrefix!;
factoryMethod = `export function ${factoryMethodPrefix}${name}(): ${name} ${scalar.factoryMethodValue}\n`;
if (context?.output.override?.modelFactoryMethods?.outputMode == OutputModelFactoryMethodsMode.SINGLE) {
model += factoryMethod;
factoryMethod = '';
}
}

return [
...scalar.schemas,
{
name,
model,
factoryMethod,
imports: externalModulesImportsOnly,
},
];
2 changes: 2 additions & 0 deletions packages/core/src/generators/parameter-definition.ts
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ export const generateParameterDefinition = (
if (!schema.schema || imports.length) {
acc.push({
name: modelName,
factoryMethod: '',
imports: imports.length
? [
{
@@ -63,6 +64,7 @@ export const generateParameterDefinition = (
if (modelName !== resolvedObject.value) {
acc.push({
name: modelName,
factoryMethod: '',
model,
imports: resolvedObject.imports,
});
1 change: 1 addition & 0 deletions packages/core/src/generators/schema-definition.ts
Original file line number Diff line number Diff line change
@@ -127,6 +127,7 @@ export const generateSchemasDefinition = (

acc.push(...resolvedValue.schemas, {
name: schemaName,
factoryMethod: '',
model: output,
imports,
});
2 changes: 2 additions & 0 deletions packages/core/src/getters/array.ts
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ export const getArray = ({
}
return {
type: 'array',
factoryMethodValue: `[]`,
isEnum: false,
isRef: false,
value: `[${resolvedObjects.map((o) => o.value).join(', ')}]`,
@@ -71,6 +72,7 @@ export const getArray = ({
? `(${resolvedObject.value})[]`
: `${resolvedObject.value}[]`
}`,
factoryMethodValue: `[]`,
imports: resolvedObject.imports,
schemas: resolvedObject.schemas,
isEnum: false,
3 changes: 3 additions & 0 deletions packages/core/src/getters/combine.ts
Original file line number Diff line number Diff line change
@@ -177,6 +177,7 @@ export const combineSchemas = ({

return {
value: `typeof ${pascal(name)}[keyof typeof ${pascal(name)}] ${nullable}`,
factoryMethodValue: `{}`,
imports: [
{
name: pascal(name),
@@ -192,6 +193,7 @@ export const combineSchemas = ({
})),
],
model: newEnum,
factoryMethod: '',
name: name,
},
],
@@ -219,6 +221,7 @@ export const combineSchemas = ({

return {
value: value + nullable,
factoryMethodValue: `{}`,
imports: resolvedValue
? [...resolvedData.imports, ...resolvedValue.imports]
: resolvedData.imports,
11 changes: 11 additions & 0 deletions packages/core/src/getters/object.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ export const getObject = ({
const { name, specKey } = getRefInfo(item.$ref, context);
return {
value: name + nullable,
factoryMethodValue: `''`,
imports: [{ name, specKey }],
schemas: [],
isEnum: false,
@@ -115,12 +116,16 @@ export const getObject = ({
const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
if (!index) {
acc.value += '{';
acc.factoryMethodValue += '{\n return {';
}

const doc = jsDoc(schema as SchemaObject, true);

acc.hasReadonlyProps ||= isReadOnly || false;
acc.imports.push(...resolvedValue.imports);
if (!isReadOnly || isRequired) {
acc.factoryMethodValue += `\n ${getKey(key)}: ${resolvedValue.factoryMethodValue},`;
}
acc.value += `\n ${doc ? `${doc} ` : ''}${
isReadOnly && !context.output.override.suppressReadonlyModifier
? 'readonly '
@@ -142,6 +147,7 @@ export const getObject = ({
}
} else {
acc.value += '\n}';
acc.factoryMethodValue += '\n };\n}';
}

acc.value += nullable;
@@ -153,6 +159,7 @@ export const getObject = ({
imports: [],
schemas: [],
value: '',
factoryMethodValue: '',
isEnum: false,
type: 'object' as SchemaType,
isRef: false,
@@ -168,6 +175,7 @@ export const getObject = ({
if (isBoolean(item.additionalProperties)) {
return {
value: `{ [key: string]: unknown }` + nullable,
factoryMethodValue: `{}`,
imports: [],
schemas: [],
isEnum: false,
@@ -183,6 +191,7 @@ export const getObject = ({
});
return {
value: `{[key: string]: ${resolvedValue.value}}` + nullable,
factoryMethodValue: `{}`,
imports: resolvedValue.imports ?? [],
schemas: resolvedValue.schemas ?? [],
isEnum: false,
@@ -196,6 +205,7 @@ export const getObject = ({
if (itemWithConst.const) {
return {
value: `'${itemWithConst.const}'` + nullable,
factoryMethodValue: `null`,
imports: [],
schemas: [],
isEnum: false,
@@ -209,6 +219,7 @@ export const getObject = ({
value:
(item.type === 'object' ? '{ [key: string]: unknown }' : 'unknown') +
nullable,
factoryMethodValue: `${item.type === 'object' ? '{}' : 'null'}`,
imports: [],
schemas: [],
isEnum: false,
1 change: 1 addition & 0 deletions packages/core/src/getters/props.ts
Original file line number Diff line number Diff line change
@@ -96,6 +96,7 @@ export const getProps = ({
required: true,
schema: {
name: parameterTypeName,
factoryMethod: '',
model: namedParametersTypeDefinition,
imports: params.flatMap((property) => property.imports),
},
3 changes: 2 additions & 1 deletion packages/core/src/getters/query-params.ts
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ const getQueryParamsTypes = (
imports: [{ name: enumName }],
schemas: [
...resolvedValue.schemas,
{ name: enumName, model: enumValue, imports: resolvedValue.imports },
{ name: enumName, model: enumValue, factoryMethod: '', imports: resolvedValue.imports },
],
originalSchema: resolvedValue.originalSchema,
};
@@ -125,6 +125,7 @@ export const getQueryParams = ({

const schema = {
name,
factoryMethod: '',
model: `export type ${name} = {\n${type}\n};\n`,
imports,
};
3 changes: 3 additions & 0 deletions packages/core/src/getters/res-req-types.ts
Original file line number Diff line number Diff line change
@@ -72,6 +72,7 @@ export const getResReqTypes = (
return [
{
value: name,
factoryMethodValue: name,
imports: [{ name, specKey, schemaName }],
schemas: [],
type: 'unknown',
@@ -129,6 +130,7 @@ export const getResReqTypes = (
{
value: name,
imports: [{ name, specKey, schemaName }, ...additionalImports],
factoryMethodValue: name,
schemas: [],
type: 'unknown',
isEnum: false,
@@ -223,6 +225,7 @@ export const getResReqTypes = (
return [
{
value: defaultType,
factoryMethodValue: `''`,
imports: [],
schemas: [],
type: defaultType,
5 changes: 5 additions & 0 deletions packages/core/src/getters/scalar.ts
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@ export const getScalar = ({

return {
value: value + nullable,
factoryMethodValue: `0`,
isEnum,
type: 'number',
schemas: [],
@@ -79,6 +80,7 @@ export const getScalar = ({

return {
value: value + nullable,
factoryMethodValue: `false`,
type: 'boolean',
isEnum: false,
schemas: [],
@@ -133,6 +135,7 @@ export const getScalar = ({

return {
value: value + nullable,
factoryMethodValue: `''`,
isEnum,
type: 'string',
imports: [],
@@ -147,6 +150,7 @@ export const getScalar = ({
case 'null':
return {
value: 'null',
factoryMethodValue: `null`,
isEnum: false,
type: 'null',
imports: [],
@@ -167,6 +171,7 @@ export const getScalar = ({

return {
value: value + nullable,
factoryMethodValue: enumItems[0],
isEnum: true,
type: 'string',
imports: [],
8 changes: 8 additions & 0 deletions packages/core/src/resolvers/object.ts
Original file line number Diff line number Diff line change
@@ -30,11 +30,13 @@ const resolveObjectOriginal = ({
) {
return {
value: propName,
factoryMethodValue: `{}`,
imports: [{ name: propName }],
schemas: [
...resolvedValue.schemas,
{
name: propName,
factoryMethod: `export function ${context.output.override.modelFactoryMethods?.factoryMethodPrefix}${propName}(): ${propName}${resolvedValue.factoryMethodValue}`,
model: `${doc}export type ${propName} = ${resolvedValue.value};\n`,
imports: resolvedValue.imports,
},
@@ -54,13 +56,19 @@ const resolveObjectOriginal = ({
resolvedValue.originalSchema?.['x-enumNames'],
context.output.override.useNativeEnums,
);
const factoryMethodValue = context?.output.override?.useTypeOverInterfaces
? `${propName}[${resolvedValue.value.split(' | ')[0]}]`
: `${resolvedValue.value.split(' | ')[0]}`;


return {
value: propName,
factoryMethodValue,
imports: [{ name: propName }],
schemas: [
...resolvedValue.schemas,
{
factoryMethod: '',
name: propName,
model: doc + enumValue,
imports: resolvedValue.imports,
1 change: 1 addition & 0 deletions packages/core/src/resolvers/value.ts
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ export const resolveValue = ({

return {
value: resolvedImport.name,
factoryMethodValue: `${context.output.override.modelFactoryMethods?.factoryMethodPrefix}${resolvedImport.name}()`,
imports: [
{
name: resolvedImport.name,
22 changes: 22 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -50,6 +50,7 @@ export type NormalizedOutputOptions = {
override: NormalizedOverrideOutput;
client: OutputClient | OutputClientFunc;
httpClient: OutputHttpClient;
modelFactoryMethods?: boolean;
clean: boolean | string[];
docs: boolean | OutputDocsOptions;
prettier: boolean;
@@ -70,6 +71,16 @@ export type NormalizedParamsSerializerOptions = {
qs?: Record<string, any>;
};

export const OutputModelFactoryMethodsMode = {
SINGLE: 'single',
SPLIT: 'split',
} as const;

export type NormalizedModelFactoryMethodsOptions = {
factoryMethodPrefix?: string;
outputMode?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
};

export type NormalizedOverrideOutput = {
title?: (title: string) => string;
transformer?: OutputTransformer;
@@ -83,6 +94,7 @@ export type NormalizedOverrideOutput = {
formUrlEncoded: boolean | NormalizedMutator;
paramsSerializer?: NormalizedMutator;
paramsSerializerOptions?: NormalizedParamsSerializerOptions;
modelFactoryMethods?: NormalizedModelFactoryMethodsOptions;
components: {
schemas: {
suffix: string;
@@ -175,6 +187,7 @@ export type OutputOptions = {
override?: OverrideOutput;
client?: OutputClient | OutputClientFunc;
httpClient?: OutputHttpClient;
modelFactoryMethods?: boolean;
clean?: boolean | string[];
docs?: boolean | OutputDocsOptions;
prettier?: boolean;
@@ -328,6 +341,11 @@ export type ParamsSerializerOptions = {
qs?: Record<string, any>;
};

export type ModelFactoryMethodsOptions = {
factoryMethodPrefix?: string;
outputMode?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
};

export type OverrideOutput = {
title?: (title: string) => string;
transformer?: OutputTransformer;
@@ -341,6 +359,7 @@ export type OverrideOutput = {
formUrlEncoded?: boolean | Mutator;
paramsSerializer?: Mutator;
paramsSerializerOptions?: ParamsSerializerOptions;
modelFactoryMethods?: ModelFactoryMethodsOptions;
components?: {
schemas?: {
suffix?: string;
@@ -603,6 +622,7 @@ export interface GlobalOptions {
mock?: boolean | GlobalMockOptions;
client?: OutputClient;
httpClient?: OutputHttpClient;
modelFactoryMethods?: boolean;
mode?: OutputMode;
tsconfig?: string | Tsconfig;
packageJson?: string;
@@ -644,6 +664,7 @@ export interface PackageJson {
export type GeneratorSchema = {
name: string;
model: string;
factoryMethod: string;
imports: GeneratorImport[];
};

@@ -961,6 +982,7 @@ export const SchemaType = {

export type ScalarValue = {
value: string;
factoryMethodValue: string;
isEnum: boolean;
hasReadonlyProps: boolean;
type: SchemaType;
64 changes: 63 additions & 1 deletion packages/core/src/writers/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'fs-extra';
import { generateImports } from '../generators';
import { GeneratorSchema } from '../types';
import { GeneratorSchema, OutputModelFactoryMethodsMode } from '../types';
import { camel, upath } from '../utils';

const getSchema = ({
@@ -35,6 +35,46 @@ const getSchema = ({
return file;
};

const getFactoryMethod = ({
schema: { name, imports, model, factoryMethod },
target,
isRootKey,
specsName,
header,
specKey,
factoryMethodOutput,
factoryMethodPrefix
}: {
schema: GeneratorSchema;
target: string;
isRootKey: boolean;
specsName: Record<string, string>;
header: string;
specKey: string;
factoryMethodOutput?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
factoryMethodPrefix?: string;
}): string => {
let file = header;
file += generateImports({
imports: imports.filter(
(imp) =>
!model.includes(`type ${imp.alias || imp.name} =`) &&
!model.includes(`interface ${imp.alias || imp.name} {`),
),
target,
isRootKey,
specsName,
specKey,
factoryMethodOutput,
factoryMethodPrefix
});
file += `import { ${name}} from \'./${camel(name)}\';`;
file += '\n\n';

file += factoryMethod;
return file;
};

const getPath = (path: string, name: string, fileExtension: string): string =>
upath.join(path, `/${name}${fileExtension}`);

@@ -53,6 +93,9 @@ export const writeSchema = async ({
isRootKey,
specsName,
header,
factoryMethodInclude,
factoryMethodOutput,
factoryMethodPrefix
}: {
path: string;
schema: GeneratorSchema;
@@ -62,6 +105,10 @@ export const writeSchema = async ({
isRootKey: boolean;
specsName: Record<string, string>;
header: string;
factoryMethodInclude: boolean;
factoryMethodOutput?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
factoryMethodPrefix?: string;

}) => {
const name = camel(schema.name);

@@ -70,6 +117,12 @@ export const writeSchema = async ({
getPath(path, name, fileExtension),
getSchema({ schema, target, isRootKey, specsName, header, specKey }),
);
if (factoryMethodInclude && schema.factoryMethod.length > 0) {
await fs.outputFile(
getPath(path, name + '.factory', fileExtension),
getFactoryMethod({ schema, target, isRootKey, specsName, header, specKey, factoryMethodOutput, factoryMethodPrefix }),
);
}
} catch (e) {
throw `Oups... 🍻. An Error occurred while writing schema ${name} => ${e}`;
}
@@ -85,6 +138,9 @@ export const writeSchemas = async ({
specsName,
header,
indexFiles,
factoryMethodInclude,
factoryMethodOutput,
factoryMethodPrefix
}: {
schemaPath: string;
schemas: GeneratorSchema[];
@@ -95,6 +151,9 @@ export const writeSchemas = async ({
specsName: Record<string, string>;
header: string;
indexFiles: boolean;
factoryMethodInclude: boolean;
factoryMethodOutput?: (typeof OutputModelFactoryMethodsMode)[keyof typeof OutputModelFactoryMethodsMode];
factoryMethodPrefix?: string;
}) => {
await Promise.all(
schemas.map((schema) =>
@@ -107,6 +166,9 @@ export const writeSchemas = async ({
isRootKey,
specsName,
header,
factoryMethodInclude,
factoryMethodOutput,
factoryMethodPrefix
}),
),
);
12 changes: 11 additions & 1 deletion packages/orval/src/utils/options.ts
Original file line number Diff line number Diff line change
@@ -78,7 +78,7 @@ export const normalizeOptions = async (
workspace,
);

const { clean, prettier, client, httpClient, mode, tslint, biome } =
const { clean, prettier, client, httpClient, modelFactoryMethods, mode, tslint, biome } =
globalOptions;

const tsconfig = await loadTsconfig(
@@ -143,6 +143,8 @@ export const normalizeOptions = async (
fileExtension: outputOptions.fileExtension || defaultFileExtension,
workspace: outputOptions.workspace ? outputWorkspace : undefined,
client: outputOptions.client ?? client ?? OutputClient.AXIOS_FUNCTIONS,
modelFactoryMethods:
outputOptions.modelFactoryMethods ?? modelFactoryMethods ?? false,
httpClient:
outputOptions.httpClient ?? httpClient ?? OutputHttpClient.AXIOS,
mode: normalizeOutputMode(outputOptions.mode ?? mode),
@@ -209,6 +211,14 @@ export const normalizeOptions = async (
? outputOptions.override?.header!
: getDefaultFilesHeader,
requestOptions: outputOptions.override?.requestOptions ?? true,
modelFactoryMethods: {
factoryMethodPrefix:
outputOptions.override?.modelFactoryMethods?.factoryMethodPrefix
?? 'new',
outputMode:
outputOptions.override?.modelFactoryMethods?.outputMode
?? 'split'
},
components: {
schemas: {
suffix: RefComponentSuffix.schemas,
3 changes: 3 additions & 0 deletions packages/orval/src/write-specs.ts
Original file line number Diff line number Diff line change
@@ -83,6 +83,9 @@ export const writeSpecs = async (
isRootKey: isRootKey(specKey, target),
header,
indexFiles: output.indexFiles,
factoryMethodInclude: output.modelFactoryMethods ?? false,
factoryMethodOutput: output.override?.modelFactoryMethods?.outputMode,
factoryMethodPrefix: output.override?.modelFactoryMethods?.factoryMethodPrefix
});
}),
);