diff --git a/packages/api-elements/lib/elements/Category.js b/packages/api-elements/lib/elements/Category.js index 3ce25a10a..c2728ff7f 100644 --- a/packages/api-elements/lib/elements/Category.js +++ b/packages/api-elements/lib/elements/Category.js @@ -78,6 +78,15 @@ class Category extends ArrayElement { return this.children.filter(item => schemes.indexOf(item.element) !== -1); } + /** + * @name hosts + * @type ArraySlice + * @memberof Category.prototype + */ + get hosts() { + return this.children.filter(item => item.element === 'hosts'); + } + metadata(value) { const metadata = this.attributes.get('metadata'); diff --git a/packages/api-elements/lib/elements/Resource.js b/packages/api-elements/lib/elements/Resource.js index 0b051af0e..3ed9b413d 100644 --- a/packages/api-elements/lib/elements/Resource.js +++ b/packages/api-elements/lib/elements/Resource.js @@ -16,6 +16,19 @@ class Resource extends ArrayElement { this.element = 'resource'; } + /** + * @name hosts + * @type ArraySlice + * @memberof Resource.prototype + */ + get hosts() { + return this.attributes.get('hosts'); + } + + set hosts(value) { + this.attributes.set('hosts', value); + } + /** * @name href * @type StringElement diff --git a/packages/api-elements/lib/elements/Transition.js b/packages/api-elements/lib/elements/Transition.js index 02776ff90..6e4d2f75a 100644 --- a/packages/api-elements/lib/elements/Transition.js +++ b/packages/api-elements/lib/elements/Transition.js @@ -48,6 +48,19 @@ class Transition extends ArrayElement { this.attributes.set('relation', value); } + /** + * @name hosts + * @type ArraySlice + * @memberof Resource.prototype + */ + get hosts() { + return this.attributes.get('hosts'); + } + + set hosts(value) { + this.attributes.set('hosts', value); + } + /** * @name href * @type StringElement diff --git a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseOpenAPIObject.js b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseOpenAPIObject.js index d4bfeabd8..2cf25d683 100644 --- a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseOpenAPIObject.js +++ b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseOpenAPIObject.js @@ -12,6 +12,7 @@ const { const pipeParseResult = require('../../pipeParseResult'); const parseObject = require('../parseObject'); const parseOpenAPI = require('../openapi'); +const parseServersArray = require('./parseServersArray'); const parseInfoObject = require('./parseInfoObject'); const parsePathsObject = require('./parsePathsObject'); const parseComponentsObject = require('./parseComponentsObject'); @@ -19,7 +20,7 @@ const parseSecurityRequirementsArray = require('./parseSecurityRequirementsArray const name = 'OpenAPI Object'; const requiredKeys = ['openapi', 'info', 'paths']; -const unsupportedKeys = ['servers', 'tags', 'externalDocs']; +const unsupportedKeys = ['tags', 'externalDocs']; /** * Returns whether the given member element is unsupported @@ -109,6 +110,7 @@ function parseOASObject(context, object) { const parseMember = R.cond([ [hasKey('openapi'), parseOpenAPI(context)], + [hasKey('servers'), R.compose(parseServersArray(context), getValue)], [hasKey('info'), R.compose(parseInfoObject(context), getValue)], [hasKey('components'), R.compose(parseComponentsObject(context), getValue)], [hasKey('paths'), R.compose(asArray, parsePathsObject(context), getValue)], @@ -127,6 +129,7 @@ function parseOASObject(context, object) { parseObject(context, name, parseMember, requiredKeys, ['components']), (object) => { const api = object.get('info'); + const hosts = object.get('servers'); const components = object.get('components'); const security = object.get('security'); @@ -140,6 +143,10 @@ function parseOASObject(context, object) { } } + if (hosts) { + api.push(hosts); + } + const resources = object.get('paths'); if (resources) { api.content = api.content.concat(resources.content); diff --git a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServerObject.js b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServerObject.js new file mode 100644 index 000000000..8a6876785 --- /dev/null +++ b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServerObject.js @@ -0,0 +1,47 @@ +const R = require('ramda'); +const { createWarning } = require('../../elements'); +const { + createInvalidMemberWarning, createUnsupportedMemberWarning, +} = require('../annotations'); +const { + isObject, hasKey, isExtension, +} = require('../../predicates'); +const parseObject = require('../parseObject'); +const parseString = require('../parseString'); +const pipeParseResult = require('../../pipeParseResult'); + +const name = 'Server Object'; +const requiredKeys = ['url']; + +const parseMember = context => R.cond([ + [hasKey('description'), parseString(context, name, false)], + [hasKey('url'), parseString(context, name, true)], + [hasKey('variables'), createUnsupportedMemberWarning(context.namespace, name)], // NOT SUPPORTED YET + [isExtension, () => new context.namespace.elements.ParseResult()], + [R.T, createInvalidMemberWarning(context.namespace, name)], +]); + +/** + * Parse the OpenAPI 'Server Object' (`#/server`) + * @see http://spec.openapis.org/oas/v3.0.3#server-object + * @returns ParseResult + * @private + */ +const parseServerObject = context => pipeParseResult(context.namespace, + R.unless(isObject, createWarning(context.namespace, `'${name}' is not an object`)), + parseObject(context, name, parseMember(context), requiredKeys, [], true), + (object) => { + const resource = new context.namespace.elements.Resource(); + + resource.classes.push('host'); + + if (object.hasKey('description')) { + resource.description = object.get('description'); + } + + resource.href = object.get('url'); + + return resource; + }); + +module.exports = parseServerObject; diff --git a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServersArray.js b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServersArray.js new file mode 100644 index 000000000..8f84654f2 --- /dev/null +++ b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseServersArray.js @@ -0,0 +1,29 @@ +const R = require('ramda'); +const pipeParseResult = require('../../pipeParseResult'); +const parseArray = require('../parseArray'); +const parseServerObject = require('./parseServerObject'); + +const name = 'Servers Array'; + +/** + * Parse Servers Array + * + * @param namespace {Namespace} + * @param element {Element} + * @returns ParseResult + * + * @private + */ +function parseServersArray(context, element) { + const { namespace } = context; + + const parseServers = pipeParseResult(namespace, + parseArray(context, name, R.curry(parseServerObject)(context)), + array => new namespace.elements.Category( + array.content, { classes: ['hosts'] } + )); + + return parseServers(element); +} + +module.exports = R.curry(parseServersArray); diff --git a/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json b/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json index 663e3abed..fd683f5fc 100644 --- a/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json +++ b/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json @@ -49,6 +49,42 @@ } }, "content": [ + { + "element": "category", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "hosts" + } + ] + } + }, + "content": [ + { + "element": "resource", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "host" + } + ] + } + }, + "attributes": { + "href": { + "element": "string", + "content": "http://petstore.swagger.io/v1" + } + } + } + ] + }, { "element": "resource", "attributes": { @@ -551,66 +587,6 @@ } ] }, - { - "element": "annotation", - "meta": { - "classes": { - "element": "array", - "content": [ - { - "element": "string", - "content": "warning" - } - ] - } - }, - "attributes": { - "sourceMap": { - "element": "array", - "content": [ - { - "element": "sourceMap", - "content": [ - { - "element": "array", - "content": [ - { - "element": "number", - "attributes": { - "line": { - "element": "number", - "content": 7 - }, - "column": { - "element": "number", - "content": 1 - } - }, - "content": 91 - }, - { - "element": "number", - "attributes": { - "line": { - "element": "number", - "content": 7 - }, - "column": { - "element": "number", - "content": 8 - } - }, - "content": 7 - } - ] - } - ] - } - ] - } - }, - "content": "'OpenAPI Object' contains unsupported key 'servers'" - }, { "element": "annotation", "meta": { diff --git a/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.sourcemap.json b/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.sourcemap.json index 937f633b5..705afefe7 100644 --- a/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.sourcemap.json +++ b/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.sourcemap.json @@ -124,6 +124,67 @@ } }, "content": [ + { + "element": "category", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "hosts" + } + ] + } + }, + "content": [ + { + "element": "resource", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "host" + } + ] + } + }, + "attributes": { + "href": { + "element": "string", + "attributes": { + "sourceMap": { + "element": "array", + "content": [ + { + "element": "sourceMap", + "content": [ + { + "element": "array", + "content": [ + { + "element": "number", + "content": 109 + }, + { + "element": "number", + "content": 29 + } + ] + } + ] + } + ] + } + }, + "content": "http://petstore.swagger.io/v1" + } + } + } + ] + }, { "element": "resource", "attributes": { @@ -1326,66 +1387,6 @@ } ] }, - { - "element": "annotation", - "meta": { - "classes": { - "element": "array", - "content": [ - { - "element": "string", - "content": "warning" - } - ] - } - }, - "attributes": { - "sourceMap": { - "element": "array", - "content": [ - { - "element": "sourceMap", - "content": [ - { - "element": "array", - "content": [ - { - "element": "number", - "attributes": { - "line": { - "element": "number", - "content": 7 - }, - "column": { - "element": "number", - "content": 1 - } - }, - "content": 91 - }, - { - "element": "number", - "attributes": { - "line": { - "element": "number", - "content": 7 - }, - "column": { - "element": "number", - "content": 8 - } - }, - "content": 7 - } - ] - } - ] - } - ] - } - }, - "content": "'OpenAPI Object' contains unsupported key 'servers'" - }, { "element": "annotation", "meta": { diff --git a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseOpenAPIObject-test.js b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseOpenAPIObject-test.js index 91fe77174..a13e3ac38 100644 --- a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseOpenAPIObject-test.js +++ b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseOpenAPIObject-test.js @@ -47,6 +47,41 @@ describe('#parseOpenAPIObject', () => { expect(parseResult.api.get(0).href.toValue()).to.equal('/'); }); + it('can parse a document with servers array', () => { + const object = new namespace.elements.Object({ + openapi: '3.0.0', + info: { + title: 'My API', + version: '1.0.0', + }, + paths: {}, + servers: [ + { + url: 'https://user.server.com/1.0', + }, + { + url: 'https://user.server.com/2.0', + description: 'The production API server', + }, + ], + }); + + const parseResult = parse(context, object); + + expect(parseResult.length).to.equal(1); + expect(parseResult.api.title.toValue()).to.equal('My API'); + expect(parseResult.api.length).to.equal(1); + + const hostsCategory = parseResult.api.get(0); + expect(hostsCategory).to.be.instanceof(namespace.elements.Category); + expect(hostsCategory.classes.toValue()).to.deep.equal(['hosts']); + expect(hostsCategory.length).to.equal(2); + + const firstServer = hostsCategory.get(0); + expect(firstServer).to.be.instanceof(namespace.elements.Resource); + expect(firstServer.href.toValue()).to.equal('https://user.server.com/1.0'); + }); + describe('with schema components', () => { it('can parse a document with schema components into data structures', () => { const object = new namespace.elements.Object({ @@ -184,22 +219,6 @@ describe('#parseOpenAPIObject', () => { expect(parseResult).to.contain.warning("'OpenAPI Object' contains unsupported key 'externalDocs'"); }); - it('provides warning for unsupported servers key', () => { - const object = new namespace.elements.Object({ - openapi: '3.0.0', - info: { - title: 'My API', - version: '1.0.0', - }, - paths: {}, - servers: [], - }); - - const parseResult = parse(context, object); - - expect(parseResult).to.contain.warning("'OpenAPI Object' contains unsupported key 'servers'"); - }); - it('provides warning for invalid keys', () => { const object = new namespace.elements.Object({ openapi: '3.0.0', diff --git a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServerObject-test.js b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServerObject-test.js new file mode 100644 index 000000000..0bac0f4d1 --- /dev/null +++ b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServerObject-test.js @@ -0,0 +1,91 @@ +const { Fury } = require('fury'); +const { expect } = require('../../chai'); + +const parse = require('../../../../lib/parser/oas/parseServerObject'); +const Context = require('../../../../lib/context'); + +const { minim: namespace } = new Fury(); + +describe('#parseServerObject', () => { + let context; + beforeEach(() => { + context = new Context(namespace); + }); + + it('provides warning when server is non-object', () => { + const server = new namespace.elements.String(); + + const parseResult = parse(context)(server); + + expect(parseResult.length).to.equal(1); + expect(parseResult).to.contain.warning("'Server Object' is not an object"); + }); + + describe('#url', () => { + it('warns when server object does not contain URL', () => { + const server = new namespace.elements.Object({ + }); + + const parseResult = parse(context)(server); + expect(parseResult.length).to.equal(1); + expect(parseResult).to.contain.warning("'Server Object' is missing required property 'url'"); + }); + + it('warns when URL is not a string', () => { + const server = new namespace.elements.Object({ + url: 1234, + description: 'The production API server', + }); + + const parseResult = parse(context)(server); + expect(parseResult).to.contain.annotations; + expect(parseResult).to.contain.error("'Server Object' 'url' is not a string"); + }); + + it('parse server object with URL', () => { + const server = new namespace.elements.Object({ + url: 'https://{username}.gigantic-server.com/{version}', + }); + + const parseResult = parse(context)(server); + expect(parseResult).to.not.contain.annotations; + const resource = parseResult.get(0); + expect(resource).to.be.instanceof(namespace.elements.Resource); + + const hostClass = resource.classes.getValue(0); + expect(hostClass).to.be.equal('host'); + + const href = resource.href.toValue(); + expect(href).to.be.equal('https://{username}.gigantic-server.com/{version}'); + }); + }); + + describe('#description', () => { + it('warns when description is not a string', () => { + const server = new namespace.elements.Object({ + url: 'https://{username}.gigantic-server.com/{version}', + description: 1234, + }); + + const parseResult = parse(context)(server); + expect(parseResult.get(0)).to.be.instanceof(namespace.elements.Resource); + expect(parseResult).to.contain.annotations; + expect(parseResult).to.contain.warning("'Server Object' 'description' is not a string"); + }); + + it('parse server object with description', () => { + const server = new namespace.elements.Object({ + url: 'https://{username}.gigantic-server.com/{version}', + description: 'The production API server', + }); + + const parseResult = parse(context)(server); + expect(parseResult).to.not.contain.annotations; + const resource = parseResult.get(0); + expect(resource).to.be.instanceof(namespace.elements.Resource); + + const description = resource.description.toValue(); + expect(description).to.be.equal('The production API server'); + }); + }); +}); diff --git a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServersArray-test.js b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServersArray-test.js new file mode 100644 index 000000000..a7e8ae60c --- /dev/null +++ b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseServersArray-test.js @@ -0,0 +1,77 @@ +const { Fury } = require('fury'); +const { expect } = require('../../chai'); +const parse = require('../../../../lib/parser/oas/parseServersArray'); +const Context = require('../../../../lib/context'); + +const { minim: namespace } = new Fury(); + +describe('#parseServersArray', () => { + let context; + + beforeEach(() => { + context = new Context(namespace); + }); + + it('warns when it is not an array', () => { + const servers = new namespace.elements.Object(); + + const parseResult = parse(context, servers); + + expect(parseResult.length).to.equal(1); + expect(parseResult).to.contain.warning("'Servers Array' is not an array"); + }); + + it('parses correctly when there is a single server', () => { + const server = new namespace.elements.Array([ + { + url: 'https://user.server.com/1.0', + description: 'The production API server', + }, + ]); + + const parseResult = parse(context, server); + + expect(parseResult.length).to.equal(1); + + const hosts = parseResult.get(0); + const host = hosts.get(0); + + expect(hosts).to.be.instanceof(namespace.elements.Array); + expect(hosts.length).to.equal(1); + expect(host).to.be.instanceof(namespace.elements.Resource); + expect(host.classes.toValue()).to.deep.equal(['host']); + expect(host.description.toValue()).to.equal('The production API server'); + expect(host.href.toValue()).to.equal('https://user.server.com/1.0'); + }); + + it('parses correctly when there are multiple servers', () => { + const server = new namespace.elements.Array([ + { + url: 'https://user.server.com/1.0', + }, + { + url: 'https://user.server.com/2.0', + description: 'The production API server', + }, + ]); + + const parseResult = parse(context, server); + + expect(parseResult.length).to.equal(1); + + const hosts = parseResult.get(0); + const firstHost = hosts.get(0); + const secondHost = hosts.get(1); + + expect(hosts).to.be.instanceof(namespace.elements.Array); + expect(hosts.length).to.equal(2); + expect(firstHost).to.be.instanceof(namespace.elements.Resource); + expect(firstHost.classes.toValue()).to.deep.equal(['host']); + expect(firstHost.href.toValue()).to.equal('https://user.server.com/1.0'); + + expect(secondHost).to.be.instanceof(namespace.elements.Resource); + expect(secondHost.classes.toValue()).to.deep.equal(['host']); + expect(secondHost.description.toValue()).to.equal('The production API server'); + expect(secondHost.href.toValue()).to.equal('https://user.server.com/2.0'); + }); +});