diff --git a/.gitignore b/.gitignore index 14b89da..505276d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ pids *.seed lib-cov .idea +.vscode build node_modules typings diff --git a/package.json b/package.json index 9c7592b..b9676e4 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,63 @@ { - "name": "swagger2", - "version": "0.0.20", - "description": "Typescript-based tools for working with Swagger v2.0 documents", - "main": "index.js", - "repository": { - "type": "git", - "url": "git+https://github.com/carlansley/swagger2.git" - }, - "keywords": [ - "swagger", - "typescript", - "koa", - "koa2" - ], - "author": "Carl Ansley", - "license": "MIT", - "bugs": { - "url": "https://github.com/carlansley/swagger2/issues" - }, - "homepage": "https://github.com/carlansley/swagger2#readme", - "typings": "./dist/swagger.d.ts", - "dependencies": { - "is-my-json-valid": "^2.15.0", - "json-refs": "^2.1.6", - "json-schema-deref-sync": "^0.3.3", - "yamljs": "^0.2.8" - }, - "devDependencies": { - "@types/mocha": "^2.2.33", - "@types/nock": "^8.2.0", - "@types/node": "^6.0.52", - "@types/yamljs": "^0.2.30", - "coveralls": "^2.11.15", - "expectations": "^0.6.0", - "istanbul": "^1.1.0-alpha.1", - "mocha": "^3.2.0", - "nock": "^9.0.2", - "remap-istanbul": "^0.8.0", - "tslint": "^4.0.2", - "typescript": "^2.1.4" - }, - "maintainers": [ - { - "email": "carl.ansley@gmail.com", - "name": "Carl Ansley" + "name": "swagger2", + "version": "0.0.20", + "description": "Typescript-based tools for working with Swagger v2.0 documents", + "main": "index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/carlansley/swagger2.git" + }, + "keywords": [ + "swagger", + "typescript", + "koa", + "koa2" + ], + "author": "Carl Ansley", + "license": "MIT", + "bugs": { + "url": "https://github.com/carlansley/swagger2/issues" + }, + "homepage": "https://github.com/carlansley/swagger2#readme", + "typings": "./dist/swagger.d.ts", + "dependencies": { + "is-my-json-valid": "^2.15.0", + "json-refs": "^2.1.6", + "json-schema-deref": "^0.3.5", + "json-schema-deref-sync": "^0.3.3", + "yamljs": "^0.2.8" + }, + "devDependencies": { + "@types/mocha": "^2.2.33", + "@types/nock": "^8.2.0", + "@types/node": "^6.0.52", + "@types/yamljs": "^0.2.30", + "coveralls": "^2.11.15", + "expectations": "^0.6.0", + "istanbul": "^1.1.0-alpha.1", + "mocha": "^3.2.0", + "nock": "^9.0.2", + "remap-istanbul": "^0.8.0", + "tslint": "^4.0.2", + "typescript": "^2.1.4" + }, + "maintainers": [{ + "email": "carl.ansley@gmail.com", + "name": "Carl Ansley" + }], + "scripts": { + "preversion": "npm test", + "version": "npm run dist && git add -A dist", + "postversion": "git push && git push --tags", + "build": "rm -rf build && tsc && cp src/schema.json build", + "dist": "rm -rf dist && tsc src/typings.d.ts src/swagger.ts -m commonjs --outDir dist --sourcemap --target es5 -d --pretty --noImplicitAny && cp src/schema.json dist", + "clean": "rm -rf build && rm -rf coverage && rm -rf node_modules", + "lint": "tslint -c ./tslint.json --project ./tsconfig.json", + "test": "npm run build && _mocha --require expectations $(find build -name '*.spec.js') && npm run lint", + "cover": "npm run build && npm run cover:istanbul && npm run cover:remap", + "cover:browser": "npm run cover && istanbul report html && open coverage/coverage-remapped/index.html", + "cover:istanbul": "rm -rf ./coverage && istanbul cover _mocha -- --require expectations $(find build -name '*.spec.js')", + "cover:remap": "remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped.json && remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped.lcov -t lcovonly && remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped -t html", + "coveralls": "npm run-script cover && coveralls < coverage/coverage-remapped.lcov" } - ], - "scripts": { - "preversion": "npm test", - "version": "npm run dist && git add -A dist", - "postversion": "git push && git push --tags", - "build": "rm -rf build && tsc && cp src/schema.json build", - "dist": "rm -rf dist && tsc src/typings.d.ts src/swagger.ts -m commonjs --outDir dist --sourcemap --target es5 -d --pretty --noImplicitAny && cp src/schema.json dist", - "clean": "rm -rf build && rm -rf coverage && rm -rf node_modules", - "lint": "tslint -c ./tslint.json --project ./tsconfig.json", - "test": "npm run build && _mocha --require expectations $(find build -name '*.spec.js') && npm run lint", - "cover": "npm run build && npm run cover:istanbul && npm run cover:remap", - "cover:browser": "npm run cover && istanbul report html && open coverage/coverage-remapped/index.html", - "cover:istanbul": "rm -rf ./coverage && istanbul cover _mocha -- --require expectations $(find build -name '*.spec.js')", - "cover:remap": "remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped.json && remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped.lcov -t lcovonly && remap-istanbul -i coverage/coverage.raw.json -o coverage/coverage-remapped -t html", - "coveralls": "npm run-script cover && coveralls < coverage/coverage-remapped.lcov" - } -} +} \ No newline at end of file diff --git a/src/compiler.ts b/src/compiler.ts index 816aa1f..7219730 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -29,7 +29,6 @@ */ import * as jsonValidator from 'is-my-json-valid'; -import * as deref from 'json-schema-deref-sync'; import {CollectionFormat, Definition, Document, Parameter, PathItem} from './schema'; @@ -142,12 +141,9 @@ function stringValidator(schema: any) { export function compile(document: Document): Compiled { - // get the de-referenced version of the swagger document - let swagger = deref(document); - // add a validator for every parameter in swagger document - Object.keys(swagger.paths).forEach((pathName) => { - let path = swagger.paths[pathName]; + Object.keys(document.paths).forEach((pathName) => { + let path = document.paths[pathName]; Object.keys(path).filter((name) => name !== 'parameters').forEach((operationName) => { let operation = path[operationName]; @@ -188,12 +184,12 @@ export function compile(document: Document): Compiled { }); }); - let basePath = swagger.basePath || ''; - let matcher: CompiledPath[] = Object.keys(swagger.paths) + let basePath = document.basePath || ''; + let matcher: CompiledPath[] = Object.keys(document.paths) .map((name) => { return { name, - path: swagger.paths[name], + path: document.paths[name], regex: new RegExp(basePath + name.replace(/\{[^}]*}/g, '[^/]+') + '$'), expected: (name.match(/[^\/]+/g) || []).map((s) => s.toString()) }; diff --git a/src/deref.spec.ts b/src/deref.spec.ts new file mode 100644 index 0000000..d5a69d1 --- /dev/null +++ b/src/deref.spec.ts @@ -0,0 +1,99 @@ +// dref.spec.ts + +/* + The MIT License + + Copyright (c) 2016 Christian Holzberger + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the 'Software'), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ +import * as assert from 'assert'; +import * as swagger from './swagger'; + +const EXT_YML = { + name: 'base', + ref: { + name: 'ext' + } +}; +/* see nested dereference bug +const ext_nested_yml = { + name: 'base', + ref: { + name: 'nested', + } +}; +*/ + +const INT_YML = { + name: 'base', + intname: 'int', + ref: { + name: 'int' + } +}; + +const SUB_YML = { + name: 'base-sub', + sub: { + name: 'sub-level1', + sub: { + name: 'sub-level2' + } + } +}; + +const baseFolder = __dirname + '/../test/yaml/deref/'; + +describe('Reference resolution', () => { + + describe('in async mode', () => { + + it('does dereference external references in yaml files', () => { + const raw = swagger.loadDocumentSync(baseFolder + 'base.yaml'); + return swagger.deref(raw, {baseFolder}).then ((document) => { + assert.deepStrictEqual(document, EXT_YML); + }); + }); + /* + * this doesn't work and i can't figure out why... + * perhaps a bug in json-schema-deref? + + it('does dereference nested external references in yaml files', () => { + const raw = swagger.loadDocumentSync(baseFolder + 'base.nested.yaml'); + return swagger.deref(raw, {baseFolder: baseFolder}).then ((document) => { + assert.deepStrictEqual(document, ext_nested_yml); + }); + }); + */ + it('does dereference internal references in yaml files', () => { + const raw = swagger.loadDocumentSync(baseFolder + 'internal.yaml'); + return swagger.deref(raw, {baseFolder}).then ((document) => { + assert.deepStrictEqual(document, INT_YML); + }); + }); + + it('does dereference external references to yaml files in sub folders', () => { + const raw = swagger.loadDocumentSync(baseFolder + 'base.sub.yaml'); + return swagger.deref(raw, {baseFolder}).then ((document) => { + assert.deepStrictEqual(document, SUB_YML); + }); + }); + }); +}); diff --git a/src/deref.ts b/src/deref.ts new file mode 100644 index 0000000..c6ea7cc --- /dev/null +++ b/src/deref.ts @@ -0,0 +1,98 @@ +// deref.ts + +/* + The MIT License + + Copyright (c) 2014-2016 Carl Ansley, Christian Holzberger + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ +import js_deref = require('json-schema-deref'); +import js_deref_sync = require('json-schema-deref-sync'); +import * as YAML from 'yamljs'; +import path = require('path'); + +/** + * deref is not exporting their options interface + */ +export interface DerefOptions { + baseFolder?: string; + cache?: boolean; + cacheTTL?: number; + failOnMissing?: boolean; + loader?: any; +} + +/** + * custom loader for yml references + */ +async function ymlLoader( ref: string, derefOptions: DerefOptions, fn: any ) { + const isYamlRegex = /(.*\.y(a|)ml)($|#)/i; + const match = ref.match(isYamlRegex); + + if ( match !== null ) { + const refFileName = match[1]; + let yamlRef = YAML.load(path.join(derefOptions.baseFolder, refFileName)); + + const optionsOverride = { + loader: ymlLoader, + baseFolder: path.dirname(path.join(derefOptions.baseFolder, refFileName)) + }; + + const options = Object.assign({}, derefOptions, optionsOverride); + let resolved = await derefp(yamlRef, options); + fn(null, resolved); + } else { + fn(); + } +} + +/* + * A wrapper for async deref to provide a promise instead + */ +async function derefp(document: any, options?: any): Promise { + let p = new Promise ((resolve, reject) => { + js_deref(document, options, (err: any, derefedDocument: any) => { + if ( err ) { + reject(err); + } else { + resolve(derefedDocument); + } + }); + }); + return p; +} + +/** + * just an alias for sync dereffing of input document + */ +export function derefSync(document: any) { + return js_deref_sync(document); +} + +/** + * async dereffing of input document + */ +export async function deref(document: any, derefOptions?: DerefOptions ) { + let optionsOverride: DerefOptions = { + loader: ymlLoader + }; + const options = Object.assign({}, derefOptions , optionsOverride); + return await derefp(document, options); +} diff --git a/src/swagger.spec.ts b/src/swagger.spec.ts index 0cd8d06..879d3eb 100644 --- a/src/swagger.spec.ts +++ b/src/swagger.spec.ts @@ -30,7 +30,7 @@ import * as swagger from './swagger'; function compile(fileName: string) { const raw = swagger.loadDocumentSync(fileName); - const document: swagger.Document | undefined = swagger.validateDocument(raw); + const document: swagger.Document | undefined = swagger.derefSync( swagger.validateDocument(raw) ); /* istanbul ignore if */ if (document === undefined) { diff --git a/src/swagger.ts b/src/swagger.ts index 427d575..6d5d7b5 100644 --- a/src/swagger.ts +++ b/src/swagger.ts @@ -32,6 +32,7 @@ */ import * as compiler from './compiler'; +import * as ref from './deref'; import * as document from './document'; import * as schema from './schema'; import * as validate from './validate'; @@ -42,5 +43,8 @@ export const validateRequest = validate.request; export const validateResponse = validate.response; export const compileDocument = compiler.compile; +export const deref = ref.deref; +export const derefSync = ref.derefSync; + export import Compiled = compiler.Compiled; export import Document = schema.Document; diff --git a/test/yaml/deref/base.nested.yaml b/test/yaml/deref/base.nested.yaml new file mode 100644 index 0000000..d2adb1b --- /dev/null +++ b/test/yaml/deref/base.nested.yaml @@ -0,0 +1,3 @@ +name: "base" +ref: + $ref: './ext.nested.yaml#/name' diff --git a/test/yaml/deref/base.sub.yaml b/test/yaml/deref/base.sub.yaml new file mode 100644 index 0000000..f90a548 --- /dev/null +++ b/test/yaml/deref/base.sub.yaml @@ -0,0 +1,3 @@ +name: base-sub +sub: + $ref: "sub/sub.yaml" diff --git a/test/yaml/deref/base.yaml b/test/yaml/deref/base.yaml new file mode 100644 index 0000000..e4b4afc --- /dev/null +++ b/test/yaml/deref/base.yaml @@ -0,0 +1,3 @@ +name: "base" +ref: + $ref: "ext.yaml" diff --git a/test/yaml/deref/ext.nested.yaml b/test/yaml/deref/ext.nested.yaml new file mode 100644 index 0000000..b900e19 --- /dev/null +++ b/test/yaml/deref/ext.nested.yaml @@ -0,0 +1,2 @@ +nested: + name: "nested" \ No newline at end of file diff --git a/test/yaml/deref/ext.yaml b/test/yaml/deref/ext.yaml new file mode 100644 index 0000000..82ca5e0 --- /dev/null +++ b/test/yaml/deref/ext.yaml @@ -0,0 +1 @@ +name: 'ext' \ No newline at end of file diff --git a/test/yaml/deref/internal.yaml b/test/yaml/deref/internal.yaml new file mode 100644 index 0000000..363685d --- /dev/null +++ b/test/yaml/deref/internal.yaml @@ -0,0 +1,5 @@ +name: "base" +intname: "int" +ref: + name: + $ref: "#/intname" diff --git a/test/yaml/deref/sub/sub.yaml b/test/yaml/deref/sub/sub.yaml new file mode 100644 index 0000000..635230f --- /dev/null +++ b/test/yaml/deref/sub/sub.yaml @@ -0,0 +1,3 @@ +name: sub-level1 +sub: + $ref: sub/sub2.yaml \ No newline at end of file diff --git a/test/yaml/deref/sub/sub/sub2.yaml b/test/yaml/deref/sub/sub/sub2.yaml new file mode 100644 index 0000000..153b143 --- /dev/null +++ b/test/yaml/deref/sub/sub/sub2.yaml @@ -0,0 +1 @@ +name: sub-level2 \ No newline at end of file