Skip to content

Commit

Permalink
feat(zod): add option to setup strict mode (#1303)
Browse files Browse the repository at this point in the history
* feat(zod): add option to setup strict mode

* fix(format): check

* fix(format): check

* fix(zod): test

* fix(format): check

* docs(zod): add output reference

* test(zod): add generate test strict mode

* test(zod): strict mode change generated file name

* fix(format): check
  • Loading branch information
anymaniax authored Apr 11, 2024
1 parent 6d002f5 commit a1fa843
Show file tree
Hide file tree
Showing 18 changed files with 361 additions and 202 deletions.
36 changes: 36 additions & 0 deletions docs/src/pages/reference/configuration/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,42 @@ module.exports = {
};
```

#### zod

Type: `Object`.

Give you specific options for the zod client

```js
module.exports = {
petstore: {
output: {
...
override: {
zod: {
strict: {
response: true,
query: true,
param: true,
header: true,
body: true
},
},
},
},
...
},
};
```

##### strict

Type: `Object`.

Default Value: `false`.

Use to set the strict mode for the zod schema. If you set it to true, the schema will be generated with the strict mode.

#### mock

Type: `Object`.
Expand Down
24 changes: 24 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export type NormalizedOverrideOutput = {
query: NormalizedQueryOptions;
angular: Required<AngularOptions>;
swr: SwrOptions;
zod: NormalizedZodOptions;
operationName?: (
operation: OperationObject,
route: string,
Expand Down Expand Up @@ -129,6 +130,7 @@ export type NormalizedOperationOptions = {
query?: NormalizedQueryOptions;
angular?: Required<AngularOptions>;
swr?: SwrOptions;
zod?: NormalizedZodOptions;
operationName?: (
operation: OperationObject,
route: string,
Expand Down Expand Up @@ -329,6 +331,7 @@ export type OverrideOutput = {
query?: QueryOptions;
swr?: SwrOptions;
angular?: AngularOptions;
zod?: ZodOptions;
operationName?: (
operation: OperationObject,
route: string,
Expand All @@ -352,6 +355,26 @@ export type NormalizedHonoOptions = {
handlers?: string;
};

export type ZodOptions = {
strict?: {
param?: boolean;
query?: boolean;
header?: boolean;
body?: boolean;
response?: boolean;
};
};

export type NormalizedZodOptions = {
strict: {
param: boolean;
query: boolean;
header: boolean;
body: boolean;
response: boolean;
};
};

export type HonoOptions = {
handlers?: string;
};
Expand Down Expand Up @@ -419,6 +442,7 @@ export type OperationOptions = {
query?: QueryOptions;
angular?: Required<AngularOptions>;
swr?: SwrOptions;
zod?: ZodOptions;
operationName?: (
operation: OperationObject,
route: string,
Expand Down
19 changes: 11 additions & 8 deletions packages/hono/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,14 +199,17 @@ const getHandlerFix = ({
const getVerbOptionGroupByTag = (
verbOptions: Record<string, GeneratorVerbOptions>,
) => {
return Object.values(verbOptions).reduce((acc, value) => {
const tag = value.tags[0];
if (!acc[tag]) {
acc[tag] = [];
}
acc[tag].push(value);
return acc;
}, {} as Record<string, GeneratorVerbOptions[]>);
return Object.values(verbOptions).reduce(
(acc, value) => {
const tag = value.tags[0];
if (!acc[tag]) {
acc[tag] = [];
}
acc[tag].push(value);
return acc;
},
{} as Record<string, GeneratorVerbOptions[]>,
);
};

const generateHandlers = async (
Expand Down
23 changes: 23 additions & 0 deletions packages/orval/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ export const normalizeOptions = async (
shouldExportMutatorHooks: true,
...normalizeQueryOptions(outputOptions.override?.query, workspace),
},
zod: {
strict: {
param: outputOptions.override?.zod?.strict?.param ?? false,
query: outputOptions.override?.zod?.strict?.query ?? false,
header: outputOptions.override?.zod?.strict?.header ?? false,
body: outputOptions.override?.zod?.strict?.body ?? false,
response: outputOptions.override?.zod?.strict?.response ?? false,
},
},
swr: {
...(outputOptions.override?.swr ?? {}),
},
Expand Down Expand Up @@ -310,6 +319,7 @@ const normalizeOperationsAndTags = (
formUrlEncoded,
paramsSerializer,
query,
zod,
...rest
},
]) => {
Expand All @@ -322,6 +332,19 @@ const normalizeOperationsAndTags = (
query: normalizeQueryOptions(query, workspace),
}
: {}),
...(zod
? {
zod: {
strict: {
param: zod.strict?.param ?? false,
query: zod.strict?.query ?? false,
header: zod.strict?.header ?? false,
body: zod.strict?.body ?? false,
response: zod.strict?.response ?? false,
},
},
}
: {}),
...(transformer
? { transformer: normalizePath(transformer, workspace) }
: {}),
Expand Down
42 changes: 36 additions & 6 deletions packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const generateZodValidationSchemaDefinition = (
schema: SchemaObject | undefined,
_required: boolean | undefined,
name: string,
strict: boolean,
): { functions: [string, any][]; consts: string[] } => {
if (!schema) return { functions: [], consts: [] };

Expand Down Expand Up @@ -84,7 +85,7 @@ const generateZodValidationSchemaDefinition = (
const items = schema.items as SchemaObject | undefined;
functions.push([
'array',
generateZodValidationSchemaDefinition(items, true, camel(name)),
generateZodValidationSchemaDefinition(items, true, camel(name), strict),
]);
break;
case 'string': {
Expand Down Expand Up @@ -142,6 +143,7 @@ const generateZodValidationSchemaDefinition = (
schema as SchemaObject,
true,
camel(name),
strict,
),
),
]);
Expand All @@ -157,11 +159,16 @@ const generateZodValidationSchemaDefinition = (
schema.properties?.[key] as any,
schema.required?.includes(key),
camel(`${name}-${key}`),
strict,
),
}))
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
]);

if (strict) {
functions.push(['strict', undefined]);
}

break;
}

Expand All @@ -174,13 +181,15 @@ const generateZodValidationSchemaDefinition = (
schema.additionalProperties as SchemaObject,
true,
name,
strict,
),
]);

break;
}

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

break;
}
}
Expand Down Expand Up @@ -240,6 +249,7 @@ export type ZodValidationSchemaDefinitionInput = Record<

export const parseZodValidationSchemaDefinition = (
input: ZodValidationSchemaDefinitionInput,
strict: boolean,
coerceTypes = false,
): { zod: string; consts: string } => {
if (!Object.keys(input).length) {
Expand Down Expand Up @@ -296,7 +306,7 @@ export const parseZodValidationSchemaDefinition = (
}

if (fn === 'object') {
const parsed = parseZodValidationSchemaDefinition(args);
const parsed = parseZodValidationSchemaDefinition(args, strict);
consts += parsed.consts;
return ` ${parsed.zod}`;
}
Expand All @@ -310,6 +320,10 @@ export const parseZodValidationSchemaDefinition = (
return `.array(${value.startsWith('.') ? 'zod' : ''}${value})`;
}

if (fn === 'strict') {
return '.strict()';
}

if (coerceTypes && COERCEABLE_TYPES.includes(fn)) {
return `.coerce.${fn}(${args})`;
}
Expand All @@ -328,7 +342,7 @@ ${Object.entries(input)
return ` "${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`;
})
.join(',\n')}
})`;
})${strict ? '.strict()' : ''}`;

