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

WIP: Serialization handling for cacheability #44

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ import parseSwaggerDocumentation from 'api-doc-parser/lib/swagger/parseSwaggerDo
parseSwaggerDocumentation('https://demo.api-platform.com/docs.json').then(({api}) => console.log(api));
```

### 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) {
resolve(serializer.deserialize(serializedSpecs));
} else {
parseHydraDocumentation('https://demo.api-platform.com').then(({api}) => {
localStorage.setItem('apiSpecs', serializer.serialize(api));

resolve(api);
});
}
});

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.
Expand Down
2 changes: 2 additions & 0 deletions src/Api.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type ApiOptions = {
*/
export default class Api {
entrypoint: string;
title: string;
resources: Array<Resource>;

/**
* @param {string} entrypoint
Expand Down
163 changes: 163 additions & 0 deletions src/ApiSerializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// @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|false}
*/
deserialize(data) {
const { resources = [], entrypointUrl, ...rest } = data;
const preparedResources = [];
const allFields = [];
const allOperations = [];
const allParameters = [];

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

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
});
}
}
144 changes: 144 additions & 0 deletions src/ApiSerializer.test.js
Original file line number Diff line number Diff line change
@@ -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);
});