Merge parser and expressions into repository

This commit is contained in:
Christopher Schleiden
2023-01-06 15:54:31 -08:00
parent 4f961a6247
commit 70e33f999d
328 changed files with 35337 additions and 0 deletions
@@ -0,0 +1,52 @@
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,63 @@
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,85 @@
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 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,117 @@
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,50 @@
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,52 @@
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,223 @@
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,46 @@
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 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,63 @@
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,113 @@
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,679 @@
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 { TokenType } from "../tokens/types"
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
}
}