From e653175df54261b5ef9099673d061509c956eb2b Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Sat, 23 Mar 2019 20:26:48 +0000 Subject: [PATCH 1/2] feat(core): valueOf can now support references --- .../lib/define-value-of.js | 60 ++++++++---- .../test/value-of-test.js | 98 +++++++++++++++++++ 2 files changed, 138 insertions(+), 20 deletions(-) diff --git a/packages/minim-api-description/lib/define-value-of.js b/packages/minim-api-description/lib/define-value-of.js index fa3c525d6..0841780b0 100644 --- a/packages/minim-api-description/lib/define-value-of.js +++ b/packages/minim-api-description/lib/define-value-of.js @@ -98,38 +98,58 @@ module.exports = (namespace) => { const isEnumElement = e => (e.element === 'enum' || e instanceof EnumElement); const isPlural = e => (e instanceof ArrayElement) || (e instanceof ObjectElement); - function mapValue(e, options, f) { + function mapValue(e, options, f, elements) { const opts = updateTypeAttributes(e, options); if (e.content && (!isPlural(e) || e.content.length > 0)) { - const result = f(e, opts, 'content'); + const result = f(e, opts, elements, 'content'); if (undefined !== result) { return result; } } const sample = findFirstSample(e); if (sample) { - const result = f(sample, opts, 'sample'); + const result = f(sample, opts, elements, 'sample'); if (undefined !== result) { return result; } } const dflt = findDefault(e); if (dflt) { - const result = f(dflt, opts, 'default'); + const result = f(dflt, opts, elements, 'default'); if (undefined !== result) { return result; } } if (isFlag(NULLABLE_FLAG, opts)) { - const result = f(new NullElement(), opts, 'nullable'); + const result = f(new NullElement(), opts, elements, 'nullable'); if (undefined !== result) { return result; } } + + if (elements) { + if (e.element === 'ref') { + const result = elements.filter(el => el.id.equals(e.content))[0]; + const inheritedElements = elements.filter(el => !el.id.equals(e.content)); + + if (e.path && e.path.toValue() === 'content') { + return mapValue(result.content, opts, f, inheritedElements); + } + + return mapValue(result, opts, f, inheritedElements); + } + + const result = elements.filter(el => el.id.equals(e.element))[0]; + if (result) { + const inheritedElements = elements.filter(el => !el.id.equals(e.element)); + return mapValue(result, opts, f, inheritedElements); + } + } + if (isEnumElement(e)) { const enums = e.enumerations; if (enums && enums.content && enums.content[0]) { - const result = f(enums.content[0], opts, 'generated'); + const result = f(enums.content[0], opts, elements, 'generated'); if (undefined !== result) { return result; } @@ -137,22 +157,22 @@ module.exports = (namespace) => { } const trivial = trivialValue(e); if (trivial) { - const result = f(trivial, opts, 'generated'); + const result = f(trivial, opts, elements, 'generated'); if (undefined !== result) { return result; } } if (isPlural(e) && e.content.length === 0) { - return f(e, opts, 'generated'); + return f(e, opts, elements, 'generated'); } return undefined; } - function reduceValue(e, options) { + function reduceValue(e, options, elements) { const opts = updateTypeAttributes(e, options); if (undefined === e.content) { - return mapValue(e, opts, e => e.content); + return mapValue(e, opts, e => e.content, elements); } if (isPrimitive(e)) { return e.content; @@ -161,7 +181,7 @@ module.exports = (namespace) => { return null; } if (isEnumElement(e)) { - return mapValue(e.content, inheritFlags(opts), reduceValue); + return mapValue(e.content, inheritFlags(opts), reduceValue, elements); } if (e instanceof ObjectElement) { let result = {}; @@ -171,7 +191,7 @@ module.exports = (namespace) => { && !isFlag(FIXED_TYPE_FLAG, opts) && !hasTypeAttribute(item, 'required')); - const k = mapValue(item.key, inheritFlags(opts), reduceValue); + const k = mapValue(item.key, inheritFlags(opts), reduceValue, elements); if (undefined === k) { if (skippable) { @@ -181,7 +201,7 @@ module.exports = (namespace) => { return true; } - const v = mapValue(item.value, inheritFlags(opts), reduceValue); + const v = mapValue(item.value, inheritFlags(opts), reduceValue, elements); if (undefined === v) { if (skippable) { return false; @@ -196,7 +216,7 @@ module.exports = (namespace) => { return result; } if (e instanceof ArrayElement) { - const result = e.map(item => mapValue(item, inheritFlags(opts), reduceValue)); + const result = e.map(item => mapValue(item, inheritFlags(opts), reduceValue, elements)); if (!isFlag(FIXED_FLAG, opts) && !isFlag(FIXED_TYPE_FLAG, opts)) { return result.filter(item => item !== undefined); } @@ -210,17 +230,17 @@ module.exports = (namespace) => { if (!Object.getOwnPropertyNames(Element.prototype).includes('valueOf')) { Object.defineProperty(Element.prototype, 'valueOf', { - value(flags) { + value(flags, elements) { if (flags !== undefined && flags.source) { - return mapValue(this, 0, (value, opts, source) => { - const result = reduceValue(value, opts); + return mapValue(this, 0, (value, opts, elements, source) => { + const result = reduceValue(value, opts, elements); if (undefined === result) { return undefined; } - return [reduceValue(value, opts), source]; - }); + return [reduceValue(value, opts, elements), source]; + }, elements); } - return mapValue(this, 0, (value, opts) => reduceValue(value, opts)); + return mapValue(this, 0, (value, opts) => reduceValue(value, opts, elements), elements); }, }); } diff --git a/packages/minim-api-description/test/value-of-test.js b/packages/minim-api-description/test/value-of-test.js index fc52f9b8e..ad9cfbbeb 100644 --- a/packages/minim-api-description/test/value-of-test.js +++ b/packages/minim-api-description/test/value-of-test.js @@ -4,6 +4,7 @@ const apiDescription = require('../lib/api-description'); const namespace = minim.namespace().use(apiDescription); +const { Element } = namespace; const ArrayElement = namespace.elements.Array; const ObjectElement = namespace.elements.Object; const BooleanElement = namespace.elements.Boolean; @@ -1106,3 +1107,100 @@ describe('valueOf ObjectElement with source', () => { expect(value).to.deep.equal([null, 'nullable']); }); }); + + +describe('valueOf RefElement', () => { + it('returns value from referenced element', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const element = name.toRef('element'); + const value = element.valueOf(undefined, [name]); + + expect(value).to.equal('doe'); + }); + + it('returns value from referenced elements content', () => { + const name = new StringElement('doe'); + const container = new Element(name); + container.id = 'name'; + + const element = container.toRef('content'); + const value = element.valueOf(undefined, [container]); + + expect(value).to.equal('doe'); + }); + + it('returns value from referenced element recursively', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const names = new ArrayElement([name.toRef()]); + + const value = names.valueOf(undefined, [name]); + + expect(value).to.deep.equal(['doe']); + }); +}); + +describe('valueOf RefElement with source', () => { + it('returns value from referenced element', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const element = name.toRef('element'); + const value = element.valueOf({ source: true }, [name]); + + expect(value).to.deep.equal(['doe', 'content']); + }); + + it('returns value from referenced elements content', () => { + const name = new StringElement('doe'); + const container = new Element(name); + container.id = 'name'; + + const element = container.toRef('content'); + const value = element.valueOf({ source: true }, [container]); + + expect(value).to.deep.equal(['doe', 'content']); + }); + + it('returns value from referenced element recursively', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const names = new ArrayElement([name.toRef()]); + + const value = names.valueOf({ source: true }, [name]); + + expect(value).to.deep.equal([['doe'], 'content']); + }); +}); + +describe('valueOf referenced element', () => { + it('returns value from dereferenced element', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const element = new Element(); + element.element = 'name'; + + const value = element.valueOf(undefined, [name]); + + expect(value).to.equal('doe'); + }); +}); + +describe('valueOf referenced element with source', () => { + it('returns value from dereferenced element', () => { + const name = new StringElement('doe'); + name.id = 'name'; + + const element = new Element(); + element.element = 'name'; + + const value = element.valueOf({ source: true }, [name]); + + expect(value).to.deep.equal(['doe', 'content']); + }); +}); From 759caec4623a9aa3788b5e40266c5091d40c5296 Mon Sep 17 00:00:00 2001 From: Kyle Fuller Date: Sat, 23 Mar 2019 22:09:42 +0000 Subject: [PATCH 2/2] feat(oas3): support generating body from schema with references Closes #100 Closes #186 --- .../fury-adapter-oas3-parser/CHANGELOG.md | 4 +- .../lib/parser/oas/parseComponentsObject.js | 12 +++- .../lib/parser/oas/parseMediaTypeObject.js | 13 +++- .../components/media-type-object-schema.json | 2 +- ...responses-object-response-with-schema.json | 21 ++++++ .../test/integration/fixtures/petstore.json | 4 +- .../fixtures/petstore.sourcemap.json | 4 +- .../parser/oas/parseMediaTypeObject-test.js | 70 +++++++++++++++++++ .../parser/oas/parseResponseObject-test.js | 11 +-- 9 files changed, 123 insertions(+), 18 deletions(-) diff --git a/packages/fury-adapter-oas3-parser/CHANGELOG.md b/packages/fury-adapter-oas3-parser/CHANGELOG.md index 2f6dded27..346470534 100644 --- a/packages/fury-adapter-oas3-parser/CHANGELOG.md +++ b/packages/fury-adapter-oas3-parser/CHANGELOG.md @@ -7,8 +7,8 @@ - Added primitive support for 'examples' in 'Media Type Object'. The first example value is used for JSON media types. -- Added primitive support for generating a JSON message body from a schema for - JSON media types. Referencing is not supported for this feature. +- Added support for generating a JSON message body from a schema for + JSON media types. - Added support for header parameters. diff --git a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseComponentsObject.js b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseComponentsObject.js index 27ad80bdd..a4a950881 100644 --- a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseComponentsObject.js +++ b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseComponentsObject.js @@ -119,7 +119,14 @@ function parseComponentsObject(context, element) { validateIsObject, R.compose(parseObject(context, name, parseMember), getValue), (object) => { - context.state.components.push(new namespace.elements.Member(member.key, object)); + const contextMember = context.state.components.getMember(member.key.toValue()); + + if (contextMember) { + contextMember.value = object; + } else { + context.state.components.push(new namespace.elements.Member(member.key, object)); + } + return object; })(member); }; @@ -169,7 +176,8 @@ function parseComponentsObject(context, element) { [R.T, createInvalidMemberWarning(namespace, name)], ]); - return parseObject(context, name, parseMember)(element); + const order = ['schemas']; + return parseObject(context, name, parseMember, [], order)(element); } diff --git a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseMediaTypeObject.js b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseMediaTypeObject.js index 4638296d7..8b6c16311 100644 --- a/packages/fury-adapter-oas3-parser/lib/parser/oas/parseMediaTypeObject.js +++ b/packages/fury-adapter-oas3-parser/lib/parser/oas/parseMediaTypeObject.js @@ -115,7 +115,18 @@ function parseMediaTypeObject(context, MessageBodyClass, element) { const dataStructure = mediaTypeObject.get('schema'); if (!messageBody && dataStructure && isJSONMediaType(mediaType)) { - const value = dataStructure.content.valueOf(); + let elements = []; + const { components } = context.state; + if (components) { + const schemas = components.get('schemas'); + if (schemas) { + elements = schemas.content + .filter(e => e.value && e.value.content) + .map(e => e.value.content); + } + } + + const value = dataStructure.content.valueOf(undefined, elements); if (value) { const body = JSON.stringify(value); diff --git a/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/media-type-object-schema.json b/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/media-type-object-schema.json index 5ab9305cf..3ddc7ceba 100644 --- a/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/media-type-object-schema.json +++ b/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/media-type-object-schema.json @@ -101,7 +101,7 @@ "content": "application/json" } }, - "content": "{}" + "content": "{\"name\":\"\",\"company\":{\"name\":\"\"}}" }, { "element": "dataStructure", diff --git a/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/responses-object-response-with-schema.json b/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/responses-object-response-with-schema.json index 4d241462f..971c714fb 100644 --- a/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/responses-object-response-with-schema.json +++ b/packages/fury-adapter-oas3-parser/test/integration/fixtures/components/responses-object-response-with-schema.json @@ -82,6 +82,27 @@ } }, "content": [ + { + "element": "asset", + "meta": { + "classes": { + "element": "array", + "content": [ + { + "element": "string", + "content": "messageBody" + } + ] + } + }, + "attributes": { + "contentType": { + "element": "string", + "content": "application/json" + } + }, + "content": "{}" + }, { "element": "dataStructure", "content": { 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 cf93e85fe..fceca3c31 100644 --- a/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json +++ b/packages/fury-adapter-oas3-parser/test/integration/fixtures/petstore.json @@ -154,7 +154,7 @@ "content": "application/json" } }, - "content": "[]" + "content": "[{\"id\":0,\"name\":\"\",\"tag\":\"\"}]" }, { "element": "dataStructure", @@ -329,7 +329,7 @@ "content": "application/json" } }, - "content": "[]" + "content": "[{\"id\":0,\"name\":\"\",\"tag\":\"\"}]" }, { "element": "dataStructure", 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 57d01fd01..a57c44ff2 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 @@ -404,7 +404,7 @@ "content": "application/json" } }, - "content": "[]" + "content": "[{\"id\":0,\"name\":\"\",\"tag\":\"\"}]" }, { "element": "dataStructure", @@ -854,7 +854,7 @@ "content": "application/json" } }, - "content": "[]" + "content": "[{\"id\":0,\"name\":\"\",\"tag\":\"\"}]" }, { "element": "dataStructure", diff --git a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseMediaTypeObject-test.js b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseMediaTypeObject-test.js index 88adee939..f8ce2567e 100644 --- a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseMediaTypeObject-test.js +++ b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseMediaTypeObject-test.js @@ -260,5 +260,75 @@ describe('Media Type Object', () => { expect(message.messageBody.toValue()).to.equal('{"name":"doe"}'); expect(message.messageBody.contentType.toValue()).to.equal('application/json'); }); + + it('generates a messageBody asset for JSON type with referenced schema with no examples', () => { + context.state.components = new namespace.elements.Object({ + schemas: { + Name: new namespace.elements.DataStructure( + new namespace.elements.String('doe', { + id: 'Name', + }) + ), + }, + }); + + const mediaType = new namespace.elements.Member('application/json', { + schema: { + type: 'object', + properties: { + name: { + $ref: '#/components/schemas/Name', + }, + }, + }, + }); + + const parseResult = parse(context, messageBodyClass, mediaType); + + const message = parseResult.get(0); + expect(message).to.be.instanceof(messageBodyClass); + expect(message.messageBody.toValue()).to.equal('{"name":"doe"}'); + expect(message.messageBody.contentType.toValue()).to.equal('application/json'); + }); + + it('generates a messageBody asset for JSON type with circular referenced schema with no examples', () => { + const node = new namespace.Element(); + node.element = 'Node'; + + const nodes = new namespace.Element(); + nodes.element = 'Nodes'; + + context.state.components = new namespace.elements.Object({ + schemas: { + Nodes: new namespace.elements.DataStructure( + new namespace.elements.Array({ + node, + }, { + id: 'Nodes', + }) + ), + Node: new namespace.elements.DataStructure( + new namespace.elements.Object({ + parents: nodes, + }, { + id: 'Node', + }) + ), + }, + }); + + const mediaType = new namespace.elements.Member('application/json', { + schema: { + $ref: '#/components/schemas/Node', + }, + }); + + const parseResult = parse(context, messageBodyClass, mediaType); + + const message = parseResult.get(0); + expect(message).to.be.instanceof(messageBodyClass); + expect(message.messageBody.toValue()).to.equal('{"parents":[]}'); + expect(message.messageBody.contentType.toValue()).to.equal('application/json'); + }); }); }); diff --git a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseResponseObject-test.js b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseResponseObject-test.js index a0eca4f5b..7545f77bd 100644 --- a/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseResponseObject-test.js +++ b/packages/fury-adapter-oas3-parser/test/unit/parser/oas/parseResponseObject-test.js @@ -175,19 +175,14 @@ describe('Response Object', () => { }, }); - const dataStructure = new namespace.elements.DataStructure(); - dataStructure.id = 'Node'; - + const pets = new namespace.elements.Array(); + pets.id = 'Pets'; context.state.components = new namespace.elements.Object({ schemas: { - Node: dataStructure, + Pets: new namespace.elements.DataStructure(pets), }, }); - context.state.components.set('schemas', new namespace.elements.Object([ - new namespace.elements.Member('Pets', new namespace.elements.Array()), - ])); - const parseResult = parse(context, response); expect(parseResult.get(0).headers.length).to.be.equal(2);