Merge pull request #79 from github/joshmgross/format
Use a standard format configuration across packages
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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,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")}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,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,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) {}
|
||||||
|
|||||||
@@ -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}');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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) {}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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")
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
import { ExpressionData } from "../data";
|
import {ExpressionData} from "../data";
|
||||||
|
|
||||||
export interface FunctionInfo {
|
export interface FunctionInfo {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -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("");
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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));
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"tabWidth": 2,
|
|
||||||
"useTabs": false,
|
|
||||||
"printWidth": 120,
|
|
||||||
"singleQuote": false,
|
|
||||||
"bracketSpacing": false,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
/dist
|
|
||||||
/node_modules
|
|
||||||
/src/workflows/workflow-v1.0.json
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": false
|
|
||||||
}
|
|
||||||
@@ -60,4 +60,4 @@
|
|||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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: []
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user