@firebolt-js/openrpc
Version:
The Firebolt SDK Code & Doc Generator
954 lines (855 loc) • 43.8 kB
JavaScript
/*
* Copyright 2021 Comcast Cable Communications Management, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
import deepmerge from 'deepmerge'
import { getPath, localizeDependencies, getSafeEnumKeyName, getSafeKeyName } from '../shared/json-schema.mjs'
import path from "path"
import { getConfig } from '../shared/configLoader.mjs'
const config = getConfig()
let convertTuplesToArraysOrObjects = false
const templates = {}
const state = {}
let primitives = {
"integer": "number",
"number": "number",
"boolean": "boolean",
"string": "string"
}
const stdPrimitives = [ "integer", "number", "boolean", "string" ]
const isVoid = type => (type === 'void') ? true : false
const isPrimitiveType = type => stdPrimitives.includes(type) ? true : false
const allocatedPrimitiveProxies = {}
const isObject = schema => (schema.type === 'object') || (Array.isArray(schema.type) && schema.type.includes("object"))
function setTemplates(t) {
Object.assign(templates, t)
}
function setPrimitives(p) {
if (p) {
primitives = p
}
}
function setConvertTuples(t) {
convertTuplesToArraysOrObjects = t
}
function setAllocatedPrimitiveProxies(m) {
Object.assign(allocatedPrimitiveProxies, m)
}
const capitalize = str => str ? str[0].toUpperCase() + str.substr(1) : str
const indent = (str, padding) => {
let first = true
if (str) {
return str.split('\n').map(line => {
if (first) {
first = false
return line
}
else {
return padding + line
}
}).join('\n')
}
}
// TODO: This is what's left of getMethodSignatureParams. We need to figure out / handle C's `FireboltTypes_StringHandle`
function getMethodSignatureParams(method, module, { destination, callback }) {
const paramOptional = getTemplate('/parameters/optional')
let polymorphicPull = method.tags.find(t => t.name === 'polymorphic-pull')
return method.params.map(param => {
if (polymorphicPull && (param.name === 'correlationId')) {
return
}
let type = getSchemaType(param.schema, module, { destination, namespace : true })
if (callback && allocatedPrimitiveProxies[type]) {
type = allocatedPrimitiveProxies[type]
}
let paramRequired = ''
let jsonType = getJsonType(param.schema, module, { destination })
if (!isPrimitiveType(jsonType) && getTemplate('/parameters/nonprimitive')) {
paramRequired = getTemplate('/parameters/nonprimitive')
}
else if ((jsonType === 'string') && getTemplate('/parameters/string')) {
paramRequired = getTemplate('/parameters/string')
}
else {
paramRequired = getTemplate('/parameters/default')
}
if (callback) {
return (!(param.required && param.required === true) ? paramOptional.replace(/\$\{method\.param\.name\}/g, param.name).replace(/\$\{method\.param\.type\}/g, type) : '')
}
else {
return (param.required ? paramRequired : paramOptional).replace(/\$\{method\.param\.name\}/g, param.name).replace(/\$\{method\.param\.type\}/g, type)
}
}).filter(param => param).join(', ')
}
function getMethodSignatureResult(method, module, { destination, callback }) {
let type = getSchemaType(method.result.schema, module, { destination, namespace : true })
let result = ''
if (callback) {
let jsonType = getJsonType(method.result.schema, module, { destination })
if (!isVoid(type) && !isPrimitiveType(jsonType) && getTemplate('/result-callback/nonprimitive')) {
result = getTemplate('/result-callback/nonprimitive')
}
else if ((jsonType === 'string') && getTemplate('/result-callback/string')) {
result = getTemplate('/result-callback/string')
}
else {
result = getTemplate('/result-callback/default')
}
}
else {
result = getTemplate('/result/default')
}
return result.replace(/\$\{method\.result\.type\}/g, type)
}
const getTemplate = (name) => {
if (name[0] !== '/') {
name = '/' + name
}
return templates[Object.keys(templates).find(k => k === name)] || templates[Object.keys(templates).find(k => k.startsWith(name.split('.').shift() + '.'))] || ''
}
const getXSchemaGroupFromProperties = (schema, title, properties, group) => {
if (properties) {
Object.entries(properties).forEach(([name, prop]) => {
if ((schema.title === prop.title) || (prop.items && prop.items.title === schema.title)) {
group = title
}
else {
group = getXSchemaGroupFromProperties(schema, title, prop.properties, group)
}
})
}
return group
}
// TODO: this assumes the same title doesn't exist in multiple x-schema groups!
const getXSchemaGroup = (schema, module) => {
let group = module.info.title
if (schema.title && module['x-schemas']) {
Object.entries(module['x-schemas']).forEach(([title, module]) => {
Object.values(module).forEach(moduleSchema => {
let schemas = moduleSchema.allOf ? moduleSchema.allOf : [moduleSchema]
schemas.forEach((s) => {
if (schema.title === s.title || schema.title === moduleSchema.title) {
group = title
} else {
group = getXSchemaGroupFromProperties(schema, title, s.properties, group)
}
})
})
})
}
return group
}
function getSchemaDescription(schema, module) {
let description = schema.description || ''
if (schema.type === 'array' && schema.items) {
schema = schema.items
}
if (schema['$ref'] && (schema['$ref'][0] === '#')) {
const refSchema = getPath(schema['$ref'], module)
description = (refSchema && refSchema.description) || description
}
return description
}
function insertSchemaMacros(content, schema, module, { name = '', parent = '', property = '', required = false, recursive = true, templateDir = 'types'}) {
const title = name || schema.title || ''
const moduleTitle = getXSchemaGroup(schema, module)
const description = getSchemaDescription(schema, module)
content = content
.replace(/\$\{title\}/g, title)
.replace(/\$\{Title\}/g, capitalize(title))
.replace(/\$\{TITLE\}/g, title.toUpperCase())
.replace(/\$\{property\}/g, property)
.replace(/\$\{Property\}/g, capitalize(property))
.replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (module.info.title !== (parent || moduleTitle)) ? '$1' : '')
.replace(/\$\{parent\.title\}/g, parent || moduleTitle)
.replace(/\$\{parent\.Title\}/g, capitalize(parent || moduleTitle))
.replace(/\$\{description\}/g, description)
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '' : '$1')
.replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '$1' : '')
.replace(/\$\{if\.summary\}(.*?)\$\{end\.if\.summary\}/gms, description ? '$1' : '')
.replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '')
.replace(/\$\{name\}/g, title)
.replace(/\$\{NAME\}/g, title.toUpperCase())
.replace(/\$\{info.title\}/g, moduleTitle)
.replace(/\$\{info.Title\}/g, capitalize(moduleTitle))
.replace(/\$\{info.TITLE\}/g, moduleTitle.toUpperCase())
if (recursive) {
content = content.replace(/\$\{type\}/g, getSchemaType(schema, module, { templateDir: templateDir, destination: state.destination, section: state.section, code: false, namespace: true }))
}
return content
}
// TODO using JSON.stringify probably won't work for many languages...
const insertConstMacros = (content, schema, module, name) => {
content = content.replace(/\$\{value\}/g, (typeof schema.const === 'string' ? `'${schema.const}'` : schema.const))
return content
}
const insertEnumMacros = (content, schema, module, name, suffix, templateDir = "types") => {
const template = content.split('\n')
for (var i = 0; i < template.length; i++) {
if (template[i].indexOf('${key}') >= 0) {
let values = []
schema.enum.map(value => {
if (!value) {
value = getTemplate(path.join(templateDir, 'unset' + suffix))
}
value ? values.push(template[i].replace(/\$\{key\}/g, getSafeEnumKeyName(value))
.replace(/\$\{value\}/g, value)) : ''
})
template[i] = values.map((value, id) => {
return value.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, id === values.length - 1 ? '' : '$1')
}).join('\n')
}
}
return template.join('\n')
}
const insertObjectAdditionalPropertiesMacros = (content, schema, module, title, options) => {
const options2 = options ? JSON.parse(JSON.stringify(options)) : {}
options2.parent = title
options2.level = options.level + 1
options2.required = options.required
const shape = getSchemaShape(schema.additionalProperties, module, options2)
let type = getSchemaType(schema.additionalProperties, module, options2).trimEnd()
const propertyNames = localizeDependencies(schema, module).propertyNames
let jsonType = getJsonType(schema.additionalProperties, module)
if (!isPrimitiveType(jsonType)) {
jsonType = 'string'
}
const additionalType = getPrimitiveType(jsonType, 'additional-types')
let namespace = ''
let defaultKey = true
let key = getTemplate(path.join('types', 'additionalPropertiesKey')).trimEnd()
if (propertyNames && propertyNames.title) {
let parent = getXSchemaGroup(propertyNames, module)
key = propertyNames.title
namespace = getTemplate(path.join(options.templateDir, 'namespace'))
.replace(/\$\{if\.namespace\.notsame}(.*?)\$\{end\.if\.namespace\.notsame\}/g, (module.info.title !== (parent || moduleTitle)) ? '$1' : '')
.replace(/\$\{parent\.Title\}/g, (parent && module.info.title !== parent) ? parent : '')
defaultKey = false
}
content = content
.replace(/\$\{shape\}/g, shape)
.replace(/\$\{if\.default\}(.*?)\$\{end\.if\.default\}/g, defaultKey ? '$1' : '')
.replace(/\$\{if\.not.default\}(.*?)\$\{end\.if\.not.default\}/gms, defaultKey ? '' : '$1')
.replace(/\$\{parent\.title\}/g, title)
.replace(/\$\{title\}/g, title)
.replace(/\$\{type\}/g, type)
.replace(/\$\{additional\.type\}/g, additionalType)
.replace(/\$\{key\}/g, key)
.replace(/\$\{namespace\}/g, namespace)
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, '')
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/g, '')
.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/g, options.required ? '' : '$1')
.replace(/\$\{if\.impl.non.optional\}(.*?)\$\{end\.if\.impl.non.optional\}/g, options.required ? '$1' : '')
return content
}
const insertObjectPatternPropertiesMacros = (content, schema, module, title, options) => {
const options2 = options ? JSON.parse(JSON.stringify(options)) : {}
options2.parent = title
options2.level = options.level + 1
options2.required = options.required
let patternSchema;
Object.entries(schema.patternProperties).forEach(([pattern, sch]) => {
patternSchema = sch
})
if (patternSchema) {
const shape = getSchemaShape(patternSchema, module, options2)
let type = getSchemaType(patternSchema, module, options2).trimEnd()
const propertyNames = localizeDependencies(schema, module).propertyNames
content = content
.replace(/\$\{shape\}/g, shape)
.replace(/\$\{parent\.title\}/g, title)
.replace(/\$\{title\}/g, title)
.replace(/\$\{type\}/g, type)
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, '')
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/g, '')
.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/g, options.required ? '' : '$1')
.replace(/\$\{if\.impl.non.optional\}(.*?)\$\{end\.if\.impl.non.optional\}/g, options.required ? '$1' : '')
}
return content
}
const getIndents = level => level ? ' ' : ''
const wrapProp = name => name.match(/[/\.\+]/) ? `"${name}"` : name
const safePropName = name => config.enableStringPropertyKeys ? wrapProp(name) : getSafeKeyName(name)
const insertObjectMacros = (content, schema, module, title, property, options) => {
const options2 = options ? JSON.parse(JSON.stringify(options)) : {}
options2.parent = title
options2.parentLevel = options.parentLevel
options2.level = options.level + 1
options2.templateDir = options.templateDir
;(['properties', 'properties.register', 'properties.assign']).forEach(macro => {
const indent = getIndents(options.parentLevel || (options.level ? 1 : 0))
const templateType = macro.split('.').slice(1).join('')
const template = getTemplate(path.join(options.templateDir, 'property' + (templateType ? `-${templateType}` : ''))).replace(/\n/gms, '\n' + indent)
const properties = []
if (schema.properties) {
Object.entries(schema.properties).forEach(([name, prop], i) => {
let localizedProp = localizeDependencies(prop, module)
const subProperty = getTemplate(path.join(options2.templateDir, 'sub-property/object'))
options2.templateDir += subProperty ? '/sub-property' : ''
const objSeparator = getTemplate(path.join(options2.templateDir, 'object-separator'))
if (localizedProp.type === 'array' || localizedProp.anyOf || localizedProp.oneOf || (typeof localizedProp.const === 'string')) {
options2.property = name
options2.required = schema.required
} else {
options2.property = options.property
options2.required = schema.required && schema.required.includes(name)
}
const schemaShape = indent + getSchemaShape(localizedProp, module, options2).replace(/\n/gms, '\n' + indent)
let type = getSchemaType(localizedProp, module, options2)
if (type === 'object') {
type = getSchemaShape(localizedProp, module, { ...options2, type: true })
}
// don't push properties w/ unsupported types
if (type) {
const description = getSchemaDescription(prop, module)
let replacedTemplate = template
.replace(/(^\s+)/g, '$1'.repeat(options2.level))
.replace(/\$\{property.raw\}/g, name) //Gives the raw RPC propery name, even if it's unsafe
.replace(/\$\{Property.raw\}/g, capitalize(name))
.replace(/\$\{property\}/g, safePropName(name))
.replace(/\$\{Property\}/g, capitalize(safePropName(name)))
.replace(/\$\{parent\.title\}/g, title)
.replace(/\$\{title\}/g, type)
.replace(/\$\{shape\}/g, schemaShape)
.replace(/\$\{description\}/g, description)
.replace(/\$\{if\.summary\}(.*?)\$\{end\.if\.summary\}/gms, description ? '$1' : '')
.replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '')
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/gms, i === schema.properties.length - 1 ? '' : '$1')
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '' : '$1')
.replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '$1' : '')
.replace(/\$\{if\.base\.optional\}(.*?)\$\{end\.if\.base\.optional\}/gms, options.required ? '' : '$1')
.replace(/\$\{if\.non\.object\}(.*?)\$\{end\.if\.non\.object\}/gms, isObject(localizedProp) ? '' : '$1')
.replace(/\$\{if\.non\.array\}(.*?)\$\{end\.if\.non\.array\}/gms, (localizedProp.type === 'array') ? '' : '$1')
.replace(/\$\{if\.non\.anyOf\}(.*?)\$\{end\.if\.non\.anyOf\}/gms, (localizedProp.anyOf || localizedProp.oneOf) ? '' : '$1')
.replace(/\$\{if\.non\.const\}(.*?)\$\{end\.if\.non\.const\}/gms, (typeof localizedProp.const === 'string') ? '' : '$1')
let baseTitle = options.property
if (isObject(localizedProp)) {
replacedTemplate = replacedTemplate
.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '' : '$1')
.replace(/\$\{if\.impl.non.optional\}(.*?)\$\{end\.if\.impl.non.optional\}/gms, ((schema.required && schema.required.includes(name)) || (localizedProp.required && localizedProp.required === true)) ? '$1' : '')
.replace(/\$\{property.dependency\}/g, ((options.level > 0) ? '${property.dependency}${if.impl.optional}.value()${end.if.impl.optional}' : '') + objSeparator + name)
.replace(/\$\{Property.dependency\}/g, ((options.level > 0) ? '${Property.dependency}' : '') + (objSeparator) + capitalize(name))
}
else {
if (options2.level <= 1) {
replacedTemplate = replacedTemplate
.replace(/\$\{property.dependency\}/g, '')
.replace(/\$\{Property.dependency\}/g, '')
.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/gms, '')
}
}
replacedTemplate = replacedTemplate
.replace(/\$\{obj\.separator}/g, objSeparator)
.replace(/\$\{base.title\}/g, (baseTitle ? (baseTitle)[0].toLowerCase() + (baseTitle).substr(1) : '')).trimEnd()
.replace(/\$\{base.Title\}/g, (baseTitle ? (baseTitle)[0].toUpperCase() + (baseTitle).substr(1) : '')).trimEnd()
properties.push(replacedTemplate)
}
})
}
if (schema.propertyNames) {
const { propertyNames } = localizeDependencies(schema, module)
if (propertyNames.enum) {
propertyNames.enum.forEach((prop, i) => {
if (schema.properties && schema.properties[prop]) {
// skip properties that were already defined above
return
}
// TODO: add language config feature for 'unknown' type
let type; // = { type: "null" }
if (schema.additionalProperties && (typeof schema.additionalProperties === 'object')) {
type = schema.additionalProperties
}
if (schema.patternProperties) {
Object.entries(schema.patternProperties).forEach(([pattern, schema]) => {
let regex = new RegExp(pattern)
if (prop.match(regex)) {
type = schema
}
})
}
if (type) {
options2.property = prop
const schemaShape = getSchemaShape(type, module, options2)
const description = getSchemaDescription(prop, module)
properties.push(template
.replace(/\$\{property\}/g, getSafeEnumKeyName(prop))
.replace(/\$\{Property\}/g, capitalize(getSafeEnumKeyName(prop)))
.replace(/\$\{parent\.title\}/g, title)
.replace(/\$\{title\}/g, getSchemaType(type, module, options2))
.replace(/\$\{shape\}/g, schemaShape)
.replace(/\$\{description\}/g, description)
.replace(/\$\{if\.summary\}(.*?)\$\{end\.if\.summary\}/gms, description ? '$1' : '')
.replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '')
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/gms, i === propertyNames.enum.length - 1 ? '' : '$1')
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, schema.required && schema.required.includes(prop) ? '' : '$1')
.replace(/\$\{if\.non.optional\}(.*?)\$\{end\.if\.non.optional\}/gms, schema.required && schema.required.includes(prop) ? '$1' : ''))
}
})
}
}
const regex = new RegExp("\\$\\{" + macro + "\\}", "g")
content = content.replace(regex, properties.join('\n')).replace(/\$\{level}/g, options.parentLevel > 0 ? options.parentLevel : '')
if (!schema.properties && !schema.additionalProperties) {
if (schema.propertyNames && schema.propertyNames.enum) {
content = getTemplate(path.join(options.templateDir, 'enum-empty-property'))
}
else {
content = getTemplate(path.join(options.templateDir, 'object-empty-property'))
}
}
})
return content
}
const insertArrayMacros = (content, schema, module, level = 0, items, required = false) => {
content = content
.replace(/\$\{json\.type\}/g, getSchemaType(schema.items, module, { templateDir: 'json-types', destination: state.destination, section: state.section, code: false, namespace: true }))
.replace(/\$\{items\}/g, items)
.replace(/\$\{items\.with\.indent\}/g, required ? indent(items, ' ') : indent(items, ' '))
.replace(/\$\{if\.impl.array.optional\}(.*?)\$\{end\.if\.impl.array.optional\}/gms, required ? '' : '$1')
.replace(/\$\{if\.impl.array.non.optional\}(.*?)\$\{end\.if\.impl.array.non.optional\}/gms, required ? '$1' : '')
return content
}
const insertTupleMacros = (content, schema, module, title, options) => {
options.level = options.level + 1
options.name = ''
const propTemplate = getTemplate(path.join(options.templateDir, 'property'))
const itemsTemplate = getTemplate(path.join(options.templateDir, 'items'))
const propIndent = (content.split('\n').find(line => line.includes("${properties}")) || '').match(/^\s+/) || [''][0]
const itemsIndent = (content.split('\n').find(line => line.includes("${items}")) || '').match(/^\s+/) || [''][0]
const tupleDelimiter = getTemplate(path.join(options.templateDir, 'tuple-delimiter'))
const doMacroWork = (str, prop, i, indent) => {
const schemaShape = getSchemaShape(prop, module, options)
const description = getSchemaDescription(prop, module)
return (i !== 0 ? indent : '') + str
.replace(/\$\{property\}/g, prop['x-property'])
.replace(/\$\{Property\}/g, capitalize(prop['x-property']))
.replace(/\$\{parent\.title\}/g, title)
.replace(/\$\{shape\}/g, schemaShape)
.replace(/\$\{title\}/g, getSchemaType(prop, module, options))
.replace(/\$\{description\}/g, description)
.replace(/\$\{if\.summary\}(.*?)\$\{end\.if\.summary\}/gms, description ? '$1' : '')
.replace(/\$\{summary\}/g, description ? description.split('\n')[0] : '')
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, i === schema.items.length - 1 ? '' : '$1')
.replace(/\$\{if\.optional\}(.*?)\$\{end\.if\.optional\}/gms, '')
.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/gms, '')
}
content = content.replace(/\$\{properties\}/g, schema.items.map((prop, i) => doMacroWork(propTemplate, prop, i, propIndent)).join(tupleDelimiter))
content = content.replace(/\$\{items\}/g, schema.items.map((prop, i) => doMacroWork(itemsTemplate, prop, i, itemsIndent)).join(tupleDelimiter))
content = content.replace(/\$\{json\.type\}/g, getSchemaType(schema.items[0], module, { templateDir: 'json-types', destination: state.destination, section: state.section, code: false, namespace: true }))
return content
}
const getPrimitiveType = (type, templateDir = 'types', title = false) => {
const template = getTemplate(path.join(templateDir, type)) || getTemplate(path.join(templateDir, 'generic'))
return (title ? template : primitives[type] || template)
}
const pickBestType = types => Array.isArray(types) ? types.find(t => t !== 'null') : types
const insertPrimitiveMacros = (content, schema, module, name, templateDir) => {
content = content.replace(/\$\{type\}/g, getPrimitiveType(pickBestType(schema.type), templateDir))
return content
}
const insertAnyOfMacros = (content, schema, module, name) => {
const itemTemplate = content
if (content.split('\n').find(line => line.includes("${type}"))) {
content = schema.anyOf.map((item, i) => itemTemplate
.replace(/\$\{type\}/g, getSchemaType(item, module))
.replace(/\$\{delimiter\}(.*?)\$\{end.delimiter\}/g, i === schema.anyOf.length - 1 ? '' : '$1')
).join('')
}
return content
}
const sanitize = (schema) => {
const result = JSON.parse(JSON.stringify(schema))
if (result.oneOf && result.oneOf.length === 2 && result.oneOf.find(s => s.const === null)) {
Object.assign(result, result.oneOf.find(s => s.const !== null))
delete result.oneOf
}
if (result.anyOf && result.anyOf.length === 2 && result.anyOf.find(s => s.const === null)) {
Object.assign(result, result.anyOf.find(s => s.const !== null))
delete result.anyOf
}
return result
}
function getSchemaShape(schema = {}, module = {}, { templateDir = 'types', parent = '', property = '', required = false, parentLevel = 0, level = 0, summary, descriptions = true, destination, section, enums = true, skipTitleOnce = false, array = false, primitive = false, type = false } = {}) {
schema = sanitize(schema)
state.destination = destination
state.section = section
if (level === 0 && !schema.title && !primitive) {
return ''
}
const suffix = destination && ('.' + destination.split('.').pop()) || ''
const theTitle = insertSchemaMacros(getTemplate(path.join(templateDir, 'title' + suffix)), schema, module, { name: schema.title, parent, property, required, recursive: false })
let result = !type && getTemplate(path.join(templateDir, 'default' + suffix)) || '${shape}'
let genericTemplate = getTemplate(path.join(templateDir, 'generic' + suffix))
if (enums && level === 0 && Array.isArray(schema.enum) && ((schema.type === "string") || (schema.type[0] === "string"))) {
result = getTemplate(path.join(templateDir, 'enum' + suffix)) || genericTemplate
return insertSchemaMacros(insertEnumMacros(result, schema, module, theTitle, suffix, templateDir), schema, module, { name: theTitle, parent, property, required })
}
if (schema['$ref']) {
const someJson = getPath(schema['$ref'], module)
if (someJson) {
return getSchemaShape(someJson, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums, array, primitive })
}
throw "Unresolvable $ref: " + schema['ref'] + ", in " + module.info.title
}
else if (schema.hasOwnProperty('const')) {
const shape = insertConstMacros(getTemplate(path.join(templateDir, 'const' + suffix)) || genericTemplate, schema, module, theTitle)
return insertSchemaMacros(result.replace(/\$\{shape\}/g, shape), schema, module, { name: theTitle, parent, property, required })
}
else if (!skipTitleOnce && (level > 0) && schema.title) {
let enumType = (schema.type === 'string' && Array.isArray(schema.enum))
// TODO: allow the 'ref' template to actually insert the shape using getSchemaShape
const innerShape = getSchemaShape(schema, module, { skipTitleOnce: true, templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums: enumType, array, primitive })
const shape = getTemplate(path.join(templateDir, 'ref' + suffix))
.replace(/\$\{shape\}/g, innerShape)
result = result.replace(/\$\{shape\}/g, shape)
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required })
}
else if (isObject(schema)) {
let shape
const additionalPropertiesTemplate = getTemplate(path.join(templateDir, 'additionalProperties'))
if (additionalPropertiesTemplate && schema.additionalProperties && (typeof schema.additionalProperties === 'object')) {
shape = insertObjectAdditionalPropertiesMacros(additionalPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace: true, required })
}
else {
const patternPropertiesTemplate = getTemplate(path.join(templateDir, 'patternProperties'))
if (patternPropertiesTemplate && schema.patternProperties) {
shape = insertObjectPatternPropertiesMacros(patternPropertiesTemplate, schema, module, theTitle, { level, parent, templateDir, namespace: true, required })
} else {
let objectLevel = array ? 0 : level
shape = insertObjectMacros(getTemplate(path.join(templateDir, 'object' + (array ? '-array' : '') + suffix)) || genericTemplate, schema, module, theTitle, property, { parentLevel, level: objectLevel, parent, property, required, templateDir, descriptions, destination, section, enums, namespace: true, primitive })
}
}
result = result.replace(/\$\{shape\}/g, shape)
if (level === 0) {
result = result.replace(/\$\{if\.impl.optional\}(.*?)\$\{end\.if\.impl.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '' : '$1')
result = result.replace(/\$\{if\.impl.non.optional\}(.*?)\$\{end\.if\.impl.non.optional\}/gms, (Array.isArray(required) ? required.includes(property) : required) ? '$1' : '')
}
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir })
}
else if (schema.anyOf || schema.oneOf) {
const template = getTemplate(path.join(templateDir, 'anyOfSchemaShape' + suffix))
let shape
if (template) {
shape = insertAnyOfMacros(template, schema, module, theTitle)
}
else {
// borrow anyOf logic, note that schema is a copy, so we're not breaking it.
if (!schema.anyOf) {
schema.anyOf = schema.oneOf
}
shape = insertAnyOfMacros(getTemplate(path.join(templateDir, 'anyOf' + suffix)) || genericTemplate, schema, module, theTitle)
}
if (shape) {
result = result.replace(/\$\{shape\}/g, shape)
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required })
}
else {
return ''
}
}
else if (schema.allOf) {
const merger = (key) => function (a, b) {
if (a.const) {
return JSON.parse(JSON.stringify(a))
}
else if (b.const) {
return JSON.parse(JSON.stringify(b))
}
else {
return deepmerge(a, b, { customMerge: merger })
}
}
let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x).reverse()], {
customMerge: merger
})
if (schema.title) {
union.title = schema.title
}
delete union['$ref']
return getSchemaShape(union, module, { templateDir, parent, property, required, parentLevel, level, summary, descriptions, destination, enums: false, array, primitive })
}
else if (schema.type === "array" && schema.items && isSupportedTuple(schema)) {
// tuple
const shape = insertTupleMacros(getTemplate(path.join(templateDir, 'tuple' + suffix)) || genericTemplate, schema, module, theTitle, { level, templateDir, descriptions, destination, section, enums })
result = result.replace(/\$\{shape\}/g, shape)
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir })
}
else if (schema.type === "array" && schema.items && !Array.isArray(schema.items)) {
// array
const items = getSchemaShape(schema.items, module, { templateDir, parent, property, required, parentLevel: parentLevel + 1, level, summary, descriptions, destination, enums: false, array: true, primitive })
const shape = insertArrayMacros(getTemplate(path.join(templateDir, 'array' + suffix)) || genericTemplate, schema, module, level, items, Array.isArray(required) ? required.includes(property) : required)
result = result.replace(/\$\{shape\}/g, shape)
.replace(/\$\{if\.object\}(.*?)\$\{end\.if\.object\}/gms, isObject(schema.items) ? '$1' : '')
.replace(/\$\{if\.non\.object\}(.*?)\$\{end\.if\.non\.object\}/gms, (schema.items.type !== 'object') ? '$1' : '')
return insertSchemaMacros(result, schema, module, { name: items, parent, property, required, templateDir })
}
else if (schema.type) {
const shape = insertPrimitiveMacros(getTemplate(path.join(templateDir, 'primitive' + suffix)), schema, module, theTitle, templateDir)
result = result.replace(/\$\{shape\}/g, shape)
if (!config.langcode) {
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir })
}
if (level > 0 || primitive) {
return insertSchemaMacros(result, schema, module, { name: theTitle, parent, property, required, templateDir })
}
}
return ''
}
const isHomogenous = schema => {
if (schema.items && Array.isArray(schema.items)) {
// all items have a type and they are all the same
if (schema.items.length === 0) {
return true
}
else if (schema.items.every(item => item.type) && schema.items.every(item => item.type === schema.items[0].type)) {
return true
}
else if (schema.items.every(item => item.$ref) && schema.items.every(item => item.$ref === schema.items[0].$ref)) {
return true
}
else {
return false
}
}
return true
}
const isTuple = schema => schema.items && Array.isArray(schema.items)
const isSupportedTuple = schema => {
if (schema.items && Array.isArray(schema.items)) {
// if the convert flag isn't set, then all tuples are supported
if (!convertTuplesToArraysOrObjects) {
return true
}
else {
// if every item has an `x-property` extension, then this tuple is supported (tuple template can use ${property})
if (schema.items.every(item => item['x-property'])) {
return true
}
// For homogenous tuples just treat them as arrays (i.e. not tuples)
else if (isHomogenous(schema)) {
console.log(`Treating homogenous tuple as array ${schema.items.map(item => item.type||item.$ref).join(' | ')}: ${convertTuplesToArraysOrObjects}`)
return false
}
else {
console.log(`Warning: non-homogenous tuples not supported (schema: ${schema.title})`)
}
}
}
else {
return false
}
}
function getSchemaType(schema, module, { destination, templateDir = 'types', link = false, code = false, asPath = false, event = false, result = false, expandEnums = true, baseUrl = '', namespace = false } = {}) {
const wrap = (str, wrapper) => wrapper + str + wrapper
schema = sanitize(schema)
const suffix = destination && ('.' + destination.split('.').pop()) || ''
const namespaceStr = namespace ? getTemplate(path.join(templateDir, 'namespace' + suffix)) : ''
const theTitle = insertSchemaMacros(namespaceStr + getTemplate(path.join(templateDir, 'title' + suffix)), schema, module, { name: schema.title, parent: getXSchemaGroup(schema, module), recursive: false })
const allocatedProxy = event || result
let title = schema.type === "object" || schema.anyOf || schema.oneOf || Array.isArray(schema.type) && schema.type.includes("object") || schema.enum ? true : false
if (config?.enableUnionTypes === false) {
title = schema.type === "object" || Array.isArray(schema.type) && schema.type.includes("object") || schema.enum ? true : false
}
if (schema['$ref']) {
if (schema['$ref'][0] === '#') {
const refSchema = getPath(schema['$ref'], module)
const includeNamespace = (module.info.title !== getXSchemaGroup(refSchema, module))
return getSchemaType(refSchema, module, {destination, templateDir, link, code, asPath, event, result, expandEnums, baseUrl, namespace:includeNamespace })// { link: link, code: code, destination })
}
else {
// TODO: This never happens... but might be worth keeping in case we link to an opaque external schema at some point?
if (link) {
return '[' + wrap(theTitle, code ? '`' : '') + '](' + schema['$ref'] + ')'
}
else {
return wrap(theTitle, code ? '`' : '')
}
}
}
else if (title && schema.title) {
if (link) {
return '[' + wrap(theTitle, code ? '`' : '') + '](#' + schema.title.toLowerCase() + ')'
}
else {
return wrap(theTitle, code ? '`' : '')
}
}
else if (schema.const) {
return insertConstMacros(getTemplate(path.join(templateDir, 'const' + suffix)), schema, module)
}
else if (schema['x-method']) {
const target = JSON.parse(JSON.stringify(module.methods.find(m => m.name === schema['x-method'].split('.').pop())))
// transform the method copy params to be in the order of the x-additional-params array (and leave out any we don't want)
if (schema['x-additional-params']) {
const params = []
schema['x-additional-params'].forEach(key => {
params.push(target.params.find(p => p.name === key))
})
target.params = params
}
else {
target.params = []
}
const params = getMethodSignatureParams(target, module, { destination })
const template = getTemplate(path.join(templateDir, 'x-method'))
return insertSchemaMacros(template.replace(/\$\{params\}/g, params), target.result.schema, module, { name: theTitle, recursive: false })
}
else if (schema.type === 'string' && schema.enum) {
let type = expandEnums ? schema.enum.map(e => wrap(e, '\'')).join(' | ') : schema.type
if (code) {
type = wrap(type, '`')
}
return type
}
// else if (schema.type === 'array' && Array.isArray(schema.items)) {
// // tuple
// }
else if ((schema.type === 'object' || (schema.type === 'array')) && schema.title) {
const maybeGetPath = (path, json) => {
try {
return getPath(path, json)
}
catch (e) {
return null
}
}
const def = maybeGetPath('#/definitions/' + schema.title, module) || maybeGetPath('#/components/schemas/' + schema.title, module)
if (def && link) {
return '[' + wrap(theTitle, code ? '`' : '') + '](./' + '#' + schema.title.toLowerCase() + ')'
}
else {
return wrap(theTitle, code ? '`' : '')
}
}
else if (schema.type === 'array' && schema.items) {
let firstItem
if (Array.isArray(schema.items)) {
if (!isHomogenous(schema.items)) {
console.log(`Non-homogenous tuples not supported: ${schema.items} in ${module.info.title}, ${theTitle}`)
return ''
}
firstItem = schema.items[0]
// let type = '[' + schema.items.map(x => getSchemaType(x, module, { destination })).join(', ') + ']' // no links, no code
}
let template
// Tuple -> Array
if (convertTuplesToArraysOrObjects && isTuple(schema) && isHomogenous(schema)) {
template = insertArrayMacros(getTemplate(path.join(templateDir, 'array')), schema, module)
template = insertSchemaMacros(template, firstItem, module, { name: getSchemaType(firstItem, module, {destination, templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace }), recursive: false })
}
// Normal Array
else if (!isTuple(schema)) {
const baseDir = (templateDir !== 'json-types' ? 'types': templateDir)
template = insertArrayMacros(getTemplate(path.join(baseDir, 'array')), schema, module)
template = insertSchemaMacros(template, schema.items, module, { name: getSchemaType(schema.items, module, {destination, templateDir, link, title, code, asPath, event, result, expandEnums, baseUrl, namespace })})
}
else {
template = insertTupleMacros(getTemplate(path.join(templateDir, 'tuple')), schema, module, '', { templateDir })
template = insertSchemaMacros(template, firstItem, module, { recursive: false })
}
if (code) {
template = wrap(template, '`')
}
// TODO need to support link: true
return template
}
else if (schema.allOf) {
let union = deepmerge.all([...schema.allOf.map(x => x['$ref'] ? getPath(x['$ref'], module) || x : x)])
if (schema.title) {
union.title = schema.title
}
return getSchemaType(union, module, { templateDir, destination, link, title, code, asPath, baseUrl, namespace })
}
else if (schema.oneOf || schema.anyOf) {
if (!schema.anyOf) {
schema.anyOf = schema.oneOf
}
// todo... we probably shouldn't allow untitled anyOfs, at least not w/out a feature flag
const shape = insertAnyOfMacros(getTemplate(path.join(templateDir, 'anyOf' + suffix)), schema, module, theTitle)
return insertSchemaMacros(shape, schema, module, { name: theTitle, recursive: false })
// if (event) {
// return getSchemaType((schema.oneOf || schema.anyOf)[0], module, { destination, link, title, code, asPath, baseUrl })
// }
// else {
// const newOptions = JSON.parse(JSON.stringify({ destination, link, title, code, asPath, baseUrl }))
// newOptions.code = false
// const result = (schema.oneOf || schema.anyOf).map(s => getSchemaType(s, module, newOptions)).join(' | ')
// return code ? wrap(result, '`') : result
// }
}
else if (schema.type) {
let template = getTemplate(path.join(templateDir, 'additionalProperties'))
if (schema.additionalProperties && template ) {
const result = insertSchemaMacros(getTemplate(path.join(templateDir, 'Title')), schema, module, { name: theTitle, recursive: false })
if (result) {
return result
}
}
template = getTemplate(path.join(templateDir, 'patternProperties'))
if (schema.patternProperties && template ) {
return insertSchemaMacros(getTemplate(path.join(templateDir, 'Title')), schema, module, { name: theTitle, recursive: false })
} else {
// TODO: this assumes that when type is an array of types, that it's one other primative & 'null', which isn't necessarily true.
const schemaType = !Array.isArray(schema.type) ? schema.type : schema.type.find(t => t !== 'null')
const baseDir = (templateDir !== 'json-types' ? 'types': templateDir)
let primitive = getPrimitiveType(schemaType, baseDir, schema.title ? true: false)
primitive = primitive ? primitive.replace(/\$\{title\}/g, schema.title) : primitive
const type = allocatedProxy ? allocatedPrimitiveProxies[schemaType] || primitive : primitive
return wrap(type, code ? '`' : '')
}
}
else {
let type
if (schema.title) {
const baseDir = (templateDir !== 'json-types' ? 'types': templateDir)
type = getPrimitiveType('string', baseDir)
}
const template = type || getTemplate(path.join(templateDir, 'void')) || 'void'
// TODO this is TypeScript specific
return wrap(template, code ? '`' : '')
}
}
function getJsonType(schema, module, { destination, link = false, title = false, code = false, asPath = false, event = false, expandEnums = true, baseUrl = '' } = {}) {
schema = sanitize(schema)
let type
if (schema['$ref']) {
if (schema['$ref'][0] === '#') {
//Ref points to local schema
//Get Path to ref in this module and getSchemaType
let definition = getPath(schema['$ref'], module)
type = getJsonType(definition, schema, {destination})
}
}
else {
type = !Array.isArray(schema.type) ? schema.type : schema.type.find(t => t !== 'null')
}
return type
}
function getSchemaInstantiation(schema, module, { instantiationType }) {
return ''
}
export default {
setTemplates,
setPrimitives,
setConvertTuples,
setAllocatedPrimitiveProxies,
getMethodSignatureParams,
getMethodSignatureResult,
getSchemaShape,
getSchemaType,
getSchemaInstantiation,
getXSchemaGroup
}