Skip to content

Commit

Permalink
Merge branch 'master' into O933
Browse files Browse the repository at this point in the history
  • Loading branch information
melloware authored Nov 15, 2023
2 parents 0cc2dd4 + 67856c2 commit 85a823c
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 13 deletions.
24 changes: 24 additions & 0 deletions docs/src/pages/reference/configuration/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,30 @@ module.exports = {
};
```

#### coerceTypes

Type: `Boolean`

Valid values: true or false. Defaults to false.

Use this property to enable [type coercion](https://zod.dev/?id=coercion-for-primitives) for [Zod](https://zod.dev/) schemas (only applies to query parameters schemas).

This is helpful if you want to use the zod schema to coerce (likely string-serialized) query parameters into the correct type before validation.

Example:

```js
module.exports = {
petstore: {
output: {
override: {
coerceTypes: true,
},
},
},
};
```

#### useNamedParameters

Type: `Boolean`.
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export type NormalizedOverrideOutput = {
) => string;
requestOptions: Record<string, any> | boolean;
useDates?: boolean;
coerceTypes?: boolean;
useTypeOverInterfaces?: boolean;
useDeprecatedOperations?: boolean;
useBigInt?: boolean;
Expand Down
3 changes: 2 additions & 1 deletion packages/zod/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"scripts": {
"build": "tsup ./src/index.ts --target node12 --clean --dts --sourcemap",
"dev": "tsup ./src/index.ts --target node12 --clean --watch src",
"lint": "eslint src/**/*.ts"
"lint": "eslint src/**/*.ts",
"test": "vitest --global test.ts"
},
"dependencies": {
"@orval/core": "6.20.0",
Expand Down
41 changes: 29 additions & 12 deletions packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => {
}
};

// counter for unique naming
let counter = 0;

// https://github.com/colinhacks/zod#coercion-for-primitives
const COERCEABLE_TYPES = ['string', 'number', 'boolean', 'bigint', 'date'];


const generateZodValidationSchemaDefinition = (
schema: SchemaObject | undefined,
_required: boolean | undefined,
Expand Down Expand Up @@ -90,13 +95,13 @@ const generateZodValidationSchemaDefinition = (
break;
}

functions.push([type as string, undefined]);

if (schema.format === 'date') {
functions.push(['regex', 'new RegExp(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/)']);
functions.push(['date', undefined]);
break;
}

functions.push([type as string, undefined]);

if (schema.format === 'date-time') {
functions.push(['datetime', undefined]);
break;
Expand Down Expand Up @@ -231,8 +236,14 @@ const generateZodValidationSchemaDefinition = (
return { functions, consts: uniq(consts) };
};

const parseZodValidationSchemaDefinition = (
input: Record<string, { functions: [string, any][]; consts: string[] }>,
export type ZodValidationSchemaDefinitionInput = Record<
string,
{ functions: [string, any][]; consts: string[] }
>;

export const parseZodValidationSchemaDefinition = (
input: ZodValidationSchemaDefinitionInput,
coerceTypes = false,
): { zod: string; consts: string } => {
if (!Object.keys(input).length) {
return { zod: '', consts: '' };
Expand Down Expand Up @@ -301,6 +312,11 @@ const parseZodValidationSchemaDefinition = (
}
return `.array(${value.startsWith('.') ? 'zod' : ''}${value})`;
}

if (coerceTypes && COERCEABLE_TYPES.includes(fn)) {
return `.coerce.${fn}(${args})`;
}

return `.${fn}(${args})`;
};

Expand All @@ -309,12 +325,12 @@ const parseZodValidationSchemaDefinition = (
}, '');

const zod = `zod.object({
${Object.entries(input)
.map(([key, schema]) => {
const value = schema.functions.map(parseProperty).join('');
return `"${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`;
})
.join(',')}
${Object.entries(input)
.map(([key, schema]) => {
const value = schema.functions.map(parseProperty).join('');
return ` "${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`;
})
.join(',\n')}
})`;

return { zod, consts };
Expand Down Expand Up @@ -359,7 +375,7 @@ const deference = (

const generateZodRoute = (
{ operationName, body, verb }: GeneratorVerbOptions,
{ pathRoute, context }: GeneratorOptions,
{ pathRoute, context, override }: GeneratorOptions,
) => {
const spec = context.specs[context.specKey].paths[pathRoute] as
| PathItemObject
Expand Down Expand Up @@ -493,6 +509,7 @@ const generateZodRoute = (
);
const inputQueryParams = parseZodValidationSchemaDefinition(
zodDefinitionsParameters.queryParams,
override.coerceTypes,
);
const inputHeaders = parseZodValidationSchemaDefinition(
zodDefinitionsParameters.headers,
Expand Down
53 changes: 53 additions & 0 deletions packages/zod/src/zod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest';
import {
type ZodValidationSchemaDefinitionInput,
parseZodValidationSchemaDefinition,
} from '.';

const queryParams: ZodValidationSchemaDefinitionInput = {
// limit = non-required integer schema (coerce-able)
limit: {
functions: [
['number', undefined],
['optional', undefined],
],
consts: [],
},

// q = non-required string array schema (not coerce-able)
q: {
functions: [
[
'array',
{
functions: [['string', undefined]],
consts: [],
},
],
['optional', undefined],
],
consts: [],
},
};

describe('parseZodValidationSchemaDefinition', () => {
describe('with `override.coerceTypes = false` (default)', () => {
it('does not emit coerced zod property schemas', () => {
const parseResult = parseZodValidationSchemaDefinition(queryParams);

expect(parseResult.zod).toBe(
'zod.object({\n "limit": zod.number().optional(),\n "q": zod.array(zod.string()).optional()\n})',
);
});
});

describe('with `override.coerceTypes = true`', () => {
it('emits coerced zod property schemas', () => {
const parseResult = parseZodValidationSchemaDefinition(queryParams, true);

expect(parseResult.zod).toBe(
'zod.object({\n "limit": zod.coerce.number().optional(),\n "q": zod.array(zod.coerce.string()).optional()\n})',
);
});
});
});
16 changes: 16 additions & 0 deletions tests/configs/zod-parameters.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { defineConfig } from 'orval';

export default defineConfig({
basic: {
output: {
target: '../generated/zod',
client: 'zod',
override: {
coerceTypes: true,
},
},
input: {
target: '../specifications/parameters.yaml',
},
},
});
1 change: 1 addition & 0 deletions tests/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"generate:swr": "yarn orval --config ./configs/swr.config.ts",
"generate:multi-file": "yarn orval --config ./configs/multi-file.config.ts",
"generate:zod": "yarn orval --config ./configs/zod.config.ts",
"generate:zod-parameters": "yarn orval --config ./configs/zod-parameters.config.ts",
"build": "tsc"
},
"author": "Victor Bury",
Expand Down
15 changes: 15 additions & 0 deletions tests/specifications/circular.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ paths:
maximum: 10
required:
- list
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: string
pattern: '^\+\d{10, 15}'
- name: birthdate
in: query
description: birth date
required: false
schema:
type: string
format: 'date'
components:
schemas:
Node:
Expand Down
44 changes: 44 additions & 0 deletions tests/specifications/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,56 @@ paths:
tags:
- pets
parameters:
- name: q
in: query
description: Filter pets by substring
required: false
schema:
type: array
items:
type: string
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
- name: offset
in: query
description: How many items to skip
required: false
schema:
type: integer
format: int32
- name: order
in: query
description: How to order the items
required: false
schema:
type: string
enum: [asc, desc]
- name: sort
in: query
description: |
Which property to sort by?
Example: name sorts ASC while -name sorts DESC.
required: false
schema:
type: string
enum:
- name
- -name
- age
- -age
- name: cnonly
in: query
description: |
Only return pets from China?
Example: true
required: false
schema:
type: boolean
responses:
'200':
description: A paged array of pets
Expand Down

0 comments on commit 85a823c

Please sign in to comment.