Skip to content

Commit

Permalink
deepObject default value support #287
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed May 7, 2020
1 parent 60c2abd commit e548b9a
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 19 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ dist
secrets.zip
jest
junk
/a_reference
/a_reference
/sample2
4 changes: 2 additions & 2 deletions src/middlewares/openapi.request.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export class RequestValidator {
contentType: ContentType,
): RequestHandler {
const apiDoc = this.apiDoc;
const schemaParser = new ParametersSchemaParser(apiDoc);
const schemaParser = new ParametersSchemaParser(this.ajv, apiDoc);
const bodySchemaParser = new BodySchemaParser(this.ajv, apiDoc);
const parameters = schemaParser.parse(path, reqSchema.parameters);
const securityQueryParam = Security.queryParam(apiDoc, reqSchema);
Expand All @@ -112,7 +112,7 @@ export class RequestValidator {
req.params = openapi.pathParams ?? req.params;
}

const mutator = new RequestParameterMutator(apiDoc, path, properties);
const mutator = new RequestParameterMutator(this.ajv, apiDoc, path, properties);

mutator.modifyRequest(req);

Expand Down
35 changes: 27 additions & 8 deletions src/middlewares/parsers/req.parameter.mutator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Request } from 'express';
import { Ajv } from 'ajv';
import * as ajv from 'ajv';
import {
OpenAPIV3,
OpenApiRequest,
Expand All @@ -7,7 +9,7 @@ import {
} from '../../framework/types';
import * as url from 'url';
import { validationError } from '../util';
import { dereferenceParameter, normalizeParameter } from './util';
import { /* dereference, */dereferenceParameter, normalizeParameter } from './util';
import * as mediaTypeParser from 'media-typer';
import * as contentTypeParser from 'content-type';

Expand Down Expand Up @@ -40,13 +42,16 @@ type Parameter = ReferenceObject | ParameterObject;
export class RequestParameterMutator {
private _apiDocs: OpenAPIV3.Document;
private path: string;
private ajv: Ajv;
private parsedSchema: ValidationSchema;

constructor(
ajv: Ajv,
apiDocs: OpenAPIV3.Document,
path: string,
parsedSchema: ValidationSchema,
) {
this.ajv = ajv;
this._apiDocs = apiDocs;
this.path = path;
this.parsedSchema = parsedSchema;
Expand All @@ -63,9 +68,10 @@ export class RequestParameterMutator {
url.parse(req.originalUrl).query,
);

parameters.forEach(p => {
parameters.forEach((p) => {
const parameter = dereferenceParameter(this._apiDocs, p);
const { name, schema } = normalizeParameter(parameter);
const { name, schema } = normalizeParameter(this.ajv, parameter);

const { type } = <SchemaObject>schema;
const { style, explode } = parameter;
const i = req.originalUrl.indexOf('?');
Expand All @@ -78,11 +84,13 @@ export class RequestParameterMutator {
if (parameter.content) {
this.handleContent(req, name, parameter);
} else if (parameter.in === 'query' && this.isObjectOrXOf(schema)) {
this.parseJsonAndMutateRequest(req, parameter.in, name);
if (style === 'form' && explode) {
this.parseJsonAndMutateRequest(req, parameter.in, name);
this.handleFormExplode(req, name, <SchemaObject>schema, parameter);
} else if (style === 'deepObject') {
this.handleDeepObject(req, queryString, name);
} else {
this.parseJsonAndMutateRequest(req, parameter.in, name);
}
} else if (type === 'array' && !explode) {
const delimiter = ARRAY_DELIMITER[parameter.style];
Expand All @@ -97,6 +105,10 @@ export class RequestParameterMutator {
}

private handleDeepObject(req: Request, qs: string, name: string): void {
if (!req.query?.[name]) {
req.query[name] = {};
}
this.parseJsonAndMutateRequest(req, 'query', name);
// nothing to do
// TODO handle url encoded?
}
Expand Down Expand Up @@ -153,6 +165,9 @@ export class RequestParameterMutator {
return acc;
} else {
const foundProperties = schema[key].reduce((acc2, obj) => {
// if (obj.$ref) {
// obj = dereference(apiDocs, obj);
// }
return obj.type === 'object'
? acc2.concat(...Object.keys(obj.properties))
: acc2;
Expand Down Expand Up @@ -220,10 +235,13 @@ export class RequestParameterMutator {
const field = REQUEST_FIELDS[$in];
if (req[field]) {
// check if there is at least one of the nested properties before create the parent
const atLeastOne = properties.some(p => req[field].hasOwnProperty(p));
if (atLeastOne) {
const atLeastOne = properties.some((p) => req[field].hasOwnProperty(p));
const val = req[field][name];
const shouldBeObject =
val && typeof val === 'object' && !Array.isArray(val);
if (shouldBeObject || atLeastOne) {
req[field][name] = {};
properties.forEach(property => {
properties.forEach((property) => {
if (req[field][property]) {
const schema = this.parsedSchema[field];
const type = schema.properties[name].properties?.[property]?.type;
Expand Down Expand Up @@ -254,8 +272,9 @@ export class RequestParameterMutator {
}

private isObjectOrXOf(schema: Schema): boolean {
const schemaHasObject = schema => {
const schemaHasObject = (schema) => {
if (!schema) return false;
if (schema.$ref) return true;
const { type, allOf, oneOf, anyOf } = schema;
return (
type === 'object' ||
Expand Down
9 changes: 6 additions & 3 deletions src/middlewares/parsers/schema.parse.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OpenAPIV3, ParametersSchema } from '../../framework/types';
import { validationError } from '../util';
import { dereferenceParameter, normalizeParameter } from './util';
import { Ajv } from 'ajv';

const PARAM_TYPE = {
query: 'query',
Expand All @@ -16,9 +17,11 @@ type Parameter = OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject;
* whose value must later be parsed as a JSON object, JSON Exploded Object, JSON Array, or JSON Exploded Array
*/
export class ParametersSchemaParser {
private _ajv: Ajv;
private _apiDocs: OpenAPIV3.Document;

constructor(apiDocs: OpenAPIV3.Document) {
constructor(ajv: Ajv, apiDocs: OpenAPIV3.Document) {
this._ajv = ajv;
this._apiDocs = apiDocs;
}

Expand All @@ -31,13 +34,13 @@ export class ParametersSchemaParser {
public parse(path: string, parameters: Parameter[] = []): ParametersSchema {
const schemas = { query: {}, headers: {}, params: {}, cookies: {} };

parameters.forEach(p => {
parameters.forEach((p) => {
const parameter = dereferenceParameter(this._apiDocs, p);

this.validateParameterType(path, parameter);

const reqField = PARAM_TYPE[parameter.in];
const { name, schema } = normalizeParameter(parameter);
const { name, schema } = normalizeParameter(this._ajv, parameter);

if (!schemas[reqField].properties) {
schemas[reqField] = {
Expand Down
51 changes: 47 additions & 4 deletions src/middlewares/parsers/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Ajv } from 'ajv';
import { OpenAPIV3 } from '../../framework/types';
import ajv = require('ajv');
import { OpenAPIFramework } from '../../framework';

export function dereferenceParameter(
apiDocs: OpenAPIV3.Document,
Expand All @@ -15,24 +18,64 @@ export function dereferenceParameter(
}
}

// export function dereference(
// apiDocs: OpenAPIV3.Document,
// schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject,
// ): OpenAPIV3.SchemaObject {
// // TODO this should recurse or use ajv.getSchema - if implemented as such, may want to cache the result
// // as it is called by query.paraer and req.parameter mutator

// if (schema?.['$ref']) {
// const ref = (<OpenAPIV3.ReferenceObject>schema).$ref;
// const id = ref.replace(/^.+\//i, '');
// if (apiDocs.components?.parameters?.[id]) {
// return <OpenAPIV3.SchemaObject>(
// (<unknown>apiDocs.components.parameters[id])
// );
// } else if (apiDocs.components?.schemas?.[id]) {
// return <OpenAPIV3.SchemaObject>apiDocs.components.schemas[id];
// }
// }
// return <OpenAPIV3.SchemaObject>schema;
// }

export function normalizeParameter(
ajv: Ajv,
parameter: OpenAPIV3.ParameterObject,
): {
name: string;
schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
schema: OpenAPIV3.SchemaObject;
} {
// TODO this should recurse or use ajv.getSchema - if implemented as such, may want to cache the result
// as it is called by query.paraer and req.parameter mutator
let schema = parameter.schema;
let schema;
if (is$Ref(parameter)) {
schema = dereferenceSchema(ajv, parameter['$ref']);
} else if (parameter?.schema?.['$ref']) {
schema = dereferenceSchema(ajv, parameter.schema['$ref']);
} else {
schema = parameter.schema
}
if (!schema) {
const contentType = Object.keys(parameter.content)[0];
schema = parameter.content?.[contentType]?.schema;
}
if (!schema) {
schema = parameter;
}

const name =
parameter.in === 'header' ? parameter.name.toLowerCase() : parameter.name;
return { name, schema };
}

export function dereferenceSchema(ajv: Ajv, ref: string) {
// TODO cache schemas - so that we don't recurse every time
const derefSchema = ajv.getSchema(ref);
if (derefSchema?.['$ref']) {
return dereferenceSchema(ajv, '');
}
return derefSchema.schema;
}

function is$Ref(
parameter: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject,
): boolean {
Expand Down
2 changes: 1 addition & 1 deletion test/common/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { OpenApiValidatorOpts } from '../../src/framework/types';
export async function createApp(
opts?: OpenApiValidatorOpts,
port = 3000,
customRoutes = app => {},
customRoutes = (app) => {},
useRoutes = true,
apiRouter = undefined,
) {
Expand Down
58 changes: 58 additions & 0 deletions test/resources/serialized.objects.defaults.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
components:
schemas:
PageSort:
allOf:
- $ref: "#/components/schemas/Paging"
- $ref: "#/components/schemas/Sorting"
Paging:
properties:
page:
default: 1
minimum: 1
type: integer
perPage:
default: 25
type: integer
type: object
Sorting:
properties:
field:
default: id
enum:
- id
- name
type: string
order:
default: ASC
enum:
- ASC
- DESC
type: string
type: object
info:
description: API
title: API
version: 1.0.0
openapi: 3.0.0
servers:
- url: /v1/
paths:
/deep_object:
get:
operationId: getDeepObject
parameters:
- explode: true
in: query
name: pagesort
schema:
$ref: "#/components/schemas/PageSort"
style: deepObject
responses:
"200":
description: description
content:
application/json:
schema:
items:
type: number
type: array
51 changes: 51 additions & 0 deletions test/serialized.objects.defaults.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as path from 'path';
import * as express from 'express';
import * as request from 'supertest';
import * as packageJson from '../package.json';
import { expect } from 'chai';
import { createApp } from './common/app';

describe(packageJson.name, () => {
let app = null;

before(async () => {
// Set up the express app
const apiSpec = path.join(
'test',
'resources',
'serialized.objects.defaults.yaml',
);
app = await createApp({ apiSpec }, 3005, (app) =>
app.use(
`${app.basePath}`,
express.Router().get(`/deep_object`, (req, res) => res.json(req.query)),
),
);
});

after(() => {
app.server.close();
});

it('should use defaults when empty', async () =>
request(app)
.get(`${app.basePath}/deep_object`)
.expect(200)
.then((r) => {
console.log(r.body);
expect(r.body).to.deep.equals({
pagesort: { page: 1, perPage: 25, field: 'id', order: 'ASC' },
});
}));

it('should use defaults for values not provided', async () =>
request(app)
.get(`${app.basePath}/deep_object?pagesort[field]=name`)
.expect(200)
.then((r) => {
console.log(r.body);
expect(r.body).to.deep.equals({
pagesort: { page: 1, perPage: 25, field: 'name', order: 'ASC' },
});
}));
});

0 comments on commit e548b9a

Please sign in to comment.