Merge parser and expressions into repository
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user