Skip to content

Commit 0c4fe03

Browse files
nt0tskyn.totskii
andauthored
Add deep discriminator support (#1106)
* Added deep discriminator support * fixed typo * fixed suggestions --------- Co-authored-by: n.totskii <[email protected]>
1 parent e76e354 commit 0c4fe03

File tree

5 files changed

+478
-2
lines changed

5 files changed

+478
-2
lines changed

src/framework/ajv/options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class AjvOptions {
3030
}
3131

3232
get request(): RequestValidatorOptions {
33-
const { allErrors, allowUnknownQueryParameters, coerceTypes, removeAdditional } = <
33+
const { allErrors, allowUnknownQueryParameters, coerceTypes, removeAdditional, discriminator } = <
3434
ValidateRequestOpts
3535
>this.options.validateRequests;
3636
return {
@@ -39,6 +39,7 @@ export class AjvOptions {
3939
allowUnknownQueryParameters,
4040
coerceTypes,
4141
removeAdditional,
42+
discriminator,
4243
};
4344
}
4445

src/framework/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import AjvDraft4 from 'ajv-draft-04';
77
import Ajv2020 from 'ajv/dist/2020';
88
export { OpenAPIFrameworkArgs };
99

10-
export type AjvInstance = AjvDraft4 | Ajv2020
10+
export type AjvInstance = AjvDraft4 | Ajv2020
1111

1212
export type BodySchema =
1313
| OpenAPIV3.ReferenceObject
@@ -61,6 +61,7 @@ export type ValidateRequestOpts = {
6161
allowUnknownQueryParameters?: boolean;
6262
coerceTypes?: boolean | 'array';
6363
removeAdditional?: boolean | 'all' | 'failing';
64+
discriminator?: boolean;
6465
};
6566

6667
export type ValidateResponseOpts = {

test/ajv.options.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,34 @@ describe('AjvOptions', () => {
121121
expect(options.serDesMap['custom-1']).has.property('serialize');
122122
expect(options.serDesMap['custom-1']).has.property('deserialize');
123123
});
124+
125+
it('should handle discriminator parameter when not specified (undefined by default)', () => {
126+
const ajv = new AjvOptions(baseOptions);
127+
const options = ajv.request;
128+
expect(options.discriminator).to.be.undefined;
129+
});
130+
131+
it('should set discriminator to true when specified', () => {
132+
const ajv = new AjvOptions({
133+
...baseOptions,
134+
validateRequests: {
135+
...baseOptions.validateRequests,
136+
discriminator: true,
137+
},
138+
});
139+
const options = ajv.request;
140+
expect(options.discriminator).to.be.true;
141+
});
142+
143+
it('should set discriminator to false when specified', () => {
144+
const ajv = new AjvOptions({
145+
...baseOptions,
146+
validateRequests: {
147+
...baseOptions.validateRequests,
148+
discriminator: false,
149+
},
150+
});
151+
const options = ajv.request;
152+
expect(options.discriminator).to.be.false;
153+
});
124154
});

test/discriminator.spec.ts

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import * as path from 'path';
2+
import { expect } from 'chai';
3+
import * as request from 'supertest';
4+
import { createApp } from './common/app';
5+
import { AppWithServer } from './common/app.common';
6+
7+
type Op =
8+
| {
9+
type: 'create_screen';
10+
data: {
11+
key: string;
12+
titleTranslationKey: string;
13+
type: 'normal' | 'intermediate';
14+
step?: string;
15+
descriptionTranslationKey?: string;
16+
description?: string;
17+
};
18+
}
19+
| {
20+
type: 'update_screen';
21+
data: {
22+
id: string;
23+
props: {
24+
key?: string;
25+
titleTranslationKey?: string;
26+
type?: 'normal' | 'intermediate';
27+
step: string | null;
28+
descriptionTranslationKey?: string;
29+
description?: string;
30+
};
31+
};
32+
}
33+
| {
34+
type: 'create_question';
35+
data: {
36+
key: string;
37+
titleTranslationKey: string;
38+
uiElementType: string;
39+
valueType: string;
40+
};
41+
}
42+
| {
43+
type: 'update_question';
44+
data: {
45+
id: string;
46+
props: {
47+
key?: string;
48+
titleTranslationKey?: string;
49+
uiElementType?: string;
50+
valueType?: string;
51+
};
52+
};
53+
};
54+
55+
const postOps = (app: any, op: Op) =>
56+
request(app)
57+
.post(`${app.basePath}/operations`)
58+
.set('content-type', 'application/json')
59+
.send({ operations: [op] })
60+
.expect(204);
61+
62+
describe.only('Operation discriminator', () => {
63+
let app: AppWithServer;
64+
65+
before(async () => {
66+
const apiSpec = path.join('test', 'resources', 'discriminator.yaml');
67+
app = await createApp(
68+
{ apiSpec, validateRequests: { discriminator: true, allErrors: true } },
69+
3001,
70+
(app) => {
71+
app.post(`${app.basePath}/operations`, (req, res) => {
72+
res.status(204).send();
73+
});
74+
},
75+
);
76+
});
77+
78+
after(() => {
79+
app.server.close();
80+
});
81+
82+
describe('/operations', () => {
83+
const cases: Array<[string, Op]> = [
84+
[
85+
'create_screen',
86+
{
87+
type: 'create_screen',
88+
data: {
89+
key: 'test_screen',
90+
titleTranslationKey: 'screen.test.title',
91+
type: 'normal',
92+
step: 'step1',
93+
},
94+
},
95+
],
96+
[
97+
'update_screen',
98+
{
99+
type: 'update_screen',
100+
data: {
101+
id: '550e8400-e29b-41d4-a716-446655440000',
102+
props: {
103+
key: 'updated_screen',
104+
titleTranslationKey: 'screen.updated.title',
105+
type: 'intermediate',
106+
step: 'step2',
107+
},
108+
},
109+
},
110+
],
111+
[
112+
'create_question',
113+
{
114+
type: 'create_question',
115+
data: {
116+
key: 'test_question',
117+
titleTranslationKey: 'question.test.title',
118+
uiElementType: 'input',
119+
valueType: 'string',
120+
},
121+
},
122+
],
123+
[
124+
'update_question',
125+
{
126+
type: 'update_question',
127+
data: {
128+
id: '550e8400-e29b-41d4-a716-446655440000',
129+
props: {
130+
key: 'updated_question',
131+
titleTranslationKey: 'question.updated.title',
132+
uiElementType: 'checkbox',
133+
valueType: 'boolean',
134+
},
135+
},
136+
},
137+
],
138+
];
139+
140+
for (const [name, op] of cases) {
141+
it(`should return 204 for valid ${name} operation`, async function () {
142+
const res = await postOps(app, op);
143+
expect(res.status).to.equal(204);
144+
});
145+
}
146+
147+
it('should return 400 for invalid discriminator type', async () =>
148+
request(app)
149+
.post(`${app.basePath}/operations`)
150+
.set('content-type', 'application/json')
151+
.send({
152+
operations: [
153+
{
154+
type: 'invalid_operation',
155+
data: {
156+
key: 'test',
157+
titleTranslationKey: 'test',
158+
type: 'normal',
159+
step: 'step1',
160+
},
161+
},
162+
],
163+
})
164+
.expect(400)
165+
.then((r) => {
166+
expect(r.body.errors).to.have.lengthOf(1);
167+
168+
const [error] = r.body.errors;
169+
170+
expect(error.path).to.include('/body/operations/0');
171+
expect(error.message).to.match(
172+
/value of tag "type" must be in oneOf/,
173+
);
174+
expect(error.errorCode).to.equal('discriminator.openapi.validation');
175+
}));
176+
177+
it('should return 400 for create_screen operation with missing required fields', async () =>
178+
request(app)
179+
.post(`${app.basePath}/operations`)
180+
.set('content-type', 'application/json')
181+
.send({
182+
operations: [
183+
{
184+
type: 'create_screen',
185+
data: {
186+
key: 'test_screen',
187+
// missing titleTranslationKey, type, step
188+
},
189+
},
190+
],
191+
})
192+
.expect(400)
193+
.then((r) => {
194+
const expected = [
195+
{
196+
path: '/body/operations/0/data/titleTranslationKey',
197+
message: "must have required property 'titleTranslationKey'",
198+
errorCode: 'required.openapi.validation',
199+
},
200+
{
201+
path: '/body/operations/0/data/type',
202+
message: "must have required property 'type'",
203+
errorCode: 'required.openapi.validation',
204+
},
205+
{
206+
path: '/body/operations/0/data/step',
207+
message: "must have required property 'step'",
208+
errorCode: 'required.openapi.validation',
209+
},
210+
];
211+
212+
const errors = r.body.errors.map(({ path, message, errorCode }) => ({
213+
path,
214+
message,
215+
errorCode,
216+
}));
217+
218+
expect(errors).to.have.lengthOf(expected.length);
219+
expect(errors).to.have.deep.members(expected);
220+
}));
221+
});
222+
});

0 commit comments

Comments
 (0)