@json-layout/core
Version:
Compilation and state management utilities for JSON Layout.
587 lines (559 loc) • 22.4 kB
JavaScript
// import Debug from 'debug'
import { isSwitchStruct, isGetItemsExpression, isGetItemsFetch, isItemsLayout, isCompositeLayout, childIsCompositeCompObject, isListLayout } from '@json-layout/vocabulary'
import { normalizeLayoutFragment, mergeNullableSubSchema, getSchemaFragmentType } from '@json-layout/vocabulary/normalize'
import { makeSkeletonTree } from './skeleton-tree.js'
import { partialResolveRefs } from './utils/resolve-refs.js'
/**
* @param {any} rawSchema
* @param {string} sourceSchemaId
* @param {import('./index.js').CompileOptions} options
* @param {(schemaId: string, ref: string) => [any, string, string]} getJSONRef
* @param {Record<string, import('./types.js').SkeletonTree>} skeletonTrees
* @param {Record<string, import('./types.js').SkeletonNode>} skeletonNodes
* @param {string[]} validatePointers
* @param {Record<string, string[]>} validationErrors
* @param {Record<string, import('@json-layout/vocabulary').NormalizedLayout>} normalizedLayouts
* @param {import('@json-layout/vocabulary').Expression[]} expressions
* @param {string | number} key
* @param {string} pointer
* @param {boolean} required
* @param {string} [condition]
* @param {boolean} [dependent]
* @param {string} [knownType]
* @returns {import('./types.js').SkeletonNode}
*/
export function makeSkeletonNode (
rawSchema,
sourceSchemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
key,
pointer,
required,
condition,
dependent,
knownType
) {
let schemaId = sourceSchemaId
let schema = rawSchema
let refPointer = pointer
let refFragment
// improve on ajv error messages based on ajv-errors (https://ajv.js.org/packages/ajv-errors.html)
let errorMessage = rawSchema.errorMessage
rawSchema.__pointer = pointer
if (schema.$ref) {
[refFragment, schemaId, refPointer] = getJSONRef(sourceSchemaId, schema.$ref)
refFragment.__pointer = refPointer
schema = { ...rawSchema, ...refFragment }
if (errorMessage && refFragment.errorMessage) {
// console.warn('errorMessage should not be defined both on ref source and target', pointer, errorMessage, refPointer, refFragment.errorMessage)
// throw new Error('errorMessage cannot be defined both on ref source and target')
}
errorMessage = refFragment.errorMessage = refFragment.errorMessage ?? errorMessage ?? {}
delete schema.$ref
}
errorMessage = rawSchema.errorMessage = errorMessage ?? {}
const nullableType = mergeNullableSubSchema(schema)
if (nullableType) {
schema = nullableType
if (pointer === refPointer) pointer = schema.__pointer
refPointer = schema.__pointer
}
const refPointerPrefix = (refPointer === schema.$id) ? refPointer += '#' : refPointer
const resolvedSchema = partialResolveRefs(schema, schemaId, getJSONRef)
let { type, nullable } = getSchemaFragmentType(resolvedSchema)
if (knownType) type = knownType
if (nullableType) nullable = true
if (!normalizedLayouts[pointer]) {
const normalizationResult = normalizeLayoutFragment(
key,
/** @type {import('@json-layout/vocabulary').SchemaFragment} */(resolvedSchema),
pointer,
options,
undefined,
type,
nullable
)
normalizedLayouts[pointer] = normalizationResult.layout
if (normalizationResult.errors.length) {
validationErrors[pointer.replace('_jl#', '/')] = normalizationResult.errors
}
}
const normalizedLayout = normalizedLayouts[pointer]
let pure = !dependent
/**
* @param {import('@json-layout/vocabulary').Expression[]} expressions
* @param {import('@json-layout/vocabulary').Expression} expression
*/
const pushExpression = (expressions, expression) => {
if (!expression.pure) pure = false
const index = expressions.findIndex(e => e.type === expression.type && e.expr === expression.expr)
if (index !== -1) {
expression.ref = index
} else {
expression.ref = expressions.length
expressions.push(expression)
}
}
/**
* @param {import('@json-layout/vocabulary').Child} child
*/
const prepareLayoutChild = (child) => {
if (child.if) pushExpression(expressions, child.if)
if (childIsCompositeCompObject(child)) {
for (const grandChild of child.children) prepareLayoutChild(grandChild)
}
}
const compObjects = isSwitchStruct(normalizedLayout) ? normalizedLayout.switch : [normalizedLayout]
for (const compObject of compObjects) {
const component = options.components[compObject.comp]
if (!component) throw new Error(`Component "${compObject.comp}" not found`)
if (compObject.if) pushExpression(expressions, compObject.if)
if (isCompositeLayout(compObject, options.components)) {
for (const child of compObject.children) prepareLayoutChild(child)
}
if (schema.const !== undefined && compObject.constData === undefined) compObject.constData = schema.const
if (compObject.constData !== undefined && !compObject.getConstData) compObject.getConstData = { type: 'js-eval', expr: 'layout.constData', pure: true, dataAlias: 'value' }
if (compObject.getConstData) pushExpression(expressions, compObject.getConstData)
let defaultData
if ('default' in schema && (options.useDefault === 'data' || options.useDefault === true || required)) defaultData = schema.default
else if (required) {
if (nullable) defaultData = null
else if (type === 'object' && isCompositeLayout(compObject, options.components)) defaultData = {}
else if (type === 'array') defaultData = []
else if (type === 'boolean') defaultData = false
}
if (defaultData !== undefined && compObject.defaultData === undefined) compObject.defaultData = defaultData
if (compObject.defaultData !== undefined && !compObject.getDefaultData) compObject.getDefaultData = { type: 'js-eval', expr: 'layout.defaultData', pure: true, dataAlias: 'value' }
if (compObject.getDefaultData) pushExpression(expressions, compObject.getDefaultData)
if (compObject.options !== undefined && !compObject.getOptions) compObject.getOptions = { type: 'js-eval', expr: 'layout.options', pure: true, dataAlias: 'value' }
if (compObject.getOptions) pushExpression(expressions, compObject.getOptions)
if (compObject.props !== undefined && !compObject.getProps) compObject.getProps = { type: 'js-eval', expr: 'layout.props', pure: true, dataAlias: 'value' }
if (compObject.getProps) pushExpression(expressions, compObject.getProps)
if (compObject.transformData) pushExpression(expressions, compObject.transformData)
if (isListLayout(compObject)) {
if (compObject.itemTitle) pushExpression(expressions, compObject.itemTitle)
if (compObject.itemSubtitle) pushExpression(expressions, compObject.itemSubtitle)
if (compObject.itemCopy) pushExpression(expressions, compObject.itemCopy)
}
if (isItemsLayout(compObject, options.components) && compObject.getItems) {
if (isGetItemsExpression(compObject.getItems)) pushExpression(expressions, compObject.getItems)
if (isGetItemsFetch(compObject.getItems)) {
pushExpression(expressions, compObject.getItems.url)
if (compObject.getItems.searchParams) {
for (const expr of Object.values(compObject.getItems.searchParams)) {
pushExpression(expressions, expr)
}
}
if (compObject.getItems.headers) {
for (const expr of Object.values(compObject.getItems.headers)) {
pushExpression(expressions, expr)
}
}
}
if (compObject.getItems.itemHeader) pushExpression(expressions, compObject.getItems.itemHeader)
if (compObject.getItems.itemTitle) pushExpression(expressions, compObject.getItems.itemTitle)
if (compObject.getItems.itemKey) pushExpression(expressions, compObject.getItems.itemKey)
if (compObject.getItems.itemValue) pushExpression(expressions, compObject.getItems.itemValue)
if (compObject.getItems.itemIcon) pushExpression(expressions, compObject.getItems.itemIcon)
if (compObject.getItems.itemsResults) pushExpression(expressions, compObject.getItems.itemsResults)
}
}
/** @type {import('./types.js').SkeletonNode} */
const node = {
title: schema.title,
key: key ?? '',
pointer,
refPointer,
pure,
propertyKeys: [],
roPropertyKeys: [],
nullable,
required: required && !nullable
}
if (condition) {
if (isSwitchStruct(normalizedLayout)) throw new Error('Switch struct not allowed in conditional schema')
node.condition = { type: 'js-eval', expr: condition, pure: true, dataAlias: 'value' }
pushExpression(expressions, node.condition)
}
if (schema.oneOf) {
errorMessage.oneOf = errorMessage.oneOf ?? options.messages.errorOneOf
}
if (type === 'object') {
if (schema.properties) {
node.children = node.children ?? []
for (const propertyKey of Object.keys(schema.properties)) {
node.propertyKeys.push(propertyKey)
if (schema.properties[propertyKey].readOnly) node.roPropertyKeys.push(propertyKey)
const dependent = schema.dependentRequired && Object.values(schema.dependentRequired).some(dependentProperties => dependentProperties.includes(propertyKey))
const childPointer = `${refPointerPrefix}/properties/${propertyKey}`
if (!skeletonNodes[childPointer]) {
// @ts-ignore
skeletonNodes[childPointer] = 'recursing'
skeletonNodes[childPointer] = makeSkeletonNode(
schema.properties[propertyKey],
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
propertyKey,
childPointer,
schema.required?.includes(propertyKey),
undefined,
dependent
)
}
node.children.push(childPointer)
if (schema.dependentSchemas?.[propertyKey] || (schema.dependencies?.[propertyKey] && !Array.isArray(schema.dependencies[propertyKey]))) {
const dependentSchema = schema.dependentSchemas?.[propertyKey] ?? schema.dependencies[propertyKey]
const dependentPointer = schema.dependentSchemas?.[propertyKey] ? `${refPointerPrefix}/dependentSchemas/${propertyKey}` : `${refPointerPrefix}/dependencies/${propertyKey}`
if (!skeletonNodes[dependentPointer]) {
// @ts-ignore
skeletonNodes[dependentPointer] = 'recursing'
skeletonNodes[dependentPointer] = makeSkeletonNode(
dependentSchema,
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
`$deps-${propertyKey}`,
dependentPointer,
false,
`data["${propertyKey}"] !== undefined`,
undefined,
'object'
)
}
node.propertyKeys.push('$' + dependentPointer)
node.roPropertyKeys.push('$' + dependentPointer)
node.children.push(dependentPointer)
}
}
}
if (schema.allOf) {
for (let i = 0; i < schema.allOf.length; i++) {
const childPointer = `${refPointerPrefix}/allOf/${i}`
if (!skeletonNodes[childPointer]) {
// @ts-ignore
skeletonNodes[childPointer] = 'recursing'
skeletonNodes[childPointer] = makeSkeletonNode(
schema.allOf[i],
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
`$allOf-${i}`,
childPointer,
false,
undefined,
undefined,
'object'
)
}
node.propertyKeys.push('$' + childPointer)
node.roPropertyKeys.push('$' + childPointer)
node.children = node.children ?? []
node.children.push(childPointer)
}
}
if (schema.oneOf) {
/** @type {string | undefined} */
let discriminator
if (schema.discriminator?.propertyName) discriminator = schema.discriminator?.propertyName
const oneOfPointer = `${refPointerPrefix}/oneOf`
if (!normalizedLayouts[oneOfPointer]) {
const normalizationResult = normalizeLayoutFragment(
'',
schema,
oneOfPointer,
options,
'oneOf',
type,
nullable
)
const compObjects = isSwitchStruct(normalizationResult.layout) ? normalizationResult.layout.switch : [normalizationResult.layout]
for (const compObject of compObjects) {
let defaultData
if ('default' in schema && (options.useDefault === 'data' || options.useDefault === true || required)) defaultData = schema.default
else defaultData = nullable ? null : {}
if (compObject.defaultData === undefined) compObject.defaultData = defaultData
if (compObject.defaultData !== undefined && !compObject.getDefaultData) compObject.getDefaultData = { type: 'js-eval', expr: 'layout.defaultData', pure: true, dataAlias: 'value' }
if (compObject.getDefaultData) pushExpression(expressions, compObject.getDefaultData)
}
normalizedLayouts[oneOfPointer] = normalizationResult.layout
if (normalizationResult.errors.length) {
validationErrors[oneOfPointer.replace('_jl#', '/')] = normalizationResult.errors
}
}
/** @type {string[]} */
const childrenTrees = []
/** @type {string[]} */
const propertyKeys = []
/** @type {string[]} */
const roPropertyKeys = []
for (let i = 0; i < schema.oneOf.length; i++) {
if (!schema.oneOf[i].type) schema.oneOf[i].type = type
const childTreePointer = `${oneOfPointer}/${i}`
if (!skeletonTrees[childTreePointer]) {
// @ts-ignore
skeletonTrees[childTreePointer] = 'recursing'
skeletonTrees[childTreePointer] = makeSkeletonTree(
schema.oneOf[i],
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
childTreePointer,
`option ${i + 1}`,
true,
discriminator
)
}
childrenTrees.push(childTreePointer)
propertyKeys.push('$' + childTreePointer)
roPropertyKeys.push('$' + childTreePointer)
}
if (!skeletonNodes[oneOfPointer]) {
skeletonNodes[oneOfPointer] = {
key: '$oneOf',
pointer: oneOfPointer,
refPointer: oneOfPointer,
childrenTrees,
discriminator,
pure: !childrenTrees.some(childTree => !skeletonNodes[skeletonTrees[childTree]?.root]?.pure),
propertyKeys,
roPropertyKeys
}
}
node.propertyKeys = node.propertyKeys.concat(skeletonNodes[oneOfPointer].propertyKeys)
node.roPropertyKeys = node.roPropertyKeys.concat(skeletonNodes[oneOfPointer].roPropertyKeys)
node.children = node.children ?? []
node.children.push(oneOfPointer)
}
if (schema.patternProperties) {
const patternPropertiesPointer = `${pointer}/patternProperties`
if (!normalizedLayouts[patternPropertiesPointer]) {
const normalizationResult = normalizeLayoutFragment(
'',
schema,
patternPropertiesPointer,
options,
'patternProperties',
type,
nullable
)
normalizedLayouts[patternPropertiesPointer] = normalizationResult.layout
if (normalizationResult.errors.length) {
validationErrors[patternPropertiesPointer.replace('_jl#', '/')] = normalizationResult.errors
}
}
/** @type {string[]} */
const childrenTrees = []
for (const pattern of Object.keys(schema.patternProperties)) {
const childTreePointer = `${patternPropertiesPointer}/${pattern}`
if (!skeletonTrees[childTreePointer]) {
// @ts-ignore
skeletonTrees[childTreePointer] = 'recursing'
skeletonTrees[childTreePointer] = makeSkeletonTree(
schema.patternProperties[pattern],
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
childTreePointer,
'pattern ' + pattern
)
const childLayout = normalizedLayouts[skeletonNodes[skeletonTrees[childTreePointer].root].pointer]
if (isSwitchStruct(childLayout)) {
for (const switchCase of childLayout.switch) {
switchCase.nullable = true
}
} else {
childLayout.nullable = true
}
}
childrenTrees.push(childTreePointer)
}
if (!skeletonNodes[patternPropertiesPointer]) {
skeletonNodes[patternPropertiesPointer] = {
key: '$patternProperties',
pointer: patternPropertiesPointer,
refPointer: patternPropertiesPointer,
childrenTrees,
pure: !childrenTrees.some(childTree => !skeletonNodes[skeletonTrees[childTree]?.root]?.pure),
propertyKeys: [],
roPropertyKeys: []
}
}
node.children = node.children ?? []
node.children.push(patternPropertiesPointer)
}
if (schema.if) {
validatePointers.push(`${pointer}/if`)
if (schema.then) {
const childPointer = `${refPointerPrefix}/then`
if (!skeletonNodes[childPointer]) {
// @ts-ignore
skeletonNodes[childPointer] = 'recursing'
skeletonNodes[childPointer] = makeSkeletonNode(
schema.then,
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
'$then',
childPointer,
false,
`validates["${pointer}/if"](data)`,
undefined,
'object'
)
}
node.children = node.children ?? []
node.propertyKeys.push('$' + childPointer)
node.roPropertyKeys.push('$' + childPointer)
node.children.push(childPointer)
}
if (schema.else) {
const childPointer = `${refPointerPrefix}/else`
if (!skeletonNodes[childPointer]) {
// @ts-ignore
skeletonNodes[childPointer] = 'recursing'
skeletonNodes[childPointer] = makeSkeletonNode(
schema.else,
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
'$else',
childPointer,
false,
`!validates["${pointer}/if"](data)`,
undefined,
'object'
)
}
node.children = node.children ?? []
node.propertyKeys.push('$' + childPointer)
node.roPropertyKeys.push('$' + childPointer)
node.children.push(childPointer)
}
}
for (const propertyKey of node.propertyKeys) {
if (schema?.required?.includes(propertyKey)) {
errorMessage.required = rawSchema.errorMessage.required ?? {}
errorMessage.required[propertyKey] = errorMessage.required[propertyKey] ?? options.messages.errorRequired
}
if (schema.dependentRequired && Object.keys(schema.dependentRequired).includes(propertyKey)) {
errorMessage.dependentRequired = errorMessage.dependentRequired ?? options.messages.errorRequired
}
}
}
if (type === 'array' && schema.items) {
if (Array.isArray(schema.items)) {
node.children = node.children ?? []
for (let i = 0; i < schema.items.length; i++) {
/** @type {any} */
const itemSchema = schema.items[i]
const childPointer = `${refPointerPrefix}/items/${i}`
if (!skeletonNodes[childPointer]) {
// @ts-ignore
skeletonNodes[childPointer] = 'recursing'
skeletonNodes[childPointer] = makeSkeletonNode(
itemSchema,
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
i,
childPointer,
true
)
}
node.children.push(childPointer)
}
} else {
const childTreePointer = `${refPointerPrefix}/items`
if (!skeletonTrees[childTreePointer]) {
// @ts-ignore
skeletonTrees[childTreePointer] = 'recursing'
skeletonTrees[childTreePointer] = makeSkeletonTree(
schema.items,
schemaId,
options,
getJSONRef,
skeletonTrees,
skeletonNodes,
validatePointers,
validationErrors,
normalizedLayouts,
expressions,
childTreePointer
)
}
node.childrenTrees = [childTreePointer]
const childLayout = normalizedLayouts[skeletonNodes[skeletonTrees[childTreePointer].root].pointer]
if (isSwitchStruct(childLayout)) {
for (const switchCase of childLayout.switch) {
switchCase.nullable = true
}
} else {
childLayout.nullable = true
}
}
}
for (const childPointer of node.children || []) {
const child = skeletonNodes[childPointer]
if (!child.pure) node.pure = false
}
for (const childTree of node.childrenTrees || []) {
if (!skeletonNodes[skeletonTrees[childTree]?.root]?.pure) node.pure = false
}
return node
}