Skip to content

Commit

Permalink
support validation by operation id
Browse files Browse the repository at this point in the history
  • Loading branch information
supertong committed Mar 14, 2020
1 parent e29d107 commit 5b741ba
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 47 deletions.
9 changes: 4 additions & 5 deletions __test__/response.spec.ts
@@ -1,6 +1,5 @@
import ChowChow from '../src';
import ChowError, { ResponseValidationError } from '../src/error';
import {ResponseMeta} from '../src/compiler';

const fixture = require('./fixtures/response.json');

Expand All @@ -12,7 +11,7 @@ describe('Response', () => {
});

it('should validate the response with status code', () => {
const responseMeta: ResponseMeta = {
const responseMeta = {
method: 'get',
status: 200,
header: {
Expand All @@ -31,7 +30,7 @@ describe('Response', () => {
});

it("should pass if a field that is nullable: true is null", () => {
const responseMeta: ResponseMeta = {
const responseMeta = {
method: "get",
status: 200,
header: {
Expand All @@ -49,7 +48,7 @@ describe('Response', () => {
});

it('should fail validation the response with writeOnly property', () => {
const responseMeta: ResponseMeta = {
const responseMeta = {
method: 'get',
status: 200,
header: {
Expand All @@ -67,7 +66,7 @@ describe('Response', () => {
});

it('should fall back to default if no status code is matched', () => {
const responseMeta: ResponseMeta = {
const responseMeta = {
method: 'get',
status: 500,
header: {
Expand Down
1 change: 0 additions & 1 deletion src/compiler/CompiledOperation.ts
Expand Up @@ -77,7 +77,6 @@ export default class CompiledOperation {
}

return {
method: request.method,
operationId: request.operationId || this.operationId,
header,
query,
Expand Down
12 changes: 6 additions & 6 deletions src/compiler/CompiledPath.ts
@@ -1,5 +1,5 @@
import { PathItemObject } from 'openapi3-ts';
import CompiledPathItem from './CompiledPathItem';
import CompiledPathItem, { OperationRegisterFunc } from './CompiledPathItem';
import { RequestMeta, ResponseMeta } from '.';
import * as XRegExp from 'xregexp';
import { ChowOptions } from '..';
Expand All @@ -14,7 +14,7 @@ export default class CompiledPath {
private compiledPathItem: CompiledPathItem;
private ignoredMatches = ['index', 'input'];

constructor(path: string, pathItemObject: PathItemObject, options: Partial<ChowOptions>) {
constructor(path: string, pathItemObject: PathItemObject, options: Partial<ChowOptions & { registerCompiledOperationWithId: OperationRegisterFunc }>) {
this.path = path;
/**
* The following statement should create Named Capturing Group for
Expand All @@ -33,15 +33,15 @@ export default class CompiledPath {
return XRegExp.test(path, this.regex);
}

public validateRequest(path: string, request: RequestMeta) {
return this.compiledPathItem.validateRequest({
public validateRequest(path: string, method: string, request: RequestMeta) {
return this.compiledPathItem.validateRequest(method, {
...request,
path: this.extractPathParams(path)
});
}

public validateResponse(response: ResponseMeta) {
return this.compiledPathItem.validateResponse(response);
public validateResponse(method: string, response: ResponseMeta) {
return this.compiledPathItem.validateResponse(method, response);
}

private extractPathParams = (path: string): PathParameters => {
Expand Down
57 changes: 34 additions & 23 deletions src/compiler/CompiledPathItem.ts
Expand Up @@ -4,47 +4,58 @@ import { RequestMeta, ResponseMeta } from '.';
import ChowError from '../error';
import { ChowOptions } from '..';

export type OperationRegisterFunc = (operationId: string, compiledOperation: CompiledOperation) => void;

export default class CompiledPathItem {
private supportedMethod = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
private compiledOperations: {
[key: string]: CompiledOperation;
} = {};
static readonly SupportedMethod = <const>['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
private compiledOperationsByMethod: Map<string, CompiledOperation> = new Map();
private path: string;

constructor(pathItemObject: PathItemObject, path: string, options: Partial<ChowOptions>) {
this.compiledOperations = this.supportedMethod.reduce((compiled: any, method: string) => {
const m = method.toLowerCase();
if (pathItemObject[m]) {
compiled[m] = new CompiledOperation(
pathItemObject[m],
(pathItemObject.parameters as ParameterObject[]) || [],
options);
constructor(pathItemObject: PathItemObject, path: string, options: Partial<ChowOptions & { registerCompiledOperationWithId: OperationRegisterFunc }>) {
CompiledPathItem.SupportedMethod.forEach(method => {
const operationObject = pathItemObject[method];

if (!operationObject) {
return;
}
return compiled;
}, {})

const compiledOperation = new CompiledOperation(
operationObject,
(pathItemObject.parameters as ParameterObject[]) || [],
options
);
this.compiledOperationsByMethod.set(method, compiledOperation);

if (operationObject.operationId && options.registerCompiledOperationWithId) {
options.registerCompiledOperationWithId(operationObject.operationId, compiledOperation);
}
});

this.path = path;
}

public getDefinedRequestBodyContentType(method: string): string[] {
const m = method.toLowerCase();
return this.compiledOperations[m] ? this.compiledOperations[m].getDefinedRequestBodyContentType() : [];

const compiledOperation = this.compiledOperationsByMethod.get(m);
return !!compiledOperation ? compiledOperation.getDefinedRequestBodyContentType() : [];
}

public validateRequest(request: RequestMeta) {
const method = request.method.toLowerCase();
const compiledOperation = this.compiledOperations[method];
public validateRequest(method: string, request: RequestMeta) {
const mt = method.toLowerCase();
const compiledOperation = this.compiledOperationsByMethod.get(mt);
if (!compiledOperation) {
throw new ChowError(`Invalid request method - ${method}`, { in: 'path' })
throw new ChowError(`Invalid request method - ${mt}`, { in: 'path' })
}

return compiledOperation.validateRequest(request);
}

public validateResponse(response: ResponseMeta) {
const method = response.method.toLowerCase();
const compiledOperation = this.compiledOperations[method];
public validateResponse(method: string, response: ResponseMeta) {
const mt = method.toLowerCase();
const compiledOperation = this.compiledOperationsByMethod.get(mt);
if (!compiledOperation) {
throw new ChowError(`Invalid request method - ${method}`, { in: 'path' })
throw new ChowError(`Invalid request method - ${mt}`, { in: 'path' })
}

return compiledOperation.validateResponse(response);
Expand Down
24 changes: 17 additions & 7 deletions src/compiler/index.ts
Expand Up @@ -5,9 +5,10 @@ import {
import CompiledPath from "./CompiledPath";
import * as deref from "json-schema-deref-sync";
import { ChowOptions } from "..";
import CompiledOperation from "./CompiledOperation";
import { OperationRegisterFunc } from "./CompiledPathItem";

export interface RequestMeta {
method: string;
query?: any;
header?: any;
path?: any;
Expand All @@ -17,23 +18,32 @@ export interface RequestMeta {
}

export interface ResponseMeta {
method: string;
status: number;
header?: any;
body?: any;
}

export default function compile(oas: OpenAPIObject, options: Partial<ChowOptions>): CompiledPath[] {
const document: OpenAPIObject = deref(oas, {failOnMissing: true});
export default function compile(oas: OpenAPIObject, options: Partial<ChowOptions>): { compiledPaths: CompiledPath[], compiledOperationById: Map<string, CompiledOperation> } {
const document: OpenAPIObject = deref(oas, { failOnMissing: true });

if (document instanceof Error){
if (document instanceof Error) {
throw document;
}

return Object.keys(document.paths).map((path: string) => {
const compiledOperationById = new Map<string, CompiledOperation>();
const registerOperationById: OperationRegisterFunc = (operationId: string, compiledOperation: CompiledOperation) => {
compiledOperationById.set(operationId, compiledOperation);
}

const compiledPaths = Object.keys(document.paths).map((path: string) => {
const pathItemObject: PathItemObject = document.paths[path];

// TODO: support for base path
return new CompiledPath(path, pathItemObject, options);
return new CompiledPath(path, pathItemObject, { ...options, registerCompiledOperationWithId: registerOperationById });
});

return {
compiledPaths,
compiledOperationById
};
}
65 changes: 60 additions & 5 deletions src/index.ts
Expand Up @@ -3,6 +3,8 @@ import { OpenAPIObject } from 'openapi3-ts';
import compile, { RequestMeta, ResponseMeta } from './compiler';
import CompiledPath from './compiler/CompiledPath';
import ChowError, { RequestValidationError, ResponseValidationError } from './error';
import CompiledOperation from './compiler/CompiledOperation';
import * as util from 'util';

/**
* Export Errors so that consumers can use it to ditinguish different error type.
Expand All @@ -20,15 +22,32 @@ export interface ChowOptions {

export default class ChowChow {
private compiledPaths: CompiledPath[];
private compiledOperationById: Map<string, CompiledOperation>;

constructor(document: OpenAPIObject, options: Partial<ChowOptions> = {}) {
this.compiledPaths = compile(document, options);
const { compiledPaths, compiledOperationById } = compile(document, options);
this.compiledPaths = compiledPaths;
this.compiledOperationById = compiledOperationById;
}

validateRequest(path: string, request: RequestMeta) {
validateRequest(path: string, request: RequestMeta & { method: string }) {
return util.deprecate(
this.validateRequestByPath.bind(this),
'validateRequest() is now deprecated, please use validateRequestByPath or validateRequestByOperationId instead'
)(path, request.method, request);
}

validateResponse(path: string, response: ResponseMeta & { method: string }) {
return util.deprecate(
this.validateResponseByPath.bind(this),
'validateResponse() is now deprecated, please use validateResponseByPath or validateResponseByOperationId instead'
)(path, response.method, response);
}

validateRequestByPath(path: string, method: string, request: RequestMeta) {
try {
const compiledPath = this.identifyCompiledPath(path);
return compiledPath.validateRequest(path, request);
return compiledPath.validateRequest(path, method, request);
} catch(err) {
if (err instanceof ChowError) {
throw new RequestValidationError(err.message, err.meta);
Expand All @@ -38,10 +57,46 @@ export default class ChowChow {
}
}

validateResponse(path: string, response: ResponseMeta) {
validateResponseByPath(path: string, method: string, response: ResponseMeta) {
try {
const compiledPath = this.identifyCompiledPath(path);
return compiledPath.validateResponse(response);
return compiledPath.validateResponse(method, response);
} catch(err) {
if (err instanceof ChowError) {
throw new ResponseValidationError(err.message, err.meta);
} else {
throw err;
}
}
}

validateRequestByOperationId(operationId: string, request: RequestMeta) {
const compiledOperation = this.compiledOperationById.get(operationId);

if (!compiledOperation) {
throw new ChowError(`No matches found for the given operationId - ${operationId}`, {in: 'request', code: 404});
}

try {
return compiledOperation.validateRequest(request);
} catch(err) {
if (err instanceof ChowError) {
throw new RequestValidationError(err.message, err.meta);
} else {
throw err;
}
}
}

validateResponseByOperationId(operationId: string, response: ResponseMeta) {
const compiledOperation = this.compiledOperationById.get(operationId);

if (!compiledOperation) {
throw new ChowError(`No matches found for the given operationId - ${operationId}`, {in: 'response', code: 404});
}

try {
return compiledOperation.validateRequest(response);
} catch(err) {
if (err instanceof ChowError) {
throw new ResponseValidationError(err.message, err.meta);
Expand Down

0 comments on commit 5b741ba

Please sign in to comment.