@croct/content-model
Version:
A library for modeling, validating and interpolating structured content.
501 lines (500 loc) • 17.1 kB
TypeScript
import { JsonPointer } from '@croct/json-pointer';
/**
* Provides additional information about definitions for clarity and documentation.
*
* Annotations are optional properties that enhance the understandability of a definition.
* They are not used for validation but as documentation elements to improve the definition's
* readability and maintainability.
*/
export type Annotations = {
/**
* A human-readable label for the definition.
*/
title?: string;
/**
* A human-readable description of the definition.
*/
description?: string;
};
/**
* Definition of a single structure attribute.
*/
export type AttributeDefinition = {
/**
* The type of the attribute.
*/
type: ContentDefinition;
/**
* The label of the attribute.
*/
label?: string;
/**
* The description of the attribute.
*/
description?: string;
/**
* Whether the attribute is optional.
*/
optional?: boolean;
/**
* Whether the attribute is private.
*/
private?: boolean;
/**
* The order in which the attribute should be displayed.
*
* Interfaces should use this value to order the attributes as follows:
*
* - Attributes with a lower position value must be displayed before
* attributes with a higher position value
* - If two attributes have the same position value, no specific order
* is guaranteed between them
* - If no position is specified, no specific order is guaranteed.
*
* This property is used for display purposes only and does not affect
* validation or serialization.
*/
position?: number;
};
/**
* Definition of one of the possible choices.
*/
export type ChoiceDefinition = {
/**
* The choice label.
*/
label?: string;
/**
* The choice description.
*/
description?: string;
/**
* Whether the choice is the default one.
*
* Interfaces should use this value to select the default choice as follows:
*
* - If exactly one option is marked as default, that option must be selected by default
* - If no choice is marked as default, then no choice may be selected by default,
* and an explicit choice must be required
*
* Multiple choices marked as default must be rejected as part of the schema validation.
* Since it is possible from a typing perspective, the interface must arbitrarily select
* one of the choices as the default one.
*
* This property is used for display purposes only and does not affect
* validation or serialization.
*/
default?: boolean;
/**
* The order in which the choice should be displayed.
*
* Interfaces should use this value to order the choices as follows:
*
* - Choices with a lower position value must be displayed before
* choices with a higher position value
* - If two choices have the same position value, no specific order
* is guaranteed between them
* - If no position is specified, no specific order is guaranteed.
*
* This property is used for display purposes only and does not affect
* validation or serialization.
*/
position?: number;
};
/**
* The map of attribute types to their definitions.
*/
type ContentDefinitionMap = {
boolean: {
/**
* The labels of the two possible values.
*/
label?: {
/**
* The label for the true value.
*/
true: string;
/**
* The label for the false value.
*/
false: string;
};
/**
* The value to pre-select in the user interface.
*
* Interfaces should use this value to preselect the default value as follows:
*
* - If a default value is specified, that value must be selected by default
* - If no default value is specified, then no value may be selected by default,
* and an explicit value must be required
*
* This property is used for display purposes only and does not affect
* validation or serialization. It does not affect the default value of
* the attribute when absent.
*/
default?: boolean;
};
text: {
/**
* The minimum length (inclusive).
*/
minimumLength?: number;
/**
* The maximum length (inclusive).
*/
maximumLength?: number;
/**
* The predefined format.
*/
format?: string;
/**
* The regular expression specifying the allowed pattern.
*/
pattern?: string;
/**
* The map of choice values to definitions.
*/
choices?: Record<string, ChoiceDefinition>;
};
number: {
/**
* Whether to allow integral numbers only.
*
* JavaScript does not have distinct types for integers and floating-point values.
* Therefore, the presence or absence of a decimal point is not enough to distinguish
* between integers and non-integers. For example, `1` and `1.0` are two ways to represent
* the same value in JSON. Schemas must consider that value an integer no matter which
* representation was used.
*/
integer?: boolean;
/**
* The minimum allowed value (inclusive).
*/
minimum?: number;
/**
* The maximum allowed value (inclusive).
*/
maximum?: number;
};
structure: {
/**
* The map of attribute names to definitions.
*/
attributes: {
[]: AttributeDefinition;
};
};
list: {
/**
* The type of the list items.
*/
items: ContentDefinition;
/**
* A label to prefix items in the user interface.
*
* Interfaces should use this value to prefix items as follows:
*
* - If a label is specified, that label must be used to identify items.
* For example, if the label is `Tag`, the first item must be labeled
* `Tag 1`, the second `Tag 2`, and so on.
* - If the item is a structure, and the structure has a title, that
* title must be used as the prefix. For example, if the structure
* has a title `Card`, the first item must be labeled `Card 1`, the
* second `Card 2`, and so on.
* - If none of the above applies, a generic label, such as `Item`,
* must be used to prefix items.
*
* This property is used for display purposes only and does not affect
* validation or serialization.
*/
itemLabel?: string;
/**
* The minimum number of items (inclusive).
*/
minimumLength?: number;
/**
* The maximum number of items (inclusive).
*/
maximumLength?: number;
};
union: {
/**
* The map of type names to definitions.
*/
types: Record<string, ContentDefinition<'structure' | 'reference'>>;
};
reference: {
/**
* The ID of the referenced structure.
*/
id: string;
/**
* Extra properties to merge with the referenced definition.
*/
properties?: Record<string, any>;
};
};
/**
* The set of all content definition types.
*/
export type ContentDefinitionType = keyof ContentDefinitionMap;
/**
* The map of logical types to their definitions.
*
* This interface can be augmented to add support for custom types.
* For example, to add support for a new type named `color`,
* create a new declaration file with the following content:
*
* @example
* declare module '@croct-tech/content-model/definition' {
* interface LogicalDefinitionMap {
* color: {
* type: 'text',
* format: 'color';
* }
* }
* }
*/
export interface LogicalDefinitionMap {
}
/**
* The set of all logical definition types.
*/
export type LogicalDefinitionType = keyof LogicalDefinitionMap;
/**
* An augmented content definition.
*
* Logical types provide a way to define extensions to the content model
* that add semantic meaning to the content. For example, a logical type
* named `media` can be used to represent a media type, such as an image
* or a video. Consumers of the content model can then use this information
* to provide a better editing experience and feedback, such as a file picker
* for media types or a rich text editor for rich text types.
*
* A logical content type must always be validated against the constraints
* of the underlying content type. Implementations may ignore unknown logical
* types and treat them as regular content types with no extra constraints.
*
* Logical types are defined by augmenting the {@link LogicalDefinitionMap}
* interface.
*
* @typeParam T The logical type name.
*/
export type LogicalDefinition<T extends LogicalDefinitionType = LogicalDefinitionType> = BaseContentDefinition<LogicalDefinitionMap[T]['type']> & BaseLogicalDefinition<T>;
/**
* The base definition of a logical type.
*
* @typeParam T The logical type name.
*/
type BaseLogicalDefinition<T extends LogicalDefinitionType = LogicalDefinitionType> = {
[]: {
logicalType: P;
} & Omit<LogicalDefinitionMap[P], 'type'>;
}[T];
/**
* A template for a content definition.
*
* @typeParam T The logical type name.
*/
export type TemplateDefinition<T extends LogicalDefinitionType = LogicalDefinitionType> = BaseContentDefinition<LogicalDefinitionMap[T]['type']> & {
template: true;
} & Partial<BaseLogicalDefinition<T>>;
/**
* A content definition augmented with logical types.
*
* @typeParam T The type of the content definition.
*/
type DerivedLogicalDefinition<T extends ContentDefinitionType> = {
[]: LogicalDefinitionMap[K]['type'] extends T ? (LogicalDefinition<K> & {
template?: false;
}) | TemplateDefinition<K> : never;
}[LogicalDefinitionType];
/**
* The base definition of a content definition.
*/
type BaseContentDefinition<T extends ContentDefinitionType = ContentDefinitionType> = {
[]: ContentDefinitionMap[K] & Annotations & {
type: K;
};
}[T];
/**
* The definition of a content type.
*/
export type ContentDefinition<T extends ContentDefinitionType = ContentDefinitionType> = (BaseContentDefinition<T> & {
logicalType?: never;
}) | DerivedLogicalDefinition<T>;
/**
* A root definition.
*/
export type RootDefinition = ContentDefinition<'structure' | 'union'>;
/**
* A utility type that excludes content references from a content definition.
*
* This type works recursively on structure and union definitions, meaning that
* it can be used on {@link ContentDefinition}, {@link RootDefinition},
* or any other definition that may contain references.
*
* @typeParam T The content definition type.
*/
export type Resolved<T> = T extends Record<string, unknown> ? Exclude<{
[]: Resolved<T[K]>;
}, ContentDefinition<'reference'> | TemplateDefinition> : T;
/**
* The set of all content definition types excluding references.
*
* This type is the same as {@link ContentDefinitionType} except that it excludes
* the `reference` type.
*/
export type ResolvedDefinitionType = Exclude<ContentDefinitionType, 'reference'>;
/**
* A content definition excluding references in all its nested structures.
*
* This type is the same as {@link ContentDefinition} except that it excludes
* the `reference` type.
*
* @typeParam T The content definition type.
*/
export type ResolvedDefinition<T extends ResolvedDefinitionType = ResolvedDefinitionType> = Resolved<ContentDefinition<T>>;
/**
* A root definition excluding references in all its nested structures.
*
* This type is the same as {@link RootDefinition} except that it excludes
* the `reference` type.
*/
export type ResolvedRootDefinition = ResolvedDefinition<'structure' | 'union'>;
/**
* A self-contained content definition.
*/
export type ContentDefinitionBundle = {
/**
* The root content definition.
*/
root: ContentDefinition;
/**
* The map of referenced content definitions.
*/
definitions: Record<string, ContentDefinition>;
};
export declare namespace ContentDefinition {
/**
* A definition visitor.
*
* Visitor is a generic interface for classes that perform an operation on a
* content definition, usually by traversing it.
*
* @typeParam T The type of the visitor's return value.
*/
interface Visitor<T> {
/**
* Visit a content definition.
*
* @param definition The definition to visit
*
* @returns {T} The visitor's return value
*/
visit(definition: ContentDefinition): T;
}
/**
* A visitor that dispatches the call to specific visit methods.
*
* @typeParam T The type of the visitor's return value.
*/
abstract class Dispatcher<T> implements Visitor<T> {
/**
* Visit a content definition.
*
* @param definition The definition to visit
* @param path The path to the definition.
*
* @returns {T} The visitor's return value
*/
visit(definition: ContentDefinition, path?: JsonPointer): T;
/**
* Visit a boolean definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitBoolean(definition: ContentDefinition<'boolean'>, path: JsonPointer): T;
/**
* Visit a text definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitText(definition: ContentDefinition<'text'>, path: JsonPointer): T;
/**
* Visit a number definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitNumber(definition: ContentDefinition<'number'>, path: JsonPointer): T;
/**
* Visit a structure definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitStructure(definition: ContentDefinition<'structure'>, path: JsonPointer): T;
/**
* Visit a list definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitList(definition: ContentDefinition<'list'>, path: JsonPointer): T;
/**
* Visit a union definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitUnion(definition: ContentDefinition<'union'>, path: JsonPointer): T;
/**
* Visit a reference definition.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The result.
*/
protected abstract visitReference(definition: ContentDefinition<'reference'>, path: JsonPointer): T;
}
/**
* A visitor that dispatches the call to the corresponding method with a default return value.
*
* @typeParam T The type of the visitor's return value.
*/
abstract class PartialVisitor<T> extends Dispatcher<T> {
protected visitBoolean(definition: ContentDefinition<'boolean'>, path: JsonPointer): T;
protected visitText(definition: ContentDefinition<'text'>, path: JsonPointer): T;
protected visitNumber(definition: ContentDefinition<'number'>, path: JsonPointer): T;
protected visitStructure(definition: ContentDefinition<'structure'>, path: JsonPointer): T;
protected visitList(definition: ContentDefinition<'list'>, path: JsonPointer): T;
protected visitUnion(definition: ContentDefinition<'union'>, path: JsonPointer): T;
protected visitReference(definition: ContentDefinition<'reference'>, path: JsonPointer): T;
/**
* The default return value for non-overridden methods.
*
* @param definition The definition to visit.
* @param path The path to the definition.
*
* @returns {T} The default result.
*/
protected abstract visitDefinition(definition: ContentDefinition, path: JsonPointer): T;
}
}
export {};