return { zod, consts };
};
Expand Down Expand Up @@ -371,8 +385,8 @@ const deference = (
};

const generateZodRoute = (
{ operationName, verb }: GeneratorVerbOptions,
{ pathRoute, context, override }: GeneratorOptions,
{ operationName, verb, override }: GeneratorVerbOptions,
{ pathRoute, context }: GeneratorOptions,
) => {
const spec = context.specs[context.specKey].paths[pathRoute] as
| PathItemObject
Expand Down Expand Up @@ -414,6 +428,7 @@ const generateZodRoute = (
(requiredKey: string) => requiredKey === key,
),
camel(`${operationName}-response-${key}`),
override.zod.strict.response,
),
};
})
Expand Down Expand Up @@ -447,6 +462,7 @@ const generateZodRoute = (
(requiredKey: string) => requiredKey === key,
),
camel(`${operationName}-body-${key}`),
override.zod.strict.body,
),
};
})
Expand All @@ -462,10 +478,17 @@ const generateZodRoute = (

const schema = deference(parameter.schema, context);

const strict = {
path: override.zod.strict.param,
query: override.zod.strict.query,
header: override.zod.strict.header,
};

const definition = generateZodValidationSchemaDefinition(
schema,
parameter.required,
camel(`${operationName}-${parameter.in}-${parameter.name}`),
strict[parameter.in as 'path' | 'query' | 'header'] ?? false,
);

if (parameter.in === 'header') {
Expand Down Expand Up @@ -503,17 +526,24 @@ const generateZodRoute = (

const inputParams = parseZodValidationSchemaDefinition(
zodDefinitionsParameters.params,
override.zod.strict.param,
);
const inputQueryParams = parseZodValidationSchemaDefinition(
zodDefinitionsParameters.queryParams,
override.zod.strict.query,
override.coerceTypes,
);
const inputHeaders = parseZodValidationSchemaDefinition(
zodDefinitionsParameters.headers,
override.zod.strict.header,
);
const inputBody = parseZodValidationSchemaDefinition(
zodDefinitionsBody,
override.zod.strict.body,
);
const inputBody = parseZodValidationSchemaDefinition(zodDefinitionsBody);
const inputResponse = parseZodValidationSchemaDefinition(
zodDefinitionsResponse,
override.zod.strict.response,
);

if (
Expand Down
22 changes: 20 additions & 2 deletions packages/zod/src/zod.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ const queryParams: ZodValidationSchemaDefinitionInput = {
describe('parseZodValidationSchemaDefinition', () => {
describe('with `override.coerceTypes = false` (default)', () => {
it('does not emit coerced zod property schemas', () => {
const parseResult = parseZodValidationSchemaDefinition(queryParams);
const parseResult = parseZodValidationSchemaDefinition(
queryParams,
false,
false,
);

expect(parseResult.zod).toBe(
'zod.object({\n "limit": zod.number().optional().null(),\n "q": zod.array(zod.string()).optional()\n})',
Expand All @@ -44,11 +48,25 @@ describe('parseZodValidationSchemaDefinition', () => {

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

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

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

expect(parseResult.zod).toBe(
'zod.object({\n "limit": zod.number().optional().null(),\n "q": zod.array(zod.string()).optional()\n}).strict()',
);
});
});
});
5 changes: 5 additions & 0 deletions samples/hono/orval.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ export default defineConfig({
hono: {
handlers: 'src/handlers',
},
zod: {
strict: {
response: true,
},
},
},
},
input: {
Expand Down
3 changes: 2 additions & 1 deletion samples/hono/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
},
"dependencies": {
"@hono/zod-validator": "^0.2.0",
"hono": "^4.0.4"
"hono": "^4.0.4",
"zod": "^3.22.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
Expand Down
Loading

0 comments on commit a1fa843

Please sign in to comment.