Skip to content

Commit

Permalink
feat(core): property sorting (#1763)
Browse files Browse the repository at this point in the history
* feat(core): property sorting

* fix: specification is default sorting
  • Loading branch information
AllieJonsson authored Dec 23, 2024
1 parent 7c36284 commit c0eba70
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 89 deletions.
17 changes: 17 additions & 0 deletions docs/src/pages/reference/configuration/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -2082,3 +2082,20 @@ module.exports = {
},
};
```

### propertySortOrder

Type: `Alphabetical` | `Specification`

Default Value: `Specification`
This enables you to specify how properties in the models are sorted, either alphabetically or in the order they appear in the specification.

```js
module.exports = {
petstore: {
output: {
propertySortOrder: 'Alphabetical',
},
},
};
```
175 changes: 89 additions & 86 deletions packages/core/src/getters/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30';
import { resolveExampleRefs, resolveObject, resolveValue } from '../resolvers';
import {
ContextSpecs,
PropertySortOrder,
ScalarValue,
SchemaType,
SchemaWithConst,
Expand Down Expand Up @@ -70,98 +71,100 @@ export const getObject = ({
}

if (item.properties && Object.entries(item.properties).length > 0) {
return Object.entries(item.properties)
.sort((a, b) => {
const entries = Object.entries(item.properties);
if (context.output.propertySortOrder === PropertySortOrder.ALPHABETICAL) {
entries.sort((a, b) => {
return a[0].localeCompare(b[0]);
})
.reduce(
(
acc,
[key, schema]: [string, ReferenceObject | SchemaObject],
index,
arr,
) => {
const isRequired = (
Array.isArray(item.required) ? item.required : []
).includes(key);

let propName = '';

if (name) {
const isKeyStartWithUnderscore = key.startsWith('_');

propName += pascal(
`${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`,
);
}

const allSpecSchemas =
context.specs[context.target]?.components?.schemas ?? {};

const isNameAlreadyTaken = Object.keys(allSpecSchemas).some(
(schemaName) => pascal(schemaName) === propName,
});
}
return entries.reduce(
(
acc,
[key, schema]: [string, ReferenceObject | SchemaObject],
index,
arr,
) => {
const isRequired = (
Array.isArray(item.required) ? item.required : []
).includes(key);

let propName = '';

if (name) {
const isKeyStartWithUnderscore = key.startsWith('_');

propName += pascal(
`${isKeyStartWithUnderscore ? '_' : ''}${name}_${key}`,
);

if (isNameAlreadyTaken) {
propName = propName + 'Property';
}

const resolvedValue = resolveObject({
schema,
propName,
context,
});

const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
if (!index) {
acc.value += '{';
}

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

acc.hasReadonlyProps ||= isReadOnly || false;
acc.imports.push(...resolvedValue.imports);
acc.value += `\n ${doc ? `${doc} ` : ''}${
isReadOnly && !context.output.override.suppressReadonlyModifier
? 'readonly '
: ''
}${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`;
acc.schemas.push(...resolvedValue.schemas);

if (arr.length - 1 === index) {
if (item.additionalProperties) {
if (isBoolean(item.additionalProperties)) {
acc.value += `\n [key: string]: unknown;\n }`;
} else {
const resolvedValue = resolveValue({
schema: item.additionalProperties,
name,
context,
});
acc.value += `\n [key: string]: ${resolvedValue.value};\n}`;
}
}

const allSpecSchemas =
context.specs[context.target]?.components?.schemas ?? {};

const isNameAlreadyTaken = Object.keys(allSpecSchemas).some(
(schemaName) => pascal(schemaName) === propName,
);

if (isNameAlreadyTaken) {
propName = propName + 'Property';
}

const resolvedValue = resolveObject({
schema,
propName,
context,
});

const isReadOnly = item.readOnly || (schema as SchemaObject).readOnly;
if (!index) {
acc.value += '{';
}

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

acc.hasReadonlyProps ||= isReadOnly || false;
acc.imports.push(...resolvedValue.imports);
acc.value += `\n ${doc ? `${doc} ` : ''}${
isReadOnly && !context.output.override.suppressReadonlyModifier
? 'readonly '
: ''
}${getKey(key)}${isRequired ? '' : '?'}: ${resolvedValue.value};`;
acc.schemas.push(...resolvedValue.schemas);

if (arr.length - 1 === index) {
if (item.additionalProperties) {
if (isBoolean(item.additionalProperties)) {
acc.value += `\n [key: string]: unknown;\n }`;
} else {
acc.value += '\n}';
const resolvedValue = resolveValue({
schema: item.additionalProperties,
name,
context,
});
acc.value += `\n [key: string]: ${resolvedValue.value};\n}`;
}

acc.value += nullable;
} else {
acc.value += '\n}';
}

return acc;
},
{
imports: [],
schemas: [],
value: '',
isEnum: false,
type: 'object' as SchemaType,
isRef: false,
schema: {},
hasReadonlyProps: false,
example: item.example,
examples: resolveExampleRefs(item.examples, context),
} as ScalarValue,
);
acc.value += nullable;
}

return acc;
},
{
imports: [],
schemas: [],
value: '',
isEnum: false,
type: 'object' as SchemaType,
isRef: false,
schema: {},
hasReadonlyProps: false,
example: item.example,
examples: resolveExampleRefs(item.examples, context),
} as ScalarValue,
);
}

if (item.additionalProperties) {
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type NormalizedOutputOptions = {
urlEncodeParameters: boolean;
unionAddMissingProperties: boolean;
optionsParamRequired: boolean;
propertySortOrder: PropertySortOrder;
};

export type NormalizedParamsSerializerOptions = {
Expand Down Expand Up @@ -180,6 +181,14 @@ export type BaseUrlFromConstant = {
baseUrl: string;
};

export const PropertySortOrder = {
ALPHABETICAL: 'Alphabetical',
SPECIFICATION: 'Specification',
} as const;

export type PropertySortOrder =
(typeof PropertySortOrder)[keyof typeof PropertySortOrder];

export type OutputOptions = {
workspace?: string;
target?: string;
Expand All @@ -205,6 +214,7 @@ export type OutputOptions = {
urlEncodeParameters?: boolean;
unionAddMissingProperties?: boolean;
optionsParamRequired?: boolean;
propertySortOrder?: PropertySortOrder;
};

export type SwaggerParserOptions = Omit<SwaggerParser.Options, 'validate'> & {
Expand Down
10 changes: 7 additions & 3 deletions packages/mock/src/faker/getters/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
isReference,
MockOptions,
pascal,
PropertySorting,
} from '@orval/core';
import { ReferenceObject, SchemaObject } from 'openapi3-ts/oas30';
import { resolveMockValue } from '../resolvers/value';
Expand Down Expand Up @@ -105,10 +106,13 @@ export const getMockObject = ({
let imports: GeneratorImport[] = [];
let includedProperties: string[] = [];

const properyScalars = Object.entries(item.properties)
.sort((a, b) => {
const entries = Object.entries(item.properties);
if (context.output.propertySortOrder === PropertySorting.Alphabetical) {
entries.sort((a, b) => {
return a[0].localeCompare(b[0]);
})
});
}
const properyScalars = entries
.map(([key, prop]: [string, ReferenceObject | SchemaObject]) => {
if (combine?.includedProperties.includes(key)) {
return undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/orval/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
OutputClient,
OutputHttpClient,
OutputMode,
PropertySortOrder,
QueryOptions,
RefComponentSuffix,
SwaggerParserOptions,
Expand Down Expand Up @@ -320,6 +321,8 @@ export const normalizeOptions = async (
allParamsOptional: outputOptions.allParamsOptional ?? false,
urlEncodeParameters: outputOptions.urlEncodeParameters ?? false,
optionsParamRequired: outputOptions.optionsParamRequired ?? false,
propertySortOrder:
outputOptions.propertySortOrder ?? PropertySortOrder.SPECIFICATION,
},
hooks: options.hooks ? normalizeHooks(options.hooks) : {},
};
Expand Down

0 comments on commit c0eba70

Please sign in to comment.