Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): add support for OAS3 Server object #428

Merged
merged 11 commits into from
Apr 6, 2020
9 changes: 9 additions & 0 deletions packages/api-elements/lib/elements/Category.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
13 changes: 13 additions & 0 deletions packages/api-elements/lib/elements/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions packages/api-elements/lib/elements/Transition.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const R = require('ramda');
const { createWarning } = require('../../elements');
const {
createInvalidMemberWarning,
} = require('../annotations');
const {
isObject, hasKey, isExtension,
} = require('../../predicates');
const parseObject = require('../parseObject');
const parseString = require('../parseString');
const pipeParseResult = require('../../pipeParseResult');

const name = 'Hosts Object';
const requiredKeys = ['url'];

const parseMember = context => R.cond([
[hasKey('description'), parseString(context, name, false)],
[hasKey('url'), parseString(context, name, false)],
[hasKey('variables'), parseString(context, name, false)],
[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<Link>
* @private
*/
const parseHostsObject = context => pipeParseResult(context.namespace,
R.unless(isObject, createWarning(context.namespace, `'${name}' is not an object`)),
parseObject(context, name, parseMember(context), requiredKeys, [], true),
(hostsObject) => {
const hosts = [];

hostsObject.forEach((hostObject) => {
const resource = new context.namespace.elements.Resource();

resource.push({ classes: ['host'] });

const description = hostObject.get('description');
if (description) {
resource.push(description);
}

const variables = hostObject.get('variables');
if (variables) {
resource.hrefVariables = variables;
}

resource.href = hostObject.getValue('url');

hosts.push(resource);
});
});

module.exports = parseHostsObject;
kylef marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ 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');
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
Expand Down Expand Up @@ -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)],
Expand All @@ -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');

Expand All @@ -140,6 +143,14 @@ function parseOASObject(context, object) {
}
}

if (hosts) {
if (!hosts.isEmpty) {
api.push(new namespace.elements.Category(
hosts.content, { classes: ['hosts'] }
));
}
kylef marked this conversation as resolved.
Show resolved Hide resolved
}

const resources = object.get('paths');
if (resources) {
api.content = api.content.concat(resources.content);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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'), R.compose(parseServerVariablesObject(context), getValue)], // NOT SUPPORTED YET
[hasKey('variables'), createUnsupportedMemberWarning(context.namespace, name)],
[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<Link>
marcofriso marked this conversation as resolved.
Show resolved Hide resolved
* @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')) {
const description = object.getValue('description');
resource.description = description;
kylef marked this conversation as resolved.
Show resolved Hide resolved
}

resource.href = object.getValue('url');
kylef marked this conversation as resolved.
Show resolved Hide resolved

return resource;
});

module.exports = parseServerObject;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)));
return parseServers(element);
}

module.exports = R.curry(parseServersArray);
Original file line number Diff line number Diff line change
Expand Up @@ -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 serversArray = parseResult.api.get(0);
marcofriso marked this conversation as resolved.
Show resolved Hide resolved
expect(serversArray).to.be.instanceof(namespace.elements.Category);
expect(serversArray.classes.toValue()).to.deep.equal(['hosts']);
expect(serversArray.length).to.equal(2);

const firstServer = serversArray.get(0);
expect(firstServer).to.be.instanceof(namespace.elements.Resource);
expect(firstServer.attributes.content[0].toValue().value).to.equal('https://user.server.com/1.0');
marcofriso marked this conversation as resolved.
Show resolved Hide resolved
});

describe('with schema components', () => {
it('can parse a document with schema components into data structures', () => {
const object = new namespace.elements.Object({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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('#parseHostsObject', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
describe('#parseHostsObject', () => {
describe('#parseServerObject', () => {

I think this has been renamed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returned to old denomination

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is still valid, the name here should match the function we are testing, which is parseServerObject, from:

const parse = require('../../../../lib/parser/oas/parseServerObject');

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, changed, sorry

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('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}');
});

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'");
});
marcofriso marked this conversation as resolved.
Show resolved Hide resolved
});

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.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');
});
});
});
Loading