UNPKG

apikana

Version:

Integrated tools for REST API design - アピ

501 lines (452 loc) 20.7 kB
// 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; }