diff --git a/lib/30XUtils/componentsParentMatcher.js b/lib/30XUtils/componentsParentMatcher.js new file mode 100644 index 0000000..570f9b9 --- /dev/null +++ b/lib/30XUtils/componentsParentMatcher.js @@ -0,0 +1,72 @@ +const COMPONENTS_KEYS_30 = [ + 'schemas', + 'responses', + 'parameters', + 'examples', + 'requestBodies', + 'headers', + 'securitySchemes', + 'links', + 'callbacks' + ], + SCHEMA_CONTAINERS = [ + 'allOf', + 'oneOf', + 'anyOf', + 'not', + 'additionalProperties', + 'items', + 'schema' + ], + EXAMPLE_CONTAINERS = [ + 'example' + ], + PROPERTY_DEFINITION = [ + 'properties' + ]; + +module.exports = { + /** + * Generates the trace to the key that will wrap de component using 3.0 version + * @param {array} traceFromParent - The trace from the parent key + * @param {string} filePathName - The filePath name from the file + * @param {string} localPart - The local path part + * @param {function} jsonPointerDecodeAndReplace - Function to decode a json pointer + * @returns {array} The trace to the container key + */ + getKeyInComponents30: function (traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace) { + let res = traceFromParent, + trace = [], + traceToKey = [], + matchFound = false, + isInComponents = traceFromParent[0] === 'components'; + + if (isInComponents) { + return []; + } + + res.push(jsonPointerDecodeAndReplace(`${filePathName}${localPart}`)); + trace = [...res].reverse(); + + for (let [index, item] of trace.entries()) { + if (SCHEMA_CONTAINERS.includes(item)) { + item = 'schemas'; + } + if (EXAMPLE_CONTAINERS.includes(item)) { + item = 'examples'; + } + if (PROPERTY_DEFINITION.includes(trace[index + 2])) { + trace[index + 1] = 'schemas'; + } + traceToKey.push(item); + if (COMPONENTS_KEYS_30.includes(item)) { + matchFound = true; + break; + } + } + return matchFound ? + traceToKey.reverse() : + []; + }, + COMPONENTS_KEYS_30 +}; diff --git a/lib/bundle.js b/lib/bundle.js index b1a90d9..b1e3941 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -6,10 +6,12 @@ const { removeLocalReferenceFromPath, localPointer, jsonPointerLevelSeparator, - isLocalRef + isLocalRef, + jsonPointerDecodeAndReplace } = require('./jsonPointer'), traverseUtility = require('traverse'), - parse = require('./parse.js'); + parse = require('./parse.js'), + { ParseError } = require('./common/ParseError'); let path = require('path'), pathBrowserify = require('path-browserify'), @@ -40,6 +42,18 @@ function comparePaths(path1, path2) { return path1 === path2; } +/** + * Parses a node content or throw ParseError if there's any error + * @param {string} fileContent The content from the current node + * @returns {object} The parsed content + */ +function parseFileOrThrow(fileContent) { + const result = parse.getOasObject(fileContent); + if (result.result === false) { + throw new ParseError(result.reason); + } + return result; +} /** * Calculates the path relative to parent @@ -136,7 +150,9 @@ function getContentFromTrace(content, partial) { return content; } partial = partial[0] === jsonPointerLevelSeparator ? partial.substring(1) : partial; - const trace = partial.split(jsonPointerLevelSeparator); + const trace = partial.split(jsonPointerLevelSeparator).map((item) => { + return jsonPointerDecodeAndReplace(item); + }); let currentValue = content; currentValue = deref._getEscaped(content, trace, undefined); return currentValue; @@ -196,8 +212,8 @@ function getTraceFromParentKeyInComponents(nodeContext, property) { [key, ...parentKeys], nodeTrace = getRootFileTrace(nodeParentsKey), [file, local] = property.split(localPointer), - [keyTraceInComponents, inComponents] = getKeyInComponents(nodeTrace, file, local); - return [keyTraceInComponents, inComponents]; + keyTraceInComponents = getKeyInComponents(nodeTrace, file, local); + return keyTraceInComponents; } /** @@ -227,7 +243,7 @@ function getReferences (currentNode, isOutOfRoot, pathSolver, parentFilename) { ); if (hasReferenceTypeKey) { const tempRef = calculatePath(parentFilename, property.$ref), - [nodeTrace] = getTraceFromParentKeyInComponents(this, tempRef), + nodeTrace = getTraceFromParentKeyInComponents(this, tempRef), referenceInDocument = getJsonPointerRelationToRoot( jsonPointerEncodeAndReplace, tempRef, @@ -281,7 +297,7 @@ function getNodeContentAndReferences (currentNode, allData, specRoot) { nodeContent = currentNode.parsed.oasObject; } else { - nodeContent = parse.getOasObject(currentNode.content).oasObject; + nodeContent = parseFileOrThrow(currentNode.content).oasObject; } const { referencesInNode, nodeReferenceDirectory } = getReferences( @@ -321,6 +337,17 @@ function getNodeContentAndReferences (currentNode, allData, specRoot) { * @returns {object} The components object related to the file */ function generateComponentsObject (documentContext, rootContent, refTypeResolver, components) { + let notInLine = Object.entries(documentContext.globalReferences).filter(([, value]) => { + return value.keyInComponents.length !== 0; + }); + notInLine.forEach(([key, value]) => { + let [, partial] = key.split('#'); + setValueInComponents( + value.keyInComponents, + components, + getContentFromTrace(documentContext.nodeContents[key], partial) + ); + }); [rootContent, components].forEach((contentData) => { traverseUtility(contentData).forEach(function (property) { if (property) { @@ -375,6 +402,22 @@ function generateComponentsObject (documentContext, rootContent, refTypeResolver }); } +/** + * Generates the components object wrapper + * @param {object} parsedOasObject The parsed root + * @param {string} version - The current version + * @returns {object} The components object wrapper + */ +function generateComponentsWrapper(parsedOasObject) { + let components = {}; + + if (parsedOasObject.hasOwnProperty('components')) { + components = parsedOasObject.components; + } + + return components; +} + module.exports = { /** * Takes in an spec root file and an array of data files @@ -396,14 +439,18 @@ module.exports = { rootContextData = algorithm.traverseAndBundle(specRoot, (currentNode) => { return getNodeContentAndReferences(currentNode, allData, specRoot); }); - if (specRoot.parsed.oasObject.hasOwnProperty('components')) { - components = specRoot.parsed.oasObject.components; - } - generateComponentsObject(rootContextData, rootContextData.nodeContents[specRoot.fileName], isExtRef, components); + components = generateComponentsWrapper(specRoot.parsed.oasObject); + generateComponentsObject( + rootContextData, + rootContextData.nodeContents[specRoot.fileName], + isExtRef, + components + ); return { fileContent: rootContextData.nodeContents[specRoot.fileName], components }; }, - getReferences + getReferences, + parseFileOrThrow }; diff --git a/lib/jsonPointer.js b/lib/jsonPointer.js index 44cba59..6951785 100644 --- a/lib/jsonPointer.js +++ b/lib/jsonPointer.js @@ -6,26 +6,7 @@ const slashes = /\//g, escapedTilde = /~0/g, jsonPointerLevelSeparator = '/', escapedTildeString = '~0', - COMPONENTS_KEYS = [ - 'schemas', - 'responses', - 'parameters', - 'examples', - 'requestBodies', - 'headers', - 'securitySchemes', - 'links', - 'callbacks' - ], - SCHEMA_PARENT_KEYS_IN_DOC = [ - 'allOf', - 'oneOf', - 'anyOf', - 'not', - 'additionalProperties', - 'items', - 'schema' - ]; + { getKeyInComponents30 } = require('./30XUtils/componentsParentMatcher'); /** * Encodes a filepath name so it can be a json pointer @@ -52,49 +33,14 @@ function jsonPointerDecodeAndReplace(filePathName) { * @param {string} traceFromParent the node trace from root. * @param {string} filePathName the filePathName of the file * @param {string} localPath the local path that the pointer will reach +* @param {string} version - The current spec version * @returns {Array} - the calculated keys in an array representing each nesting property name */ function getKeyInComponents(traceFromParent, filePathName, localPath) { const localPart = localPath ? `${localPointer}${localPath}` : ''; - let res = traceFromParent, - trace = [], - traceToKey = [], - matchFound = false, - inComponents = false; - - if (traceFromParent[0] === 'components') { - inComponents = true; - return [[], inComponents]; - } - - res.push(jsonPointerDecodeAndReplace(`${filePathName}${localPart}`)); - trace = [...res].reverse(); - - for (let item of trace) { - if (SCHEMA_PARENT_KEYS_IN_DOC.includes(item)) { - item = 'schemas'; - } - traceToKey.push(item); - if (COMPONENTS_KEYS.includes(item)) { - matchFound = true; - break; - } - } - return [matchFound ? - traceToKey.reverse() : - [], inComponents]; -} - -/** -* returns the local path of a pointer #/definitions/dog etc. -* @param {string} jsonPointer the complet pointer -* @returns {string} - the calculated key -*/ -function getLocalPath(jsonPointer) { - if (jsonPointer.includes(localPointer)) { - return jsonPointer.split(localPointer)[1]; - } - return ''; + let result; + result = getKeyInComponents30(traceFromParent, filePathName, localPart, jsonPointerDecodeAndReplace); + return result; } /** @@ -102,13 +48,14 @@ function getLocalPath(jsonPointer) { * @constructor * @param {Function} encodeFunction function to encode url * @param {string} traceFromParent the trace from parent. +* @param {string} targetInRoot - The root element where we will point * @returns {string} - the concatenated json pointer */ -function concatJsonPointer(encodeFunction, traceFromParent) { +function concatJsonPointer(encodeFunction, traceFromParent, targetInRoot) { const traceFromParentAsString = traceFromParent.map((trace) => { return encodeFunction(trace); }).join('/'); - return localPointer + '/components' + jsonPointerLevelSeparator + traceFromParentAsString; + return localPointer + targetInRoot + jsonPointerLevelSeparator + traceFromParentAsString; } /** @@ -117,14 +64,15 @@ function concatJsonPointer(encodeFunction, traceFromParent) { * @param {Function} encodeFunction function to encode url * @param {string} refValue the type of component e.g. schemas, parameters, etc. * @param {string} traceFromKey the trace from the parent node. +* @param {string} version - The version we are working on * @returns {string} - the concatenated json pointer */ function getJsonPointerRelationToRoot(encodeFunction, refValue, traceFromKey) { + let targetInRoot = '/components'; if (refValue.startsWith(localPointer)) { return refValue; } - const localPath = getLocalPath(refValue); - return concatJsonPointer(encodeFunction, traceFromKey, localPath); + return concatJsonPointer(encodeFunction, traceFromKey, targetInRoot); } /** diff --git a/lib/schemaUtils.js b/lib/schemaUtils.js index 40d526e..477f42e 100644 --- a/lib/schemaUtils.js +++ b/lib/schemaUtils.js @@ -92,7 +92,7 @@ const { formatDataPath, checkIsCorrectType, isKnownType } = require('./common/sc { getRelatedFiles } = require('./relatedFiles'), { compareVersion } = require('./common/versionUtils.js'), parse = require('./parse'), - { getBundleContentAndComponents } = require('./bundle.js'), + { getBundleContentAndComponents, parseFileOrThrow } = require('./bundle.js'), MULTI_FILE_API_TYPE_ALLOWED_VALUE = 'multiFile'; /* eslint-enable */ @@ -4884,7 +4884,7 @@ module.exports = { mapProcessRelatedFiles(rootFiles, inputData, origin, version, format, toBundle = false) { let bundleFormat = format, parsedRootFiles = rootFiles.map((rootFile) => { - let parsedContent = this.parseFileOrThrow(rootFile.content); + let parsedContent = parseFileOrThrow(rootFile.content); return { fileName: rootFile.fileName, content: rootFile.content, parsed: parsedContent }; }).filter((rootWithParsedContent) => { bundleFormat = bundleFormat ? bundleFormat : rootWithParsedContent.parsed.inputFormat; @@ -4968,13 +4968,5 @@ module.exports = { throw new Error('"Path" of the data element should be provided'); } }, - - parseFileOrThrow(fileContent) { - const result = parse.getOasObject(fileContent); - if (result.result === false) { - throw new ParseError(result.reason); - } - return result; - }, MULTI_FILE_API_TYPE_ALLOWED_VALUE }; diff --git a/test/data/toBundleExamples/bring_local_dependencies_from_external/expected.json b/test/data/toBundleExamples/bring_local_dependencies_from_external/expected.json index 4866525..7aa1b10 100644 --- a/test/data/toBundleExamples/bring_local_dependencies_from_external/expected.json +++ b/test/data/toBundleExamples/bring_local_dependencies_from_external/expected.json @@ -49,19 +49,22 @@ "type": "object", "properties": { "theUsersPet": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "breed": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1schemas~1user.yaml%23~1Pet" } } } } + }, + "/schemas/user.yaml#/Pet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + } + } } } } diff --git a/test/data/toBundleExamples/bring_local_dependencies_from_external_multiple_local/expected.json b/test/data/toBundleExamples/bring_local_dependencies_from_external_multiple_local/expected.json index 194fcb4..19b6f4c 100644 --- a/test/data/toBundleExamples/bring_local_dependencies_from_external_multiple_local/expected.json +++ b/test/data/toBundleExamples/bring_local_dependencies_from_external_multiple_local/expected.json @@ -49,43 +49,58 @@ "type": "object", "properties": { "favoriteFood": { - "type": "object", - "properties": { - "brand": { - "type": "string" - }, - "benefits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/~1schemas~1food.yaml%23~1Benefit" - } - }, - "cost": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1schemas~1food.yaml%23Food" }, "theUsersPet": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "breed": { - "type": "string" - }, - "color": { - "type": "array", - "items": { - "$ref": "#/components/schemas/~1schemas~1user.yaml%23~1Color" - } - } - } + "$ref": "#/components/schemas/~1schemas~1user.yaml%23~1Pet" } } } } }, + "/schemas/food.yaml#Food": { + "type": "object", + "properties": { + "brand": { + "$ref": "#/components/schemas/~1schemas~1food.yaml%23~1Brand" + }, + "benefits": { + "type": "array", + "items": { + "$ref": "#/components/schemas/~1schemas~1food.yaml%23~1Benefit" + } + }, + "cost": { + "type": "string" + } + } + }, + "/schemas/user.yaml#/Pet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "breed": { + "type": "string" + }, + "color": { + "$ref": "#/components/schemas/~1schemas~1user.yaml%23~1Colors" + } + } + }, + "/schemas/user.yaml#/Colors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/~1schemas~1user.yaml%23~1Color" + } + }, + "/schemas/user.yaml#/Color": { + "type": "string" + }, + "/schemas/food.yaml#/Brand": { + "type": "string" + }, "/schemas/food.yaml#/Benefit": { "type": "object", "properties": { @@ -96,9 +111,6 @@ "type": "string" } } - }, - "/schemas/user.yaml#/Color": { - "type": "string" } } } diff --git a/test/data/toBundleExamples/multiple_references_from_root_components/expected.json b/test/data/toBundleExamples/multiple_references_from_root_components/expected.json index a60e361..4081bea 100644 --- a/test/data/toBundleExamples/multiple_references_from_root_components/expected.json +++ b/test/data/toBundleExamples/multiple_references_from_root_components/expected.json @@ -80,31 +80,13 @@ "type": "object", "properties": { "userInfo": { - "type": "object", - "properties": { - "id": { - "type": "integer" - }, - "userName": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1schemas~1user.yaml" }, "carType": { - "type": "object", - "properties": { - "model": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1schemas~1carType.yaml" }, "work": { - "type": "object", - "properties": { - "office": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1otherSchemas~1work.yaml" } } }, @@ -118,6 +100,36 @@ "type": "string" } } + }, + "/schemas/user.yaml": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "userName": { + "type": "string" + } + } + }, + "/schemas/carType.yaml": { + "type": "object", + "properties": { + "model": { + "$ref": "#/components/schemas/~1otherSchemas~1model.yaml" + } + } + }, + "/otherSchemas/work.yaml": { + "type": "object", + "properties": { + "office": { + "type": "string" + } + } + }, + "/otherSchemas/model.yaml": { + "type": "string" } } } diff --git a/test/data/toBundleExamples/nestedProperties/expected.json b/test/data/toBundleExamples/nestedProperties/expected.json new file mode 100644 index 0000000..9edf318 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/expected.json @@ -0,0 +1,126 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.", + "version": "0.1.9" + }, + "servers": [ + { + "url": "http://api.example.com/v1", + "description": "Optional server description, e.g. Main (production) server" + }, + { + "url": "http://staging-api.example.com", + "description": "Optional server description, e.g. Internal staging server for testing" + } + ], + "paths": { + "/users/{userId}": { + "get": { + "summary": "Get a user by ID", + "responses": { + "200": { + "description": "A single user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/~1schemas~1user.yaml" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "/schemas/user.yaml": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "userName": { + "type": "string" + }, + "complexProp": { + "$ref": "#/components/schemas/~1properties~1prop.yaml" + } + } + }, + "/properties/prop.yaml": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "secondName": { + "type": "string" + }, + "age": { + "type": "integer" + }, + "nestedProp": { + "$ref": "#/components/schemas/~1properties~1nestedProp.yaml" + }, + "country": { + "$ref": "#/components/schemas/~1properties~1country.yaml" + }, + "warrior": { + "$ref": "#/components/schemas/~1properties~1warrior.yaml" + } + } + }, + "/properties/nestedProp.yaml": { + "type": "object", + "rock": { + "type": "boolean" + }, + "friendly": { + "type": "string" + }, + "lastNested": { + "type": "object", + "properties": { + "this": { + "type": "string" + }, + "is": { + "type": "string" + }, + "the": { + "type": "string" + }, + "last": { + "type": "integer" + } + } + } + }, + "/properties/country.yaml": { + "type": "object", + "properties": { + "region": { + "type": "string" + }, + "flag": { + "type": "string" + } + } + }, + "/properties/warrior.yaml": { + "type": "object", + "properties": { + "power": { + "type": "string" + }, + "weapon": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/properties/country.yaml b/test/data/toBundleExamples/nestedProperties/properties/country.yaml new file mode 100644 index 0000000..63eb6c6 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/properties/country.yaml @@ -0,0 +1,6 @@ +type: object +properties: + region: + type: string + flag: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/properties/lastNested.yaml b/test/data/toBundleExamples/nestedProperties/properties/lastNested.yaml new file mode 100644 index 0000000..ec593dc --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/properties/lastNested.yaml @@ -0,0 +1,10 @@ +type: object +properties: + this: + type: string + is: + type: string + the: + type: string + last: + type: integer \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/properties/nestedProp.yaml b/test/data/toBundleExamples/nestedProperties/properties/nestedProp.yaml new file mode 100644 index 0000000..35bc146 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/properties/nestedProp.yaml @@ -0,0 +1,7 @@ +type: object +rock: + type: boolean +friendly: + type: string +lastNested: + $ref: "./lastNested.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/properties/prop.yaml b/test/data/toBundleExamples/nestedProperties/properties/prop.yaml new file mode 100644 index 0000000..1164054 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/properties/prop.yaml @@ -0,0 +1,14 @@ +type: object +properties: + firstName: + type: string + secondName: + type: string + age: + type: integer + nestedProp: + $ref: "./nestedProp.yaml" + country: + $ref: "./country.yaml" + warrior: + $ref: "./warrior.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/properties/warrior.yaml b/test/data/toBundleExamples/nestedProperties/properties/warrior.yaml new file mode 100644 index 0000000..6434c31 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/properties/warrior.yaml @@ -0,0 +1,6 @@ +type: object +properties: + power: + type: string + weapon: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/root.yaml b/test/data/toBundleExamples/nestedProperties/root.yaml new file mode 100644 index 0000000..3666bf2 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/root.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +paths: + /users/{userId}: + get: + summary: Get a user by ID + responses: + 200: + description: A single user. + content: + application/json: + schema: + $ref: "./schemas/user.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/nestedProperties/schemas/user.yaml b/test/data/toBundleExamples/nestedProperties/schemas/user.yaml new file mode 100644 index 0000000..70f6021 --- /dev/null +++ b/test/data/toBundleExamples/nestedProperties/schemas/user.yaml @@ -0,0 +1,8 @@ +type: object +properties: + id: + type: integer + userName: + type: string + complexProp: + $ref: "../properties/prop.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/properties/expected.json b/test/data/toBundleExamples/properties/expected.json new file mode 100644 index 0000000..48c9877 --- /dev/null +++ b/test/data/toBundleExamples/properties/expected.json @@ -0,0 +1,69 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.", + "version": "0.1.9" + }, + "servers": [ + { + "url": "http://api.example.com/v1", + "description": "Optional server description, e.g. Main (production) server" + }, + { + "url": "http://staging-api.example.com", + "description": "Optional server description, e.g. Internal staging server for testing" + } + ], + "paths": { + "/users/{userId}": { + "get": { + "summary": "Get a user by ID", + "responses": { + "200": { + "description": "A single user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/~1schemas~1user.yaml" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "/schemas/user.yaml": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "userName": { + "type": "string" + }, + "complexProp": { + "$ref": "#/components/schemas/~1schemas~1prop.yaml" + } + } + }, + "/schemas/prop.yaml": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "secondName": { + "type": "string" + }, + "age": { + "type": "integer" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/properties/root.yaml b/test/data/toBundleExamples/properties/root.yaml new file mode 100644 index 0000000..3666bf2 --- /dev/null +++ b/test/data/toBundleExamples/properties/root.yaml @@ -0,0 +1,23 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +paths: + /users/{userId}: + get: + summary: Get a user by ID + responses: + 200: + description: A single user. + content: + application/json: + schema: + $ref: "./schemas/user.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/properties/schemas/prop.yaml b/test/data/toBundleExamples/properties/schemas/prop.yaml new file mode 100644 index 0000000..799a814 --- /dev/null +++ b/test/data/toBundleExamples/properties/schemas/prop.yaml @@ -0,0 +1,8 @@ +type: object +properties: + firstName: + type: string + secondName: + type: string + age: + type: integer \ No newline at end of file diff --git a/test/data/toBundleExamples/properties/schemas/user.yaml b/test/data/toBundleExamples/properties/schemas/user.yaml new file mode 100644 index 0000000..2241722 --- /dev/null +++ b/test/data/toBundleExamples/properties/schemas/user.yaml @@ -0,0 +1,8 @@ +type: object +properties: + id: + type: integer + userName: + type: string + complexProp: + $ref: ./prop.yaml \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_examples/examples.yaml b/test/data/toBundleExamples/referenced_examples/examples.yaml new file mode 100644 index 0000000..921c511 --- /dev/null +++ b/test/data/toBundleExamples/referenced_examples/examples.yaml @@ -0,0 +1,5 @@ +foo: + summary: sum + value: + code: 1 + message: test error message \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_examples/expected.json b/test/data/toBundleExamples/referenced_examples/expected.json new file mode 100644 index 0000000..2067a82 --- /dev/null +++ b/test/data/toBundleExamples/referenced_examples/expected.json @@ -0,0 +1,100 @@ +{ + "openapi": "3.0.2", + "info": { + "version": "1.0.0", + "title": "Swagger Petstore", + "description": "A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "name": "Swagger API Team", + "email": "apiteam@swagger.io", + "url": "http://swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "paths": { + "/pets": { + "get": { + "description": "Returns all pets alesuada ac...", + "operationId": "findPets", + "responses": { + "200": { + "description": "pet response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + }, + "example": { + "$ref": "#/components/examples/~1examples.yaml%23~1foo" + } + } + } + }, + "default": { + "description": "unexpected error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + } + }, + "Error": { + "required": [ + "code", + "message" + ], + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + } + } + } + }, + "examples": { + "/examples.yaml#/foo": { + "summary": "sum", + "value": { + "code": 1, + "message": "test error message" + } + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/referenced_examples/root.yaml b/test/data/toBundleExamples/referenced_examples/root.yaml new file mode 100644 index 0000000..2ac1c4a --- /dev/null +++ b/test/data/toBundleExamples/referenced_examples/root.yaml @@ -0,0 +1,60 @@ + +openapi: "3.0.2" +info: + version: 1.0.0 + title: Swagger Petstore + description: A sample API that uses a petstore as an example to demonstrate features in the swagger-2.0 specification + termsOfService: http://swagger.io/terms/ + contact: + name: Swagger API Team + email: apiteam@swagger.io + url: http://swagger.io + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html +paths: + /pets: + get: + description: Returns all pets alesuada ac... + operationId: findPets + responses: + "200": + description: pet response + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/Pet" + example: + $ref: "examples.yaml#/foo" + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" +components: + schemas: + Pet: + required: + - id + - name + properties: + id: + type: integer + format: int64 + name: + type: string + tag: + type: string + Error: + required: + - code + - message + properties: + code: + type: integer + format: int32 + message: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/same_ref_different_source/expected.json b/test/data/toBundleExamples/same_ref_different_source/expected.json index 811eb70..5b81da0 100644 --- a/test/data/toBundleExamples/same_ref_different_source/expected.json +++ b/test/data/toBundleExamples/same_ref_different_source/expected.json @@ -63,12 +63,7 @@ "type": "string" }, "special": { - "type": "object", - "properties": { - "specialUserId": { - "type": "string" - } - } + "$ref": "#/components/schemas/~1schemas~1user~1special.yaml" } } }, @@ -82,23 +77,37 @@ "type": "string" }, "special": { - "type": "object", - "properties": { - "specialClientId": { - "type": "string" - }, - "magic": { - "type": "object", - "properties": { - "magicNumber": { - "type": "integer" - }, - "magicString": { - "type": "string" - } - } - } - } + "$ref": "#/components/schemas/~1schemas~1client~1special.yaml" + } + } + }, + "/schemas/client/special.yaml": { + "type": "object", + "properties": { + "specialClientId": { + "type": "string" + }, + "magic": { + "$ref": "#/components/schemas/~1schemas~1client~1magic.yaml" + } + } + }, + "/schemas/client/magic.yaml": { + "type": "object", + "properties": { + "magicNumber": { + "type": "integer" + }, + "magicString": { + "type": "string" + } + } + }, + "/schemas/user/special.yaml": { + "type": "object", + "properties": { + "specialUserId": { + "type": "string" } } } diff --git a/test/data/toBundleExamples/same_source_different_place/expected.json b/test/data/toBundleExamples/same_source_different_place/expected.json new file mode 100644 index 0000000..d701d31 --- /dev/null +++ b/test/data/toBundleExamples/same_source_different_place/expected.json @@ -0,0 +1,94 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Sample API", + "description": "Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.", + "version": "0.1.9" + }, + "servers": [ + { + "url": "http://api.example.com/v1", + "description": "Optional server description, e.g. Main (production) server" + }, + { + "url": "http://staging-api.example.com", + "description": "Optional server description, e.g. Internal staging server for testing" + } + ], + "paths": { + "/users": { + "get": { + "summary": "Get a user by ID", + "responses": { + "200": { + "description": "A single user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/~1schemas~1user~1user.yaml" + } + } + } + } + } + } + }, + "/clients": { + "get": { + "summary": "Get a user by ID", + "responses": { + "200": { + "description": "A single user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/~1schemas~1client~1client.yaml" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "/schemas/user/user.yaml": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "userName": { + "type": "string" + }, + "special": { + "$ref": "#/components/schemas/~1schemas~1user~1special.yaml" + } + } + }, + "/schemas/client/client.yaml": { + "type": "object", + "properties": { + "idClient": { + "type": "integer" + }, + "clientName": { + "type": "string" + }, + "special": { + "$ref": "#/components/schemas/~1schemas~1user~1special.yaml" + } + } + }, + "/schemas/user/special.yaml": { + "type": "object", + "properties": { + "specialUserId": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/data/toBundleExamples/same_source_different_place/root.yaml b/test/data/toBundleExamples/same_source_different_place/root.yaml new file mode 100644 index 0000000..1b9d218 --- /dev/null +++ b/test/data/toBundleExamples/same_source_different_place/root.yaml @@ -0,0 +1,33 @@ +openapi: 3.0.0 +info: + title: Sample API + description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. + version: 0.1.9 + +servers: + - url: http://api.example.com/v1 + description: Optional server description, e.g. Main (production) server + - url: http://staging-api.example.com + description: Optional server description, e.g. Internal staging server for testing + +paths: + /users: + get: + summary: Get a user by ID + responses: + 200: + description: A single user. + content: + application/json: + schema: + $ref: "./schemas/user/user.yaml" + /clients: + get: + summary: Get a user by ID + responses: + 200: + description: A single user. + content: + application/json: + schema: + $ref: "./schemas/client/client.yaml" \ No newline at end of file diff --git a/test/data/toBundleExamples/same_source_different_place/schemas/client/client.yaml b/test/data/toBundleExamples/same_source_different_place/schemas/client/client.yaml new file mode 100644 index 0000000..fc6eed6 --- /dev/null +++ b/test/data/toBundleExamples/same_source_different_place/schemas/client/client.yaml @@ -0,0 +1,8 @@ +type: object +properties: + idClient: + type: integer + clientName: + type: string + special: + $ref: ../user/special.yaml \ No newline at end of file diff --git a/test/data/toBundleExamples/same_source_different_place/schemas/user/special.yaml b/test/data/toBundleExamples/same_source_different_place/schemas/user/special.yaml new file mode 100644 index 0000000..d35d2f1 --- /dev/null +++ b/test/data/toBundleExamples/same_source_different_place/schemas/user/special.yaml @@ -0,0 +1,4 @@ +type: object +properties: + specialUserId: + type: string \ No newline at end of file diff --git a/test/data/toBundleExamples/same_source_different_place/schemas/user/user.yaml b/test/data/toBundleExamples/same_source_different_place/schemas/user/user.yaml new file mode 100644 index 0000000..04c5121 --- /dev/null +++ b/test/data/toBundleExamples/same_source_different_place/schemas/user/user.yaml @@ -0,0 +1,8 @@ +type: object +properties: + id: + type: integer + userName: + type: string + special: + $ref: ./special.yaml \ No newline at end of file diff --git a/test/unit/bundle.test.js b/test/unit/bundle.test.js index 7fb6c0c..6190237 100644 --- a/test/unit/bundle.test.js +++ b/test/unit/bundle.test.js @@ -22,7 +22,11 @@ let expect = require('chai').expect, refTags = path.join(__dirname, BUNDLES_FOLDER + '/referenced_tags'), refInfo = path.join(__dirname, BUNDLES_FOLDER + '/referenced_info'), refPaths = path.join(__dirname, BUNDLES_FOLDER + '/referenced_paths'), - refPathsRefToLocalSchema = path.join(__dirname, BUNDLES_FOLDER + '/referenced_paths_local_schema'); + refPathsRefToLocalSchema = path.join(__dirname, BUNDLES_FOLDER + '/referenced_paths_local_schema'), + refExample = path.join(__dirname, BUNDLES_FOLDER + '/referenced_examples'), + properties = path.join(__dirname, BUNDLES_FOLDER + '/properties'), + sameSourceDifferentPlace = path.join(__dirname, BUNDLES_FOLDER + '/same_source_different_place'), + nestedProperties = path.join(__dirname, BUNDLES_FOLDER + '/nestedProperties'); describe('bundle files method - 3.0', function () { @@ -937,6 +941,7 @@ describe('bundle files method - 3.0', function () { expect(error.message).to.equal('Root file content not found in data array'); } }); + it('Should return bundled 1 file with 2 root but 1 is missing', async function () { let contentRootFile = fs.readFileSync(schemaFromResponse + '/root.yaml', 'utf8'), user = fs.readFileSync(schemaFromResponse + '/schemas/user.yaml', 'utf8'), @@ -951,9 +956,6 @@ describe('bundle files method - 3.0', function () { rootFiles: [ { path: '/root.yaml' - }, - { - path: '/root2.yaml' } ], data: [ @@ -1026,7 +1028,6 @@ describe('bundle files method - 3.0', function () { bundleFormat: 'JSON' }; const res = await Converter.bundle(input); - expect(res).to.not.be.empty; expect(res.result).to.be.true; expect(res.output.specification.version).to.equal('3.1'); @@ -1068,7 +1069,6 @@ describe('bundle files method - 3.0', function () { bundleFormat: 'JSON' }; const res = await Converter.bundle(input); - expect(res).to.not.be.empty; expect(res.result).to.be.true; expect(res.output.specification.version).to.equal('3.0'); @@ -1109,13 +1109,245 @@ describe('bundle files method - 3.0', function () { bundleFormat: 'JSON' }; const res = await Converter.bundle(input); - expect(res).to.not.be.empty; expect(res.result).to.be.true; expect(res.output.specification.version).to.equal('3.0'); expect(res.output.data.length).to.equal(1); expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); }); + + it('Should return bundled file with referenced example', async function () { + let contentRootFile = fs.readFileSync(refExample + '/root.yaml', 'utf8'), + example = fs.readFileSync(refExample + '/examples.yaml', 'utf8'), + expected = fs.readFileSync(refExample + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/examples.yaml', + content: example + } + ], + options: {}, + bundleFormat: 'JSON' + }; + const res = await Converter.bundle(input); + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); + expect(res.output.data.length).to.equal(1); + }); + + it('should return error when "type" parameter is not sent', async function () { + let contentRootFile = fs.readFileSync(refExample + '/root.yaml', 'utf8'), + example = fs.readFileSync(refExample + '/examples.yaml', 'utf8'), + input = { + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/examples.yaml', + content: example + } + ], + options: {}, + bundleFormat: 'JSON' + }; + try { + await Converter.bundle(input); + } + catch (error) { + expect(error).to.not.be.undefined; + expect(error.message).to.equal('"Type" parameter should be provided'); + } + }); + + it('should return error when input is an empty object', async function () { + try { + await Converter.bundle({}); + } + catch (error) { + expect(error).to.not.be.undefined; + expect(error.message).to.equal('Input object must have "type" and "data" information'); + } + }); + + it('should return error when input has no root files', async function () { + let contentRootFile = fs.readFileSync(refExample + '/root.yaml', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [], + data: [ + { + path: '/root.yaml', + content: contentRootFile + } + ], + options: {}, + bundleFormat: 'JSON' + }; + try { + await Converter.bundle(input); + } + catch (error) { + expect(error).to.not.be.undefined; + expect(error.message).to.equal('Input should have at least one root file'); + } + }); + + it('Should return bundled file as json - sameSourceDifferentPlace', async function () { + let contentRootFile = fs.readFileSync(sameSourceDifferentPlace + '/root.yaml', 'utf8'), + user = fs.readFileSync(sameSourceDifferentPlace + '/schemas/user/user.yaml', 'utf8'), + special = fs.readFileSync(sameSourceDifferentPlace + '/schemas/user/special.yaml', 'utf8'), + client = fs.readFileSync(sameSourceDifferentPlace + '/schemas/client/client.yaml', 'utf8'), + expected = fs.readFileSync(sameSourceDifferentPlace + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/schemas/user/user.yaml', + content: user + }, + { + path: '/schemas/user/special.yaml', + content: special + }, + { + path: '/schemas/client/client.yaml', + content: client + } + ], + options: {}, + bundleFormat: 'JSON' + }; + const res = await Converter.bundle(input); + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); + expect(res.output.data.length).to.equal(1); + }); + + it('Should return bundled file as json - nestedProperties', async function () { + let contentRootFile = fs.readFileSync(nestedProperties + '/root.yaml', 'utf8'), + user = fs.readFileSync(nestedProperties + '/schemas/user.yaml', 'utf8'), + prop = fs.readFileSync(nestedProperties + '/properties/prop.yaml', 'utf8'), + nestedProp = fs.readFileSync(nestedProperties + '/properties/nestedProp.yaml', 'utf8'), + lastNested = fs.readFileSync(nestedProperties + '/properties/lastNested.yaml', 'utf8'), + warrior = fs.readFileSync(nestedProperties + '/properties/warrior.yaml', 'utf8'), + country = fs.readFileSync(nestedProperties + '/properties/country.yaml', 'utf8'), + expected = fs.readFileSync(nestedProperties + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/schemas/user.yaml', + content: user + }, + { + path: '/properties/prop.yaml', + content: prop + }, + { + path: '/properties/nestedProp.yaml', + content: nestedProp + }, + { + path: '/properties/country.yaml', + content: country + }, + { + path: '/properties/lastNested.yaml', + content: lastNested + }, + { + path: '/properties/warrior.yaml', + content: warrior + } + ], + options: {}, + bundleFormat: 'JSON' + }; + const res = await Converter.bundle(input); + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); + expect(res.output.data.length).to.equal(1); + }); + + it('Should return bundled file as json - properties', async function () { + let contentRootFile = fs.readFileSync(properties + '/root.yaml', 'utf8'), + user = fs.readFileSync(properties + '/schemas/user.yaml', 'utf8'), + prop = fs.readFileSync(properties + '/schemas/prop.yaml', 'utf8'), + expected = fs.readFileSync(properties + '/expected.json', 'utf8'), + input = { + type: 'multiFile', + specificationVersion: '3.0', + rootFiles: [ + { + path: '/root.yaml' + } + ], + data: [ + { + path: '/root.yaml', + content: contentRootFile + }, + { + path: '/schemas/user.yaml', + content: user + }, + { + path: '/schemas/prop.yaml', + content: prop + } + ], + options: {}, + bundleFormat: 'JSON' + }; + const res = await Converter.bundle(input); + expect(res).to.not.be.empty; + expect(res.result).to.be.true; + expect(JSON.stringify(res.output.data[0].bundledContent, null, 2)).to.be.equal(expected); + expect(res.output.data.length).to.equal(1); + }); }); diff --git a/test/unit/jsonPointer.test.js b/test/unit/jsonPointer.test.js index 64b06d3..83053fe 100644 --- a/test/unit/jsonPointer.test.js +++ b/test/unit/jsonPointer.test.js @@ -6,27 +6,21 @@ const expect = require('chai').expect, getKeyInComponents } = require('./../../lib/jsonPointer'); describe('getKeyInComponents function', function () { - it('should return [[], true] when is pointing to an element in components', function () { + it('should return [] when is pointing to an element in components', function () { const result = getKeyInComponents(['components', 'schemas'], 'pet.yaml'); - expect(result).to.be.an('array').with.length(2); - expect(result[0].length).to.equal(0); - expect(result[1]).to.equal(true); + expect(result).to.be.an('array').with.length(0); }); - it('should return [[], true] when is pointing to a local ref in components', + it('should return [] when is pointing to a local ref in components', function () { const result = getKeyInComponents(['components', 'schemas'], 'pet.yaml', '/definitions/world'); - expect(result).to.be.an('array').with.length(2); - expect(result[0].length).to.equal(0); - expect(result[1]).to.equal(true); + expect(result).to.be.an('array').with.length(0); }); - it('should return [["schemas", "folder/pet.yaml"], false] when there is an scaped slash', function () { + it('should return ["schemas", "folder/pet.yaml"] when there is an scaped slash', function () { const result = getKeyInComponents(['path', 'schemas'], 'folder~1pet.yaml'); expect(result).to.be.an('array').with.length(2); - expect(result[0].length).to.equal(2); - expect(result[0][0]).to.equal('schemas'); - expect(result[1]).to.equal(false); + expect(result[0]).to.equal('schemas'); }); }); @@ -62,7 +56,8 @@ describe('concatJsonPointer function ', function () { it('should return "#/components/schemas/Pets.yaml" no local path and schema', function () { let res = concatJsonPointer( jsonPointerEncodeAndReplace, - ['schemas', 'Pets.yaml'] + ['schemas', 'Pets.yaml'], + '/components' ); expect(res).to.equal('#/components/schemas/Pets.yaml'); }); @@ -70,21 +65,24 @@ describe('concatJsonPointer function ', function () { it('should return "#/components/schemas/other~1Pets.yaml" no local path and schema folder in filename', function () { let res = concatJsonPointer( jsonPointerEncodeAndReplace, - ['schemas', 'other/Pets.yaml'] + ['schemas', 'other/Pets.yaml'], + '/components' ); expect(res).to.equal('#/components/schemas/other~1Pets.yaml'); }); it('should return "#/components/schemas/some~1Pet" no local path and schema folder in filename', function () { let res = concatJsonPointer( jsonPointerEncodeAndReplace, - ['schemas', 'some/Pet.yaml'] + ['schemas', 'some/Pet.yaml'], + '/components' ); expect(res).to.equal('#/components/schemas/some~1Pet.yaml'); }); it('should return "#/components/schemas/hello.yaml" no local path and schema', function () { let res = concatJsonPointer( jsonPointerEncodeAndReplace, - ['schemas', 'hello.yaml'] + ['schemas', 'hello.yaml'], + '/components' ); expect(res).to.equal('#/components/schemas/hello.yaml'); }); @@ -92,7 +90,8 @@ describe('concatJsonPointer function ', function () { it('should return "#/components/schemas/~1Pets.yaml" no local path and schema', function () { let res = concatJsonPointer( jsonPointerEncodeAndReplace, - ['schemas', '/Pets.yaml'] + ['schemas', '/Pets.yaml'], + '/components' ); expect(res).to.equal('#/components/schemas/~1Pets.yaml'); });