@croct/content-model
Version:
A library for modeling, validating and interpolating structured content.
1,228 lines • 67.9 kB
JavaScript
"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