Rename folders

This commit is contained in:
Christopher Schleiden
2023-02-22 15:52:40 -08:00
parent 16cc4d9bda
commit 2a3d63551f
469 changed files with 0 additions and 0 deletions
@@ -0,0 +1,44 @@
import {TemplateSchema} from ".";
import {DEFINITION, BOOLEAN} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {TokenType} from "../tokens/types";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
export class BooleanDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case BOOLEAN: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${BOOLEAN}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${BOOLEAN} key`);
switch (mappingKey.value) {
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${BOOLEAN} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.Boolean;
}
public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Boolean;
}
public override validate(schema: TemplateSchema, name: string): void {}
}
@@ -0,0 +1,57 @@
import {TemplateSchema} from ".";
import {Definition} from "./definition";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
export class DefinitionInfo {
private readonly _schema: TemplateSchema;
public readonly isDefinitionInfo = true;
public readonly definition: Definition;
public readonly allowedContext: string[];
public constructor(schema: TemplateSchema, name: string);
public constructor(parent: DefinitionInfo, name: string);
public constructor(parent: DefinitionInfo, definition: Definition);
public constructor(schemaOrParent: TemplateSchema | DefinitionInfo, nameOrDefinition: string | Definition) {
const parent: DefinitionInfo | undefined =
(schemaOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true
? (schemaOrParent as DefinitionInfo)
: undefined;
this._schema = parent === undefined ? (schemaOrParent as TemplateSchema) : parent._schema;
// Lookup the definition if a key was passed in
this.definition =
typeof nameOrDefinition === "string" ? this._schema.getDefinition(nameOrDefinition) : nameOrDefinition;
// Record allowed context
if (this.definition.readerContext.length > 0) {
this.allowedContext = [];
// Copy parent allowed context
const upperSeen: {[upper: string]: boolean} = {};
for (const context of parent?.allowedContext ?? []) {
this.allowedContext.push(context);
upperSeen[context.toUpperCase()] = true;
}
// Append context if unseen
for (const context of this.definition.readerContext) {
const upper = context.toUpperCase();
if (!upperSeen[upper]) {
this.allowedContext.push(context);
upperSeen[upper] = true;
}
}
} else {
this.allowedContext = parent?.allowedContext ?? [];
}
}
public getScalarDefinitions(): ScalarDefinition[] {
return this._schema.getScalarDefinitions(this.definition);
}
public getDefinitionsOfType(type: DefinitionType): Definition[] {
return this._schema.getDefinitionsOfType(this.definition, type);
}
}
@@ -0,0 +1,10 @@
export enum DefinitionType {
Null,
Boolean,
Number,
String,
Sequence,
Mapping,
OneOf,
AllowedValues
}
@@ -0,0 +1,78 @@
import {CONTEXT, DEFINITION, DESCRIPTION} from "../template-constants";
import {MappingToken} from "../tokens";
import {DefinitionType} from "./definition-type";
import {TemplateSchema} from "./template-schema";
/**
* Defines the allowable schema for a user defined type
*/
export abstract class Definition {
/**
* Used by the template reader to determine allowed expression values and functions.
* Also used by the template reader to validate function min/max parameters.
*/
public readonly readerContext: string[] = [];
/**
* Used by the template evaluator to determine allowed expression values and functions.
* The min/max parameter info is omitted.
*/
public readonly evaluatorContext: string[] = [];
// A key to uniquely identify a definition
public readonly key: string;
public readonly description: string | undefined;
public constructor(key: string, definition?: MappingToken) {
this.key = key;
if (definition) {
for (let i = 0; i < definition.count; ) {
const definitionKey = definition.get(i).key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case CONTEXT: {
const context = definition.get(i).value.assertSequence(`${DEFINITION} ${CONTEXT}`);
definition.remove(i);
const seenReaderContext: {[key: string]: boolean} = {};
const seenEvaluatorContext: {[key: string]: boolean} = {};
for (const item of context) {
const itemStr = item.assertString(`${CONTEXT} item`).value;
const upperItemStr = itemStr.toUpperCase();
if (seenReaderContext[upperItemStr]) {
throw new Error(`Duplicate context item '${itemStr}'`);
}
seenReaderContext[upperItemStr] = true;
this.readerContext.push(itemStr);
// Remove min/max parameter info
const paramIndex = itemStr.indexOf("(");
const modifiedItemStr = paramIndex > 0 ? itemStr.substr(0, paramIndex + 1) + ")" : itemStr;
const upperModifiedItemStr = modifiedItemStr.toUpperCase();
if (seenEvaluatorContext[upperModifiedItemStr]) {
throw new Error(`Duplicate context item '${modifiedItemStr}'`);
}
seenEvaluatorContext[upperModifiedItemStr] = true;
this.evaluatorContext.push(modifiedItemStr);
}
break;
}
case DESCRIPTION: {
const value = definition.get(i).value;
this.description = value.assertString(DESCRIPTION).value;
definition.remove(i);
break;
}
default: {
i++;
break;
}
}
}
}
}
public abstract get definitionType(): DefinitionType;
public abstract validate(schema: TemplateSchema, name: string): void;
}
@@ -0,0 +1 @@
export {TemplateSchema} from "./template-schema";
@@ -0,0 +1,90 @@
import {TemplateSchema} from "./template-schema";
import {DEFINITION, MAPPING, PROPERTIES, LOOSE_KEY_TYPE, LOOSE_VALUE_TYPE} from "../template-constants";
import {MappingToken} from "../tokens";
import {Definition} from "./definition";
import {DefinitionType} from "./definition-type";
import {PropertyDefinition} from "./property-definition";
export class MappingDefinition extends Definition {
public readonly properties: {[key: string]: PropertyDefinition} = {};
public looseKeyType = "";
public looseValueType = "";
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case MAPPING: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${MAPPING}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${MAPPING} key`);
switch (mappingKey.value) {
case PROPERTIES: {
const properties = mappingPair.value.assertMapping(`${DEFINITION} ${MAPPING} ${PROPERTIES}`);
for (const propertiesPair of properties) {
const propertyName = propertiesPair.key.assertString(`${DEFINITION} ${MAPPING} ${PROPERTIES} key`);
this.properties[propertyName.value] = new PropertyDefinition(propertiesPair.value);
}
break;
}
case LOOSE_KEY_TYPE: {
const looseKeyType = mappingPair.value.assertString(`${DEFINITION} ${MAPPING} ${LOOSE_KEY_TYPE}`);
this.looseKeyType = looseKeyType.value;
break;
}
case LOOSE_VALUE_TYPE: {
const looseValueType = mappingPair.value.assertString(`${DEFINITION} ${MAPPING} ${LOOSE_VALUE_TYPE}`);
this.looseValueType = looseValueType.value;
break;
}
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${MAPPING} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.Mapping;
}
public override validate(schema: TemplateSchema, name: string): void {
// Lookup loose key type
if (this.looseKeyType) {
schema.getDefinition(this.looseKeyType);
// Lookup loose value type
if (this.looseValueType) {
schema.getDefinition(this.looseValueType);
} else {
throw new Error(
`Property '${LOOSE_KEY_TYPE}' is defined but '${LOOSE_VALUE_TYPE}' is not defined on '${name}'`
);
}
}
// Otherwise validate loose value type not be defined
else if (this.looseValueType) {
throw new Error(`Property '${LOOSE_VALUE_TYPE}' is defined but '${LOOSE_KEY_TYPE}' is not defined on '${name}'`);
}
// Lookup each property
for (const propertyName of Object.keys(this.properties)) {
const propertyDef = this.properties[propertyName];
if (!propertyDef.type) {
throw new Error(`Type not specified for the property '${propertyName}' on '${name}'`);
}
schema.getDefinition(propertyDef.type);
}
}
}
@@ -0,0 +1,44 @@
import {TemplateSchema} from "./template-schema";
import {DEFINITION, NULL} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
import {TokenType} from "../tokens/types";
export class NullDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case NULL: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${NULL}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${NULL} key`);
switch (mappingKey.value) {
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${NULL} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.Null;
}
public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Null;
}
public override validate(schema: TemplateSchema, name: string): void {}
}
@@ -0,0 +1,44 @@
import {TemplateSchema} from "./template-schema";
import {DEFINITION, NUMBER} from "../template-constants";
import {MappingToken, LiteralToken} from "../tokens";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
import {TokenType} from "../tokens/types";
export class NumberDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case NUMBER: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${NUMBER}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${NUMBER} key`);
switch (mappingKey.value) {
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${NUMBER} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.Number;
}
public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Number;
}
public override validate(schema: TemplateSchema, name: string): void {}
}
@@ -0,0 +1,193 @@
import {TemplateSchema} from "./template-schema";
import {
DEFINITION,
ONE_OF,
SEQUENCE,
NULL,
BOOLEAN,
NUMBER,
SCALAR,
CONSTANT,
LOOSE_KEY_TYPE,
ALLOWED_VALUES
} from "../template-constants";
import {MappingToken} from "../tokens";
import {BooleanDefinition} from "./boolean-definition";
import {Definition} from "./definition";
import {DefinitionType} from "./definition-type";
import {MappingDefinition} from "./mapping-definition";
import {NullDefinition} from "./null-definition";
import {NumberDefinition} from "./number-definition";
import {SequenceDefinition} from "./sequence-definition";
import {StringDefinition} from "./string-definition";
import {PropertyDefinition} from "./property-definition";
/**
* Must resolve to exactly one of the referenced definitions
*/
export class OneOfDefinition extends Definition {
public readonly oneOf: string[] = [];
public readonly oneOfPrefix: string[] = [];
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case ONE_OF: {
const oneOf = definitionPair.value.assertSequence(`${DEFINITION} ${ONE_OF}`);
for (const item of oneOf) {
const oneOfItem = item.assertString(`${DEFINITION} ${ONE_OF} item`);
this.oneOf.push(oneOfItem.value);
}
break;
}
case ALLOWED_VALUES: {
const oneOf = definitionPair.value.assertSequence(`${DEFINITION} ${ALLOWED_VALUES}`);
for (const item of oneOf) {
const oneOfItem = item.assertString(`${DEFINITION} ${ONE_OF} item`);
this.oneOf.push(this.key + "-" + oneOfItem.value);
}
break;
}
default:
// throws
definitionKey.assertUnexpectedValue(`${DEFINITION} key`);
break;
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.OneOf;
}
public override validate(schema: TemplateSchema, name: string): void {
if (this.oneOf.length === 0) {
throw new Error(`'${name}' does not contain any references`);
}
let foundLooseKeyType = false;
const mappingDefinitions: MappingDefinition[] = [];
let allowedValuesDefinition: OneOfDefinition | undefined;
let sequenceDefinition: SequenceDefinition | undefined;
let nullDefinition: NullDefinition | undefined;
let booleanDefinition: BooleanDefinition | undefined;
let numberDefinition: NumberDefinition | undefined;
const stringDefinitions: StringDefinition[] = [];
const seenNestedTypes: {[key: string]: boolean} = {};
for (const nestedType of this.oneOf) {
if (seenNestedTypes[nestedType]) {
throw new Error(`'${name}' contains duplicate nested type '${nestedType}'`);
}
seenNestedTypes[nestedType] = true;
const nestedDefinition = schema.getDefinition(nestedType);
if (nestedDefinition.readerContext.length > 0) {
throw new Error(
`'${name}' is a one-of definition and references another definition that defines context. This is currently not supported.`
);
}
switch (nestedDefinition.definitionType) {
case DefinitionType.Mapping: {
const mappingDefinition = nestedDefinition as MappingDefinition;
mappingDefinitions.push(mappingDefinition);
if (mappingDefinition.looseKeyType) {
foundLooseKeyType = true;
}
break;
}
case DefinitionType.Sequence: {
// Multiple sequence definitions not allowed
if (sequenceDefinition) {
throw new Error(`'${name}' refers to more than one definition of type '${SEQUENCE}'`);
}
sequenceDefinition = nestedDefinition as SequenceDefinition;
break;
}
case DefinitionType.Null: {
// Multiple null definitions not allowed
if (nullDefinition) {
throw new Error(`'${name}' refers to more than one definition of type '${NULL}'`);
}
nullDefinition = nestedDefinition as NullDefinition;
break;
}
case DefinitionType.Boolean: {
// Multiple boolean definitions not allowed
if (booleanDefinition) {
throw new Error(`'${name}' refers to more than one definition of type '${BOOLEAN}'`);
}
booleanDefinition = nestedDefinition as BooleanDefinition;
break;
}
case DefinitionType.Number: {
// Multiple number definitions not allowed
if (numberDefinition) {
throw new Error(`'${name}' refers to more than one definition of type '${NUMBER}'`);
}
numberDefinition = nestedDefinition as NumberDefinition;
break;
}
case DefinitionType.String: {
const stringDefinition = nestedDefinition as StringDefinition;
// Multiple string definitions
if (stringDefinitions.length > 0 && (!stringDefinitions[0].constant || !stringDefinition.constant)) {
throw new Error(`'${name}' refers to more than one '${SCALAR}', but some do not set '${CONSTANT}'`);
}
stringDefinitions.push(stringDefinition);
break;
}
case DefinitionType.OneOf: {
// Multiple allowed-values definitions not allowed
if (allowedValuesDefinition) {
throw new Error(`'${name}' contains multiple allowed-values definitions`);
}
allowedValuesDefinition = nestedDefinition as OneOfDefinition;
break;
}
default:
throw new Error(`'${name}' refers to a definition with type '${nestedDefinition.definitionType}'`);
}
}
if (mappingDefinitions.length > 1) {
if (foundLooseKeyType) {
throw new Error(
`'${name}' refers to two mappings and at least one sets '${LOOSE_KEY_TYPE}'. This is not currently supported.`
);
}
const seenProperties: {[key: string]: PropertyDefinition} = {};
for (const mappingDefinition of mappingDefinitions) {
for (const propertyName of Object.keys(mappingDefinition.properties)) {
const newPropertyDef = mappingDefinition.properties[propertyName];
// Already seen
const existingPropertyDef: PropertyDefinition | undefined = seenProperties[propertyName];
if (existingPropertyDef) {
// Types match
if (existingPropertyDef.type === newPropertyDef.type) {
continue;
}
// Collision
throw new Error(
`'${name}' contains two mappings with the same property, but each refers to a different type. All matching properties must refer to the same type.`
);
}
// New
else {
seenProperties[propertyName] = newPropertyDef;
}
}
}
}
}
}
@@ -0,0 +1,33 @@
import {MAPPING_PROPERTY_VALUE, TYPE, REQUIRED, DESCRIPTION} from "../template-constants";
import {TemplateToken, StringToken} from "../tokens";
import {TokenType} from "../tokens/types";
export class PropertyDefinition {
public readonly type: string = "";
public readonly required: boolean = false;
public readonly description: string | undefined;
public constructor(token: TemplateToken) {
if (token.templateTokenType === TokenType.String) {
this.type = (token as StringToken).value;
} else {
const mapping = token.assertMapping(MAPPING_PROPERTY_VALUE);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${MAPPING_PROPERTY_VALUE} key`);
switch (mappingKey.value) {
case TYPE:
this.type = mappingPair.value.assertString(`${MAPPING_PROPERTY_VALUE} ${TYPE}`).value;
break;
case REQUIRED:
this.required = mappingPair.value.assertBoolean(`${MAPPING_PROPERTY_VALUE} ${REQUIRED}`).value;
break;
case DESCRIPTION:
this.description = mappingPair.value.assertString(`${MAPPING_PROPERTY_VALUE} ${DESCRIPTION}`).value;
break;
default:
mappingKey.assertUnexpectedValue(`${MAPPING_PROPERTY_VALUE} key`); // throws
}
}
}
}
}
@@ -0,0 +1,10 @@
import {LiteralToken, MappingToken} from "../tokens";
import {Definition} from "./definition";
export abstract class ScalarDefinition extends Definition {
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
}
public abstract isMatch(literal: LiteralToken): boolean;
}
@@ -0,0 +1,53 @@
import {DEFINITION, SEQUENCE, ITEM_TYPE} from "../template-constants";
import {MappingToken} from "../tokens";
import {Definition} from "./definition";
import {DefinitionType} from "./definition-type";
import {TemplateSchema} from "./template-schema";
export class SequenceDefinition extends Definition {
public itemType = "";
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case SEQUENCE: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${SEQUENCE}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${SEQUENCE} key`);
switch (mappingKey.value) {
case ITEM_TYPE: {
const itemType = mappingPair.value.assertString(`${DEFINITION} ${SEQUENCE} ${ITEM_TYPE}`);
this.itemType = itemType.value;
break;
}
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${SEQUENCE} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.Sequence;
}
public override validate(schema: TemplateSchema, name: string): void {
if (!this.itemType) {
throw new Error(`'${name}' does not defined '${ITEM_TYPE}'`);
}
// Lookup item type
schema.getDefinition(this.itemType);
}
}
@@ -0,0 +1,88 @@
import {CONSTANT, DEFINITION, IGNORE_CASE, IS_EXPRESSION, REQUIRE_NON_EMPTY, STRING} from "../template-constants";
import {LiteralToken, MappingToken, StringToken} from "../tokens";
import {TokenType} from "../tokens/types";
import {DefinitionType} from "./definition-type";
import {ScalarDefinition} from "./scalar-definition";
import {TemplateSchema} from "./template-schema";
export class StringDefinition extends ScalarDefinition {
public constant = "";
public ignoreCase = false;
public requireNonEmpty = false;
public isExpression = false;
public constructor(key: string, definition?: MappingToken) {
super(key, definition);
if (definition) {
for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
switch (definitionKey.value) {
case STRING: {
const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${STRING}`);
for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${STRING} key`);
switch (mappingKey.value) {
case CONSTANT: {
const constantStringToken = mappingPair.value.assertString(`${DEFINITION} ${STRING} ${CONSTANT}`);
this.constant = constantStringToken.value;
break;
}
case IGNORE_CASE: {
const ignoreCaseBooleanToken = mappingPair.value.assertBoolean(
`${DEFINITION} ${STRING} ${IGNORE_CASE}`
);
this.ignoreCase = ignoreCaseBooleanToken.value;
break;
}
case REQUIRE_NON_EMPTY: {
const requireNonEmptyBooleanToken = mappingPair.value.assertBoolean(
`${DEFINITION} ${STRING} ${REQUIRE_NON_EMPTY}`
);
this.requireNonEmpty = requireNonEmptyBooleanToken.value;
break;
}
case IS_EXPRESSION: {
const isExpressionToken = mappingPair.value.assertBoolean(`${DEFINITION} ${STRING} ${IS_EXPRESSION}`);
this.isExpression = isExpressionToken.value;
break;
}
default:
// throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${STRING} key`);
break;
}
}
break;
}
default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
}
}
}
}
public override get definitionType(): DefinitionType {
return DefinitionType.String;
}
public override isMatch(literal: LiteralToken): boolean {
if (literal.templateTokenType === TokenType.String) {
const value = (literal as StringToken).value;
if (this.constant) {
return this.ignoreCase ? this.constant.toUpperCase() === value.toUpperCase() : this.constant === value;
} else if (this.requireNonEmpty) {
return !!value;
} else {
return true;
}
}
return false;
}
public override validate(schema: TemplateSchema, name: string): void {
if (this.constant && this.requireNonEmpty) {
throw new Error(`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`);
}
}
}
@@ -0,0 +1,551 @@
import {TokenType} from "../../templates/tokens/types";
import {ObjectReader} from "../object-reader";
import {
ALLOWED_VALUES,
ANY,
BOOLEAN,
BOOLEAN_DEFINITION,
BOOLEAN_DEFINITION_PROPERTIES,
CONSTANT,
CONTEXT,
DEFINITION,
DEFINITIONS,
DESCRIPTION,
IGNORE_CASE,
IS_EXPRESSION,
ITEM_TYPE,
LOOSE_KEY_TYPE,
LOOSE_VALUE_TYPE,
MAPPING,
MAPPING_DEFINITION,
MAPPING_DEFINITION_PROPERTIES,
MAPPING_PROPERTY_VALUE,
NON_EMPTY_STRING,
NULL,
NULL_DEFINITION,
NULL_DEFINITION_PROPERTIES,
NUMBER,
NUMBER_DEFINITION,
NUMBER_DEFINITION_PROPERTIES,
ONE_OF,
ONE_OF_DEFINITION,
PROPERTIES,
PROPERTY_VALUE,
REQUIRED,
REQUIRE_NON_EMPTY,
SEQUENCE,
SEQUENCE_DEFINITION,
SEQUENCE_DEFINITION_PROPERTIES,
SEQUENCE_OF_NON_EMPTY_STRING,
STRING,
STRING_DEFINITION,
STRING_DEFINITION_PROPERTIES,
TEMPLATE_SCHEMA,
TYPE,
VERSION
} from "../template-constants";
import {TemplateContext, TemplateValidationErrors} from "../template-context";
import {readTemplate} from "../template-reader";
import {MappingToken, SequenceToken, StringToken} from "../tokens";
import {NoOperationTraceWriter} from "../trace-writer";
import {BooleanDefinition} from "./boolean-definition";
import {Definition} from "./definition";
import {DefinitionType} from "./definition-type";
import {MappingDefinition} from "./mapping-definition";
import {NullDefinition} from "./null-definition";
import {NumberDefinition} from "./number-definition";
import {OneOfDefinition} from "./one-of-definition";
import {PropertyDefinition} from "./property-definition";
import {ScalarDefinition} from "./scalar-definition";
import {SequenceDefinition} from "./sequence-definition";
import {StringDefinition} from "./string-definition";
/**
* This models the root schema object and contains definitions
*/
export class TemplateSchema {
private static readonly _definitionNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
private static _internalSchema: TemplateSchema | undefined;
public readonly definitions: {[key: string]: Definition} = {};
public readonly version: string = "";
public constructor(mapping?: MappingToken) {
// Add built-in type: null
this.definitions[NULL] = new NullDefinition(NULL);
// Add built-in type: boolean
this.definitions[BOOLEAN] = new BooleanDefinition(BOOLEAN);
// Add built-in type: number
this.definitions[NUMBER] = new NumberDefinition(NUMBER);
// Add built-in type: string
this.definitions[STRING] = new StringDefinition(STRING);
// Add built-in type: sequence
const sequenceDefinition = new SequenceDefinition(SEQUENCE);
sequenceDefinition.itemType = ANY;
this.definitions[sequenceDefinition.key] = sequenceDefinition;
// Add built-in type: mapping
const mappingDefinition = new MappingDefinition(MAPPING);
mappingDefinition.looseKeyType = STRING;
mappingDefinition.looseValueType = ANY;
this.definitions[mappingDefinition.key] = mappingDefinition;
// Add built-in type: any
const anyDefinition = new OneOfDefinition(ANY);
anyDefinition.oneOf.push(NULL);
anyDefinition.oneOf.push(BOOLEAN);
anyDefinition.oneOf.push(NUMBER);
anyDefinition.oneOf.push(STRING);
anyDefinition.oneOf.push(SEQUENCE);
anyDefinition.oneOf.push(MAPPING);
this.definitions[anyDefinition.key] = anyDefinition;
if (mapping) {
for (const pair of mapping) {
const key = pair.key.assertString(`${TEMPLATE_SCHEMA} key`);
switch (key.value) {
case VERSION: {
this.version = pair.value.assertString(`${TEMPLATE_SCHEMA} ${VERSION}`).value;
break;
}
case DEFINITIONS: {
const definitions = pair.value.assertMapping(`${TEMPLATE_SCHEMA} ${DEFINITIONS}`);
for (const definitionsPair of definitions) {
const definitionsKey = definitionsPair.key.assertString(`${TEMPLATE_SCHEMA} ${DEFINITIONS} key`);
const definitionsValue = definitionsPair.value.assertMapping(`${TEMPLATE_SCHEMA} ${DEFINITIONS} value`);
let definition: Definition | undefined;
for (const definitionPair of definitionsValue) {
const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
const mappingToken = definitionsPair.value as MappingToken;
switch (definitionKey.value) {
case NULL:
definition = new NullDefinition(definitionsKey.value, definitionsValue);
break;
case BOOLEAN:
definition = new BooleanDefinition(definitionsKey.value, definitionsValue);
break;
case NUMBER:
definition = new NumberDefinition(definitionsKey.value, definitionsValue);
break;
case STRING:
definition = new StringDefinition(definitionsKey.value, definitionsValue);
break;
case SEQUENCE:
definition = new SequenceDefinition(definitionsKey.value, definitionsValue);
break;
case MAPPING:
definition = new MappingDefinition(definitionsKey.value, definitionsValue);
break;
case ONE_OF:
definition = new OneOfDefinition(definitionsKey.value, definitionsValue);
break;
case ALLOWED_VALUES:
// Change the allowed-values definition into a one-of definition and its corresponding string definitions
for (const item of mappingToken) {
if (item.value.templateTokenType === TokenType.Sequence) {
// Create a new string definition for each StringToken in the sequence
const sequenceToken = item.value as SequenceToken;
for (const activity of sequenceToken) {
if (activity.templateTokenType === TokenType.String) {
const stringToken = activity as StringToken;
const allowedValuesKey = definitionsKey.value + "-" + stringToken.value;
const allowedValuesDef = new StringDefinition(allowedValuesKey);
allowedValuesDef.constant = stringToken.toDisplayString();
this.definitions[allowedValuesKey] = allowedValuesDef;
}
}
}
}
definition = new OneOfDefinition(definitionsKey.value, definitionsValue);
break;
case CONTEXT:
case DESCRIPTION:
continue;
default:
// throws
definitionKey.assertUnexpectedValue(`${DEFINITION} mapping key`);
break;
}
break;
}
if (!definition) {
throw new Error(`Not enough information to construct definition '${definitionsKey.value}'`);
}
this.definitions[definitionsKey.value] = definition;
}
break;
}
default:
// throws
key.assertUnexpectedValue(`${TEMPLATE_SCHEMA} key`);
break;
}
}
}
}
/**
* Looks up a definition by name
*/
public getDefinition(name: string): Definition {
const result = this.definitions[name];
if (result) {
return result;
}
throw new Error(`Schema definition '${name}' not found`);
}
/**
* Expands one-of definitions and returns all scalar definitions
*/
public getScalarDefinitions(definition: Definition): ScalarDefinition[] {
const result: ScalarDefinition[] = [];
switch (definition.definitionType) {
case DefinitionType.Null:
case DefinitionType.Boolean:
case DefinitionType.Number:
case DefinitionType.String:
result.push(definition as ScalarDefinition);
break;
case DefinitionType.OneOf: {
const oneOf = definition as OneOfDefinition;
// Expand nested one-of definitions
for (const nestedName of oneOf.oneOf) {
const nestedDefinition = this.getDefinition(nestedName);
result.push(...this.getScalarDefinitions(nestedDefinition));
}
break;
}
}
return result;
}
/**
* Expands one-of definitions and returns all matching definitions by type
*/
public getDefinitionsOfType(definition: Definition, type: DefinitionType): Definition[] {
const result: Definition[] = [];
if (definition.definitionType === type) {
result.push(definition);
} else if (definition.definitionType === DefinitionType.OneOf) {
const oneOf = definition as OneOfDefinition;
for (const nestedName of oneOf.oneOf) {
const nestedDefinition = this.getDefinition(nestedName);
if (nestedDefinition.definitionType === type) {
result.push(nestedDefinition);
}
}
}
return result;
}
/**
* Attempts match the property name to a property defined by any of the specified definitions.
* If matched, any unmatching definitions are filtered from the definitions array.
* Returns the type information for the matched property.
*/
public matchPropertyAndFilter(
definitions: MappingDefinition[],
propertyName: string
): PropertyDefinition | undefined {
let result: PropertyDefinition | undefined;
// Check for a matching well-known property
let notFoundInSome = false;
for (const definition of definitions) {
const propertyDef = definition.properties[propertyName];
if (propertyDef) {
result = propertyDef;
} else {
notFoundInSome = true;
}
}
// Filter the matched definitions if needed
if (result && notFoundInSome) {
for (let i = 0; i < definitions.length; ) {
if (definitions[i].properties[propertyName]) {
i++;
} else {
definitions.splice(i, 1);
}
}
}
return result;
}
private validate(): void {
const oneOfDefinitions: {[key: string]: OneOfDefinition} = {};
for (const name of Object.keys(this.definitions)) {
if (!name.match(TemplateSchema._definitionNamePattern)) {
throw new Error(`Invalid definition name '${name}'`);
}
const definition = this.definitions[name];
// Delay validation for 'one-of' definitions
if (definition.definitionType === DefinitionType.OneOf) {
oneOfDefinitions[name] = definition as OneOfDefinition;
}
// Otherwise validate now
else {
definition.validate(this, name);
}
}
// Validate 'one-of' definitions
for (const name of Object.keys(oneOfDefinitions)) {
const oneOf = oneOfDefinitions[name];
oneOf.validate(this, name);
}
}
/**
* Loads a user-defined schema file
*/
public static load(objectReader: ObjectReader): TemplateSchema {
const context = new TemplateContext(
new TemplateValidationErrors(10, 500),
TemplateSchema.getInternalSchema(),
new NoOperationTraceWriter()
);
const template = readTemplate(context, TEMPLATE_SCHEMA, objectReader, undefined);
context.errors.check();
const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
const schema = new TemplateSchema(mapping);
schema.validate();
return schema;
}
/**
* Gets the internal schema used for reading user-defined schema files
*/
private static getInternalSchema(): TemplateSchema {
if (TemplateSchema._internalSchema === undefined) {
const schema = new TemplateSchema();
// template-schema
let mappingDefinition = new MappingDefinition(TEMPLATE_SCHEMA);
mappingDefinition.properties[VERSION] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[DEFINITIONS] = new PropertyDefinition(
new StringToken(undefined, undefined, DEFINITIONS, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// definitions
mappingDefinition = new MappingDefinition(DEFINITIONS);
mappingDefinition.looseKeyType = NON_EMPTY_STRING;
mappingDefinition.looseValueType = DEFINITION;
schema.definitions[mappingDefinition.key] = mappingDefinition;
// definition
let oneOfDefinition = new OneOfDefinition(DEFINITION);
oneOfDefinition.oneOf.push(NULL_DEFINITION);
oneOfDefinition.oneOf.push(BOOLEAN_DEFINITION);
oneOfDefinition.oneOf.push(NUMBER_DEFINITION);
oneOfDefinition.oneOf.push(STRING_DEFINITION);
oneOfDefinition.oneOf.push(SEQUENCE_DEFINITION);
oneOfDefinition.oneOf.push(MAPPING_DEFINITION);
oneOfDefinition.oneOf.push(ONE_OF_DEFINITION);
schema.definitions[oneOfDefinition.key] = oneOfDefinition;
// null-definition
mappingDefinition = new MappingDefinition(NULL_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[NULL] = new PropertyDefinition(
new StringToken(undefined, undefined, NULL_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// null-definition-properties
mappingDefinition = new MappingDefinition(NULL_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// boolean-definition
mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[BOOLEAN] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// boolean-definition-properties
mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// number-definition
mappingDefinition = new MappingDefinition(NUMBER_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[NUMBER] = new PropertyDefinition(
new StringToken(undefined, undefined, NUMBER_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// number-definition-properties
mappingDefinition = new MappingDefinition(NUMBER_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// string-definition
mappingDefinition = new MappingDefinition(STRING_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[STRING] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// string-definition-properties
mappingDefinition = new MappingDefinition(STRING_DEFINITION_PROPERTIES);
mappingDefinition.properties[CONSTANT] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[IGNORE_CASE] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined)
);
mappingDefinition.properties[REQUIRE_NON_EMPTY] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined)
);
mappingDefinition.properties[IS_EXPRESSION] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// sequence-definition
mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[SEQUENCE] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// sequence-definition-properties
mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION_PROPERTIES);
mappingDefinition.properties[ITEM_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// mapping-definition
mappingDefinition = new MappingDefinition(MAPPING_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[MAPPING] = new PropertyDefinition(
new StringToken(undefined, undefined, MAPPING_DEFINITION_PROPERTIES, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// mapping-definition-properties
mappingDefinition = new MappingDefinition(MAPPING_DEFINITION_PROPERTIES);
mappingDefinition.properties[PROPERTIES] = new PropertyDefinition(
new StringToken(undefined, undefined, PROPERTIES, undefined)
);
mappingDefinition.properties[LOOSE_KEY_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[LOOSE_VALUE_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// properties
mappingDefinition = new MappingDefinition(PROPERTIES);
mappingDefinition.looseKeyType = NON_EMPTY_STRING;
mappingDefinition.looseValueType = PROPERTY_VALUE;
schema.definitions[mappingDefinition.key] = mappingDefinition;
// property-value
oneOfDefinition = new OneOfDefinition(PROPERTY_VALUE);
oneOfDefinition.oneOf.push(NON_EMPTY_STRING);
oneOfDefinition.oneOf.push(MAPPING_PROPERTY_VALUE);
schema.definitions[oneOfDefinition.key] = oneOfDefinition;
// mapping-property-value
mappingDefinition = new MappingDefinition(MAPPING_PROPERTY_VALUE);
mappingDefinition.properties[TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[REQUIRED] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined)
);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// one-of-definition
mappingDefinition = new MappingDefinition(ONE_OF_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined)
);
mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[ONE_OF] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
mappingDefinition.properties[ALLOWED_VALUES] = new PropertyDefinition(
new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
);
schema.definitions[mappingDefinition.key] = mappingDefinition;
// non-empty-string
const stringDefinition = new StringDefinition(NON_EMPTY_STRING);
stringDefinition.requireNonEmpty = true;
schema.definitions[stringDefinition.key] = stringDefinition;
// sequence-of-non-empty-string
const sequenceDefinition = new SequenceDefinition(SEQUENCE_OF_NON_EMPTY_STRING);
sequenceDefinition.itemType = NON_EMPTY_STRING;
schema.definitions[sequenceDefinition.key] = sequenceDefinition;
schema.validate();
TemplateSchema._internalSchema = schema;
}
return TemplateSchema._internalSchema;
}
}