From 768d529405ce01d1a896145ad6bd58b8fd43b32c Mon Sep 17 00:00:00 2001 From: Oliver Mack Date: Thu, 17 Jan 2019 17:34:40 +0100 Subject: [PATCH 1/4] Added `ApiSerializer` which transforms and object graph of the `Api` into a JSON-serializable POJO tree and back to an `Api` graph. --- src/ApiSerializer.js | 164 ++++++++++++++++++++++++++++++++++++++ src/ApiSerializer.test.js | 144 +++++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+) create mode 100644 src/ApiSerializer.js create mode 100644 src/ApiSerializer.test.js diff --git a/src/ApiSerializer.js b/src/ApiSerializer.js new file mode 100644 index 0000000..3c7fa6d --- /dev/null +++ b/src/ApiSerializer.js @@ -0,0 +1,164 @@ +// @flow + +import Api from "./Api"; +import Resource from "./Resource"; +import Field from "./Field"; +import Operation from "./Operation"; +import Parameter from "./Parameter"; + +export default class ApiSerializer { + /** + * @param {Api} api + * @return {object} a POJO + */ + serialize(api: Api) { + const { resources, ...rest } = api; + + return { + ...rest, + resources: resources.map(this.serializeResource) + }; + } + + serializeResource = (resource: Resource) => { + const { + readableFields, + writableFields, + operations, + parameters, + ...rest + } = resource; + const result = { + ...rest + }; + + if (readableFields) { + result.readableFields = readableFields.map(this.serializeField); + } + if (writableFields) { + result.writableFields = writableFields.map(this.serializeField); + } + if (operations) { + result.operations = operations.map(this.serializeOperation); + } + if (parameters) { + result.parameters = parameters.map(this.serializeParameter); + } + + return result; + }; + + serializeField = (field: Field) => { + const { reference, ...rest } = field; + return { + ...rest, + reference: + reference && reference instanceof Resource ? reference.id : null + }; + }; + + serializeOperation = (operation: Operation) => { + return { + ...operation + }; + }; + + serializeParameter = (parameter: Parameter) => { + return { + ...parameter + }; + }; + + /** + * @param {object} data the serialized POJO + * @return {Api} + */ + deserialize(data) { + const { resources = [], entrypointUrl, ...rest } = data; + const preparedResources = []; + const allFields = []; + const allOperations = []; + const allParameters = []; + + if (resources) { + for (const resourceData of resources) { + const { + name, + url, + readableFields, + writableFields, + operations, + parameters, + ...resourceRest + } = resourceData; + const resourceOptions = { ...resourceRest }; + const resourceReadableFields = []; + const resourceWritableFields = []; + const resourceOperations = []; + const resourceParameters = []; + + if (readableFields) { + for (const { fieldName, ...fieldOptions } of readableFields) { + const field = new Field(fieldName, fieldOptions); + resourceReadableFields.push(field); + allFields.push(field); + } + + resourceOptions.readableFields = resourceReadableFields; + } + + if (writableFields) { + for (const { fieldName, ...fieldOptions } of writableFields) { + const field = new Field(fieldName, fieldOptions); + resourceWritableFields.push(field); + allFields.push(field); + } + + resourceOptions.writableFields = resourceWritableFields; + } + + if (operations) { + for (const { operationName, ...operationOptions } of operations) { + const operation = new Operation(operationName, operationOptions); + resourceOperations.push(operation); + allOperations.push(operation); + } + + resourceOptions.operations = resourceOperations; + } + + if (parameters) { + for (const { variable, range, required, description } of parameters) { + const parameter = new Parameter( + variable, + range, + required, + description + ); + resourceParameters.push(parameter); + allParameters.push(parameter); + } + + resourceOptions.parameters = resourceParameters; + } + + preparedResources.push(new Resource(name, url, resourceOptions)); + } + + // Resolve references + for (const field of allFields) { + if (null !== field.reference) { + field.reference = + preparedResources.find( + resource => resource.id === field.reference + ) || null; + } + } + } + + return new Api(entrypointUrl, { + ...rest, + resources: preparedResources + }); + } +} diff --git a/src/ApiSerializer.test.js b/src/ApiSerializer.test.js new file mode 100644 index 0000000..9071e2c --- /dev/null +++ b/src/ApiSerializer.test.js @@ -0,0 +1,144 @@ +import Api from "./Api"; +import ApiSerializer from "./ApiSerializer"; +import Resource from "./Resource"; +import Field from "./Field"; +import Operation from "./Operation"; +import Parameter from "./Parameter"; + +test("can serialize and deserialize Api", () => { + const book = new Resource("Books", "http://localhost/books", { + id: "http://schema.org/Book", + title: "Book", + readableFields: [ + new Field("isbn", { + id: "http://schema.org/isbn", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "The ISBN of the book", + maxCardinality: null, + deprecated: false + }), + new Field("name", { + id: "http://schema.org/name", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "The name of the item", + maxCardinality: null, + deprecated: false + }) + ], + writableFields: [ + new Field("isbn", { + id: "http://schema.org/isbn", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "The ISBN of the book", + maxCardinality: null, + deprecated: false + }), + new Field("name", { + id: "http://schema.org/name", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "The name of the item", + maxCardinality: null, + deprecated: false + }) + ], + operations: [ + new Operation("Retrieves Book resource.", { + method: "GET", + returns: "http://schema.org/Book", + types: ["http://www.w3.org/ns/hydra/core#Operation"], + deprecated: false + }), + new Operation("Replaces the Book resource.", { + method: "PUT", + expects: "http://schema.org/Book", + returns: "http://schema.org/Book", + types: ["http://www.w3.org/ns/hydra/core#ReplaceResourceOperation"], + deprecated: false + }), + new Operation("Deletes the Book resource.", { + method: "DELETE", + returns: "http://www.w3.org/2002/07/owl#Nothing", + types: ["http://www.w3.org/ns/hydra/core#Operation"], + deprecated: false + }) + ], + parameters: [ + new Parameter( + "isbn", + "http://www.w3.org/2001/XMLSchema#string", + false, + "" + ) + ] + }); + + const review = new Resource("Reviews", "http://localhost/reviews", { + id: "http://schema.org/Review", + title: "Book", + readableFields: [ + new Field("reviewBody", { + id: "http://schema.org/reviewBody", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: false, + description: "The actual body of the review", + maxCardinality: null, + deprecated: false + }), + new Field("itemReviewed", { + id: "http://schema.org/itemReviewed", + range: "http://schema.org/Book", + reference: book, + required: true, + description: "The name of the item", + maxCardinality: null, + deprecated: false + }) + ], + writableFields: [ + new Field("reviewBody", { + id: "http://schema.org/reviewBody", + range: "http://www.w3.org/2001/XMLSchema#string", + reference: null, + required: true, + description: "The actual body of the review", + maxCardinality: null, + deprecated: false + }), + new Field("itemReviewed", { + id: "http://schema.org/itemReviewed", + range: "http://schema.org/Book", + reference: book, + required: true, + description: "The item that is being reviewed/rated", + maxCardinality: null, + deprecated: false + }) + ] + }); + + const api = new Api("http://localhost", { + title: "API Platform's demo", + resources: [book, review] + }); + + const serializer = new ApiSerializer(); + const serialized = serializer.serialize(api); + + // verify that the serialized and stringified versions don't differ + // this ensures that the serializer does not return stuff that cannot be + // expressed in json-string notation properly + expect(JSON.parse(JSON.stringify(serialized))).toEqual(serialized); + + const deserialized = serializer.deserialize(serialized); + + expect(deserialized).toEqual(api); +}); From c0dbccdb9c07c5dd772e617483f8c26159a72ffb Mon Sep 17 00:00:00 2001 From: Oliver Mack Date: Thu, 17 Jan 2019 17:48:45 +0100 Subject: [PATCH 2/4] Some basic docs --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 83c82eb..6927236 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,46 @@ API Doc Parser is designed to parse any API documentation format and convert it For now, only Hydra and Swagger is supported but if you develop a parser for another format, please [open a Pull Request](https://github.com/dunglas/api-doc-parser/pulls) to include it in the library. +## Serialization + +In order to allow caching (e.g. for performance or offline fallback purpose) you can utilize the `ApiSerializer` which can serialize the `Api` object graph to a plain javascript object tree which can be json-serialized easily (the `Api` object graph may have circular references which means it is in some circumstances not json-serializable as is). + +```javascript +import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation'; +import ApiSerializer from 'api-doc-parse/lib/ApiSerializer'; + +parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => { + const serializer = new ApiSerializer(); + const serialized = serializer.serialize(api); + + console.log(JSON.stringify(serialized)); +}); +``` + +A scenario where you'd like to utilize some storage (e.g. `localStorage`) for caching you could implement something like this: + +```javascript +import parseHydraDocumentation from 'api-doc-parser/lib/hydra/parseHydraDocumentation'; +import ApiSerializer from 'api-doc-parse/lib/ApiSerializer'; + +const getApiSpecs = () => new Promise(resolve => { + const serializer = new ApiSerializer(); + const serializedSpecs = localStorage.getItem('apiSpecs'); + + if (!serializedSpecs) { + parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => { + localStorage.setItem('apiSpecs', serializer.serialize(api)); + + resolve(api); + }); + } else { + resolve(serializer.deserialize(serializedSpecs)); + } +}); + +getApiSpecs().then(specs => console.log(specs)); +``` + ## Run tests yarn test From aee3ba9b0c5fc30233101a65074065ae2277d817 Mon Sep 17 00:00:00 2001 From: Oliver Mack Date: Thu, 17 Jan 2019 17:49:15 +0100 Subject: [PATCH 3/4] Some basic docs --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6927236..69cc2ea 100644 --- a/README.md +++ b/README.md @@ -40,13 +40,7 @@ import parseSwaggerDocumentation from 'api-doc-parser/lib/swagger/parseSwaggerDo parseSwaggerDocumentation('https://demo.api-platform.com/docs.json').then(({api}) => console.log(api)); ``` -## Support for other formats (GraphQL, JSONAPI...) - -API Doc Parser is designed to parse any API documentation format and convert it in the same intermediate representation. -For now, only Hydra and Swagger is supported but if you develop a parser for another format, please [open a Pull Request](https://github.com/dunglas/api-doc-parser/pulls) -to include it in the library. - -## Serialization +### Serialization In order to allow caching (e.g. for performance or offline fallback purpose) you can utilize the `ApiSerializer` which can serialize the `Api` object graph to a plain javascript object tree which can be json-serialized easily (the `Api` object graph may have circular references which means it is in some circumstances not json-serializable as is). @@ -86,6 +80,12 @@ const getApiSpecs = () => new Promise(resolve => { getApiSpecs().then(specs => console.log(specs)); ``` +## Support for other formats (GraphQL, JSONAPI...) + +API Doc Parser is designed to parse any API documentation format and convert it in the same intermediate representation. +For now, only Hydra and Swagger is supported but if you develop a parser for another format, please [open a Pull Request](https://github.com/dunglas/api-doc-parser/pulls) +to include it in the library. + ## Run tests yarn test From 13ba05b81849304a1573eeef10d4c3236898825a Mon Sep 17 00:00:00 2001 From: Oliver Mack Date: Mon, 28 Jan 2019 10:44:15 +0100 Subject: [PATCH 4/4] Added requested changes from review --- README.md | 8 +-- src/Api.js | 2 + src/ApiSerializer.js | 123 +++++++++++++++++++++---------------------- 3 files changed, 67 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 69cc2ea..a4ef905 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,14 @@ const getApiSpecs = () => new Promise(resolve => { const serializer = new ApiSerializer(); const serializedSpecs = localStorage.getItem('apiSpecs'); - if (!serializedSpecs) { + if (serializedSpecs) { + resolve(serializer.deserialize(serializedSpecs)); + } else { parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => { localStorage.setItem('apiSpecs', serializer.serialize(api)); resolve(api); - }); - } else { - resolve(serializer.deserialize(serializedSpecs)); + }); } }); diff --git a/src/Api.js b/src/Api.js index df17533..57f45b2 100644 --- a/src/Api.js +++ b/src/Api.js @@ -12,6 +12,8 @@ type ApiOptions = { */ export default class Api { entrypoint: string; + title: string; + resources: Array; /** * @param {string} entrypoint diff --git a/src/ApiSerializer.js b/src/ApiSerializer.js index 3c7fa6d..32e6e2b 100644 --- a/src/ApiSerializer.js +++ b/src/ApiSerializer.js @@ -71,7 +71,7 @@ export default class ApiSerializer { /** * @param {object} data the serialized POJO - * @return {Api} + * @return {Api|false} */ deserialize(data) { const { resources = [], entrypointUrl, ...rest } = data; @@ -80,79 +80,78 @@ export default class ApiSerializer { const allOperations = []; const allParameters = []; - if (resources) { - for (const resourceData of resources) { - const { - name, - url, - readableFields, - writableFields, - operations, - parameters, - ...resourceRest - } = resourceData; - const resourceOptions = { ...resourceRest }; - const resourceReadableFields = []; - const resourceWritableFields = []; - const resourceOperations = []; - const resourceParameters = []; - - if (readableFields) { - for (const { fieldName, ...fieldOptions } of readableFields) { - const field = new Field(fieldName, fieldOptions); - resourceReadableFields.push(field); - allFields.push(field); - } - - resourceOptions.readableFields = resourceReadableFields; + if (!resources) return false; + + for (const resourceData of resources) { + const { + name, + url, + readableFields, + writableFields, + operations, + parameters, + ...resourceRest + } = resourceData; + const resourceOptions = { ...resourceRest }; + const resourceReadableFields = []; + const resourceWritableFields = []; + const resourceOperations = []; + const resourceParameters = []; + + if (readableFields) { + for (const { fieldName, ...fieldOptions } of readableFields) { + const field = new Field(fieldName, fieldOptions); + resourceReadableFields.push(field); + allFields.push(field); } - if (writableFields) { - for (const { fieldName, ...fieldOptions } of writableFields) { - const field = new Field(fieldName, fieldOptions); - resourceWritableFields.push(field); - allFields.push(field); - } + resourceOptions.readableFields = resourceReadableFields; + } - resourceOptions.writableFields = resourceWritableFields; + if (writableFields) { + for (const { fieldName, ...fieldOptions } of writableFields) { + const field = new Field(fieldName, fieldOptions); + resourceWritableFields.push(field); + allFields.push(field); } - if (operations) { - for (const { operationName, ...operationOptions } of operations) { - const operation = new Operation(operationName, operationOptions); - resourceOperations.push(operation); - allOperations.push(operation); - } + resourceOptions.writableFields = resourceWritableFields; + } - resourceOptions.operations = resourceOperations; + if (operations) { + for (const { operationName, ...operationOptions } of operations) { + const operation = new Operation(operationName, operationOptions); + resourceOperations.push(operation); + allOperations.push(operation); } - if (parameters) { - for (const { variable, range, required, description } of parameters) { - const parameter = new Parameter( - variable, - range, - required, - description - ); - resourceParameters.push(parameter); - allParameters.push(parameter); - } - - resourceOptions.parameters = resourceParameters; + resourceOptions.operations = resourceOperations; + } + + if (parameters) { + for (const { variable, range, required, description } of parameters) { + const parameter = new Parameter( + variable, + range, + required, + description + ); + resourceParameters.push(parameter); + allParameters.push(parameter); } - preparedResources.push(new Resource(name, url, resourceOptions)); + resourceOptions.parameters = resourceParameters; } - // Resolve references - for (const field of allFields) { - if (null !== field.reference) { - field.reference = - preparedResources.find( - resource => resource.id === field.reference - ) || null; - } + preparedResources.push(new Resource(name, url, resourceOptions)); + } + + // Resolve references + for (const field of allFields) { + if (null !== field.reference) { + field.reference = + preparedResources.find(resource => resource.id === field.reference) || + null; } }