UNPKG

@croct/content-model

Version:

A library for modeling, validating and interpolating structured content.

1,228 lines 67.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DefinitionJsonSchemaGenerator = void 0; /** * Standard JSON schema features. */ class StandardFeatures { /** * Creates a new instance. * * @param features The map of features to support. */ constructor(features = {}) { this.features = features; } get lenientMode() { return this.features.lenientMode === true; } getTitleSchema(options) { const { docs, maximumTitleLength } = options; return this.features.structure?.title === false ? false : { type: 'string', title: docs.definition.title.$title, description: docs.definition.title.$description, examples: docs.definition.title.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumTitleLength !== undefined ? { maxLength: maximumTitleLength } : {}), }), }; } getDescriptionSchema(options) { const { docs, maximumDescriptionLength } = options; return this.features.structure?.description === false ? false : { type: 'string', title: docs.definition.description.$title, description: docs.definition.description.$description, examples: docs.definition.description.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumDescriptionLength !== undefined ? { maxLength: maximumDescriptionLength } : {}), }), }; } getBooleanLabelSchema(options) { const { docs, maximumBooleanLabelLength } = options; const featureDocs = docs.boolean.label; return this.features.boolean?.label === false ? false : { type: 'object', title: featureDocs.$title, description: featureDocs.$description, properties: { true: { type: 'string', title: featureDocs.true.$title, description: featureDocs.true.$description, examples: featureDocs.true.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumBooleanLabelLength !== undefined ? { maxLength: maximumBooleanLabelLength } : {}), }), }, false: { type: 'string', title: featureDocs.false.$title, description: featureDocs.false.$description, examples: featureDocs.false.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumBooleanLabelLength !== undefined ? { maxLength: maximumBooleanLabelLength } : {}), }), }, }, required: this.lenientMode ? [] : [ 'true', 'false', ], additionalProperties: false, }; } getBooleanDefaultSchema({ docs }) { return this.features.boolean?.default === false ? false : { type: 'boolean', title: docs.boolean.default.$title, description: docs.boolean.default.$description, }; } getNumberIntegerSchema({ docs }) { return this.features.number?.integer === false ? false : { type: 'boolean', title: docs.number.integer.$title, description: docs.number.integer.$description, }; } getNumberMinimumSchema({ docs }) { return this.features.number?.minimum === false ? false : { type: 'number', title: docs.number.minimum.$title, description: docs.number.minimum.$description, ...(this.lenientMode ? {} : { minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER, }), }; } getNumberMaximumSchema({ docs }) { return this.features.number?.maximum === false ? false : { type: 'number', title: docs.number.maximum.$title, description: docs.number.maximum.$description, ...(this.lenientMode ? {} : { minimum: Number.MIN_SAFE_INTEGER, maximum: Number.MAX_SAFE_INTEGER, }), }; } getTextMinimumLengthSchema(props) { const { docs, maximumStringLength } = props; return this.features.text?.minimumLength === false ? false : { type: 'number', title: docs.text.minimumLength.$title, description: docs.text.minimumLength.$description, ...(this.lenientMode ? {} : { minimum: 0, maximum: maximumStringLength ?? Number.MAX_SAFE_INTEGER, }), }; } getTextMaximumLengthSchema(props) { const { docs, maximumStringLength } = props; return this.features.text?.maximumLength === false ? false : { type: 'number', title: docs.text.maximumLength.$title, description: docs.text.maximumLength.$description, ...(this.lenientMode ? {} : { minimum: 0, maximum: maximumStringLength ?? Number.MAX_SAFE_INTEGER, }), }; } getTextPatternSchema({ docs, maximumPatternLength }) { return this.features.text?.pattern === false ? false : { type: 'string', title: docs.text.pattern.$title, description: docs.text.pattern.$description, examples: docs.text.pattern.$examples, ...(this.lenientMode ? {} : { format: 'regex', minLength: 1, ...(maximumPatternLength !== undefined ? { maxLength: maximumPatternLength } : {}), }), }; } getTextFormatSchema({ docs }) { const toggle = this.features.text?.format; const formats = []; if (toggle?.color !== false) { formats.push('color'); } if (toggle?.url !== false) { formats.push('url'); } if (toggle?.multiline !== false) { formats.push('multiline'); } if (formats.length === 0) { return false; } return { type: 'string', title: docs.text.format.$title, description: docs.text.format.$description, ...(this.lenientMode ? {} : { enum: formats }), }; } getTextChoicesSchema(options) { const { docs, maximumChoiceValueLength, maximumChoiceLabelLength, maximumChoiceDescriptionLength, maximumChoices, enforceSingleDefaultChoice = false, } = options; return this.features.text?.choices === false ? false : { type: 'object', title: docs.text.choices.$title, description: docs.text.choices.$description, ...(enforceSingleDefaultChoice && !this.lenientMode ? { exclusiveFlag: 'default' } : {}), additionalProperties: { type: 'object', properties: { label: { type: 'string', title: docs.text.choices.label.$title, description: docs.text.choices.label.$description, examples: docs.text.choices.label.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumChoiceLabelLength !== undefined ? { maxLength: maximumChoiceLabelLength } : {}), }), }, description: { type: 'string', title: docs.text.choices.description.$title, description: docs.text.choices.description.$description, examples: docs.text.choices.description.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumChoiceDescriptionLength !== undefined ? { maxLength: maximumChoiceDescriptionLength } : {}), }), }, default: { type: 'boolean', title: docs.text.choices.default.$title, description: docs.text.choices.default.$description, }, position: { type: 'integer', title: docs.text.choices.position.$title, description: docs.text.choices.position.$description, ...(this.lenientMode ? {} : { minimum: 0 }), }, }, additionalProperties: false, }, ...(this.lenientMode ? {} : { ...(maximumChoiceValueLength !== undefined ? { propertyNames: { type: 'string', maxLength: maximumChoiceValueLength, }, } : {}), minProperties: 1, ...(maximumChoices !== undefined ? { maxProperties: maximumChoices } : {}), }), }; } getListItemLabelSchema(props) { const { docs, maximumItemLabelLength } = props; return this.features.list?.itemLabel === false ? false : { type: 'string', title: docs.list.itemLabel.$title, description: docs.list.itemLabel.$description, examples: docs.list.itemLabel.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumItemLabelLength !== undefined ? { maxLength: maximumItemLabelLength } : {}), }), }; } getListMinimumLengthSchema(options) { const { docs, maximumListLength } = options; return this.features.text?.minimumLength === false ? false : { type: 'number', title: docs.list.minimumLength.$title, description: docs.list.minimumLength.$description, ...(this.lenientMode ? {} : { minimum: 0, maximum: maximumListLength ?? Number.MAX_SAFE_INTEGER, }), }; } getListMaximumLengthSchema(options) { const { docs, maximumListLength } = options; return this.features.text?.maximumLength === false ? false : { type: 'number', title: docs.list.maximumLength.$title, description: docs.list.maximumLength.$description, ...(this.lenientMode ? {} : { minimum: 0, maximum: maximumListLength ?? Number.MAX_SAFE_INTEGER, }), }; } getAttributeLabelSchema(options) { const { docs, maximumAttributeLabelLength } = options; return this.features.structure?.attributes?.label === false ? false : { type: 'string', title: docs.structure.attributes.label.$title, description: docs.structure.attributes.label.$description, examples: docs.structure.attributes.label.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumAttributeLabelLength !== undefined ? { maxLength: maximumAttributeLabelLength } : {}), }), }; } getAttributeDescriptionSchema(options) { const { docs, maximumAttributeDescriptionLength } = options; return this.features.structure?.attributes?.description === false ? false : { type: 'string', title: docs.structure.attributes.description.$title, description: docs.structure.attributes.description.$description, examples: docs.structure.attributes.description.$examples, ...(this.lenientMode ? {} : { minLength: 1, ...(maximumAttributeDescriptionLength !== undefined ? { maxLength: maximumAttributeDescriptionLength } : {}), }), }; } getAttributeOptionalSchema({ docs }) { return this.features.structure?.attributes?.optional === false ? false : { type: 'boolean', title: docs.structure.attributes.optional.$title, description: docs.structure.attributes.optional.$description, }; } getAttributePrivateSchema({ docs }) { return this.features.structure?.attributes?.private === false ? false : { type: 'boolean', title: docs.structure.attributes.private.$title, description: docs.structure.attributes.private.$description, }; } getAttributePositionSchema({ docs }) { return this.features.structure?.attributes?.position === false ? false : { type: 'integer', title: docs.structure.attributes.position.$title, description: docs.structure.attributes.position.$description, ...(this.lenientMode ? {} : { minimum: 0 }), }; } } /** * Standard schema extended with the extra features supported by the Ajv validator. * * This extension validates dependent ranges constraints using the `$data` keyword for `minimum`, * `minimumLength`, and `minimumLength` to ensure that the lower bound is always less than or * equal to the upper bound. */ class AjvFeatures extends StandardFeatures { getNumberMinimumSchema(options) { const schema = super.getNumberMinimumSchema(options); if (schema === false || this.features.lenientMode === true || this.features.number?.maximum === false) { return schema; } return { allOf: [ schema, { type: 'number', maximum: { $data: '1/maximum' }, }, ], }; } getTextMinimumLengthSchema(options) { const schema = super.getTextMinimumLengthSchema(options); if (schema === false || this.features.lenientMode === true || this.features.text?.maximumLength === false) { return schema; } return { allOf: [ schema, { type: 'number', maximum: { $data: '1/maximumLength' }, }, ], }; } getListMinimumLengthSchema(options) { const schema = super.getListMinimumLengthSchema(options); if (schema === false || this.features.lenientMode === true || this.features.list?.maximumLength === false) { return schema; } return { allOf: [ schema, { type: 'number', maximum: { $data: '1/maximumLength' }, }, ], }; } } /** * A JSON schema generator for validating content definitions. */ class DefinitionJsonSchemaGenerator { /** * Construct a new instance. * * @param configuration The configuration for generating the schema. */ constructor(configuration = {}) { const { maximumDepth, references, requiresUnionDiscriminatorPairing, maximumUnionCardinality, } = configuration; if (maximumDepth !== undefined) { if (maximumDepth < 1) { throw new Error('The maximum depth must be greater than or equal to 1.'); } if (references === undefined) { throw new Error('The references must be specified for accurate depth calculation.'); } } if (requiresUnionDiscriminatorPairing === true && references === undefined) { throw new Error('The references must be specified to enforce union discriminator pairing.'); } if (maximumUnionCardinality !== undefined && maximumUnionCardinality < 1) { throw new Error('The maximum union cardinality must be greater than or equal to 1.'); } this.configuration = { ...configuration, docs: configuration.docs ?? DefinitionJsonSchemaGenerator.DEFAULT_DOCS, }; this.features = DefinitionJsonSchemaGenerator.getSchemaFeatures(configuration.extension, configuration.features); } /** * Creates a lenient schema generator. * * This static factory method configures a generator for generating schemas * that validate schemas without strict checking constraints involving empty labels, * descriptions, and other properties. * * @param options The configuration options. */ static lenient(options = {}) { // Do not spread the options because it can be any subtype of options and include // any of the omitted properties. return new DefinitionJsonSchemaGenerator({ features: { ...options.features, lenientMode: true, }, references: options.references, docs: options.docs, extension: options.extension, }); } /** * Returns the features to support. * * @param extension The validator-specific features to support. * @param features The standard features to support. * * @returns The features to support. */ static getSchemaFeatures(extension, features) { switch (extension) { case 'ajv': return new AjvFeatures(features); default: return new StandardFeatures(features); } } /** * Generate the JSON schema for validating content definitions. * * @return The JSON schema in JSON notation. */ generate() { return JSON.stringify(this.generateSchema()); } /** * Generate the JSON schema for validating content definitions. * * @return The JSON schema in object form. */ generateSchema() { const { maximumDepth, primitiveListItemsOnly } = this.configuration; // If the maximum depth is 1, then only empty structure are allowed. const definitions = maximumDepth === 1 ? {} : { boolean: this.createBooleanSchema(), number: this.createNumberSchema(), text: this.createTextSchema(), ...this.createLogicalTypeSchemas(maximumDepth), }; if (primitiveListItemsOnly === true && maximumDepth !== 1) { definitions['list-item'] = this.createSchemaSwitch(definitions); } if (maximumDepth !== undefined) { return { ...this.generateRootSchema(maximumDepth), $defs: { ...definitions, ...this.generateDepthDefinitions(maximumDepth), }, }; } const reference = this.generateReferenceSchema(); const unionReference = (!this.isCheckedReferences // Reference schemas are inlined when validating discriminator pairing || this.configuration.requiresUnionDiscriminatorPairing === true) ? false : this.generateReferenceSchema(undefined, true); const union = this.generateUnionSchema(); return { ...this.generateRootSchema(), unevaluatedProperties: false, $defs: { ...definitions, definition: this.generateDefinitionSchema(), structure: this.generateStructureSchema(), list: this.generateListSchema(), ...(union !== false ? { union: union } : {}), ...(unionReference !== false ? { 'union-reference': unionReference } : {}), ...(reference !== false ? { reference: reference } : {}), }, }; } /** * Generates schemas for validating definitions at different depths. * * @param maximumDepth The maximum depth for generating schemas. * * @return A map of schemas for validating definitions at different depths. */ generateDepthDefinitions(maximumDepth) { const definitions = {}; for (let currentDepth = 1; currentDepth <= maximumDepth; currentDepth++) { const allowedDepth = maximumDepth - (currentDepth - 1); const suffix = `-${allowedDepth}`; if (currentDepth > 1) { definitions[`definition${suffix}`] = this.generateDefinitionSchema(allowedDepth); } definitions[`structure${suffix}`] = this.generateStructureSchema({ maximumDepth: allowedDepth, rootAnnotations: allowedDepth === maximumDepth, }); if (allowedDepth > 1 && allowedDepth < maximumDepth) { definitions[`list${suffix}`] = this.generateListSchema(allowedDepth); } if (currentDepth > 1) { const reference = this.generateReferenceSchema(allowedDepth); if (reference !== false) { definitions[`reference${suffix}`] = reference; } } // Reference schemas are inlined when validating discriminator pairing, // so no need to generate schemas for them if (this.configuration.requiresUnionDiscriminatorPairing !== true) { const unionReference = this.generateReferenceSchema(allowedDepth, true); if (unionReference !== false) { definitions[`union-reference${suffix}`] = unionReference; } } const union = this.generateUnionSchema({ maximumDepth: allowedDepth, rootAnnotations: allowedDepth === maximumDepth, }); if (union !== false) { definitions[`union${suffix}`] = union; } } return definitions; } /** * Generates a schema for validating the root definition. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ generateRootSchema(maximumDepth) { const { rootDefinition, rootAnnotationsOnly = false } = this.configuration; const schemas = {}; if (rootDefinition !== 'union') { if (maximumDepth === undefined && rootAnnotationsOnly) { schemas.structure = this.generateStructureSchema({ rootAnnotations: true }); } else { schemas.structure = maximumDepth !== undefined ? this.getStructureSchema(maximumDepth) : { $ref: '#/$defs/structure' }; } } if (rootDefinition !== 'structure') { const union = this.getUnionSchema(maximumDepth); if (union === false) { schemas.union = this.createUnionSchema({ rootAnnotations: true }); } else { const rootSchema = maximumDepth === undefined && rootAnnotationsOnly ? this.generateUnionSchema({ rootAnnotations: true }) : false; schemas.union = rootSchema !== false ? rootSchema : union; } } if (rootDefinition === 'any') { schemas.text = this.getTextSchema(); schemas.number = this.getNumberSchema(); schemas.boolean = this.getBooleanSchema(); schemas.list = this.getListSchema(maximumDepth); const referenceSchema = this.getReferenceSchema(maximumDepth); if (referenceSchema !== false) { schemas.reference = referenceSchema; } } const branches = Object.values(schemas); return branches.length === 1 ? { type: 'object', ...branches[0] } : this.createSchemaSwitch(schemas); } /** * Returns the schema for validating definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ getDefinitionSchema(maximumDepth) { return { $ref: `#/$defs/definition${maximumDepth !== undefined ? `-${maximumDepth}` : ''}`, }; } /** * Generates a schema for validating definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ generateDefinitionSchema(maximumDepth) { const schemas = { boolean: this.getBooleanSchema(), number: this.getNumberSchema(), text: this.getTextSchema(), structure: this.getStructureSchema(maximumDepth), }; const union = this.getUnionSchema(maximumDepth); if (union !== false) { schemas.union = union; } const reference = this.getReferenceSchema(maximumDepth); if (reference !== false) { schemas.reference = reference; } if (maximumDepth === undefined || maximumDepth >= 2) { schemas.list = this.getListSchema(maximumDepth); } return this.createSchemaSwitch(schemas); } /** * Returns the schema for validating a boolean definition. * * @return The schema for validating a boolean definition. */ getBooleanSchema() { return { $ref: '#/$defs/boolean', }; } /** * Creates the schema for validating a boolean definition. * * @return The schema for validating a boolean definition. */ createBooleanSchema() { const docs = this.configuration.docs.boolean; return { type: 'object', title: docs.$title, description: docs.$description, properties: { type: { type: 'string', title: docs.type.$title, description: docs.type.$description, const: 'boolean', }, ...this.getNonRootAnnotationProperties(), ...DefinitionJsonSchemaGenerator.cleanSchemaProperties({ label: this.features.getBooleanLabelSchema(this.configuration), default: this.features.getBooleanDefaultSchema(this.configuration), }), }, required: ['type'], additionalProperties: false, }; } /** * Returns the schema for validating a number definition. * * @return The schema for validating a number definition. */ getNumberSchema() { return { $ref: '#/$defs/number', }; } /** * Creates the schema for validating a number definition. * * @return The schema for validating a number definition. */ createNumberSchema() { const { docs } = this.configuration; return { type: 'object', title: docs.number.$title, description: docs.number.$description, properties: { type: { type: 'string', title: docs.number.type.$title, description: docs.number.type.$description, const: 'number', }, ...this.getNonRootAnnotationProperties(), ...DefinitionJsonSchemaGenerator.cleanSchemaProperties({ minimum: this.features.getNumberMinimumSchema(this.configuration), maximum: this.features.getNumberMaximumSchema(this.configuration), integer: this.features.getNumberIntegerSchema(this.configuration), }), }, required: ['type'], additionalProperties: false, }; } /** * Returns the schema for validating a text definition. * * @return The schema for validating a text definition. */ getTextSchema() { return { $ref: '#/$defs/text', }; } /** * Creates the schema for validating a text definition. * * @return The schema for validating a text definition. */ createTextSchema() { const { docs, nonConflictingConstraints = false, features: featureFlags } = this.configuration; const features = { minimumLength: this.features.getTextMinimumLengthSchema(this.configuration), maximumLength: this.features.getTextMaximumLengthSchema(this.configuration), format: this.features.getTextFormatSchema(this.configuration), pattern: this.features.getTextPatternSchema(this.configuration), choices: this.features.getTextChoicesSchema(this.configuration), }; const constraints = []; if (nonConflictingConstraints) { const isMultilineFormatEnabled = features.format !== false && featureFlags?.text?.format?.multiline !== false; const multilineConstraint = isMultilineFormatEnabled ? { format: { type: 'string', enum: ['multiline'], }, } : {}; if (features.format !== false) { constraints.push({ properties: { format: features.format }, required: ['format'], }); } if (features.pattern !== false) { constraints.push({ properties: { pattern: features.pattern, ...multilineConstraint, ...(features.minimumLength !== false ? { minimumLength: features.minimumLength } : {}), ...(features.maximumLength !== false ? { maximumLength: features.maximumLength } : {}), }, required: ['pattern'], }); } if (features.choices !== false) { constraints.push({ properties: { choices: features.choices }, required: ['choices'], }); } if (features.minimumLength !== false) { constraints.push({ properties: { minimumLength: features.minimumLength, ...multilineConstraint, }, required: ['minimumLength'], }); } if (features.maximumLength !== false) { constraints.push({ properties: { maximumLength: features.maximumLength, ...multilineConstraint, }, required: ['maximumLength'], }); } if (features.minimumLength !== false && features.maximumLength !== false) { constraints.push({ properties: { minimumLength: features.minimumLength, maximumLength: features.maximumLength, ...multilineConstraint, }, required: ['minimumLength', 'maximumLength'], }); } } else { constraints.push({ properties: DefinitionJsonSchemaGenerator.cleanSchemaProperties(features) }); } const type = { type: { type: 'string', title: docs.text.type.$title, description: docs.text.type.$description, const: 'text', }, }; return { type: 'object', title: docs.text.$title, description: docs.text.$description, ...(constraints.length === 1 ? { properties: { ...type, ...this.getNonRootAnnotationProperties(), ...constraints[0].properties, }, required: ['type'], additionalProperties: false, } : { oneOf: [ { properties: type, required: ['type'], additionalProperties: false, }, ...constraints.map(constraint => ({ properties: { ...type, ...this.getNonRootAnnotationProperties(), ...constraint.properties, }, required: [...new Set(['type', ...(constraint.required ?? [])])], additionalProperties: false, })), ], }), }; } /** * Returns the schema for validating list definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ getListSchema(maximumDepth) { return { $ref: `#/$defs/list${maximumDepth !== undefined ? `-${maximumDepth}` : ''}`, }; } /** * Generates a schema for validating list definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ generateListSchema(maximumDepth) { const { docs } = this.configuration; const features = { itemLabel: this.features.getListItemLabelSchema(this.configuration), minimumLength: this.features.getListMinimumLengthSchema(this.configuration), maximumLength: this.features.getListMaximumLengthSchema(this.configuration), }; return { type: 'object', title: docs.list.$title, description: docs.list.$description, properties: { type: { type: 'string', title: docs.list.type.$title, description: docs.list.type.$description, const: 'list', }, ...this.getNonRootAnnotationProperties(), items: { title: docs.list.items.$title, description: docs.list.items.$description, ...this.getListItemSchema(maximumDepth !== undefined ? maximumDepth - 1 : undefined), }, ...DefinitionJsonSchemaGenerator.cleanSchemaProperties(features), }, required: ['type', 'items'], additionalProperties: false, }; } /** * Returns the schema for validating list items with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ getListItemSchema(maximumDepth) { if (this.configuration.primitiveListItemsOnly !== true) { return this.getDefinitionSchema(maximumDepth); } // The depth is irrelevant for primitive list items return { $ref: '#/$defs/list-item', }; } /** * Returns the schema for validating structure definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ getStructureSchema(maximumDepth) { return { $ref: `#/$defs/structure${maximumDepth !== undefined ? `-${maximumDepth}` : ''}`, }; } /** * Generates a schema for validating structure definitions with at most the given depth. * * @param options The options for generating the schema. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ generateStructureSchema(options = {}) { const { maximumDepth, rootAnnotations = false } = options; const { attributeNamePattern, minimumAttributeNameLength, maximumAttributeNameLength, maximumAttributesPerStructure, nonEmptyStructure = false, docs, } = this.configuration; return { type: 'object', title: docs.structure.$title, description: docs.structure.$description, properties: { type: { type: 'string', title: docs.structure.type.$title, description: docs.structure.type.$description, const: 'structure', }, ...(rootAnnotations ? this.getAnnotationProperties() : this.getNonRootAnnotationProperties()), attributes: { type: 'object', title: docs.structure.attributes.$title, description: docs.structure.attributes.$description, ...(this.features.lenientMode ? {} : { propertyNames: { type: 'string', ...(minimumAttributeNameLength !== undefined ? { minLength: minimumAttributeNameLength } : {}), ...(maximumAttributeNameLength !== undefined ? { maxLength: maximumAttributeNameLength } : {}), ...(attributeNamePattern !== undefined ? { pattern: attributeNamePattern } : {}), }, }), ...(nonEmptyStructure ? { minProperties: 1 } : {}), ...(maximumAttributesPerStructure !== undefined ? { maxProperties: maximumAttributesPerStructure } : {}), additionalProperties: maximumDepth !== undefined && maximumDepth < 2 ? false : { type: 'object', properties: { type: this.getDefinitionSchema(maximumDepth !== undefined ? maximumDepth - 1 : undefined), ...DefinitionJsonSchemaGenerator.cleanSchemaProperties({ label: this.features.getAttributeLabelSchema(this.configuration), description: this.features.getAttributeDescriptionSchema(this.configuration), optional: this.features.getAttributeOptionalSchema(this.configuration), private: this.features.getAttributePrivateSchema(this.configuration), position: this.features.getAttributePositionSchema(this.configuration), }), }, required: ['type'], additionalProperties: false, }, }, }, required: ['type', 'attributes'], additionalProperties: false, }; } /** * Returns the schema for validating union definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ getUnionSchema(maximumDepth) { const references = this.getReferences(maximumDepth, true); if (references.length === 0 && this.configuration.unionReferenceOnly === true && this.configuration.references !== undefined) { return false; } return { $ref: `#/$defs/union${maximumDepth !== undefined ? `-${maximumDepth}` : ''}`, }; } /** * Returns the schema for validating union definitions with at most the given depth. * * @param options The options for generating the schema. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ generateUnionSchema(options = {}) { const { maximumDepth, rootAnnotations = false } = options; const { unionReferenceOnly = false, references: referenceMap } = this.configuration; const references = this.getReferences(maximumDepth, true); if (referenceMap !== undefined && references.length === 0 && unionReferenceOnly) { return false; } const { requiresUnionDiscriminatorPairing = false } = this.configuration; if (requiresUnionDiscriminatorPairing) { return this.createUnionSchema({ types: Object.fromEntries(references.map(id => [ id, unionReferenceOnly ? this.createReferenceSchema([id]) : this.createSchemaSwitch({ structure: this.getStructureSchema(maximumDepth), reference: this.createReferenceSchema([id]), }), ])), additionalTypes: unionReferenceOnly ? false : this.getStructureSchema(maximumDepth), rootAnnotations: rootAnnotations, }); } const referenceSchema = this.getReferenceSchema(maximumDepth, true); if (unionReferenceOnly) { return this.createUnionSchema({ additionalTypes: referenceSchema, rootAnnotations: rootAnnotations, }); } return this.createUnionSchema({ additionalTypes: referenceSchema === false ? this.getStructureSchema(maximumDepth) : this.createSchemaSwitch({ structure: this.getStructureSchema(maximumDepth), reference: referenceSchema, }), rootAnnotations: rootAnnotations, }); } /** * Creates a union schema. * * @param options The options for creating the union schema. * * @return The union schema. */ createUnionSchema(options = {}) { const { types, additionalTypes = false, rootAnnotations = false } = options; const { unionDiscriminatorPattern, maximumUnionCardinality } = this.configuration; const docs = this.configuration.docs.union; return { type: 'object', title: docs.$title, description: docs.$description, properties: { type: { type: 'string', title: docs.type.$title, description: docs.type.$description, const: 'union', }, ...(rootAnnotations ? this.getAnnotationProperties() : this.getNonRootAnnotationProperties()), types: { type: 'object', title: docs.types.$title, description: docs.types.$description, ...(types !== undefined ? { properties: types } : {}), additionalProperties: additionalTypes, ...(this.features.lenientMode ? {} : { minProperties: 2, propertyNames: { type: 'string', minLength: 1, // When types !== undefined && additionalTypes === false, this could be // removed as an optimization. However, keeping it provides a way to ensure // the discriminator matches the required pattern from a certain // point forward (i.e., a bug that allowed invalid identifiers previously) ...(unionDiscriminatorPattern !== undefined ? { pattern: unionDiscriminatorPattern } : {}), }, ...(maximumUnionCardinality !== undefined ? { maxProperties: maximumUnionCardinality } : {}), }), }, }, required: ['type', 'types'], additionalProperties: false, }; } /** * Returns the schema for validating logical definitions with at most the given depth. * * @param maximumDepth The maximum depth that the schema should allow. * * @return If a maximum depth is specified, the schema for validating definitions with at most * the given depth; otherwise, a schema that allows any depth. */ createLogicalTypeSchemas(maximumDepth) { const references = this.getReferences(maximumDe