Skip to content

Commit

Permalink
Merge pull request #30 from asteasolutions/task/#29-add-support-for-e…
Browse files Browse the repository at this point in the history
…xtended-schemas

Task/#29 add support for extended schemas
  • Loading branch information
AGalabov authored Sep 15, 2022
2 parents 11ccbc4 + b0e8502 commit 5fc40ed
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 12 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,8 @@ The list of all supported types as of now is:
- `ZodRecord`
- `ZodUnknown`

Extending an instance of `ZodObject` is also supported and results in an OpenApi definition with `allOf`

### Unsupported types

In case you try to create an OpenAPI schema from a zod schema that is not one of the aforementioned types then you'd receive an `UnknownZodTypeError`.
Expand Down
38 changes: 38 additions & 0 deletions spec/lodash.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { objectEquals } from '../src/lib/lodash';

describe('Lodash', () => {
describe('objectEquals', () => {
it('can compare plain values', () => {
expect(objectEquals(3, 4)).toEqual(false);
expect(objectEquals(3, 3)).toEqual(true);

expect(objectEquals('3', '4')).toEqual(false);
expect(objectEquals('3', '3')).toEqual(true);
});

it('can compare objects', () => {
expect(objectEquals({ a: 3 }, { b: 3 })).toEqual(false);
expect(objectEquals({ a: 3 }, { a: '3' })).toEqual(false);

expect(objectEquals({ a: 3 }, { a: 3, b: false })).toEqual(false);

expect(objectEquals({ a: 3 }, { a: 3 })).toEqual(true);
});

it('can compare nested objects', () => {
expect(
objectEquals(
{ test: { a: ['asd', 3, true] } },
{ test: { a: ['asd', 3, true, { b: null }] } }
)
).toEqual(false);

expect(
objectEquals(
{ test: { a: ['asd', 3, true, { b: null }] } },
{ test: { a: ['asd', 3, true, { b: null }] } }
)
).toEqual(true);
});
});
});
141 changes: 141 additions & 0 deletions spec/polymorphism.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as z from 'zod';
import { extendZodWithOpenApi } from '../src/zod-extensions';
import { expectSchema } from './lib/helpers';

// TODO: setupTests.ts
extendZodWithOpenApi(z);

describe('Polymorphism', () => {
it('can use allOf for extended schemas', () => {
const BaseSchema = z.object({ id: z.string() }).openapi({
refId: 'Base',
});

const ExtendedSchema = BaseSchema.extend({
bonus: z.number(),
}).openapi({
refId: 'Extended',
});

expectSchema([BaseSchema, ExtendedSchema], {
Base: {
type: 'object',
required: ['id'],
properties: {
id: {
type: 'string',
},
},
},
Extended: {
allOf: [
{ $ref: '#/components/schemas/Base' },
{
type: 'object',
required: ['bonus'],
properties: {
bonus: {
type: 'number',
},
},
},
],
},
});
});

it('can apply nullable', () => {
const BaseSchema = z.object({ id: z.ostring() }).openapi({
refId: 'Base',
});

const ExtendedSchema = BaseSchema.extend({
bonus: z.onumber(),
})
.nullable()
.openapi({
refId: 'Extended',
});

expectSchema([BaseSchema, ExtendedSchema], {
Base: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
Extended: {
allOf: [
{ $ref: '#/components/schemas/Base' },
{
type: 'object',
properties: {
bonus: {
type: 'number',
},
},
nullable: true,
},
],
},
});
});

it('can override properties', () => {
const AnimalSchema = z
.object({
name: z.ostring(),
type: z.enum(['dog', 'cat']).optional(),
})
.openapi({
refId: 'Animal',
discriminator: {
propertyName: 'type',
},
});

const DogSchema = AnimalSchema.extend({
type: z.string().openapi({ const: 'dog' }),
}).openapi({
refId: 'Dog',
discriminator: {
propertyName: 'type',
},
});

expectSchema([AnimalSchema, DogSchema], {
Animal: {
discriminator: {
propertyName: 'type',
},
type: 'object',
properties: {
name: {
type: 'string',
},
type: { type: 'string', enum: ['dog', 'cat'] },
},
},
Dog: {
discriminator: {
propertyName: 'type',
},
allOf: [
{ $ref: '#/components/schemas/Animal' },
{
type: 'object',
properties: {
type: {
type: 'string',
const: 'dog',
},
},
required: ['type'],
},
],
},
});
});
});
54 changes: 54 additions & 0 deletions spec/simple.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,60 @@ describe('Simple', () => {
});
});

it('supports nullable for registered schemas', () => {
const StringSchema = z.string().openapi({ refId: 'String' });

const TestSchema = z
.object({ key: StringSchema.nullable() })
.openapi({ refId: 'Test' });

expectSchema([StringSchema, TestSchema], {
String: {
type: 'string',
},
Test: {
type: 'object',
properties: {
key: {
allOf: [
{ $ref: '#/components/schemas/String' },
{ nullable: true },
],
},
},
required: ['key'],
},
});
});

it('supports .openapi for registered schemas', () => {
const StringSchema = z.string().openapi({ refId: 'String' });

const TestSchema = z
.object({
key: StringSchema.openapi({ example: 'test', deprecated: true }),
})
.openapi({ refId: 'Test' });

expectSchema([StringSchema, TestSchema], {
String: {
type: 'string',
},
Test: {
type: 'object',
properties: {
key: {
allOf: [
{ $ref: '#/components/schemas/String' },
{ example: 'test', deprecated: true },
],
},
},
required: ['key'],
},
});
});

describe('defaults', () => {
it('supports defaults', () => {
expectSchema(
Expand Down
32 changes: 32 additions & 0 deletions src/lib/lodash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,35 @@ export function omitBy<
export function compact<T extends any>(arr: (T | null | undefined)[]) {
return arr.filter((elem): elem is T => !isNil(elem));
}

export function objectEquals(x: any, y: any): boolean {
if (x === null || x === undefined || y === null || y === undefined) {
return x === y;
}

if (x === y || x.valueOf() === y.valueOf()) {
return true;
}

if (Array.isArray(x)) {
if (!Array.isArray(y)) {
return false;
}

if (x.length !== y.length) {
return false;
}
}

// if they are strictly equal, they both need to be object at least
if (!(x instanceof Object) || !(y instanceof Object)) {
return false;
}

// recursive object equality check
const keysX = Object.keys(x);
return (
Object.keys(y).every(keyY => keysX.indexOf(keyY) !== -1) &&
keysX.every(key => objectEquals(x[key], y[key]))
);
}
Loading

0 comments on commit 5fc40ed

Please sign in to comment.