apikana
Version:
Integrated tools for REST API design - アピ
501 lines (452 loc) • 20.7 kB
JavaScript
// NOTE: This file incorporates work covered by the following copyright and permissions notice:
// MIT License
// Copyright (c) 2017 Mark Terry
// Copyright (c) 2019 Diptesh Mishra
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// MIT License
const RefParser = require('json-schema-ref-parser');
module.exports = function() {
const $RefParser = new RefParser();
const jsonSchemaAvro = {}
// Json schema on the left, avro on the right
const typeMapping = {
'array': 'array',
'string': 'string',
'null': 'null',
'boolean': 'boolean',
'integer': 'int',
'number': 'double'
}
const reSymbol = /^[A-Za-z_][A-Za-z0-9_]*$/;
jsonSchemaAvro._collectCombinationReferences = (contents) => {
return !contents ? [] : [].concat.apply([],
jsonSchemaAvro._getCombinationOf(contents).map(
(it) => {
return jsonSchemaAvro._isCombinationOf(it) ?
jsonSchemaAvro._collectCombinationReferences(it) :
(it.properties ? it.properties : [])
}
)
)
}
jsonSchemaAvro._mapPropertiesToTypes = (dereferencedAvroSchema) => {
if (!dereferencedAvroSchema) return new Map();
const new_obj = new Map();
let name
let prop
for (name in dereferencedAvroSchema) {
if (dereferencedAvroSchema.hasOwnProperty(name)) {
prop = dereferencedAvroSchema[name];
prop["$ref"] = name;
if (dereferencedAvroSchema[name].hasOwnProperty('properties')) {
new_obj.set(dereferencedAvroSchema[name].properties, prop);
} else if (dereferencedAvroSchema[name].hasOwnProperty('additionalProperties') &&
dereferencedAvroSchema[name].additionalProperties) {
new_obj.set(dereferencedAvroSchema[name].additionalProperties, prop);
} else if (jsonSchemaAvro._isCombinationOf(dereferencedAvroSchema[name])) {
new_obj.set(jsonSchemaAvro._collectCombinationReferences(dereferencedAvroSchema[name]), prop);
} else {
// throw new Error(`Invalid or unhandled reference: ${JSON.stringify(prop)}`);
}
}
}
return new_obj
};
jsonSchemaAvro.convert = async (schema, recordSuffix, splitIdForMain, enumSuffix, config) => {
if (!schema) {
throw new Error('No schema given')
}
if (typeof recordSuffix === 'undefined' || recordSuffix === null) {
recordSuffix = '_record';
}
if (typeof enumSuffix === 'undefined' || enumSuffix === null) {
enumSuffix = '_enum';
}
jsonSchemaAvro._config = config || {};
jsonSchemaAvro._splitIdForMain = splitIdForMain;
jsonSchemaAvro._enumSuffix = enumSuffix;
jsonSchemaAvro._globalTypesCache = new Map();
jsonSchemaAvro._definitions = new Map();
jsonSchemaAvro._recordSuffix = recordSuffix;
jsonSchemaAvro._schema = schema;
const avroSchema = await $RefParser.dereference(schema)
.then(function (jsonSchema) {
jsonSchemaAvro._avroJsonSchema = jsonSchema;
jsonSchemaAvro._definitions = jsonSchemaAvro._mapPropertiesToTypes(
$RefParser.$refs.values()[$RefParser.$refs.paths()].definitions
);
return jsonSchemaAvro._mainRecord(jsonSchema)
})
.catch(function (err) {
throw err;
});
return avroSchema
}
jsonSchemaAvro._mainRecord = (jsonSchema) => {
const schemaId = jsonSchemaAvro._convertId(jsonSchema.id);
const schemaIdParts = schemaId.split('.');
let schemaName = schemaIdParts.pop();
if (typeof jsonSchemaAvro._splitIdForMain === 'string') {
schemaIdParts.push(schemaName);
schemaName = schemaName + jsonSchemaAvro._splitIdForMain
}
const ns = jsonSchemaAvro._splitIdForMain ? schemaIdParts.join('.') : schemaId;
const mainRecordName = jsonSchemaAvro._splitIdForMain ? schemaName : 'main';
return jsonSchemaAvro._isOneOf(jsonSchema) || jsonSchemaAvro._isAnyOf(jsonSchema) ?
{
namespace: ns,
...jsonSchemaAvro._convertCombinationOfProperty(mainRecordName, jsonSchema)
} :
{
namespace: ns,
name: mainRecordName,
type: 'record',
doc: jsonSchema.description,
fields: [].concat.apply([], jsonSchemaAvro._getCombinationOf(jsonSchema).
map((it) => it.properties ? jsonSchemaAvro._convertProperties(it.properties, it.required) : [])
)
}
}
jsonSchemaAvro._convertId = (id) => {
return id ? id.replace(/([^a-z0-9]+)/ig, '.') : id
}
jsonSchemaAvro._isComplex = (schema) => {
return schema && schema.type === 'object'
}
jsonSchemaAvro._isArray = (schema) => {
return schema && schema.type === 'array'
}
jsonSchemaAvro._hasEnum = (schema) => {
return schema && Boolean(schema.enum)
}
jsonSchemaAvro._isCombinationOf = (schema) => {
// common handling for 'union' in avro
return schema && (schema.hasOwnProperty('oneOf') ||
schema.hasOwnProperty('allOf') ||
schema.hasOwnProperty('anyOf'))
}
jsonSchemaAvro._isOneOf = (schema) => {
return schema && schema.hasOwnProperty('oneOf')
}
jsonSchemaAvro._isAllOf = (schema) => {
return schema && schema.hasOwnProperty('allOf')
}
jsonSchemaAvro._isAnyOf = (schema) => {
return schema && schema.hasOwnProperty('anyOf')
}
jsonSchemaAvro._getCombinationOf = (schema) => {
// common handling for 'union' in avro
return schema && schema.hasOwnProperty('anyOf') ?
schema.anyOf :
(schema && schema.hasOwnProperty('oneOf') ?
schema.oneOf :
(schema && schema.hasOwnProperty('allOf') ?
schema.allOf :
// wrap to simplify recursion in edge cases
(schema ? [schema] : schema)
)
)
}
jsonSchemaAvro._isRequired = (list, item) => list && list.includes(item)
jsonSchemaAvro._convertProperties = (schema, required = []) => {
return Object.keys(schema).map((item) => {
if (jsonSchemaAvro._isComplex(schema[item])) {
return jsonSchemaAvro._convertComplexProperty(item, schema[item], jsonSchemaAvro._isRequired(required, item))
}
else if (jsonSchemaAvro._isArray(schema[item])) {
return jsonSchemaAvro._convertArrayProperty(item, schema[item], jsonSchemaAvro._isRequired(required, item))
}
else if (jsonSchemaAvro._hasEnum(schema[item])) {
return jsonSchemaAvro._convertEnumProperty(item, schema[item], schema[item].description, jsonSchemaAvro._isRequired(required, item))
}
else if (jsonSchemaAvro._isCombinationOf(schema[item])) {
return jsonSchemaAvro._convertCombinationOfProperty(item, schema[item])
}
return jsonSchemaAvro._convertProperty(item, schema[item], jsonSchemaAvro._isRequired(required, item))
})
}
jsonSchemaAvro._collectCombinationProperties = (contents) => {
return !contents ? [] : [].concat.apply([],
jsonSchemaAvro._getCombinationOf(contents).map(
(it) => {
return jsonSchemaAvro._isCombinationOf(it) ?
jsonSchemaAvro._collectCombinationProperties(it) :
(it.properties ? jsonSchemaAvro._convertProperties(it.properties, it.required) : [])
}
)
)
}
jsonSchemaAvro._getDereferencedType = (schema) => {
if (!schema) return schema;
let typeDef;
if (schema.hasOwnProperty('properties')) {
typeDef = jsonSchemaAvro._definitions.get(schema.properties)
} else if (schema.hasOwnProperty('additionalProperties' && schema.additionalProperties)) {
typeDef = jsonSchemaAvro._definitions.get(schema.additionalProperties)
} else {
typeDef = schema;
}
if (!typeDef) return typeDef;
const dereferencedType = {
type: typeDef['$ref'],
};
if (typeDef.hasOwnProperty('name')) {
dereferencedType.name = typeDef.name;
}
if (typeDef.hasOwnProperty('description')) {
dereferencedType.doc = typeDef.description;
}
return dereferencedType;
}
jsonSchemaAvro._convertCombinationOfProperty = (name, contents) => {
return ({
name: name,
doc: contents.description || '',
type: !contents ? [] : [].concat.apply([], jsonSchemaAvro._getCombinationOf(contents).
map((it) => {
const recordName = it.name ?
`${it.name}${jsonSchemaAvro._recordSuffix}` :
`${name}${jsonSchemaAvro._recordSuffix}`;
if (it && it.type && it.type === 'null') {
return 'null'
} else if (jsonSchemaAvro._globalTypesCache.get(it['$ref'])) {
return jsonSchemaAvro._globalTypesCache.get(it['$ref']);
} else {
let dereferencedType = jsonSchemaAvro._getDereferencedType(it);
jsonSchemaAvro._globalTypesCache.set(it['$ref'], dereferencedType);
let complexProperty = jsonSchemaAvro._hasEnum(it) ?
jsonSchemaAvro._convertEnumProperty(dereferencedType ? dereferencedType.type : recordName, it,
dereferencedType ? dereferencedType.doc : it.description || '') :
{
type: 'record',
name: dereferencedType ? dereferencedType.type : recordName,
doc: dereferencedType ? dereferencedType.doc : it.description || '',
fields: jsonSchemaAvro._isCombinationOf(it) ?
jsonSchemaAvro._collectCombinationProperties(it) :
(it.properties ? jsonSchemaAvro._convertProperties(it.properties, it.required) : [])
};
return complexProperty;
}
}))
});
}
jsonSchemaAvro._optionalizeType = (parent, required) => {
if(!required) {
Object.assign(parent, {default: null, type: [ "null" ].concat(parent.type) });
}
}
jsonSchemaAvro._convertComplexProperty = (name, contents, required) => {
const recordName = `${name}${jsonSchemaAvro._recordSuffix}`;
var complexProperty;
if(contents.additionalProperties) {
complexProperty = {
name: name,
doc: contents.description || '',
type: {
type: 'map',
values: contents.additionalProperties.type !== 'array' &&
contents.additionalProperties.type !== 'object' ?
typeMapping[contents.additionalProperties.type] :
jsonSchemaAvro._convertProperty(undefined, contents.additionalProperties),
default: {}
}
}
} else if (jsonSchemaAvro._globalTypesCache.get(contents.properties)) {
complexProperty = {
name: name,
doc: contents.description || '',
type: jsonSchemaAvro._globalTypesCache.get(contents.properties).type, required
};
} else {
const dereferencedType = jsonSchemaAvro._getDereferencedType(contents);
const recordType = {
type: 'record',
name: (dereferencedType && (dereferencedType.name || dereferencedType.type)) || recordName,
fields: [].concat.apply([], jsonSchemaAvro._getCombinationOf(contents || {}).
map((it) => it.properties ? jsonSchemaAvro._convertProperties(it.properties, it.required) : []))
};
if (dereferencedType && dereferencedType.doc) {
recordType.doc = dereferencedType.doc;
}
jsonSchemaAvro._globalTypesCache.set(contents.properties, dereferencedType);
complexProperty = {
name: name,
doc: contents.description || '',
type: recordType
};
}
jsonSchemaAvro._optionalizeType(complexProperty, required);
return complexProperty;
}
jsonSchemaAvro._getItems = (name, contents) => {
const recordName = `${name}${jsonSchemaAvro._recordSuffix}`;
if (jsonSchemaAvro._isComplex(contents.items)) {
const key = contents.items['$ref'] || contents.items.additionalProperties || contents.items.properties;
if (jsonSchemaAvro._globalTypesCache.get(key)) {
return jsonSchemaAvro._globalTypesCache.get(key);
} else {
const dereferencedType = jsonSchemaAvro._getDereferencedType(contents.items);
if (contents.items.additionalProperties) {
const map = {
type: 'map',
};
if (dereferencedType && dereferencedType.doc || contents.items && contents.items.description) {
map.doc = dereferencedType ? dereferencedType.doc : (contents.items.description || '')
}
const mappedType = typeMapping[contents.items.additionalProperties.type];
map.values = mappedType && mappedType !== 'array' ? mappedType :
jsonSchemaAvro._convertProperty(undefined, contents.items.additionalProperties);
return map;
}
jsonSchemaAvro._globalTypesCache.set(key, dereferencedType);
return {
type: 'record',
name: (dereferencedType && (dereferencedType.name || dereferencedType.type)) || recordName,
doc: dereferencedType ? dereferencedType.doc : (contents.items.description || ''),
fields: jsonSchemaAvro._convertProperties(contents.items.properties || {}, contents.items.required)
}
}
}
return jsonSchemaAvro._convertProperty(name, contents.items, true)
}
jsonSchemaAvro._convertArrayProperty = (name, contents, required) => {
var result = {
name: name,
doc: contents.description || '',
type: {
type: 'array',
items: jsonSchemaAvro._getItems(name, contents)
}
}
jsonSchemaAvro._optionalizeType(result, required);
return result;
}
var enums = {};
function sameArray(a1,a2) {
return a1.join(" ") == a2.join(" ");
}
jsonSchemaAvro._convertEnumProperty = (name, contents, doc, required) => {
const valid = contents.enum.every((symbol) => reSymbol.test(symbol))
const enumProp = {
name: name,
doc: doc || contents.description || '',
};
if ( !jsonSchemaAvro._config.enumAsString && valid) {
var enumName = contents.id || `${name}${jsonSchemaAvro._enumSuffix}`;
if(enums[enumName] && sameArray(contents.enum, enums[enumName])) {
enumProp.type = enumName;
} else {
if(enums[enumName]) {
var num = enumName.replace(/[^\d.]/g, '');
num = num + 1;
if(num > 1) {
var reg = new RegExp(num);
enumName = enumName.replace(reg, newVal);
} else {
enumName = enumName+"_1";
}
}
enumProp.type = {
type: 'enum',
name: enumName,
symbols: contents.enum
}
enums[enumName] = contents.enum;
}
} else {
enumProp.type = "string"
}
if(enumProp.type != "string") {
enumProp.type = [ enumProp.type, 'string' ]
}
jsonSchemaAvro._optionalizeType(enumProp, required)
if (contents.hasOwnProperty('default')) {
enumProp.default = contents.default
}
return enumProp
}
jsonSchemaAvro._convertProperty = (name, value, required = false) => {
const prop = {};
if (name) {
prop.name = name;
prop.doc = value.description || ''
}
let types = []
if (value.hasOwnProperty('default')) {
prop.default = value.default
} else if (!required) {
prop.default = null
types.push('null')
}
if (Array.isArray(value.type)) {
var type = value.type.map(type => {
return type === 'array' ?
{
type: 'array',
items: jsonSchemaAvro._getItems(name, value)
} :
type === 'object' && value.additionalProperties ?
{
type: 'map',
values: value.additionalProperties.type !== 'array' &&
value.additionalProperties.type !== 'object' ?
typeMapping[value.additionalProperties.type] :
jsonSchemaAvro._convertProperty(undefined, value.additionalProperties)
}
: typeMapping[type];
})
types.push(type);
}
else {
types.push(typeMapping[value.type])
if (types.indexOf('array') != -1) {
const itemType = jsonSchemaAvro._getItems(name, value);
prop.items = itemType && itemType.type && typeMapping[itemType.type] || itemType
}
}
prop.type = types.length > 1 ? types : types.shift()
return prop
}
jsonSchemaAvro._convertProperty = (name, value, required = false) => {
let prop = {
name: name,
doc: value.description || ''
}
let types = []
if (value.hasOwnProperty('default')) {
//console.log('has a default')
prop.default = value.default
}
else if (!required) {
//console.log('not required and has no default')
prop.default = null
types.push('null')
}
if (Array.isArray(value.type)) {
types = types.concat(value.type.filter(type => type !== 'null').map(type => typeMapping[type]))
}
else {
types.push(typeMapping[value.type])
}
//console.log('types', types)
//console.log('size', types.length)
prop.type = types.length > 1 ? types : types.shift()
//console.log('prop', prop)
return prop
}
return jsonSchemaAvro;
}