From c49300108c2fb56d681a4e1142dbd5295ccadeae Mon Sep 17 00:00:00 2001 From: Carmine Date: Sun, 31 Mar 2019 21:02:55 -0400 Subject: [PATCH] support server variables --- .travis.yml | 18 +++--- openapi.yaml | 19 +++++- package-lock.json | 8 +-- package.json | 7 ++- src/framework/base.path.ts | 89 +++++++++++++++++++++++++++-- src/framework/index.ts | 67 +++++++--------------- src/framework/types.ts | 82 +++++++++----------------- src/middlewares/openapi.metadata.ts | 11 +--- src/openapi.context.ts | 9 ++- src/openapi.spec.loader.ts | 15 +++-- tsconfig.json | 1 + 11 files changed, 186 insertions(+), 140 deletions(-) diff --git a/.travis.yml b/.travis.yml index b338a56a..5e5c3c22 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,18 @@ sudo: required language: node_js node_js: -- '10' + - '10' before_install: -- openssl aes-256-cbc -K $encrypted_5d8f15d35f17_key -iv $encrypted_5d8f15d35f17_iv - -in secrets.zip.enc -out secrets.zip -d -- unzip secrets.zip + - openssl aes-256-cbc -K $encrypted_5d8f15d35f17_key -iv $encrypted_5d8f15d35f17_iv + -in secrets.zip.enc -out secrets.zip -d + - unzip secrets.zip install: -- npm install + - npm install script: -- npm run compile -- npm test -- npm run coveralls -- npm run codacy + - npm run compile + - npm run test:coverage + - npm run coveralls + - npm run codacy env: global: secure: ytiwXpn7JDc0fhB2KQa2z/OM9LNyVqf/5XCg6LX1WtXRn3R6qYfqCMBUq+eu/Pvym5SlPUwPwZEzRwSBUBivAhNsSPMNGnzNVyX+OxVruXlaD+Wsx3ROTGnwTpPEh7JUXXpytWmG6IAXwyhOvnhAgILjDFaRPkRNc/9Y1zsRTtFxiaisN/6X4IFxGRdI6OVobLEDKmnUBa2nDw5fZws+hOGWlv7b1NqbzXJ1yrOTH0E/uPsill3aWqXuDSEtv4VhAk0z9Xyw4Bi28XtDNQNSou8LkGmN4QzXHdKz73UgY3RaKQ99PVhyTZz0TrvL54BSB/poUoXnu3ZZODKA9GIrthnHW6rn3u0ovnHRAA+L2CLBTqFbGoctOe3XhnUcDBmcvH3FnE+IcLdSeeEjDO+oGIZdut2af2aPj+rA7Hd76+I/CGEf8osnfZVuLYYWW+semuTCF4rDSI+EPu8Rb3w3POCzskkinONnxdYQpcEw01ltMBUnJxLi7fYVpLeSNsOs5rBMOL+LlbsnG505UvmqaFlzyt7GzvcCXW0hrBAARjq3ADAmtSBVZuzMWebN2X93wjZZ9SLyVtCdVNtCxCwA3X0zYJ1dH6roSNi99gMDG9fxbb+Hhc/0+9+u7Sn2CMvOLy9LiV+lm0MeANDOitRb6OTLuOKlLLjr8lvvJxf9qwE= diff --git a/openapi.yaml b/openapi.yaml index bf1f7d8d..ef59762e 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -12,7 +12,24 @@ info: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - - url: http://petstore.swagger.io/v1 + - url: /v1 + - url: http://{name}.swagger.io:{port}/{version} + variables: + name: + default: petstore + enum: + - petstore + - storeofpets + port: + enum: + - '443' + - '8443' + default: '443' + version: + default: v1 + enum: + - v1 + - v2 paths: /pets: get: diff --git a/package-lock.json b/package-lock.json index 4ad5cf87..58a120e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "express-openapi-validator", - "version": "0.3.36", + "version": "0.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2752,9 +2752,9 @@ "dev": true }, "js-yaml": { - "version": "3.12.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", - "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.0.tgz", + "integrity": "sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" diff --git a/package.json b/package.json index cbaab495..b419b7e7 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "express-openapi-validator", - "version": "0.3.36", + "version": "0.9.1", "description": "", "main": "dist/index.js", "scripts": { "compile": "rm -rf dist/ && tsc", - "test": "nyc mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts", + "test": "mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts", + "test:coverage": "nyc mocha -r source-map-support/register -r ts-node/register --recursive test/**/*.spec.ts", "coveralls": "cat coverage/lcov.info | coveralls -v", "codacy": "cat coverage/lcov.info | codacy-coverage" }, @@ -20,7 +21,7 @@ "openapi-request-validator": "^3.7.0", "openapi-schema-validator": "^3.0.3", "openapi-security-handler": "^2.0.4", - "openapi-types": "1.3.4", + "openapi-types": "^1.3.4", "path-to-regexp": "^3.0.0", "ts-log": "^2.1.4" }, diff --git a/src/framework/base.path.ts b/src/framework/base.path.ts index 70713f0c..fe570f1a 100644 --- a/src/framework/base.path.ts +++ b/src/framework/base.path.ts @@ -1,15 +1,23 @@ +import * as pathToRegexp from 'path-to-regexp'; import { OpenAPIV3 } from 'openapi-types'; -import { URL } from 'url'; + +interface ServerUrlVariables { + [key: string]: ServerUrlValues; +} +interface ServerUrlValues { + enum: string[]; + default?: string; +} export default class BasePath { - public readonly variables: { [key: string]: { enum: string[] } } = {}; + public readonly variables: ServerUrlVariables = {}; public readonly path: string = ''; + private allPaths: string[] = null; constructor(server: OpenAPIV3.ServerObject) { // break the url into parts // baseUrl param added to make the parsing of relative paths go well - const serverUrl = new URL(server.url, 'http://localhost'); - let urlPath = decodeURI(serverUrl.pathname).replace(/\/$/, ''); + let urlPath = this.findUrlPath(server.url); if (/{\w+}/.test(urlPath)) { // has variable that we need to check out urlPath = urlPath.replace(/{(\w+)}/g, (substring, p1) => `:${p1}`); @@ -17,19 +25,88 @@ export default class BasePath { this.path = urlPath; for (const variable in server.variables) { if (server.variables.hasOwnProperty(variable)) { - this.variables[variable] = { enum: server.variables[variable].enum }; + const v = server.variables[variable]; + const enums = v.enum || []; + if (enums.length === 0 && v.default) enums.push(v.default); + + this.variables[variable] = { + enum: enums, + default: v.default, + }; } } } - public hasVariables() { + public hasVariables(): boolean { return Object.keys(this.variables).length > 0; } + public all(): string[] { + if (!this.hasVariables()) return [this.path]; + if (this.allPaths) return this.allPaths; + // TODO performance optimization + // ignore variables that are not part of path params + const allParams = Object.entries(this.variables).reduce((acc, v) => { + const [key, value] = v; + const params = value.enum.map(e => ({ + [key]: e, + })); + acc.push(params); + return acc; + }, []); + + const allParamCombos = cartesian(...allParams); + const toPath = pathToRegexp.compile(this.path); + const paths = new Set(); + for (const combo of allParamCombos) { + paths.add(toPath(combo)); + } + this.allPaths = Array.from(paths); + return this.allPaths; + } + public static fromServers(servers: OpenAPIV3.ServerObject[]) { if (!servers) { return [new BasePath({ url: '' })]; } return servers.map(server => new BasePath(server)); } + + private findUrlPath(u) { + const findColonSlashSlash = p => { + const r = /:\/\//.exec(p); + if (r) return r.index; + return -1; + }; + const findFirstSlash = p => { + const r = /\//.exec(p); + if (r) return r.index; + return -1; + }; + + const fcssIdx = findColonSlashSlash(u); + const startSearchIdx = fcssIdx !== -1 ? fcssIdx + 3 : 0; + const startPathIdx = findFirstSlash(u.substring(startSearchIdx)); + if (startPathIdx === -1) return '/'; + + const pathIdx = startPathIdx + startSearchIdx; + return u.substring(pathIdx); + } +} + +function cartesian(...arg) { + const r = [], + max = arg.length - 1; + function helper(obj, i) { + const values = arg[i]; + for (var j = 0, l = values.length; j < l; j++) { + const a = { ...obj }; + const key = Object.keys(values[j])[0]; + a[key] = values[j][key]; + if (i == max) r.push(a); + else helper(a, i + 1); + } + } + helper({}, 0); + return r; } diff --git a/src/framework/index.ts b/src/framework/index.ts index 3a6d186e..1e7266a8 100644 --- a/src/framework/index.ts +++ b/src/framework/index.ts @@ -1,11 +1,6 @@ -// import fsRoutes from 'fs-routes'; -// import OpenAPIDefaultSetter from 'openapi-default-setter'; -// import OpenAPIRequestCoercer from 'openapi-request-coercer'; -// import OpenAPIRequestValidator from 'openapi-request-validator'; -// import OpenAPIResponseValidator from 'openapi-response-validator'; import OpenAPISchemaValidator from 'openapi-schema-validator'; import OpenAPISecurityHandler from 'openapi-security-handler'; -import { OpenAPI, OpenAPIV2, OpenAPIV3 } from 'openapi-types'; +import { OpenAPIV2, OpenAPIV3 } from 'openapi-types'; import BasePath from './base.path'; import { ConsoleDebugAdapterLogger, @@ -13,39 +8,17 @@ import { OpenAPIFrameworkAPIContext, OpenAPIFrameworkArgs, OpenAPIFrameworkConstructorArgs, - // OpenAPIFrameworkOperationContext, OpenAPIFrameworkPathContext, OpenAPIFrameworkPathObject, OpenAPIFrameworkVisitor, } from './types'; import { - // addOperationTagToApiDoc, - // allowsCoercionFeature, - // allowsDefaultsFeature, - // allowsFeatures, - // allowsResponseValidationFeature, - // allowsValidationFeature, assertRegExpAndSecurity, - // byDefault, - // byDirectory, - // byMethods, - // byRoute, - // byString, copy, - // getAdditionalFeatures, getBasePathsFromServers, - // getMethodDoc, - // getSecurityDefinitionByPath, loadSpecFile, handleYaml, - // injectDependencies, - // METHOD_ALIASES, - // resolveParameterRefs, - // resolveResponseRefs, sortApiDocTags, - // sortOperationDocTags, - // toAbsolutePath, - // withNoDuplicates, } from './util'; export { @@ -63,18 +36,18 @@ export default class OpenAPIFramework implements IOpenAPIFramework { public readonly featureType; public readonly loggingPrefix; public readonly name; - private customFormats; - private dependencies; - private enableObjectCoercion; - private errorTransformer; - private externalSchemas; + // private customFormats; + // private dependencies; + // private enableObjectCoercion; + // private errorTransformer; + // private externalSchemas; private originalApiDoc; - private operations; - private paths; - private pathsIgnore; + // private operations; + // private paths; + // private pathsIgnore; private pathSecurity; - private routesGlob; - private routesIndexFileRegExp; + // private routesGlob; + // private routesIndexFileRegExp; private securityHandlers; private validateApiDoc; private validator; @@ -124,7 +97,7 @@ export default class OpenAPIFramework implements IOpenAPIFramework { } }); - this.enableObjectCoercion = !!args.enableObjectCoercion; + // this.enableObjectCoercion = !!args.enableObjectCoercion; this.originalApiDoc = handleYaml(loadSpecFile(args.apiDoc)); if (!this.originalApiDoc) { throw new Error(`spec could not be read at ${args.apiDoc}`); @@ -145,17 +118,17 @@ export default class OpenAPIFramework implements IOpenAPIFramework { (this.apiDoc as OpenAPIV2.Document).swagger, extensions: this.apiDoc[`x-${this.name}-schema-extension`], }); - this.customFormats = args.customFormats; - this.dependencies = args.dependencies; - this.errorTransformer = args.errorTransformer; - this.externalSchemas = args.externalSchemas; - this.operations = args.operations; - this.pathsIgnore = args.pathsIgnore; + // this.customFormats = args.customFormats; + // this.dependencies = args.dependencies; + // this.errorTransformer = args.errorTransformer; + // this.externalSchemas = args.externalSchemas; + // this.operations = args.operations; + // this.pathsIgnore = args.pathsIgnore; this.pathSecurity = Array.isArray(args.pathSecurity) ? args.pathSecurity : []; - this.routesGlob = args.routesGlob; - this.routesIndexFileRegExp = args.routesIndexFileRegExp; + // this.routesGlob = args.routesGlob; + // this.routesIndexFileRegExp = args.routesIndexFileRegExp; this.securityHandlers = args.securityHandlers; this.pathSecurity.forEach(assertRegExpAndSecurity.bind(null, this)); diff --git a/src/framework/types.ts b/src/framework/types.ts index ddeeb922..ff3e5f70 100644 --- a/src/framework/types.ts +++ b/src/framework/types.ts @@ -1,12 +1,5 @@ -// import { IOpenAPIDefaultSetter } from 'openapi-default-setter'; -// import { IOpenAPIRequestCoercer } from 'openapi-request-coercer'; -// import { IOpenAPIRequestValidator } from 'openapi-request-validator'; -// import { IOpenAPIResponseValidator } from 'openapi-response-validator'; import { Request } from 'express'; -import { - // IOpenAPISecurityHandler, - SecurityHandlers, -} from 'openapi-security-handler'; +import { SecurityHandlers } from 'openapi-security-handler'; import { IJsonSchema, OpenAPIV2, OpenAPIV3 } from 'openapi-types'; import { Logger } from 'ts-log'; import BasePath from './base.path'; @@ -16,32 +9,6 @@ export { OpenAPIErrorTransformer, }; -export class ConsoleDebugAdapterLogger implements Logger { - /** - * `console.debug` is just an alias for `.log()`, and we want debug logging to be optional. - * This class delegates to `console` and overrides `.debug()` to be a no-op. - */ - public debug(message?: any, ...optionalParams: any[]): void { - // no-op - } - - public error(message?: any, ...optionalParams: any[]): void { - console.error(message, ...optionalParams); - } - - public info(message?: any, ...optionalParams: any[]): void { - console.info(message, ...optionalParams); - } - - public trace(message?: any, ...optionalParams: any[]): void { - console.trace(message, ...optionalParams); - } - - public warn(message?: any, ...optionalParams: any[]): void { - console.warn(message, ...optionalParams); - } -} - // TODO move this to openapi-request-validator type OpenAPIErrorTransformer = ({}, {}) => object; @@ -111,26 +78,6 @@ export interface OpenAPIFrameworkPathContext { getPathDoc(): any; } -// export interface OpenAPIFrameworkOperationContext { -// additionalFeatures: any[]; -// allowsFeatures: boolean; -// apiDoc: any; -// basePaths: BasePath[]; -// consumes: string[]; -// features: { -// coercer?: IOpenAPIRequestCoercer; -// // defaultSetter?: IOpenAPIDefaultSetter; -// requestValidator?: IOpenAPIRequestValidator; -// // responseValidator?: IOpenAPIResponseValidator; -// securityHandler?: IOpenAPISecurityHandler; -// }; -// methodName: string; -// methodParameters: any[]; -// operationDoc: any; -// operationHandler: any; -// path: string; -// } - export interface OpenAPIFrameworkVisitor { visitApi?(context: OpenAPIFrameworkAPIContext): void; visitPath?(context: OpenAPIFrameworkPathContext): void; @@ -140,3 +87,30 @@ export interface OpenAPIFrameworkVisitor { export interface OpenApiRequest extends Request { openapi; } + +/* istanbul ignore next */ +export class ConsoleDebugAdapterLogger implements Logger { + /** + * `console.debug` is just an alias for `.log()`, and we want debug logging to be optional. + * This class delegates to `console` and overrides `.debug()` to be a no-op. + */ + public debug(message?: any, ...optionalParams: any[]): void { + // no-op + } + + public error(message?: any, ...optionalParams: any[]): void { + console.error(message, ...optionalParams); + } + + public info(message?: any, ...optionalParams: any[]): void { + console.info(message, ...optionalParams); + } + + public trace(message?: any, ...optionalParams: any[]): void { + console.trace(message, ...optionalParams); + } + + public warn(message?: any, ...optionalParams: any[]): void { + console.warn(message, ...optionalParams); + } +} diff --git a/src/middlewares/openapi.metadata.ts b/src/middlewares/openapi.metadata.ts index a91bdbf3..2944ff6f 100644 --- a/src/middlewares/openapi.metadata.ts +++ b/src/middlewares/openapi.metadata.ts @@ -14,15 +14,8 @@ export function applyOpenApiMetadata(openApiContext: OpenApiContext) { req.openapi.pathParams = pathParams; req.openapi.schema = schema; req.params = pathParams; - } else { - // add openapi object if the route was not matched - // but is contained beneath a base path - for (const bp of openApiContext.basePaths) { - if (req.path.startsWith(bp.path + '/')) { - req.openapi = {}; - break; - } - } + } else if (openApiContext.isManagedRoute(req.path)) { + req.openapi = {}; } next(); }; diff --git a/src/openapi.context.ts b/src/openapi.context.ts index aa9e3432..3ad6dbba 100644 --- a/src/openapi.context.ts +++ b/src/openapi.context.ts @@ -7,7 +7,7 @@ export class OpenApiContext { openApiRouteMap = {}; routes = []; apiDoc; - basePaths: BasePath[]; + private basePaths: Set; constructor(opts: OpenAPIFrameworkArgs) { const openApiRouteDiscovery = new OpenApiSpecLoader(opts); const { apiDoc, basePaths, routes } = openApiRouteDiscovery.load(); @@ -37,6 +37,13 @@ export class OpenApiContext { return routes; } + isManagedRoute(path) { + for (const bp of this.basePaths) { + if (path.startsWith(bp)) return true; + } + return false; + } + routePair(route) { const methods = this.methods(route); if (methods) { diff --git a/src/openapi.spec.loader.ts b/src/openapi.spec.loader.ts index b048f44a..d7e817c2 100644 --- a/src/openapi.spec.loader.ts +++ b/src/openapi.spec.loader.ts @@ -1,5 +1,4 @@ import * as _ from 'lodash'; - import OpenAPIFramework, { OpenAPIFrameworkArgs, OpenAPIFrameworkConstructorArgs, @@ -15,8 +14,12 @@ export class OpenApiSpecLoader { load() { const framework = this.createFramework(this.opts); const apiDoc = framework.apiDoc || {}; - const basePaths = framework.basePaths || []; - const routes = this.discoverRoutes(framework); + const bps = framework.basePaths || []; + const basePaths = bps.reduce((acc, bp) => { + const all = bp.all().forEach(path => acc.add(path)); + return acc; + }, new Set()); + const routes = this.discoverRoutes(framework, basePaths); return { apiDoc, basePaths, @@ -35,13 +38,13 @@ export class OpenApiSpecLoader { return framework; } - private discoverRoutes(framework) { + private discoverRoutes(framework: OpenAPIFramework, basePaths: Set) { const routes = []; const toExpressParams = this.toExpressParams; framework.initialize({ visitApi(ctx: OpenAPIFrameworkAPIContext) { const apiDoc = ctx.getApiDoc(); - for (const bp of ctx.basePaths) { + for (const bp of basePaths) { for (const [path, methods] of Object.entries(apiDoc.paths)) { for (const [method, schema] of Object.entries(methods)) { const pathParams = new Set(); @@ -50,7 +53,7 @@ export class OpenApiSpecLoader { pathParams.add(param.name); } } - const openApiRoute = `${bp.path}${path}`; + const openApiRoute = `${bp}${path}`; const expressRoute = `${openApiRoute}` .split('/') .map(toExpressParams) diff --git a/tsconfig.json b/tsconfig.json index 99b7f0b4..b2519467 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "declaration": true, + "target": "es2015", "lib": ["es6", "dom"], "module": "commonjs", "outDir": "dist",