Skip to content

Commit

Permalink
fix: top-level discriminator using enum (#494)
Browse files Browse the repository at this point in the history
* fix: #482 discriminator with enums

* fix: discriminator with enums #482

* fix: node 10
  • Loading branch information
cdimascio committed Dec 19, 2020
1 parent e71e1b9 commit 7de0485
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 28 deletions.
49 changes: 33 additions & 16 deletions src/middlewares/parsers/request.schema.preprocessor.ts
Expand Up @@ -7,6 +7,13 @@ type SchemaObject = OpenAPIV3.SchemaObject;
type ReferenceObject = OpenAPIV3.ReferenceObject;
type Schema = ReferenceObject | SchemaObject;

if (!Array.prototype['flatMap']) {
// polyfill flatMap
// TODO remove me when dropping node 10 support
Array.prototype['flatMap'] = function (lambda) {
return Array.prototype.concat.apply([], this.map(lambda));
};
}
const httpMethods = new Set([
'get',
'put',
Expand Down Expand Up @@ -122,14 +129,15 @@ export class RequestSchemaPreprocessor {

const xOf = schemaObj.oneOf ? 'oneOf' : 'anyOf';
if (schemaObj?.discriminator?.propertyName && !o.discriminator) {
// TODO discriminator can be used for anyOf too!
const options = schemaObj[xOf].map((refObject) => {
const option = this.findKey(
const options = schemaObj[xOf].flatMap((refObject) => {
const keys = this.findKeys(
schemaObj.discriminator.mapping,
(value) => value === refObject['$ref'],
);
const ref = this.getKeyFromRef(refObject['$ref']);
return { option: option || ref, ref };
return keys.length > 0
? keys.map((option) => ({ option, ref }))
: [{ option: ref, ref }];
});
o.options = options;
o.discriminator = schemaObj.discriminator?.propertyName;
Expand All @@ -145,13 +153,16 @@ export class RequestSchemaPreprocessor {
);
} else if (schemaObj) {
const ancestor: any = parent;
const option =
this.findKey(
ancestor.discriminator?.mapping,
(value) => value === schema['$ref'],
) || this.getKeyFromRef(schema['$ref']);
const options = this.findKeys(
ancestor.discriminator?.mapping,
(value) => value === schema['$ref'],
);
const ref = this.getKeyFromRef(schema['$ref']);
if (options.length === 0 && ref) {
options.push(ref);
}

if (option) {
if (options.length > 0) {
const newSchema = JSON.parse(JSON.stringify(schemaObj));
newSchema.properties = {
...(o.properties ?? {}),
Expand All @@ -166,9 +177,12 @@ export class RequestSchemaPreprocessor {
options: o.options,
property: o.discriminator,
};
ancestor._discriminator.validators[option] = this.ajv.compile(
newSchema,
);

for (const option of options) {
ancestor._discriminator.validators[option] = this.ajv.compile(
newSchema,
);
}
}
//reset data
o.properties = {};
Expand All @@ -194,17 +208,20 @@ export class RequestSchemaPreprocessor {
}
}

private findKey(object, searchFunc) {
private findKeys(object, searchFunc): string[] {
const matches = [];
if (!object) {
return;
return matches;
}
const keys = Object.keys(object);
for (let i = 0; i < keys.length; i++) {
if (searchFunc(object[keys[i]])) {
return keys[i];
matches.push(keys[i]);
}
}
return matches;
}

getKeyFromRef(ref) {
return ref.split('/components/schemas/')[1];
}
Expand Down
31 changes: 26 additions & 5 deletions test/one.of.2.spec.ts
Expand Up @@ -2,9 +2,8 @@ import * as path from 'path';
import { expect } from 'chai';
import * as request from 'supertest';
import { createApp } from './common/app';
import * as packageJson from '../package.json';

describe('one.of.2.spec', () => {
describe('oneOf with discriminator', () => {
let app = null;

before(async () => {
Expand Down Expand Up @@ -94,7 +93,7 @@ describe('one.of.2.spec', () => {
.expect(400)
.then((r) => {
const e = r.body;
expect(e.message).to.include('one of the allowed values: cat, dog');
expect(e.message).to.include('one of the allowed values: cat, kitty, dog, puppy');
}));

it('should return 200 for dog', async () =>
Expand All @@ -108,14 +107,36 @@ describe('one.of.2.spec', () => {
})
.expect(200));

it('should return 200 for puppy', async () =>
request(app)
.post(`${app.basePath}/pets`)
.set('content-type', 'application/json')
.send({
pet_type: 'puppy',
bark: true,
breed: 'Dingo',
})
.expect(200));

it('should return 200 for cat', async () =>
request(app)
.post(`${app.basePath}/pets`)
.set('content-type', 'application/json')
.send({
pet_type: 'cat',
hunts: true,
age: 3,
age: 1,
})
.expect(200));

it('should return 200 for kitty', async () =>
request(app)
.post(`${app.basePath}/pets`)
.set('content-type', 'application/json')
.send({
pet_type: 'kitty',
hunts: true,
age: 1,
})
.expect(200));
});
Expand Down Expand Up @@ -160,4 +181,4 @@ describe('one.of.2.spec', () => {
})
.expect(200));
});
});
});
20 changes: 15 additions & 5 deletions test/resources/one.of.2.yaml
@@ -1,7 +1,7 @@
openapi: '3.0.3'
info:
version: 1.0.0
title: Swagger
title: Swagger
servers:
- url: /v1
paths:
Expand Down Expand Up @@ -38,11 +38,13 @@ paths:
propertyName: pet_type
mapping:
cat: '#/components/schemas/CatObject'
kitty: '#/components/schemas/CatObject'
dog: '#/components/schemas/DogObject'
puppy: '#/components/schemas/DogObject'
responses:
'200':
description: Updated

/pets_all:
post:
description: Creates a new pet in the store.
Expand Down Expand Up @@ -80,6 +82,10 @@ components:
enum: [Dingo, Husky, Retriever, Shepherd]
pet_type:
type: string
# since we use an enum here
# add DogObject as an option
# so that the non-mapping / implied mapping tests can pass
enum: [dog, puppy, DogObject]

CatObject:
type: object
Expand All @@ -94,7 +100,11 @@ components:
type: integer
pet_type:
type: string

# since we use an enum here
# add CatObject as an option
# so that the non-mapping / implied mapping tests can pass
enum: [cat, kitty, CatObject]

Pet:
type: object
required:
Expand All @@ -106,7 +116,7 @@ components:
propertyName: pet_type

Dog: # "Dog" is a value for the pet_type property (the discriminator value)
allOf: # Combines the main `Pet` schema with `Dog`-specific properties
allOf: # Combines the main `Pet` schema with `Dog`-specific properties
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Dog`
Expand All @@ -117,7 +127,7 @@ components:
type: string
enum: [Dingo, Husky, Retriever, Shepherd]
Cat: # "Cat" is a value for the pet_type property (the discriminator value)
allOf: # Combines the main `Pet` schema with `Cat`-specific properties
allOf: # Combines the main `Pet` schema with `Cat`-specific properties
- $ref: '#/components/schemas/Pet'
- type: object
# all other properties specific to a `Cat`
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"declaration": true,
"target": "ES2017",
"lib": ["ES2017"],
"target": "es2017",
"lib": ["es2019", "es2019.array"],
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
Expand Down

0 comments on commit 7de0485

Please sign in to comment.