Message body serialization adapters #372
Description
In order to serialize message bodies when generating examples for request/response body from a data structure, we can provide numerous adapters for serializing content. For example serializing a data structure element into JSON, multipart form, JSON Schema or MSON etc.
Components:
-
Serialize should return a parse result. They should be able to emit warnings and errors. This is a problem in the current design, for example serialising API Blueprint or OpenAPI can result in loss of information which isn't clear to the end user during conversion. Loss of information should create warnings, and we should have source map information where possible.
"This line from my OAS 2 document cannot be serialized into API Blueprint because X".
-
Other parsers such as OAS 2 and OAS 3 should make use of the registered serializers when generating message bodies (where generateMessageBody option is enabled). The parser consumer may load JSON if they want to generate JSON bodies, or custom adapters such as yaml, messagepack, protobuf etc.
-
Extra note: "Console" functionality in documentation can provide a UI for entering the data structures, headers, parameters which can update value in a copy of the data structure elements. It can use these adapters to generate the message body so it does not have to duplicate the efforts for each content-type.
Here's a rough idea of how this might look and how you can use these
adapters:
const { Namespace } = require('@apielements/core');
const jsonAdapter = require('@apielements/json');
const jsonSchemaAdapter = require('@apielements/json-schema');
const formURLEncodedAdapter = require('@apielements/form-urlencoded');
const multipartFormAdapter = require('@apielements/multipart-form');
const msonAdapter = require('@apielements/mson');
const namespace = new Namespace();
namespace.use(jsonAdapter);
namespace.use(jsonSchemaAdapter);
namespace.use(formURLEncodedAdapter);
namespace.use(multipartFormAdapter);
namespace.use(msonAdapter);
describe('JSON Adapter', () => {
it('can convert data structure to JSON', async () => {
const structure = new namespace.elements.Object({ name: 'Doe' })'
const parseResult = await namespace.serialize({
input: structure,
mediaType: 'application/json',
});
assert.equal(parseResult.value.toValue()), '{"name": "Doe"}');
});
});
describe('JSON Schema Adapter', () => {
it('can convert data structure to JSON', async () => {
const structure = new namespace.elements.Object({ name: 'Doe' })'
const parseResult = await namespace.serialize({
input: structure,
mediaType: 'application/schema+json',
});
assert.equal(parseResult.value.toValue()), '{"type": "object", "properties": { "name": { "type": "string" }}}');
});
});
describe('URL Form Adapter', () => {
it('can convert data structure to multipart form', async () => {
const structure = new namespace.elements.Object({ name: 'Doe' })'
const parseResult = await namespace.serialize({
input: structure,
mediaType: 'application/x-www-form-urlencoded',
});
assert.equal(parseResult.value.toValue()), 'name=doe');
});
});
describe('Multipart Form Adapter', () => {
it('can convert data structure to multipart form', async () => {
const structure = new namespace.elements.Object({ name: 'Doe' })'
const parseResult = await namespace.serialize({
input: structure,
mediaType: 'multipart/formdata; BOUNDARY=foo',
});
assert.equal(parseResult.value.toValue()), `--foo\r\nContent-Disposition: form-data; name="name"\r\n\r\nDoe\r\n\r\n--foo--\r\n');
});
});
describe('MSON Adapter', () => {
it('can convert data structure to MSON', async () => {
const structure = new namespace.elements.Object({ name: 'Doe' })'
const parseResult = await namespace.serialize({
input: structure,
mediaType: 'text/mson+markdown',
});
assert.equal(parseResult.value.toValue()), '+ name: Doe\n');
});
});
The above interface expects that the input element is frozen so it can traverse up to the parents to resolve any references. We likely want to be able to provide a cache so that there is no duplicate efforts when resolving numerous
structures which contain shared references in the same document. Possible you can register a cache with the namespace, perhaps something like:
namespace.use(new Cache());
// or
namespace.serialize({ input, cache: new Cache() });
Adapters will be able to detect cache and utilise them to prevent redundant expensive work during serializing recursive structures:
// namespace's serialize function
async function serialize({ input, mediaType }) {
if (id(for: input) && cache && cache has cache for id(for: input) + mediaType) {
return cached result;
}
// dispatch to an adapter to handle serialize
return await find('serialize', mediaType)(input);
}
async function serialize({ input, namespace }) {
// exit early if found in cache
const result = namespace.cache(forSerializer: 'application/json', input.id);
if (result) {
return result;
}
// mayor fixed pseudo/imperitive code for demo purposes: recursively go over result
// assuming input is object element
const object = {};
input.forEach((value, key) => {
// recurse serialize, it will handle cache of the value if it has ID /
// resolving references
const v = await namespace.serialize({ input: value, mediaType });
// note in this example v is actually stringified JSON, we should design a
// way around that else we'll double encode
object[key.toValue] = v;
});
return JSON.stringify(object);
}