Skip to content

Commit

Permalink
feat: hide definitions purposal
Browse files Browse the repository at this point in the history
  • Loading branch information
Thibault LR committed Aug 2, 2023
1 parent 6d910a1 commit 469a22a
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 36 deletions.
77 changes: 77 additions & 0 deletions packages/zod-openapi/src/lib/zod-openapi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,83 @@ describe('zodOpenapi', () => {
});
});

describe('Regarding omitted schema definitions', () => {
it('supports schema hideDefinitions properties', () => {
const zodSchema = extendApi(
z
.object({
aArrayMin: z.array(z.string()).min(3),
aArrayMax: z.array(z.number()).max(8),
aArrayLength: z.array(z.boolean()).length(10),
})
.partial(),
{
description: 'I need arrays',
hideDefinitions: ['aArrayLength'],
}
);

const apiSchema = generateSchema(zodSchema);

expect(apiSchema).toEqual({
type: 'object',
properties: {
aArrayMin: {
type: 'array',
minItems: 3,
items: { type: 'string' },
},
aArrayMax: {
type: 'array',
maxItems: 8,
items: { type: 'number' },
},
},
"hideDefinitions": ["aArrayLength"],
description: 'I need arrays',
});
});

it('supports hideDefinitions on nested schemas', () => {
const zodNestedSchema = extendApi(
z.strictObject({
aString: z.string(),
aNumber: z.number().optional()
}),
{
hideDefinitions: ['aNumber']
}
)
const zodSchema = extendApi(
z.object({ data: zodNestedSchema }).partial(),
{
description: 'I need arrays',
}
);
const apiSchema = generateSchema(zodSchema);
expect(apiSchema).toEqual({
"description": "I need arrays",
"properties": {
"data": {
"additionalProperties": false,
"properties": {
"aString": {
"type": "string",
},
},
"hideDefinitions": ["aNumber"],
"required": [
"aString",
],
"type": "object",
},
},
"type": "object",
}
);
});
});

describe('record support', () => {
describe('with a value type', () => {
it('adds the value type to additionalProperties', () => {
Expand Down
103 changes: 67 additions & 36 deletions packages/zod-openapi/src/lib/zod-openapi.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,48 @@
import type {SchemaObject, SchemaObjectType} from 'openapi3-ts/oas31';
import type { SchemaObject, SchemaObjectType } from 'openapi3-ts/oas31';
import merge from 'ts-deepmerge';
import {AnyZodObject, z, ZodTypeAny} from 'zod';
import { AnyZodObject, z, ZodTypeAny } from 'zod';

type AnatineSchemaObject = SchemaObject & { hideDefinitions?: string[] };

export interface OpenApiZodAny extends ZodTypeAny {
metaOpenApi?: SchemaObject | SchemaObject[];
metaOpenApi?: AnatineSchemaObject | AnatineSchemaObject[];
}

interface OpenApiZodAnyObject extends AnyZodObject {
metaOpenApi?: SchemaObject | SchemaObject[];
metaOpenApi?: AnatineSchemaObject | AnatineSchemaObject[];
}

interface ParsingArgs<T> {
zodRef: T;
schemas: SchemaObject[];
schemas: AnatineSchemaObject[];
useOutput?: boolean;
hideDefinitions?: string[];
}

export function extendApi<T extends OpenApiZodAny>(
schema: T,
SchemaObject: SchemaObject = {}
schemaObject: AnatineSchemaObject = {}
): T {
schema.metaOpenApi = Object.assign(schema.metaOpenApi || {}, SchemaObject);
schema.metaOpenApi = Object.assign(schema.metaOpenApi || {}, schemaObject);
return schema;
}

function iterateZodObject({
zodRef,
useOutput,
hideDefinitions,
}: ParsingArgs<OpenApiZodAnyObject>) {
return Object.keys(zodRef.shape).reduce(
(carry, key) => ({
...carry,
[key]: generateSchema(zodRef.shape[key], useOutput),
}),
{} as Record<string, SchemaObject>
);
const reduced = Object.keys(zodRef.shape)
.filter((key) => hideDefinitions?.includes(key) === false)
.reduce(
(carry, key) => ({
...carry,
[key]: generateSchema(zodRef.shape[key], useOutput),
}),
{} as Record<string, SchemaObject>
);

return reduced;
}

function parseTransformation({
Expand All @@ -54,16 +62,16 @@ function parseTransformation({
['integer', 'number'].includes(`${input.type}`)
? 0
: 'string' === input.type
? ''
: 'boolean' === input.type
? false
: 'object' === input.type
? {}
: 'null' === input.type
? null
: 'array' === input.type
? []
: undefined,
? ''
: 'boolean' === input.type
? false
: 'object' === input.type
? {}
: 'null' === input.type
? null
: 'array' === input.type
? []
: undefined,
{ addIssue: () => undefined, path: [] } // TODO: Discover if context is necessary here
);
} catch (e) {
Expand All @@ -77,8 +85,8 @@ function parseTransformation({
...input,
...(['number', 'string', 'boolean', 'null'].includes(output)
? {
type: output as 'number' | 'string' | 'boolean' | 'null',
}
type: output as 'number' | 'string' | 'boolean' | 'null',
}
: {}),
},
...schemas
Expand Down Expand Up @@ -165,10 +173,26 @@ function parseNumber({
);
}



function getExcludedDefinitionsFromSchema(schemas: AnatineSchemaObject[]): string[] {


const excludedDefinitions = [];
for (const schema of schemas) {
if (Array.isArray(schema.hideDefinitions)) {
excludedDefinitions.push(...schema.hideDefinitions)
}
}

return excludedDefinitions
}

function parseObject({
zodRef,
schemas,
useOutput,
hideDefinitions,
}: ParsingArgs<
z.ZodObject<never, 'passthrough' | 'strict' | 'strip'>
>): SchemaObject {
Expand Down Expand Up @@ -213,11 +237,13 @@ function parseObject({
zodRef: zodRef as OpenApiZodAnyObject,
schemas,
useOutput,
hideDefinitions: getExcludedDefinitionsFromSchema(schemas),
}),
...required,
...additionalProperties,
...hideDefinitions
},
zodRef.description ? {description: zodRef.description} : {},
zodRef.description ? { description: zodRef.description, hideDefinitions } : {},
...schemas
);
}
Expand Down Expand Up @@ -389,22 +415,27 @@ function parseUnion({
useOutput,
}: ParsingArgs<z.ZodUnion<[z.ZodTypeAny, ...z.ZodTypeAny[]]>>): SchemaObject {
const contents = zodRef._def.options;
if (contents.reduce((prev, content) => prev && content._def.typeName === 'ZodLiteral', true)) {
if (
contents.reduce(
(prev, content) => prev && content._def.typeName === 'ZodLiteral',
true
)
) {
// special case to transform unions of literals into enums
const literals = contents as unknown as z.ZodLiteral<OpenApiZodAny>[];
const type = literals
.reduce((prev, content) =>
!prev || prev === typeof content._def.value ?
typeof content._def.value :
null,
null as null | string
);
const type = literals.reduce(
(prev, content) =>
!prev || prev === typeof content._def.value
? typeof content._def.value
: null,
null as null | string
);

if (type) {
return merge(
{
type: type as 'string' | 'number' | 'boolean',
enum: literals.map((literal) => literal._def.value)
enum: literals.map((literal) => literal._def.value),
},
zodRef.description ? { description: zodRef.description } : {},
...schemas
Expand Down

0 comments on commit 469a22a

Please sign in to comment.