Skip to content
This repository has been archived by the owner on Jul 2, 2019. It is now read-only.

Commit

Permalink
Browse files Browse the repository at this point in the history
Validate against Swagger schema and spec
  • Loading branch information
danielgtaylor committed Dec 7, 2015
1 parent 58fda5c commit f927a3a
Show file tree
Hide file tree
Showing 25 changed files with 1,452 additions and 198 deletions.
1 change: 1 addition & 0 deletions Changelog.md
@@ -1,5 +1,6 @@
# Unreleased

- Generate Swagger schema and spec validation annotations.
- Implement support for link relations in parser annotations.
- Encode dashes (`-`) in URI template parameters.

Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -39,6 +39,7 @@ Code | Description
---: | -----------
2 | Source maps are unavailable due either to the input format or an issue parsing the input.
3 | Data is being lost in the conversion.
4 | Swagger validation error.

Errors:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -19,7 +19,7 @@
"dependencies": {
"babel-runtime": "^5.8.20",
"js-yaml": "^3.4.2",
"json-schema-ref-parser": "^1.4.0",
"swagger-parser": "^3.3.0",
"underscore": "^1.8.3",
"yaml-js": "^0.1.3"
},
Expand Down
68 changes: 55 additions & 13 deletions src/adapter.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import buildUriTemplate from './uri-template';
import $RefParser from 'json-schema-ref-parser';
import SwaggerParser from 'swagger-parser';
import yaml from 'js-yaml';
import yamlAst from 'yaml-js';

Expand All @@ -11,17 +11,22 @@ const ANNOTATIONS = {
CANNOT_PARSE: {
type: 'error',
code: 1,
relation: 'yaml-parser',
fragment: 'yaml-parser',
},
AST_UNAVAILABLE: {
type: 'warning',
code: 2,
relation: 'yaml-parser',
fragment: 'yaml-parser',
},
DATA_LOST: {
type: 'warning',
code: 3,
relation: 'refract-not-supported',
fragment: 'refract-not-supported',
},
VALIDATION_ERROR: {
type: 'warning',
code: 4,
fragment: 'swagger-validation',
},
};

