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

Commit

Permalink
Merge pull request #205 from apiaryio/kylef/circular
Browse files Browse the repository at this point in the history
Support circular referencing in JSON / JSON Schema
  • Loading branch information
pksunkara authored Sep 4, 2018
2 parents 4cac366 + 605e9d0 commit f0d4cd7
Show file tree
Hide file tree
Showing 10 changed files with 704 additions and 101 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
- The API Version is now exposed in the parse result. The API category now
contains a version attribute including the API version.

- Circular references are now supported in schemas and JSON and JSON Schemas
will now be present in parse results when you use circular references.

### Bug Fixes

- Example values in a Schema Object will now be placed into the dataStructure
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"minim": "^0.20.5",
"minim-parse-result": "^0.10.1",
"peasant": "1.1.0",
"swagger-zoo": "2.16.0"
"swagger-zoo": "2.17.0"
},
"engines": {
"node": ">=6"
Expand Down
104 changes: 91 additions & 13 deletions src/json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ function isExtension(value, key) {
return _.startsWith(key, 'x-');
}

function convertSubSchema(schema) {
function convertSubSchema(schema, references) {
if (schema.$ref) {
references.push(schema.$ref);
return { $ref: schema.$ref };
}

let actualSchema = _.omit(schema, ['discriminator', 'readOnly', 'xml', 'externalDocs', 'example']);
actualSchema = _.omitBy(actualSchema, isExtension);

Expand All @@ -26,58 +31,131 @@ function convertSubSchema(schema) {
}

if (schema.allOf) {
actualSchema.allOf = schema.allOf.map(convertSubSchema);
actualSchema.allOf = schema.allOf.map(s => convertSubSchema(s, references));
}

if (schema.anyOf) {
actualSchema.anyOf = schema.anyOf.map(convertSubSchema);
actualSchema.anyOf = schema.anyOf.map(s => convertSubSchema(s, references));
}

if (schema.oneOf) {
actualSchema.oneOf = schema.oneOf.map(convertSubSchema);
actualSchema.oneOf = schema.oneOf.map(s => convertSubSchema(s, references));
}

if (schema.not) {
actualSchema.not = convertSubSchema(schema.not);
actualSchema.not = convertSubSchema(schema.not, references);
}

// Array

if (schema.items) {
if (Array.isArray(schema.items)) {
actualSchema.items = schema.items.map(convertSubSchema);
actualSchema.items = schema.items.map(s => convertSubSchema(s, references));
} else {
actualSchema.items = convertSubSchema(schema.items);
actualSchema.items = convertSubSchema(schema.items, references);
}
}

if (schema.additionalItems && typeof schema.additionalItems === 'object') {
actualSchema.additionalItems = convertSubSchema(schema.additionalItems);
actualSchema.additionalItems = convertSubSchema(schema.additionalItems, references);
}

// Object

if (schema.properties) {
Object.keys(schema.properties).forEach((key) => {
actualSchema.properties[key] = convertSubSchema(schema.properties[key]);
actualSchema.properties[key] = convertSubSchema(schema.properties[key], references);
});
}

if (schema.patternProperties) {
Object.keys(schema.patternProperties).forEach((key) => {
actualSchema.patternProperties[key] = convertSubSchema(schema.patternProperties[key]);
actualSchema.patternProperties[key] =
convertSubSchema(schema.patternProperties[key], references);
});
}

if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
actualSchema.additionalProperties = convertSubSchema(schema.additionalProperties);
actualSchema.additionalProperties = convertSubSchema(schema.additionalProperties, references);
}

return actualSchema;
}

function lookupReference(reference, root) {
const parts = reference.split('/');

if (parts[0] !== '#') {
throw new Error('Schema reference must start with document root (#)');
}

if (parts[1] !== 'definitions' || parts.length !== 3) {
throw new Error('Schema reference must be reference to #/definitions');
}

const id = parts[2];

if (!root.definitions || !root.definitions[id]) {
throw new Error(`Reference to ${reference} does not exist`);
}

return {
id,
referenced: root.definitions[id],
};
}

/** Returns true if the given schema contains any references
*/
function checkSchemaHasReferences(schema) {
if (schema.$ref) {
return true;
}

return Object.values(schema).some((value) => {
if (_.isArray(value)) {
return value.some(checkSchemaHasReferences);
} else if (_.isObject(value)) {
return checkSchemaHasReferences(value);
}

return false;
});
}

/** Convert Swagger schema to JSON Schema
*/
export default function convertSchema(schema) {
return convertSubSchema(schema);
export default function convertSchema(schema, root) {
const references = [];
const result = convertSubSchema(schema, references);

if (references.length !== 0) {
result.definitions = {};
}

while (references.length !== 0) {
const lookup = lookupReference(references.pop(), root);

if (result.definitions[lookup.id] === undefined) {
result.definitions[lookup.id] = convertSubSchema(lookup.referenced, references);
}
}

if (result.$ref) {
const reference = lookupReference(result.$ref, root);

if (!checkSchemaHasReferences(result.definitions[reference.id])) {
// Dereference the root reference if possible
return result.definitions[reference.id];
}

// Wrap any root reference in allOf because faker will end up in
// loop with root references which is avoided with allOf
return {
allOf: [{ $ref: result.$ref }],
definitions: result.definitions,
};
}

return result;
}
32 changes: 11 additions & 21 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default class Parser {
const swaggerOptions = {
$refs: {
external: false,
circular: 'ignore',
},
};

Expand Down Expand Up @@ -1091,27 +1092,19 @@ export default class Parser {
if (responseBody !== undefined) {
this.withPath(contentType, () => {
let formattedResponseBody = responseBody;
let serialized = true;

if (typeof responseBody !== 'string') {
try {
formattedResponseBody = JSON.stringify(responseBody, null, 2);
} catch (exception) {
this.createAnnotation(annotations.DATA_LOST, this.path, 'Circular references in examples are not yet supported.');
serialized = false;
}
formattedResponseBody = JSON.stringify(responseBody, null, 2);
}

if (serialized) {
const bodyAsset = new Asset(formattedResponseBody);
bodyAsset.classes.push('messageBody');

if (this.generateSourceMap) {
this.createSourceMap(bodyAsset, this.path);
}
const bodyAsset = new Asset(formattedResponseBody);
bodyAsset.classes.push('messageBody');

response.content.push(bodyAsset);
if (this.generateSourceMap) {
this.createSourceMap(bodyAsset, this.path);
}

response.content.push(bodyAsset);
});
}

Expand Down Expand Up @@ -1489,12 +1482,9 @@ export default class Parser {
let jsonSchema;

try {
jsonSchema = convertSchema(schema);
} catch (exception) {
this.createAnnotation(
annotations.DATA_LOST, this.path,
'Circular references in schema are not yet supported',
);
jsonSchema = convertSchema(schema, this.swagger);
} catch (error) {
this.createAnnotation(annotations.VALIDATION_ERROR, this.path, error.message);
return;
}

Expand Down
Loading

0 comments on commit f0d4cd7

Please sign in to comment.