Skip to content
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

feat(zod): add option to setup strict mode #1303

Merged
merged 9 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
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?: {
anymaniax marked this conversation as resolved.
Show resolved Hide resolved
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also updating the unit tests, but at the same time, I thought it would be a good idea to add a test that the options added this time work using the automatically generated source code of zod in the tests directory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure to understand. I added a test for the new option too. And you want to test the generated files also right? If so indeed would be great

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is a good idea to continuously test that the automatically generated source code does not break when the options added this time are specified. And it's now in the tests directory.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test with the option

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

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
12 changes: 4 additions & 8 deletions samples/hono/src/handlers/createPets.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { createFactory } from 'hono/factory';
import { zValidator } from '../petstore.validator';
import { CreatePetsContext } from '../petstore.context';
import { createPetsBody,
createPetsResponse } from '../petstore.zod';
import { createPetsBody, createPetsResponse } from '../petstore.zod';

const factory = createFactory();


export const createPetsHandlers = factory.createHandlers(
zValidator('json', createPetsBody),
zValidator('response', createPetsResponse),
(c: CreatePetsContext) => {

},
zValidator('json', createPetsBody),
zValidator('response', createPetsResponse),
(c: CreatePetsContext) => {},
);
12 changes: 4 additions & 8 deletions samples/hono/src/handlers/listPets.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { createFactory } from 'hono/factory';
import { zValidator } from '../petstore.validator';
import { ListPetsContext } from '../petstore.context';
import { listPetsQueryParams,
listPetsResponse } from '../petstore.zod';
import { listPetsQueryParams, listPetsResponse } from '../petstore.zod';

const factory = createFactory();


export const listPetsHandlers = factory.createHandlers(
zValidator('query', listPetsQueryParams),
zValidator('response', listPetsResponse),
(c: ListPetsContext) => {

},
zValidator('query', listPetsQueryParams),
zValidator('response', listPetsResponse),
(c: ListPetsContext) => {},
);
Loading