Merge pull request #79 from github/joshmgross/format

Use a standard format configuration across packages
This commit is contained in:
Josh Gross
2023-01-09 19:11:58 -05:00
committed by GitHub
123 changed files with 3267 additions and 4599 deletions
+3 -1
View File
@@ -29,6 +29,8 @@
"scripts": { "scripts": {
"build": "tsc --build tsconfig.build.json", "build": "tsc --build tsconfig.build.json",
"clean": "rimraf dist", "clean": "rimraf dist",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"prepublishOnly": "npm run build && npm run test", "prepublishOnly": "npm run build && npm run test",
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest", "test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch", "test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
@@ -48,4 +50,4 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"typescript": "^4.7.4" "typescript": "^4.7.4"
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { ExpressionData } from "./data"; import {ExpressionData} from "./data";
import { Token } from "./lexer"; import {Token} from "./lexer";
export interface ExprVisitor<R> { export interface ExprVisitor<R> {
visitLiteral(literal: Literal): R; visitLiteral(literal: Literal): R;
+26 -47
View File
@@ -1,9 +1,9 @@
import { complete, CompletionItem, trimTokenVector } from "./completion"; import {complete, CompletionItem, trimTokenVector} from "./completion";
import { BooleanData } from "./data/boolean"; import {BooleanData} from "./data/boolean";
import { Dictionary } from "./data/dictionary"; import {Dictionary} from "./data/dictionary";
import { StringData } from "./data/string"; import {StringData} from "./data/string";
import { FunctionInfo } from "./funcs/info"; import {FunctionInfo} from "./funcs/info";
import { Lexer, TokenType } from "./lexer"; import {Lexer, TokenType} from "./lexer";
const testContext = new Dictionary( const testContext = new Dictionary(
{ {
@@ -11,24 +11,24 @@ const testContext = new Dictionary(
value: new Dictionary( value: new Dictionary(
{ {
key: "FOO", key: "FOO",
value: new StringData(""), value: new StringData("")
}, },
{ {
key: "BAR_TEST", key: "BAR_TEST",
value: new StringData(""), value: new StringData("")
} }
), )
}, },
{ {
key: "secrets", key: "secrets",
value: new Dictionary({ value: new Dictionary({
key: "AWS_TOKEN", key: "AWS_TOKEN",
value: new BooleanData(true), value: new BooleanData(true)
}), })
}, },
{ {
key: "hashFiles(1,255)", key: "hashFiles(1,255)",
value: new Dictionary(), value: new Dictionary()
} }
); );
@@ -38,17 +38,13 @@ const testComplete = (input: string): CompletionItem[] => {
const pos = input.indexOf("|"); const pos = input.indexOf("|");
input = input.replace("|", ""); input = input.replace("|", "");
const results = complete( const results = complete(input.slice(0, pos >= 0 ? pos : input.length), testContext, testFunctions);
input.slice(0, pos >= 0 ? pos : input.length),
testContext,
testFunctions
);
return results; return results;
}; };
function completionItems(...labels: string[]): CompletionItem[] { function completionItems(...labels: string[]): CompletionItem[] {
return labels.map((label) => ({ label, function: false })); return labels.map(label => ({label, function: false}));
} }
describe("auto-complete", () => { describe("auto-complete", () => {
@@ -58,7 +54,7 @@ describe("auto-complete", () => {
label: "toJson", label: "toJson",
description: description:
"`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.", "`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.",
function: true, function: true
}; };
expect(testComplete("to")).toContainEqual(expected); expect(testComplete("to")).toContainEqual(expected);
expect(testComplete("toJs")).toContainEqual(expected); expect(testComplete("toJs")).toContainEqual(expected);
@@ -69,7 +65,7 @@ describe("auto-complete", () => {
it("removes parentheses from passed in function context", () => { it("removes parentheses from passed in function context", () => {
expect(testComplete("|")).toContainEqual({ expect(testComplete("|")).toContainEqual({
label: "hashFiles", label: "hashFiles",
function: true, function: true
}); });
}); });
}); });
@@ -92,9 +88,7 @@ describe("auto-complete", () => {
}); });
it("provides suggestions for contexts in function call", () => { it("provides suggestions for contexts in function call", () => {
expect(testComplete("toJSON(env.|)")).toEqual( expect(testComplete("toJSON(env.|)")).toEqual(completionItems("BAR_TEST", "FOO"));
completionItems("BAR_TEST", "FOO")
);
}); });
}); });
}); });
@@ -106,34 +100,19 @@ describe("trimTokenVector", () => {
}>([ }>([
{ {
input: "env.", input: "env.",
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF], expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF]
}, },
{ {
input: "github.act", input: "github.act",
expected: [ expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
TokenType.IDENTIFIER,
TokenType.DOT,
TokenType.IDENTIFIER,
TokenType.EOF,
],
}, },
{ {
input: "1 == github.act", input: "1 == github.act",
expected: [ expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
TokenType.IDENTIFIER,
TokenType.DOT,
TokenType.IDENTIFIER,
TokenType.EOF,
],
}, },
{ {
input: "github.mona == github.act", input: "github.mona == github.act",
expected: [ expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.IDENTIFIER, TokenType.EOF]
TokenType.IDENTIFIER,
TokenType.DOT,
TokenType.IDENTIFIER,
TokenType.EOF,
],
}, },
{ {
input: "github['test'].", input: "github['test'].",
@@ -143,14 +122,14 @@ describe("trimTokenVector", () => {
TokenType.STRING, TokenType.STRING,
TokenType.RIGHT_BRACKET, TokenType.RIGHT_BRACKET,
TokenType.DOT, TokenType.DOT,
TokenType.EOF, TokenType.EOF
], ]
}, }
])("$input", ({ input, expected }) => { ])("$input", ({input, expected}) => {
const l = new Lexer(input); const l = new Lexer(input);
const lr = l.lex(); const lr = l.lex();
const tv = trimTokenVector(lr.tokens); const tv = trimTokenVector(lr.tokens);
expect(tv.map((x) => x.type)).toEqual(expected); expect(tv.map(x => x.type)).toEqual(expected);
}); });
}); });
+13 -20
View File
@@ -1,10 +1,10 @@
import { Dictionary, isDictionary } from "./data/dictionary"; import {Dictionary, isDictionary} from "./data/dictionary";
import { ExpressionData } from "./data/expressiondata"; import {ExpressionData} from "./data/expressiondata";
import { Evaluator } from "./evaluator"; import {Evaluator} from "./evaluator";
import { wellKnownFunctions } from "./funcs"; import {wellKnownFunctions} from "./funcs";
import { FunctionInfo } from "./funcs/info"; import {FunctionInfo} from "./funcs/info";
import { Lexer, Token, TokenType } from "./lexer"; import {Lexer, Token, TokenType} from "./lexer";
import { Parser } from "./parser"; import {Parser} from "./parser";
export type CompletionItem = { export type CompletionItem = {
label: string; label: string;
@@ -20,11 +20,7 @@ export type CompletionItem = {
// - context.path.| or context.path['| -- auto-complete context access // - context.path.| or context.path['| -- auto-complete context access
// - toJS| -- auto-complete function call or top-level // - toJS| -- auto-complete function call or top-level
// - | -- auto-complete function call or top-level context access // - | -- auto-complete function call or top-level context access
export function complete( export function complete(input: string, context: Dictionary, extensionFunctions: FunctionInfo[]): CompletionItem[] {
input: string,
context: Dictionary,
extensionFunctions: FunctionInfo[]
): CompletionItem[] {
// Lex // Lex
const lexer = new Lexer(input); const lexer = new Lexer(input);
const lexResult = lexer.lex(); const lexResult = lexer.lex();
@@ -68,7 +64,7 @@ export function complete(
const p = new Parser( const p = new Parser(
pathTokenVector, pathTokenVector,
context.pairs().map((x) => x.key), context.pairs().map(x => x.key),
extensionFunctions extensionFunctions
); );
const expr = p.parse(); const expr = p.parse();
@@ -82,14 +78,11 @@ export function complete(
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] { function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
const result: CompletionItem[] = []; const result: CompletionItem[] = [];
for (const fdef of [ for (const fdef of [...Object.values(wellKnownFunctions), ...extensionFunctions]) {
...Object.values(wellKnownFunctions),
...extensionFunctions,
]) {
result.push({ result.push({
label: fdef.name, label: fdef.name,
description: fdef.description, description: fdef.description,
function: true, function: true
}); });
} }
@@ -104,7 +97,7 @@ function contextKeys(context: ExpressionData): CompletionItem[] {
return ( return (
context context
.pairs() .pairs()
.map((x) => completionItemFromContext(x.key)) .map(x => completionItemFromContext(x.key))
// Sort contexts // Sort contexts
.sort((a, b) => a.label.localeCompare(b.label)) .sort((a, b) => a.label.localeCompare(b.label))
); );
@@ -119,7 +112,7 @@ function completionItemFromContext(context: string): CompletionItem {
return { return {
label: isFunc ? context.substring(0, parenIndex) : context, label: isFunc ? context.substring(0, parenIndex) : context,
function: isFunc, function: isFunc
}; };
} }
+1 -6
View File
@@ -1,9 +1,4 @@
import { import {ExpressionData, ExpressionDataInterface, Kind, kindStr} from "./expressiondata";
ExpressionData,
ExpressionDataInterface,
Kind,
kindStr,
} from "./expressiondata";
export class Array implements ExpressionDataInterface { export class Array implements ExpressionDataInterface {
private v: ExpressionData[] = []; private v: ExpressionData[] = [];
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExpressionDataInterface, Kind } from "./expressiondata"; import {ExpressionDataInterface, Kind} from "./expressiondata";
export class BooleanData implements ExpressionDataInterface { export class BooleanData implements ExpressionDataInterface {
constructor(public readonly value: boolean) {} constructor(public readonly value: boolean) {}
@@ -1,12 +1,12 @@
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { StringData } from "./string"; import {StringData} from "./string";
describe("dictionary", () => { describe("dictionary", () => {
it("pairs contains all values", () => { it("pairs contains all values", () => {
const d = new Dictionary(); const d = new Dictionary();
d.add("ABC", new StringData("val")); d.add("ABC", new StringData("val"));
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val") }]); expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val")}]);
}); });
it("does not add duplicate entries", () => { it("does not add duplicate entries", () => {
@@ -14,6 +14,6 @@ describe("dictionary", () => {
d.add("ABC", new StringData("val1")); d.add("ABC", new StringData("val1"));
d.add("abc", new StringData("val2")); d.add("abc", new StringData("val2"));
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val1") }]); expect(d.pairs()).toEqual([{key: "ABC", value: new StringData("val1")}]);
}); });
}); });
+3 -9
View File
@@ -1,15 +1,9 @@
import { import {ExpressionData, ExpressionDataInterface, Kind, kindStr, Pair} from "./expressiondata";
ExpressionData,
ExpressionDataInterface,
Kind,
kindStr,
Pair,
} from "./expressiondata";
export class Dictionary implements ExpressionDataInterface { export class Dictionary implements ExpressionDataInterface {
private keys: string[] = []; private keys: string[] = [];
private v: ExpressionData[] = []; private v: ExpressionData[] = [];
private indexMap: { [key: string]: number } = {}; private indexMap: {[key: string]: number} = {};
constructor(...pairs: Pair[]) { constructor(...pairs: Pair[]) {
for (const p of pairs) { for (const p of pairs) {
@@ -56,7 +50,7 @@ export class Dictionary implements ExpressionDataInterface {
const result: Pair[] = []; const result: Pair[] = [];
for (const key of this.keys) { for (const key of this.keys) {
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] }); result.push({key, value: this.v[this.indexMap[key.toLowerCase()]]});
} }
return result; return result;
+8 -14
View File
@@ -1,9 +1,9 @@
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { Null } from "./null"; import {Null} from "./null";
import { Array } from "./array"; import {Array} from "./array";
import { StringData } from "./string"; import {StringData} from "./string";
import { NumberData } from "./number"; import {NumberData} from "./number";
import { BooleanData } from "./boolean"; import {BooleanData} from "./boolean";
export enum Kind { export enum Kind {
String = 0, String = 0,
@@ -12,7 +12,7 @@ export enum Kind {
Boolean, Boolean,
Number, Number,
CaseSensitiveDictionary, CaseSensitiveDictionary,
Null, Null
} }
export function kindStr(k: Kind): string { export function kindStr(k: Kind): string {
@@ -43,13 +43,7 @@ export interface ExpressionDataInterface {
number(): number; number(): number;
} }
export type ExpressionData = export type ExpressionData = Array | Dictionary | StringData | BooleanData | NumberData | Null;
| Array
| Dictionary
| StringData
| BooleanData
| NumberData
| Null;
export type Pair = { export type Pair = {
key: string; key: string;
+9 -9
View File
@@ -1,9 +1,9 @@
export { Array } from "./array"; export {Array} from "./array";
export { BooleanData } from "./boolean"; export {BooleanData} from "./boolean";
export { Dictionary } from "./dictionary"; export {Dictionary} from "./dictionary";
export { ExpressionData, Kind } from "./expressiondata"; export {ExpressionData, Kind} from "./expressiondata";
export { Null } from "./null"; export {Null} from "./null";
export { NumberData } from "./number"; export {NumberData} from "./number";
export { replacer } from "./replacer"; export {replacer} from "./replacer";
export { reviver } from "./reviver"; export {reviver} from "./reviver";
export { StringData } from "./string"; export {StringData} from "./string";
+1 -5
View File
@@ -1,8 +1,4 @@
import { import {ExpressionData, ExpressionDataInterface, Kind} from "./expressiondata";
ExpressionData,
ExpressionDataInterface,
Kind,
} from "./expressiondata";
export class Null implements ExpressionDataInterface { export class Null implements ExpressionDataInterface {
constructor() {} constructor() {}
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExpressionDataInterface, Kind } from "./expressiondata"; import {ExpressionDataInterface, Kind} from "./expressiondata";
export class NumberData implements ExpressionDataInterface { export class NumberData implements ExpressionDataInterface {
constructor(public readonly value: number) {} constructor(public readonly value: number) {}
+12 -20
View File
@@ -1,9 +1,9 @@
import { Array } from "./array"; import {Array} from "./array";
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { Null } from "./null"; import {Null} from "./null";
import { NumberData } from "./number"; import {NumberData} from "./number";
import { replacer } from "./replacer"; import {replacer} from "./replacer";
import { StringData } from "./string"; import {StringData} from "./string";
describe("replacer", () => { describe("replacer", () => {
it("null", () => { it("null", () => {
@@ -11,22 +11,14 @@ describe("replacer", () => {
}); });
it("array", () => { it("array", () => {
expect( expect(JSON.stringify(new Array(new StringData("a"), new StringData("b")), replacer, " ")).toEqual(
JSON.stringify( '[\n "a",\n "b"\n]'
new Array(new StringData("a"), new StringData("b")), );
replacer,
" "
)
).toEqual('[\n "a",\n "b"\n]');
}); });
it("dictionary", () => { it("dictionary", () => {
expect( expect(JSON.stringify(new Dictionary({key: "a", value: new NumberData(42)}), replacer, " ")).toEqual(
JSON.stringify( '{\n "a": 42\n}'
new Dictionary({ key: "a", value: new NumberData(42) }), );
replacer,
" "
)
).toEqual('{\n "a": 42\n}');
}); });
}); });
+6 -6
View File
@@ -1,9 +1,9 @@
import { Array } from "./array"; import {Array} from "./array";
import { BooleanData } from "./boolean"; import {BooleanData} from "./boolean";
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { Null } from "./null"; import {Null} from "./null";
import { NumberData } from "./number"; import {NumberData} from "./number";
import { StringData } from "./string"; import {StringData} from "./string";
/** /**
* Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON * Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON
+25 -28
View File
@@ -1,11 +1,11 @@
import { Array } from "./array"; import {Array} from "./array";
import { BooleanData } from "./boolean"; import {BooleanData} from "./boolean";
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { ExpressionData } from "./expressiondata"; import {ExpressionData} from "./expressiondata";
import { Null } from "./null"; import {Null} from "./null";
import { NumberData } from "./number"; import {NumberData} from "./number";
import { reviver } from "./reviver"; import {reviver} from "./reviver";
import { StringData } from "./string"; import {StringData} from "./string";
describe("reviver", () => { describe("reviver", () => {
const tests: { const tests: {
@@ -16,32 +16,32 @@ describe("reviver", () => {
{ {
name: "null", name: "null",
data: "null", data: "null",
want: new Null(), want: new Null()
}, },
{ {
name: "number", name: "number",
data: "1", data: "1",
want: new NumberData(1), want: new NumberData(1)
}, },
{ {
name: "string", name: "string",
data: `"a"`, data: `"a"`,
want: new StringData("a"), want: new StringData("a")
}, },
{ {
name: "true boolean", name: "true boolean",
data: "true", data: "true",
want: new BooleanData(true), want: new BooleanData(true)
}, },
{ {
name: "false boolean", name: "false boolean",
data: "false", data: "false",
want: new BooleanData(false), want: new BooleanData(false)
}, },
{ {
name: "array", name: "array",
data: `[1,2,3]`, data: `[1,2,3]`,
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)), want: new Array(new NumberData(1), new NumberData(2), new NumberData(3))
}, },
{ {
name: "nested array", name: "nested array",
@@ -51,7 +51,7 @@ describe("reviver", () => {
new NumberData(2), new NumberData(2),
new Array(new NumberData(3), new NumberData(4)), new Array(new NumberData(3), new NumberData(4)),
new NumberData(5) new NumberData(5)
), )
}, },
{ {
name: "complex array", name: "complex array",
@@ -59,26 +59,23 @@ describe("reviver", () => {
want: new Array( want: new Array(
new Dictionary({ new Dictionary({
key: "a", key: "a",
value: new Array(new BooleanData(true), new NumberData(2)), value: new Array(new BooleanData(true), new NumberData(2))
}), }),
new Dictionary({ key: "b", value: new StringData("three") }), new Dictionary({key: "b", value: new StringData("three")}),
new Dictionary({ key: "c", value: new Null() }) new Dictionary({key: "c", value: new Null()})
), )
}, },
{ {
name: "dictionary", name: "dictionary",
data: `{ "object1": {} }`, data: `{ "object1": {} }`,
want: new Dictionary({ want: new Dictionary({
key: "object1", key: "object1",
value: new Dictionary(), value: new Dictionary()
}), })
}, }
]; ];
test.each(tests)( test.each(tests)("$name", ({data, want}: {data: string; want: ExpressionData}) => {
"$name", expect(JSON.parse(data, reviver)).toEqual(want);
({ data, want }: { data: string; want: ExpressionData }) => { });
expect(JSON.parse(data, reviver)).toEqual(want);
}
);
}); });
+9 -9
View File
@@ -1,10 +1,10 @@
import { Array as dArray } from "./array"; import {Array as dArray} from "./array";
import { BooleanData } from "./boolean"; import {BooleanData} from "./boolean";
import { Dictionary } from "./dictionary"; import {Dictionary} from "./dictionary";
import { ExpressionData, Pair } from "./expressiondata"; import {ExpressionData, Pair} from "./expressiondata";
import { Null } from "./null"; import {Null} from "./null";
import { NumberData } from "./number"; import {NumberData} from "./number";
import { StringData } from "./string"; import {StringData} from "./string";
/** /**
* Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object. * Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object.
@@ -35,10 +35,10 @@ export function reviver(key: string, val: any): ExpressionData {
if (typeof val === "object") { if (typeof val === "object") {
return new Dictionary( return new Dictionary(
...Object.keys(val).map( ...Object.keys(val).map(
(k) => k =>
({ ({
key: k, key: k,
value: val[k], value: val[k]
} as Pair) } as Pair)
) )
); );
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExpressionDataInterface, Kind } from "./expressiondata"; import {ExpressionDataInterface, Kind} from "./expressiondata";
export class StringData implements ExpressionDataInterface { export class StringData implements ExpressionDataInterface {
constructor(public readonly value: string) {} constructor(public readonly value: string) {}
+4 -4
View File
@@ -1,4 +1,4 @@
import { Pos, Token, tokenString } from "./lexer"; import {Pos, Token, tokenString} from "./lexer";
export const MAX_PARSER_DEPTH = 50; export const MAX_PARSER_DEPTH = 50;
export const MAX_EXPRESSION_LENGTH = 21000; export const MAX_EXPRESSION_LENGTH = 21000;
@@ -13,7 +13,7 @@ export enum ErrorType {
ErrorTooFewParameters, ErrorTooFewParameters,
ErrorTooManyParameters, ErrorTooManyParameters,
ErrorUnrecognizedContext, ErrorUnrecognizedContext,
ErrorUnrecognizedFunction, ErrorUnrecognizedFunction
} }
export class ExpressionError extends Error { export class ExpressionError extends Error {
@@ -42,8 +42,8 @@ function errorDescription(typ: ErrorType): string {
return "Too few parameters supplied"; return "Too few parameters supplied";
case ErrorType.ErrorTooManyParameters: case ErrorType.ErrorTooManyParameters:
return "Too many parameters supplied"; return "Too many parameters supplied";
case ErrorType.ErrorUnrecognizedContext: case ErrorType.ErrorUnrecognizedContext:
return "Unrecognized named-value"; return "Unrecognized named-value";
case ErrorType.ErrorUnrecognizedFunction: case ErrorType.ErrorUnrecognizedFunction:
return "Unrecognized function"; return "Unrecognized function";
default: // Should never reach here. default: // Should never reach here.
+4 -4
View File
@@ -1,7 +1,7 @@
import * as data from "./data"; import * as data from "./data";
import { Evaluator } from "./evaluator"; import {Evaluator} from "./evaluator";
import { Lexer } from "./lexer"; import {Lexer} from "./lexer";
import { Parser } from "./parser"; import {Parser} from "./parser";
test("evaluator", () => { test("evaluator", () => {
const input = "foo['']"; const input = "foo['']";
@@ -18,7 +18,7 @@ test("evaluator", () => {
expr, expr,
new data.Dictionary({ new data.Dictionary({
key: "foo", key: "foo",
value: new data.Dictionary({ key: "bar", value: new data.NumberData(42) }), value: new data.Dictionary({key: "bar", value: new data.NumberData(42)})
}) })
); );
const eresult = evaluator.evaluate(); const eresult = evaluator.evaluate();
+11 -22
View File
@@ -9,14 +9,14 @@ import {
Literal, Literal,
Logical, Logical,
Star, Star,
Unary, Unary
} from "./ast"; } from "./ast";
import * as data from "./data"; import * as data from "./data";
import { FilteredArray } from "./filtered_array"; import {FilteredArray} from "./filtered_array";
import { wellKnownFunctions } from "./funcs"; import {wellKnownFunctions} from "./funcs";
import { idxHelper } from "./idxHelper"; import {idxHelper} from "./idxHelper";
import { TokenType } from "./lexer"; import {TokenType} from "./lexer";
import { equals, falsy, greaterThan, lessThan, truthy } from "./result"; import {equals, falsy, greaterThan, lessThan, truthy} from "./result";
export class EvaluationError extends Error {} export class EvaluationError extends Error {}
@@ -60,9 +60,7 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
return new data.BooleanData(greaterThan(left, right)); return new data.BooleanData(greaterThan(left, right));
case TokenType.GREATER_EQUAL: case TokenType.GREATER_EQUAL:
return new data.BooleanData( return new data.BooleanData(equals(left, right) || greaterThan(left, right));
equals(left, right) || greaterThan(left, right)
);
case TokenType.LESS: case TokenType.LESS:
return new data.BooleanData(lessThan(left, right)); return new data.BooleanData(lessThan(left, right));
@@ -150,25 +148,19 @@ export class Evaluator implements ExprVisitor<data.ExpressionData> {
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData { visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
// Evaluate arguments // Evaluate arguments
const args = functionCall.args.map((arg) => this.eval(arg)); const args = functionCall.args.map(arg => this.eval(arg));
return fcall(functionCall, args); return fcall(functionCall, args);
} }
} }
function fcall( function fcall(fc: FunctionCall, args: data.ExpressionData[]): data.ExpressionData {
fc: FunctionCall,
args: data.ExpressionData[]
): data.ExpressionData {
const f = wellKnownFunctions[fc.functionName.lexeme.toLowerCase()]; const f = wellKnownFunctions[fc.functionName.lexeme.toLowerCase()];
return f.call(...args); return f.call(...args);
} }
function filteredArrayAccess( function filteredArrayAccess(fa: FilteredArray, idx: idxHelper): data.ExpressionData {
fa: FilteredArray,
idx: idxHelper
): data.ExpressionData {
const result = new FilteredArray(); const result = new FilteredArray();
for (const item of fa.values()) { for (const item of fa.values()) {
@@ -225,10 +217,7 @@ function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
return new data.Null(); return new data.Null();
} }
function objectAccess( function objectAccess(obj: data.Dictionary, idx: idxHelper): data.ExpressionData {
obj: data.Dictionary,
idx: idxHelper
): data.ExpressionData {
if (idx.star) { if (idx.star) {
const fa = new FilteredArray(...obj.values()); const fa = new FilteredArray(...obj.values());
return fa; return fa;
+14 -21
View File
@@ -1,13 +1,13 @@
import { ErrorType, ExpressionError } from "./errors"; import {ErrorType, ExpressionError} from "./errors";
import { contains } from "./funcs/contains"; import {contains} from "./funcs/contains";
import { endswith } from "./funcs/endswith"; import {endswith} from "./funcs/endswith";
import { format } from "./funcs/format"; import {format} from "./funcs/format";
import { fromjson } from "./funcs/fromjson"; import {fromjson} from "./funcs/fromjson";
import { FunctionDefinition, FunctionInfo } from "./funcs/info"; import {FunctionDefinition, FunctionInfo} from "./funcs/info";
import { join } from "./funcs/join"; import {join} from "./funcs/join";
import { startswith } from "./funcs/startswith"; import {startswith} from "./funcs/startswith";
import { tojson } from "./funcs/tojson"; import {tojson} from "./funcs/tojson";
import { Token } from "./lexer"; import {Token} from "./lexer";
export type ParseContext = { export type ParseContext = {
allowUnknownKeywords: boolean; allowUnknownKeywords: boolean;
@@ -15,24 +15,20 @@ export type ParseContext = {
extensionFunctions: Map<string, FunctionInfo>; extensionFunctions: Map<string, FunctionInfo>;
}; };
export const wellKnownFunctions: { [name: string]: FunctionDefinition } = { export const wellKnownFunctions: {[name: string]: FunctionDefinition} = {
contains: contains, contains: contains,
endswith: endswith, endswith: endswith,
format: format, format: format,
fromjson: fromjson, fromjson: fromjson,
join: join, join: join,
startswith: startswith, startswith: startswith,
tojson: tojson, tojson: tojson
}; };
// validateFunction returns the function definition for the given function name. // validateFunction returns the function definition for the given function name.
// If the function does not exist or an incorrect number of arguments is provided, // If the function does not exist or an incorrect number of arguments is provided,
// an error is returned. // an error is returned.
export function validateFunction( export function validateFunction(context: ParseContext, identifier: Token, argCount: number) {
context: ParseContext,
identifier: Token,
argCount: number
) {
// Expression function names are case-insensitive. // Expression function names are case-insensitive.
const name = identifier.lexeme.toLowerCase(); const name = identifier.lexeme.toLowerCase();
@@ -42,10 +38,7 @@ export function validateFunction(
f = context.extensionFunctions.get(name); f = context.extensionFunctions.get(name);
if (!f) { if (!f) {
if (!context.allowUnknownKeywords) { if (!context.allowUnknownKeywords) {
throw new ExpressionError( throw new ExpressionError(ErrorType.ErrorUnrecognizedFunction, identifier);
ErrorType.ErrorUnrecognizedFunction,
identifier
);
} }
// Skip argument validation for unknown functions // Skip argument validation for unknown functions
+4 -4
View File
@@ -1,6 +1,6 @@
import { Array, BooleanData, ExpressionData, Kind } from "../data"; import {Array, BooleanData, ExpressionData, Kind} from "../data";
import { equals } from "../result"; import {equals} from "../result";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const contains: FunctionDefinition = { export const contains: FunctionDefinition = {
name: "contains", name: "contains",
@@ -32,5 +32,5 @@ export const contains: FunctionDefinition = {
} }
return new BooleanData(false); return new BooleanData(false);
}, }
}; };
+4 -4
View File
@@ -1,6 +1,6 @@
import { BooleanData, ExpressionData } from "../data"; import {BooleanData, ExpressionData} from "../data";
import { toUpperSpecial } from "../result"; import {toUpperSpecial} from "../result";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const endswith: FunctionDefinition = { export const endswith: FunctionDefinition = {
name: "endsWith", name: "endsWith",
@@ -23,5 +23,5 @@ export const endswith: FunctionDefinition = {
const rs = toUpperSpecial(right.coerceString()); const rs = toUpperSpecial(right.coerceString());
return new BooleanData(ls.endsWith(rs)); return new BooleanData(ls.endsWith(rs));
}, }
}; };
+3 -5
View File
@@ -1,13 +1,11 @@
import { Null, NumberData, StringData } from "../data"; import {Null, NumberData, StringData} from "../data";
import { format } from "./format"; import {format} from "./format";
describe("format", () => { describe("format", () => {
it("null", () => { it("null", () => {
expect(format.call(new StringData("{0}"), new Null())).toEqual(new StringData("")); expect(format.call(new StringData("{0}"), new Null())).toEqual(new StringData(""));
}); });
it("number", () => { it("number", () => {
expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual( expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual(new StringData("42"));
new StringData("42")
);
}); });
}); });
+6 -8
View File
@@ -1,5 +1,5 @@
import { ExpressionData, StringData } from "../data"; import {ExpressionData, StringData} from "../data";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const format: FunctionDefinition = { export const format: FunctionDefinition = {
name: "format", name: "format",
@@ -32,9 +32,7 @@ export const format: FunctionDefinition = {
if (argIndex.success) { if (argIndex.success) {
// Check parameter count // Check parameter count
if (1 + argIndex.result > args.length - 1) { if (1 + argIndex.result > args.length - 1) {
throw new Error( throw new Error(`The following format string references more arguments than were supplied: ${fs}`);
`The following format string references more arguments than were supplied: ${fs}`
);
} }
// Append the portion before the left brace // Append the portion before the left brace
@@ -69,7 +67,7 @@ export const format: FunctionDefinition = {
} }
return new StringData(result.join("")); return new StringData(result.join(""));
}, }
}; };
function safeCharAt(string: string, index: number): string { function safeCharAt(string: string, index: number): string {
@@ -95,7 +93,7 @@ function readArgIndex(string: string, startIndex: number): ArgIndex {
// Validate at least one digit // Validate at least one digit
if (length < 1) { if (length < 1) {
return <ArgIndex>{ return <ArgIndex>{
success: false, success: false
}; };
} }
@@ -105,7 +103,7 @@ function readArgIndex(string: string, startIndex: number): ArgIndex {
return <ArgIndex>{ return <ArgIndex>{
success: !isNaN(result), success: !isNaN(result),
result: result, result: result,
endIndex: endIndex, endIndex: endIndex
}; };
} }
+4 -4
View File
@@ -1,6 +1,6 @@
import { ExpressionData } from "../data"; import {ExpressionData} from "../data";
import { reviver } from "../data/reviver"; import {reviver} from "../data/reviver";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const fromjson: FunctionDefinition = { export const fromjson: FunctionDefinition = {
name: "fromJson", name: "fromJson",
@@ -18,5 +18,5 @@ export const fromjson: FunctionDefinition = {
const d = JSON.parse(is, reviver); const d = JSON.parse(is, reviver);
return d; return d;
}, }
}; };
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExpressionData } from "../data"; import {ExpressionData} from "../data";
export interface FunctionInfo { export interface FunctionInfo {
name: string; name: string;
+4 -4
View File
@@ -1,5 +1,5 @@
import { Array, ExpressionData, Kind, StringData } from "../data"; import {Array, ExpressionData, Kind, StringData} from "../data";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const join: FunctionDefinition = { export const join: FunctionDefinition = {
name: "join", name: "join",
@@ -25,11 +25,11 @@ export const join: FunctionDefinition = {
return new StringData( return new StringData(
(args[0] as Array) (args[0] as Array)
.values() .values()
.map((item) => item.coerceString()) .map(item => item.coerceString())
.join(separator) .join(separator)
); );
} }
return new StringData(""); return new StringData("");
}, }
}; };
+4 -4
View File
@@ -1,6 +1,6 @@
import { BooleanData, ExpressionData } from "../data"; import {BooleanData, ExpressionData} from "../data";
import { toUpperSpecial } from "../result"; import {toUpperSpecial} from "../result";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const startswith: FunctionDefinition = { export const startswith: FunctionDefinition = {
name: "startsWith", name: "startsWith",
@@ -23,5 +23,5 @@ export const startswith: FunctionDefinition = {
const rs = toUpperSpecial(right.coerceString()); const rs = toUpperSpecial(right.coerceString());
return new BooleanData(ls.startsWith(rs)); return new BooleanData(ls.startsWith(rs));
}, }
}; };
+4 -4
View File
@@ -1,6 +1,6 @@
import { ExpressionData, StringData } from "../data"; import {ExpressionData, StringData} from "../data";
import { replacer } from "../data/replacer"; import {replacer} from "../data/replacer";
import { FunctionDefinition } from "./info"; import {FunctionDefinition} from "./info";
export const tojson: FunctionDefinition = { export const tojson: FunctionDefinition = {
name: "toJson", name: "toJson",
@@ -10,5 +10,5 @@ export const tojson: FunctionDefinition = {
maxArgs: 1, maxArgs: 1,
call: (...args: ExpressionData[]): ExpressionData => { call: (...args: ExpressionData[]): ExpressionData => {
return new StringData(JSON.stringify(args[0], replacer, " ")); return new StringData(JSON.stringify(args[0], replacer, " "));
}, }
}; };
+1 -1
View File
@@ -1,4 +1,4 @@
import { ExpressionData } from "./data"; import {ExpressionData} from "./data";
export class idxHelper { export class idxHelper {
public readonly str: string | undefined; public readonly str: string | undefined;
+5 -5
View File
@@ -1,6 +1,6 @@
export { complete } from "./completion"; export {complete} from "./completion";
export * as data from "./data"; export * as data from "./data";
export { ExpressionError } from "./errors"; export {ExpressionError} from "./errors";
export { Evaluator } from "./evaluator"; export {Evaluator} from "./evaluator";
export { Lexer } from "./lexer"; export {Lexer} from "./lexer";
export { Parser } from "./parser"; export {Parser} from "./parser";
+41 -58
View File
@@ -1,4 +1,4 @@
import { Lexer, Token, TokenType } from "./lexer"; import {Lexer, Token, TokenType} from "./lexer";
describe("lexer", () => { describe("lexer", () => {
const tests: { const tests: {
@@ -6,26 +6,26 @@ describe("lexer", () => {
tokenType: TokenType[]; tokenType: TokenType[];
token?: Token; token?: Token;
}[] = [ }[] = [
{ input: "<", tokenType: [TokenType.LESS] }, {input: "<", tokenType: [TokenType.LESS]},
{ input: ">", tokenType: [TokenType.GREATER] }, {input: ">", tokenType: [TokenType.GREATER]},
{ input: "!=", tokenType: [TokenType.BANG_EQUAL] }, {input: "!=", tokenType: [TokenType.BANG_EQUAL]},
{ input: "==", tokenType: [TokenType.EQUAL_EQUAL] }, {input: "==", tokenType: [TokenType.EQUAL_EQUAL]},
{ input: "<=", tokenType: [TokenType.LESS_EQUAL] }, {input: "<=", tokenType: [TokenType.LESS_EQUAL]},
{ input: ">=", tokenType: [TokenType.GREATER_EQUAL] }, {input: ">=", tokenType: [TokenType.GREATER_EQUAL]},
{ input: "&&", tokenType: [TokenType.AND] }, {input: "&&", tokenType: [TokenType.AND]},
{ input: "||", tokenType: [TokenType.OR] }, {input: "||", tokenType: [TokenType.OR]},
// Numbers // Numbers
{ input: "12", tokenType: [TokenType.NUMBER] }, {input: "12", tokenType: [TokenType.NUMBER]},
{ input: "12.0", tokenType: [TokenType.NUMBER] }, {input: "12.0", tokenType: [TokenType.NUMBER]},
{ input: "0", tokenType: [TokenType.NUMBER] }, {input: "0", tokenType: [TokenType.NUMBER]},
{ input: "-0", tokenType: [TokenType.NUMBER] }, {input: "-0", tokenType: [TokenType.NUMBER]},
{ input: "-12.0", tokenType: [TokenType.NUMBER] }, {input: "-12.0", tokenType: [TokenType.NUMBER]},
// Strings // Strings
{ input: "'It''s okay'", tokenType: [TokenType.STRING] }, {input: "'It''s okay'", tokenType: [TokenType.STRING]},
{ {
input: "format('{0} == ''queued''', needs)", input: "format('{0} == ''queued''', needs)",
tokenType: [ tokenType: [
@@ -34,34 +34,28 @@ describe("lexer", () => {
TokenType.STRING, TokenType.STRING,
TokenType.COMMA, TokenType.COMMA,
TokenType.IDENTIFIER, TokenType.IDENTIFIER,
TokenType.RIGHT_PAREN, TokenType.RIGHT_PAREN
], ]
}, },
// Arrays // Arrays
{ {
input: "[1,2]", input: "[1,2]",
tokenType: [ tokenType: [TokenType.LEFT_BRACKET, TokenType.NUMBER, TokenType.COMMA, TokenType.NUMBER, TokenType.RIGHT_BRACKET]
TokenType.LEFT_BRACKET,
TokenType.NUMBER,
TokenType.COMMA,
TokenType.NUMBER,
TokenType.RIGHT_BRACKET,
],
}, },
// Simple expressions // Simple expressions
{ {
input: "1 == 2", input: "1 == 2",
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER], tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
}, },
{ {
input: "1== 1", input: "1== 1",
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER], tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER]
}, },
{ {
input: "1< 1", input: "1< 1",
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER], tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER]
}, },
// Identifiers // Identifiers
@@ -73,9 +67,9 @@ describe("lexer", () => {
lexeme: "github", lexeme: "github",
pos: { pos: {
line: 0, line: 0,
column: 0, column: 0
}, }
}, }
}, },
// Keywords // Keywords
@@ -87,9 +81,9 @@ describe("lexer", () => {
lexeme: "true", lexeme: "true",
pos: { pos: {
line: 0, line: 0,
column: 0, column: 0
}, }
}, }
}, },
{ {
@@ -100,9 +94,9 @@ describe("lexer", () => {
lexeme: "false", lexeme: "false",
pos: { pos: {
line: 0, line: 0,
column: 0, column: 0
}, }
}, }
}, },
{ {
@@ -113,32 +107,21 @@ describe("lexer", () => {
lexeme: "null", lexeme: "null",
pos: { pos: {
line: 0, line: 0,
column: 0, column: 0
}, }
}, }
}, }
]; ];
test.each(tests)( test.each(tests)("$input", ({input, tokenType, token}: {input: string; tokenType: TokenType[]; token?: Token}) => {
"$input", const l = new Lexer(input);
({
input,
tokenType,
token,
}: {
input: string;
tokenType: TokenType[];
token?: Token;
}) => {
const l = new Lexer(input);
const r = l.lex(); const r = l.lex();
const want = r.tokens.map((t) => t.type); const want = r.tokens.map(t => t.type);
tokenType.push(TokenType.EOF); tokenType.push(TokenType.EOF);
expect(want).toEqual(tokenType); expect(want).toEqual(tokenType);
} });
);
}); });
+17 -40
View File
@@ -1,5 +1,5 @@
import { StringData } from "./data"; import {StringData} from "./data";
import { MAX_EXPRESSION_LENGTH } from "./errors"; import {MAX_EXPRESSION_LENGTH} from "./errors";
export enum TokenType { export enum TokenType {
UNKNOWN, UNKNOWN,
@@ -28,7 +28,7 @@ export enum TokenType {
FALSE, FALSE,
NULL, NULL,
EOF, EOF
} }
export type Pos = { export type Pos = {
@@ -104,7 +104,6 @@ export class Lexer {
this.previous() != TokenType.RIGHT_BRACKET && this.previous() != TokenType.RIGHT_BRACKET &&
this.previous() != TokenType.RIGHT_PAREN && this.previous() != TokenType.RIGHT_PAREN &&
this.previous() != TokenType.STAR this.previous() != TokenType.STAR
) { ) {
this.consumeNumber(); this.consumeNumber();
} else { } else {
@@ -118,9 +117,7 @@ export class Lexer {
break; break;
case "!": case "!":
this.addToken( this.addToken(this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG);
this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG
);
break; break;
case "=": case "=":
@@ -134,15 +131,11 @@ export class Lexer {
break; break;
case "<": case "<":
this.addToken( this.addToken(this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS);
this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS
);
break; break;
case ">": case ">":
this.addToken( this.addToken(this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER);
this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER
);
break; break;
case "&": case "&":
@@ -199,18 +192,18 @@ export class Lexer {
this.tokens.push({ this.tokens.push({
type: TokenType.EOF, type: TokenType.EOF,
lexeme: "", lexeme: "",
pos: this.pos(), pos: this.pos()
}); });
return { return {
tokens: this.tokens, tokens: this.tokens
}; };
} }
private pos(): Pos { private pos(): Pos {
return { return {
line: this.line, line: this.line,
column: this.start - this.lastLineOffset, column: this.start - this.lastLineOffset
}; };
} }
@@ -268,7 +261,7 @@ export class Lexer {
type, type,
lexeme: this.input.substring(this.start, this.offset), lexeme: this.input.substring(this.start, this.offset),
pos: this.pos(), pos: this.pos(),
value, value
}); });
} }
@@ -282,9 +275,7 @@ export class Lexer {
if (isNaN(value)) { if (isNaN(value)) {
throw new Error( throw new Error(
`Unexpected symbol: '${lexeme}'. Located at position ${ `Unexpected symbol: '${lexeme}'. Located at position ${this.start + 1} within expression: ${this.input}`
this.start + 1
} within expression: ${this.input}`
); );
} }
@@ -304,11 +295,9 @@ export class Lexer {
if (this.atEnd()) { if (this.atEnd()) {
// Unterminated string // Unterminated string
throw new Error( throw new Error(
`Unexpected symbol: '${this.input.substring( `Unexpected symbol: '${this.input.substring(this.start)}'. Located at position ${
this.start this.start + 1
)}'. Located at position ${this.start + 1} within expression: ${ } within expression: ${this.input}`
this.input
}`
); );
} }
@@ -360,9 +349,7 @@ export class Lexer {
if (!isLegalIdentifier(lexeme)) { if (!isLegalIdentifier(lexeme)) {
throw new Error( throw new Error(
`Unexpected symbol: '${lexeme}'. Located at position ${ `Unexpected symbol: '${lexeme}'. Located at position ${this.start + 1} within expression: ${this.input}`
this.start + 1
} within expression: ${this.input}`
); );
} }
@@ -400,19 +387,9 @@ function isLegalIdentifier(str: string): boolean {
} }
const first = str[0]; const first = str[0];
if ( if ((first >= "a" && first <= "z") || (first >= "A" && first <= "Z") || first == "_") {
(first >= "a" && first <= "z") ||
(first >= "A" && first <= "Z") ||
first == "_"
) {
for (const c of str.substring(1).split("")) { for (const c of str.substring(1).split("")) {
if ( if ((c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || (c >= "0" && c <= "9") || c == "_" || c == "-") {
(c >= "a" && c <= "z") ||
(c >= "A" && c <= "Z") ||
(c >= "0" && c <= "9") ||
c == "_" ||
c == "-"
) {
// OK // OK
} else { } else {
return false; return false;
+17 -58
View File
@@ -1,20 +1,9 @@
import { import {Binary, ContextAccess, Expr, FunctionCall, Grouping, IndexAccess, Literal, Logical, Star, Unary} from "./ast";
Binary,
ContextAccess,
Expr,
FunctionCall,
Grouping,
IndexAccess,
Literal,
Logical,
Star,
Unary,
} from "./ast";
import * as data from "./data"; import * as data from "./data";
import { ErrorType, ExpressionError, MAX_PARSER_DEPTH } from "./errors"; import {ErrorType, ExpressionError, MAX_PARSER_DEPTH} from "./errors";
import { ParseContext, validateFunction } from "./funcs"; import {ParseContext, validateFunction} from "./funcs";
import { FunctionInfo } from "./funcs/info"; import {FunctionInfo} from "./funcs/info";
import { Token, TokenType } from "./lexer"; import {Token, TokenType} from "./lexer";
export class Parser { export class Parser {
private extContexts: Map<string, boolean>; private extContexts: Map<string, boolean>;
@@ -32,11 +21,7 @@ export class Parser {
* @param extensionContexts Available context names * @param extensionContexts Available context names
* @param extensionFunctions Available functions (beyond the built-in ones) * @param extensionFunctions Available functions (beyond the built-in ones)
*/ */
constructor( constructor(private tokens: Token[], extensionContexts: string[], extensionFunctions: FunctionInfo[]) {
private tokens: Token[],
extensionContexts: string[],
extensionFunctions: FunctionInfo[]
) {
this.extContexts = new Map<string, boolean>(); this.extContexts = new Map<string, boolean>();
this.extFuncs = new Map(); this.extFuncs = new Map();
@@ -44,9 +29,9 @@ export class Parser {
this.extContexts.set(contextName.toLowerCase(), true); this.extContexts.set(contextName.toLowerCase(), true);
} }
for (const { name, func } of extensionFunctions.map((x) => ({ for (const {name, func} of extensionFunctions.map(x => ({
name: x.name, name: x.name,
func: x, func: x
}))) { }))) {
this.extFuncs.set(name.toLowerCase(), func); this.extFuncs.set(name.toLowerCase(), func);
} }
@@ -54,7 +39,7 @@ export class Parser {
this.context = { this.context = {
allowUnknownKeywords: false, allowUnknownKeywords: false,
extensionContexts: this.extContexts, extensionContexts: this.extContexts,
extensionFunctions: this.extFuncs, extensionFunctions: this.extFuncs
}; };
} }
@@ -65,7 +50,7 @@ export class Parser {
if (this.atEnd()) { if (this.atEnd()) {
return result; return result;
} }
result = this.expression(); result = this.expression();
if (!this.atEnd()) { if (!this.atEnd()) {
@@ -151,14 +136,7 @@ export class Parser {
// ! is higher precedence than >, >=, <, <= // ! is higher precedence than >, >=, <, <=
let expr = this.unary(); let expr = this.unary();
while ( while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
this.match(
TokenType.GREATER,
TokenType.GREATER_EQUAL,
TokenType.LESS,
TokenType.LESS_EQUAL
)
) {
const operator = this.previous(); const operator = this.previous();
const right = this.unary(); const right = this.unary();
@@ -191,11 +169,7 @@ export class Parser {
let depthIncreased = 0; let depthIncreased = 0;
if ( if (expr instanceof Grouping || expr instanceof FunctionCall || expr instanceof ContextAccess) {
expr instanceof Grouping ||
expr instanceof FunctionCall ||
expr instanceof ContextAccess
) {
let cont = true; let cont = true;
while (cont) { while (cont) {
switch (true) { switch (true) {
@@ -207,10 +181,7 @@ export class Parser {
indexExpr = this.expression(); indexExpr = this.expression();
} }
this.consume( this.consume(TokenType.RIGHT_BRACKET, ErrorType.ErrorUnexpectedSymbol);
TokenType.RIGHT_BRACKET,
ErrorType.ErrorUnexpectedSymbol
);
// Track depth // Track depth
this.incrDepth(); this.incrDepth();
@@ -225,17 +196,11 @@ export class Parser {
if (this.match(TokenType.IDENTIFIER)) { if (this.match(TokenType.IDENTIFIER)) {
let property = this.previous(); let property = this.previous();
expr = new IndexAccess( expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme)));
expr,
new Literal(new data.StringData(property.lexeme))
);
} else if (this.match(TokenType.STAR)) { } else if (this.match(TokenType.STAR)) {
expr = new IndexAccess(expr, new Star()); expr = new IndexAccess(expr, new Star());
} else { } else {
throw this.buildError( throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
ErrorType.ErrorUnexpectedSymbol,
this.peek()
);
} }
break; break;
@@ -307,20 +272,14 @@ export class Parser {
const expr = this.expression(); const expr = this.expression();
if (this.atEnd()) { if (this.atEnd()) {
throw this.buildError( throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
ErrorType.ErrorUnexpectedEndOfExpression,
this.previous()
); // Back up to get the last token before the EOF
} }
this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol); this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol);
return new Grouping(expr); return new Grouping(expr);
case this.atEnd(): case this.atEnd():
throw this.buildError( throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
ErrorType.ErrorUnexpectedEndOfExpression,
this.previous()
); // Back up to get the last token before the EOF
} }
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek()); throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
+34 -37
View File
@@ -1,5 +1,5 @@
import { BooleanData, ExpressionData, NumberData, StringData } from "./data"; import {BooleanData, ExpressionData, NumberData, StringData} from "./data";
import { coerceTypes, toUpperSpecial } from "./result"; import {coerceTypes, toUpperSpecial} from "./result";
describe("coerceTypes", () => { describe("coerceTypes", () => {
const tests: { const tests: {
@@ -13,52 +13,52 @@ describe("coerceTypes", () => {
}[] = [ }[] = [
{ {
name: "number-bool", name: "number-bool",
data: { l: new NumberData(1), r: new BooleanData(true) }, data: {l: new NumberData(1), r: new BooleanData(true)},
wantL: new NumberData(1), wantL: new NumberData(1),
wantR: new NumberData(1), wantR: new NumberData(1)
}, },
{ {
name: "number-bool-false", name: "number-bool-false",
data: { l: new NumberData(1), r: new BooleanData(false) }, data: {l: new NumberData(1), r: new BooleanData(false)},
wantL: new NumberData(1), wantL: new NumberData(1),
wantR: new NumberData(0), wantR: new NumberData(0)
}, },
{ {
name: "bool-number-false", name: "bool-number-false",
data: { l: new BooleanData(false), r: new NumberData(1) }, data: {l: new BooleanData(false), r: new NumberData(1)},
wantL: new NumberData(0), wantL: new NumberData(0),
wantR: new NumberData(1), wantR: new NumberData(1)
}, },
{ {
name: "number-number", name: "number-number",
data: { l: new NumberData(1), r: new NumberData(2) }, data: {l: new NumberData(1), r: new NumberData(2)},
wantL: new NumberData(1), wantL: new NumberData(1),
wantR: new NumberData(2), wantR: new NumberData(2)
}, },
{ {
name: "string-string", name: "string-string",
data: { l: new StringData("a"), r: new StringData("b") }, data: {l: new StringData("a"), r: new StringData("b")},
wantL: new StringData("a"), wantL: new StringData("a"),
wantR: new StringData("b"), wantR: new StringData("b")
}, },
{ {
name: "string-number", name: "string-number",
data: { l: new StringData("a"), r: new NumberData(1) }, data: {l: new StringData("a"), r: new NumberData(1)},
wantL: new NumberData(NaN), wantL: new NumberData(NaN),
wantR: new NumberData(1), wantR: new NumberData(1)
}, },
{ {
name: "number-string", name: "number-string",
data: { l: new NumberData(1), r: new StringData("a") }, data: {l: new NumberData(1), r: new StringData("a")},
wantL: new NumberData(1), wantL: new NumberData(1),
wantR: new NumberData(NaN), wantR: new NumberData(NaN)
}, },
{ {
name: "bool-bool", name: "bool-bool",
data: { l: new BooleanData(false), r: new BooleanData(true) }, data: {l: new BooleanData(false), r: new BooleanData(true)},
wantL: new BooleanData(false), wantL: new BooleanData(false),
wantR: new BooleanData(true), wantR: new BooleanData(true)
}, }
]; ];
test.each(tests)( test.each(tests)(
@@ -66,9 +66,9 @@ describe("coerceTypes", () => {
({ ({
data, data,
wantL, wantL,
wantR, wantR
}: { }: {
data: { l: ExpressionData; r: ExpressionData }; data: {l: ExpressionData; r: ExpressionData};
wantL: ExpressionData; wantL: ExpressionData;
wantR: ExpressionData; wantR: ExpressionData;
}) => { }) => {
@@ -80,22 +80,19 @@ describe("coerceTypes", () => {
}); });
describe("toUpperSpecial", () => { describe("toUpperSpecial", () => {
const tests: { input: string; want: string }[] = [ const tests: {input: string; want: string}[] = [
{ input: "", want: "" }, {input: "", want: ""},
{ input: "abc", want: "ABC" }, {input: "abc", want: "ABC"},
{ input: "ıabc", want: "ıABC" }, {input: "ıabc", want: "ıABC"},
{ input: "ııabc", want: "ııABC" }, {input: "ııabc", want: "ııABC"},
{ input: "abcı", want: "ABCı" }, {input: "abcı", want: "ABCı"},
{ input: "abcıı", want: "ABCıı" }, {input: "abcıı", want: "ABCıı"},
{ input: "abcıdef", want: "ABCıDEF" }, {input: "abcıdef", want: "ABCıDEF"},
{ input: "abcııdef", want: "ABCııDEF" }, {input: "abcııdef", want: "ABCııDEF"},
{ input: "abcıdefıghi", want: "ABCıDEFıGHI" }, {input: "abcıdefıghi", want: "ABCıDEFıGHI"}
]; ];
test.each(tests)( test.each(tests)("$input", ({input, want}: {input: string; want: string}) => {
"$input", expect(toUpperSpecial(input)).toEqual(want);
({ input, want }: { input: string; want: string }) => { });
expect(toUpperSpecial(input)).toEqual(want);
}
);
}); });
+3 -12
View File
@@ -76,10 +76,7 @@ export function coerceTypes(
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3. // Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives. // Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
export function equals( export function equals(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
lhs: data.ExpressionData,
rhs: data.ExpressionData
): boolean {
let [lv, rv] = coerceTypes(lhs, rhs); let [lv, rv] = coerceTypes(lhs, rhs);
if (lv.kind != rv.kind) { if (lv.kind != rv.kind) {
@@ -125,10 +122,7 @@ export function equals(
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3. // Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives. // Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
export function greaterThan( export function greaterThan(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
lhs: data.ExpressionData,
rhs: data.ExpressionData
): boolean {
const [lv, rv] = coerceTypes(lhs, rhs); const [lv, rv] = coerceTypes(lhs, rhs);
if (lv.kind != rv.kind) { if (lv.kind != rv.kind) {
@@ -166,10 +160,7 @@ export function greaterThan(
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3. // Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
// Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives. // Except string comparison is OrdinalIgnoreCase, and objects are not coerced to primitives.
export function lessThan( export function lessThan(lhs: data.ExpressionData, rhs: data.ExpressionData): boolean {
lhs: data.ExpressionData,
rhs: data.ExpressionData
): boolean {
const [lv, rv] = coerceTypes(lhs, rhs); const [lv, rv] = coerceTypes(lhs, rhs);
if (lv.kind != rv.kind) { if (lv.kind != rv.kind) {
+22 -42
View File
@@ -1,14 +1,14 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { Expr } from "./ast"; import {Expr} from "./ast";
import * as data from "./data"; import * as data from "./data";
import { kindStr } from "./data/expressiondata"; import {kindStr} from "./data/expressiondata";
import { replacer } from "./data/replacer"; import {replacer} from "./data/replacer";
import { reviver } from "./data/reviver"; import {reviver} from "./data/reviver";
import { ExpressionError } from "./errors"; import {ExpressionError} from "./errors";
import { Evaluator } from "./evaluator"; import {Evaluator} from "./evaluator";
import { Lexer, Result } from "./lexer"; import {Lexer, Result} from "./lexer";
import { Parser } from "./parser"; import {Parser} from "./parser";
interface TestResult { interface TestResult {
value: data.ExpressionData; value: data.ExpressionData;
@@ -18,11 +18,11 @@ interface TestResult {
enum TestErrorKind { enum TestErrorKind {
Lexing = "lexing", Lexing = "lexing",
Parsing = "parsing", Parsing = "parsing",
Evaluation = "evaluation", Evaluation = "evaluation"
} }
enum SkipOptionKind { enum SkipOptionKind {
Typescript = "typescript", Typescript = "typescript"
} }
interface TestOptions { interface TestOptions {
@@ -83,9 +83,7 @@ describe("x-lang tests", () => {
let tests = testCases[testCaseName]; let tests = testCases[testCaseName];
// Filter out tests that are not supported by typescript // Filter out tests that are not supported by typescript
tests = tests.filter( tests = tests.filter(t => !t.options?.skip?.includes(SkipOptionKind.Typescript));
(t) => !t.options?.skip?.includes(SkipOptionKind.Typescript)
);
const testName = path.basename(file, ".json"); const testName = path.basename(file, ".json");
@@ -94,7 +92,7 @@ describe("x-lang tests", () => {
} }
describe(testName, () => { describe(testName, () => {
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, (testCase) => { test.each(tests)(`${testName}: ${testCaseName} ($expr)`, testCase => {
if (!testCase.contexts) { if (!testCase.contexts) {
testCase.contexts = new data.Dictionary(); testCase.contexts = new data.Dictionary();
} }
@@ -114,37 +112,29 @@ describe("x-lang tests", () => {
return; return;
} }
throw new Error( throw new Error(`unexpected error lexing expression: ${e.message} ${e.stack}`);
`unexpected error lexing expression: ${e.message} ${e.stack}`
);
} }
// Parse // Parse
const contextNames = testCase.contexts.pairs().map((x) => x.key); const contextNames = testCase.contexts.pairs().map(x => x.key);
const parser = new Parser(result.tokens, contextNames, []); const parser = new Parser(result.tokens, contextNames, []);
let expr: Expr; let expr: Expr;
try { try {
expr = parser.parse(); expr = parser.parse();
if (testCase.err?.kind === TestErrorKind.Parsing) { if (testCase.err?.kind === TestErrorKind.Parsing) {
throw new Error( throw new Error("expected error parsing expression, but got none");
"expected error parsing expression, but got none"
);
} }
} catch (e: any) { } catch (e: any) {
// Did test expect parsing error? // Did test expect parsing error?
if (testCase.err?.kind === TestErrorKind.Parsing) { if (testCase.err?.kind === TestErrorKind.Parsing) {
// Test expects parsing error // Test expects parsing error
const pe = e as ExpressionError; const pe = e as ExpressionError;
expect(errorWithExpression(pe, testCase.expr)).toContain( expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
testCase.err.value
);
return; return;
} }
throw new Error( throw new Error(`unexpected error parsing expression: ${e.message} ${e.stack}`);
`unexpected error parsing expression: ${e.message} ${e.stack}`
);
} }
// Evaluate expression // Evaluate expression
@@ -158,28 +148,20 @@ describe("x-lang tests", () => {
} }
if (testCase.err?.kind === TestErrorKind.Evaluation) { if (testCase.err?.kind === TestErrorKind.Evaluation) {
throw new Error( throw new Error("expected error evaluating expression, but got none");
"expected error evaluating expression, but got none"
);
} }
expect(kindStr(result.kind)).toBe(testCase.result.kind); expect(kindStr(result.kind)).toBe(testCase.result.kind);
expect(JSON.stringify(result, replacer)).toEqual( expect(JSON.stringify(result, replacer)).toEqual(JSON.stringify(testCase.result.value, replacer));
JSON.stringify(testCase.result.value, replacer)
);
} catch (e: any) { } catch (e: any) {
if (testCase.err?.kind === TestErrorKind.Evaluation) { if (testCase.err?.kind === TestErrorKind.Evaluation) {
const pe = e as ExpressionError; const pe = e as ExpressionError;
expect(errorWithExpression(pe, testCase.expr)).toContain( expect(errorWithExpression(pe, testCase.expr)).toContain(testCase.err.value);
testCase.err.value
);
return; return;
} }
throw new Error( throw new Error(`unexpected error evaluating expression: ${e.message} ${e.stack}`);
`unexpected error evaluating expression: ${e.message} ${e.stack}`
);
} }
}); });
}); });
@@ -189,9 +171,7 @@ describe("x-lang tests", () => {
function errorWithExpression(e: ExpressionError, expr: string): string { function errorWithExpression(e: ExpressionError, expr: string): string {
if (e.pos !== undefined) { if (e.pos !== undefined) {
return `${e.message}. Located at position ${ return `${e.message}. Located at position ${e.pos.column + 1} within expression: ${expr}`;
e.pos.column + 1
} within expression: ${expr}`;
} }
return `${e.message}. Located within expression: ${expr}`; return `${e.message}. Located within expression: ${expr}`;
+4 -4
View File
@@ -30,12 +30,12 @@
"scripts": { "scripts": {
"build": "tsc --build tsconfig.build.json", "build": "tsc --build tsconfig.build.json",
"clean": "rimraf dist", "clean": "rimraf dist",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"prepublishOnly": "npm run build && npm run test", "prepublishOnly": "npm run build && npm run test",
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest", "test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch", "test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
"watch": "tsc --build tsconfig.build.json --watch", "watch": "tsc --build tsconfig.build.json --watch"
"prettier": "prettier .",
"prettier-fix": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@github/actions-languageservice": "^0.1.76", "@github/actions-languageservice": "^0.1.76",
@@ -60,4 +60,4 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }
-9
View File
@@ -1,9 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"printWidth": 120,
"singleQuote": false,
"bracketSpacing": false,
"trailingComma": "none",
"arrowParens": "avoid"
}
+4 -4
View File
@@ -30,12 +30,12 @@
"scripts": { "scripts": {
"build": "tsc --build tsconfig.build.json", "build": "tsc --build tsconfig.build.json",
"clean": "rimraf dist", "clean": "rimraf dist",
"format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'",
"prepublishOnly": "npm run build && npm run test", "prepublishOnly": "npm run build && npm run test",
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest", "test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch", "test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
"watch": "tsc --build tsconfig.build.json --watch", "watch": "tsc --build tsconfig.build.json --watch"
"prettier": "prettier .",
"prettier-fix": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@github/actions-expressions": "^0.1.76", "@github/actions-expressions": "^0.1.76",
@@ -59,4 +59,4 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }
-3
View File
@@ -1,3 +0,0 @@
/dist
/node_modules
/src/workflows/workflow-v1.0.json
-3
View File
@@ -1,3 +0,0 @@
{
"semi": false
}
+1 -1
View File
@@ -60,4 +60,4 @@
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"typescript": "^4.8.4" "typescript": "^4.8.4"
} }
} }
+72 -82
View File
@@ -1,7 +1,7 @@
import { StringToken } from "./templates/tokens" import {StringToken} from "./templates/tokens";
import { isBasicExpression, isString } from "./templates/tokens/type-guards" import {isBasicExpression, isString} from "./templates/tokens/type-guards";
import { nullTrace } from "./test-utils/null-trace" import {nullTrace} from "./test-utils/null-trace";
import { parseWorkflow } from "./workflows/workflow-parser" import {parseWorkflow} from "./workflows/workflow-parser";
describe("Workflow Expression Parsing", () => { describe("Workflow Expression Parsing", () => {
it("preserves original expressions when building format", () => { it("preserves original expressions when building format", () => {
@@ -16,33 +16,31 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: echo 'hello'`, - run: echo 'hello'`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(0) expect(result.context.errors.getErrors()).toHaveLength(0);
const run = result.value!.assertMapping("run")! const run = result.value!.assertMapping("run")!;
const runNameMapping = run.get(1)! const runNameMapping = run.get(1)!;
expect(runNameMapping?.key?.assertString("run-name key").value).toBe( expect(runNameMapping?.key?.assertString("run-name key").value).toBe("run-name");
"run-name"
)
const v = runNameMapping.value! const v = runNameMapping.value!;
expect(v).not.toBeUndefined() expect(v).not.toBeUndefined();
if (!isBasicExpression(v)) { if (!isBasicExpression(v)) {
throw new Error("expected run-name to be a basic expression") throw new Error("expected run-name to be a basic expression");
} }
expect(v.originalExpressions).toHaveLength(2) expect(v.originalExpressions).toHaveLength(2);
expect(v.originalExpressions!.map((x) => x.toDisplayString())).toEqual([ expect(v.originalExpressions!.map(x => x.toDisplayString())).toEqual([
"${{ github.event_name }}", "${{ github.event_name }}",
"${{ github.ref }}", "${{ github.ref }}"
]) ]);
}) });
it("preserves original expressions when building format for multi-line strings", () => { it("preserves original expressions when building format for multi-line strings", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -57,54 +55,49 @@ jobs:
steps: steps:
- run: | - run: |
echo \${{ github.event_name }} echo \${{ github.event_name }}
echo 'hello' \${{ github.ref }}`, echo 'hello' \${{ github.ref }}`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(0) expect(result.context.errors.getErrors()).toHaveLength(0);
const run = result.value!.assertMapping("run")! const run = result.value!.assertMapping("run")!;
const jobsMapping = run.get(1)! const jobsMapping = run.get(1)!;
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs") expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
const job = jobsMapping const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
.value!.assertMapping("jobs")!
.get(0)!
.value!.assertMapping("job")
const stepRun = job const stepRun = job
.get(1) .get(1)
.value!.assertSequence("steps") .value!.assertSequence("steps")
.get(0) .get(0)
.assertMapping("step") .assertMapping("step")
.get(0) .get(0)
.value!.assertScalar("step-run") .value!.assertScalar("step-run");
if (!isBasicExpression(stepRun)) { if (!isBasicExpression(stepRun)) {
throw new Error("expected run-name to be a basic expression") throw new Error("expected run-name to be a basic expression");
} }
expect(stepRun.originalExpressions).toHaveLength(2) expect(stepRun.originalExpressions).toHaveLength(2);
expect( expect(stepRun.originalExpressions!.map(x => [x.toDisplayString(), x.range])).toEqual([
stepRun.originalExpressions!.map((x) => [x.toDisplayString(), x.range])
).toEqual([
[ [
"${{ github.event_name }}", "${{ github.event_name }}",
{ {
start: [7, 16], start: [7, 16],
end: [7, 40], end: [7, 40]
}, }
], ],
[ [
"${{ github.ref }}", "${{ github.ref }}",
{ {
start: [8, 24], start: [8, 24],
end: [8, 41], end: [8, 41]
}, }
], ]
]) ]);
}) });
it("return errors and string token with preserved expressions for (multiple) expression errors", () => { it("return errors and string token with preserved expressions for (multiple) expression errors", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -119,33 +112,30 @@ jobs:
steps: steps:
- run: | - run: |
echo \${{ abc }} echo \${{ abc }}
echo 'hello' \${{ gith }}`, echo 'hello' \${{ gith }}`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(2) expect(result.context.errors.getErrors()).toHaveLength(2);
const run = result.value!.assertMapping("run")! const run = result.value!.assertMapping("run")!;
const jobsMapping = run.get(1)! const jobsMapping = run.get(1)!;
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs") expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs");
const job = jobsMapping const job = jobsMapping.value!.assertMapping("jobs")!.get(0)!.value!.assertMapping("job");
.value!.assertMapping("jobs")!
.get(0)!
.value!.assertMapping("job")
const stepRun = job const stepRun = job
.get(1) .get(1)
.value!.assertSequence("steps") .value!.assertSequence("steps")
.get(0) .get(0)
.assertMapping("step") .assertMapping("step")
.get(0) .get(0)
.value!.assertScalar("step-run") .value!.assertScalar("step-run");
expect(isString(stepRun)).toBe(true) expect(isString(stepRun)).toBe(true);
expect((stepRun as StringToken).value).toContain("${{") expect((stepRun as StringToken).value).toContain("${{");
}) });
it("reports all errors for multi-line expressions at the correct locations", () => { it("reports all errors for multi-line expressions at the correct locations", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -160,31 +150,31 @@ jobs:
steps: steps:
- run: | - run: |
echo \${{ fromJSON2('test') }} echo \${{ fromJSON2('test') }}
echo 'hello' \${{ toJSON2(inputs.test) }}`, echo 'hello' \${{ toJSON2(inputs.test) }}`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toEqual([ expect(result.context.errors.getErrors()).toEqual([
{ {
prefix: "test.yaml (Line: 6, Col: 14)", prefix: "test.yaml (Line: 6, Col: 14)",
range: { range: {
start: [7, 16], start: [7, 16],
end: [7, 40], end: [7, 40]
}, },
rawMessage: "Unrecognized function: 'fromJSON2'", rawMessage: "Unrecognized function: 'fromJSON2'"
}, },
{ {
prefix: "test.yaml (Line: 6, Col: 14)", prefix: "test.yaml (Line: 6, Col: 14)",
range: { range: {
start: [8, 24], start: [8, 24],
end: [8, 51], end: [8, 51]
}, },
rawMessage: "Unrecognized function: 'toJSON2'", rawMessage: "Unrecognized function: 'toJSON2'"
}, }
]) ]);
}) });
it("parses isExpression strings into expression tokens", () => { it("parses isExpression strings into expression tokens", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -198,22 +188,22 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' if: github.event_name == 'push'
steps: steps:
- run: echo 'hello'`, - run: echo 'hello'`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(0) expect(result.context.errors.getErrors()).toHaveLength(0);
const workflowRoot = result.value!.assertMapping("root")! const workflowRoot = result.value!.assertMapping("root")!;
const jobs = workflowRoot.get(1).value.assertMapping("jobs") const jobs = workflowRoot.get(1).value.assertMapping("jobs");
const build = jobs.get(0).value.assertMapping("job") const build = jobs.get(0).value.assertMapping("job");
const ifToken = build.get(1).value const ifToken = build.get(1).value;
expect(ifToken.toString()).toEqual("${{ github.event_name == 'push' }}") expect(ifToken.toString()).toEqual("${{ github.event_name == 'push' }}");
if (!isBasicExpression(ifToken)) { if (!isBasicExpression(ifToken)) {
throw new Error("expected if to be a basic expression") throw new Error("expected if to be a basic expression");
} }
}) });
}) });
+45 -54
View File
@@ -1,6 +1,6 @@
import { TemplateValidationError } from "./templates/template-validation-error" import {TemplateValidationError} from "./templates/template-validation-error";
import { nullTrace } from "./test-utils/null-trace" import {nullTrace} from "./test-utils/null-trace";
import { parseWorkflow } from "./workflows/workflow-parser" import {parseWorkflow} from "./workflows/workflow-parser";
describe("parseWorkflow", () => { describe("parseWorkflow", () => {
it("parses valid workflow", () => { it("parses valid workflow", () => {
@@ -9,15 +9,14 @@ describe("parseWorkflow", () => {
[ [
{ {
name: "test.yaml", name: "test.yaml",
content: content: "on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'", }
},
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(0) expect(result.context.errors.getErrors()).toHaveLength(0);
}) });
it("contains range for error", () => { it("contains range for error", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -25,25 +24,19 @@ describe("parseWorkflow", () => {
[ [
{ {
name: "test.yaml", name: "test.yaml",
content: content: "on: push\njobs:\n build:\n steps:\n - run: echo 'hello'"
"on: push\njobs:\n build:\n steps:\n - run: echo 'hello'", }
},
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toEqual([ expect(result.context.errors.getErrors()).toEqual([
new TemplateValidationError( new TemplateValidationError("Required property is missing: runs-on", "test.yaml (Line: 4, Col: 5)", undefined, {
"Required property is missing: runs-on", start: [4, 5],
"test.yaml (Line: 4, Col: 5)", end: [5, 24]
undefined, })
{ ]);
start: [4, 5], });
end: [5, 24],
}
),
])
})
it("error range for expression is constrained to scalar node", () => { it("error range for expression is constrained to scalar node", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -57,11 +50,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: \${{ github.event = 12 }} - name: \${{ github.event = 12 }}
run: echo 'hello'`, run: echo 'hello'`
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toEqual([ expect(result.context.errors.getErrors()).toEqual([
new TemplateValidationError( new TemplateValidationError(
@@ -70,11 +63,11 @@ jobs:
undefined, undefined,
{ {
start: [6, 13], start: [6, 13],
end: [6, 37], end: [6, 37]
} }
), )
]) ]);
}) });
it("tokens contain descriptions", () => { it("tokens contain descriptions", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -83,39 +76,37 @@ jobs:
{ {
name: "test.yaml", name: "test.yaml",
content: content:
"on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'", "on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'"
}, }
], ],
nullTrace nullTrace
) );
expect(result.context.errors.getErrors()).toHaveLength(0) expect(result.context.errors.getErrors()).toHaveLength(0);
expect(result.value).not.toBeUndefined() expect(result.value).not.toBeUndefined();
const root = result.value!.assertMapping("root") const root = result.value!.assertMapping("root");
expect(root.description).toBe("Workflow file with strict validation") expect(root.description).toBe("Workflow file with strict validation");
for (const pair of root) { for (const pair of root) {
const key = pair.key.assertString("key").value const key = pair.key.assertString("key").value;
switch (key) { switch (key) {
case "name": { case "name": {
const nameKey = pair.key.assertString("name") const nameKey = pair.key.assertString("name");
expect(nameKey.definition).not.toBeUndefined() expect(nameKey.definition).not.toBeUndefined();
expect(nameKey.description).toBe("The name of the workflow.") expect(nameKey.description).toBe("The name of the workflow.");
break break;
} }
case "on": { case "on": {
const onKey = pair.key.assertString("on") const onKey = pair.key.assertString("on");
const onValue = pair.value.assertString("push") const onValue = pair.value.assertString("push");
expect(onKey.definition).not.toBeUndefined() expect(onKey.definition).not.toBeUndefined();
expect(onKey.description).toBe( expect(onKey.description).toBe(
"The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows." "The name of the GitHub event that triggers the workflow. You can provide a single event string, array of events, array of event types, or an event configuration map that schedules a workflow or restricts the execution of a workflow to specific files, tags, or branch changes. For a list of available events, see https://help.github.com/en/github/automating-your-workflow-with-github-actions/events-that-trigger-workflows."
) );
expect(onValue.definition).not.toBeUndefined() expect(onValue.definition).not.toBeUndefined();
expect(onValue.description).toBe( expect(onValue.description).toBe("Runs your workflow when you push a commit or tag.");
"Runs your workflow when you push a commit or tag." break;
)
break
} }
} }
} }
}) });
}) });
+5 -5
View File
@@ -1,5 +1,5 @@
export { convertWorkflowTemplate } from "./model/convert" export {convertWorkflowTemplate} from "./model/convert";
export { WorkflowTemplate } from "./model/workflow-template" export {WorkflowTemplate} from "./model/workflow-template";
export * from "./templates/tokens/type-guards" export * from "./templates/tokens/type-guards";
export { NoOperationTraceWriter, TraceWriter } from "./templates/trace-writer" export {NoOperationTraceWriter, TraceWriter} from "./templates/trace-writer";
export { parseWorkflow, ParseWorkflowResult } from "./workflows/workflow-parser" export {parseWorkflow, ParseWorkflowResult} from "./workflows/workflow-parser";
+92 -117
View File
@@ -1,9 +1,9 @@
import { nullTrace } from "../test-utils/null-trace" import {nullTrace} from "../test-utils/null-trace";
import { parseWorkflow } from "../workflows/workflow-parser" import {parseWorkflow} from "../workflows/workflow-parser";
import { convertWorkflowTemplate, ErrorPolicy } from "./convert" import {convertWorkflowTemplate, ErrorPolicy} from "./convert";
function serializeTemplate(template: unknown): unknown { function serializeTemplate(template: unknown): unknown {
return JSON.parse(JSON.stringify(template)) return JSON.parse(JSON.stringify(template));
} }
describe("convertWorkflowTemplate", () => { describe("convertWorkflowTemplate", () => {
@@ -16,39 +16,35 @@ describe("convertWorkflowTemplate", () => {
content: `on: push content: `on: push
jobs: jobs:
build: build:
runs-on: ubuntu-latest`, runs-on: ubuntu-latest`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
events: { events: {
push: {}, push: {}
}, },
jobs: [ jobs: [
{ {
id: "build", id: "build",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "build", name: "build",
needs: undefined, needs: undefined,
outputs: undefined, outputs: undefined,
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, }
], ]
}) });
}) });
it("converts workflow if expressions", () => { it("converts workflow if expressions", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -63,48 +59,44 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
deploy: deploy:
if: true if: true
runs-on: ubuntu-latest`, runs-on: ubuntu-latest`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
events: { events: {
push: {}, push: {}
}, },
jobs: [ jobs: [
{ {
id: "build", id: "build",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "build", name: "build",
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, },
{ {
id: "deploy", id: "deploy",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "deploy", name: "deploy",
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, }
], ]
}) });
}) });
it("converts workflow with empty needs", () => { it("converts workflow with empty needs", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -116,44 +108,40 @@ jobs:
jobs: jobs:
build: build:
needs: # comment to preserve whitespace in test needs: # comment to preserve whitespace in test
runs-on: ubuntu-latest`, runs-on: ubuntu-latest`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
errors: [ errors: [
{ {
Message: "wf.yaml (Line: 4, Col: 12): Unexpected value ''", Message: "wf.yaml (Line: 4, Col: 12): Unexpected value ''"
}, }
], ],
events: { events: {
push: {}, push: {}
}, },
jobs: [ jobs: [
{ {
id: "build", id: "build",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "build", name: "build",
needs: [], needs: [],
outputs: undefined, outputs: undefined,
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, }
], ]
}) });
}) });
it("converts workflow with needs errors", () => { it("converts workflow with needs errors", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -170,75 +158,70 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
job3: job3:
needs: job1 needs: job1
runs-on: ubuntu-latest`, runs-on: ubuntu-latest`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
errors: [ errors: [
{ {
Message: Message: "wf.yaml (Line: 4, Col: 13): Job 'job1' depends on unknown job 'unknown-job'."
"wf.yaml (Line: 4, Col: 13): Job 'job1' depends on unknown job 'unknown-job'.",
}, },
{ {
Message: Message:
"wf.yaml (Line: 4, Col: 26): Job 'job1' depends on job 'job3' which creates a cycle in the dependency graph.", "wf.yaml (Line: 4, Col: 26): Job 'job1' depends on job 'job3' which creates a cycle in the dependency graph."
}, },
{ {
Message: Message:
"wf.yaml (Line: 9, Col: 12): Job 'job3' depends on job 'job1' which creates a cycle in the dependency graph.", "wf.yaml (Line: 9, Col: 12): Job 'job3' depends on job 'job1' which creates a cycle in the dependency graph."
}, }
], ],
events: { events: {
push: {}, push: {}
}, },
jobs: [ jobs: [
{ {
id: "job1", id: "job1",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "job1", name: "job1",
needs: ["unknown-job", "job3"], needs: ["unknown-job", "job3"],
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, },
{ {
id: "job2", id: "job2",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "job2", name: "job2",
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, },
{ {
id: "job3", id: "job3",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "job3", name: "job3",
needs: ["job1"], needs: ["job1"],
"runs-on": "ubuntu-latest", "runs-on": "ubuntu-latest",
steps: [], steps: [],
type: "job", type: "job"
}, }
], ]
}) });
}) });
it("converts workflow with invalid on", () => { it("converts workflow with invalid on", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -255,29 +238,25 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- run: echo hello`, - run: echo hello`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(template.jobs).not.toBeUndefined() expect(template.jobs).not.toBeUndefined();
expect(template.jobs).toHaveLength(1) expect(template.jobs).toHaveLength(1);
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
errors: [ errors: [
{ {
Message: "wf.yaml (Line: 5, Col: 18): Unexpected value '123'", Message: "wf.yaml (Line: 5, Col: 18): Unexpected value '123'"
}, },
{ {
Message: Message:
"wf.yaml (Line: 5, Col: 18): Unexpected type 'NumberToken' encountered while reading 'input options'. The type 'SequenceToken' was expected.", "wf.yaml (Line: 5, Col: 18): Unexpected type 'NumberToken' encountered while reading 'input options'. The type 'SequenceToken' was expected."
}, }
], ],
events: {}, events: {},
jobs: [ jobs: [
@@ -285,7 +264,7 @@ jobs:
id: "build", id: "build",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
name: "build", name: "build",
needs: undefined, needs: undefined,
@@ -296,16 +275,16 @@ jobs:
id: "__run", id: "__run",
if: { if: {
expr: "success()", expr: "success()",
type: 3, type: 3
}, },
run: "echo hello", run: "echo hello"
}, }
], ],
type: "job", type: "job"
}, }
], ]
}) });
}) });
it("converts workflow with invalid jobs", () => { it("converts workflow with invalid jobs", () => {
const result = parseWorkflow( const result = parseWorkflow(
@@ -315,34 +294,30 @@ jobs:
name: "wf.yaml", name: "wf.yaml",
content: `on: push content: `on: push
jobs: jobs:
build:`, build:`
}, }
], ],
nullTrace nullTrace
) );
const template = convertWorkflowTemplate( const template = convertWorkflowTemplate(result.context, result.value!, ErrorPolicy.TryConversion);
result.context,
result.value!,
ErrorPolicy.TryConversion
)
expect(template.jobs).not.toBeUndefined() expect(template.jobs).not.toBeUndefined();
expect(template.jobs).toHaveLength(0) expect(template.jobs).toHaveLength(0);
expect(serializeTemplate(template)).toEqual({ expect(serializeTemplate(template)).toEqual({
errors: [ errors: [
{ {
Message: "wf.yaml (Line: 3, Col: 9): Unexpected value ''", Message: "wf.yaml (Line: 3, Col: 9): Unexpected value ''"
}, },
{ {
Message: Message:
"wf.yaml (Line: 3, Col: 9): Unexpected type 'NullToken' encountered while reading 'job build'. The type 'MappingToken' was expected.", "wf.yaml (Line: 3, Col: 9): Unexpected type 'NullToken' encountered while reading 'job build'. The type 'MappingToken' was expected."
}, }
], ],
events: { events: {
push: {}, push: {}
}, },
jobs: [], jobs: []
}) });
}) });
}) });
+31 -43
View File
@@ -1,17 +1,14 @@
import { TemplateContext } from "../templates/template-context" import {TemplateContext} from "../templates/template-context";
import { import {TemplateToken, TemplateTokenError} from "../templates/tokens/template-token";
TemplateToken, import {convertConcurrency} from "./converter/concurrency";
TemplateTokenError, import {convertOn} from "./converter/events";
} from "../templates/tokens/template-token" import {handleTemplateTokenErrors} from "./converter/handle-errors";
import { convertConcurrency } from "./converter/concurrency" import {convertJobs} from "./converter/jobs";
import { convertOn } from "./converter/events" import {WorkflowTemplate} from "./workflow-template";
import { handleTemplateTokenErrors } from "./converter/handle-errors"
import { convertJobs } from "./converter/jobs"
import { WorkflowTemplate } from "./workflow-template"
export enum ErrorPolicy { export enum ErrorPolicy {
ReturnErrorsOnly, ReturnErrorsOnly,
TryConversion, TryConversion
} }
export function convertWorkflowTemplate( export function convertWorkflowTemplate(
@@ -19,62 +16,53 @@ export function convertWorkflowTemplate(
root: TemplateToken, root: TemplateToken,
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly
): WorkflowTemplate { ): WorkflowTemplate {
const result = {} as WorkflowTemplate const result = {} as WorkflowTemplate;
if ( if (context.errors.getErrors().length > 0 && errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
context.errors.getErrors().length > 0 && result.errors = context.errors.getErrors().map(x => ({
errorPolicy === ErrorPolicy.ReturnErrorsOnly Message: x.message
) { }));
result.errors = context.errors.getErrors().map((x) => ({ return result;
Message: x.message,
}))
return result
} }
try { try {
const rootMapping = root.assertMapping("root") const rootMapping = root.assertMapping("root");
for (const item of rootMapping) { for (const item of rootMapping) {
const key = item.key.assertString("root key") const key = item.key.assertString("root key");
switch (key.value) { switch (key.value) {
case "on": case "on":
result.events = handleTemplateTokenErrors(root, context, {}, () => result.events = handleTemplateTokenErrors(root, context, {}, () => convertOn(context, item.value));
convertOn(context, item.value) break;
)
break
case "jobs": case "jobs":
result.jobs = handleTemplateTokenErrors(root, context, [], () => result.jobs = handleTemplateTokenErrors(root, context, [], () => convertJobs(context, item.value));
convertJobs(context, item.value) break;
)
break
case "concurrency": case "concurrency":
handleTemplateTokenErrors(root, context, {}, () => handleTemplateTokenErrors(root, context, {}, () => convertConcurrency(context, item.value));
convertConcurrency(context, item.value) result.concurrency = item.value;
) break;
result.concurrency = item.value
break
case "env": case "env":
result.env = item.value result.env = item.value;
break break;
} }
} }
} catch (err) { } catch (err) {
if (err instanceof TemplateTokenError) { if (err instanceof TemplateTokenError) {
context.error(err.token, err) context.error(err.token, err);
} else { } else {
// Report error for the root node // Report error for the root node
context.error(root, err) context.error(root, err);
} }
} finally { } finally {
if (context.errors.getErrors().length > 0) { if (context.errors.getErrors().length > 0) {
result.errors = context.errors.getErrors().map((x) => ({ result.errors = context.errors.getErrors().map(x => ({
Message: x.message, Message: x.message
})) }));
} }
} }
return result return result;
} }
@@ -1,42 +1,35 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { TemplateToken } from "../../templates/tokens/template-token" import {TemplateToken} from "../../templates/tokens/template-token";
import { isString } from "../../templates/tokens/type-guards" import {isString} from "../../templates/tokens/type-guards";
import { ConcurrencySetting } from "../workflow-template" import {ConcurrencySetting} from "../workflow-template";
export function convertConcurrency( export function convertConcurrency(context: TemplateContext, token: TemplateToken): ConcurrencySetting {
context: TemplateContext, const result: ConcurrencySetting = {};
token: TemplateToken
): ConcurrencySetting {
const result: ConcurrencySetting = {}
if (token.isExpression) { if (token.isExpression) {
return result return result;
} }
if (isString(token)) { if (isString(token)) {
result.group = token result.group = token;
return result return result;
} }
const concurrencyProperty = token.assertMapping("concurrency group") const concurrencyProperty = token.assertMapping("concurrency group");
for (const property of concurrencyProperty) { for (const property of concurrencyProperty) {
const propertyName = property.key.assertString("concurrency group key") const propertyName = property.key.assertString("concurrency group key");
if (property.key.isExpression || property.value.isExpression) { if (property.key.isExpression || property.value.isExpression) {
continue continue;
} }
switch (propertyName.value) { switch (propertyName.value) {
case "group": case "group":
result.group = property.value.assertString("concurrency group") result.group = property.value.assertString("concurrency group");
break break;
case "cancel-in-progress": case "cancel-in-progress":
result.cancelInProgress = result.cancelInProgress = property.value.assertBoolean("cancel-in-progress").value;
property.value.assertBoolean("cancel-in-progress").value break;
break
default: default:
context.error( context.error(propertyName, `Invalid property name: ${propertyName.value}`);
propertyName,
`Invalid property name: ${propertyName.value}`
)
} }
} }
return result return result;
} }
@@ -1,124 +1,110 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { import {MappingToken, SequenceToken, StringToken, TemplateToken} from "../../templates/tokens";
MappingToken, import {isString} from "../../templates/tokens/type-guards";
SequenceToken, import {Container, Credential} from "../workflow-template";
StringToken,
TemplateToken,
} from "../../templates/tokens"
import { isString } from "../../templates/tokens/type-guards"
import { Container, Credential } from "../workflow-template"
export function convertToJobContainer( export function convertToJobContainer(context: TemplateContext, container: TemplateToken): Container | undefined {
context: TemplateContext, let image: StringToken | undefined;
container: TemplateToken let env: MappingToken | undefined;
): Container | undefined { let ports: SequenceToken | undefined;
let image: StringToken | undefined let volumes: SequenceToken | undefined;
let env: MappingToken | undefined let options: StringToken | undefined;
let ports: SequenceToken | undefined
let volumes: SequenceToken | undefined
let options: StringToken | undefined
// Skip validation for expressions for now to match // Skip validation for expressions for now to match
// behavior of the other parsers // behavior of the other parsers
for (const [_, token, __] of TemplateToken.traverse(container)) { for (const [_, token, __] of TemplateToken.traverse(container)) {
if (token.isExpression) { if (token.isExpression) {
return return;
} }
} }
if (isString(container)) { if (isString(container)) {
// Workflow uses shorthand syntax `container: image-name` // Workflow uses shorthand syntax `container: image-name`
image = container.assertString("container item") image = container.assertString("container item");
return { image: image } return {image: image};
} }
const mapping = container.assertMapping("container item") const mapping = container.assertMapping("container item");
if (mapping) if (mapping)
for (const item of mapping) { for (const item of mapping) {
const key = item.key.assertString("container item key") const key = item.key.assertString("container item key");
const value = item.value const value = item.value;
switch (key.value) { switch (key.value) {
case "image": case "image":
image = value.assertString("container image") image = value.assertString("container image");
break break;
case "credentials": case "credentials":
convertToJobCredentials(context, value) convertToJobCredentials(context, value);
break break;
case "env": case "env":
env = value.assertMapping("container env") env = value.assertMapping("container env");
for (const envItem of env) { for (const envItem of env) {
envItem.key.assertString("container env value") envItem.key.assertString("container env value");
} }
break break;
case "ports": case "ports":
ports = value.assertSequence("container ports") ports = value.assertSequence("container ports");
for (const port of ports) { for (const port of ports) {
port.assertString("container port") port.assertString("container port");
} }
break break;
case "volumes": case "volumes":
volumes = value.assertSequence("container volumes") volumes = value.assertSequence("container volumes");
for (const volume of volumes) { for (const volume of volumes) {
volume.assertString("container volume") volume.assertString("container volume");
} }
break break;
case "options": case "options":
options = value.assertString("container options") options = value.assertString("container options");
break break;
default: default:
context.error(key, `Unexpected container item key: ${key.value}`) context.error(key, `Unexpected container item key: ${key.value}`);
} }
} }
if (!image) { if (!image) {
context.error(container, "Container image cannot be empty") context.error(container, "Container image cannot be empty");
} else { } else {
return { image, env, ports, volumes, options } return {image, env, ports, volumes, options};
} }
} }
export function convertToJobServices( export function convertToJobServices(context: TemplateContext, services: TemplateToken): Container[] | undefined {
context: TemplateContext, const serviceList: Container[] = [];
services: TemplateToken
): Container[] | undefined {
const serviceList: Container[] = []
const mapping = services.assertMapping("services") const mapping = services.assertMapping("services");
for (const service of mapping) { for (const service of mapping) {
service.key.assertString("service key") service.key.assertString("service key");
const container = convertToJobContainer(context, service.value) const container = convertToJobContainer(context, service.value);
if (container) { if (container) {
serviceList.push(container) serviceList.push(container);
} }
} }
return serviceList return serviceList;
} }
function convertToJobCredentials( function convertToJobCredentials(context: TemplateContext, value: TemplateToken): Credential | undefined {
context: TemplateContext, const mapping = value.assertMapping("credentials");
value: TemplateToken
): Credential | undefined {
const mapping = value.assertMapping("credentials")
let username: StringToken | undefined let username: StringToken | undefined;
let password: StringToken | undefined let password: StringToken | undefined;
for (const item of mapping) { for (const item of mapping) {
const key = item.key.assertString("credentials item") const key = item.key.assertString("credentials item");
const value = item.value const value = item.value;
switch (key.value) { switch (key.value) {
case "username": case "username":
username = value.assertString("credentials username") username = value.assertString("credentials username");
break break;
case "password": case "password":
password = value.assertString("credentials password") password = value.assertString("credentials password");
break break;
default: default:
context.error(key, `credentials key ${key.value}`) context.error(key, `credentials key ${key.value}`);
} }
} }
return { username, password } return {username, password};
} }
@@ -1,14 +1,9 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { MappingToken } from "../../templates/tokens/mapping-token" import {MappingToken} from "../../templates/tokens/mapping-token";
import { SequenceToken } from "../../templates/tokens/sequence-token" import {SequenceToken} from "../../templates/tokens/sequence-token";
import { TemplateToken } from "../../templates/tokens/template-token" import {TemplateToken} from "../../templates/tokens/template-token";
import { import {isLiteral, isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
isLiteral, import {TokenType} from "../../templates/tokens/types";
isMapping,
isSequence,
isString,
} from "../../templates/tokens/type-guards"
import { TokenType } from "../../templates/tokens/types"
import { import {
BranchFilterConfig, BranchFilterConfig,
EventsConfig, EventsConfig,
@@ -16,56 +11,53 @@ import {
ScheduleConfig, ScheduleConfig,
TagFilterConfig, TagFilterConfig,
TypesFilterConfig, TypesFilterConfig,
WorkflowFilterConfig, WorkflowFilterConfig
} from "../workflow-template" } from "../workflow-template";
import { convertStringList } from "./string-list" import {convertStringList} from "./string-list";
import { convertEventWorkflowDispatchInputs } from "./workflow-dispatch" import {convertEventWorkflowDispatchInputs} from "./workflow-dispatch";
export function convertOn( export function convertOn(context: TemplateContext, token: TemplateToken): EventsConfig {
context: TemplateContext,
token: TemplateToken
): EventsConfig {
if (isLiteral(token)) { if (isLiteral(token)) {
const event = token.assertString("on") const event = token.assertString("on");
return { return {
[event.value]: {}, [event.value]: {}
} as EventsConfig } as EventsConfig;
} }
if (isSequence(token)) { if (isSequence(token)) {
const result = {} as EventsConfig const result = {} as EventsConfig;
for (const item of token) { for (const item of token) {
const event = item.assertString("on") const event = item.assertString("on");
result[event.value] = {} result[event.value] = {};
} }
return result return result;
} }
if (isMapping(token)) { if (isMapping(token)) {
const result = {} as EventsConfig const result = {} as EventsConfig;
for (const item of token) { for (const item of token) {
const eventKey = item.key.assertString("event name") const eventKey = item.key.assertString("event name");
const eventName = eventKey.value const eventName = eventKey.value;
if (item.value.templateTokenType === TokenType.Null) { if (item.value.templateTokenType === TokenType.Null) {
result[eventName] = {} result[eventName] = {};
continue continue;
} }
// Schedule is the only event that can be a sequence, handle that separately // Schedule is the only event that can be a sequence, handle that separately
if (eventName === "schedule") { if (eventName === "schedule") {
const scheduleToken = item.value.assertSequence(`event ${eventName}`) const scheduleToken = item.value.assertSequence(`event ${eventName}`);
result.schedule = convertSchedule(context, scheduleToken) result.schedule = convertSchedule(context, scheduleToken);
continue continue;
} }
// All other events are defined as mappings. During schema validation we already ensure that events // All other events are defined as mappings. During schema validation we already ensure that events
// receive only known keys, so here we can focus on the values and whether they are valid. // receive only known keys, so here we can focus on the values and whether they are valid.
const eventToken = item.value.assertMapping(`event ${eventName}`) const eventToken = item.value.assertMapping(`event ${eventName}`);
result[eventName] = { result[eventName] = {
...convertPatternFilter("branches", eventToken), ...convertPatternFilter("branches", eventToken),
@@ -74,105 +66,95 @@ export function convertOn(
...convertFilter("types", eventToken), ...convertFilter("types", eventToken),
...convertFilter("workflows", eventToken), ...convertFilter("workflows", eventToken),
// TODO - share input parsing for now, but workflow_call also needs outputs and secrets // TODO - share input parsing for now, but workflow_call also needs outputs and secrets
...convertEventWorkflowDispatchInputs(context, eventToken), ...convertEventWorkflowDispatchInputs(context, eventToken)
} };
} }
return result return result;
} }
context.error(token, "Invalid format for 'on'") context.error(token, "Invalid format for 'on'");
return {} return {};
} }
function convertPatternFilter< function convertPatternFilter<T extends BranchFilterConfig & TagFilterConfig & PathFilterConfig>(
T extends BranchFilterConfig & TagFilterConfig & PathFilterConfig name: "branches" | "tags" | "paths",
>(name: "branches" | "tags" | "paths", token: MappingToken): T { token: MappingToken
const result = {} as T ): T {
const result = {} as T;
for (const item of token) { for (const item of token) {
const key = item.key.assertString(`${name} filter key`) const key = item.key.assertString(`${name} filter key`);
switch (key.value) { switch (key.value) {
case name: case name:
if (isString(item.value)) { if (isString(item.value)) {
result[name] = [item.value.value] result[name] = [item.value.value];
} else { } else {
result[name] = convertStringList( result[name] = convertStringList(name, item.value.assertSequence(`${name} list`));
name,
item.value.assertSequence(`${name} list`)
)
} }
break break;
case `${name}-ignore`: case `${name}-ignore`:
if (isString(item.value)) { if (isString(item.value)) {
result[`${name}-ignore`] = [item.value.value] result[`${name}-ignore`] = [item.value.value];
} else { } else {
result[`${name}-ignore`] = convertStringList( result[`${name}-ignore`] = convertStringList(
`${name}-ignore`, `${name}-ignore`,
item.value.assertSequence(`${name}-ignore list`) item.value.assertSequence(`${name}-ignore list`)
) );
} }
break break;
} }
} }
return result return result;
} }
function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig>( function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig>(
name: "types" | "workflows", name: "types" | "workflows",
token: MappingToken token: MappingToken
): T { ): T {
const result = {} as T const result = {} as T;
for (const item of token) { for (const item of token) {
const key = item.key.assertString(`${name} filter key`) const key = item.key.assertString(`${name} filter key`);
switch (key.value) { switch (key.value) {
case name: case name:
if (isString(item.value)) { if (isString(item.value)) {
result[name] = [item.value.value] result[name] = [item.value.value];
} else { } else {
result[name] = convertStringList( result[name] = convertStringList(name, item.value.assertSequence(`${name} list`));
name,
item.value.assertSequence(`${name} list`)
)
} }
break break;
} }
} }
return result return result;
} }
function convertSchedule( function convertSchedule(context: TemplateContext, token: SequenceToken): ScheduleConfig[] | undefined {
context: TemplateContext, const result = [] as ScheduleConfig[];
token: SequenceToken
): ScheduleConfig[] | undefined {
const result = [] as ScheduleConfig[]
for (const item of token) { for (const item of token) {
const mappingToken = item.assertMapping(`event schedule`) const mappingToken = item.assertMapping(`event schedule`);
if (mappingToken.count == 1) { if (mappingToken.count == 1) {
const schedule = mappingToken.get(0) const schedule = mappingToken.get(0);
const scheduleKey = schedule.key.assertString(`schedule key`) const scheduleKey = schedule.key.assertString(`schedule key`);
if (scheduleKey.value == "cron") { if (scheduleKey.value == "cron") {
const cron = schedule.value.assertString(`schedule cron`) const cron = schedule.value.assertString(`schedule cron`);
// Validate the cron string // Validate the cron string
if ( if (!cron.value.match(/((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/)) {
!cron.value.match(/((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/) context.error(cron, "Invalid cron string");
) {
context.error(cron, "Invalid cron string")
} }
result.push({ cron: cron.value }) result.push({cron: cron.value});
} else { } else {
context.error(scheduleKey, `Invalid schedule key`) context.error(scheduleKey, `Invalid schedule key`);
} }
} else { } else {
context.error(mappingToken, "Invalid format for 'schedule'") context.error(mappingToken, "Invalid format for 'schedule'");
} }
} }
return result return result;
} }
@@ -1,8 +1,5 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { import {TemplateToken, TemplateTokenError} from "../../templates/tokens/template-token";
TemplateToken,
TemplateTokenError,
} from "../../templates/tokens/template-token"
export function handleTemplateTokenErrors<TResult>( export function handleTemplateTokenErrors<TResult>(
root: TemplateToken, root: TemplateToken,
@@ -10,18 +7,18 @@ export function handleTemplateTokenErrors<TResult>(
defaultValue: TResult, defaultValue: TResult,
f: () => TResult f: () => TResult
): TResult { ): TResult {
let r: TResult = defaultValue let r: TResult = defaultValue;
try { try {
r = f() r = f();
} catch (err) { } catch (err) {
if (err instanceof TemplateTokenError) { if (err instanceof TemplateTokenError) {
context.error(err.token, err) context.error(err.token, err);
} else { } else {
// Report error for the root node // Report error for the root node
context.error(root, err) context.error(root, err);
} }
} }
return r return r;
} }
@@ -1,147 +1,146 @@
import { IdBuilder } from "./id-builder" import {IdBuilder} from "./id-builder";
function build(...segments: string[]): string { function build(...segments: string[]): string {
const builder = new IdBuilder() const builder = new IdBuilder();
for (const segment of segments) { for (const segment of segments) {
builder.appendSegment(segment) builder.appendSegment(segment);
} }
return builder.build() return builder.build();
} }
describe("ID Builder", () => { describe("ID Builder", () => {
it("builds IDs", () => { it("builds IDs", () => {
expect(build("one")).toEqual("one") expect(build("one")).toEqual("one");
expect(build("one", "two")).toEqual("one_two") expect(build("one", "two")).toEqual("one_two");
expect(build("one", "two", "three")).toEqual("one_two_three") expect(build("one", "two", "three")).toEqual("one_two_three");
}) });
it("empty builder", () => { it("empty builder", () => {
const builder = new IdBuilder() const builder = new IdBuilder();
expect(builder.build()).toEqual("job") expect(builder.build()).toEqual("job");
}) });
it("ignores empty segments", () => { it("ignores empty segments", () => {
expect(build("", "one")).toEqual("one") expect(build("", "one")).toEqual("one");
expect(build("one", "", "two", "")).toEqual("one_two") expect(build("one", "", "two", "")).toEqual("one_two");
}) });
it("handles illegal characters", () => { it("handles illegal characters", () => {
const builder = new IdBuilder() const builder = new IdBuilder();
builder.appendSegment("hello world!") builder.appendSegment("hello world!");
expect(builder.build()).toEqual("hello_world_") expect(builder.build()).toEqual("hello_world_");
}) });
it("handles illegal leading characters", () => { it("handles illegal leading characters", () => {
expect(build("!hello")).toEqual("_hello") expect(build("!hello")).toEqual("_hello");
expect(build("!hello", "!world")).toEqual("_hello__world") expect(build("!hello", "!world")).toEqual("_hello__world");
expect(build("!@world", "!@world")).toEqual("__world___world") expect(build("!@world", "!@world")).toEqual("__world___world");
expect(build("123")).toEqual("_123") expect(build("123")).toEqual("_123");
expect(build("123", "456")).toEqual("_123_456") expect(build("123", "456")).toEqual("_123_456");
expect(build("-abc")).toEqual("_-abc") expect(build("-abc")).toEqual("_-abc");
expect(build("-abc", "-def")).toEqual("_-abc_-def") expect(build("-abc", "-def")).toEqual("_-abc_-def");
}) });
it("allows legal characters", () => { it("allows legal characters", () => {
expect(build("abyzABYZ0189_-")).toEqual("abyzABYZ0189_-") expect(build("abyzABYZ0189_-")).toEqual("abyzABYZ0189_-");
}) });
it("allows legal leading characters", () => { it("allows legal leading characters", () => {
expect(build("abc")).toEqual("abc") expect(build("abc")).toEqual("abc");
expect(build("bcd")).toEqual("bcd") expect(build("bcd")).toEqual("bcd");
expect(build("zyx")).toEqual("zyx") expect(build("zyx")).toEqual("zyx");
expect(build("yxw")).toEqual("yxw") expect(build("yxw")).toEqual("yxw");
expect(build("ABCD")).toEqual("ABCD") expect(build("ABCD")).toEqual("ABCD");
expect(build("BCDE")).toEqual("BCDE") expect(build("BCDE")).toEqual("BCDE");
expect(build("ZYXW")).toEqual("ZYXW") expect(build("ZYXW")).toEqual("ZYXW");
expect(build("YXWV")).toEqual("YXWV") expect(build("YXWV")).toEqual("YXWV");
expect(build("_abc")).toEqual("_abc") expect(build("_abc")).toEqual("_abc");
}) });
it("errors for max collisions", () => { it("errors for max collisions", () => {
const builder = new IdBuilder() const builder = new IdBuilder();
builder.appendSegment("abc") builder.appendSegment("abc");
builder.appendSegment("def") builder.appendSegment("def");
expect(builder.build()).toEqual("abc_def") expect(builder.build()).toEqual("abc_def");
for (let i = 2; i < 1000; i++) { for (let i = 2; i < 1000; i++) {
builder.appendSegment("abc") builder.appendSegment("abc");
builder.appendSegment("def") builder.appendSegment("def");
expect(builder.build()).toEqual(`abc_def_${i}`) expect(builder.build()).toEqual(`abc_def_${i}`);
} }
builder.appendSegment("abc") builder.appendSegment("abc");
builder.appendSegment("def") builder.appendSegment("def");
expect(() => builder.build()).toThrowError("Unable to create a unique name") expect(() => builder.build()).toThrowError("Unable to create a unique name");
}) });
it("takes suffix into account for max length", () => { it("takes suffix into account for max length", () => {
const builder = new IdBuilder() const builder = new IdBuilder();
const name = const name = "_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890" builder.appendSegment(name);
builder.appendSegment(name) expect(builder.build()).toEqual(name);
expect(builder.build()).toEqual(name)
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_2" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_2"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_3" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_3"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_4" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_4"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_5" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_5"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_6" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_6"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_7" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_7"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_8" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_8"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_9" "_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_9"
) );
builder.appendSegment(name) builder.appendSegment(name);
expect(builder.build()).toEqual( expect(builder.build()).toEqual(
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567_10" "_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567_10"
) );
}) });
}) });
@@ -1,69 +1,65 @@
const SEPARATOR = "_" const SEPARATOR = "_";
const MAX_ATTEMPTS = 1000 const MAX_ATTEMPTS = 1000;
const MAX_LENGTH = 100 const MAX_LENGTH = 100;
export class IdBuilder { export class IdBuilder {
private name: string[] = [] private name: string[] = [];
private readonly distinctNames: Set<string> = new Set() private readonly distinctNames: Set<string> = new Set();
public appendSegment(value: string) { public appendSegment(value: string) {
if (value.length === 0) { if (value.length === 0) {
return return;
} }
if (this.name.length == 0) { if (this.name.length == 0) {
const first = value[0] const first = value[0];
if (this.isAlpha(first) || first == "_") { if (this.isAlpha(first) || first == "_") {
// Legal first char // Legal first char
} else if (this.isNumeric(first) || first == "-") { } else if (this.isNumeric(first) || first == "-") {
// Illegal first char, but legal char. // Illegal first char, but legal char.
// Prepend "_". // Prepend "_".
this.name.push("_") this.name.push("_");
} else { } else {
// Illegal char // Illegal char
} }
} else { } else {
// Separator // Separator
this.name.push(SEPARATOR) this.name.push(SEPARATOR);
} }
for (const c of value) { for (const c of value) {
{ {
if (this.isAlphaNumeric(c) || c == "_" || c == "-") { if (this.isAlphaNumeric(c) || c == "_" || c == "-") {
// Legal // Legal
this.name.push(c) this.name.push(c);
} else { } else {
// Illegal // Illegal
this.name.push(SEPARATOR) this.name.push(SEPARATOR);
} }
} }
} }
} }
public build(): string { public build(): string {
const original = this.name.length > 0 ? this.name.join("") : "job" const original = this.name.length > 0 ? this.name.join("") : "job";
let suffix = "" let suffix = "";
for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) { for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) {
if (attempt === 1) { if (attempt === 1) {
suffix = "" suffix = "";
} else { } else {
suffix = "_" + attempt suffix = "_" + attempt;
} }
const candidate = const candidate = original.substring(0, Math.min(original.length, MAX_LENGTH - suffix.length)) + suffix;
original.substring(
0,
Math.min(original.length, MAX_LENGTH - suffix.length)
) + suffix
if (!this.distinctNames.has(candidate)) { if (!this.distinctNames.has(candidate)) {
this.distinctNames.add(candidate) this.distinctNames.add(candidate);
this.name = [] this.name = [];
return candidate return candidate;
} }
} }
throw new Error("Unable to create a unique name") throw new Error("Unable to create a unique name");
} }
/** /**
@@ -73,19 +69,19 @@ export class IdBuilder {
*/ */
public tryAddKnownId(value: string): string | undefined { public tryAddKnownId(value: string): string | undefined {
if (!value || !this.isValid(value) || value.length >= MAX_LENGTH) { if (!value || !this.isValid(value) || value.length >= MAX_LENGTH) {
return `The identifier '${value}' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and and must be less than ${MAX_LENGTH} characters.` return `The identifier '${value}' is invalid. IDs may only contain alphanumeric characters, '_', and '-'. IDs must start with a letter or '_' and and must be less than ${MAX_LENGTH} characters.`;
} }
if (value.startsWith(SEPARATOR + SEPARATOR)) { if (value.startsWith(SEPARATOR + SEPARATOR)) {
return `The identifier '${value}' is invalid. IDs starting with '__' are reserved.` return `The identifier '${value}' is invalid. IDs starting with '__' are reserved.`;
} }
if (this.distinctNames.has(value)) { if (this.distinctNames.has(value)) {
return `The identifier '${value}' may not be used more than once within the same scope.` return `The identifier '${value}' may not be used more than once within the same scope.`;
} }
this.distinctNames.add(value) this.distinctNames.add(value);
return return;
} }
/** /**
@@ -95,32 +91,32 @@ export class IdBuilder {
* @returns Whether the name is valid * @returns Whether the name is valid
*/ */
private isValid(name: string): boolean { private isValid(name: string): boolean {
let first = true let first = true;
for (const c of name) { for (const c of name) {
if (first) { if (first) {
first = false first = false;
if (!this.isAlpha(c) && c != "_") { if (!this.isAlpha(c) && c != "_") {
return false return false;
} }
continue continue;
} }
if (!this.isAlphaNumeric(c) && c != "_" && c != "-") { if (!this.isAlphaNumeric(c) && c != "_" && c != "-") {
return false return false;
} }
} }
return true return true;
} }
private isAlphaNumeric(c: string): boolean { private isAlphaNumeric(c: string): boolean {
return this.isAlpha(c) || this.isNumeric(c) return this.isAlpha(c) || this.isNumeric(c);
} }
private isNumeric(c: string): boolean { private isNumeric(c: string): boolean {
return c >= "0" && c <= "9" return c >= "0" && c <= "9";
} }
private isAlpha(c: string): boolean { private isAlpha(c: string): boolean {
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
} }
} }
@@ -1,41 +1,41 @@
import { TemplateContext } from "../../../templates/template-context" import {TemplateContext} from "../../../templates/template-context";
import { TemplateToken } from "../../../templates/tokens/template-token" import {TemplateToken} from "../../../templates/tokens/template-token";
import { isScalar } from "../../../templates/tokens/type-guards" import {isScalar} from "../../../templates/tokens/type-guards";
import { ActionsEnvironmentReference } from "../../workflow-template" import {ActionsEnvironmentReference} from "../../workflow-template";
export function convertToActionsEnvironmentRef( export function convertToActionsEnvironmentRef(
context: TemplateContext, context: TemplateContext,
token: TemplateToken token: TemplateToken
): ActionsEnvironmentReference { ): ActionsEnvironmentReference {
const result: ActionsEnvironmentReference = {} const result: ActionsEnvironmentReference = {};
if (token.isExpression) { if (token.isExpression) {
return result return result;
} }
if (isScalar(token)) { if (isScalar(token)) {
result.name = token result.name = token;
return result return result;
} }
const environmentMapping = token.assertMapping("job environment") const environmentMapping = token.assertMapping("job environment");
for (const property of environmentMapping) { for (const property of environmentMapping) {
const propertyName = property.key.assertString("job environment key") const propertyName = property.key.assertString("job environment key");
if (property.key.isExpression || property.value.isExpression) { if (property.key.isExpression || property.value.isExpression) {
continue continue;
} }
switch (propertyName.value) { switch (propertyName.value) {
case "name": case "name":
result.name = property.value.assertScalar("job environment name key") result.name = property.value.assertScalar("job environment name key");
break break;
case "url": case "url":
result.url = property.value result.url = property.value;
break break;
} }
} }
return result return result;
} }
@@ -1,72 +1,53 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { import {BasicExpressionToken, MappingToken, StringToken} from "../../templates/tokens";
BasicExpressionToken, import {TemplateToken} from "../../templates/tokens/template-token";
MappingToken, import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
StringToken, import {Job} from "../workflow-template";
} from "../../templates/tokens" import {convertConcurrency} from "./concurrency";
import { TemplateToken } from "../../templates/tokens/template-token" import {convertToJobContainer, convertToJobServices} from "./container";
import { import {handleTemplateTokenErrors} from "./handle-errors";
isMapping, import {IdBuilder} from "./id-builder";
isSequence, import {convertToActionsEnvironmentRef} from "./job/environment";
isString, import {convertSteps} from "./steps";
} from "../../templates/tokens/type-guards"
import { Job } from "../workflow-template"
import { convertConcurrency } from "./concurrency"
import { convertToJobContainer, convertToJobServices } from "./container"
import { handleTemplateTokenErrors } from "./handle-errors"
import { IdBuilder } from "./id-builder"
import { convertToActionsEnvironmentRef } from "./job/environment"
import { convertSteps } from "./steps"
type nodeInfo = { type nodeInfo = {
name: string name: string;
needs: StringToken[] needs: StringToken[];
} };
export function convertJobs( export function convertJobs(context: TemplateContext, token: TemplateToken): Job[] {
context: TemplateContext,
token: TemplateToken
): Job[] {
if (isMapping(token)) { if (isMapping(token)) {
const result: Job[] = [] const result: Job[] = [];
const jobsWithSatisfiedNeeds: nodeInfo[] = [] const jobsWithSatisfiedNeeds: nodeInfo[] = [];
const alljobsWithUnsatisfiedNeeds: nodeInfo[] = [] const alljobsWithUnsatisfiedNeeds: nodeInfo[] = [];
for (const item of token) { for (const item of token) {
const jobKey = item.key.assertString("job name") const jobKey = item.key.assertString("job name");
const jobDef = item.value.assertMapping(`job ${jobKey.value}`) const jobDef = item.value.assertMapping(`job ${jobKey.value}`);
const job = handleTemplateTokenErrors(token, context, undefined, () => const job = handleTemplateTokenErrors(token, context, undefined, () => convertJob(context, jobKey, jobDef));
convertJob(context, jobKey, jobDef)
)
if (job) { if (job) {
result.push(job) result.push(job);
const node = { const node = {
name: job.id.value, name: job.id.value,
needs: Object.assign([], job.needs), needs: Object.assign([], job.needs)
} };
if (node.needs.length > 0) { if (node.needs.length > 0) {
alljobsWithUnsatisfiedNeeds.push(node) alljobsWithUnsatisfiedNeeds.push(node);
} else { } else {
jobsWithSatisfiedNeeds.push(node) jobsWithSatisfiedNeeds.push(node);
} }
} }
} }
//validate job needs //validate job needs
validateNeeds( validateNeeds(token, context, result, jobsWithSatisfiedNeeds, alljobsWithUnsatisfiedNeeds);
token,
context,
result,
jobsWithSatisfiedNeeds,
alljobsWithUnsatisfiedNeeds
)
return result return result;
} }
context.error(token, "Invalid format for jobs") context.error(token, "Invalid format for jobs");
return [] return [];
} }
function validateNeeds( function validateNeeds(
@@ -77,28 +58,25 @@ function validateNeeds(
alljobsWithUnsatisfiedNeeds: nodeInfo[] alljobsWithUnsatisfiedNeeds: nodeInfo[]
) { ) {
if (jobsWithSatisfiedNeeds.length == 0) { if (jobsWithSatisfiedNeeds.length == 0) {
context.error( context.error(token, "The workflow must contain at least one job with no dependencies.");
token, return;
"The workflow must contain at least one job with no dependencies."
)
return
} }
// Figure out which nodes would start after current completes // Figure out which nodes would start after current completes
while (jobsWithSatisfiedNeeds.length > 0) { while (jobsWithSatisfiedNeeds.length > 0) {
const currentJob = jobsWithSatisfiedNeeds.shift() const currentJob = jobsWithSatisfiedNeeds.shift();
if (currentJob == undefined) { if (currentJob == undefined) {
break break;
} }
for (let i = alljobsWithUnsatisfiedNeeds.length - 1; i >= 0; i--) { for (let i = alljobsWithUnsatisfiedNeeds.length - 1; i >= 0; i--) {
const unsatisfiedJob = alljobsWithUnsatisfiedNeeds[i] const unsatisfiedJob = alljobsWithUnsatisfiedNeeds[i];
for (let j = unsatisfiedJob.needs.length - 1; j >= 0; j--) { for (let j = unsatisfiedJob.needs.length - 1; j >= 0; j--) {
const need = unsatisfiedJob.needs[j] const need = unsatisfiedJob.needs[j];
if (need.value == currentJob.name) { if (need.value == currentJob.name) {
unsatisfiedJob.needs.splice(j, 1) unsatisfiedJob.needs.splice(j, 1);
if (unsatisfiedJob.needs.length == 0) { if (unsatisfiedJob.needs.length == 0) {
jobsWithSatisfiedNeeds.push(unsatisfiedJob) jobsWithSatisfiedNeeds.push(unsatisfiedJob);
alljobsWithUnsatisfiedNeeds.splice(i, 1) alljobsWithUnsatisfiedNeeds.splice(i, 1);
} }
} }
} }
@@ -107,46 +85,33 @@ function validateNeeds(
// Check whether some jobs will never execute // Check whether some jobs will never execute
if (alljobsWithUnsatisfiedNeeds.length > 0) { if (alljobsWithUnsatisfiedNeeds.length > 0) {
const jobNames = result.map((x) => x.id.value) const jobNames = result.map(x => x.id.value);
for (const unsatisfiedJob of alljobsWithUnsatisfiedNeeds) { for (const unsatisfiedJob of alljobsWithUnsatisfiedNeeds) {
for (const need of unsatisfiedJob.needs) { for (const need of unsatisfiedJob.needs) {
if (jobNames.includes(need.value)) { if (jobNames.includes(need.value)) {
context.error( context.error(
need, need,
`Job '${unsatisfiedJob.name}' depends on job '${need.value}' which creates a cycle in the dependency graph.` `Job '${unsatisfiedJob.name}' depends on job '${need.value}' which creates a cycle in the dependency graph.`
) );
} else { } else {
context.error( context.error(need, `Job '${unsatisfiedJob.name}' depends on unknown job '${need.value}'.`);
need,
`Job '${unsatisfiedJob.name}' depends on unknown job '${need.value}'.`
)
} }
} }
} }
} }
} }
function convertJob( function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job {
context: TemplateContext, const error = new IdBuilder().tryAddKnownId(jobKey.value);
jobKey: StringToken,
token: MappingToken
): Job {
const error = new IdBuilder().tryAddKnownId(jobKey.value)
if (error) { if (error) {
context.error(jobKey, error) context.error(jobKey, error);
} }
const result: Job = { const result: Job = {
type: "job", type: "job",
id: jobKey, id: jobKey,
name: undefined, name: undefined,
needs: undefined, needs: undefined,
if: new BasicExpressionToken( if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined),
undefined,
undefined,
"success()",
undefined,
undefined
),
env: undefined, env: undefined,
concurrency: undefined, concurrency: undefined,
environment: undefined, environment: undefined,
@@ -155,191 +120,180 @@ function convertJob(
container: undefined, container: undefined,
services: undefined, services: undefined,
outputs: undefined, outputs: undefined,
steps: [], steps: []
} };
for (const item of token) { for (const item of token) {
const propertyName = item.key.assertString("job property name") const propertyName = item.key.assertString("job property name");
switch (propertyName.value) { switch (propertyName.value) {
case "concurrency": case "concurrency":
handleTemplateTokenErrors(item.value, context, undefined, () => handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
convertConcurrency(context, item.value) result.concurrency = item.value;
) break;
result.concurrency = item.value
break
case "container": case "container":
// Do early validation, but don't convert // Do early validation, but don't convert
convertToJobContainer(context, item.value) convertToJobContainer(context, item.value);
result.container = item.value result.container = item.value;
break break;
case "env": case "env":
result.env = item.value.assertMapping("job env") result.env = item.value.assertMapping("job env");
break break;
case "environment": case "environment":
handleTemplateTokenErrors(item.value, context, undefined, () => handleTemplateTokenErrors(item.value, context, undefined, () =>
convertToActionsEnvironmentRef(context, item.value) convertToActionsEnvironmentRef(context, item.value)
) );
result.environment = item.value result.environment = item.value;
break break;
case "name": case "name":
result.name = item.value.assertScalar("job name") result.name = item.value.assertScalar("job name");
break break;
case "needs": case "needs":
result.needs = [] result.needs = [];
if (isString(item.value)) { if (isString(item.value)) {
const jobNeeds = item.value.assertString("job needs id") const jobNeeds = item.value.assertString("job needs id");
result.needs.push(jobNeeds) result.needs.push(jobNeeds);
} }
if (isSequence(item.value)) { if (isSequence(item.value)) {
for (const seqItem of item.value) { for (const seqItem of item.value) {
const jobNeeds = seqItem.assertString("job needs id") const jobNeeds = seqItem.assertString("job needs id");
result.needs.push(jobNeeds) result.needs.push(jobNeeds);
} }
} }
break break;
case "outputs": case "outputs":
result.outputs = item.value.assertMapping("job outputs") result.outputs = item.value.assertMapping("job outputs");
break break;
case "runs-on": case "runs-on":
handleTemplateTokenErrors(item.value, context, undefined, () => handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
convertRunsOn(context, item.value) result["runs-on"] = item.value;
) break;
result["runs-on"] = item.value
break
case "services": case "services":
// Do early validation, but don't convert // Do early validation, but don't convert
convertToJobServices(context, item.value) convertToJobServices(context, item.value);
result.services = item.value result.services = item.value;
break break;
case "steps": case "steps":
result.steps = convertSteps(context, item.value) result.steps = convertSteps(context, item.value);
break break;
case "strategy": case "strategy":
result.strategy = item.value result.strategy = item.value;
break break;
} }
} }
if (!result.name) { if (!result.name) {
result.name = result.id result.name = result.id;
} }
return result return result;
} }
type RunsOn = { type RunsOn = {
labels: Set<string> labels: Set<string>;
group: string group: string;
} };
function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn { function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
const labels = convertRunsOnLabels(token) const labels = convertRunsOnLabels(token);
if (!isMapping(token)) { if (!isMapping(token)) {
return { return {
labels, labels,
group: "", group: ""
} };
} }
let group = "" let group = "";
for (const item of token) { for (const item of token) {
const key = item.key.assertString("job runs-on property name") const key = item.key.assertString("job runs-on property name");
switch (key.value) { switch (key.value) {
case "group": { case "group": {
if (item.value.isExpression) { if (item.value.isExpression) {
continue continue;
} }
const groupName = item.value.assertString( const groupName = item.value.assertString("job runs-on group name").value;
"job runs-on group name" const names = groupName.split("/");
).value
const names = groupName.split("/")
switch (names.length) { switch (names.length) {
case 1: { case 1: {
group = groupName group = groupName;
break break;
} }
case 2: { case 2: {
if ( if (!["org", "organization", "ent", "enterprise"].includes(names[0])) {
!["org", "organization", "ent", "enterprise"].includes(names[0])
) {
context.error( context.error(
item.value, item.value,
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
) );
continue continue;
} }
if (!names[1]) { if (!names[1]) {
context.error( context.error(item.value, `Invalid runs-on group name '${groupName}'.`);
item.value, continue;
`Invalid runs-on group name '${groupName}'.`
)
continue
} }
group = groupName group = groupName;
break break;
} }
default: { default: {
context.error( context.error(
item.value, item.value,
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'` `Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
) );
break break;
} }
} }
break break;
} }
case "labels": { case "labels": {
const mapLabels = convertRunsOnLabels(item.value) const mapLabels = convertRunsOnLabels(item.value);
for (const label of mapLabels) { for (const label of mapLabels) {
labels.add(label) labels.add(label);
} }
break break;
} }
} }
} }
return { return {
labels, labels,
group, group
} };
} }
function convertRunsOnLabels(token: TemplateToken): Set<string> { function convertRunsOnLabels(token: TemplateToken): Set<string> {
const labels = new Set<string>() const labels = new Set<string>();
if (token.isExpression) { if (token.isExpression) {
return labels return labels;
} }
if (isString(token)) { if (isString(token)) {
labels.add(token.value) labels.add(token.value);
return labels return labels;
} }
if (isSequence(token)) { if (isSequence(token)) {
for (const item of token) { for (const item of token) {
if (item.isExpression) { if (item.isExpression) {
continue continue;
} }
const label = item.assertString("job runs-on label sequence item") const label = item.assertString("job runs-on label sequence item");
labels.add(label.value) labels.add(label.value);
} }
} }
return labels return labels;
} }
@@ -1,107 +1,84 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { import {BasicExpressionToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "../../templates/tokens";
BasicExpressionToken, import {isSequence} from "../../templates/tokens/type-guards";
MappingToken, import {isActionStep} from "../type-guards";
ScalarToken, import {ActionStep, Step} from "../workflow-template";
StringToken, import {handleTemplateTokenErrors} from "./handle-errors";
TemplateToken, import {IdBuilder} from "./id-builder";
} from "../../templates/tokens"
import { isSequence } from "../../templates/tokens/type-guards"
import { isActionStep } from "../type-guards"
import { ActionStep, Step } from "../workflow-template"
import { handleTemplateTokenErrors } from "./handle-errors"
import { IdBuilder } from "./id-builder"
export function convertSteps( export function convertSteps(context: TemplateContext, steps: TemplateToken): Step[] {
context: TemplateContext,
steps: TemplateToken
): Step[] {
if (!isSequence(steps)) { if (!isSequence(steps)) {
context.error(steps, "Invalid format for steps") context.error(steps, "Invalid format for steps");
return [] return [];
} }
const idBuilder = new IdBuilder() const idBuilder = new IdBuilder();
const result: Step[] = [] const result: Step[] = [];
for (const item of steps) { for (const item of steps) {
const step = handleTemplateTokenErrors(steps, context, undefined, () => const step = handleTemplateTokenErrors(steps, context, undefined, () => convertStep(context, idBuilder, item));
convertStep(context, idBuilder, item)
)
if (step) { if (step) {
result.push(step) result.push(step);
} }
} }
for (const step of result) { for (const step of result) {
if (step.id) { if (step.id) {
continue continue;
} }
let id = "" let id = "";
if (isActionStep(step)) { if (isActionStep(step)) {
id = createActionStepId(step) id = createActionStepId(step);
} }
if (!id) { if (!id) {
id = "run" id = "run";
} }
idBuilder.appendSegment(`__${id}`) idBuilder.appendSegment(`__${id}`);
step.id = idBuilder.build() step.id = idBuilder.build();
} }
return result return result;
} }
function convertStep( function convertStep(context: TemplateContext, idBuilder: IdBuilder, step: TemplateToken): Step | undefined {
context: TemplateContext, const mapping = step.assertMapping("steps item");
idBuilder: IdBuilder,
step: TemplateToken
): Step | undefined {
const mapping = step.assertMapping("steps item")
let run: ScalarToken | undefined let run: ScalarToken | undefined;
let id: StringToken | undefined let id: StringToken | undefined;
let name: ScalarToken | undefined let name: ScalarToken | undefined;
let uses: StringToken | undefined let uses: StringToken | undefined;
let continueOnError: boolean | undefined let continueOnError: boolean | undefined;
let env: MappingToken | undefined let env: MappingToken | undefined;
const ifCondition = new BasicExpressionToken( const ifCondition = new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined);
undefined,
undefined,
"success()",
undefined,
undefined
)
for (const item of mapping) { for (const item of mapping) {
const key = item.key.assertString("steps item key") const key = item.key.assertString("steps item key");
switch (key.value) { switch (key.value) {
case "id": case "id":
id = item.value.assertString("steps item id") id = item.value.assertString("steps item id");
if (id) { if (id) {
const error = idBuilder.tryAddKnownId(id.value) const error = idBuilder.tryAddKnownId(id.value);
if (error) { if (error) {
context.error(id, error) context.error(id, error);
} }
} }
break break;
case "name": case "name":
name = item.value.assertScalar("steps item name") name = item.value.assertScalar("steps item name");
break break;
case "run": case "run":
run = item.value.assertScalar("steps item run") run = item.value.assertScalar("steps item run");
break break;
case "uses": case "uses":
uses = item.value.assertString("steps item uses") uses = item.value.assertString("steps item uses");
break break;
case "env": case "env":
env = item.value.assertMapping("step env") env = item.value.assertMapping("step env");
break break;
case "continue-on-error": case "continue-on-error":
continueOnError = item.value.assertBoolean( continueOnError = item.value.assertBoolean("steps item continue-on-error").value;
"steps item continue-on-error"
).value
} }
} }
@@ -112,8 +89,8 @@ function convertStep(
if: ifCondition, if: ifCondition,
"continue-on-error": continueOnError, "continue-on-error": continueOnError,
env, env,
run, run
} };
} }
if (uses) { if (uses) {
@@ -123,38 +100,33 @@ function convertStep(
if: ifCondition, if: ifCondition,
"continue-on-error": continueOnError, "continue-on-error": continueOnError,
env, env,
uses, uses
} };
} }
context.error(step, "Expected uses or run to be defined") context.error(step, "Expected uses or run to be defined");
} }
function createActionStepId(step: ActionStep): string { function createActionStepId(step: ActionStep): string {
const uses = step.uses.value const uses = step.uses.value;
if (uses.startsWith("docker://")) { if (uses.startsWith("docker://")) {
return uses.substring("docker://".length) return uses.substring("docker://".length);
} }
if (uses.startsWith("./") || uses.startsWith(".\\")) { if (uses.startsWith("./") || uses.startsWith(".\\")) {
return "self" return "self";
} }
const segments = uses.split("@") const segments = uses.split("@");
if (segments.length != 2) { if (segments.length != 2) {
return "" return "";
} }
const pathSegments = segments[0].split(/[\\/]/).filter((s) => s.length > 0) const pathSegments = segments[0].split(/[\\/]/).filter(s => s.length > 0);
const gitRef = segments[1] const gitRef = segments[1];
if ( if (pathSegments.length >= 2 && pathSegments[0] && pathSegments[1] && gitRef) {
pathSegments.length >= 2 && return `${pathSegments[0]}/${pathSegments[1]}`;
pathSegments[0] &&
pathSegments[1] &&
gitRef
) {
return `${pathSegments[0]}/${pathSegments[1]}`
} }
return "" return "";
} }
@@ -1,14 +1,11 @@
import { SequenceToken } from "../../templates/tokens/sequence-token" import {SequenceToken} from "../../templates/tokens/sequence-token";
export function convertStringList( export function convertStringList(name: string, token: SequenceToken): string[] {
name: string, const result = [] as string[];
token: SequenceToken
): string[] {
const result = [] as string[]
for (const item of token) { for (const item of token) {
result.push(item.assertString(`${name} item`).value) result.push(item.assertString(`${name} item`).value);
} }
return result return result;
} }
@@ -1,99 +1,79 @@
import { TemplateContext } from "../../templates/template-context" import {TemplateContext} from "../../templates/template-context";
import { MappingToken } from "../../templates/tokens/mapping-token" import {MappingToken} from "../../templates/tokens/mapping-token";
import { ScalarToken } from "../../templates/tokens/scalar-token" import {ScalarToken} from "../../templates/tokens/scalar-token";
import { import {InputConfig, InputType, WorkflowDispatchConfig} from "../workflow-template";
InputConfig, import {convertStringList} from "./string-list";
InputType,
WorkflowDispatchConfig,
} from "../workflow-template"
import { convertStringList } from "./string-list"
export function convertEventWorkflowDispatchInputs( export function convertEventWorkflowDispatchInputs(
context: TemplateContext, context: TemplateContext,
token: MappingToken token: MappingToken
): WorkflowDispatchConfig { ): WorkflowDispatchConfig {
const result: WorkflowDispatchConfig = {} const result: WorkflowDispatchConfig = {};
for (const item of token) { for (const item of token) {
const key = item.key.assertString("workflow dispatch input key") const key = item.key.assertString("workflow dispatch input key");
switch (key.value) { switch (key.value) {
case "inputs": case "inputs":
result.inputs = convertWorkflowDispatchInputs( result.inputs = convertWorkflowDispatchInputs(context, item.value.assertMapping("workflow dispatch inputs"));
context, break;
item.value.assertMapping("workflow dispatch inputs")
)
break
} }
} }
return result return result;
} }
export function convertWorkflowDispatchInputs( export function convertWorkflowDispatchInputs(
context: TemplateContext, context: TemplateContext,
token: MappingToken token: MappingToken
): { ): {
[inputName: string]: InputConfig [inputName: string]: InputConfig;
} { } {
const result: { [inputName: string]: InputConfig } = {} const result: {[inputName: string]: InputConfig} = {};
for (const item of token) { for (const item of token) {
const inputName = item.key.assertString("input name") const inputName = item.key.assertString("input name");
const inputMapping = item.value.assertMapping("input configuration") const inputMapping = item.value.assertMapping("input configuration");
result[inputName.value] = convertWorkflowDispatchInput( result[inputName.value] = convertWorkflowDispatchInput(context, inputMapping);
context,
inputMapping
)
} }
return result return result;
} }
export function convertWorkflowDispatchInput( export function convertWorkflowDispatchInput(context: TemplateContext, token: MappingToken): InputConfig {
context: TemplateContext,
token: MappingToken
): InputConfig {
const result: InputConfig = { const result: InputConfig = {
type: InputType.string, // Default to string type: InputType.string // Default to string
} };
let defaultValue: undefined | ScalarToken let defaultValue: undefined | ScalarToken;
for (const item of token) { for (const item of token) {
const key = item.key.assertString("workflow dispatch input key") const key = item.key.assertString("workflow dispatch input key");
switch (key.value) { switch (key.value) {
case "description": case "description":
result.description = item.value.assertString("input description").value result.description = item.value.assertString("input description").value;
break break;
case "required": case "required":
result.required = item.value.assertBoolean("input required").value result.required = item.value.assertBoolean("input required").value;
break break;
case "default": case "default":
defaultValue = item.value.assertScalar("input default") defaultValue = item.value.assertScalar("input default");
break break;
case "type": case "type":
result.type = result.type = InputType[item.value.assertString("input type").value as keyof typeof InputType];
InputType[ break;
item.value.assertString("input type")
.value as keyof typeof InputType
]
break
case "options": case "options":
result.options = convertStringList( result.options = convertStringList("input options", item.value.assertSequence("input options"));
"input options", break;
item.value.assertSequence("input options")
)
break
default: default:
context.error(item.key, `Invalid key '${key.value}'`) context.error(item.key, `Invalid key '${key.value}'`);
} }
} }
@@ -102,34 +82,31 @@ export function convertWorkflowDispatchInput(
try { try {
switch (result.type) { switch (result.type) {
case InputType.boolean: case InputType.boolean:
result.default = defaultValue.assertBoolean("input default").value result.default = defaultValue.assertBoolean("input default").value;
break break;
case InputType.string: case InputType.string:
case InputType.choice: case InputType.choice:
case InputType.environment: case InputType.environment:
result.default = defaultValue.assertString("input default").value result.default = defaultValue.assertString("input default").value;
break break;
} }
} catch (e) { } catch (e) {
context.error(defaultValue, e) context.error(defaultValue, e);
} }
} }
// Validate `options` for `choice` type // Validate `options` for `choice` type
if (result.type === InputType.choice) { if (result.type === InputType.choice) {
if (result.options === undefined || result.options.length === 0) { if (result.options === undefined || result.options.length === 0) {
context.error(token, "Missing 'options' for choice input") context.error(token, "Missing 'options' for choice input");
} }
} else { } else {
if (result.options !== undefined) { if (result.options !== undefined) {
context.error( context.error(token, "Input type is not 'choice', but 'options' is defined");
token,
"Input type is not 'choice', but 'options' is defined"
)
} }
} }
return result return result;
} }
@@ -1,9 +1,9 @@
import { ActionStep, RunStep, Step } from "./workflow-template" import {ActionStep, RunStep, Step} from "./workflow-template";
export function isRunStep(step: Step): step is RunStep { export function isRunStep(step: Step): step is RunStep {
return (step as RunStep).run !== undefined return (step as RunStep).run !== undefined;
} }
export function isActionStep(step: Step): step is ActionStep { export function isActionStep(step: Step): step is ActionStep {
return (step as ActionStep).uses !== undefined return (step as ActionStep).uses !== undefined;
} }
@@ -4,166 +4,164 @@ import {
ScalarToken, ScalarToken,
SequenceToken, SequenceToken,
StringToken, StringToken,
TemplateToken, TemplateToken
} from "../templates/tokens" } from "../templates/tokens";
export type WorkflowTemplate = { export type WorkflowTemplate = {
events: EventsConfig events: EventsConfig;
jobs: Job[] jobs: Job[];
concurrency: TemplateToken concurrency: TemplateToken;
env: TemplateToken env: TemplateToken;
errors?: { errors?: {
Message: string Message: string;
}[] }[];
} };
export type ConcurrencySetting = { export type ConcurrencySetting = {
group?: StringToken group?: StringToken;
cancelInProgress?: boolean cancelInProgress?: boolean;
} };
export type ActionsEnvironmentReference = { export type ActionsEnvironmentReference = {
name?: TemplateToken name?: TemplateToken;
url?: TemplateToken url?: TemplateToken;
} };
export type Job = { export type Job = {
type: string type: string;
id: StringToken id: StringToken;
name?: ScalarToken name?: ScalarToken;
needs?: StringToken[] needs?: StringToken[];
if: BasicExpressionToken if: BasicExpressionToken;
env?: MappingToken env?: MappingToken;
concurrency?: TemplateToken concurrency?: TemplateToken;
environment?: TemplateToken environment?: TemplateToken;
strategy?: TemplateToken strategy?: TemplateToken;
"runs-on"?: TemplateToken "runs-on"?: TemplateToken;
container?: TemplateToken container?: TemplateToken;
services?: TemplateToken services?: TemplateToken;
outputs?: MappingToken outputs?: MappingToken;
steps: Step[] steps: Step[];
} };
export type Container = { export type Container = {
image: StringToken image: StringToken;
credentials?: Credential credentials?: Credential;
env?: MappingToken env?: MappingToken;
ports?: SequenceToken ports?: SequenceToken;
volumes?: SequenceToken volumes?: SequenceToken;
options?: StringToken options?: StringToken;
} };
export type Credential = { export type Credential = {
username: StringToken | undefined username: StringToken | undefined;
password: StringToken | undefined password: StringToken | undefined;
} };
export type Step = ActionStep | RunStep export type Step = ActionStep | RunStep;
type BaseStep = { type BaseStep = {
id: string id: string;
name?: ScalarToken name?: ScalarToken;
if: BasicExpressionToken if: BasicExpressionToken;
"continue-on-error"?: boolean "continue-on-error"?: boolean;
env?: MappingToken env?: MappingToken;
} };
export type RunStep = BaseStep & { export type RunStep = BaseStep & {
run: ScalarToken run: ScalarToken;
} };
export type ActionStep = BaseStep & { export type ActionStep = BaseStep & {
uses: StringToken uses: StringToken;
} };
export type EventsConfig = { export type EventsConfig = {
schedule?: ScheduleConfig[] schedule?: ScheduleConfig[];
workflow_dispatch?: WorkflowDispatchConfig workflow_dispatch?: WorkflowDispatchConfig;
workflow_call?: WorkflowCallConfig workflow_call?: WorkflowCallConfig;
// Events that support filters // Events that support filters
pull_request?: BranchFilterConfig & PathFilterConfig & TypesFilterConfig pull_request?: BranchFilterConfig & PathFilterConfig & TypesFilterConfig;
pull_request_target?: BranchFilterConfig & pull_request_target?: BranchFilterConfig & PathFilterConfig & TypesFilterConfig;
PathFilterConfig & push?: BranchFilterConfig & TagFilterConfig & PathFilterConfig;
TypesFilterConfig workflow_run?: WorkflowFilterConfig & BranchFilterConfig & TypesFilterConfig;
push?: BranchFilterConfig & TagFilterConfig & PathFilterConfig
workflow_run?: WorkflowFilterConfig & BranchFilterConfig & TypesFilterConfig
// Events that only support activity types // Events that only support activity types
branch_protection_rule?: TypesFilterConfig branch_protection_rule?: TypesFilterConfig;
check_run?: TypesFilterConfig check_run?: TypesFilterConfig;
check_suite?: TypesFilterConfig check_suite?: TypesFilterConfig;
disccusion?: TypesFilterConfig disccusion?: TypesFilterConfig;
disccusion_comment?: TypesFilterConfig disccusion_comment?: TypesFilterConfig;
issue_comment?: TypesFilterConfig issue_comment?: TypesFilterConfig;
issues?: TypesFilterConfig issues?: TypesFilterConfig;
label?: TypesFilterConfig label?: TypesFilterConfig;
merge_group?: TypesFilterConfig merge_group?: TypesFilterConfig;
milestone?: TypesFilterConfig milestone?: TypesFilterConfig;
project?: TypesFilterConfig project?: TypesFilterConfig;
project_card?: TypesFilterConfig project_card?: TypesFilterConfig;
project_column?: TypesFilterConfig project_column?: TypesFilterConfig;
pull_request_review?: TypesFilterConfig pull_request_review?: TypesFilterConfig;
pull_request_review_comment?: TypesFilterConfig pull_request_review_comment?: TypesFilterConfig;
registry_package?: TypesFilterConfig registry_package?: TypesFilterConfig;
repository_dispatch?: TypesFilterConfig repository_dispatch?: TypesFilterConfig;
release?: TypesFilterConfig release?: TypesFilterConfig;
watch?: TypesFilterConfig watch?: TypesFilterConfig;
// Index signature to allow easier lookup // Index signature to allow easier lookup
[eventName: string]: unknown [eventName: string]: unknown;
} };
export type TypesFilterConfig = { export type TypesFilterConfig = {
types?: string[] types?: string[];
} };
export type BranchFilterConfig = { export type BranchFilterConfig = {
branches?: string[] branches?: string[];
"branches-ignore"?: string[] "branches-ignore"?: string[];
} };
export type TagFilterConfig = { export type TagFilterConfig = {
tags?: string[] tags?: string[];
"tags-ignore"?: string[] "tags-ignore"?: string[];
} };
export type PathFilterConfig = { export type PathFilterConfig = {
paths?: string[] paths?: string[];
"paths-ignore"?: string[] "paths-ignore"?: string[];
} };
export type WorkflowDispatchConfig = { export type WorkflowDispatchConfig = {
inputs?: { [inputName: string]: InputConfig } inputs?: {[inputName: string]: InputConfig};
} };
export type WorkflowCallConfig = { export type WorkflowCallConfig = {
inputs: { [inputName: string]: InputConfig } inputs: {[inputName: string]: InputConfig};
// TODO - these are supported in C# and Go but not in TS yet // TODO - these are supported in C# and Go but not in TS yet
// outputs: { [outputName: string]: OutputConfig } // outputs: { [outputName: string]: OutputConfig }
// secrets: { [secretName: string]: SecretConfig } // secrets: { [secretName: string]: SecretConfig }
} };
export enum InputType { export enum InputType {
string = "string", string = "string",
choice = "choice", choice = "choice",
boolean = "boolean", boolean = "boolean",
environment = "environment", environment = "environment"
} }
export type InputConfig = { export type InputConfig = {
type: InputType type: InputType;
description?: string description?: string;
required?: boolean required?: boolean;
default?: string | boolean | number default?: string | boolean | number;
options?: string[] options?: string[];
} };
export type ScheduleConfig = { export type ScheduleConfig = {
cron: string cron: string;
} };
export type WorkflowFilterConfig = { export type WorkflowFilterConfig = {
workflows?: string[] workflows?: string[];
} };
@@ -1,38 +1,36 @@
import { FunctionInfo } from "@github/actions-expressions/funcs/info" import {FunctionInfo} from "@github/actions-expressions/funcs/info";
import { MAX_CONSTANT } from "./template-constants" import {MAX_CONSTANT} from "./template-constants";
export function splitAllowedContext(allowedContext: string[]): { export function splitAllowedContext(allowedContext: string[]): {
namedContexts: string[] namedContexts: string[];
functions: FunctionInfo[] functions: FunctionInfo[];
} { } {
const FUNCTION_REGEXP = /^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$/ const FUNCTION_REGEXP = /^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$/;
const namedContexts: string[] = [] const namedContexts: string[] = [];
const functions: FunctionInfo[] = [] const functions: FunctionInfo[] = [];
if (allowedContext.length > 0) { if (allowedContext.length > 0) {
for (const contextItem of allowedContext) { for (const contextItem of allowedContext) {
const match = contextItem.match(FUNCTION_REGEXP) const match = contextItem.match(FUNCTION_REGEXP);
if (match) { if (match) {
const functionName = match[1] const functionName = match[1];
const minParameters = Number.parseInt(match[2]) const minParameters = Number.parseInt(match[2]);
const maxParametersRaw = match[3] const maxParametersRaw = match[3];
const maxParameters = const maxParameters =
maxParametersRaw === MAX_CONSTANT maxParametersRaw === MAX_CONSTANT ? Number.MAX_SAFE_INTEGER : Number.parseInt(maxParametersRaw);
? Number.MAX_SAFE_INTEGER
: Number.parseInt(maxParametersRaw)
functions.push(<FunctionInfo>{ functions.push(<FunctionInfo>{
name: functionName, name: functionName,
minArgs: minParameters, minArgs: minParameters,
maxArgs: maxParameters, maxArgs: maxParameters
}) });
} else { } else {
namedContexts.push(contextItem) namedContexts.push(contextItem);
} }
} }
} }
return { return {
namedContexts: namedContexts, namedContexts: namedContexts,
functions: functions, functions: functions
} };
} }
@@ -1,12 +1,8 @@
import { Evaluator, Lexer, Parser } from "@github/actions-expressions" import {Evaluator, Lexer, Parser} from "@github/actions-expressions";
import { Expr } from "@github/actions-expressions/ast" import {Expr} from "@github/actions-expressions/ast";
import { import {Dictionary, ExpressionData, Kind} from "@github/actions-expressions/data/index";
Dictionary, import {FunctionInfo} from "@github/actions-expressions/funcs/info";
ExpressionData, import {TemplateContext} from "./template-context";
Kind,
} from "@github/actions-expressions/data/index"
import { FunctionInfo } from "@github/actions-expressions/funcs/info"
import { TemplateContext } from "./template-context"
import { import {
BasicExpressionToken, BasicExpressionToken,
BooleanToken, BooleanToken,
@@ -16,115 +12,74 @@ import {
NumberToken, NumberToken,
SequenceToken, SequenceToken,
StringToken, StringToken,
TemplateToken, TemplateToken
} from "./tokens" } from "./tokens";
import { TokenType } from "./tokens/types" import {TokenType} from "./tokens/types";
export function evaluateStringToken( export function evaluateStringToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken {
token: BasicExpressionToken, const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions);
context: TemplateContext
): TemplateToken {
const expr = parseExpression(
token.expression,
context.expressionNamedContexts,
context.expressionFunctions
)
if (!expr) { if (!expr) {
throw new Error("Unexpected empty expression") throw new Error("Unexpected empty expression");
} }
// TODO: Pass in context // TODO: Pass in context
const evaluator = new Evaluator(expr, new Dictionary()) const evaluator = new Evaluator(expr, new Dictionary());
const result = evaluator.evaluate() const result = evaluator.evaluate();
if (!result.primitive) { if (!result.primitive) {
context.error(token, "Expected a string") context.error(token, "Expected a string");
return new StringToken( return new StringToken(token.file, token.range, token.expression, token.definitionInfo);
token.file,
token.range,
token.expression,
token.definitionInfo
)
} }
return new StringToken( return new StringToken(token.file, token.range, result.coerceString(), token.definitionInfo);
token.file,
token.range,
result.coerceString(),
token.definitionInfo
)
} }
export function evaluateSequenceToken( export function evaluateSequenceToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken {
token: BasicExpressionToken, const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions);
context: TemplateContext
): TemplateToken {
const expr = parseExpression(
token.expression,
context.expressionNamedContexts,
context.expressionFunctions
)
if (!expr) { if (!expr) {
throw new Error("Unexpected empty expression") throw new Error("Unexpected empty expression");
} }
// TODO: Pass in context // TODO: Pass in context
const evaluator = new Evaluator(expr, new Dictionary()) const evaluator = new Evaluator(expr, new Dictionary());
const result = evaluator.evaluate() const result = evaluator.evaluate();
const value = convertToTemplateToken(token, result) const value = convertToTemplateToken(token, result);
if (value.templateTokenType !== TokenType.Sequence) { if (value.templateTokenType !== TokenType.Sequence) {
context.error(token, "Expected a sequence") context.error(token, "Expected a sequence");
return new SequenceToken(token.file, token.range, token.definitionInfo) return new SequenceToken(token.file, token.range, token.definitionInfo);
} }
return value return value;
} }
export function evaluateMappingToken( export function evaluateMappingToken(token: BasicExpressionToken, context: TemplateContext): MappingToken {
token: BasicExpressionToken, const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions);
context: TemplateContext
): MappingToken {
const expr = parseExpression(
token.expression,
context.expressionNamedContexts,
context.expressionFunctions
)
if (!expr) { if (!expr) {
throw new Error("Unexpected empty expression") throw new Error("Unexpected empty expression");
} }
// TODO: Pass in context // TODO: Pass in context
const evaluator = new Evaluator(expr, new Dictionary()) const evaluator = new Evaluator(expr, new Dictionary());
const result = evaluator.evaluate() const result = evaluator.evaluate();
const value = convertToTemplateToken(token, result) const value = convertToTemplateToken(token, result);
if (value.templateTokenType !== TokenType.Mapping) { if (value.templateTokenType !== TokenType.Mapping) {
context.error(token, "Expected a mapping") context.error(token, "Expected a mapping");
return new MappingToken(token.file, token.range, token.definitionInfo) return new MappingToken(token.file, token.range, token.definitionInfo);
} }
return value as MappingToken return value as MappingToken;
} }
export function evaluateTemplateToken( export function evaluateTemplateToken(token: BasicExpressionToken, context: TemplateContext): TemplateToken {
token: BasicExpressionToken, const expr = parseExpression(token.expression, context.expressionNamedContexts, context.expressionFunctions);
context: TemplateContext
): TemplateToken {
const expr = parseExpression(
token.expression,
context.expressionNamedContexts,
context.expressionFunctions
)
if (!expr) { if (!expr) {
throw new Error("Unexpected empty expression") throw new Error("Unexpected empty expression");
} }
// TODO: Pass in context // TODO: Pass in context
const evaluator = new Evaluator(expr, new Dictionary()) const evaluator = new Evaluator(expr, new Dictionary());
const result = evaluator.evaluate() const result = evaluator.evaluate();
return convertToTemplateToken(token, result) return convertToTemplateToken(token, result);
} }
function convertToTemplateToken( function convertToTemplateToken(token: BasicExpressionToken, result: ExpressionData): TemplateToken {
token: BasicExpressionToken,
result: ExpressionData
): TemplateToken {
// Literal // Literal
const literal = convertToLiteralToken(token, result) const literal = convertToLiteralToken(token, result);
if (literal) { if (literal) {
return literal return literal;
} }
// TODO: Support expressions that return a sequence or mapping token // TODO: Support expressions that return a sequence or mapping token
@@ -132,80 +87,45 @@ function convertToTemplateToken(
// Leverage the expression SDK to traverse the object // Leverage the expression SDK to traverse the object
switch (result.kind) { switch (result.kind) {
case Kind.Dictionary: { case Kind.Dictionary: {
const mapping = new MappingToken( const mapping = new MappingToken(token.file, token.range, token.definitionInfo);
token.file, for (const {key, value} of result.pairs()) {
token.range, const keyToken = new StringToken(token.file, token.range, key, token.definitionInfo);
token.definitionInfo const valueToken = convertToTemplateToken(token, value);
) mapping.add(keyToken, valueToken);
for (const { key, value } of result.pairs()) {
const keyToken = new StringToken(
token.file,
token.range,
key,
token.definitionInfo
)
const valueToken = convertToTemplateToken(token, value)
mapping.add(keyToken, valueToken)
} }
return mapping return mapping;
} }
case Kind.Array: { case Kind.Array: {
const sequence = new SequenceToken( const sequence = new SequenceToken(token.file, token.range, token.definitionInfo);
token.file,
token.range,
token.definitionInfo
)
for (const value of result.values()) { for (const value of result.values()) {
const itemToken = convertToTemplateToken(token, value) const itemToken = convertToTemplateToken(token, value);
sequence.add(itemToken) sequence.add(itemToken);
} }
return sequence return sequence;
} }
default: default:
throw new Error("Unable to convert the object to a template token") throw new Error("Unable to convert the object to a template token");
} }
} }
function convertToLiteralToken( function convertToLiteralToken(token: BasicExpressionToken, result: ExpressionData): LiteralToken | undefined {
token: BasicExpressionToken,
result: ExpressionData
): LiteralToken | undefined {
switch (result.kind) { switch (result.kind) {
case Kind.Null: case Kind.Null:
return new NullToken(token.file, token.range, token.definitionInfo) return new NullToken(token.file, token.range, token.definitionInfo);
case Kind.Boolean: case Kind.Boolean:
return new BooleanToken( return new BooleanToken(token.file, token.range, result.value, token.definitionInfo);
token.file,
token.range,
result.value,
token.definitionInfo
)
case Kind.Number: case Kind.Number:
return new NumberToken( return new NumberToken(token.file, token.range, result.value, token.definitionInfo);
token.file,
token.range,
result.value,
token.definitionInfo
)
case Kind.String: case Kind.String:
return new StringToken( return new StringToken(token.file, token.range, result.value, token.definitionInfo);
token.file,
token.range,
result.value,
token.definitionInfo
)
} }
return undefined return undefined;
} }
function parseExpression( function parseExpression(expression: string, contexts: string[], functions: FunctionInfo[]): Expr {
expression: string, const lexer = new Lexer(expression);
contexts: string[], const result = lexer.lex();
functions: FunctionInfo[] const p = new Parser(result.tokens, contexts, functions);
): Expr { return p.parse();
const lexer = new Lexer(expression)
const result = lexer.lex()
const p = new Parser(result.tokens, contexts, functions)
return p.parse()
} }
@@ -1,193 +1,156 @@
import { ObjectReader } from "./object-reader" import {ObjectReader} from "./object-reader";
import { EventType, ParseEvent } from "./parse-event" import {EventType, ParseEvent} from "./parse-event";
import { import {LiteralToken, SequenceToken, MappingToken, NullToken, BooleanToken, NumberToken, StringToken} from "./tokens";
LiteralToken,
SequenceToken,
MappingToken,
NullToken,
BooleanToken,
NumberToken,
StringToken,
} from "./tokens"
export class JSONObjectReader implements ObjectReader { export class JSONObjectReader implements ObjectReader {
private readonly _fileId: number | undefined private readonly _fileId: number | undefined;
private readonly _generator: Generator<ParseEvent, void> private readonly _generator: Generator<ParseEvent, void>;
private _current: IteratorResult<ParseEvent, void> private _current: IteratorResult<ParseEvent, void>;
public constructor(fileId: number | undefined, input: string) { public constructor(fileId: number | undefined, input: string) {
this._fileId = fileId this._fileId = fileId;
const value = JSON.parse(input) const value = JSON.parse(input);
this._generator = this.getParseEvents(value, true) this._generator = this.getParseEvents(value, true);
this._current = this._generator.next() this._current = this._generator.next();
} }
public allowLiteral(): LiteralToken | undefined { public allowLiteral(): LiteralToken | undefined {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value const parseEvent = this._current.value;
if (parseEvent.type === EventType.Literal) { if (parseEvent.type === EventType.Literal) {
this._current = this._generator.next() this._current = this._generator.next();
return parseEvent.token as LiteralToken return parseEvent.token as LiteralToken;
} }
} }
return undefined return undefined;
} }
public allowSequenceStart(): SequenceToken | undefined { public allowSequenceStart(): SequenceToken | undefined {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value const parseEvent = this._current.value;
if (parseEvent.type === EventType.SequenceStart) { if (parseEvent.type === EventType.SequenceStart) {
this._current = this._generator.next() this._current = this._generator.next();
return parseEvent.token as SequenceToken return parseEvent.token as SequenceToken;
} }
} }
return undefined return undefined;
} }
public allowSequenceEnd(): boolean { public allowSequenceEnd(): boolean {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value const parseEvent = this._current.value;
if (parseEvent.type === EventType.SequenceEnd) { if (parseEvent.type === EventType.SequenceEnd) {
this._current = this._generator.next() this._current = this._generator.next();
return true return true;
} }
} }
return false return false;
} }
public allowMappingStart(): MappingToken | undefined { public allowMappingStart(): MappingToken | undefined {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value const parseEvent = this._current.value;
if (parseEvent.type === EventType.MappingStart) { if (parseEvent.type === EventType.MappingStart) {
this._current = this._generator.next() this._current = this._generator.next();
return parseEvent.token as MappingToken return parseEvent.token as MappingToken;
} }
} }
return undefined return undefined;
} }
public allowMappingEnd(): boolean { public allowMappingEnd(): boolean {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value const parseEvent = this._current.value;
if (parseEvent.type === EventType.MappingEnd) { if (parseEvent.type === EventType.MappingEnd) {
this._current = this._generator.next() this._current = this._generator.next();
return true return true;
} }
} }
return false return false;
} }
public validateEnd(): void { public validateEnd(): void {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value as ParseEvent const parseEvent = this._current.value as ParseEvent;
if (parseEvent.type === EventType.DocumentEnd) { if (parseEvent.type === EventType.DocumentEnd) {
this._current = this._generator.next() this._current = this._generator.next();
return return;
} }
} }
throw new Error("Expected end of reader") throw new Error("Expected end of reader");
} }
public validateStart(): void { public validateStart(): void {
if (!this._current.done) { if (!this._current.done) {
const parseEvent = this._current.value as ParseEvent const parseEvent = this._current.value as ParseEvent;
if (parseEvent.type === EventType.DocumentStart) { if (parseEvent.type === EventType.DocumentStart) {
this._current = this._generator.next() this._current = this._generator.next();
return return;
} }
} }
throw new Error("Expected start of reader") throw new Error("Expected start of reader");
} }
/** /**
* Returns all tokens (depth first) * Returns all tokens (depth first)
*/ */
private *getParseEvents( private *getParseEvents(value: any, root?: boolean): Generator<ParseEvent, void> {
value: any,
root?: boolean
): Generator<ParseEvent, void> {
if (root) { if (root) {
yield new ParseEvent(EventType.DocumentStart, undefined) yield new ParseEvent(EventType.DocumentStart, undefined);
} }
switch (typeof value) { switch (typeof value) {
case "undefined": case "undefined":
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new NullToken(this._fileId, undefined, undefined));
EventType.Literal, break;
new NullToken(this._fileId, undefined, undefined)
)
break
case "boolean": case "boolean":
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new BooleanToken(this._fileId, undefined, value, undefined));
EventType.Literal, break;
new BooleanToken(this._fileId, undefined, value, undefined)
)
break
case "number": case "number":
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new NumberToken(this._fileId, undefined, value, undefined));
EventType.Literal, break;
new NumberToken(this._fileId, undefined, value, undefined)
)
break
case "string": case "string":
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new StringToken(this._fileId, undefined, value, undefined));
EventType.Literal, break;
new StringToken(this._fileId, undefined, value, undefined)
)
break
case "object": case "object":
// null // null
if (value === null) { if (value === null) {
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new NullToken(this._fileId, undefined, undefined));
EventType.Literal,
new NullToken(this._fileId, undefined, undefined)
)
} }
// array // array
else if (Object.prototype.hasOwnProperty.call(value, "length")) { else if (Object.prototype.hasOwnProperty.call(value, "length")) {
yield new ParseEvent( yield new ParseEvent(EventType.SequenceStart, new SequenceToken(this._fileId, undefined, undefined));
EventType.SequenceStart,
new SequenceToken(this._fileId, undefined, undefined)
)
for (const item of value as []) { for (const item of value as []) {
for (const e of this.getParseEvents(item)) { for (const e of this.getParseEvents(item)) {
yield e yield e;
} }
} }
yield new ParseEvent(EventType.SequenceEnd, undefined) yield new ParseEvent(EventType.SequenceEnd, undefined);
} }
// object // object
else { else {
yield new ParseEvent( yield new ParseEvent(EventType.MappingStart, new MappingToken(this._fileId, undefined, undefined));
EventType.MappingStart,
new MappingToken(this._fileId, undefined, undefined)
)
for (const key of Object.keys(value)) { for (const key of Object.keys(value)) {
yield new ParseEvent( yield new ParseEvent(EventType.Literal, new StringToken(this._fileId, undefined, key, undefined));
EventType.Literal,
new StringToken(this._fileId, undefined, key, undefined)
)
for (const e of this.getParseEvents(value[key])) { for (const e of this.getParseEvents(value[key])) {
yield e yield e;
} }
} }
yield new ParseEvent(EventType.MappingEnd, undefined) yield new ParseEvent(EventType.MappingEnd, undefined);
} }
break break;
default: default:
throw new Error( throw new Error(`Unexpected value type '${typeof value}' when reading object`);
`Unexpected value type '${typeof value}' when reading object`
)
} }
if (root) { if (root) {
yield new ParseEvent(EventType.DocumentEnd, undefined) yield new ParseEvent(EventType.DocumentEnd, undefined);
} }
} }
} }
@@ -1,22 +1,22 @@
import { LiteralToken, SequenceToken, MappingToken } from "./tokens" import {LiteralToken, SequenceToken, MappingToken} from "./tokens";
/** /**
* Interface for reading a source object (or file). * Interface for reading a source object (or file).
* This interface is used by TemplateReader to build a TemplateToken DOM. * This interface is used by TemplateReader to build a TemplateToken DOM.
*/ */
export interface ObjectReader { export interface ObjectReader {
allowLiteral(): LiteralToken | undefined allowLiteral(): LiteralToken | undefined;
// maybe rename these since we don't have out params // maybe rename these since we don't have out params
allowSequenceStart(): SequenceToken | undefined allowSequenceStart(): SequenceToken | undefined;
allowSequenceEnd(): boolean allowSequenceEnd(): boolean;
allowMappingStart(): MappingToken | undefined allowMappingStart(): MappingToken | undefined;
allowMappingEnd(): boolean allowMappingEnd(): boolean;
validateStart(): void validateStart(): void;
validateEnd(): void validateEnd(): void;
} }
@@ -1,11 +1,11 @@
import { TemplateToken } from "./tokens" import {TemplateToken} from "./tokens";
export class ParseEvent { export class ParseEvent {
public readonly type: EventType public readonly type: EventType;
public readonly token: TemplateToken | undefined public readonly token: TemplateToken | undefined;
public constructor(type: EventType, token?: TemplateToken | undefined) { public constructor(type: EventType, token?: TemplateToken | undefined) {
this.type = type this.type = type;
this.token = token this.token = token;
} }
} }
@@ -16,5 +16,5 @@ export enum EventType {
MappingStart, MappingStart,
MappingEnd, MappingEnd,
DocumentStart, DocumentStart,
DocumentEnd, DocumentEnd
} }
@@ -1,51 +1,43 @@
import { TemplateSchema } from "." import {TemplateSchema} from ".";
import { DEFINITION, BOOLEAN } from "../template-constants" import {DEFINITION, BOOLEAN} from "../template-constants";
import { MappingToken, LiteralToken } from "../tokens" import {MappingToken, LiteralToken} from "../tokens";
import { TokenType } from "../tokens/types" import {TokenType} from "../tokens/types";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { ScalarDefinition } from "./scalar-definition" import {ScalarDefinition} from "./scalar-definition";
export class BooleanDefinition extends ScalarDefinition { export class BooleanDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case BOOLEAN: { case BOOLEAN: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${BOOLEAN}`);
`${DEFINITION} ${BOOLEAN}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${BOOLEAN} key`);
`${DEFINITION} ${BOOLEAN} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
default: default:
// throws // throws
mappingKey.assertUnexpectedValue( mappingKey.assertUnexpectedValue(`${DEFINITION} ${BOOLEAN} key`);
`${DEFINITION} ${BOOLEAN} key` break;
)
break
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.Boolean return DefinitionType.Boolean;
} }
public override isMatch(literal: LiteralToken): boolean { public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Boolean return literal.templateTokenType === TokenType.Boolean;
} }
public override validate(schema: TemplateSchema, name: string): void {} public override validate(schema: TemplateSchema, name: string): void {}
@@ -1,63 +1,57 @@
import { TemplateSchema } from "." import {TemplateSchema} from ".";
import { Definition } from "./definition" import {Definition} from "./definition";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { ScalarDefinition } from "./scalar-definition" import {ScalarDefinition} from "./scalar-definition";
export class DefinitionInfo { export class DefinitionInfo {
private readonly _schema: TemplateSchema private readonly _schema: TemplateSchema;
public readonly isDefinitionInfo = true public readonly isDefinitionInfo = true;
public readonly definition: Definition public readonly definition: Definition;
public readonly allowedContext: string[] public readonly allowedContext: string[];
public constructor(schema: TemplateSchema, name: string) public constructor(schema: TemplateSchema, name: string);
public constructor(parent: DefinitionInfo, name: string) public constructor(parent: DefinitionInfo, name: string);
public constructor(parent: DefinitionInfo, definition: Definition) public constructor(parent: DefinitionInfo, definition: Definition);
public constructor( public constructor(schemaOrParent: TemplateSchema | DefinitionInfo, nameOrDefinition: string | Definition) {
schemaOrParent: TemplateSchema | DefinitionInfo,
nameOrDefinition: string | Definition
) {
const parent: DefinitionInfo | undefined = const parent: DefinitionInfo | undefined =
(schemaOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true (schemaOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true
? (schemaOrParent as DefinitionInfo) ? (schemaOrParent as DefinitionInfo)
: undefined : undefined;
this._schema = this._schema = parent === undefined ? (schemaOrParent as TemplateSchema) : parent._schema;
parent === undefined ? (schemaOrParent as TemplateSchema) : parent._schema
// Lookup the definition if a key was passed in // Lookup the definition if a key was passed in
this.definition = this.definition =
typeof nameOrDefinition === "string" typeof nameOrDefinition === "string" ? this._schema.getDefinition(nameOrDefinition) : nameOrDefinition;
? this._schema.getDefinition(nameOrDefinition)
: nameOrDefinition
// Record allowed context // Record allowed context
if (this.definition.readerContext.length > 0) { if (this.definition.readerContext.length > 0) {
this.allowedContext = [] this.allowedContext = [];
// Copy parent allowed context // Copy parent allowed context
const upperSeen: { [upper: string]: boolean } = {} const upperSeen: {[upper: string]: boolean} = {};
for (const context of parent?.allowedContext ?? []) { for (const context of parent?.allowedContext ?? []) {
this.allowedContext.push(context) this.allowedContext.push(context);
upperSeen[context.toUpperCase()] = true upperSeen[context.toUpperCase()] = true;
} }
// Append context if unseen // Append context if unseen
for (const context of this.definition.readerContext) { for (const context of this.definition.readerContext) {
const upper = context.toUpperCase() const upper = context.toUpperCase();
if (!upperSeen[upper]) { if (!upperSeen[upper]) {
this.allowedContext.push(context) this.allowedContext.push(context);
upperSeen[upper] = true upperSeen[upper] = true;
} }
} }
} else { } else {
this.allowedContext = parent?.allowedContext ?? [] this.allowedContext = parent?.allowedContext ?? [];
} }
} }
public getScalarDefinitions(): ScalarDefinition[] { public getScalarDefinitions(): ScalarDefinition[] {
return this._schema.getScalarDefinitions(this.definition) return this._schema.getScalarDefinitions(this.definition);
} }
public getDefinitionsOfType(type: DefinitionType): Definition[] { public getDefinitionsOfType(type: DefinitionType): Definition[] {
return this._schema.getDefinitionsOfType(this.definition, type) return this._schema.getDefinitionsOfType(this.definition, type);
} }
} }
@@ -6,5 +6,5 @@ export enum DefinitionType {
Sequence, Sequence,
Mapping, Mapping,
OneOf, OneOf,
AllowedValues, AllowedValues
} }
@@ -1,7 +1,7 @@
import { CONTEXT, DEFINITION, DESCRIPTION } from "../template-constants" import {CONTEXT, DEFINITION, DESCRIPTION} from "../template-constants";
import { MappingToken } from "../tokens" import {MappingToken} from "../tokens";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
/** /**
* Defines the allowable schema for a user defined type * Defines the allowable schema for a user defined type
@@ -11,75 +11,68 @@ export abstract class Definition {
* Used by the template reader to determine allowed expression values and functions. * Used by the template reader to determine allowed expression values and functions.
* Also used by the template reader to validate function min/max parameters. * Also used by the template reader to validate function min/max parameters.
*/ */
public readonly readerContext: string[] = [] public readonly readerContext: string[] = [];
/** /**
* Used by the template evaluator to determine allowed expression values and functions. * Used by the template evaluator to determine allowed expression values and functions.
* The min/max parameter info is omitted. * The min/max parameter info is omitted.
*/ */
public readonly evaluatorContext: string[] = [] public readonly evaluatorContext: string[] = [];
// A key to uniquely identify a definition // A key to uniquely identify a definition
public readonly key: string public readonly key: string;
public readonly description: string | undefined public readonly description: string | undefined;
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
this.key = key this.key = key;
if (definition) { if (definition) {
for (let i = 0; i < definition.count; ) { for (let i = 0; i < definition.count; ) {
const definitionKey = definition const definitionKey = definition.get(i).key.assertString(`${DEFINITION} key`);
.get(i)
.key.assertString(`${DEFINITION} key`)
switch (definitionKey.value) { switch (definitionKey.value) {
case CONTEXT: { case CONTEXT: {
const context = definition const context = definition.get(i).value.assertSequence(`${DEFINITION} ${CONTEXT}`);
.get(i) definition.remove(i);
.value.assertSequence(`${DEFINITION} ${CONTEXT}`) const seenReaderContext: {[key: string]: boolean} = {};
definition.remove(i) const seenEvaluatorContext: {[key: string]: boolean} = {};
const seenReaderContext: { [key: string]: boolean } = {}
const seenEvaluatorContext: { [key: string]: boolean } = {}
for (const item of context) { for (const item of context) {
const itemStr = item.assertString(`${CONTEXT} item`).value const itemStr = item.assertString(`${CONTEXT} item`).value;
const upperItemStr = itemStr.toUpperCase() const upperItemStr = itemStr.toUpperCase();
if (seenReaderContext[upperItemStr]) { if (seenReaderContext[upperItemStr]) {
throw new Error(`Duplicate context item '${itemStr}'`) throw new Error(`Duplicate context item '${itemStr}'`);
} }
seenReaderContext[upperItemStr] = true seenReaderContext[upperItemStr] = true;
this.readerContext.push(itemStr) this.readerContext.push(itemStr);
// Remove min/max parameter info // Remove min/max parameter info
const paramIndex = itemStr.indexOf("(") const paramIndex = itemStr.indexOf("(");
const modifiedItemStr = const modifiedItemStr = paramIndex > 0 ? itemStr.substr(0, paramIndex + 1) + ")" : itemStr;
paramIndex > 0 const upperModifiedItemStr = modifiedItemStr.toUpperCase();
? itemStr.substr(0, paramIndex + 1) + ")"
: itemStr
const upperModifiedItemStr = modifiedItemStr.toUpperCase()
if (seenEvaluatorContext[upperModifiedItemStr]) { if (seenEvaluatorContext[upperModifiedItemStr]) {
throw new Error(`Duplicate context item '${modifiedItemStr}'`) throw new Error(`Duplicate context item '${modifiedItemStr}'`);
} }
seenEvaluatorContext[upperModifiedItemStr] = true seenEvaluatorContext[upperModifiedItemStr] = true;
this.evaluatorContext.push(modifiedItemStr) this.evaluatorContext.push(modifiedItemStr);
} }
break break;
} }
case DESCRIPTION: { case DESCRIPTION: {
const value = definition.get(i).value const value = definition.get(i).value;
this.description = value.assertString(DESCRIPTION).value this.description = value.assertString(DESCRIPTION).value;
definition.remove(i) definition.remove(i);
break break;
} }
default: { default: {
i++ i++;
break break;
} }
} }
} }
} }
} }
public abstract get definitionType(): DefinitionType public abstract get definitionType(): DefinitionType;
public abstract validate(schema: TemplateSchema, name: string): void public abstract validate(schema: TemplateSchema, name: string): void;
} }
@@ -1 +1 @@
export { TemplateSchema } from "./template-schema" export {TemplateSchema} from "./template-schema";
@@ -1,117 +1,90 @@
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
import { import {DEFINITION, MAPPING, PROPERTIES, LOOSE_KEY_TYPE, LOOSE_VALUE_TYPE} from "../template-constants";
DEFINITION, import {MappingToken} from "../tokens";
MAPPING, import {Definition} from "./definition";
PROPERTIES, import {DefinitionType} from "./definition-type";
LOOSE_KEY_TYPE, import {PropertyDefinition} from "./property-definition";
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 { export class MappingDefinition extends Definition {
public readonly properties: { [key: string]: PropertyDefinition } = {} public readonly properties: {[key: string]: PropertyDefinition} = {};
public looseKeyType = "" public looseKeyType = "";
public looseValueType = "" public looseValueType = "";
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case MAPPING: { case MAPPING: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${MAPPING}`);
`${DEFINITION} ${MAPPING}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${MAPPING} key`);
`${DEFINITION} ${MAPPING} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
case PROPERTIES: { case PROPERTIES: {
const properties = mappingPair.value.assertMapping( const properties = mappingPair.value.assertMapping(`${DEFINITION} ${MAPPING} ${PROPERTIES}`);
`${DEFINITION} ${MAPPING} ${PROPERTIES}`
)
for (const propertiesPair of properties) { for (const propertiesPair of properties) {
const propertyName = propertiesPair.key.assertString( const propertyName = propertiesPair.key.assertString(`${DEFINITION} ${MAPPING} ${PROPERTIES} key`);
`${DEFINITION} ${MAPPING} ${PROPERTIES} key` this.properties[propertyName.value] = new PropertyDefinition(propertiesPair.value);
)
this.properties[propertyName.value] =
new PropertyDefinition(propertiesPair.value)
} }
break break;
} }
case LOOSE_KEY_TYPE: { case LOOSE_KEY_TYPE: {
const looseKeyType = mappingPair.value.assertString( const looseKeyType = mappingPair.value.assertString(`${DEFINITION} ${MAPPING} ${LOOSE_KEY_TYPE}`);
`${DEFINITION} ${MAPPING} ${LOOSE_KEY_TYPE}` this.looseKeyType = looseKeyType.value;
) break;
this.looseKeyType = looseKeyType.value
break
} }
case LOOSE_VALUE_TYPE: { case LOOSE_VALUE_TYPE: {
const looseValueType = mappingPair.value.assertString( const looseValueType = mappingPair.value.assertString(`${DEFINITION} ${MAPPING} ${LOOSE_VALUE_TYPE}`);
`${DEFINITION} ${MAPPING} ${LOOSE_VALUE_TYPE}` this.looseValueType = looseValueType.value;
) break;
this.looseValueType = looseValueType.value
break
} }
default: default:
// throws // throws
mappingKey.assertUnexpectedValue( mappingKey.assertUnexpectedValue(`${DEFINITION} ${MAPPING} key`);
`${DEFINITION} ${MAPPING} key` break;
)
break
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.Mapping return DefinitionType.Mapping;
} }
public override validate(schema: TemplateSchema, name: string): void { public override validate(schema: TemplateSchema, name: string): void {
// Lookup loose key type // Lookup loose key type
if (this.looseKeyType) { if (this.looseKeyType) {
schema.getDefinition(this.looseKeyType) schema.getDefinition(this.looseKeyType);
// Lookup loose value type // Lookup loose value type
if (this.looseValueType) { if (this.looseValueType) {
schema.getDefinition(this.looseValueType) schema.getDefinition(this.looseValueType);
} else { } else {
throw new Error( throw new Error(
`Property '${LOOSE_KEY_TYPE}' is defined but '${LOOSE_VALUE_TYPE}' is not defined on '${name}'` `Property '${LOOSE_KEY_TYPE}' is defined but '${LOOSE_VALUE_TYPE}' is not defined on '${name}'`
) );
} }
} }
// Otherwise validate loose value type not be defined // Otherwise validate loose value type not be defined
else if (this.looseValueType) { else if (this.looseValueType) {
throw new Error( throw new Error(`Property '${LOOSE_VALUE_TYPE}' is defined but '${LOOSE_KEY_TYPE}' is not defined on '${name}'`);
`Property '${LOOSE_VALUE_TYPE}' is defined but '${LOOSE_KEY_TYPE}' is not defined on '${name}'`
)
} }
// Lookup each property // Lookup each property
for (const propertyName of Object.keys(this.properties)) { for (const propertyName of Object.keys(this.properties)) {
const propertyDef = this.properties[propertyName] const propertyDef = this.properties[propertyName];
if (!propertyDef.type) { if (!propertyDef.type) {
throw new Error( throw new Error(`Type not specified for the property '${propertyName}' on '${name}'`);
`Type not specified for the property '${propertyName}' on '${name}'`
)
} }
schema.getDefinition(propertyDef.type) schema.getDefinition(propertyDef.type);
} }
} }
} }
@@ -1,49 +1,43 @@
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
import { DEFINITION, NULL } from "../template-constants" import {DEFINITION, NULL} from "../template-constants";
import { MappingToken, LiteralToken } from "../tokens" import {MappingToken, LiteralToken} from "../tokens";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { ScalarDefinition } from "./scalar-definition" import {ScalarDefinition} from "./scalar-definition";
import { TokenType } from "../tokens/types" import {TokenType} from "../tokens/types";
export class NullDefinition extends ScalarDefinition { export class NullDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case NULL: { case NULL: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${NULL}`);
`${DEFINITION} ${NULL}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${NULL} key`);
`${DEFINITION} ${NULL} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
default: default:
// throws // throws
mappingKey.assertUnexpectedValue(`${DEFINITION} ${NULL} key`) mappingKey.assertUnexpectedValue(`${DEFINITION} ${NULL} key`);
break break;
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.Null return DefinitionType.Null;
} }
public override isMatch(literal: LiteralToken): boolean { public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Null return literal.templateTokenType === TokenType.Null;
} }
public override validate(schema: TemplateSchema, name: string): void {} public override validate(schema: TemplateSchema, name: string): void {}
@@ -1,51 +1,43 @@
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
import { DEFINITION, NUMBER } from "../template-constants" import {DEFINITION, NUMBER} from "../template-constants";
import { MappingToken, LiteralToken } from "../tokens" import {MappingToken, LiteralToken} from "../tokens";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { ScalarDefinition } from "./scalar-definition" import {ScalarDefinition} from "./scalar-definition";
import { TokenType } from "../tokens/types" import {TokenType} from "../tokens/types";
export class NumberDefinition extends ScalarDefinition { export class NumberDefinition extends ScalarDefinition {
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case NUMBER: { case NUMBER: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${NUMBER}`);
`${DEFINITION} ${NUMBER}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${NUMBER} key`);
`${DEFINITION} ${NUMBER} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
default: default:
// throws // throws
mappingKey.assertUnexpectedValue( mappingKey.assertUnexpectedValue(`${DEFINITION} ${NUMBER} key`);
`${DEFINITION} ${NUMBER} key` break;
)
break
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.Number return DefinitionType.Number;
} }
public override isMatch(literal: LiteralToken): boolean { public override isMatch(literal: LiteralToken): boolean {
return literal.templateTokenType === TokenType.Number return literal.templateTokenType === TokenType.Number;
} }
public override validate(schema: TemplateSchema, name: string): void {} public override validate(schema: TemplateSchema, name: string): void {}
@@ -1,4 +1,4 @@
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
import { import {
DEFINITION, DEFINITION,
ONE_OF, ONE_OF,
@@ -9,180 +9,151 @@ import {
SCALAR, SCALAR,
CONSTANT, CONSTANT,
LOOSE_KEY_TYPE, LOOSE_KEY_TYPE,
ALLOWED_VALUES, ALLOWED_VALUES
} from "../template-constants" } from "../template-constants";
import { MappingToken } from "../tokens" import {MappingToken} from "../tokens";
import { BooleanDefinition } from "./boolean-definition" import {BooleanDefinition} from "./boolean-definition";
import { Definition } from "./definition" import {Definition} from "./definition";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { MappingDefinition } from "./mapping-definition" import {MappingDefinition} from "./mapping-definition";
import { NullDefinition } from "./null-definition" import {NullDefinition} from "./null-definition";
import { NumberDefinition } from "./number-definition" import {NumberDefinition} from "./number-definition";
import { SequenceDefinition } from "./sequence-definition" import {SequenceDefinition} from "./sequence-definition";
import { StringDefinition } from "./string-definition" import {StringDefinition} from "./string-definition";
import { PropertyDefinition } from "./property-definition" import {PropertyDefinition} from "./property-definition";
/** /**
* Must resolve to exactly one of the referenced definitions * Must resolve to exactly one of the referenced definitions
*/ */
export class OneOfDefinition extends Definition { export class OneOfDefinition extends Definition {
public readonly oneOf: string[] = [] public readonly oneOf: string[] = [];
public readonly oneOfPrefix: string[] = [] public readonly oneOfPrefix: string[] = [];
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case ONE_OF: { case ONE_OF: {
const oneOf = definitionPair.value.assertSequence( const oneOf = definitionPair.value.assertSequence(`${DEFINITION} ${ONE_OF}`);
`${DEFINITION} ${ONE_OF}`
)
for (const item of oneOf) { for (const item of oneOf) {
const oneOfItem = item.assertString( const oneOfItem = item.assertString(`${DEFINITION} ${ONE_OF} item`);
`${DEFINITION} ${ONE_OF} item` this.oneOf.push(oneOfItem.value);
)
this.oneOf.push(oneOfItem.value)
} }
break break;
} }
case ALLOWED_VALUES: { case ALLOWED_VALUES: {
const oneOf = definitionPair.value.assertSequence( const oneOf = definitionPair.value.assertSequence(`${DEFINITION} ${ALLOWED_VALUES}`);
`${DEFINITION} ${ALLOWED_VALUES}`
)
for (const item of oneOf) { for (const item of oneOf) {
const oneOfItem = item.assertString( const oneOfItem = item.assertString(`${DEFINITION} ${ONE_OF} item`);
`${DEFINITION} ${ONE_OF} item` this.oneOf.push(this.key + "-" + oneOfItem.value);
)
this.oneOf.push(this.key + "-" + oneOfItem.value)
} }
break break;
} }
default: default:
// throws // throws
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) definitionKey.assertUnexpectedValue(`${DEFINITION} key`);
break break;
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.OneOf return DefinitionType.OneOf;
} }
public override validate(schema: TemplateSchema, name: string): void { public override validate(schema: TemplateSchema, name: string): void {
if (this.oneOf.length === 0) { if (this.oneOf.length === 0) {
throw new Error(`'${name}' does not contain any references`) throw new Error(`'${name}' does not contain any references`);
} }
let foundLooseKeyType = false let foundLooseKeyType = false;
const mappingDefinitions: MappingDefinition[] = [] const mappingDefinitions: MappingDefinition[] = [];
let allowedValuesDefinition: OneOfDefinition | undefined let allowedValuesDefinition: OneOfDefinition | undefined;
let sequenceDefinition: SequenceDefinition | undefined let sequenceDefinition: SequenceDefinition | undefined;
let nullDefinition: NullDefinition | undefined let nullDefinition: NullDefinition | undefined;
let booleanDefinition: BooleanDefinition | undefined let booleanDefinition: BooleanDefinition | undefined;
let numberDefinition: NumberDefinition | undefined let numberDefinition: NumberDefinition | undefined;
const stringDefinitions: StringDefinition[] = [] const stringDefinitions: StringDefinition[] = [];
const seenNestedTypes: { [key: string]: boolean } = {} const seenNestedTypes: {[key: string]: boolean} = {};
for (const nestedType of this.oneOf) { for (const nestedType of this.oneOf) {
if (seenNestedTypes[nestedType]) { if (seenNestedTypes[nestedType]) {
throw new Error( throw new Error(`'${name}' contains duplicate nested type '${nestedType}'`);
`'${name}' contains duplicate nested type '${nestedType}'`
)
} }
seenNestedTypes[nestedType] = true seenNestedTypes[nestedType] = true;
const nestedDefinition = schema.getDefinition(nestedType) const nestedDefinition = schema.getDefinition(nestedType);
if (nestedDefinition.readerContext.length > 0) { if (nestedDefinition.readerContext.length > 0) {
throw new Error( throw new Error(
`'${name}' is a one-of definition and references another definition that defines context. This is currently not supported.` `'${name}' is a one-of definition and references another definition that defines context. This is currently not supported.`
) );
} }
switch (nestedDefinition.definitionType) { switch (nestedDefinition.definitionType) {
case DefinitionType.Mapping: { case DefinitionType.Mapping: {
const mappingDefinition = nestedDefinition as MappingDefinition const mappingDefinition = nestedDefinition as MappingDefinition;
mappingDefinitions.push(mappingDefinition) mappingDefinitions.push(mappingDefinition);
if (mappingDefinition.looseKeyType) { if (mappingDefinition.looseKeyType) {
foundLooseKeyType = true foundLooseKeyType = true;
} }
break break;
} }
case DefinitionType.Sequence: { case DefinitionType.Sequence: {
// Multiple sequence definitions not allowed // Multiple sequence definitions not allowed
if (sequenceDefinition) { if (sequenceDefinition) {
throw new Error( throw new Error(`'${name}' refers to more than one definition of type '${SEQUENCE}'`);
`'${name}' refers to more than one definition of type '${SEQUENCE}'`
)
} }
sequenceDefinition = nestedDefinition as SequenceDefinition sequenceDefinition = nestedDefinition as SequenceDefinition;
break break;
} }
case DefinitionType.Null: { case DefinitionType.Null: {
// Multiple null definitions not allowed // Multiple null definitions not allowed
if (nullDefinition) { if (nullDefinition) {
throw new Error( throw new Error(`'${name}' refers to more than one definition of type '${NULL}'`);
`'${name}' refers to more than one definition of type '${NULL}'`
)
} }
nullDefinition = nestedDefinition as NullDefinition nullDefinition = nestedDefinition as NullDefinition;
break break;
} }
case DefinitionType.Boolean: { case DefinitionType.Boolean: {
// Multiple boolean definitions not allowed // Multiple boolean definitions not allowed
if (booleanDefinition) { if (booleanDefinition) {
throw new Error( throw new Error(`'${name}' refers to more than one definition of type '${BOOLEAN}'`);
`'${name}' refers to more than one definition of type '${BOOLEAN}'`
)
} }
booleanDefinition = nestedDefinition as BooleanDefinition booleanDefinition = nestedDefinition as BooleanDefinition;
break break;
} }
case DefinitionType.Number: { case DefinitionType.Number: {
// Multiple number definitions not allowed // Multiple number definitions not allowed
if (numberDefinition) { if (numberDefinition) {
throw new Error( throw new Error(`'${name}' refers to more than one definition of type '${NUMBER}'`);
`'${name}' refers to more than one definition of type '${NUMBER}'`
)
} }
numberDefinition = nestedDefinition as NumberDefinition numberDefinition = nestedDefinition as NumberDefinition;
break break;
} }
case DefinitionType.String: { case DefinitionType.String: {
const stringDefinition = nestedDefinition as StringDefinition const stringDefinition = nestedDefinition as StringDefinition;
// Multiple string definitions // Multiple string definitions
if ( if (stringDefinitions.length > 0 && (!stringDefinitions[0].constant || !stringDefinition.constant)) {
stringDefinitions.length > 0 && throw new Error(`'${name}' refers to more than one '${SCALAR}', but some do not set '${CONSTANT}'`);
(!stringDefinitions[0].constant || !stringDefinition.constant)
) {
throw new Error(
`'${name}' refers to more than one '${SCALAR}', but some do not set '${CONSTANT}'`
)
} }
stringDefinitions.push(stringDefinition) stringDefinitions.push(stringDefinition);
break break;
} }
case DefinitionType.OneOf: { case DefinitionType.OneOf: {
// Multiple allowed-values definitions not allowed // Multiple allowed-values definitions not allowed
if (allowedValuesDefinition) { if (allowedValuesDefinition) {
throw new Error( throw new Error(`'${name}' contains multiple allowed-values definitions`);
`'${name}' contains multiple allowed-values definitions`
)
} }
allowedValuesDefinition = nestedDefinition as OneOfDefinition allowedValuesDefinition = nestedDefinition as OneOfDefinition;
break break;
} }
default: default:
throw new Error( throw new Error(`'${name}' refers to a definition with type '${nestedDefinition.definitionType}'`);
`'${name}' refers to a definition with type '${nestedDefinition.definitionType}'`
)
} }
} }
@@ -190,31 +161,30 @@ export class OneOfDefinition extends Definition {
if (foundLooseKeyType) { if (foundLooseKeyType) {
throw new Error( throw new Error(
`'${name}' refers to two mappings and at least one sets '${LOOSE_KEY_TYPE}'. This is not currently supported.` `'${name}' refers to two mappings and at least one sets '${LOOSE_KEY_TYPE}'. This is not currently supported.`
) );
} }
const seenProperties: { [key: string]: PropertyDefinition } = {} const seenProperties: {[key: string]: PropertyDefinition} = {};
for (const mappingDefinition of mappingDefinitions) { for (const mappingDefinition of mappingDefinitions) {
for (const propertyName of Object.keys(mappingDefinition.properties)) { for (const propertyName of Object.keys(mappingDefinition.properties)) {
const newPropertyDef = mappingDefinition.properties[propertyName] const newPropertyDef = mappingDefinition.properties[propertyName];
// Already seen // Already seen
const existingPropertyDef: PropertyDefinition | undefined = const existingPropertyDef: PropertyDefinition | undefined = seenProperties[propertyName];
seenProperties[propertyName]
if (existingPropertyDef) { if (existingPropertyDef) {
// Types match // Types match
if (existingPropertyDef.type === newPropertyDef.type) { if (existingPropertyDef.type === newPropertyDef.type) {
continue continue;
} }
// Collision // Collision
throw new Error( 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.` `'${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 // New
else { else {
seenProperties[propertyName] = newPropertyDef seenProperties[propertyName] = newPropertyDef;
} }
} }
} }
@@ -1,44 +1,31 @@
import { import {MAPPING_PROPERTY_VALUE, TYPE, REQUIRED, DESCRIPTION} from "../template-constants";
MAPPING_PROPERTY_VALUE, import {TemplateToken, StringToken} from "../tokens";
TYPE, import {TokenType} from "../tokens/types";
REQUIRED,
DESCRIPTION,
} from "../template-constants"
import { TemplateToken, StringToken } from "../tokens"
import { TokenType } from "../tokens/types"
export class PropertyDefinition { export class PropertyDefinition {
public readonly type: string = "" public readonly type: string = "";
public readonly required: boolean = false public readonly required: boolean = false;
public readonly description: string | undefined public readonly description: string | undefined;
public constructor(token: TemplateToken) { public constructor(token: TemplateToken) {
if (token.templateTokenType === TokenType.String) { if (token.templateTokenType === TokenType.String) {
this.type = (token as StringToken).value this.type = (token as StringToken).value;
} else { } else {
const mapping = token.assertMapping(MAPPING_PROPERTY_VALUE) const mapping = token.assertMapping(MAPPING_PROPERTY_VALUE);
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${MAPPING_PROPERTY_VALUE} key`);
`${MAPPING_PROPERTY_VALUE} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
case TYPE: case TYPE:
this.type = mappingPair.value.assertString( this.type = mappingPair.value.assertString(`${MAPPING_PROPERTY_VALUE} ${TYPE}`).value;
`${MAPPING_PROPERTY_VALUE} ${TYPE}` break;
).value
break
case REQUIRED: case REQUIRED:
this.required = mappingPair.value.assertBoolean( this.required = mappingPair.value.assertBoolean(`${MAPPING_PROPERTY_VALUE} ${REQUIRED}`).value;
`${MAPPING_PROPERTY_VALUE} ${REQUIRED}` break;
).value
break
case DESCRIPTION: case DESCRIPTION:
this.description = mappingPair.value.assertString( this.description = mappingPair.value.assertString(`${MAPPING_PROPERTY_VALUE} ${DESCRIPTION}`).value;
`${MAPPING_PROPERTY_VALUE} ${DESCRIPTION}` break;
).value
break
default: default:
mappingKey.assertUnexpectedValue(`${MAPPING_PROPERTY_VALUE} key`) // throws mappingKey.assertUnexpectedValue(`${MAPPING_PROPERTY_VALUE} key`); // throws
} }
} }
} }
@@ -1,10 +1,10 @@
import { LiteralToken, MappingToken } from "../tokens" import {LiteralToken, MappingToken} from "../tokens";
import { Definition } from "./definition" import {Definition} from "./definition";
export abstract class ScalarDefinition extends Definition { export abstract class ScalarDefinition extends Definition {
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
} }
public abstract isMatch(literal: LiteralToken): boolean public abstract isMatch(literal: LiteralToken): boolean;
} }
@@ -1,63 +1,53 @@
import { DEFINITION, SEQUENCE, ITEM_TYPE } from "../template-constants" import {DEFINITION, SEQUENCE, ITEM_TYPE} from "../template-constants";
import { MappingToken } from "../tokens" import {MappingToken} from "../tokens";
import { Definition } from "./definition" import {Definition} from "./definition";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { TemplateSchema } from "./template-schema" import {TemplateSchema} from "./template-schema";
export class SequenceDefinition extends Definition { export class SequenceDefinition extends Definition {
public itemType = "" public itemType = "";
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case SEQUENCE: { case SEQUENCE: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${SEQUENCE}`);
`${DEFINITION} ${SEQUENCE}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${SEQUENCE} key`);
`${DEFINITION} ${SEQUENCE} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
case ITEM_TYPE: { case ITEM_TYPE: {
const itemType = mappingPair.value.assertString( const itemType = mappingPair.value.assertString(`${DEFINITION} ${SEQUENCE} ${ITEM_TYPE}`);
`${DEFINITION} ${SEQUENCE} ${ITEM_TYPE}` this.itemType = itemType.value;
) break;
this.itemType = itemType.value
break
} }
default: default:
// throws // throws
mappingKey.assertUnexpectedValue( mappingKey.assertUnexpectedValue(`${DEFINITION} ${SEQUENCE} key`);
`${DEFINITION} ${SEQUENCE} key` break;
)
break
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.Sequence return DefinitionType.Sequence;
} }
public override validate(schema: TemplateSchema, name: string): void { public override validate(schema: TemplateSchema, name: string): void {
if (!this.itemType) { if (!this.itemType) {
throw new Error(`'${name}' does not defined '${ITEM_TYPE}'`) throw new Error(`'${name}' does not defined '${ITEM_TYPE}'`);
} }
// Lookup item type // Lookup item type
schema.getDefinition(this.itemType) schema.getDefinition(this.itemType);
} }
} }
@@ -1,113 +1,88 @@
import { import {CONSTANT, DEFINITION, IGNORE_CASE, IS_EXPRESSION, REQUIRE_NON_EMPTY, STRING} from "../template-constants";
CONSTANT, import {LiteralToken, MappingToken, StringToken} from "../tokens";
DEFINITION, import {TokenType} from "../tokens/types";
IGNORE_CASE, import {DefinitionType} from "./definition-type";
IS_EXPRESSION, import {ScalarDefinition} from "./scalar-definition";
REQUIRE_NON_EMPTY, import {TemplateSchema} from "./template-schema";
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 { export class StringDefinition extends ScalarDefinition {
public constant = "" public constant = "";
public ignoreCase = false public ignoreCase = false;
public requireNonEmpty = false public requireNonEmpty = false;
public isExpression = false public isExpression = false;
public constructor(key: string, definition?: MappingToken) { public constructor(key: string, definition?: MappingToken) {
super(key, definition) super(key, definition);
if (definition) { if (definition) {
for (const definitionPair of definition) { for (const definitionPair of definition) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key`
)
switch (definitionKey.value) { switch (definitionKey.value) {
case STRING: { case STRING: {
const mapping = definitionPair.value.assertMapping( const mapping = definitionPair.value.assertMapping(`${DEFINITION} ${STRING}`);
`${DEFINITION} ${STRING}`
)
for (const mappingPair of mapping) { for (const mappingPair of mapping) {
const mappingKey = mappingPair.key.assertString( const mappingKey = mappingPair.key.assertString(`${DEFINITION} ${STRING} key`);
`${DEFINITION} ${STRING} key`
)
switch (mappingKey.value) { switch (mappingKey.value) {
case CONSTANT: { case CONSTANT: {
const constantStringToken = mappingPair.value.assertString( const constantStringToken = mappingPair.value.assertString(`${DEFINITION} ${STRING} ${CONSTANT}`);
`${DEFINITION} ${STRING} ${CONSTANT}` this.constant = constantStringToken.value;
) break;
this.constant = constantStringToken.value
break
} }
case IGNORE_CASE: { case IGNORE_CASE: {
const ignoreCaseBooleanToken = const ignoreCaseBooleanToken = mappingPair.value.assertBoolean(
mappingPair.value.assertBoolean( `${DEFINITION} ${STRING} ${IGNORE_CASE}`
`${DEFINITION} ${STRING} ${IGNORE_CASE}` );
) this.ignoreCase = ignoreCaseBooleanToken.value;
this.ignoreCase = ignoreCaseBooleanToken.value break;
break
} }
case REQUIRE_NON_EMPTY: { case REQUIRE_NON_EMPTY: {
const requireNonEmptyBooleanToken = const requireNonEmptyBooleanToken = mappingPair.value.assertBoolean(
mappingPair.value.assertBoolean( `${DEFINITION} ${STRING} ${REQUIRE_NON_EMPTY}`
`${DEFINITION} ${STRING} ${REQUIRE_NON_EMPTY}` );
) this.requireNonEmpty = requireNonEmptyBooleanToken.value;
this.requireNonEmpty = requireNonEmptyBooleanToken.value break;
break
} }
case IS_EXPRESSION: { case IS_EXPRESSION: {
const isExpressionToken = mappingPair.value.assertBoolean( const isExpressionToken = mappingPair.value.assertBoolean(`${DEFINITION} ${STRING} ${IS_EXPRESSION}`);
`${DEFINITION} ${STRING} ${IS_EXPRESSION}` this.isExpression = isExpressionToken.value;
) break;
this.isExpression = isExpressionToken.value
break
} }
default: default:
// throws // throws
mappingKey.assertUnexpectedValue( mappingKey.assertUnexpectedValue(`${DEFINITION} ${STRING} key`);
`${DEFINITION} ${STRING} key` break;
)
break
} }
} }
break break;
} }
default: default:
definitionKey.assertUnexpectedValue(`${DEFINITION} key`) // throws definitionKey.assertUnexpectedValue(`${DEFINITION} key`); // throws
} }
} }
} }
} }
public override get definitionType(): DefinitionType { public override get definitionType(): DefinitionType {
return DefinitionType.String return DefinitionType.String;
} }
public override isMatch(literal: LiteralToken): boolean { public override isMatch(literal: LiteralToken): boolean {
if (literal.templateTokenType === TokenType.String) { if (literal.templateTokenType === TokenType.String) {
const value = (literal as StringToken).value const value = (literal as StringToken).value;
if (this.constant) { if (this.constant) {
return this.ignoreCase return this.ignoreCase ? this.constant.toUpperCase() === value.toUpperCase() : this.constant === value;
? this.constant.toUpperCase() === value.toUpperCase()
: this.constant === value
} else if (this.requireNonEmpty) { } else if (this.requireNonEmpty) {
return !!value return !!value;
} else { } else {
return true return true;
} }
} }
return false return false;
} }
public override validate(schema: TemplateSchema, name: string): void { public override validate(schema: TemplateSchema, name: string): void {
if (this.constant && this.requireNonEmpty) { if (this.constant && this.requireNonEmpty) {
throw new Error( throw new Error(`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`);
`Properties '${CONSTANT}' and '${REQUIRE_NON_EMPTY}' cannot both be set`
)
} }
} }
} }
@@ -1,5 +1,5 @@
import { TokenType } from "../../templates/tokens/types" import {TokenType} from "../../templates/tokens/types";
import { ObjectReader } from "../object-reader" import {ObjectReader} from "../object-reader";
import { import {
ALLOWED_VALUES, ALLOWED_VALUES,
ANY, ANY,
@@ -42,192 +42,149 @@ import {
STRING_DEFINITION_PROPERTIES, STRING_DEFINITION_PROPERTIES,
TEMPLATE_SCHEMA, TEMPLATE_SCHEMA,
TYPE, TYPE,
VERSION, VERSION
} from "../template-constants" } from "../template-constants";
import { TemplateContext, TemplateValidationErrors } from "../template-context" import {TemplateContext, TemplateValidationErrors} from "../template-context";
import { readTemplate } from "../template-reader" import {readTemplate} from "../template-reader";
import { MappingToken, SequenceToken, StringToken } from "../tokens" import {MappingToken, SequenceToken, StringToken} from "../tokens";
import { NoOperationTraceWriter } from "../trace-writer" import {NoOperationTraceWriter} from "../trace-writer";
import { BooleanDefinition } from "./boolean-definition" import {BooleanDefinition} from "./boolean-definition";
import { Definition } from "./definition" import {Definition} from "./definition";
import { DefinitionType } from "./definition-type" import {DefinitionType} from "./definition-type";
import { MappingDefinition } from "./mapping-definition" import {MappingDefinition} from "./mapping-definition";
import { NullDefinition } from "./null-definition" import {NullDefinition} from "./null-definition";
import { NumberDefinition } from "./number-definition" import {NumberDefinition} from "./number-definition";
import { OneOfDefinition } from "./one-of-definition" import {OneOfDefinition} from "./one-of-definition";
import { PropertyDefinition } from "./property-definition" import {PropertyDefinition} from "./property-definition";
import { ScalarDefinition } from "./scalar-definition" import {ScalarDefinition} from "./scalar-definition";
import { SequenceDefinition } from "./sequence-definition" import {SequenceDefinition} from "./sequence-definition";
import { StringDefinition } from "./string-definition" import {StringDefinition} from "./string-definition";
/** /**
* This models the root schema object and contains definitions * This models the root schema object and contains definitions
*/ */
export class TemplateSchema { export class TemplateSchema {
private static readonly _definitionNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/ private static readonly _definitionNamePattern = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
private static _internalSchema: TemplateSchema | undefined private static _internalSchema: TemplateSchema | undefined;
public readonly definitions: { [key: string]: Definition } = {} public readonly definitions: {[key: string]: Definition} = {};
public readonly version: string = "" public readonly version: string = "";
public constructor(mapping?: MappingToken) { public constructor(mapping?: MappingToken) {
// Add built-in type: null // Add built-in type: null
this.definitions[NULL] = new NullDefinition(NULL) this.definitions[NULL] = new NullDefinition(NULL);
// Add built-in type: boolean // Add built-in type: boolean
this.definitions[BOOLEAN] = new BooleanDefinition(BOOLEAN) this.definitions[BOOLEAN] = new BooleanDefinition(BOOLEAN);
// Add built-in type: number // Add built-in type: number
this.definitions[NUMBER] = new NumberDefinition(NUMBER) this.definitions[NUMBER] = new NumberDefinition(NUMBER);
// Add built-in type: string // Add built-in type: string
this.definitions[STRING] = new StringDefinition(STRING) this.definitions[STRING] = new StringDefinition(STRING);
// Add built-in type: sequence // Add built-in type: sequence
const sequenceDefinition = new SequenceDefinition(SEQUENCE) const sequenceDefinition = new SequenceDefinition(SEQUENCE);
sequenceDefinition.itemType = ANY sequenceDefinition.itemType = ANY;
this.definitions[sequenceDefinition.key] = sequenceDefinition this.definitions[sequenceDefinition.key] = sequenceDefinition;
// Add built-in type: mapping // Add built-in type: mapping
const mappingDefinition = new MappingDefinition(MAPPING) const mappingDefinition = new MappingDefinition(MAPPING);
mappingDefinition.looseKeyType = STRING mappingDefinition.looseKeyType = STRING;
mappingDefinition.looseValueType = ANY mappingDefinition.looseValueType = ANY;
this.definitions[mappingDefinition.key] = mappingDefinition this.definitions[mappingDefinition.key] = mappingDefinition;
// Add built-in type: any // Add built-in type: any
const anyDefinition = new OneOfDefinition(ANY) const anyDefinition = new OneOfDefinition(ANY);
anyDefinition.oneOf.push(NULL) anyDefinition.oneOf.push(NULL);
anyDefinition.oneOf.push(BOOLEAN) anyDefinition.oneOf.push(BOOLEAN);
anyDefinition.oneOf.push(NUMBER) anyDefinition.oneOf.push(NUMBER);
anyDefinition.oneOf.push(STRING) anyDefinition.oneOf.push(STRING);
anyDefinition.oneOf.push(SEQUENCE) anyDefinition.oneOf.push(SEQUENCE);
anyDefinition.oneOf.push(MAPPING) anyDefinition.oneOf.push(MAPPING);
this.definitions[anyDefinition.key] = anyDefinition this.definitions[anyDefinition.key] = anyDefinition;
if (mapping) { if (mapping) {
for (const pair of mapping) { for (const pair of mapping) {
const key = pair.key.assertString(`${TEMPLATE_SCHEMA} key`) const key = pair.key.assertString(`${TEMPLATE_SCHEMA} key`);
switch (key.value) { switch (key.value) {
case VERSION: { case VERSION: {
this.version = pair.value.assertString( this.version = pair.value.assertString(`${TEMPLATE_SCHEMA} ${VERSION}`).value;
`${TEMPLATE_SCHEMA} ${VERSION}` break;
).value
break
} }
case DEFINITIONS: { case DEFINITIONS: {
const definitions = pair.value.assertMapping( const definitions = pair.value.assertMapping(`${TEMPLATE_SCHEMA} ${DEFINITIONS}`);
`${TEMPLATE_SCHEMA} ${DEFINITIONS}`
)
for (const definitionsPair of definitions) { for (const definitionsPair of definitions) {
const definitionsKey = definitionsPair.key.assertString( const definitionsKey = definitionsPair.key.assertString(`${TEMPLATE_SCHEMA} ${DEFINITIONS} key`);
`${TEMPLATE_SCHEMA} ${DEFINITIONS} key` const definitionsValue = definitionsPair.value.assertMapping(`${TEMPLATE_SCHEMA} ${DEFINITIONS} value`);
) let definition: Definition | undefined;
const definitionsValue = definitionsPair.value.assertMapping(
`${TEMPLATE_SCHEMA} ${DEFINITIONS} value`
)
let definition: Definition | undefined
for (const definitionPair of definitionsValue) { for (const definitionPair of definitionsValue) {
const definitionKey = definitionPair.key.assertString( const definitionKey = definitionPair.key.assertString(`${DEFINITION} key`);
`${DEFINITION} key` const mappingToken = definitionsPair.value as MappingToken;
)
const mappingToken = definitionsPair.value as MappingToken
switch (definitionKey.value) { switch (definitionKey.value) {
case NULL: case NULL:
definition = new NullDefinition( definition = new NullDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case BOOLEAN: case BOOLEAN:
definition = new BooleanDefinition( definition = new BooleanDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case NUMBER: case NUMBER:
definition = new NumberDefinition( definition = new NumberDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case STRING: case STRING:
definition = new StringDefinition( definition = new StringDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case SEQUENCE: case SEQUENCE:
definition = new SequenceDefinition( definition = new SequenceDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case MAPPING: case MAPPING:
definition = new MappingDefinition( definition = new MappingDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case ONE_OF: case ONE_OF:
definition = new OneOfDefinition( definition = new OneOfDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case ALLOWED_VALUES: case ALLOWED_VALUES:
// Change the allowed-values definition into a one-of definition and its corresponding string definitions // Change the allowed-values definition into a one-of definition and its corresponding string definitions
for (const item of mappingToken) { for (const item of mappingToken) {
if (item.value.templateTokenType === TokenType.Sequence) { if (item.value.templateTokenType === TokenType.Sequence) {
// Create a new string definition for each StringToken in the sequence // Create a new string definition for each StringToken in the sequence
const sequenceToken = item.value as SequenceToken const sequenceToken = item.value as SequenceToken;
for (const activity of sequenceToken) { for (const activity of sequenceToken) {
if (activity.templateTokenType === TokenType.String) { if (activity.templateTokenType === TokenType.String) {
const stringToken = activity as StringToken const stringToken = activity as StringToken;
const allowedValuesKey = const allowedValuesKey = definitionsKey.value + "-" + stringToken.value;
definitionsKey.value + "-" + stringToken.value const allowedValuesDef = new StringDefinition(allowedValuesKey);
const allowedValuesDef = new StringDefinition( allowedValuesDef.constant = stringToken.toDisplayString();
allowedValuesKey this.definitions[allowedValuesKey] = allowedValuesDef;
)
allowedValuesDef.constant =
stringToken.toDisplayString()
this.definitions[allowedValuesKey] =
allowedValuesDef
} }
} }
} }
} }
definition = new OneOfDefinition( definition = new OneOfDefinition(definitionsKey.value, definitionsValue);
definitionsKey.value, break;
definitionsValue
)
break
case CONTEXT: case CONTEXT:
case DESCRIPTION: case DESCRIPTION:
continue continue;
default: default:
// throws // throws
definitionKey.assertUnexpectedValue( definitionKey.assertUnexpectedValue(`${DEFINITION} mapping key`);
`${DEFINITION} mapping key` break;
)
break
} }
break break;
} }
if (!definition) { if (!definition) {
throw new Error( throw new Error(`Not enough information to construct definition '${definitionsKey.value}'`);
`Not enough information to construct definition '${definitionsKey.value}'`
)
} }
this.definitions[definitionsKey.value] = definition this.definitions[definitionsKey.value] = definition;
} }
break break;
} }
default: default:
// throws // throws
key.assertUnexpectedValue(`${TEMPLATE_SCHEMA} key`) key.assertUnexpectedValue(`${TEMPLATE_SCHEMA} key`);
break break;
} }
} }
} }
@@ -237,62 +194,59 @@ export class TemplateSchema {
* Looks up a definition by name * Looks up a definition by name
*/ */
public getDefinition(name: string): Definition { public getDefinition(name: string): Definition {
const result = this.definitions[name] const result = this.definitions[name];
if (result) { if (result) {
return result return result;
} }
throw new Error(`Schema definition '${name}' not found`) throw new Error(`Schema definition '${name}' not found`);
} }
/** /**
* Expands one-of definitions and returns all scalar definitions * Expands one-of definitions and returns all scalar definitions
*/ */
public getScalarDefinitions(definition: Definition): ScalarDefinition[] { public getScalarDefinitions(definition: Definition): ScalarDefinition[] {
const result: ScalarDefinition[] = [] const result: ScalarDefinition[] = [];
switch (definition.definitionType) { switch (definition.definitionType) {
case DefinitionType.Null: case DefinitionType.Null:
case DefinitionType.Boolean: case DefinitionType.Boolean:
case DefinitionType.Number: case DefinitionType.Number:
case DefinitionType.String: case DefinitionType.String:
result.push(definition as ScalarDefinition) result.push(definition as ScalarDefinition);
break break;
case DefinitionType.OneOf: { case DefinitionType.OneOf: {
const oneOf = definition as OneOfDefinition const oneOf = definition as OneOfDefinition;
// Expand nested one-of definitions // Expand nested one-of definitions
for (const nestedName of oneOf.oneOf) { for (const nestedName of oneOf.oneOf) {
const nestedDefinition = this.getDefinition(nestedName) const nestedDefinition = this.getDefinition(nestedName);
result.push(...this.getScalarDefinitions(nestedDefinition)) result.push(...this.getScalarDefinitions(nestedDefinition));
} }
break break;
} }
} }
return result return result;
} }
/** /**
* Expands one-of definitions and returns all matching definitions by type * Expands one-of definitions and returns all matching definitions by type
*/ */
public getDefinitionsOfType( public getDefinitionsOfType(definition: Definition, type: DefinitionType): Definition[] {
definition: Definition, const result: Definition[] = [];
type: DefinitionType
): Definition[] {
const result: Definition[] = []
if (definition.definitionType === type) { if (definition.definitionType === type) {
result.push(definition) result.push(definition);
} else if (definition.definitionType === DefinitionType.OneOf) { } else if (definition.definitionType === DefinitionType.OneOf) {
const oneOf = definition as OneOfDefinition const oneOf = definition as OneOfDefinition;
for (const nestedName of oneOf.oneOf) { for (const nestedName of oneOf.oneOf) {
const nestedDefinition = this.getDefinition(nestedName) const nestedDefinition = this.getDefinition(nestedName);
if (nestedDefinition.definitionType === type) { if (nestedDefinition.definitionType === type) {
result.push(nestedDefinition) result.push(nestedDefinition);
} }
} }
} }
return result return result;
} }
/** /**
@@ -304,16 +258,16 @@ export class TemplateSchema {
definitions: MappingDefinition[], definitions: MappingDefinition[],
propertyName: string propertyName: string
): PropertyDefinition | undefined { ): PropertyDefinition | undefined {
let result: PropertyDefinition | undefined let result: PropertyDefinition | undefined;
// Check for a matching well-known property // Check for a matching well-known property
let notFoundInSome = false let notFoundInSome = false;
for (const definition of definitions) { for (const definition of definitions) {
const propertyDef = definition.properties[propertyName] const propertyDef = definition.properties[propertyName];
if (propertyDef) { if (propertyDef) {
result = propertyDef result = propertyDef;
} else { } else {
notFoundInSome = true notFoundInSome = true;
} }
} }
@@ -321,40 +275,40 @@ export class TemplateSchema {
if (result && notFoundInSome) { if (result && notFoundInSome) {
for (let i = 0; i < definitions.length; ) { for (let i = 0; i < definitions.length; ) {
if (definitions[i].properties[propertyName]) { if (definitions[i].properties[propertyName]) {
i++ i++;
} else { } else {
definitions.splice(i, 1) definitions.splice(i, 1);
} }
} }
} }
return result return result;
} }
private validate(): void { private validate(): void {
const oneOfDefinitions: { [key: string]: OneOfDefinition } = {} const oneOfDefinitions: {[key: string]: OneOfDefinition} = {};
for (const name of Object.keys(this.definitions)) { for (const name of Object.keys(this.definitions)) {
if (!name.match(TemplateSchema._definitionNamePattern)) { if (!name.match(TemplateSchema._definitionNamePattern)) {
throw new Error(`Invalid definition name '${name}'`) throw new Error(`Invalid definition name '${name}'`);
} }
const definition = this.definitions[name] const definition = this.definitions[name];
// Delay validation for 'one-of' definitions // Delay validation for 'one-of' definitions
if (definition.definitionType === DefinitionType.OneOf) { if (definition.definitionType === DefinitionType.OneOf) {
oneOfDefinitions[name] = definition as OneOfDefinition oneOfDefinitions[name] = definition as OneOfDefinition;
} }
// Otherwise validate now // Otherwise validate now
else { else {
definition.validate(this, name) definition.validate(this, name);
} }
} }
// Validate 'one-of' definitions // Validate 'one-of' definitions
for (const name of Object.keys(oneOfDefinitions)) { for (const name of Object.keys(oneOfDefinitions)) {
const oneOf = oneOfDefinitions[name] const oneOf = oneOfDefinitions[name];
oneOf.validate(this, name) oneOf.validate(this, name);
} }
} }
@@ -366,19 +320,14 @@ export class TemplateSchema {
new TemplateValidationErrors(10, 500), new TemplateValidationErrors(10, 500),
TemplateSchema.getInternalSchema(), TemplateSchema.getInternalSchema(),
new NoOperationTraceWriter() new NoOperationTraceWriter()
) );
const template = readTemplate( const template = readTemplate(context, TEMPLATE_SCHEMA, objectReader, undefined);
context, context.errors.check();
TEMPLATE_SCHEMA,
objectReader,
undefined
)
context.errors.check()
const mapping = template!.assertMapping(TEMPLATE_SCHEMA) const mapping = template!.assertMapping(TEMPLATE_SCHEMA);
const schema = new TemplateSchema(mapping) const schema = new TemplateSchema(mapping);
schema.validate() schema.validate();
return schema return schema;
} }
/** /**
@@ -386,294 +335,217 @@ export class TemplateSchema {
*/ */
private static getInternalSchema(): TemplateSchema { private static getInternalSchema(): TemplateSchema {
if (TemplateSchema._internalSchema === undefined) { if (TemplateSchema._internalSchema === undefined) {
const schema = new TemplateSchema() const schema = new TemplateSchema();
// template-schema // template-schema
let mappingDefinition = new MappingDefinition(TEMPLATE_SCHEMA) let mappingDefinition = new MappingDefinition(TEMPLATE_SCHEMA);
mappingDefinition.properties[VERSION] = new PropertyDefinition( mappingDefinition.properties[VERSION] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
mappingDefinition.properties[DEFINITIONS] = new PropertyDefinition( mappingDefinition.properties[DEFINITIONS] = new PropertyDefinition(
new StringToken(undefined, undefined, DEFINITIONS, undefined) new StringToken(undefined, undefined, DEFINITIONS, undefined)
) );
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// definitions // definitions
mappingDefinition = new MappingDefinition(DEFINITIONS) mappingDefinition = new MappingDefinition(DEFINITIONS);
mappingDefinition.looseKeyType = NON_EMPTY_STRING mappingDefinition.looseKeyType = NON_EMPTY_STRING;
mappingDefinition.looseValueType = DEFINITION mappingDefinition.looseValueType = DEFINITION;
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// definition // definition
let oneOfDefinition = new OneOfDefinition(DEFINITION) let oneOfDefinition = new OneOfDefinition(DEFINITION);
oneOfDefinition.oneOf.push(NULL_DEFINITION) oneOfDefinition.oneOf.push(NULL_DEFINITION);
oneOfDefinition.oneOf.push(BOOLEAN_DEFINITION) oneOfDefinition.oneOf.push(BOOLEAN_DEFINITION);
oneOfDefinition.oneOf.push(NUMBER_DEFINITION) oneOfDefinition.oneOf.push(NUMBER_DEFINITION);
oneOfDefinition.oneOf.push(STRING_DEFINITION) oneOfDefinition.oneOf.push(STRING_DEFINITION);
oneOfDefinition.oneOf.push(SEQUENCE_DEFINITION) oneOfDefinition.oneOf.push(SEQUENCE_DEFINITION);
oneOfDefinition.oneOf.push(MAPPING_DEFINITION) oneOfDefinition.oneOf.push(MAPPING_DEFINITION);
oneOfDefinition.oneOf.push(ONE_OF_DEFINITION) oneOfDefinition.oneOf.push(ONE_OF_DEFINITION);
schema.definitions[oneOfDefinition.key] = oneOfDefinition schema.definitions[oneOfDefinition.key] = oneOfDefinition;
// null-definition // null-definition
mappingDefinition = new MappingDefinition(NULL_DEFINITION) mappingDefinition = new MappingDefinition(NULL_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[NULL] = new PropertyDefinition( mappingDefinition.properties[NULL] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, NULL_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
NULL_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// null-definition-properties // null-definition-properties
mappingDefinition = new MappingDefinition(NULL_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(NULL_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// boolean-definition // boolean-definition
mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION) mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[BOOLEAN] = new PropertyDefinition( mappingDefinition.properties[BOOLEAN] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, BOOLEAN_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
BOOLEAN_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// boolean-definition-properties // boolean-definition-properties
mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(BOOLEAN_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// number-definition // number-definition
mappingDefinition = new MappingDefinition(NUMBER_DEFINITION) mappingDefinition = new MappingDefinition(NUMBER_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[NUMBER] = new PropertyDefinition( mappingDefinition.properties[NUMBER] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, NUMBER_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
NUMBER_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// number-definition-properties // number-definition-properties
mappingDefinition = new MappingDefinition(NUMBER_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(NUMBER_DEFINITION_PROPERTIES);
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// string-definition // string-definition
mappingDefinition = new MappingDefinition(STRING_DEFINITION) mappingDefinition = new MappingDefinition(STRING_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[STRING] = new PropertyDefinition( mappingDefinition.properties[STRING] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, STRING_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
STRING_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// string-definition-properties // string-definition-properties
mappingDefinition = new MappingDefinition(STRING_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(STRING_DEFINITION_PROPERTIES);
mappingDefinition.properties[CONSTANT] = new PropertyDefinition( mappingDefinition.properties[CONSTANT] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
mappingDefinition.properties[IGNORE_CASE] = new PropertyDefinition( mappingDefinition.properties[IGNORE_CASE] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined) new StringToken(undefined, undefined, BOOLEAN, undefined)
) );
mappingDefinition.properties[REQUIRE_NON_EMPTY] = new PropertyDefinition( mappingDefinition.properties[REQUIRE_NON_EMPTY] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined) new StringToken(undefined, undefined, BOOLEAN, undefined)
) );
mappingDefinition.properties[IS_EXPRESSION] = new PropertyDefinition( mappingDefinition.properties[IS_EXPRESSION] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined) new StringToken(undefined, undefined, BOOLEAN, undefined)
) );
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// sequence-definition // sequence-definition
mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION) mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[SEQUENCE] = new PropertyDefinition( mappingDefinition.properties[SEQUENCE] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
SEQUENCE_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// sequence-definition-properties // sequence-definition-properties
mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(SEQUENCE_DEFINITION_PROPERTIES);
mappingDefinition.properties[ITEM_TYPE] = new PropertyDefinition( mappingDefinition.properties[ITEM_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// mapping-definition // mapping-definition
mappingDefinition = new MappingDefinition(MAPPING_DEFINITION) mappingDefinition = new MappingDefinition(MAPPING_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[MAPPING] = new PropertyDefinition( mappingDefinition.properties[MAPPING] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, MAPPING_DEFINITION_PROPERTIES, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
MAPPING_DEFINITION_PROPERTIES,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// mapping-definition-properties // mapping-definition-properties
mappingDefinition = new MappingDefinition(MAPPING_DEFINITION_PROPERTIES) mappingDefinition = new MappingDefinition(MAPPING_DEFINITION_PROPERTIES);
mappingDefinition.properties[PROPERTIES] = new PropertyDefinition( mappingDefinition.properties[PROPERTIES] = new PropertyDefinition(
new StringToken(undefined, undefined, PROPERTIES, undefined) new StringToken(undefined, undefined, PROPERTIES, undefined)
) );
mappingDefinition.properties[LOOSE_KEY_TYPE] = new PropertyDefinition( mappingDefinition.properties[LOOSE_KEY_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
mappingDefinition.properties[LOOSE_VALUE_TYPE] = new PropertyDefinition( mappingDefinition.properties[LOOSE_VALUE_TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// properties // properties
mappingDefinition = new MappingDefinition(PROPERTIES) mappingDefinition = new MappingDefinition(PROPERTIES);
mappingDefinition.looseKeyType = NON_EMPTY_STRING mappingDefinition.looseKeyType = NON_EMPTY_STRING;
mappingDefinition.looseValueType = PROPERTY_VALUE mappingDefinition.looseValueType = PROPERTY_VALUE;
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// property-value // property-value
oneOfDefinition = new OneOfDefinition(PROPERTY_VALUE) oneOfDefinition = new OneOfDefinition(PROPERTY_VALUE);
oneOfDefinition.oneOf.push(NON_EMPTY_STRING) oneOfDefinition.oneOf.push(NON_EMPTY_STRING);
oneOfDefinition.oneOf.push(MAPPING_PROPERTY_VALUE) oneOfDefinition.oneOf.push(MAPPING_PROPERTY_VALUE);
schema.definitions[oneOfDefinition.key] = oneOfDefinition schema.definitions[oneOfDefinition.key] = oneOfDefinition;
// mapping-property-value // mapping-property-value
mappingDefinition = new MappingDefinition(MAPPING_PROPERTY_VALUE) mappingDefinition = new MappingDefinition(MAPPING_PROPERTY_VALUE);
mappingDefinition.properties[TYPE] = new PropertyDefinition( mappingDefinition.properties[TYPE] = new PropertyDefinition(
new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined) new StringToken(undefined, undefined, NON_EMPTY_STRING, undefined)
) );
mappingDefinition.properties[REQUIRED] = new PropertyDefinition( mappingDefinition.properties[REQUIRED] = new PropertyDefinition(
new StringToken(undefined, undefined, BOOLEAN, undefined) new StringToken(undefined, undefined, BOOLEAN, undefined)
) );
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
schema.definitions[mappingDefinition.key] = mappingDefinition schema.definitions[mappingDefinition.key] = mappingDefinition;
// one-of-definition // one-of-definition
mappingDefinition = new MappingDefinition(ONE_OF_DEFINITION) mappingDefinition = new MappingDefinition(ONE_OF_DEFINITION);
mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition( mappingDefinition.properties[DESCRIPTION] = new PropertyDefinition(
new StringToken(undefined, undefined, STRING, undefined) new StringToken(undefined, undefined, STRING, undefined)
) );
mappingDefinition.properties[CONTEXT] = new PropertyDefinition( mappingDefinition.properties[CONTEXT] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[ONE_OF] = new PropertyDefinition( mappingDefinition.properties[ONE_OF] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined,
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
mappingDefinition.properties[ALLOWED_VALUES] = new PropertyDefinition( mappingDefinition.properties[ALLOWED_VALUES] = new PropertyDefinition(
new StringToken( new StringToken(undefined, undefined, SEQUENCE_OF_NON_EMPTY_STRING, undefined)
undefined, );
undefined, schema.definitions[mappingDefinition.key] = mappingDefinition;
SEQUENCE_OF_NON_EMPTY_STRING,
undefined
)
)
schema.definitions[mappingDefinition.key] = mappingDefinition
// non-empty-string // non-empty-string
const stringDefinition = new StringDefinition(NON_EMPTY_STRING) const stringDefinition = new StringDefinition(NON_EMPTY_STRING);
stringDefinition.requireNonEmpty = true stringDefinition.requireNonEmpty = true;
schema.definitions[stringDefinition.key] = stringDefinition schema.definitions[stringDefinition.key] = stringDefinition;
// sequence-of-non-empty-string // sequence-of-non-empty-string
const sequenceDefinition = new SequenceDefinition( const sequenceDefinition = new SequenceDefinition(SEQUENCE_OF_NON_EMPTY_STRING);
SEQUENCE_OF_NON_EMPTY_STRING sequenceDefinition.itemType = NON_EMPTY_STRING;
) schema.definitions[sequenceDefinition.key] = sequenceDefinition;
sequenceDefinition.itemType = NON_EMPTY_STRING
schema.definitions[sequenceDefinition.key] = sequenceDefinition
schema.validate() schema.validate();
TemplateSchema._internalSchema = schema TemplateSchema._internalSchema = schema;
} }
return TemplateSchema._internalSchema return TemplateSchema._internalSchema;
} }
} }
@@ -1,48 +1,48 @@
export const ALLOWED_VALUES = "allowed-values" export const ALLOWED_VALUES = "allowed-values";
export const ANY = "any" export const ANY = "any";
export const BOOLEAN = "boolean" export const BOOLEAN = "boolean";
export const BOOLEAN_DEFINITION = "boolean-definition" export const BOOLEAN_DEFINITION = "boolean-definition";
export const BOOLEAN_DEFINITION_PROPERTIES = "boolean-definition-properties" export const BOOLEAN_DEFINITION_PROPERTIES = "boolean-definition-properties";
export const CLOSE_EXPRESSION = "}}" export const CLOSE_EXPRESSION = "}}";
export const CONSTANT = "constant" export const CONSTANT = "constant";
export const CONTEXT = "context" export const CONTEXT = "context";
export const DEFINITION = "definition" export const DEFINITION = "definition";
export const DEFINITIONS = "definitions" export const DEFINITIONS = "definitions";
export const DESCRIPTION = "description" export const DESCRIPTION = "description";
export const IGNORE_CASE = "ignore-case" export const IGNORE_CASE = "ignore-case";
export const INSERT_DIRECTIVE = "insert" export const INSERT_DIRECTIVE = "insert";
export const IS_EXPRESSION = "is-expression" export const IS_EXPRESSION = "is-expression";
export const ITEM_TYPE = "item-type" export const ITEM_TYPE = "item-type";
export const LOOSE_KEY_TYPE = "loose-key-type" export const LOOSE_KEY_TYPE = "loose-key-type";
export const LOOSE_VALUE_TYPE = "loose-value-type" export const LOOSE_VALUE_TYPE = "loose-value-type";
export const MAX_CONSTANT = "MAX" export const MAX_CONSTANT = "MAX";
export const MAPPING = "mapping" export const MAPPING = "mapping";
export const MAPPING_DEFINITION = "mapping-definition" export const MAPPING_DEFINITION = "mapping-definition";
export const MAPPING_DEFINITION_PROPERTIES = "mapping-definition-properties" export const MAPPING_DEFINITION_PROPERTIES = "mapping-definition-properties";
export const MAPPING_PROPERTY_VALUE = "mapping-property-value" export const MAPPING_PROPERTY_VALUE = "mapping-property-value";
export const NON_EMPTY_STRING = "non-empty-string" export const NON_EMPTY_STRING = "non-empty-string";
export const NULL = "null" export const NULL = "null";
export const NULL_DEFINITION = "null-definition" export const NULL_DEFINITION = "null-definition";
export const NULL_DEFINITION_PROPERTIES = "null-definition-properties" export const NULL_DEFINITION_PROPERTIES = "null-definition-properties";
export const NUMBER = "number" export const NUMBER = "number";
export const NUMBER_DEFINITION = "number-definition" export const NUMBER_DEFINITION = "number-definition";
export const NUMBER_DEFINITION_PROPERTIES = "number-definition-properties" export const NUMBER_DEFINITION_PROPERTIES = "number-definition-properties";
export const ONE_OF = "one-of" export const ONE_OF = "one-of";
export const ONE_OF_DEFINITION = "one-of-definition" export const ONE_OF_DEFINITION = "one-of-definition";
export const OPEN_EXPRESSION = "${{" export const OPEN_EXPRESSION = "${{";
export const PROPERTY_VALUE = "property-value" export const PROPERTY_VALUE = "property-value";
export const PROPERTIES = "properties" export const PROPERTIES = "properties";
export const REQUIRED = "required" export const REQUIRED = "required";
export const REQUIRE_NON_EMPTY = "require-non-empty" export const REQUIRE_NON_EMPTY = "require-non-empty";
export const SCALAR = "scalar" export const SCALAR = "scalar";
export const SEQUENCE = "sequence" export const SEQUENCE = "sequence";
export const SEQUENCE_DEFINITION = "sequence-definition" export const SEQUENCE_DEFINITION = "sequence-definition";
export const SEQUENCE_DEFINITION_PROPERTIES = "sequence-definition-properties" export const SEQUENCE_DEFINITION_PROPERTIES = "sequence-definition-properties";
export const TYPE = "type" export const TYPE = "type";
export const SEQUENCE_OF_NON_EMPTY_STRING = "sequence-of-non-empty-string" export const SEQUENCE_OF_NON_EMPTY_STRING = "sequence-of-non-empty-string";
export const STRING = "string" export const STRING = "string";
export const STRING_DEFINITION = "string-definition" export const STRING_DEFINITION = "string-definition";
export const STRING_DEFINITION_PROPERTIES = "string-definition-properties" export const STRING_DEFINITION_PROPERTIES = "string-definition-properties";
export const STRUCTURE = "structure" export const STRUCTURE = "structure";
export const TEMPLATE_SCHEMA = "template-schema" export const TEMPLATE_SCHEMA = "template-schema";
export const VERSION = "version" export const VERSION = "version";
@@ -1,135 +1,100 @@
import { FunctionInfo } from "@github/actions-expressions/funcs/info" import {FunctionInfo} from "@github/actions-expressions/funcs/info";
import { TemplateSchema } from "./schema/template-schema" import {TemplateSchema} from "./schema/template-schema";
import { TemplateValidationError } from "./template-validation-error" import {TemplateValidationError} from "./template-validation-error";
import { TemplateToken } from "./tokens" import {TemplateToken} from "./tokens";
import { TokenRange } from "./tokens/token-range" import {TokenRange} from "./tokens/token-range";
import { TraceWriter } from "./trace-writer" import {TraceWriter} from "./trace-writer";
/** /**
* Context object that is flowed through while loading and evaluating object templates * Context object that is flowed through while loading and evaluating object templates
*/ */
export class TemplateContext { export class TemplateContext {
private readonly _fileIds: { [name: string]: number } = {} private readonly _fileIds: {[name: string]: number} = {};
private readonly _fileNames: string[] = [] private readonly _fileNames: string[] = [];
/** /**
* Available functions within expression contexts * Available functions within expression contexts
*/ */
public readonly expressionFunctions: FunctionInfo[] = [] public readonly expressionFunctions: FunctionInfo[] = [];
/** /**
* Available values within expression contexts * Available values within expression contexts
*/ */
public readonly expressionNamedContexts: string[] = [] public readonly expressionNamedContexts: string[] = [];
public readonly errors: TemplateValidationErrors public readonly errors: TemplateValidationErrors;
public readonly schema: TemplateSchema public readonly schema: TemplateSchema;
public readonly trace: TraceWriter public readonly trace: TraceWriter;
public readonly state: { [key: string]: any } = {} public readonly state: {[key: string]: any} = {};
public constructor( public constructor(errors: TemplateValidationErrors, schema: TemplateSchema, trace: TraceWriter) {
errors: TemplateValidationErrors, this.errors = errors;
schema: TemplateSchema, this.schema = schema;
trace: TraceWriter this.trace = trace;
) {
this.errors = errors
this.schema = schema
this.trace = trace
} }
public error( public error(token: TemplateToken | undefined, err: string, tokenRange?: TokenRange): void;
token: TemplateToken | undefined, public error(token: TemplateToken | undefined, err: Error, tokenRange?: TokenRange): void;
err: string, public error(token: TemplateToken | undefined, err: unknown): void;
tokenRange?: TokenRange public error(fileId: number | undefined, err: string, tokenRange?: TokenRange): void;
): void public error(fileId: number | undefined, err: Error, tokenRange?: TokenRange): void;
public error( public error(fileId: number | undefined, err: unknown, tokenRange?: TokenRange): void;
token: TemplateToken | undefined,
err: Error,
tokenRange?: TokenRange
): void
public error(token: TemplateToken | undefined, err: unknown): void
public error(
fileId: number | undefined,
err: string,
tokenRange?: TokenRange
): void
public error(
fileId: number | undefined,
err: Error,
tokenRange?: TokenRange
): void
public error(
fileId: number | undefined,
err: unknown,
tokenRange?: TokenRange
): void
public error( public error(
tokenOrFileId: TemplateToken | number | undefined, tokenOrFileId: TemplateToken | number | undefined,
err: string | Error | unknown, err: string | Error | unknown,
tokenRange?: TokenRange tokenRange?: TokenRange
): void { ): void {
const token = tokenOrFileId as TemplateToken | undefined const token = tokenOrFileId as TemplateToken | undefined;
const range = tokenRange || token?.range const range = tokenRange || token?.range;
const prefix = this.getErrorPrefix( const prefix = this.getErrorPrefix(token?.file ?? (tokenOrFileId as number | undefined), token?.line, token?.col);
token?.file ?? (tokenOrFileId as number | undefined), const message = (err as Error | undefined)?.message ?? `${err}`;
token?.line,
token?.col
)
const message = (err as Error | undefined)?.message ?? `${err}`
const e = new TemplateValidationError(message, prefix, undefined, range) const e = new TemplateValidationError(message, prefix, undefined, range);
this.errors.add(e) this.errors.add(e);
this.trace.error(e.message) this.trace.error(e.message);
} }
/** /**
* Gets or adds the file ID * Gets or adds the file ID
*/ */
public getFileId(file: string) { public getFileId(file: string) {
const key = file.toUpperCase() const key = file.toUpperCase();
let id: number | undefined = this._fileIds[key] let id: number | undefined = this._fileIds[key];
if (id === undefined) { if (id === undefined) {
id = this._fileNames.length + 1 id = this._fileNames.length + 1;
this._fileIds[key] = id this._fileIds[key] = id;
this._fileNames.push(file) this._fileNames.push(file);
} }
return id return id;
} }
/** /**
* Looks up a file name by ID. Returns undefined if not found. * Looks up a file name by ID. Returns undefined if not found.
*/ */
public getFileName(fileId: number): string | undefined { public getFileName(fileId: number): string | undefined {
return this._fileNames.length >= fileId return this._fileNames.length >= fileId ? this._fileNames[fileId - 1] : undefined;
? this._fileNames[fileId - 1]
: undefined
} }
/** /**
* Gets a copy of the file table * Gets a copy of the file table
*/ */
public getFileTable(): string[] { public getFileTable(): string[] {
return this._fileNames.slice() return this._fileNames.slice();
} }
private getErrorPrefix( private getErrorPrefix(fileId?: number, line?: number, column?: number): string {
fileId?: number, const fileName = fileId !== undefined ? this.getFileName(fileId as number) : undefined;
line?: number,
column?: number
): string {
const fileName =
fileId !== undefined ? this.getFileName(fileId as number) : undefined
if (fileName) { if (fileName) {
if (line !== undefined && column !== undefined) { if (line !== undefined && column !== undefined) {
return `${fileName} (Line: ${line}, Col: ${column})` return `${fileName} (Line: ${line}, Col: ${column})`;
} else { } else {
return fileName return fileName;
} }
} else if (line !== undefined && column !== undefined) { } else if (line !== undefined && column !== undefined) {
return `(Line: ${line}, Col: ${column})` return `(Line: ${line}, Col: ${column})`;
} else { } else {
return "" return "";
} }
} }
} }
@@ -138,17 +103,17 @@ export class TemplateContext {
* Provides information about errors which occurred during validation * Provides information about errors which occurred during validation
*/ */
export class TemplateValidationErrors { export class TemplateValidationErrors {
private readonly _maxErrors: number private readonly _maxErrors: number;
private readonly _maxMessageLength: number private readonly _maxMessageLength: number;
private _errors: TemplateValidationError[] = [] private _errors: TemplateValidationError[] = [];
public constructor(maxErrors?: number, maxMessageLength?: number) { public constructor(maxErrors?: number, maxMessageLength?: number) {
this._maxErrors = maxErrors ?? 0 this._maxErrors = maxErrors ?? 0;
this._maxMessageLength = maxMessageLength ?? 0 this._maxMessageLength = maxMessageLength ?? 0;
} }
public get count(): number { public get count(): number {
return this._errors.length return this._errors.length;
} }
public add(err: TemplateValidationError | TemplateValidationError[]): void { public add(err: TemplateValidationError | TemplateValidationError[]): void {
@@ -156,19 +121,16 @@ export class TemplateValidationErrors {
// Check max errors // Check max errors
if (this._maxErrors <= 0 || this._errors.length < this._maxErrors) { if (this._maxErrors <= 0 || this._errors.length < this._maxErrors) {
// Check max message length // Check max message length
if ( if (this._maxMessageLength > 0 && e.message.length > this._maxMessageLength) {
this._maxMessageLength > 0 &&
e.message.length > this._maxMessageLength
) {
e = new TemplateValidationError( e = new TemplateValidationError(
e.message.substring(0, this._maxMessageLength) + "[...]", e.message.substring(0, this._maxMessageLength) + "[...]",
e.prefix, e.prefix,
e.code, e.code,
e.range e.range
) );
} }
this._errors.push(e) this._errors.push(e);
} }
} }
} }
@@ -179,21 +141,21 @@ export class TemplateValidationErrors {
*/ */
public check(prefix?: string): void { public check(prefix?: string): void {
if (this._errors.length <= 0) { if (this._errors.length <= 0) {
return return;
} }
if (!prefix) { if (!prefix) {
prefix = "The template is not valid." prefix = "The template is not valid.";
} }
throw new Error(`${prefix} ${this._errors.map((x) => x.message).join(",")}`) throw new Error(`${prefix} ${this._errors.map(x => x.message).join(",")}`);
} }
public clear(): void { public clear(): void {
this._errors = [] this._errors = [];
} }
public getErrors(): TemplateValidationError[] { public getErrors(): TemplateValidationError[] {
return this._errors.slice() return this._errors.slice();
} }
} }
@@ -1,22 +1,16 @@
// called at runtime. expands expressions and re-validates the schema result after expansion // called at runtime. expands expressions and re-validates the schema result after expansion
import { TemplateSchema } from "./schema" import {TemplateSchema} from "./schema";
import { Definition } from "./schema/definition" import {Definition} from "./schema/definition";
import { DefinitionType } from "./schema/definition-type" import {DefinitionType} from "./schema/definition-type";
import { MappingDefinition } from "./schema/mapping-definition" import {MappingDefinition} from "./schema/mapping-definition";
import { ScalarDefinition } from "./schema/scalar-definition" import {ScalarDefinition} from "./schema/scalar-definition";
import { SequenceDefinition } from "./schema/sequence-definition" import {SequenceDefinition} from "./schema/sequence-definition";
import { ANY } from "./template-constants" import {ANY} from "./template-constants";
import { TemplateContext } from "./template-context" import {TemplateContext} from "./template-context";
import { TemplateUnraveler } from "./template-unraveler" import {TemplateUnraveler} from "./template-unraveler";
import { import {LiteralToken, MappingToken, ScalarToken, StringToken, TemplateToken} from "./tokens";
LiteralToken, import {TokenType} from "./tokens/types";
MappingToken,
ScalarToken,
StringToken,
TemplateToken,
} from "./tokens"
import { TokenType } from "./tokens/types"
export function evaluateTemplate( export function evaluateTemplate(
context: TemplateContext, context: TemplateContext,
@@ -25,90 +19,81 @@ export function evaluateTemplate(
removeBytes: number, removeBytes: number,
fileId: number | undefined fileId: number | undefined
): TemplateToken | undefined { ): TemplateToken | undefined {
let result: TemplateToken | undefined let result: TemplateToken | undefined;
const evaluator = new TemplateEvaluator(context, template, removeBytes) const evaluator = new TemplateEvaluator(context, template, removeBytes);
try { try {
const definitionInfo = new DefinitionInfo(context, type) const definitionInfo = new DefinitionInfo(context, type);
result = evaluator.evaluate(definitionInfo) result = evaluator.evaluate(definitionInfo);
if (result) { if (result) {
evaluator.unraveler.readEnd() evaluator.unraveler.readEnd();
} }
} catch (err) { } catch (err) {
context.error(fileId, err) context.error(fileId, err);
result = undefined result = undefined;
} }
return result return result;
} }
class TemplateEvaluator { class TemplateEvaluator {
public readonly context: TemplateContext public readonly context: TemplateContext;
public readonly schema: TemplateSchema public readonly schema: TemplateSchema;
public readonly unraveler: TemplateUnraveler public readonly unraveler: TemplateUnraveler;
public constructor( public constructor(context: TemplateContext, template: TemplateToken, removeBytes: number) {
context: TemplateContext, this.context = context;
template: TemplateToken, this.schema = context.schema;
removeBytes: number this.unraveler = new TemplateUnraveler(context, template, removeBytes);
) {
this.context = context
this.schema = context.schema
this.unraveler = new TemplateUnraveler(context, template, removeBytes)
} }
public evaluate(definition: DefinitionInfo): TemplateToken { public evaluate(definition: DefinitionInfo): TemplateToken {
// Scalar // Scalar
const scalar = this.unraveler.allowScalar(definition.expand) const scalar = this.unraveler.allowScalar(definition.expand);
if (scalar) { if (scalar) {
if (scalar.isLiteral) { if (scalar.isLiteral) {
return this.validate(scalar as LiteralToken, definition) return this.validate(scalar as LiteralToken, definition);
} else { } else {
return scalar return scalar;
} }
} }
// Sequence // Sequence
const sequence = this.unraveler.allowSequenceStart(definition.expand) const sequence = this.unraveler.allowSequenceStart(definition.expand);
if (sequence) { if (sequence) {
const sequenceDefinition = definition.getDefinitionsOfType( const sequenceDefinition = definition.getDefinitionsOfType(DefinitionType.Sequence)[0] as
DefinitionType.Sequence | SequenceDefinition
)[0] as SequenceDefinition | undefined | undefined;
// Legal // Legal
if (sequenceDefinition) { if (sequenceDefinition) {
const itemDefinition = new DefinitionInfo( const itemDefinition = new DefinitionInfo(definition, sequenceDefinition.itemType);
definition,
sequenceDefinition.itemType
)
// Add each item // Add each item
while (!this.unraveler.allowSequenceEnd(definition.expand)) { while (!this.unraveler.allowSequenceEnd(definition.expand)) {
const item = this.evaluate(itemDefinition) const item = this.evaluate(itemDefinition);
sequence.add(item) sequence.add(item);
} }
} }
// Illegal // Illegal
else { else {
// Error // Error
this.context.error(sequence, "A sequence was not expected") this.context.error(sequence, "A sequence was not expected");
// Skip each item // Skip each item
while (!this.unraveler.allowSequenceEnd(false)) { while (!this.unraveler.allowSequenceEnd(false)) {
this.unraveler.skipSequenceItem() this.unraveler.skipSequenceItem();
} }
} }
return sequence return sequence;
} }
// Mapping // Mapping
const mapping = this.unraveler.allowMappingStart(definition.expand) const mapping = this.unraveler.allowMappingStart(definition.expand);
if (mapping) { if (mapping) {
const mappingDefinitions = definition.getDefinitionsOfType( const mappingDefinitions = definition.getDefinitionsOfType(DefinitionType.Mapping) as MappingDefinition[];
DefinitionType.Mapping
) as MappingDefinition[]
// Legal // Legal
if (mappingDefinitions.length > 0) { if (mappingDefinitions.length > 0) {
@@ -117,42 +102,27 @@ class TemplateEvaluator {
Object.keys(mappingDefinitions[0].properties).length > 0 || Object.keys(mappingDefinitions[0].properties).length > 0 ||
!mappingDefinitions[0].looseKeyType !mappingDefinitions[0].looseKeyType
) { ) {
this.handleMappingWithWellKnownProperties( this.handleMappingWithWellKnownProperties(definition, mappingDefinitions, mapping);
definition,
mappingDefinitions,
mapping
)
} else { } else {
const keyDefinition = new DefinitionInfo( const keyDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseKeyType);
definition, const valueDefinition = new DefinitionInfo(definition, mappingDefinitions[0].looseValueType);
mappingDefinitions[0].looseKeyType this.handleMappingWithAllLooseProperties(definition, keyDefinition, valueDefinition, mapping);
)
const valueDefinition = new DefinitionInfo(
definition,
mappingDefinitions[0].looseValueType
)
this.handleMappingWithAllLooseProperties(
definition,
keyDefinition,
valueDefinition,
mapping
)
} }
} }
// Illegal // Illegal
else { else {
this.context.error(mapping, "A mapping was not expected") this.context.error(mapping, "A mapping was not expected");
while (!this.unraveler.allowMappingEnd(false)) { while (!this.unraveler.allowMappingEnd(false)) {
this.unraveler.skipMappingKey() this.unraveler.skipMappingKey();
this.unraveler.skipMappingValue() this.unraveler.skipMappingValue();
} }
} }
return mapping return mapping;
} }
throw new Error("Expected a scalar value, a sequence, or a mapping") throw new Error("Expected a scalar value, a sequence, or a mapping");
} }
private handleMappingWithWellKnownProperties( private handleMappingWithWellKnownProperties(
@@ -161,26 +131,26 @@ class TemplateEvaluator {
mapping: MappingToken mapping: MappingToken
) { ) {
// Check if loose properties are allowed // Check if loose properties are allowed
let looseKeyType: string | undefined let looseKeyType: string | undefined;
let looseValueType: string | undefined let looseValueType: string | undefined;
let looseKeyDefinition: DefinitionInfo | undefined let looseKeyDefinition: DefinitionInfo | undefined;
let looseValueDefinition: DefinitionInfo | undefined let looseValueDefinition: DefinitionInfo | undefined;
if (mappingDefinitions[0].looseKeyType) { if (mappingDefinitions[0].looseKeyType) {
looseKeyType = mappingDefinitions[0].looseKeyType looseKeyType = mappingDefinitions[0].looseKeyType;
looseValueType = mappingDefinitions[0].looseValueType looseValueType = mappingDefinitions[0].looseValueType;
} }
const upperKeys: { [upperKey: string]: boolean } = {} const upperKeys: {[upperKey: string]: boolean} = {};
let hasExpressionKey = false let hasExpressionKey = false;
let nextKeyScalar: ScalarToken | undefined let nextKeyScalar: ScalarToken | undefined;
while ((nextKeyScalar = this.unraveler.allowScalar(definition.expand))) { while ((nextKeyScalar = this.unraveler.allowScalar(definition.expand))) {
// Expression // Expression
if (nextKeyScalar.isExpression) { if (nextKeyScalar.isExpression) {
hasExpressionKey = true hasExpressionKey = true;
const anyDefinition = new DefinitionInfo(definition, ANY) const anyDefinition = new DefinitionInfo(definition, ANY);
mapping.add(nextKeyScalar, this.evaluate(anyDefinition)) mapping.add(nextKeyScalar, this.evaluate(anyDefinition));
continue continue;
} }
// Convert to StringToken if required // Convert to StringToken if required
@@ -192,64 +162,58 @@ class TemplateEvaluator {
nextKeyScalar.range, nextKeyScalar.range,
nextKeyScalar.toString(), nextKeyScalar.toString(),
nextKeyScalar.definitionInfo nextKeyScalar.definitionInfo
) );
// Duplicate // Duplicate
const upperKey = nextKey.value.toUpperCase() const upperKey = nextKey.value.toUpperCase();
if (upperKeys[upperKey]) { if (upperKeys[upperKey]) {
this.context.error(nextKey, `'${nextKey.value}' is already defined`) this.context.error(nextKey, `'${nextKey.value}' is already defined`);
this.unraveler.skipMappingValue() this.unraveler.skipMappingValue();
continue continue;
} }
upperKeys[upperKey] = true upperKeys[upperKey] = true;
// Well known // Well known
const nextValuePropertyDef = this.schema.matchPropertyAndFilter( const nextValuePropertyDef = this.schema.matchPropertyAndFilter(mappingDefinitions, nextKey.value);
mappingDefinitions,
nextKey.value
)
if (nextValuePropertyDef?.type) { if (nextValuePropertyDef?.type) {
const nextValueDefinition = new DefinitionInfo( const nextValueDefinition = new DefinitionInfo(definition, nextValuePropertyDef.type);
definition, const nextValue = this.evaluate(nextValueDefinition);
nextValuePropertyDef.type nextValue.propertyDefinition = nextValuePropertyDef;
) mapping.add(nextKey, nextValue);
const nextValue = this.evaluate(nextValueDefinition) continue;
nextValue.propertyDefinition = nextValuePropertyDef
mapping.add(nextKey, nextValue)
continue
} }
// Loose // Loose
if (looseKeyType) { if (looseKeyType) {
if (!looseKeyDefinition) { if (!looseKeyDefinition) {
looseKeyDefinition = new DefinitionInfo(definition, looseKeyType) looseKeyDefinition = new DefinitionInfo(definition, looseKeyType);
looseValueDefinition = new DefinitionInfo(definition, looseValueType!) looseValueDefinition = new DefinitionInfo(definition, looseValueType!);
} }
this.validate(nextKey, looseKeyDefinition) this.validate(nextKey, looseKeyDefinition);
const nextValue = this.evaluate(looseValueDefinition!) const nextValue = this.evaluate(looseValueDefinition!);
mapping.add(nextKey, nextValue) mapping.add(nextKey, nextValue);
continue continue;
} }
// Error // Error
this.context.error(nextKey, `Unexpected value '${nextKey.value}'`) this.context.error(nextKey, `Unexpected value '${nextKey.value}'`);
this.unraveler.skipMappingValue() this.unraveler.skipMappingValue();
} }
// Unable to filter to one definition // Unable to filter to one definition
if (mappingDefinitions.length > 1) { if (mappingDefinitions.length > 1) {
const hitCount: { [key: string]: number } = {} const hitCount: {[key: string]: number} = {};
for (const mappingDefinition of mappingDefinitions) { for (const mappingDefinition of mappingDefinitions) {
for (const key of Object.keys(mappingDefinition.properties)) { for (const key of Object.keys(mappingDefinition.properties)) {
hitCount[key] = (hitCount[key] ?? 0) + 1 hitCount[key] = (hitCount[key] ?? 0) + 1;
} }
} }
const nonDuplicates: string[] = [] const nonDuplicates: string[] = [];
for (const key of Object.keys(hitCount)) { for (const key of Object.keys(hitCount)) {
if (hitCount[key] === 1) { if (hitCount[key] === 1) {
nonDuplicates.push(key) nonDuplicates.push(key);
} }
} }
@@ -258,24 +222,19 @@ class TemplateEvaluator {
`There's not enough info to determine what you meant. Add one of these properties: ${nonDuplicates `There's not enough info to determine what you meant. Add one of these properties: ${nonDuplicates
.sort() .sort()
.join(", ")}` .join(", ")}`
) );
} }
// Check required properties // Check required properties
else if (mappingDefinitions.length === 1 && !hasExpressionKey) { else if (mappingDefinitions.length === 1 && !hasExpressionKey) {
for (const propertyName of Object.keys( for (const propertyName of Object.keys(mappingDefinitions[0].properties)) {
mappingDefinitions[0].properties const propertyDef = mappingDefinitions[0].properties[propertyName];
)) {
const propertyDef = mappingDefinitions[0].properties[propertyName]
if (propertyDef.required && !upperKeys[propertyName.toUpperCase()]) { if (propertyDef.required && !upperKeys[propertyName.toUpperCase()]) {
this.context.error( this.context.error(mapping, `Required property is missing: ${propertyName}`);
mapping,
`Required property is missing: ${propertyName}`
)
} }
} }
} }
this.unraveler.readMappingEnd() this.unraveler.readMappingEnd();
} }
private handleMappingWithAllLooseProperties( private handleMappingWithAllLooseProperties(
@@ -284,22 +243,20 @@ class TemplateEvaluator {
valueDefinition: DefinitionInfo, valueDefinition: DefinitionInfo,
mapping: MappingToken mapping: MappingToken
): void { ): void {
const upperKeys: { [key: string]: boolean } = {} const upperKeys: {[key: string]: boolean} = {};
let nextKeyScalar: ScalarToken | undefined let nextKeyScalar: ScalarToken | undefined;
while ( while ((nextKeyScalar = this.unraveler.allowScalar(mappingDefinition.expand))) {
(nextKeyScalar = this.unraveler.allowScalar(mappingDefinition.expand))
) {
// Expression // Expression
if (nextKeyScalar.isExpression) { if (nextKeyScalar.isExpression) {
if (nextKeyScalar.templateTokenType === TokenType.BasicExpression) { if (nextKeyScalar.templateTokenType === TokenType.BasicExpression) {
mapping.add(nextKeyScalar, this.evaluate(valueDefinition)) mapping.add(nextKeyScalar, this.evaluate(valueDefinition));
} else { } else {
const anyDefinition = new DefinitionInfo(mappingDefinition, ANY) const anyDefinition = new DefinitionInfo(mappingDefinition, ANY);
mapping.add(nextKeyScalar, this.evaluate(anyDefinition)) mapping.add(nextKeyScalar, this.evaluate(anyDefinition));
} }
continue continue;
} }
// Convert to StringToken if required // Convert to StringToken if required
@@ -311,56 +268,48 @@ class TemplateEvaluator {
nextKeyScalar.range, nextKeyScalar.range,
nextKeyScalar.toString(), nextKeyScalar.toString(),
nextKeyScalar.definitionInfo nextKeyScalar.definitionInfo
) );
// Duplicate // Duplicate
const upperKey = nextKey.value.toUpperCase() const upperKey = nextKey.value.toUpperCase();
if (upperKeys[upperKey]) { if (upperKeys[upperKey]) {
this.context.error(nextKey, `'${nextKey.value}' is already defined`) this.context.error(nextKey, `'${nextKey.value}' is already defined`);
this.unraveler.skipMappingValue() this.unraveler.skipMappingValue();
continue continue;
} }
upperKeys[upperKey] = true upperKeys[upperKey] = true;
// Validate // Validate
this.validate(nextKey, keyDefinition) this.validate(nextKey, keyDefinition);
// Add the pair // Add the pair
const nextValue = this.evaluate(valueDefinition) const nextValue = this.evaluate(valueDefinition);
mapping.add(nextKey, nextValue) mapping.add(nextKey, nextValue);
} }
this.unraveler.readMappingEnd() this.unraveler.readMappingEnd();
} }
private validate( private validate(literal: LiteralToken, definition: DefinitionInfo): LiteralToken {
literal: LiteralToken,
definition: DefinitionInfo
): LiteralToken {
// Legal // Legal
const scalarDefinitions = definition.getScalarDefinitions() const scalarDefinitions = definition.getScalarDefinitions();
if (scalarDefinitions.some((x) => x.isMatch(literal))) { if (scalarDefinitions.some(x => x.isMatch(literal))) {
return literal return literal;
} }
// Not a string, convert // Not a string, convert
if (literal.templateTokenType !== TokenType.String) { if (literal.templateTokenType !== TokenType.String) {
const stringLiteral = new StringToken( const stringLiteral = new StringToken(literal.file, literal.range, literal.toString(), literal.definitionInfo);
literal.file,
literal.range,
literal.toString(),
literal.definitionInfo
)
// Legal // Legal
if (scalarDefinitions.some((x) => x.isMatch(stringLiteral))) { if (scalarDefinitions.some(x => x.isMatch(stringLiteral))) {
return stringLiteral return stringLiteral;
} }
} }
// Illegal // Illegal
this.context.error(literal, `Unexpected value '${literal.toString()}'`) this.context.error(literal, `Unexpected value '${literal.toString()}'`);
return literal return literal;
} }
} }
@@ -368,83 +317,78 @@ class DefinitionInfo {
/** /**
* Hashtable of available contexts * Hashtable of available contexts
*/ */
private readonly _upperAvailable: { [context: string]: boolean } private readonly _upperAvailable: {[context: string]: boolean};
/** /**
* Allowed context * Allowed context
*/ */
private readonly _allowed: string[] private readonly _allowed: string[];
private readonly _schema: TemplateSchema private readonly _schema: TemplateSchema;
public readonly isDefinitionInfo = true public readonly isDefinitionInfo = true;
public readonly definition: Definition public readonly definition: Definition;
public readonly expand: boolean public readonly expand: boolean;
public constructor(context: TemplateContext, name: string) public constructor(context: TemplateContext, name: string);
public constructor(parent: DefinitionInfo, name: string) public constructor(parent: DefinitionInfo, name: string);
public constructor( public constructor(contextOrParent: TemplateContext | DefinitionInfo, name: string) {
contextOrParent: TemplateContext | DefinitionInfo,
name: string
) {
// "parent" overload // "parent" overload
let parent: DefinitionInfo | undefined let parent: DefinitionInfo | undefined;
if ( if ((contextOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true) {
(contextOrParent as DefinitionInfo | undefined)?.isDefinitionInfo === true parent = contextOrParent as DefinitionInfo;
) { this._schema = parent._schema;
parent = contextOrParent as DefinitionInfo this._upperAvailable = parent._upperAvailable;
this._schema = parent._schema
this._upperAvailable = parent._upperAvailable
} }
// "context" overload // "context" overload
else { else {
const context = contextOrParent as TemplateContext const context = contextOrParent as TemplateContext;
this._schema = context.schema this._schema = context.schema;
this._upperAvailable = {} this._upperAvailable = {};
for (const namedContext of context.expressionNamedContexts) { for (const namedContext of context.expressionNamedContexts) {
this._upperAvailable[namedContext.toUpperCase()] = true this._upperAvailable[namedContext.toUpperCase()] = true;
} }
for (const func of context.expressionFunctions) { for (const func of context.expressionFunctions) {
this._upperAvailable[`${func.name}()`.toUpperCase()] = true this._upperAvailable[`${func.name}()`.toUpperCase()] = true;
} }
} }
// Lookup the definition // Lookup the definition
this.definition = this._schema.getDefinition(name) this.definition = this._schema.getDefinition(name);
// Record allowed context // Record allowed context
if (this.definition.evaluatorContext.length > 0) { if (this.definition.evaluatorContext.length > 0) {
this._allowed = [] this._allowed = [];
this.expand = true this.expand = true;
// Copy parent allowed context // Copy parent allowed context
const upperSeen: { [upper: string]: boolean } = {} const upperSeen: {[upper: string]: boolean} = {};
for (const context of parent?._allowed ?? []) { for (const context of parent?._allowed ?? []) {
this._allowed.push(context) this._allowed.push(context);
const upper = context.toUpperCase() const upper = context.toUpperCase();
upperSeen[upper] = true upperSeen[upper] = true;
if (!this._upperAvailable[upper]) { if (!this._upperAvailable[upper]) {
this.expand = false this.expand = false;
} }
} }
// Append context if unseen // Append context if unseen
for (const context of this.definition.evaluatorContext) { for (const context of this.definition.evaluatorContext) {
const upper = context.toUpperCase() const upper = context.toUpperCase();
if (!upperSeen[upper]) { if (!upperSeen[upper]) {
this._allowed.push(context) this._allowed.push(context);
upperSeen[upper] = true upperSeen[upper] = true;
if (!this._upperAvailable[upper]) [(this.expand = false)] if (!this._upperAvailable[upper]) [(this.expand = false)];
} }
} }
} else { } else {
this._allowed = parent?._allowed ?? [] this._allowed = parent?._allowed ?? [];
this.expand = parent?.expand ?? false this.expand = parent?.expand ?? false;
} }
} }
public getScalarDefinitions(): ScalarDefinition[] { public getScalarDefinitions(): ScalarDefinition[] {
return this._schema.getScalarDefinitions(this.definition) return this._schema.getScalarDefinitions(this.definition);
} }
public getDefinitionsOfType(type: DefinitionType): Definition[] { public getDefinitionsOfType(type: DefinitionType): Definition[] {
return this._schema.getDefinitionsOfType(this.definition, type) return this._schema.getDefinitionsOfType(this.definition, type);
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
import { TokenRange } from "./tokens/token-range" import {TokenRange} from "./tokens/token-range";
/** /**
* Provides information about an error which occurred during validation * Provides information about an error which occurred during validation
@@ -13,13 +13,13 @@ export class TemplateValidationError {
public get message(): string { public get message(): string {
if (this.prefix) { if (this.prefix) {
return `${this.prefix}: ${this.rawMessage}` return `${this.prefix}: ${this.rawMessage}`;
} }
return this.rawMessage return this.rawMessage;
} }
public toString(): string { public toString(): string {
return this.message return this.message;
} }
} }
@@ -1,17 +1,17 @@
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { CLOSE_EXPRESSION, OPEN_EXPRESSION } from "../template-constants" import {CLOSE_EXPRESSION, OPEN_EXPRESSION} from "../template-constants";
import { ExpressionToken } from "./expression-token" import {ExpressionToken} from "./expression-token";
import { ScalarToken } from "./scalar-token" import {ScalarToken} from "./scalar-token";
import { SerializedExpressionToken } from "./serialization" import {SerializedExpressionToken} from "./serialization";
import { TemplateToken } from "./template-token" import {TemplateToken} from "./template-token";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
import { TokenType } from "./types" import {TokenType} from "./types";
export class BasicExpressionToken extends ExpressionToken { export class BasicExpressionToken extends ExpressionToken {
private readonly expr: string private readonly expr: string;
public readonly originalExpressions: BasicExpressionToken[] | undefined public readonly originalExpressions: BasicExpressionToken[] | undefined;
/** /**
* @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones * @param originalExpressions If the basic expression was transformed from individual expressions, these will be the original ones
@@ -23,46 +23,34 @@ export class BasicExpressionToken extends ExpressionToken {
definitionInfo: DefinitionInfo | undefined, definitionInfo: DefinitionInfo | undefined,
originalExpressions: BasicExpressionToken[] | undefined originalExpressions: BasicExpressionToken[] | undefined
) { ) {
super(TokenType.BasicExpression, file, range, undefined, definitionInfo) super(TokenType.BasicExpression, file, range, undefined, definitionInfo);
this.expr = expression this.expr = expression;
this.originalExpressions = originalExpressions this.originalExpressions = originalExpressions;
} }
public get expression(): string { public get expression(): string {
return this.expr return this.expr;
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
return omitSource return omitSource
? new BasicExpressionToken( ? new BasicExpressionToken(undefined, undefined, this.expr, this.definitionInfo, this.originalExpressions)
undefined, : new BasicExpressionToken(this.file, this.range, this.expr, this.definitionInfo, this.originalExpressions);
undefined,
this.expr,
this.definitionInfo,
this.originalExpressions
)
: new BasicExpressionToken(
this.file,
this.range,
this.expr,
this.definitionInfo,
this.originalExpressions
)
} }
public override toString(): string { public override toString(): string {
return `${OPEN_EXPRESSION} ${this.expr} ${CLOSE_EXPRESSION}` return `${OPEN_EXPRESSION} ${this.expr} ${CLOSE_EXPRESSION}`;
} }
public override toDisplayString(): string { public override toDisplayString(): string {
// TODO: Implement expression display string to match `BasicExpressionToken#ToDisplayString()` in the C# parser // TODO: Implement expression display string to match `BasicExpressionToken#ToDisplayString()` in the C# parser
return ScalarToken.trimDisplayString(this.toString()) return ScalarToken.trimDisplayString(this.toString());
} }
public override toJSON(): SerializedExpressionToken { public override toJSON(): SerializedExpressionToken {
return { return {
type: TokenType.BasicExpression, type: TokenType.BasicExpression,
expr: this.expr, expr: this.expr
} };
} }
} }
@@ -1,10 +1,10 @@
import { LiteralToken, TemplateToken } from "." import {LiteralToken, TemplateToken} from ".";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
import { TokenType } from "./types" import {TokenType} from "./types";
export class BooleanToken extends LiteralToken { export class BooleanToken extends LiteralToken {
private readonly bool: boolean private readonly bool: boolean;
public constructor( public constructor(
file: number | undefined, file: number | undefined,
@@ -12,25 +12,25 @@ export class BooleanToken extends LiteralToken {
value: boolean, value: boolean,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(TokenType.Boolean, file, range, definitionInfo) super(TokenType.Boolean, file, range, definitionInfo);
this.bool = value this.bool = value;
} }
public get value(): boolean { public get value(): boolean {
return this.bool return this.bool;
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
return omitSource return omitSource
? new BooleanToken(undefined, undefined, this.bool, this.definitionInfo) ? new BooleanToken(undefined, undefined, this.bool, this.definitionInfo)
: new BooleanToken(this.file, this.range, this.bool, this.definitionInfo) : new BooleanToken(this.file, this.range, this.bool, this.definitionInfo);
} }
public override toString(): string { public override toString(): string {
return this.bool ? "true" : "false" return this.bool ? "true" : "false";
} }
public override toJSON() { public override toJSON() {
return this.bool return this.bool;
} }
} }
@@ -1,11 +1,11 @@
import { Lexer, Parser } from "@github/actions-expressions" import {Lexer, Parser} from "@github/actions-expressions";
import { splitAllowedContext } from "../allowed-context" import {splitAllowedContext} from "../allowed-context";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { ScalarToken } from "./scalar-token" import {ScalarToken} from "./scalar-token";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
export abstract class ExpressionToken extends ScalarToken { export abstract class ExpressionToken extends ScalarToken {
public readonly directive: string | undefined public readonly directive: string | undefined;
public constructor( public constructor(
type: number, type: number,
@@ -14,28 +14,25 @@ export abstract class ExpressionToken extends ScalarToken {
directive: string | undefined, directive: string | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(type, file, range, definitionInfo) super(type, file, range, definitionInfo);
this.directive = directive this.directive = directive;
} }
public override get isLiteral(): boolean { public override get isLiteral(): boolean {
return false return false;
} }
public override get isExpression(): boolean { public override get isExpression(): boolean {
return true return true;
} }
public static validateExpression( public static validateExpression(expression: string, allowedContext: string[]): void {
expression: string, const {namedContexts, functions} = splitAllowedContext(allowedContext);
allowedContext: string[]
): void {
const { namedContexts, functions } = splitAllowedContext(allowedContext)
// Parse // Parse
const lexer = new Lexer(expression) const lexer = new Lexer(expression);
const result = lexer.lex() const result = lexer.lex();
const p = new Parser(result.tokens, namedContexts, functions) const p = new Parser(result.tokens, namedContexts, functions);
p.parse() p.parse();
} }
} }
@@ -1,13 +1,13 @@
export { TemplateToken } from "./template-token" export {TemplateToken} from "./template-token";
export { ScalarToken } from "./scalar-token" export {ScalarToken} from "./scalar-token";
export { LiteralToken } from "./literal-token" export {LiteralToken} from "./literal-token";
export { StringToken } from "./string-token" export {StringToken} from "./string-token";
export { NumberToken } from "./number-token" export {NumberToken} from "./number-token";
export { BooleanToken } from "./boolean-token" export {BooleanToken} from "./boolean-token";
export { NullToken } from "./null-token" export {NullToken} from "./null-token";
export { KeyValuePair } from "./key-value-pair" export {KeyValuePair} from "./key-value-pair";
export { SequenceToken } from "./sequence-token" export {SequenceToken} from "./sequence-token";
export { MappingToken } from "./mapping-token" export {MappingToken} from "./mapping-token";
export { ExpressionToken } from "./expression-token" export {ExpressionToken} from "./expression-token";
export { BasicExpressionToken } from "./basic-expression-token" export {BasicExpressionToken} from "./basic-expression-token";
export { InsertExpressionToken } from "./insert-expression-token" export {InsertExpressionToken} from "./insert-expression-token";
@@ -1,13 +1,9 @@
import { TemplateToken, ScalarToken, ExpressionToken } from "." import {TemplateToken, ScalarToken, ExpressionToken} from ".";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { import {INSERT_DIRECTIVE, OPEN_EXPRESSION, CLOSE_EXPRESSION} from "../template-constants";
INSERT_DIRECTIVE, import {SerializedExpressionToken} from "./serialization";
OPEN_EXPRESSION, import {TokenRange} from "./token-range";
CLOSE_EXPRESSION, import {TokenType} from "./types";
} from "../template-constants"
import { SerializedExpressionToken } from "./serialization"
import { TokenRange } from "./token-range"
import { TokenType } from "./types"
export class InsertExpressionToken extends ExpressionToken { export class InsertExpressionToken extends ExpressionToken {
public constructor( public constructor(
@@ -15,33 +11,27 @@ export class InsertExpressionToken extends ExpressionToken {
range: TokenRange | undefined, range: TokenRange | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super( super(TokenType.InsertExpression, file, range, INSERT_DIRECTIVE, definitionInfo);
TokenType.InsertExpression,
file,
range,
INSERT_DIRECTIVE,
definitionInfo
)
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
return omitSource return omitSource
? new InsertExpressionToken(undefined, undefined, this.definitionInfo) ? new InsertExpressionToken(undefined, undefined, this.definitionInfo)
: new InsertExpressionToken(this.file, this.range, this.definitionInfo) : new InsertExpressionToken(this.file, this.range, this.definitionInfo);
} }
public override toString(): string { public override toString(): string {
return `${OPEN_EXPRESSION} ${INSERT_DIRECTIVE} ${CLOSE_EXPRESSION}` return `${OPEN_EXPRESSION} ${INSERT_DIRECTIVE} ${CLOSE_EXPRESSION}`;
} }
public override toDisplayString(): string { public override toDisplayString(): string {
return ScalarToken.trimDisplayString(this.toString()) return ScalarToken.trimDisplayString(this.toString());
} }
public override toJSON(): SerializedExpressionToken { public override toJSON(): SerializedExpressionToken {
return { return {
type: TokenType.InsertExpression, type: TokenType.InsertExpression,
expr: "insert", expr: "insert"
} };
} }
} }
@@ -1,11 +1,11 @@
import { ScalarToken } from "./scalar-token" import {ScalarToken} from "./scalar-token";
import { TemplateToken } from "./template-token" import {TemplateToken} from "./template-token";
export class KeyValuePair { export class KeyValuePair {
public readonly key: ScalarToken public readonly key: ScalarToken;
public readonly value: TemplateToken public readonly value: TemplateToken;
public constructor(key: ScalarToken, value: TemplateToken) { public constructor(key: ScalarToken, value: TemplateToken) {
this.key = key this.key = key;
this.value = value this.value = value;
} }
} }
@@ -1,6 +1,6 @@
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { ScalarToken } from "./scalar-token" import {ScalarToken} from "./scalar-token";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
export abstract class LiteralToken extends ScalarToken { export abstract class LiteralToken extends ScalarToken {
public constructor( public constructor(
@@ -9,27 +9,25 @@ export abstract class LiteralToken extends ScalarToken {
range: TokenRange | undefined, range: TokenRange | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(type, file, range, definitionInfo) super(type, file, range, definitionInfo);
} }
public override get isLiteral(): boolean { public override get isLiteral(): boolean {
return true return true;
} }
public override get isExpression(): boolean { public override get isExpression(): boolean {
return false return false;
} }
public override toDisplayString(): string { public override toDisplayString(): string {
return ScalarToken.trimDisplayString(this.toString()) return ScalarToken.trimDisplayString(this.toString());
} }
/** /**
* Throws a good debug message when an unexpected literal value is encountered * Throws a good debug message when an unexpected literal value is encountered
*/ */
public assertUnexpectedValue(objectDescription: string): void { public assertUnexpectedValue(objectDescription: string): void {
throw new Error( throw new Error(`Error while reading '${objectDescription}'. Unexpected value '${this.toString()}'`);
`Error while reading '${objectDescription}'. Unexpected value '${this.toString()}'`
)
} }
} }
@@ -1,80 +1,77 @@
import { TemplateToken, KeyValuePair, ScalarToken } from "." import {TemplateToken, KeyValuePair, ScalarToken} from ".";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { MapItem, SerializedToken } from "./serialization" import {MapItem, SerializedToken} from "./serialization";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
import { TokenType } from "./types" import {TokenType} from "./types";
export class MappingToken extends TemplateToken { export class MappingToken extends TemplateToken {
private readonly map: KeyValuePair[] = [] private readonly map: KeyValuePair[] = [];
public constructor( public constructor(
file: number | undefined, file: number | undefined,
range: TokenRange | undefined, range: TokenRange | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(TokenType.Mapping, file, range, definitionInfo) super(TokenType.Mapping, file, range, definitionInfo);
} }
public get count(): number { public get count(): number {
return this.map.length return this.map.length;
} }
public override get isScalar(): boolean { public override get isScalar(): boolean {
return false return false;
} }
public override get isLiteral(): boolean { public override get isLiteral(): boolean {
return false return false;
} }
public override get isExpression(): boolean { public override get isExpression(): boolean {
return false return false;
} }
public add(key: ScalarToken, value: TemplateToken): void { public add(key: ScalarToken, value: TemplateToken): void {
this.map.push(new KeyValuePair(key, value)) this.map.push(new KeyValuePair(key, value));
} }
public get(index: number): KeyValuePair { public get(index: number): KeyValuePair {
return this.map[index] return this.map[index];
} }
public find(key: string): TemplateToken | undefined { public find(key: string): TemplateToken | undefined {
const pair = this.map.find((pair) => pair.key.toString() === key) const pair = this.map.find(pair => pair.key.toString() === key);
return pair?.value return pair?.value;
} }
public remove(index: number): void { public remove(index: number): void {
this.map.splice(index, 1) this.map.splice(index, 1);
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
const result = omitSource const result = omitSource
? new MappingToken(undefined, undefined, this.definitionInfo) ? new MappingToken(undefined, undefined, this.definitionInfo)
: new MappingToken(this.file, this.range, this.definitionInfo) : new MappingToken(this.file, this.range, this.definitionInfo);
for (const item of this.map) { for (const item of this.map) {
result.add( result.add(item.key.clone(omitSource) as ScalarToken, item.value.clone(omitSource));
item.key.clone(omitSource) as ScalarToken,
item.value.clone(omitSource)
)
} }
return result return result;
} }
public override toJSON(): SerializedToken { public override toJSON(): SerializedToken {
const items: MapItem[] = [] const items: MapItem[] = [];
for (const item of this.map) { for (const item of this.map) {
items.push({ Key: item.key, Value: item.value }) items.push({Key: item.key, Value: item.value});
} }
return { return {
type: TokenType.Mapping, type: TokenType.Mapping,
map: items, map: items
} };
} }
public *[Symbol.iterator](): Iterator<KeyValuePair> { public *[Symbol.iterator](): Iterator<KeyValuePair> {
for (const item of this.map) { for (const item of this.map) {
yield item yield item;
} }
} }
} }
@@ -1,7 +1,7 @@
import { LiteralToken, TemplateToken } from "." import {LiteralToken, TemplateToken} from ".";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
import { TokenType } from "./types" import {TokenType} from "./types";
export class NullToken extends LiteralToken { export class NullToken extends LiteralToken {
public constructor( public constructor(
@@ -9,20 +9,20 @@ export class NullToken extends LiteralToken {
range: TokenRange | undefined, range: TokenRange | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(TokenType.Null, file, range, definitionInfo) super(TokenType.Null, file, range, definitionInfo);
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
return omitSource return omitSource
? new NullToken(undefined, undefined, this.definitionInfo) ? new NullToken(undefined, undefined, this.definitionInfo)
: new NullToken(this.file, this.range, this.definitionInfo) : new NullToken(this.file, this.range, this.definitionInfo);
} }
public override toString(): string { public override toString(): string {
return "" return "";
} }
public override toJSON() { public override toJSON() {
return "null" return "null";
} }
} }
@@ -1,10 +1,10 @@
import { LiteralToken, TemplateToken } from "." import {LiteralToken, TemplateToken} from ".";
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
import { TokenType } from "./types" import {TokenType} from "./types";
export class NumberToken extends LiteralToken { export class NumberToken extends LiteralToken {
private readonly num: number private readonly num: number;
public constructor( public constructor(
file: number | undefined, file: number | undefined,
@@ -12,25 +12,25 @@ export class NumberToken extends LiteralToken {
value: number, value: number,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(TokenType.Number, file, range, definitionInfo) super(TokenType.Number, file, range, definitionInfo);
this.num = value this.num = value;
} }
public get value(): number { public get value(): number {
return this.num return this.num;
} }
public override clone(omitSource?: boolean): TemplateToken { public override clone(omitSource?: boolean): TemplateToken {
return omitSource return omitSource
? new NumberToken(undefined, undefined, this.num, this.definitionInfo) ? new NumberToken(undefined, undefined, this.num, this.definitionInfo)
: new NumberToken(this.file, this.range, this.num, this.definitionInfo) : new NumberToken(this.file, this.range, this.num, this.definitionInfo);
} }
public override toString(): string { public override toString(): string {
return `${this.num}` return `${this.num}`;
} }
public override toJSON() { public override toJSON() {
return this.num return this.num;
} }
} }
@@ -1,6 +1,6 @@
import { DefinitionInfo } from "../schema/definition-info" import {DefinitionInfo} from "../schema/definition-info";
import { TemplateToken } from "./template-token" import {TemplateToken} from "./template-token";
import { TokenRange } from "./token-range" import {TokenRange} from "./token-range";
/** /**
* Base class for everything that is not a mapping or sequence * Base class for everything that is not a mapping or sequence
@@ -12,21 +12,21 @@ export abstract class ScalarToken extends TemplateToken {
range: TokenRange | undefined, range: TokenRange | undefined,
definitionInfo: DefinitionInfo | undefined definitionInfo: DefinitionInfo | undefined
) { ) {
super(type, file, range, definitionInfo) super(type, file, range, definitionInfo);
} }
public abstract toString(): string public abstract toString(): string;
public abstract toDisplayString(): string public abstract toDisplayString(): string;
public override get isScalar(): boolean { public override get isScalar(): boolean {
return true return true;
} }
protected static trimDisplayString(displayString: string): string { protected static trimDisplayString(displayString: string): string {
let firstLine = displayString.trimStart() let firstLine = displayString.trimStart();
const firstNewLine = firstLine.indexOf("\n") const firstNewLine = firstLine.indexOf("\n");
const firstCarriageReturn = firstLine.indexOf("\r") const firstCarriageReturn = firstLine.indexOf("\r");
if (firstNewLine >= 0 || firstCarriageReturn >= 0) { if (firstNewLine >= 0 || firstCarriageReturn >= 0) {
firstLine = firstLine.substr( firstLine = firstLine.substr(
0, 0,
@@ -34,8 +34,8 @@ export abstract class ScalarToken extends TemplateToken {
firstNewLine >= 0 ? firstNewLine : Number.MAX_VALUE, firstNewLine >= 0 ? firstNewLine : Number.MAX_VALUE,
firstCarriageReturn >= 0 ? firstCarriageReturn : Number.MAX_VALUE firstCarriageReturn >= 0 ? firstCarriageReturn : Number.MAX_VALUE
) )
) );
} }
return firstLine return firstLine;
} }
} }

Some files were not shown because too many files have changed in this diff Show More