Skip to content
This repository was archived by the owner on Nov 8, 2024. It is now read-only.

Add an OpenAPI 3 Serializer #468

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ API Elements adapters:
- [API Blueprint Serializer](packages/apib-serializer)
- [OpenAPI 2 Parser](packages/openapi2-parser)
- [OpenAPI 3 Parser](packages/openapi3-parser)
- [OpenAPI 3 Serializer](packages/openapi3-serializer)

## Usage

Expand Down
26 changes: 26 additions & 0 deletions packages/openapi3-serializer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# API Elements: OpenAPI 3 Serializer

[![NPM version](https://img.shields.io/npm/v/@apielements/openapi3-serializer.svg)](https://www.npmjs.org/package/@apielements/openapi3-serializer)
[![License](https://img.shields.io/npm/l/@apielements/openapi3-serializer.svg)](https://www.npmjs.org/package/@apielements/openapi3-serializer)

This adapter provides support for serializing [OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.3) in [Fury.js](https://github.com/apiaryio/api-elements.js/tree/master/packages/fury) from API Elements.

## Install

```sh
$ npm install @apielements/openapi3-serializer
```

## Usage

```js
const fury = require('fury');
const openapi3Serializer = require('@apielements/openapi3-serializer');

fury.use(openapi3Serializer);

// Assume `api` is a Minim element instance, e.g. from `fury.parse(...)`
fury.serialize({ api, mediaType: 'application/vnd.oai.openapi' }, (error, content) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd expect the serialize would be synchronous for easy of use.

Copy link
Member Author

Choose a reason for hiding this comment

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

The parse and serialize functions in Fury are async. As we provide a uniform interface for serializing regardless of format and adapter implementation. This includes the "remote" adapter which uses HTTP to parse/serialize this could be problematic to support them all as sync.

I don't have much strong opinions against or for this, so if you would like sync API we can create an issue in the repository to investigate support across all the adapters.

Copy link
Member Author

Choose a reason for hiding this comment

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

FWIW, master has sync interface for serialize now. We can support it here 😄.

console.log(content);
});
```
97 changes: 97 additions & 0 deletions packages/openapi3-serializer/STATUS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# API Element Support

## Category Element

| Class | Support |
|:--------------|:--------|
| api | ✓ |
| authSchemes | |
| hosts | |
| resourceGroup | ✓ |
| scenario | |
| transitions | |

## Resource Element

| Meta | Support |
|:------|:--------|
| title | ✓ |

| Attributes | Support |
|:--------------|:--------|
| hosts | |
| href | ✓ |
| hrefVariables | ✓ |

| Content | Support |
|:-----------------------|:--------|
| Copy Element | ✓ |
| Category Element | |
| Transition Element | |
| Data Structure Element | |

## Href Variables: Member Element

| Meta | Support |
|:------------|---------|
| description | |

| Content | Support |
|:--------|---------|
| key | ✓ |
| value | |

## Transition Element

| Attributes | Support |
|:--------------|:--------|
| contentTypes | |
| hosts | |
| href | |
| hrefVariables | |
| relation | |

| Content | Support |
|:-------------------------|:--------|
| Copy Element | |
| HTTP Transaction Element | ✓ |

## HTTP Transaction Element

| Attributes | Support |
|:-------------|:--------|
| authSchemes | |

| Content | Support |
|:--------------|:--------|
| Copy Element | |
| HTTP Request | |
| HTTP Response | ✓ |

## HTTP Request

| Attributes | Support |
|:--------------|:--------|
| method | ✓ |
| href | |
| hrefVariables | |
| headers | |

| Content | Support |
|:----------------|:--------|
| Copy | |
| Data Structure | |
| Asset | |

## HTTP Response

| Attributes | Support |
|:-------------|:--------|
| statusCode | ✓ |
| headers | |

| Content | Support |
|:----------------|:--------|
| Copy | |
| Data Structure | |
| Asset | |
29 changes: 29 additions & 0 deletions packages/openapi3-serializer/lib/adapter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const yaml = require('js-yaml');
const serializeApi = require('./serialize/serializeApi');

const name = 'openapi3';
const openApiMediaType = 'application/vnd.oai.openapi';
const openApiJsonMediaType = 'application/vnd.oai.openapi+json';

// Per https://github.com/OAI/OpenAPI-Specification/issues/110#issuecomment-364498200
const mediaTypes = [
openApiMediaType,
openApiJsonMediaType,
];

function serialize({ api, mediaType }) {
return new Promise((resolve, reject) => {
const document = serializeApi(api);


if (mediaType === openApiMediaType) {
resolve(yaml.dump(document));
} else if (mediaType === openApiJsonMediaType) {
resolve(JSON.stringify(document));
} else {
reject(new Error(`Unsupported media type ${mediaType}`));
}
});
}

module.exports = { name, mediaTypes, serialize };
60 changes: 60 additions & 0 deletions packages/openapi3-serializer/lib/serialize/serializeApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const R = require('ramda');
const serializeResource = require('./serializeResource');

const isResource = element => element.element === 'resource';
const isCategory = element => element.element === 'category';
const isResourceGroup = R.allPass([
isCategory,
category => category.classes.contains('resourceGroup'),
]);
const isResourceOrResourceGroup = R.anyPass([isResource, isResourceGroup]);

function convertUri(href) {
return href.toValue().replace(/{[+#./;?&](.*)\*?}/, '');
}

function serializeResourceGroup(category) {
let paths = {};

category.forEach((element) => {
if (isResource(element)) {
paths[convertUri(element.href)] = serializeResource(element);
} else if (isResourceOrResourceGroup(element)) {
paths = R.mergeAll([paths, serializeResourceGroup(element)]);
}
});

return paths;
}

function serializeApi(api) {
const info = {};

const title = api.meta.get('title');
if (title) {
info.title = title.toValue();
} else {
info.title = 'API';
}

const version = api.attributes.get('version');
if (version) {
info.version = version.toValue();
} else {
info.version = 'Unknown';
}

if (api.copy.length > 0) {
info.description = api.copy.toValue().join('\n\n');
}

const document = {
openapi: '3.0.3',
info,
paths: serializeResourceGroup(api),
};

return document;
}

module.exports = serializeApi;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function serializeHrefVariables(href, hrefVariables) {
return hrefVariables.map((value, key) => {
const parameter = {
name: key.toValue(),
};

if (href.toValue().includes(`{${key.toValue()}}`)) {
parameter.in = 'path';
} else {
// FIXME assuming parameter is query
parameter.in = 'query';
}

return parameter;
});
}

module.exports = serializeHrefVariables;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
function serializeHttpResponse() {
const response = {
description: 'Unknown',
};

return response;
}

module.exports = serializeHttpResponse;
31 changes: 31 additions & 0 deletions packages/openapi3-serializer/lib/serialize/serializeResource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const serializeHrefVariables = require('./serializeHrefVariables');
const serializeTransition = require('./serializeTransition');

function serializeResource(resource) {
const pathItem = {};

const title = resource.meta.get('title');
if (title) {
pathItem.summary = title.toValue();
}

if (resource.copy.length > 0) {
pathItem.description = resource.copy.toValue().join('\n\n');
}

if (resource.hrefVariables) {
pathItem.parameters = serializeHrefVariables(resource.href, resource.hrefVariables);
}

if (resource.transitions) {
resource.transitions.forEach((transition) => {
if (transition.method) {
pathItem[transition.method.toValue().toLowerCase()] = serializeTransition(transition);
}
});
}

return pathItem;
}

module.exports = serializeResource;
19 changes: 19 additions & 0 deletions packages/openapi3-serializer/lib/serialize/serializeTransition.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const serializeHttpResponse = require('./serializeHttpResponse');

function serializeTransition(transition) {
const operation = {
responses: {},
};

transition.transactions
.compactMap(transaction => transaction.response)
.forEach((response) => {
const statusCode = String(response.statusCode.toValue());
operation.responses[statusCode] = serializeHttpResponse(response);
});


return operation;
}

module.exports = serializeTransition;
32 changes: 32 additions & 0 deletions packages/openapi3-serializer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@apielements/openapi3-serializer",
"version": "0.1.0",
"description": "Open API Specification 3 API Elements Serializer",
"main": "./lib/adapter.js",
"repository": {
"type": "git",
"url": "https://github.com/apiaryio/api-elements.js.git"
},
"scripts": {
"lint": "eslint .",
"test": "mocha --recursive test"
},
"dependencies": {
"js-yaml": "^3.13.1",
"ramda": "0.27.0"
},
"peerDependencies": {
"fury": "3.0.0-beta.14"
},
"devDependencies": {
"chai": "^4.2.0",
"eslint": "^5.16.0",
"fury": "3.0.0-beta.14",
"mocha": "^7.1.1"
},
"engines": {
"node": ">=8"
},
"author": "Apiary.io <[email protected]>",
"license": "MIT"
}
44 changes: 44 additions & 0 deletions packages/openapi3-serializer/test/unit/adapter-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { expect } = require('chai');
const { Fury } = require('fury');

const adapter = require('../../lib/adapter');

const fury = new Fury();
fury.use(adapter);

describe('Adapter', () => {
it('has a name', () => {
expect(adapter.name).to.equal('openapi3');
});

it('has OpenAPI media types', () => {
expect(adapter.mediaTypes).to.deep.equal([
'application/vnd.oai.openapi',
'application/vnd.oai.openapi+json',
]);
});

it('serializes an API Element to OpenAPI 3 as YAML', (done) => {
const api = new fury.minim.elements.Category([], { classes: ['api'] });
api.title = 'Polls API';
api.attributes.set('version', '2.0.0');

fury.serialize({ api, mediaType: 'application/vnd.oai.openapi' }, (err, result) => {
expect(err).to.be.null;
expect(result).to.equal('openapi: 3.0.3\ninfo:\n title: Polls API\n version: 2.0.0\npaths: {}\n');
done();
});
});

it('serializes an API Element to OpenAPI 3 as JSON', (done) => {
const api = new fury.minim.elements.Category([], { classes: ['api'] });
api.title = 'Polls API';
api.attributes.set('version', '2.0.0');

fury.serialize({ api, mediaType: 'application/vnd.oai.openapi+json' }, (err, result) => {
expect(err).to.be.null;
expect(result).to.equal('{"openapi":"3.0.3","info":{"title":"Polls API","version":"2.0.0"},"paths":{}}');
done();
});
});
});
Loading