Expand Down Expand Up @@ -84,7 +89,7 @@ function useResourceGroups(api) {
// Look up a position in the original source based on a JSON path, for
// example 'paths./test.get.responses.200'
function getPosition(ast, path) {
const pieces = path.split('.');
const pieces = _.isArray(path) ? path.splice(0) : path.split('.');
let end;
let node = ast;
let piece = pieces.shift();
Expand All @@ -103,7 +108,7 @@ function getPosition(ast, path) {
}

for (const subNode of node.value) {
if (subNode[0].value === piece) {
if (subNode[0] && subNode[0].value === piece) {
if (pieces.length) {
newNode = subNode[1];
if (index !== null) {
Expand All @@ -122,7 +127,7 @@ function getPosition(ast, path) {
}
}
break;
} else if (subNode[0].value === '$ref') {
} else if (subNode[0] && subNode[0].value === '$ref') {
if (subNode[1].value.indexOf('#') === 0) {
// This is an internal reference! First, we reset the node to the
// root of the document, shift the ref item off the pieces stack
Expand Down Expand Up @@ -166,15 +171,16 @@ function makeAnnotation(Annotation, Link, SourceMap, ast, result, info, path, me
annotation.code = info.code;
result.content.push(annotation);

if (info.relation) {
if (info.fragment) {
const link = new Link();
link.relation = info.relation;
link.relation = 'origin';
link.href = `http://docs.apiary.io/validations/swagger#${info.fragment}`;
annotation.links.push(link);
}

if (ast && path) {
const position = getPosition(ast, path);
if (position) {
if (position && !isNaN(position.start) && !isNaN(position.end)) {
annotation.attributes.set('sourceMap', [
new SourceMap([[position.start, position.end - position.start]]),
]);
Expand Down Expand Up @@ -276,6 +282,7 @@ export function parse({minim, source, generateSourceMap}, done) {
const paramToElement = convertParameterToElement.bind(
convertParameterToElement, minim);

const parser = new SwaggerParser();
const parseResult = new ParseResult();

let loaded;
Expand Down Expand Up @@ -304,9 +311,44 @@ export function parse({minim, source, generateSourceMap}, done) {
'Source maps are only available with string input');
}

$RefParser.dereference(loaded, (err, swagger) => {
// Some sane defaults since these are sometimes left out completely
if (loaded.info === undefined) {
loaded.info = {};
}

if (loaded.paths === undefined) {
loaded.paths = {};
}

// Parse and validate the Swagger document!
parser.validate(loaded, (err) => {
const swagger = parser.api;

if (err) {
return done(err, parseResult);
if (swagger === undefined) {
return done(err, parseResult);
}

// Non-fatal errors, so let us try and create annotations for them and
// continue with the parsing as best we can.
if (err.details) {
const queue = [err.details];
while (queue.length) {
for (const item of queue[0]) {
makeAnnotation(Annotation, Link, SourceMap, ast, parseResult,
ANNOTATIONS.VALIDATION_ERROR, item.path, item.message);

if (item.inner) {
// TODO: I am honestly not sure what the correct behavior is
// here. Some items will have within them a tree of other items,
// some of which might contain more info (but it's unclear).
// Do we treat them as their own error or do something else?
queue.push(item.inner);
}
}
queue.shift();
}
}
}

const basePath = (swagger.basePath || '').replace(/[/]+$/, '');
Expand Down Expand Up @@ -484,7 +526,7 @@ export function parse({minim, source, generateSourceMap}, done) {
if (formParameters.length) {
setupAnnotation(ANNOTATIONS.DATA_LOST,
`paths.${href}.${method}.parameters`,
'Form data paremeters are not yet supported');
'Form data parameters are not yet supported');
}

const hrefForResource = buildUriTemplate(basePath, href, pathObjectParameters, queryParameters);
Expand Down
72 changes: 32 additions & 40 deletions test/adapter.js
Expand Up @@ -12,6 +12,35 @@ import {expect} from 'chai';

fury.adapters = [adapter];

function testFixture(description, filename, subDir = true, generateSourceMap = false) {
it(description, (done) => {
const source = fs.readFileSync(filename, 'utf8');
let base = 'fixtures';

if (subDir) {
base = path.join(base, 'refract');
}

const expectedName = './' + path.join(base, path.basename(filename, path.extname(filename)) + '.json');
let expected = require(expectedName);

fury.parse({source, generateSourceMap}, (err, output) => {
if (err) {
return done(err);
}

// Invoke with the env var GENERATE set to regenerate the fixtures.
if (process.env.GENERATE) {
expected = output.toRefract();
fs.writeFileSync(path.join(__dirname, expectedName), JSON.stringify(expected, null, 2), 'utf8');
}

expect(output.toRefract()).to.deep.equal(expected);
done();
});
});
}

describe('Swagger 2.0 adapter', () => {
context('detection', () => {
it('detects JSON', () => {
Expand Down Expand Up @@ -76,51 +105,14 @@ describe('Swagger 2.0 adapter', () => {
});

context('source maps & annotations', () => {
it('can generate source maps', (done) => {
const source = fs.readFileSync('./test/fixtures/sourcemaps.yaml', 'utf8');
const expected = require('./fixtures/sourcemaps.json');

fury.parse({source, generateSourceMap: true}, (err, output) => {
if (err) {
return done(err);
}

expect(output.toRefract()).to.deep.equal(expected);
done();
});
});

it('can generate annotations', (done) => {
const source = fs.readFileSync('./test/fixtures/annotations.yaml', 'utf8');
const expected = require('./fixtures/annotations.json');

fury.parse({source, generateSourceMap: true}, (err, output) => {
if (err) {
return done(err);
}

expect(output.toRefract()).to.deep.equal(expected);
done();
});
});
testFixture('can generate source maps', './test/fixtures/sourcemaps.yaml', false, true);
testFixture('can generate annotations', './test/fixtures/annotations.yaml', false);
});

describe('can parse fixtures', () => {
const filenames = glob.sync('./test/fixtures/swagger/*.@(json|yaml)');
filenames.forEach((filename) => {
it(`Parses ${path.basename(filename, path.extname(filename))}`, (done) => {
const source = fs.readFileSync(filename, 'utf8');
const expected = require('./' + path.join('fixtures', 'refract', path.basename(filename, path.extname(filename)) + '.json'));

fury.parse({source}, (err, output) => {
if (err) {
return done(err);
}

expect(output.toRefract()).to.deep.equal(expected);
done();
});
});
testFixture(`Parses ${path.basename(filename, path.extname(filename))}`, filename);
});
});
});

0 comments on commit f927a3a

Please sign in to comment.