diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a93cf4..a70a5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Fury Swagger Parser Changelog +## Master + +### Bug Fixes + +- Fixes a regression introduced in 0.22.0 where the parse result may contain + invalid references inside a JSON Schema for example values if they used + references. This regression also caused `$ref` to be incorrectly present in + Data Structure sample values. + ## 0.22.0 (2018-10-11) ### Enhancements diff --git a/package.json b/package.json index c69b125..6897f1d 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "minim": "^0.20.5", "minim-parse-result": "^0.10.1", "peasant": "1.1.0", - "swagger-zoo": "2.19.1" + "swagger-zoo": "2.19.2" }, "engines": { "node": ">=6" diff --git a/src/json-schema.js b/src/json-schema.js index 1a6e23b..8822980 100644 --- a/src/json-schema.js +++ b/src/json-schema.js @@ -5,7 +5,73 @@ function isExtension(value, key) { return _.startsWith(key, 'x-'); } -function convertSubSchema(schema, references) { +export function parseReference(reference) { + 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]; + + return id; +} + +function lookupReference(reference, root) { + const parts = reference.split('/').reverse(); + + if (parts.pop() !== '#') { + throw new Error('Schema reference must start with document root (#)'); + } + + if (parts.pop() !== 'definitions') { + throw new Error('Schema reference must be reference to #/definitions'); + } + + const id = parts[0]; + let value = root.definitions; + + while (parts.length > 0 && value !== undefined) { + const key = parts.pop(); + value = value[key]; + } + + if (value === undefined) { + throw new Error(`Reference to ${reference} does not exist`); + } + + return { + id, + referenced: value, + }; +} + +function convertExample(example, swagger) { + if (_.isArray(example)) { + return example.map(value => convertExample(value, swagger)); + } else if (_.isObject(example)) { + if (example.$ref) { + const ref = lookupReference(example.$ref, swagger); + return convertExample(ref.referenced, swagger); + } + + const result = {}; + + _.forEach(example, (value, key) => { + result[key] = convertExample(value, swagger); + }); + + return result; + } + + return example; +} + +function convertSubSchema(schema, references, swagger) { if (schema.$ref) { references.push(schema.$ref); return { $ref: schema.$ref }; @@ -20,7 +86,7 @@ function convertSubSchema(schema, references) { } if (schema.example) { - actualSchema.examples = [schema.example]; + actualSchema.examples = [convertExample(schema.example, swagger)]; } if (schema['x-nullable']) { @@ -87,35 +153,6 @@ function convertSubSchema(schema, references) { return actualSchema; } -export function parseReference(reference) { - 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]; - - return id; -} - -function lookupReference(reference, root) { - const id = parseReference(reference); - - 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) { @@ -196,10 +233,14 @@ function findReferences(schema) { } /** Convert Swagger schema to JSON Schema + * @param schema - The Swagger schema to convert + * @param root - The document root (this contains the JSON schema definitions) + * @param swagger - The swagger document root (this contains the Swagger schema definitions) + * @param copyDefinitins - Whether to copy the referenced definitions to the resulted schema */ -export function convertSchema(schema, root, copyDefinitions = true) { +export function convertSchema(schema, root, swagger, copyDefinitions = true) { let references = []; - const result = convertSubSchema(schema, references); + const result = convertSubSchema(schema, references, swagger); if (copyDefinitions) { if (references.length !== 0) { @@ -240,7 +281,7 @@ export function convertSchemaDefinitions(definitions) { if (definitions) { _.forEach(definitions, (schema, key) => { - jsonSchemaDefinitions[key] = convertSchema(schema, { definitions }, false); + jsonSchemaDefinitions[key] = convertSchema(schema, { definitions }, { definitions }, false); }); } diff --git a/src/parser.js b/src/parser.js index c1d8537..0725e86 100644 --- a/src/parser.js +++ b/src/parser.js @@ -1073,7 +1073,8 @@ export default class Parser { pushHeader('Content-Type', FORM_CONTENT_TYPE, request, this, 'form-data-content-type'); } - const jsonSchema = convertSchema(schema, { definitions: this.definitions }); + const jsonSchema = convertSchema(schema, { definitions: this.definitions }, + this.referencedSwagger); bodyFromSchema(jsonSchema, request, this, contentType || FORM_CONTENT_TYPE); // Generating data structure @@ -1550,10 +1551,12 @@ export default class Parser { pushAssets(schema, payload, contentType, pushBody) { let jsonSchema; + const referencedPathValue = this.referencedPathValue(); try { const root = { definitions: this.definitions }; - jsonSchema = convertSchema(this.referencedPathValue() || schema, root); + jsonSchema = convertSchema(referencedPathValue || schema, root, + this.referencedSwagger); } catch (error) { this.createAnnotation(annotations.VALIDATION_ERROR, this.path, error.message); return; @@ -1564,7 +1567,16 @@ export default class Parser { } this.pushSchemaAsset(schema, jsonSchema, payload, this.path); - this.pushDataStructureAsset(schema, payload); + + if (referencedPathValue && referencedPathValue.$ref) { + // If the schema is a reference just produce a data structure for the ref + this.pushDataStructureAsset(referencedPathValue, payload); + } else { + // Otherwise, we want to use the JSON Schema instead of Swagger Schema. + // In some cases, the created JSON Schema will dereference the $ref + // so we have the above if clause. + this.pushDataStructureAsset(jsonSchema, payload); + } } // Create a Refract asset element containing JSON Schema and push into payload @@ -1603,7 +1615,7 @@ export default class Parser { pushDataStructureAsset(schema, payload) { try { const generator = new DataStructureGenerator(this.minim); - const dataStructure = generator.generateDataStructure(this.referencedPathValue() || schema); + const dataStructure = generator.generateDataStructure(schema); if (dataStructure) { payload.content.push(dataStructure); } diff --git a/src/schema.js b/src/schema.js index 2aba3e8..6a6379c 100644 --- a/src/schema.js +++ b/src/schema.js @@ -209,6 +209,14 @@ export class DataStructureGenerator { null: NullElement, }; + if (schema.allOf && schema.allOf.length === 1 && schema.definitions && + Object.keys(schema).length === 2) { + // Since we can't have $ref at root with definitions. + // `allOf` with a single item is used as a work around for this type of schema + // We can safely ignore the allOf and unwrap it as normal schema in this case + return this.generateElement(schema.allOf[0]); + } + let element; if (schema.$ref) { diff --git a/test/fixtures/data-structure-generation-ref.json b/test/fixtures/data-structure-generation-ref.json new file mode 100644 index 0000000..cc82d2d --- /dev/null +++ b/test/fixtures/data-structure-generation-ref.json @@ -0,0 +1,258 @@ +{ + "element": "parseResult", + "content": [ + { + "element": "category", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "api" + } + ] + }, + "title": { + "element": "string", + "content": "Data Structure Generation" + } + }, + "attributes": { + "version": { + "element": "string", + "content": "1.0.0" + } + }, + "content": [ + { + "element": "resource", + "attributes": { + "href": { + "element": "string", + "content": "/user" + } + }, + "content": [ + { + "element": "transition", + "meta": { + "id": { + "element": "string", + "content": "getResource" + } + }, + "content": [ + { + "element": "copy", + "content": "Get a resource" + }, + { + "element": "httpTransaction", + "content": [ + { + "element": "httpRequest", + "attributes": { + "method": { + "element": "string", + "content": "GET" + } + } + }, + { + "element": "httpResponse", + "attributes": { + "statusCode": { + "element": "string", + "content": "200" + } + }, + "content": [ + { + "element": "copy", + "content": "response description" + }, + { + "element": "asset", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "messageBodySchema" + } + ] + } + }, + "attributes": { + "contentType": { + "element": "string", + "content": "application/schema+json" + } + }, + "content": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"number\"},\"name\":{\"$ref\":\"#/definitions/User\"}},\"examples\":[{\"id\":123,\"user\":{\"name\":\"Doe\"}}],\"definitions\":{\"User\":{\"type\":\"object\",\"examples\":[{\"name\":\"Doe\"}]}}}" + }, + { + "element": "dataStructure", + "content": { + "element": "object", + "attributes": { + "samples": { + "element": "array", + "content": [ + { + "element": "object", + "content": [ + { + "element": "member", + "content": { + "key": { + "element": "string", + "content": "id" + }, + "value": { + "element": "number", + "content": 123 + } + } + }, + { + "element": "member", + "content": { + "key": { + "element": "string", + "content": "user" + }, + "value": { + "element": "object", + "content": [ + { + "element": "member", + "content": { + "key": { + "element": "string", + "content": "name" + }, + "value": { + "element": "string", + "content": "Doe" + } + } + } + ] + } + } + } + ] + } + ] + } + }, + "content": [ + { + "element": "member", + "attributes": { + "typeAttributes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "optional" + } + ] + } + }, + "content": { + "key": { + "element": "string", + "content": "id" + }, + "value": { + "element": "number", + "content": null + } + } + }, + { + "element": "member", + "attributes": { + "typeAttributes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "optional" + } + ] + } + }, + "content": { + "key": { + "element": "string", + "content": "name" + }, + "value": { + "element": "definitions/User", + "content": null + } + } + } + ] + } + } + ] + } + ] + } + ] + } + ] + }, + { + "element": "category", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "dataStructures" + } + ] + } + }, + "content": [ + { + "element": "dataStructure", + "content": { + "element": "object", + "meta": { + "id": { + "element": "string", + "content": "definitions/User" + } + }, + "content": [ + { + "element": "member", + "content": { + "key": { + "element": "string", + "content": "name" + }, + "value": { + "element": "string", + "content": "Doe" + } + } + } + ] + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/test/fixtures/data-structure-generation-ref.yaml b/test/fixtures/data-structure-generation-ref.yaml new file mode 100644 index 0000000..f945348 --- /dev/null +++ b/test/fixtures/data-structure-generation-ref.yaml @@ -0,0 +1,28 @@ +swagger: "2.0" +info: + version: 1.0.0 + title: Data Structure Generation +paths: + /user: + get: + description: Get a resource + operationId: getResource + responses: + 200: + description: response description + schema: + type: object + example: + id: 123 + user: + $ref: '#/definitions/User/example' + properties: + id: + type: number + name: + $ref: '#/definitions/User' +definitions: + User: + type: object + example: + name: Doe diff --git a/test/json-schema.js b/test/json-schema.js index 2f16dc6..57f7b3a 100644 --- a/test/json-schema.js +++ b/test/json-schema.js @@ -53,6 +53,78 @@ describe('Swagger Schema to JSON Schema', () => { ], }); }); + + it('dereferences Swagger example extension to examples', () => { + const root = { + definitions: { + User: { + example: { message: 'hello' }, + }, + }, + }; + const swaggerSchema = { + type: 'object', + example: { $ref: '#/definitions/User/example' }, + }; + const schema = convertSchema(swaggerSchema, {}, root); + + expect(schema).to.deep.equal({ + type: 'object', + examples: [ + { message: 'hello' }, + ], + }); + }); + + it('dereferences nested object Swagger example extension to examples', () => { + const root = { + definitions: { + User: { + example: { message: 'hello' }, + }, + }, + }; + const swaggerSchema = { + type: 'object', + example: { + user: { $ref: '#/definitions/User/example' }, + }, + }; + const schema = convertSchema(swaggerSchema, {}, root); + + expect(schema).to.deep.equal({ + type: 'object', + examples: [ + { + user: { message: 'hello' }, + }, + ], + }); + }); + + it('dereferences nested array Swagger example extension to examples', () => { + const root = { + definitions: { + User: { + example: { message: 'hello' }, + }, + }, + }; + const swaggerSchema = { + type: 'object', + example: [ + { $ref: '#/definitions/User/example' }, + ], + }; + const schema = convertSchema(swaggerSchema, {}, root); + + expect(schema).to.deep.equal({ + type: 'object', + examples: [ + [{ message: 'hello' }], + ], + }); + }); }); context('x-nullable', () => {