Merge parser and expressions into repository
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
@github:registry=https://npm.pkg.github.com
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ["ts", "js"],
|
||||
};
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@github/actions-expressions",
|
||||
"version": "0.0.9",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./dist/*.js"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"dist/index.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"dist/*.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/github/actions-expressions/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build tsconfig.build.json",
|
||||
"clean": "rimraf dist",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
||||
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.7.4"
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm run build
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm test
|
||||
@@ -0,0 +1,103 @@
|
||||
import { ExpressionData } from "./data";
|
||||
import { Token } from "./lexer";
|
||||
|
||||
export interface ExprVisitor<R> {
|
||||
visitLiteral(literal: Literal): R;
|
||||
visitUnary(unary: Unary): R;
|
||||
visitBinary(binary: Binary): R;
|
||||
visitLogical(binary: Logical): R;
|
||||
visitGrouping(grouping: Grouping): R;
|
||||
visitContextAccess(contextAccess: ContextAccess): R;
|
||||
visitIndexAccess(indexAccess: IndexAccess): R;
|
||||
visitFunctionCall(functionCall: FunctionCall): R;
|
||||
}
|
||||
|
||||
export abstract class Expr {
|
||||
abstract accept<R>(v: ExprVisitor<R>): R;
|
||||
}
|
||||
|
||||
export class Literal extends Expr {
|
||||
constructor(public literal: ExpressionData) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitLiteral(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Unary extends Expr {
|
||||
constructor(public operator: Token, public expr: Expr) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitUnary(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionCall extends Expr {
|
||||
constructor(public functionName: Token, public args: Expr[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitFunctionCall(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Binary extends Expr {
|
||||
constructor(public left: Expr, public operator: Token, public right: Expr) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitBinary(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Logical extends Expr {
|
||||
constructor(public operator: Token, public args: Expr[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitLogical(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Grouping extends Expr {
|
||||
constructor(public group: Expr) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitGrouping(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class ContextAccess extends Expr {
|
||||
constructor(public name: Token) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitContextAccess(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndexAccess extends Expr {
|
||||
constructor(public expr: Expr, public index: Expr) {
|
||||
super();
|
||||
}
|
||||
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
return v.visitIndexAccess(this);
|
||||
}
|
||||
}
|
||||
|
||||
export class Star extends Expr {
|
||||
accept<R>(v: ExprVisitor<R>): R {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { complete, CompletionItem, trimTokenVector } from "./completion";
|
||||
import { BooleanData } from "./data/boolean";
|
||||
import { Dictionary } from "./data/dictionary";
|
||||
import { StringData } from "./data/string";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Lexer, TokenType } from "./lexer";
|
||||
|
||||
const testContext = new Dictionary(
|
||||
{
|
||||
key: "env",
|
||||
value: new Dictionary(
|
||||
{
|
||||
key: "FOO",
|
||||
value: new StringData(""),
|
||||
},
|
||||
{
|
||||
key: "BAR_TEST",
|
||||
value: new StringData(""),
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "secrets",
|
||||
value: new Dictionary({
|
||||
key: "AWS_TOKEN",
|
||||
value: new BooleanData(true),
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "hashFiles(1,255)",
|
||||
value: new Dictionary(),
|
||||
}
|
||||
);
|
||||
|
||||
const testFunctions: FunctionInfo[] = [];
|
||||
|
||||
const testComplete = (input: string): CompletionItem[] => {
|
||||
const pos = input.indexOf("|");
|
||||
input = input.replace("|", "");
|
||||
|
||||
const results = complete(
|
||||
input.slice(0, pos >= 0 ? pos : input.length),
|
||||
testContext,
|
||||
testFunctions
|
||||
);
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
function completionItems(...labels: string[]): CompletionItem[] {
|
||||
return labels.map((label) => ({ label, function: false }));
|
||||
}
|
||||
|
||||
describe("auto-complete", () => {
|
||||
describe("top-level", () => {
|
||||
it("includes built-in functions", () => {
|
||||
const expected: CompletionItem = {
|
||||
label: "toJson",
|
||||
description:
|
||||
"`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,
|
||||
};
|
||||
expect(testComplete("to")).toContainEqual(expected);
|
||||
expect(testComplete("toJs")).toContainEqual(expected);
|
||||
expect(testComplete("1 == toJS")).toContainEqual(expected);
|
||||
expect(testComplete("toJS| == 1")).toContainEqual(expected);
|
||||
});
|
||||
|
||||
it("removes parentheses from passed in function context", () => {
|
||||
expect(testComplete("|")).toContainEqual({
|
||||
label: "hashFiles",
|
||||
function: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("for contexts", () => {
|
||||
it("provides suggestions for env", () => {
|
||||
const expected = completionItems("BAR_TEST", "FOO");
|
||||
expect(testComplete("env.X")).toEqual(expected);
|
||||
expect(testComplete("1 == env.F")).toEqual(expected);
|
||||
expect(testComplete("env.")).toEqual(expected);
|
||||
expect(testComplete("env.FOO")).toEqual(expected);
|
||||
});
|
||||
|
||||
it("provides suggestions for secrets", () => {
|
||||
const expected = completionItems("AWS_TOKEN");
|
||||
|
||||
expect(testComplete("secrets.A")).toEqual(expected);
|
||||
expect(testComplete("1 == secrets.F")).toEqual(expected);
|
||||
expect(testComplete("toJSON(secrets.")).toEqual(expected);
|
||||
});
|
||||
|
||||
it("provides suggestions for contexts in function call", () => {
|
||||
expect(testComplete("toJSON(env.|)")).toEqual(
|
||||
completionItems("BAR_TEST", "FOO")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("trimTokenVector", () => {
|
||||
test.each<{
|
||||
input: string;
|
||||
expected: TokenType[];
|
||||
}>([
|
||||
{
|
||||
input: "env.",
|
||||
expected: [TokenType.IDENTIFIER, TokenType.DOT, TokenType.EOF],
|
||||
},
|
||||
{
|
||||
input: "github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: "1 == github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: "github.mona == github.act",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.DOT,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.EOF,
|
||||
],
|
||||
},
|
||||
{
|
||||
input: "github['test'].",
|
||||
expected: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.LEFT_BRACKET,
|
||||
TokenType.STRING,
|
||||
TokenType.RIGHT_BRACKET,
|
||||
TokenType.DOT,
|
||||
TokenType.EOF,
|
||||
],
|
||||
},
|
||||
])("$input", ({ input, expected }) => {
|
||||
const l = new Lexer(input);
|
||||
const lr = l.lex();
|
||||
|
||||
const tv = trimTokenVector(lr.tokens);
|
||||
expect(tv.map((x) => x.type)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Dictionary, isDictionary } from "./data/dictionary";
|
||||
import { ExpressionData } from "./data/expressiondata";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { wellKnownFunctions } from "./funcs";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Lexer, Token, TokenType } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
|
||||
export type CompletionItem = {
|
||||
label: string;
|
||||
description?: string;
|
||||
function: boolean;
|
||||
};
|
||||
|
||||
// Complete returns a list of completion items for the given expression.
|
||||
//
|
||||
// The main functionality is auto-completing functions and context access:
|
||||
// We can only provide assistance if the input is in one of the following forms (with | denoting the cursor position):
|
||||
// - context.path.inp| or context.path['inp| -- auto-complete context access
|
||||
// - context.path.| or context.path['| -- auto-complete context access
|
||||
// - toJS| -- auto-complete function call or top-level
|
||||
// - | -- auto-complete function call or top-level context access
|
||||
export function complete(
|
||||
input: string,
|
||||
context: Dictionary,
|
||||
extensionFunctions: FunctionInfo[]
|
||||
): CompletionItem[] {
|
||||
// Lex
|
||||
const lexer = new Lexer(input);
|
||||
const lexResult = lexer.lex();
|
||||
|
||||
// Find interesting part of the tokenVector. For example, for an expression like `github.actor == env.actor.log|`, we are
|
||||
// only interested in the `env.actor.log` part for auto-completion
|
||||
const tokenInputVector = trimTokenVector(lexResult.tokens);
|
||||
|
||||
// Start by skipping the EOF token
|
||||
let tokenIdx = tokenInputVector.length - 2;
|
||||
|
||||
if (tokenIdx >= 0) {
|
||||
switch (tokenInputVector[tokenIdx].type) {
|
||||
// If there is a (partial) identifier under the cursor, ignore that
|
||||
case TokenType.IDENTIFIER:
|
||||
tokenIdx--;
|
||||
break;
|
||||
|
||||
case TokenType.STRING:
|
||||
// TODO: Support string for `context.name['test|`
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (tokenIdx < 0) {
|
||||
// Vector only contains the EOF token. Suggest functions and root context access
|
||||
const result = contextKeys(context);
|
||||
|
||||
// Merge with functions
|
||||
result.push(...functionItems(extensionFunctions));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Determine path that led to the last token
|
||||
// Use parser & evaluator to determine context to complete.
|
||||
const pathTokenVector = tokenInputVector.slice(0, tokenIdx);
|
||||
|
||||
// Include the original EOF token to make the parser happy
|
||||
pathTokenVector.push(tokenInputVector[tokenInputVector.length - 1]);
|
||||
|
||||
const p = new Parser(
|
||||
pathTokenVector,
|
||||
context.pairs().map((x) => x.key),
|
||||
extensionFunctions
|
||||
);
|
||||
const expr = p.parse();
|
||||
|
||||
const ev = new Evaluator(expr, context);
|
||||
const result = ev.evaluate();
|
||||
|
||||
return contextKeys(result);
|
||||
}
|
||||
|
||||
function functionItems(extensionFunctions: FunctionInfo[]): CompletionItem[] {
|
||||
const result: CompletionItem[] = [];
|
||||
|
||||
for (const fdef of [
|
||||
...Object.values(wellKnownFunctions),
|
||||
...extensionFunctions,
|
||||
]) {
|
||||
result.push({
|
||||
label: fdef.name,
|
||||
description: fdef.description,
|
||||
function: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Sort functions
|
||||
result.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function contextKeys(context: ExpressionData): CompletionItem[] {
|
||||
if (isDictionary(context)) {
|
||||
return (
|
||||
context
|
||||
.pairs()
|
||||
.map((x) => completionItemFromContext(x.key))
|
||||
// Sort contexts
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function completionItemFromContext(context: string): CompletionItem {
|
||||
const parenIndex = context.indexOf("(");
|
||||
const isFunc = parenIndex >= 0 && context.indexOf(")") >= 0;
|
||||
|
||||
return {
|
||||
label: isFunc ? context.substring(0, parenIndex) : context,
|
||||
function: isFunc,
|
||||
};
|
||||
}
|
||||
|
||||
export function trimTokenVector(tokenVector: Token[]): Token[] {
|
||||
let tokenIdx = tokenVector.length;
|
||||
|
||||
while (tokenIdx > 0) {
|
||||
const token = tokenVector[tokenIdx - 1];
|
||||
switch (token.type) {
|
||||
case TokenType.IDENTIFIER:
|
||||
case TokenType.DOT:
|
||||
case TokenType.EOF:
|
||||
case TokenType.LEFT_BRACKET:
|
||||
case TokenType.RIGHT_BRACKET:
|
||||
case TokenType.STRING:
|
||||
tokenIdx--;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Only keep the part of the token vector we're interested in
|
||||
return tokenVector.slice(tokenIdx);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Array implements ExpressionDataInterface {
|
||||
private v: ExpressionData[] = [];
|
||||
|
||||
constructor(...data: ExpressionData[]) {
|
||||
for (const d of data) {
|
||||
this.add(d);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly kind = Kind.Array;
|
||||
|
||||
public primitive = false;
|
||||
|
||||
coerceString(): string {
|
||||
return kindStr(this.kind);
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
add(value: ExpressionData) {
|
||||
this.v.push(value);
|
||||
}
|
||||
|
||||
get(index: number): ExpressionData {
|
||||
return this.v[index];
|
||||
}
|
||||
|
||||
values(): ExpressionData[] {
|
||||
return this.v;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class BooleanData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: boolean) {}
|
||||
|
||||
public readonly kind = Kind.Boolean;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
if (this.value) {
|
||||
return "true";
|
||||
}
|
||||
|
||||
return "false";
|
||||
}
|
||||
|
||||
number(): number {
|
||||
if (this.value) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("dictionary", () => {
|
||||
it("pairs contains all values", () => {
|
||||
const d = new Dictionary();
|
||||
d.add("ABC", new StringData("val"));
|
||||
|
||||
expect(d.pairs()).toEqual([{ key: "ABC", value: new StringData("val") }]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
kindStr,
|
||||
Pair,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Dictionary implements ExpressionDataInterface {
|
||||
private keys: string[] = [];
|
||||
private v: ExpressionData[] = [];
|
||||
private indexMap: { [key: string]: number } = {};
|
||||
|
||||
constructor(...pairs: Pair[]) {
|
||||
for (const p of pairs) {
|
||||
this.add(p.key, p.value);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly kind = Kind.Dictionary;
|
||||
|
||||
public primitive = false;
|
||||
|
||||
coerceString(): string {
|
||||
return kindStr(this.kind);
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
add(key: string, value: ExpressionData) {
|
||||
if (this.indexMap[key.toLowerCase()]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.keys.push(key);
|
||||
this.v.push(value);
|
||||
this.indexMap[key.toLowerCase()] = this.v.length - 1;
|
||||
}
|
||||
|
||||
get(key: string): ExpressionData | undefined {
|
||||
const index = this.indexMap[key.toLowerCase()];
|
||||
if (index === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.v[index];
|
||||
}
|
||||
|
||||
values(): ExpressionData[] {
|
||||
return this.v;
|
||||
}
|
||||
|
||||
pairs(): Pair[] {
|
||||
const result: Pair[] = [];
|
||||
|
||||
for (const key of this.keys) {
|
||||
result.push({ key, value: this.v[this.indexMap[key.toLowerCase()]] });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export function isDictionary(x: ExpressionData): x is Dictionary {
|
||||
return x.kind === Kind.Dictionary;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { Array } from "./array";
|
||||
import { StringData } from "./string";
|
||||
import { NumberData } from "./number";
|
||||
import { BooleanData } from "./boolean";
|
||||
|
||||
export enum Kind {
|
||||
String = 0,
|
||||
Array,
|
||||
Dictionary,
|
||||
Boolean,
|
||||
Number,
|
||||
CaseSensitiveDictionary,
|
||||
Null,
|
||||
}
|
||||
|
||||
export function kindStr(k: Kind): string {
|
||||
switch (k) {
|
||||
case Kind.Array:
|
||||
return "Array";
|
||||
case Kind.Boolean:
|
||||
return "Boolean";
|
||||
case Kind.Null:
|
||||
return "Null";
|
||||
case Kind.Number:
|
||||
return "Number";
|
||||
case Kind.Dictionary:
|
||||
return "Object";
|
||||
case Kind.String:
|
||||
return "String";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
export interface ExpressionDataInterface {
|
||||
kind: Kind;
|
||||
primitive: boolean;
|
||||
|
||||
coerceString(): string;
|
||||
|
||||
number(): number;
|
||||
}
|
||||
|
||||
export type ExpressionData =
|
||||
| Array
|
||||
| Dictionary
|
||||
| StringData
|
||||
| BooleanData
|
||||
| NumberData
|
||||
| Null;
|
||||
|
||||
export type Pair = {
|
||||
key: string;
|
||||
value: ExpressionData;
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export { Array } from "./array";
|
||||
export { BooleanData } from "./boolean";
|
||||
export { Dictionary } from "./dictionary";
|
||||
export { ExpressionData, Kind } from "./expressiondata";
|
||||
export { Null } from "./null";
|
||||
export { NumberData } from "./number";
|
||||
export { replacer } from "./replacer";
|
||||
export { reviver } from "./reviver";
|
||||
export { StringData } from "./string";
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
ExpressionData,
|
||||
ExpressionDataInterface,
|
||||
Kind,
|
||||
} from "./expressiondata";
|
||||
|
||||
export class Null implements ExpressionDataInterface {
|
||||
constructor() {}
|
||||
|
||||
public readonly kind = Kind.Null;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class NumberData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: number) {}
|
||||
|
||||
public readonly kind = Kind.Number;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
if (this.value === -0) {
|
||||
return "0";
|
||||
}
|
||||
|
||||
// Workaround to limit the precision to at most 15 digits. Format the number to a string, then parse
|
||||
// it back to a number to remove trailing zeroes to prevent numbers to be converted to 1.200000000...
|
||||
return (+this.value.toFixed(15)).toString();
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Array } from "./array";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { replacer } from "./replacer";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("replacer", () => {
|
||||
it("null", () => {
|
||||
expect(JSON.stringify(new Null(), replacer, " ")).toEqual("null");
|
||||
});
|
||||
|
||||
it("array", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Array(new StringData("a"), new StringData("b")),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('[\n "a",\n "b"\n]');
|
||||
});
|
||||
|
||||
it("dictionary", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
new Dictionary({ key: "a", value: new NumberData(42) }),
|
||||
replacer,
|
||||
" "
|
||||
)
|
||||
).toEqual('{\n "a": 42\n}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
|
||||
/**
|
||||
* Replacer can be passed to JSON.stringify to convert an ExpressionData object into plain JSON
|
||||
*
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer
|
||||
*/
|
||||
export function replacer(key: string, value: any): any {
|
||||
if (value instanceof Null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof BooleanData) {
|
||||
return value.value;
|
||||
}
|
||||
|
||||
if (value instanceof NumberData) {
|
||||
return value.number();
|
||||
}
|
||||
|
||||
if (value instanceof StringData) {
|
||||
return value.coerceString();
|
||||
}
|
||||
|
||||
if (value instanceof Array) {
|
||||
return value.values();
|
||||
}
|
||||
|
||||
if (value instanceof Dictionary) {
|
||||
const pairs = value.pairs();
|
||||
|
||||
const r: any = {};
|
||||
for (const p of pairs) {
|
||||
r[p.key] = p.value;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Array } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { reviver } from "./reviver";
|
||||
import { StringData } from "./string";
|
||||
|
||||
describe("reviver", () => {
|
||||
const tests: {
|
||||
name: string;
|
||||
data: string;
|
||||
want: ExpressionData;
|
||||
}[] = [
|
||||
{
|
||||
name: "null",
|
||||
data: "null",
|
||||
want: new Null(),
|
||||
},
|
||||
{
|
||||
name: "number",
|
||||
data: "1",
|
||||
want: new NumberData(1),
|
||||
},
|
||||
{
|
||||
name: "string",
|
||||
data: `"a"`,
|
||||
want: new StringData("a"),
|
||||
},
|
||||
{
|
||||
name: "true boolean",
|
||||
data: "true",
|
||||
want: new BooleanData(true),
|
||||
},
|
||||
{
|
||||
name: "false boolean",
|
||||
data: "false",
|
||||
want: new BooleanData(false),
|
||||
},
|
||||
{
|
||||
name: "array",
|
||||
data: `[1,2,3]`,
|
||||
want: new Array(new NumberData(1), new NumberData(2), new NumberData(3)),
|
||||
},
|
||||
{
|
||||
name: "nested array",
|
||||
data: `[1,2,[3,4],5]`,
|
||||
want: new Array(
|
||||
new NumberData(1),
|
||||
new NumberData(2),
|
||||
new Array(new NumberData(3), new NumberData(4)),
|
||||
new NumberData(5)
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "complex array",
|
||||
data: `[{"a":[true,2]},{"b":"three"},{"c":null}]`,
|
||||
want: new Array(
|
||||
new Dictionary({
|
||||
key: "a",
|
||||
value: new Array(new BooleanData(true), new NumberData(2)),
|
||||
}),
|
||||
new Dictionary({ key: "b", value: new StringData("three") }),
|
||||
new Dictionary({ key: "c", value: new Null() })
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "dictionary",
|
||||
data: `{ "object1": {} }`,
|
||||
want: new Dictionary({
|
||||
key: "object1",
|
||||
value: new Dictionary(),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$name",
|
||||
({ data, want }: { data: string; want: ExpressionData }) => {
|
||||
expect(JSON.parse(data, reviver)).toEqual(want);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Array as dArray } from "./array";
|
||||
import { BooleanData } from "./boolean";
|
||||
import { Dictionary } from "./dictionary";
|
||||
import { ExpressionData, Pair } from "./expressiondata";
|
||||
import { Null } from "./null";
|
||||
import { NumberData } from "./number";
|
||||
import { StringData } from "./string";
|
||||
|
||||
/**
|
||||
* Reviver can be passed to `JSON.parse` to convert plain JSON into an `ExpressionData` object.
|
||||
*
|
||||
* See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse#reviver
|
||||
*/
|
||||
export function reviver(key: string, val: any): ExpressionData {
|
||||
if (val === null) {
|
||||
return new Null();
|
||||
}
|
||||
|
||||
if (typeof val === "string") {
|
||||
return new StringData(val);
|
||||
}
|
||||
|
||||
if (typeof val === "number") {
|
||||
return new NumberData(val);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return new BooleanData(val as boolean);
|
||||
}
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
return new dArray(...val);
|
||||
}
|
||||
|
||||
if (typeof val === "object") {
|
||||
return new Dictionary(
|
||||
...Object.keys(val).map(
|
||||
(k) =>
|
||||
({
|
||||
key: k,
|
||||
value: val[k],
|
||||
} as Pair)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Pass through value
|
||||
return val;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ExpressionDataInterface, Kind } from "./expressiondata";
|
||||
|
||||
export class StringData implements ExpressionDataInterface {
|
||||
constructor(public readonly value: string) {}
|
||||
|
||||
public readonly kind = Kind.String;
|
||||
|
||||
public primitive = true;
|
||||
|
||||
coerceString(): string {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
number(): number {
|
||||
return Number(this.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Pos, Token, tokenString } from "./lexer";
|
||||
|
||||
export const MAX_PARSER_DEPTH = 50;
|
||||
export const MAX_EXPRESSION_LENGTH = 21000;
|
||||
|
||||
export enum ErrorType {
|
||||
ErrorUnexpectedSymbol,
|
||||
ErrorUnrecognizedNamedValue,
|
||||
ErrorUnexpectedEndOfExpression,
|
||||
|
||||
ErrorExceededMaxDepth,
|
||||
ErrorExceededMaxLength,
|
||||
ErrorTooFewParameters,
|
||||
ErrorTooManyParameters,
|
||||
ErrorUnrecognizedContext,
|
||||
ErrorUnrecognizedFunction,
|
||||
}
|
||||
|
||||
export class ExpressionError extends Error {
|
||||
constructor(private typ: ErrorType, private tok: Token) {
|
||||
super(`${errorDescription(typ)}: '${tokenString(tok)}'`);
|
||||
|
||||
this.pos = this.tok.pos;
|
||||
}
|
||||
|
||||
public pos: Pos;
|
||||
}
|
||||
|
||||
function errorDescription(typ: ErrorType): string {
|
||||
switch (typ) {
|
||||
case ErrorType.ErrorUnexpectedEndOfExpression:
|
||||
return "Unexpected end of expression";
|
||||
case ErrorType.ErrorUnexpectedSymbol:
|
||||
return "Unexpected symbol";
|
||||
case ErrorType.ErrorUnrecognizedNamedValue:
|
||||
return "Unrecognized named-value";
|
||||
case ErrorType.ErrorExceededMaxDepth:
|
||||
return `Exceeded max expression depth ${MAX_PARSER_DEPTH}`;
|
||||
case ErrorType.ErrorExceededMaxLength:
|
||||
return `Exceeded max expression length ${MAX_EXPRESSION_LENGTH}`;
|
||||
case ErrorType.ErrorTooFewParameters:
|
||||
return "Too few parameters supplied";
|
||||
case ErrorType.ErrorTooManyParameters:
|
||||
return "Too many parameters supplied";
|
||||
case ErrorType.ErrorUnrecognizedContext:
|
||||
return "Unrecognized named-value";
|
||||
case ErrorType.ErrorUnrecognizedFunction:
|
||||
return "Unrecognized function";
|
||||
default: // Should never reach here.
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as data from "./data";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { Lexer } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
|
||||
test("evaluator", () => {
|
||||
const input = "foo['']";
|
||||
|
||||
const lexer = new Lexer(input);
|
||||
const result = lexer.lex();
|
||||
|
||||
// Parse
|
||||
const parser = new Parser(result.tokens, ["foo"], []);
|
||||
const expr = parser.parse();
|
||||
|
||||
// Evaluate expression
|
||||
const evaluator = new Evaluator(
|
||||
expr,
|
||||
new data.Dictionary({
|
||||
key: "foo",
|
||||
value: new data.Dictionary({ key: "bar", value: new data.NumberData(42) }),
|
||||
})
|
||||
);
|
||||
const eresult = evaluator.evaluate();
|
||||
|
||||
expect(eresult.kind).toBe(data.Kind.Null);
|
||||
});
|
||||
@@ -0,0 +1,245 @@
|
||||
import {
|
||||
Binary,
|
||||
ContextAccess,
|
||||
Expr,
|
||||
ExprVisitor,
|
||||
FunctionCall,
|
||||
Grouping,
|
||||
IndexAccess,
|
||||
Literal,
|
||||
Logical,
|
||||
Star,
|
||||
Unary,
|
||||
} from "./ast";
|
||||
import * as data from "./data";
|
||||
import { FilteredArray } from "./filtered_array";
|
||||
import { wellKnownFunctions } from "./funcs";
|
||||
import { idxHelper } from "./idxHelper";
|
||||
import { TokenType } from "./lexer";
|
||||
import { equals, falsy, greaterThan, lessThan, truthy } from "./result";
|
||||
|
||||
export class EvaluationError extends Error {}
|
||||
|
||||
export class Evaluator implements ExprVisitor<data.ExpressionData> {
|
||||
constructor(private n: Expr, private context: data.Dictionary) {}
|
||||
|
||||
public evaluate(): data.ExpressionData {
|
||||
return this.eval(this.n);
|
||||
}
|
||||
|
||||
private eval(n: Expr): data.ExpressionData {
|
||||
return n.accept(this);
|
||||
}
|
||||
|
||||
visitLiteral(literal: Literal): data.ExpressionData {
|
||||
return literal.literal;
|
||||
}
|
||||
|
||||
visitUnary(unary: Unary): data.ExpressionData {
|
||||
const r = this.eval(unary.expr);
|
||||
|
||||
if (unary.operator.type === TokenType.BANG) {
|
||||
return new data.BooleanData(falsy(r));
|
||||
}
|
||||
|
||||
throw new Error(`unknown unary operator: ${unary.operator.lexeme}`);
|
||||
}
|
||||
|
||||
visitBinary(binary: Binary): data.ExpressionData {
|
||||
const left = this.eval(binary.left);
|
||||
const right = this.eval(binary.right);
|
||||
|
||||
switch (binary.operator.type) {
|
||||
case TokenType.EQUAL_EQUAL:
|
||||
return new data.BooleanData(equals(left, right));
|
||||
|
||||
case TokenType.BANG_EQUAL:
|
||||
return new data.BooleanData(!equals(left, right));
|
||||
|
||||
case TokenType.GREATER:
|
||||
return new data.BooleanData(greaterThan(left, right));
|
||||
|
||||
case TokenType.GREATER_EQUAL:
|
||||
return new data.BooleanData(
|
||||
equals(left, right) || greaterThan(left, right)
|
||||
);
|
||||
|
||||
case TokenType.LESS:
|
||||
return new data.BooleanData(lessThan(left, right));
|
||||
|
||||
case TokenType.LESS_EQUAL:
|
||||
return new data.BooleanData(equals(left, right) || lessThan(left, right));
|
||||
}
|
||||
|
||||
throw new Error(`unknown binary operator: ${binary.operator.lexeme}`);
|
||||
}
|
||||
|
||||
visitLogical(logical: Logical): data.ExpressionData {
|
||||
let result: data.ExpressionData;
|
||||
|
||||
for (const arg of logical.args) {
|
||||
result = this.eval(arg);
|
||||
|
||||
// Break?
|
||||
if (
|
||||
(logical.operator.type === TokenType.AND && falsy(result)) ||
|
||||
(logical.operator.type === TokenType.OR && truthy(result))
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// result is always assigned before we return here
|
||||
return result!;
|
||||
}
|
||||
|
||||
visitGrouping(grouping: Grouping): data.ExpressionData {
|
||||
return this.eval(grouping.group);
|
||||
}
|
||||
|
||||
visitContextAccess(contextAccess: ContextAccess): data.ExpressionData {
|
||||
const r = this.context.get(contextAccess.name.lexeme)!;
|
||||
return r;
|
||||
}
|
||||
|
||||
visitIndexAccess(ia: IndexAccess): data.ExpressionData {
|
||||
let idx: idxHelper;
|
||||
if (ia.index instanceof Star) {
|
||||
idx = new idxHelper(true, undefined);
|
||||
} else {
|
||||
let idxResult: data.ExpressionData;
|
||||
try {
|
||||
idxResult = this.eval(ia.index);
|
||||
} catch (e) {
|
||||
throw new Error(`could not evaluate index for index access: ${e}`);
|
||||
}
|
||||
idx = new idxHelper(false, idxResult);
|
||||
}
|
||||
|
||||
const objResult = this.eval(ia.expr);
|
||||
|
||||
let result: data.ExpressionData;
|
||||
switch (objResult.kind) {
|
||||
case data.Kind.Array: {
|
||||
const tobjResult = objResult as data.Array;
|
||||
if (tobjResult instanceof FilteredArray) {
|
||||
result = filteredArrayAccess(tobjResult as FilteredArray, idx);
|
||||
} else {
|
||||
result = arrayAccess(tobjResult, idx);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case data.Kind.Dictionary: {
|
||||
const tobjResult = objResult as data.Dictionary;
|
||||
result = objectAccess(tobjResult, idx);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
if (idx.star) {
|
||||
result = new FilteredArray();
|
||||
} else {
|
||||
result = new data.Null();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
visitFunctionCall(functionCall: FunctionCall): data.ExpressionData {
|
||||
// Evaluate arguments
|
||||
const args = functionCall.args.map((arg) => this.eval(arg));
|
||||
|
||||
return fcall(functionCall, args);
|
||||
}
|
||||
}
|
||||
|
||||
function fcall(
|
||||
fc: FunctionCall,
|
||||
args: data.ExpressionData[]
|
||||
): data.ExpressionData {
|
||||
const f = wellKnownFunctions[fc.functionName.lexeme.toLowerCase()];
|
||||
|
||||
return f.call(...args);
|
||||
}
|
||||
|
||||
function filteredArrayAccess(
|
||||
fa: FilteredArray,
|
||||
idx: idxHelper
|
||||
): data.ExpressionData {
|
||||
const result = new FilteredArray();
|
||||
|
||||
for (const item of fa.values()) {
|
||||
// Check the type of the nested item
|
||||
switch (item.kind) {
|
||||
case data.Kind.Dictionary: {
|
||||
const ti = item as data.Dictionary;
|
||||
if (idx.star) {
|
||||
for (const v of ti.values()) {
|
||||
result.add(v);
|
||||
}
|
||||
} else if (idx.str !== undefined) {
|
||||
const v = ti.get(idx.str);
|
||||
if (v !== undefined) {
|
||||
result.add(v);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case data.Kind.Array: {
|
||||
const ti = item as data.Array;
|
||||
if (idx.star) {
|
||||
for (const v of ti.values()) {
|
||||
result.add(v);
|
||||
}
|
||||
} else if (idx.int !== undefined && idx.int < ti.values().length) {
|
||||
result.add(ti.get(idx.int));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function arrayAccess(a: data.Array, idx: idxHelper): data.ExpressionData {
|
||||
if (idx.star) {
|
||||
const fa = new FilteredArray();
|
||||
for (const item of a.values()) {
|
||||
fa.add(item);
|
||||
}
|
||||
|
||||
return fa;
|
||||
}
|
||||
|
||||
if (idx.int !== undefined && idx.int < a.values().length) {
|
||||
return a.get(idx.int);
|
||||
}
|
||||
|
||||
return new data.Null();
|
||||
}
|
||||
|
||||
function objectAccess(
|
||||
obj: data.Dictionary,
|
||||
idx: idxHelper
|
||||
): data.ExpressionData {
|
||||
if (idx.star) {
|
||||
const fa = new FilteredArray(...obj.values());
|
||||
return fa;
|
||||
}
|
||||
|
||||
if (idx.str !== undefined) {
|
||||
const r = obj.get(idx.str);
|
||||
if (r !== undefined) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return new data.Null();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import * as data from "./data";
|
||||
|
||||
export class FilteredArray extends data.Array {}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ErrorType, ExpressionError } from "./errors";
|
||||
import { contains } from "./funcs/contains";
|
||||
import { endswith } from "./funcs/endswith";
|
||||
import { format } from "./funcs/format";
|
||||
import { fromjson } from "./funcs/fromjson";
|
||||
import { FunctionDefinition, FunctionInfo } from "./funcs/info";
|
||||
import { join } from "./funcs/join";
|
||||
import { startswith } from "./funcs/startswith";
|
||||
import { tojson } from "./funcs/tojson";
|
||||
import { Token } from "./lexer";
|
||||
|
||||
export type ParseContext = {
|
||||
allowUnknownKeywords: boolean;
|
||||
extensionContexts: Map<string, boolean>;
|
||||
extensionFunctions: Map<string, FunctionInfo>;
|
||||
};
|
||||
|
||||
export const wellKnownFunctions: { [name: string]: FunctionDefinition } = {
|
||||
contains: contains,
|
||||
endswith: endswith,
|
||||
format: format,
|
||||
fromjson: fromjson,
|
||||
join: join,
|
||||
startswith: startswith,
|
||||
tojson: tojson,
|
||||
};
|
||||
|
||||
// validateFunction returns the function definition for the given function name.
|
||||
// If the function does not exist or an incorrect number of arguments is provided,
|
||||
// an error is returned.
|
||||
export function validateFunction(
|
||||
context: ParseContext,
|
||||
identifier: Token,
|
||||
argCount: number
|
||||
) {
|
||||
// Expression function names are case-insensitive.
|
||||
const name = identifier.lexeme.toLowerCase();
|
||||
|
||||
let f: FunctionInfo | undefined;
|
||||
f = wellKnownFunctions[name];
|
||||
if (!f) {
|
||||
f = context.extensionFunctions.get(name);
|
||||
if (!f) {
|
||||
if (!context.allowUnknownKeywords) {
|
||||
throw new ExpressionError(
|
||||
ErrorType.ErrorUnrecognizedFunction,
|
||||
identifier
|
||||
);
|
||||
}
|
||||
|
||||
// Skip argument validation for unknown functions
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (argCount < f.minArgs) {
|
||||
throw new ExpressionError(ErrorType.ErrorTooFewParameters, identifier);
|
||||
}
|
||||
|
||||
if (argCount > f.maxArgs) {
|
||||
throw new ExpressionError(ErrorType.ErrorTooManyParameters, identifier);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Array, BooleanData, ExpressionData, Kind } from "../data";
|
||||
import { equals } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const contains: FunctionDefinition = {
|
||||
name: "contains",
|
||||
description:
|
||||
"`contains( search, item )`\n\nReturns `true` if `search` contains `item`. If `search` is an array, this function returns `true` if the `item` is an element in the array. If `search` is a string, this function returns `true` if the `item` is a substring of `search`. This function is not case sensitive. Casts values to a string.",
|
||||
minArgs: 2,
|
||||
maxArgs: 2,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
const left = args[0];
|
||||
const right = args[1];
|
||||
|
||||
if (left.primitive) {
|
||||
const ls = left.coerceString();
|
||||
if (right.primitive) {
|
||||
const rs = right.coerceString();
|
||||
return new BooleanData(ls.toLowerCase().includes(rs.toLowerCase()));
|
||||
}
|
||||
} else if (left.kind === Kind.Array) {
|
||||
const la = left as Array;
|
||||
if (la.values().length === 0) {
|
||||
return new BooleanData(false);
|
||||
}
|
||||
|
||||
for (const v of la.values()) {
|
||||
if (equals(right, v)) {
|
||||
return new BooleanData(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new BooleanData(false);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BooleanData, ExpressionData } from "../data";
|
||||
import { toUpperSpecial } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const endswith: FunctionDefinition = {
|
||||
name: "endsWith",
|
||||
description:
|
||||
"`endsWith( searchString, searchValue )`\n\nReturns `true` if `searchString` ends with `searchValue`. This function is not case sensitive. Casts values to a string.",
|
||||
minArgs: 2,
|
||||
maxArgs: 2,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
const left = args[0];
|
||||
if (!left.primitive) {
|
||||
return new BooleanData(false);
|
||||
}
|
||||
|
||||
const right = args[1];
|
||||
if (!right.primitive) {
|
||||
return new BooleanData(false);
|
||||
}
|
||||
|
||||
const ls = toUpperSpecial(left.coerceString());
|
||||
const rs = toUpperSpecial(right.coerceString());
|
||||
|
||||
return new BooleanData(ls.endsWith(rs));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Null, NumberData, StringData } from "../data";
|
||||
import { format } from "./format";
|
||||
|
||||
describe("format", () => {
|
||||
it("null", () => {
|
||||
expect(format.call(new StringData("{0}"), new Null())).toEqual(new StringData(""));
|
||||
});
|
||||
it("number", () => {
|
||||
expect(format.call(new StringData("{0}"), new NumberData(42))).toEqual(
|
||||
new StringData("42")
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { ExpressionData, StringData } from "../data";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const format: FunctionDefinition = {
|
||||
name: "format",
|
||||
description:
|
||||
"`format( string, replaceValue0, replaceValue1, ..., replaceValueN)`\n\nReplaces values in the `string`, with the variable `replaceValueN`. Variables in the `string` are specified using the `{N}` syntax, where `N` is an integer. You must specify at least one `replaceValue` and `string`. There is no maximum for the number of variables (`replaceValueN`) you can use. Escape curly braces using double braces.",
|
||||
minArgs: 1,
|
||||
maxArgs: 255 /*MAX_ARGUMENTS*/,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
const fs = args[0].coerceString();
|
||||
|
||||
const result: string[] = [];
|
||||
let index = 0;
|
||||
|
||||
while (index < fs.length) {
|
||||
const lbrace = fs.indexOf("{", index);
|
||||
let rbrace = fs.indexOf("}", index);
|
||||
|
||||
// Left brace
|
||||
if (lbrace >= 0 && (rbrace < 0 || rbrace > lbrace)) {
|
||||
// Escaped left brace
|
||||
if (safeCharAt(fs, lbrace + 1) === "{") {
|
||||
result.push(fs.substr(index, lbrace - index + 1));
|
||||
index = lbrace + 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Left brace, number, optional format specifiers, right brace
|
||||
if (rbrace > lbrace + 1) {
|
||||
const argIndex = readArgIndex(fs, lbrace + 1);
|
||||
if (argIndex.success) {
|
||||
// Check parameter count
|
||||
if (1 + argIndex.result > args.length - 1) {
|
||||
throw new Error(
|
||||
`The following format string references more arguments than were supplied: ${fs}`
|
||||
);
|
||||
}
|
||||
|
||||
// Append the portion before the left brace
|
||||
if (lbrace > index) {
|
||||
result.push(fs.substr(index, lbrace - index));
|
||||
}
|
||||
|
||||
// Append the arg
|
||||
result.push(`${args[1 + argIndex.result].coerceString()}`);
|
||||
index = rbrace + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`The following format string is invalid: ${fs}`);
|
||||
}
|
||||
// Right brace
|
||||
else if (rbrace >= 0) {
|
||||
// Escaped right brace
|
||||
if (safeCharAt(fs, rbrace + 1) === "}") {
|
||||
result.push(fs.substr(index, rbrace - index + 1));
|
||||
index = rbrace + 2;
|
||||
} else {
|
||||
throw new Error(`The following format string is invalid: ${fs}`);
|
||||
}
|
||||
}
|
||||
// Last segment
|
||||
else {
|
||||
result.push(fs.substr(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new StringData(result.join(""));
|
||||
},
|
||||
};
|
||||
|
||||
function safeCharAt(string: string, index: number): string {
|
||||
if (string.length > index) {
|
||||
return string[index];
|
||||
}
|
||||
|
||||
return "\0";
|
||||
}
|
||||
|
||||
function readArgIndex(string: string, startIndex: number): ArgIndex {
|
||||
// Count the number of digits
|
||||
let length = 0;
|
||||
while (true) {
|
||||
const nextChar = safeCharAt(string, startIndex + length);
|
||||
if (nextChar >= "0" && nextChar <= "9") {
|
||||
length++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate at least one digit
|
||||
if (length < 1) {
|
||||
return <ArgIndex>{
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the number
|
||||
const endIndex = startIndex + length - 1;
|
||||
const result = parseInt(string.substr(startIndex, length));
|
||||
return <ArgIndex>{
|
||||
success: !isNaN(result),
|
||||
result: result,
|
||||
endIndex: endIndex,
|
||||
};
|
||||
}
|
||||
|
||||
interface ArgIndex {
|
||||
success: boolean;
|
||||
result: number;
|
||||
endIndex: number;
|
||||
}
|
||||
|
||||
interface FormatSpecifiers {
|
||||
success: boolean;
|
||||
result: string;
|
||||
rbrace: number;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { ExpressionData } from "../data";
|
||||
import { reviver } from "../data/reviver";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const fromjson: FunctionDefinition = {
|
||||
name: "fromJson",
|
||||
description:
|
||||
"`fromJSON(value)`\n\nReturns a JSON object or JSON data type for `value`. You can use this function to provide a JSON object as an evaluated expression or to convert environment variables from a string.",
|
||||
minArgs: 1,
|
||||
maxArgs: 1,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
const input = args[0];
|
||||
const is = input.coerceString();
|
||||
|
||||
if (is.trim() === "") {
|
||||
throw new Error("empty input");
|
||||
}
|
||||
|
||||
const d = JSON.parse(is, reviver);
|
||||
return d;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ExpressionData } from "../data";
|
||||
|
||||
export interface FunctionInfo {
|
||||
name: string;
|
||||
|
||||
description?: string;
|
||||
|
||||
minArgs: number;
|
||||
maxArgs: number;
|
||||
}
|
||||
|
||||
export interface FunctionDefinition extends FunctionInfo {
|
||||
call: (...args: ExpressionData[]) => ExpressionData;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Array, ExpressionData, Kind, StringData } from "../data";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const join: FunctionDefinition = {
|
||||
name: "join",
|
||||
description:
|
||||
"`join( array, optionalSeparator )`\n\nThe value for `array` can be an array or a string. All values in `array` are concatenated into a string. If you provide `optionalSeparator`, it is inserted between the concatenated values. Otherwise, the default separator `,` is used. Casts values to a string.",
|
||||
minArgs: 1,
|
||||
maxArgs: 2,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
// Primitive
|
||||
if (args[0].primitive) {
|
||||
return new StringData(args[0].coerceString());
|
||||
}
|
||||
|
||||
// Array
|
||||
if (args[0].kind === Kind.Array) {
|
||||
// Separator
|
||||
let separator = ",";
|
||||
if (args.length > 1 && args[1].primitive) {
|
||||
separator = args[1].coerceString();
|
||||
}
|
||||
|
||||
// Convert items to strings
|
||||
return new StringData(
|
||||
(args[0] as Array)
|
||||
.values()
|
||||
.map((item) => item.coerceString())
|
||||
.join(separator)
|
||||
);
|
||||
}
|
||||
|
||||
return new StringData("");
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BooleanData, ExpressionData } from "../data";
|
||||
import { toUpperSpecial } from "../result";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const startswith: FunctionDefinition = {
|
||||
name: "startsWith",
|
||||
description:
|
||||
"`startsWith( searchString, searchValue )`\n\nReturns `true` when `searchString` starts with `searchValue`. This function is not case sensitive. Casts values to a string.",
|
||||
minArgs: 2,
|
||||
maxArgs: 2,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
const left = args[0];
|
||||
if (!left.primitive) {
|
||||
return new BooleanData(false);
|
||||
}
|
||||
|
||||
const right = args[1];
|
||||
if (!right.primitive) {
|
||||
return new BooleanData(false);
|
||||
}
|
||||
|
||||
const ls = toUpperSpecial(left.coerceString());
|
||||
const rs = toUpperSpecial(right.coerceString());
|
||||
|
||||
return new BooleanData(ls.startsWith(rs));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ExpressionData, StringData } from "../data";
|
||||
import { replacer } from "../data/replacer";
|
||||
import { FunctionDefinition } from "./info";
|
||||
|
||||
export const tojson: FunctionDefinition = {
|
||||
name: "toJson",
|
||||
description:
|
||||
"`toJSON(value)`\n\nReturns a pretty-print JSON representation of `value`. You can use this function to debug the information provided in contexts.",
|
||||
minArgs: 1,
|
||||
maxArgs: 1,
|
||||
call: (...args: ExpressionData[]): ExpressionData => {
|
||||
return new StringData(JSON.stringify(args[0], replacer, " "));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ExpressionData } from "./data";
|
||||
|
||||
export class idxHelper {
|
||||
public readonly str: string | undefined;
|
||||
public readonly int: number | undefined;
|
||||
|
||||
constructor(public readonly star: boolean, idx: ExpressionData | undefined) {
|
||||
if (!star) {
|
||||
if (idx!.primitive) {
|
||||
this.str = idx!.coerceString();
|
||||
}
|
||||
|
||||
let f = idx!.number();
|
||||
if (!isNaN(f) && isFinite(f) && f >= 0) {
|
||||
f = Math.floor(f);
|
||||
this.int = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { complete } from "./completion";
|
||||
export * as data from "./data";
|
||||
export { ExpressionError } from "./errors";
|
||||
export { Evaluator } from "./evaluator";
|
||||
export { Lexer } from "./lexer";
|
||||
export { Parser } from "./parser";
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Lexer, Token, TokenType } from "./lexer";
|
||||
|
||||
describe("lexer", () => {
|
||||
const tests: {
|
||||
input: string;
|
||||
tokenType: TokenType[];
|
||||
token?: Token;
|
||||
}[] = [
|
||||
{ input: "<", tokenType: [TokenType.LESS] },
|
||||
{ input: ">", tokenType: [TokenType.GREATER] },
|
||||
|
||||
{ input: "!=", tokenType: [TokenType.BANG_EQUAL] },
|
||||
{ input: "==", tokenType: [TokenType.EQUAL_EQUAL] },
|
||||
{ input: "<=", tokenType: [TokenType.LESS_EQUAL] },
|
||||
{ input: ">=", tokenType: [TokenType.GREATER_EQUAL] },
|
||||
|
||||
{ input: "&&", tokenType: [TokenType.AND] },
|
||||
{ input: "||", tokenType: [TokenType.OR] },
|
||||
|
||||
// Numbers
|
||||
{ input: "12", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "12.0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "-0", tokenType: [TokenType.NUMBER] },
|
||||
{ input: "-12.0", tokenType: [TokenType.NUMBER] },
|
||||
|
||||
// Strings
|
||||
{ input: "'It''s okay'", tokenType: [TokenType.STRING] },
|
||||
{
|
||||
input: "format('{0} == ''queued''', needs)",
|
||||
tokenType: [
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.LEFT_PAREN,
|
||||
TokenType.STRING,
|
||||
TokenType.COMMA,
|
||||
TokenType.IDENTIFIER,
|
||||
TokenType.RIGHT_PAREN,
|
||||
],
|
||||
},
|
||||
|
||||
// Arrays
|
||||
{
|
||||
input: "[1,2]",
|
||||
tokenType: [
|
||||
TokenType.LEFT_BRACKET,
|
||||
TokenType.NUMBER,
|
||||
TokenType.COMMA,
|
||||
TokenType.NUMBER,
|
||||
TokenType.RIGHT_BRACKET,
|
||||
],
|
||||
},
|
||||
|
||||
// Simple expressions
|
||||
{
|
||||
input: "1 == 2",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER],
|
||||
},
|
||||
{
|
||||
input: "1== 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.EQUAL_EQUAL, TokenType.NUMBER],
|
||||
},
|
||||
{
|
||||
input: "1< 1",
|
||||
tokenType: [TokenType.NUMBER, TokenType.LESS, TokenType.NUMBER],
|
||||
},
|
||||
|
||||
// Identifiers
|
||||
{
|
||||
input: "github",
|
||||
tokenType: [TokenType.IDENTIFIER],
|
||||
token: {
|
||||
type: TokenType.IDENTIFIER,
|
||||
lexeme: "github",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Keywords
|
||||
{
|
||||
input: "true",
|
||||
tokenType: [TokenType.TRUE],
|
||||
token: {
|
||||
type: TokenType.TRUE,
|
||||
lexeme: "true",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "false",
|
||||
tokenType: [TokenType.FALSE],
|
||||
token: {
|
||||
type: TokenType.FALSE,
|
||||
lexeme: "false",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
input: "null",
|
||||
tokenType: [TokenType.NULL],
|
||||
token: {
|
||||
type: TokenType.NULL,
|
||||
lexeme: "null",
|
||||
pos: {
|
||||
line: 0,
|
||||
column: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$input",
|
||||
({
|
||||
input,
|
||||
tokenType,
|
||||
token,
|
||||
}: {
|
||||
input: string;
|
||||
tokenType: TokenType[];
|
||||
token?: Token;
|
||||
}) => {
|
||||
const l = new Lexer(input);
|
||||
|
||||
const r = l.lex();
|
||||
|
||||
const want = r.tokens.map((t) => t.type);
|
||||
|
||||
tokenType.push(TokenType.EOF);
|
||||
|
||||
expect(want).toEqual(tokenType);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,425 @@
|
||||
import { StringData } from "./data";
|
||||
import { MAX_EXPRESSION_LENGTH } from "./errors";
|
||||
|
||||
export enum TokenType {
|
||||
UNKNOWN,
|
||||
LEFT_PAREN,
|
||||
RIGHT_PAREN,
|
||||
LEFT_BRACKET,
|
||||
RIGHT_BRACKET,
|
||||
COMMA,
|
||||
DOT,
|
||||
|
||||
BANG,
|
||||
BANG_EQUAL,
|
||||
EQUAL_EQUAL,
|
||||
GREATER,
|
||||
GREATER_EQUAL,
|
||||
LESS,
|
||||
LESS_EQUAL,
|
||||
AND,
|
||||
OR,
|
||||
|
||||
STAR,
|
||||
NUMBER,
|
||||
STRING,
|
||||
IDENTIFIER,
|
||||
TRUE,
|
||||
FALSE,
|
||||
NULL,
|
||||
|
||||
EOF,
|
||||
}
|
||||
|
||||
export type Pos = {
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
|
||||
export type Token = {
|
||||
type: TokenType;
|
||||
|
||||
lexeme: string;
|
||||
value?: string | number | boolean;
|
||||
|
||||
pos: Pos;
|
||||
};
|
||||
|
||||
export function tokenString(tok: Token): string {
|
||||
switch (tok.type) {
|
||||
case TokenType.EOF:
|
||||
return "EOF";
|
||||
case TokenType.NUMBER:
|
||||
return tok.lexeme;
|
||||
case TokenType.STRING:
|
||||
return tok.value!.toString();
|
||||
default:
|
||||
return tok.lexeme;
|
||||
}
|
||||
}
|
||||
|
||||
export type Result = {
|
||||
tokens: Token[];
|
||||
};
|
||||
|
||||
export class Lexer {
|
||||
private start = 0;
|
||||
private offset = 0;
|
||||
|
||||
private line = 0;
|
||||
private lastLineOffset = 0;
|
||||
|
||||
private tokens: Token[] = [];
|
||||
|
||||
constructor(private input: string) {}
|
||||
|
||||
lex(): Result {
|
||||
if (this.input.length > MAX_EXPRESSION_LENGTH) {
|
||||
throw new Error("ErrorExceededMaxLength");
|
||||
}
|
||||
|
||||
while (!this.atEnd()) {
|
||||
this.start = this.offset;
|
||||
const c = this.next();
|
||||
|
||||
switch (c) {
|
||||
case "(":
|
||||
this.addToken(TokenType.LEFT_PAREN);
|
||||
break;
|
||||
case ")":
|
||||
this.addToken(TokenType.RIGHT_PAREN);
|
||||
break;
|
||||
case "[":
|
||||
this.addToken(TokenType.LEFT_BRACKET);
|
||||
break;
|
||||
case "]":
|
||||
this.addToken(TokenType.RIGHT_BRACKET);
|
||||
break;
|
||||
case ",":
|
||||
this.addToken(TokenType.COMMA);
|
||||
break;
|
||||
case ".":
|
||||
if (
|
||||
this.previous() != TokenType.IDENTIFIER &&
|
||||
this.previous() != TokenType.RIGHT_BRACKET &&
|
||||
this.previous() != TokenType.RIGHT_PAREN &&
|
||||
this.previous() != TokenType.STAR
|
||||
|
||||
) {
|
||||
this.consumeNumber();
|
||||
} else {
|
||||
this.addToken(TokenType.DOT);
|
||||
}
|
||||
break;
|
||||
|
||||
case "-":
|
||||
case "+":
|
||||
this.consumeNumber();
|
||||
break;
|
||||
|
||||
case "!":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.BANG_EQUAL : TokenType.BANG
|
||||
);
|
||||
break;
|
||||
|
||||
case "=":
|
||||
if (!this.match("=")) {
|
||||
// Illegal; continue reading until we hit a boundary character and return an error
|
||||
this.consumeIdentifier();
|
||||
break;
|
||||
}
|
||||
|
||||
this.addToken(TokenType.EQUAL_EQUAL);
|
||||
break;
|
||||
|
||||
case "<":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.LESS_EQUAL : TokenType.LESS
|
||||
);
|
||||
break;
|
||||
|
||||
case ">":
|
||||
this.addToken(
|
||||
this.match("=") ? TokenType.GREATER_EQUAL : TokenType.GREATER
|
||||
);
|
||||
break;
|
||||
|
||||
case "&":
|
||||
if (!this.match("&")) {
|
||||
// Illegal; continue reading until we hit a boundary character and return an error
|
||||
this.consumeIdentifier();
|
||||
break;
|
||||
}
|
||||
|
||||
this.addToken(TokenType.AND);
|
||||
break;
|
||||
case "|":
|
||||
if (!this.match("|")) {
|
||||
// Illegal; continue reading until we hit a boundary character and return an error
|
||||
this.consumeIdentifier();
|
||||
break;
|
||||
}
|
||||
|
||||
this.addToken(TokenType.OR);
|
||||
break;
|
||||
|
||||
case "*":
|
||||
this.addToken(TokenType.STAR);
|
||||
break;
|
||||
|
||||
// Ignore whitespace.
|
||||
case " ":
|
||||
case "\r":
|
||||
case "\t":
|
||||
break;
|
||||
|
||||
case "\n":
|
||||
++this.line;
|
||||
this.lastLineOffset = this.offset;
|
||||
break;
|
||||
|
||||
case "'":
|
||||
this.consumeString();
|
||||
break;
|
||||
|
||||
default:
|
||||
switch (true) {
|
||||
case isDigit(c):
|
||||
this.consumeNumber();
|
||||
break;
|
||||
|
||||
default:
|
||||
this.consumeIdentifier();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.tokens.push({
|
||||
type: TokenType.EOF,
|
||||
lexeme: "",
|
||||
pos: this.pos(),
|
||||
});
|
||||
|
||||
return {
|
||||
tokens: this.tokens,
|
||||
};
|
||||
}
|
||||
|
||||
private pos(): Pos {
|
||||
return {
|
||||
line: this.line,
|
||||
column: this.start - this.lastLineOffset,
|
||||
};
|
||||
}
|
||||
|
||||
private atEnd(): boolean {
|
||||
return this.offset >= this.input.length;
|
||||
}
|
||||
|
||||
private peek(): string {
|
||||
if (this.atEnd()) {
|
||||
return "\0";
|
||||
}
|
||||
|
||||
return this.input[this.offset];
|
||||
}
|
||||
|
||||
private peekNext(): string {
|
||||
if (this.offset + 1 >= this.input.length) {
|
||||
return "\0";
|
||||
}
|
||||
|
||||
return this.input[this.offset + 1];
|
||||
}
|
||||
|
||||
private previous(): TokenType {
|
||||
const l = this.tokens.length;
|
||||
if (l == 0) {
|
||||
return TokenType.EOF;
|
||||
}
|
||||
|
||||
return this.tokens[l - 1].type;
|
||||
}
|
||||
|
||||
private next(): string {
|
||||
return this.input[this.offset++];
|
||||
}
|
||||
|
||||
private reverse(): string {
|
||||
return this.input[--this.offset];
|
||||
}
|
||||
|
||||
private match(expected: string): boolean {
|
||||
if (this.atEnd()) {
|
||||
return false;
|
||||
}
|
||||
if (this.input[this.offset] !== expected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.offset++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private addToken(type: TokenType, value?: string | number | boolean) {
|
||||
this.tokens.push({
|
||||
type,
|
||||
lexeme: this.input.substring(this.start, this.offset),
|
||||
pos: this.pos(),
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
private consumeNumber() {
|
||||
while (!this.atEnd() && (!isBoundary(this.peek()) || this.peek() == ".")) {
|
||||
this.next();
|
||||
}
|
||||
|
||||
const lexeme = this.input.substring(this.start, this.offset);
|
||||
const value = new StringData(lexeme).number();
|
||||
|
||||
if (isNaN(value)) {
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||
this.start + 1
|
||||
} within expression: ${this.input}`
|
||||
);
|
||||
}
|
||||
|
||||
this.addToken(TokenType.NUMBER, value);
|
||||
}
|
||||
|
||||
private consumeString() {
|
||||
while ((this.peek() !== "'" || this.peekNext() === "'") && !this.atEnd()) {
|
||||
if (this.peek() === "\n") this.line++;
|
||||
if (this.peek() === "'" && this.peekNext() === "'") {
|
||||
// Escaped "'", consume
|
||||
this.next();
|
||||
}
|
||||
this.next();
|
||||
}
|
||||
|
||||
if (this.atEnd()) {
|
||||
// Unterminated string
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${this.input.substring(
|
||||
this.start
|
||||
)}'. Located at position ${this.start + 1} within expression: ${
|
||||
this.input
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
// Closing '
|
||||
this.next();
|
||||
|
||||
// Trim the surrounding quotes.
|
||||
let value = this.input.substring(this.start + 1, this.offset - 1);
|
||||
value = value.replace("''", "'");
|
||||
|
||||
this.addToken(TokenType.STRING, value);
|
||||
}
|
||||
|
||||
private consumeIdentifier() {
|
||||
while (!this.atEnd() && !isBoundary(this.peek())) {
|
||||
this.next();
|
||||
}
|
||||
|
||||
let tokenType = TokenType.IDENTIFIER;
|
||||
let tokenValue = undefined;
|
||||
|
||||
const lexeme = this.input.substring(this.start, this.offset);
|
||||
|
||||
if (this.previous() != TokenType.DOT) {
|
||||
switch (lexeme) {
|
||||
case "true":
|
||||
tokenType = TokenType.TRUE;
|
||||
break;
|
||||
|
||||
case "false":
|
||||
tokenType = TokenType.FALSE;
|
||||
break;
|
||||
|
||||
case "null":
|
||||
tokenType = TokenType.NULL;
|
||||
break;
|
||||
|
||||
case "NaN":
|
||||
tokenType = TokenType.NUMBER;
|
||||
tokenValue = NaN;
|
||||
break;
|
||||
|
||||
case "Infinity":
|
||||
tokenType = TokenType.NUMBER;
|
||||
tokenValue = Infinity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLegalIdentifier(lexeme)) {
|
||||
throw new Error(
|
||||
`Unexpected symbol: '${lexeme}'. Located at position ${
|
||||
this.start + 1
|
||||
} within expression: ${this.input}`
|
||||
);
|
||||
}
|
||||
|
||||
this.addToken(tokenType, tokenValue);
|
||||
}
|
||||
}
|
||||
|
||||
function isDigit(c: string) {
|
||||
return c >= "0" && c <= "9";
|
||||
}
|
||||
|
||||
function isBoundary(c: string): boolean {
|
||||
switch (c) {
|
||||
case "(":
|
||||
case "[":
|
||||
case ")":
|
||||
case "]":
|
||||
case ",":
|
||||
case ".":
|
||||
case "!":
|
||||
case ">":
|
||||
case "<":
|
||||
case "=":
|
||||
case "&":
|
||||
case "|":
|
||||
return true;
|
||||
}
|
||||
|
||||
return /\s/.test(c);
|
||||
}
|
||||
|
||||
function isLegalIdentifier(str: string): boolean {
|
||||
if (str == "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const first = str[0];
|
||||
if (
|
||||
(first >= "a" && first <= "z") ||
|
||||
(first >= "A" && first <= "Z") ||
|
||||
first == "_"
|
||||
) {
|
||||
for (const c of str.substring(1).split("")) {
|
||||
if (
|
||||
(c >= "a" && c <= "z") ||
|
||||
(c >= "A" && c <= "Z") ||
|
||||
(c >= "0" && c <= "9") ||
|
||||
c == "_" ||
|
||||
c == "-"
|
||||
) {
|
||||
// OK
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
import {
|
||||
Binary,
|
||||
ContextAccess,
|
||||
Expr,
|
||||
FunctionCall,
|
||||
Grouping,
|
||||
IndexAccess,
|
||||
Literal,
|
||||
Logical,
|
||||
Star,
|
||||
Unary,
|
||||
} from "./ast";
|
||||
import * as data from "./data";
|
||||
import { ErrorType, ExpressionError, MAX_PARSER_DEPTH } from "./errors";
|
||||
import { ParseContext, validateFunction } from "./funcs";
|
||||
import { FunctionInfo } from "./funcs/info";
|
||||
import { Token, TokenType } from "./lexer";
|
||||
|
||||
export class Parser {
|
||||
private extContexts: Map<string, boolean>;
|
||||
private extFuncs: Map<string, FunctionInfo>;
|
||||
|
||||
private offset: number = 0;
|
||||
private depth: number = 0;
|
||||
|
||||
private context: ParseContext;
|
||||
|
||||
/**
|
||||
* Constructs a new parser for the given tokens
|
||||
*
|
||||
* @param tokens Tokens to build a parse tree from
|
||||
* @param extensionContexts Available context names
|
||||
* @param extensionFunctions Available functions (beyond the built-in ones)
|
||||
*/
|
||||
constructor(
|
||||
private tokens: Token[],
|
||||
extensionContexts: string[],
|
||||
extensionFunctions: FunctionInfo[]
|
||||
) {
|
||||
this.extContexts = new Map<string, boolean>();
|
||||
this.extFuncs = new Map();
|
||||
|
||||
for (const contextName of extensionContexts) {
|
||||
this.extContexts.set(contextName.toLowerCase(), true);
|
||||
}
|
||||
|
||||
for (const { name, func } of extensionFunctions.map((x) => ({
|
||||
name: x.name,
|
||||
func: x,
|
||||
}))) {
|
||||
this.extFuncs.set(name.toLowerCase(), func);
|
||||
}
|
||||
|
||||
this.context = {
|
||||
allowUnknownKeywords: false,
|
||||
extensionContexts: this.extContexts,
|
||||
extensionFunctions: this.extFuncs,
|
||||
};
|
||||
}
|
||||
|
||||
public parse(): Expr {
|
||||
let result!: Expr;
|
||||
|
||||
// No tokens
|
||||
if (this.atEnd()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this.expression();
|
||||
|
||||
if (!this.atEnd()) {
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private expression(): Expr {
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
return this.logicalOr();
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
private logicalOr(): Expr {
|
||||
// && is higher precedence than ||
|
||||
let expr = this.logicalAnd();
|
||||
|
||||
if (this.check(TokenType.OR)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
const logical = new Logical(this.peek(), [expr]);
|
||||
expr = logical;
|
||||
|
||||
while (this.match(TokenType.OR)) {
|
||||
const right = this.logicalAnd();
|
||||
logical.args.push(right);
|
||||
}
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private logicalAnd(): Expr {
|
||||
// == and != are higher precedence than &&
|
||||
let expr = this.equality();
|
||||
|
||||
if (this.check(TokenType.AND)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
const logical = new Logical(this.peek(), [expr]);
|
||||
expr = logical;
|
||||
|
||||
while (this.match(TokenType.AND)) {
|
||||
const right = this.equality();
|
||||
logical.args.push(right);
|
||||
}
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private equality(): Expr {
|
||||
// >, >=, <, <= are higher precedence than == and !=
|
||||
let expr = this.comparison();
|
||||
|
||||
while (this.match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) {
|
||||
const operator = this.previous();
|
||||
const right = this.comparison();
|
||||
|
||||
expr = new Binary(expr, operator, right);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private comparison(): Expr {
|
||||
// ! is higher precedence than >, >=, <, <=
|
||||
let expr = this.unary();
|
||||
|
||||
while (
|
||||
this.match(
|
||||
TokenType.GREATER,
|
||||
TokenType.GREATER_EQUAL,
|
||||
TokenType.LESS,
|
||||
TokenType.LESS_EQUAL
|
||||
)
|
||||
) {
|
||||
const operator = this.previous();
|
||||
const right = this.unary();
|
||||
|
||||
expr = new Binary(expr, operator, right);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private unary(): Expr {
|
||||
if (this.match(TokenType.BANG)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
const operator = this.previous();
|
||||
const unary = this.unary();
|
||||
|
||||
try {
|
||||
return new Unary(operator, unary);
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return this.index();
|
||||
}
|
||||
|
||||
private index(): Expr {
|
||||
let expr = this.call();
|
||||
|
||||
let depthIncreased = 0;
|
||||
|
||||
if (
|
||||
expr instanceof Grouping ||
|
||||
expr instanceof FunctionCall ||
|
||||
expr instanceof ContextAccess
|
||||
) {
|
||||
let cont = true;
|
||||
while (cont) {
|
||||
switch (true) {
|
||||
case this.match(TokenType.LEFT_BRACKET):
|
||||
let indexExpr: Expr;
|
||||
if (this.match(TokenType.STAR)) {
|
||||
indexExpr = new Star();
|
||||
} else {
|
||||
indexExpr = this.expression();
|
||||
}
|
||||
|
||||
this.consume(
|
||||
TokenType.RIGHT_BRACKET,
|
||||
ErrorType.ErrorUnexpectedSymbol
|
||||
);
|
||||
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
depthIncreased++;
|
||||
expr = new IndexAccess(expr, indexExpr);
|
||||
break;
|
||||
|
||||
case this.match(TokenType.DOT):
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
depthIncreased++;
|
||||
|
||||
if (this.match(TokenType.IDENTIFIER)) {
|
||||
let property = this.previous();
|
||||
expr = new IndexAccess(
|
||||
expr,
|
||||
new Literal(new data.StringData(property.lexeme))
|
||||
);
|
||||
} else if (this.match(TokenType.STAR)) {
|
||||
expr = new IndexAccess(expr, new Star());
|
||||
} else {
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedSymbol,
|
||||
this.peek()
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
cont = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < depthIncreased; i++) {
|
||||
this.decrDepth();
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private call(): Expr {
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
return this.primary();
|
||||
}
|
||||
|
||||
const identifier = this.next();
|
||||
|
||||
if (!this.match(TokenType.LEFT_PAREN)) {
|
||||
if (!this.extContexts.has(identifier.lexeme.toLowerCase())) {
|
||||
throw this.buildError(ErrorType.ErrorUnrecognizedContext, identifier);
|
||||
}
|
||||
return new ContextAccess(identifier);
|
||||
}
|
||||
|
||||
// Function call
|
||||
const args: Expr[] = [];
|
||||
|
||||
// Arguments
|
||||
while (!this.match(TokenType.RIGHT_PAREN)) {
|
||||
const aexp = this.expression();
|
||||
|
||||
args.push(aexp);
|
||||
|
||||
if (!this.check(TokenType.RIGHT_PAREN)) {
|
||||
this.consume(TokenType.COMMA, ErrorType.ErrorUnexpectedSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
validateFunction(this.context, identifier, args.length);
|
||||
|
||||
return new FunctionCall(identifier, args);
|
||||
}
|
||||
|
||||
private primary(): Expr {
|
||||
switch (true) {
|
||||
case this.match(TokenType.FALSE):
|
||||
return new Literal(new data.BooleanData(false));
|
||||
|
||||
case this.match(TokenType.TRUE):
|
||||
return new Literal(new data.BooleanData(true));
|
||||
|
||||
case this.match(TokenType.NULL):
|
||||
return new Literal(new data.Null());
|
||||
|
||||
case this.match(TokenType.NUMBER):
|
||||
return new Literal(new data.NumberData(this.previous().value as number));
|
||||
|
||||
case this.match(TokenType.STRING):
|
||||
return new Literal(new data.StringData(this.previous().value as string));
|
||||
|
||||
case this.match(TokenType.LEFT_PAREN):
|
||||
const expr = this.expression();
|
||||
|
||||
if (this.atEnd()) {
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedEndOfExpression,
|
||||
this.previous()
|
||||
); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol);
|
||||
return new Grouping(expr);
|
||||
|
||||
case this.atEnd():
|
||||
throw this.buildError(
|
||||
ErrorType.ErrorUnexpectedEndOfExpression,
|
||||
this.previous()
|
||||
); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
// match consumes the next token if it matches any of the given types
|
||||
private match(...tokenTypes: TokenType[]): boolean {
|
||||
for (const tokenType of tokenTypes) {
|
||||
if (this.check(tokenType)) {
|
||||
this.next();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check peeks whether the next token is of the given type
|
||||
private check(tokenType: TokenType): boolean {
|
||||
if (this.atEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.peek().type == tokenType;
|
||||
}
|
||||
|
||||
// atEnd peeks whether the next token is EOF
|
||||
private atEnd(): boolean {
|
||||
return this.peek().type == TokenType.EOF;
|
||||
}
|
||||
|
||||
private next(): Token {
|
||||
if (!this.atEnd()) {
|
||||
this.offset++;
|
||||
}
|
||||
|
||||
return this.previous();
|
||||
}
|
||||
|
||||
private peek(): Token {
|
||||
return this.tokens[this.offset];
|
||||
}
|
||||
|
||||
// previous returns the previous token
|
||||
private previous(): Token {
|
||||
return this.tokens[this.offset - 1];
|
||||
}
|
||||
|
||||
// consume attempts to consume the next token if it matches the given type. It returns an error of
|
||||
// the given ParseErrorKind otherwise.
|
||||
private consume(tokenType: TokenType, errorType: ErrorType) {
|
||||
if (this.check(tokenType)) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
throw this.buildError(errorType, this.peek());
|
||||
}
|
||||
|
||||
private incrDepth() {
|
||||
this.depth++;
|
||||
if (this.depth > MAX_PARSER_DEPTH) {
|
||||
throw this.buildError(ErrorType.ErrorExceededMaxDepth, this.peek());
|
||||
}
|
||||
}
|
||||
|
||||
private decrDepth() {
|
||||
this.depth--;
|
||||
}
|
||||
|
||||
private buildError(errType: ErrorType, token: Token): Error {
|
||||
return new ExpressionError(errType, token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { BooleanData, ExpressionData, NumberData, StringData } from "./data";
|
||||
import { coerceTypes, toUpperSpecial } from "./result";
|
||||
|
||||
describe("coerceTypes", () => {
|
||||
const tests: {
|
||||
name: string;
|
||||
data: {
|
||||
l: ExpressionData;
|
||||
r: ExpressionData;
|
||||
};
|
||||
wantL: ExpressionData;
|
||||
wantR: ExpressionData;
|
||||
}[] = [
|
||||
{
|
||||
name: "number-bool",
|
||||
data: { l: new NumberData(1), r: new BooleanData(true) },
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(1),
|
||||
},
|
||||
{
|
||||
name: "number-bool-false",
|
||||
data: { l: new NumberData(1), r: new BooleanData(false) },
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(0),
|
||||
},
|
||||
{
|
||||
name: "bool-number-false",
|
||||
data: { l: new BooleanData(false), r: new NumberData(1) },
|
||||
wantL: new NumberData(0),
|
||||
wantR: new NumberData(1),
|
||||
},
|
||||
{
|
||||
name: "number-number",
|
||||
data: { l: new NumberData(1), r: new NumberData(2) },
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(2),
|
||||
},
|
||||
{
|
||||
name: "string-string",
|
||||
data: { l: new StringData("a"), r: new StringData("b") },
|
||||
wantL: new StringData("a"),
|
||||
wantR: new StringData("b"),
|
||||
},
|
||||
{
|
||||
name: "string-number",
|
||||
data: { l: new StringData("a"), r: new NumberData(1) },
|
||||
wantL: new NumberData(NaN),
|
||||
wantR: new NumberData(1),
|
||||
},
|
||||
{
|
||||
name: "number-string",
|
||||
data: { l: new NumberData(1), r: new StringData("a") },
|
||||
wantL: new NumberData(1),
|
||||
wantR: new NumberData(NaN),
|
||||
},
|
||||
{
|
||||
name: "bool-bool",
|
||||
data: { l: new BooleanData(false), r: new BooleanData(true) },
|
||||
wantL: new BooleanData(false),
|
||||
wantR: new BooleanData(true),
|
||||
},
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$name",
|
||||
({
|
||||
data,
|
||||
wantL,
|
||||
wantR,
|
||||
}: {
|
||||
data: { l: ExpressionData; r: ExpressionData };
|
||||
wantL: ExpressionData;
|
||||
wantR: ExpressionData;
|
||||
}) => {
|
||||
const [gotL, gotR] = coerceTypes(data.l, data.r);
|
||||
expect(gotL).toEqual(wantL);
|
||||
expect(gotR).toEqual(wantR);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe("toUpperSpecial", () => {
|
||||
const tests: { input: string; want: string }[] = [
|
||||
{ input: "", want: "" },
|
||||
{ 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ıghi", want: "ABCıDEFıGHI" },
|
||||
];
|
||||
|
||||
test.each(tests)(
|
||||
"$input",
|
||||
({ input, want }: { input: string; want: string }) => {
|
||||
expect(toUpperSpecial(input)).toEqual(want);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
import * as data from "./data";
|
||||
|
||||
export function falsy(d: data.ExpressionData): boolean {
|
||||
switch (d.kind) {
|
||||
case data.Kind.Null:
|
||||
return true;
|
||||
|
||||
case data.Kind.Boolean:
|
||||
return !d.value;
|
||||
|
||||
case data.Kind.Number:
|
||||
const dv = d.value;
|
||||
return dv === 0 || isNaN(dv);
|
||||
|
||||
case data.Kind.String:
|
||||
return d.value.length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function truthy(d: data.ExpressionData): boolean {
|
||||
return !falsy(d);
|
||||
}
|
||||
|
||||
// Similar to the Javascript abstract equality comparison algorithm http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3.
|
||||
// Except objects are not coerced to primitives.
|
||||
export function coerceTypes(
|
||||
li: data.ExpressionData,
|
||||
ri: data.ExpressionData
|
||||
): [data.ExpressionData, data.ExpressionData] {
|
||||
let lv = li;
|
||||
let rv = ri;
|
||||
|
||||
// Do nothing, same kind
|
||||
if (li.kind === ri.kind) {
|
||||
return [lv, rv];
|
||||
}
|
||||
|
||||
switch (li.kind) {
|
||||
// Number, String
|
||||
case data.Kind.Number:
|
||||
if (ri.kind === data.Kind.String) {
|
||||
rv = new data.NumberData(ri.number());
|
||||
return [lv, rv];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// String, Number
|
||||
case data.Kind.String:
|
||||
if (ri.kind === data.Kind.Number) {
|
||||
lv = new data.NumberData(li.number());
|
||||
return [lv, rv];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Boolean|Null, Any
|
||||
case data.Kind.Null:
|
||||
case data.Kind.Boolean:
|
||||
lv = new data.NumberData(li.number());
|
||||
return coerceTypes(lv, rv);
|
||||
}
|
||||
|
||||
// Any, Boolean|Null
|
||||
switch (ri.kind) {
|
||||
case data.Kind.Null:
|
||||
case data.Kind.Boolean:
|
||||
rv = new data.NumberData(ri.number());
|
||||
return coerceTypes(lv, rv);
|
||||
}
|
||||
|
||||
return [lv, rv];
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function equals(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
let [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lv.kind) {
|
||||
// Null, Null
|
||||
case data.Kind.Null:
|
||||
return true;
|
||||
|
||||
// Number, Number
|
||||
case data.Kind.Number:
|
||||
const ld = lv.value;
|
||||
const rd = (rv as data.NumberData).value;
|
||||
if (isNaN(ld) || isNaN(rd)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ld == rd;
|
||||
|
||||
// String, String
|
||||
case data.Kind.String:
|
||||
const ls = lv.value;
|
||||
const rs = (rv as data.StringData).value;
|
||||
return toUpperSpecial(ls) === toUpperSpecial(rs);
|
||||
|
||||
// Boolean, Boolean
|
||||
case data.Kind.Boolean:
|
||||
const lb = lv.value;
|
||||
const rb = (rv as data.BooleanData).value;
|
||||
return lb == rb;
|
||||
|
||||
// Object, Object
|
||||
case data.Kind.Dictionary:
|
||||
case data.Kind.Array:
|
||||
// Check reference equality
|
||||
return lv === rv;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function greaterThan(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lv.kind) {
|
||||
// Number, Number
|
||||
case data.Kind.Number:
|
||||
const lf = lv.value;
|
||||
const rf = (rv as data.NumberData).value;
|
||||
if (isNaN(lf) || isNaN(rf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lf > rf;
|
||||
|
||||
// String, String
|
||||
case data.Kind.String:
|
||||
let ls = lv.value;
|
||||
let rs = (rv as data.StringData).value;
|
||||
ls = toUpperSpecial(ls);
|
||||
rs = toUpperSpecial(rs);
|
||||
return ls > rs;
|
||||
|
||||
// Boolean, Boolean
|
||||
case data.Kind.Boolean:
|
||||
const lb = (lv as data.BooleanData).value;
|
||||
const rb = (rv as data.BooleanData).value;
|
||||
return lb && !rb;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
export function lessThan(
|
||||
lhs: data.ExpressionData,
|
||||
rhs: data.ExpressionData
|
||||
): boolean {
|
||||
const [lv, rv] = coerceTypes(lhs, rhs);
|
||||
|
||||
if (lv.kind != rv.kind) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (lv.kind) {
|
||||
// Number, Number
|
||||
case data.Kind.Number:
|
||||
const lf = lv.value;
|
||||
const rf = (rv as data.NumberData).value;
|
||||
if (isNaN(lf) || isNaN(rf)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lf < rf;
|
||||
|
||||
// String, String
|
||||
case data.Kind.String:
|
||||
let ls = lv.value;
|
||||
let rs = (rv as data.StringData).value;
|
||||
ls = toUpperSpecial(ls);
|
||||
rs = toUpperSpecial(rs);
|
||||
return ls < rs;
|
||||
|
||||
// Boolean, Boolean
|
||||
case data.Kind.Boolean:
|
||||
const lb = lv.value;
|
||||
const rb = (rv as data.BooleanData).value;
|
||||
return !lb && rb;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not toUpper the small-dotless-ı
|
||||
export function toUpperSpecial(s: string): string {
|
||||
const sb: string[] = [];
|
||||
let i = 0;
|
||||
let len = s.length;
|
||||
let found = s.indexOf("ı");
|
||||
while (i < len) {
|
||||
if (i < found) {
|
||||
sb.push(s.substring(i, found).toUpperCase()); // Append upper segment
|
||||
i = found;
|
||||
} else if (i == found) {
|
||||
sb.push(s.substring(i, i + 1));
|
||||
i += 1;
|
||||
found = s.indexOf("ı", i);
|
||||
} else {
|
||||
sb.push(s.substring(i).toUpperCase()); // Append upper remaining
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb.join("");
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { Expr } from "./ast";
|
||||
import * as data from "./data";
|
||||
import { kindStr } from "./data/expressiondata";
|
||||
import { replacer } from "./data/replacer";
|
||||
import { reviver } from "./data/reviver";
|
||||
import { ExpressionError } from "./errors";
|
||||
import { Evaluator } from "./evaluator";
|
||||
import { Lexer, Result } from "./lexer";
|
||||
import { Parser } from "./parser";
|
||||
|
||||
interface TestResult {
|
||||
value: data.ExpressionData;
|
||||
kind: string;
|
||||
}
|
||||
|
||||
enum TestErrorKind {
|
||||
Lexing = "lexing",
|
||||
Parsing = "parsing",
|
||||
Evaluation = "evaluation",
|
||||
}
|
||||
|
||||
enum SkipOptionKind {
|
||||
Typescript = "typescript",
|
||||
}
|
||||
|
||||
interface TestOptions {
|
||||
skip: SkipOptionKind[];
|
||||
}
|
||||
|
||||
interface TestError {
|
||||
value: string;
|
||||
kind: TestErrorKind;
|
||||
}
|
||||
|
||||
interface TestCase {
|
||||
expr: string;
|
||||
result: TestResult;
|
||||
err: TestError | undefined;
|
||||
contexts: data.Dictionary;
|
||||
options: TestOptions;
|
||||
}
|
||||
|
||||
function testCaseReviver(key: string, val: any): any {
|
||||
if (key === "contexts") {
|
||||
const tmp = JSON.stringify(val);
|
||||
return JSON.parse(tmp, reviver);
|
||||
}
|
||||
|
||||
if (key === "result") {
|
||||
const t = val as TestResult;
|
||||
t.value = reviver("value", t.value);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
const testFiles = "./testData";
|
||||
|
||||
describe("x-lang tests", () => {
|
||||
const files = fs.readdirSync(testFiles);
|
||||
|
||||
for (const file of files) {
|
||||
const fileName = path.join(testFiles, file);
|
||||
|
||||
const fileStat = fs.statSync(fileName);
|
||||
if (fileStat.isDirectory()) {
|
||||
throw new Error("sub-directories are not supported");
|
||||
}
|
||||
|
||||
if (path.extname(fileName) !== ".json") {
|
||||
throw new Error("only json files are supported " + file);
|
||||
}
|
||||
|
||||
const testFile = fs.readFileSync(fileName, "utf8");
|
||||
|
||||
const testCases = JSON.parse(testFile, testCaseReviver) as {
|
||||
[name: string]: TestCase[];
|
||||
};
|
||||
|
||||
for (const testCaseName of Object.keys(testCases)) {
|
||||
let tests = testCases[testCaseName];
|
||||
|
||||
// Filter out tests that are not supported by typescript
|
||||
tests = tests.filter(
|
||||
(t) => !t.options?.skip?.includes(SkipOptionKind.Typescript)
|
||||
);
|
||||
|
||||
const testName = path.basename(file, ".json");
|
||||
|
||||
if (tests.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
describe(testName, () => {
|
||||
test.each(tests)(`${testName}: ${testCaseName} ($expr)`, (testCase) => {
|
||||
if (!testCase.contexts) {
|
||||
testCase.contexts = new data.Dictionary();
|
||||
}
|
||||
|
||||
const lexer = new Lexer(testCase.expr);
|
||||
let result: Result;
|
||||
try {
|
||||
result = lexer.lex();
|
||||
|
||||
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
|
||||
throw new Error("expected error lexing expression, but got none");
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Did test expect lexing error? If so, compare error message.
|
||||
if (testCase.err && testCase.err.kind === TestErrorKind.Lexing) {
|
||||
expect(e.message).toContain(testCase.err.value);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error lexing expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
// Parse
|
||||
const contextNames = testCase.contexts.pairs().map((x) => x.key);
|
||||
const parser = new Parser(result.tokens, contextNames, []);
|
||||
let expr: Expr;
|
||||
try {
|
||||
expr = parser.parse();
|
||||
|
||||
if (testCase.err?.kind === TestErrorKind.Parsing) {
|
||||
throw new Error(
|
||||
"expected error parsing expression, but got none"
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Did test expect parsing error?
|
||||
if (testCase.err?.kind === TestErrorKind.Parsing) {
|
||||
// Test expects parsing error
|
||||
const pe = e as ExpressionError;
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(
|
||||
testCase.err.value
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error parsing expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
}
|
||||
|
||||
// Evaluate expression
|
||||
try {
|
||||
let result: data.ExpressionData;
|
||||
if (expr !== undefined) {
|
||||
const evaluator = new Evaluator(expr, testCase.contexts);
|
||||
result = evaluator.evaluate();
|
||||
} else {
|
||||
result = new data.Null();
|
||||
}
|
||||
|
||||
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||
throw new Error(
|
||||
"expected error evaluating expression, but got none"
|
||||
);
|
||||
}
|
||||
|
||||
expect(kindStr(result.kind)).toBe(testCase.result.kind);
|
||||
|
||||
expect(JSON.stringify(result, replacer)).toEqual(
|
||||
JSON.stringify(testCase.result.value, replacer)
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (testCase.err?.kind === TestErrorKind.Evaluation) {
|
||||
const pe = e as ExpressionError;
|
||||
expect(errorWithExpression(pe, testCase.expr)).toContain(
|
||||
testCase.err.value
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`unexpected error evaluating expression: ${e.message} ${e.stack}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function errorWithExpression(e: ExpressionError, expr: string): string {
|
||||
if (e.pos !== undefined) {
|
||||
return `${e.message}. Located at position ${
|
||||
e.pos.column + 1
|
||||
} within expression: ${expr}`;
|
||||
}
|
||||
|
||||
return `${e.message}. Located within expression: ${expr}`;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"empty_expression": [{
|
||||
"expr": "",
|
||||
"result": {
|
||||
"kind": "Null",
|
||||
"value": null
|
||||
}
|
||||
}],
|
||||
"equal_simple": [{
|
||||
"expr": "1 == 2",
|
||||
"result": {
|
||||
"kind": "Boolean",
|
||||
"value": false
|
||||
}
|
||||
}],
|
||||
"context_simple_access": [{
|
||||
"expr": "simple",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "foo"
|
||||
},
|
||||
"contexts": {
|
||||
"simple": "foo"
|
||||
}
|
||||
}],
|
||||
"context_case-insensitive": [{
|
||||
"expr": "SIMple.TEst",
|
||||
"result": {
|
||||
"kind": "Number",
|
||||
"value": 123.0
|
||||
},
|
||||
"contexts": {
|
||||
"simPLE": {
|
||||
"teST": 123
|
||||
}
|
||||
}
|
||||
}],
|
||||
"context access with wildcard": [{
|
||||
"expr": "toJson(input.*.foo)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "[\n 32,\n 42,\n -10,\n 0,\n 2,\n 17\n]"
|
||||
},
|
||||
"contexts": {
|
||||
"input": {
|
||||
"test": { "foo": 32 },
|
||||
"test2": { "foo": 42 },
|
||||
"test3": { "foo": -10 },
|
||||
"test4": { "foo": 0 },
|
||||
"test5": { "foo": 2 },
|
||||
"test6": { "foo": 17 }
|
||||
}
|
||||
}
|
||||
}],
|
||||
"unknown context": [
|
||||
{
|
||||
"expr": "nosuchcontext.foo",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'nosuchcontext'. Located at position 1 within expression: nosuchcontext.foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"refer ./operator_not.json": [
|
||||
]
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"null": [
|
||||
{
|
||||
"expr": "null == 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"boolean": [
|
||||
{
|
||||
"expr": "true == 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false == 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"string": [
|
||||
{
|
||||
"expr": "'' == 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 123456.789 ' == 123456.789",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' +123456.789 ' == 123456.789",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' -123456.789 ' == -123456.789",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0xff ' == 255",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0xFF ' == 255",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0o10 ' == 8",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' Infinity ' == Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' -Infinity ' == -Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' NaN ' != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' NaN ' == NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' NaN ' > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' NaN ' < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' abc ' != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' abc ' == NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' abc ' > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' abc ' < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"array": [
|
||||
{
|
||||
"expr": "fromjson('[]') != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') == NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"object": [
|
||||
{
|
||||
"expr": "fromjson('[]') != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') == NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('[]') < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"null": [
|
||||
{
|
||||
"expr": "format('{0}', null)",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
}
|
||||
],
|
||||
"boolean": [
|
||||
{
|
||||
"expr": "format('{0}', true)",
|
||||
"result": { "kind": "String", "value": "true" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', false)",
|
||||
"result": { "kind": "String", "value": "false" }
|
||||
}
|
||||
],
|
||||
"number": [
|
||||
{
|
||||
"expr": "format('{0}', 1)",
|
||||
"result": { "kind": "String", "value": "1" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', .5)",
|
||||
"result": { "kind": "String", "value": "0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0.5)",
|
||||
"result": { "kind": "String", "value": "0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 2)",
|
||||
"result": { "kind": "String", "value": "2" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -1)",
|
||||
"result": { "kind": "String", "value": "-1" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -.5)",
|
||||
"result": { "kind": "String", "value": "-0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -0.5)",
|
||||
"result": { "kind": "String", "value": "-0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -2.0)",
|
||||
"result": { "kind": "String", "value": "-2" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0.0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -0.0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 123456.789)",
|
||||
"result": { "kind": "String", "value": "123456.789" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', +123456.789)",
|
||||
"result": { "kind": "String", "value": "123456.789" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -123456.789)",
|
||||
"result": { "kind": "String", "value": "-123456.789" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0.84551240822557006)",
|
||||
"result": { "kind": "String", "value": "0.84551240822557" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.9)",
|
||||
"result": { "kind": "String", "value": "1.9" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1234.56)",
|
||||
"result": { "kind": "String", "value": "1234.56" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0xff)",
|
||||
"result": { "kind": "String", "value": "255" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0xFF)",
|
||||
"result": { "kind": "String", "value": "255" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 0o10)",
|
||||
"result": { "kind": "String", "value": "8" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2e2)",
|
||||
"result": { "kind": "String", "value": "120" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2E2)",
|
||||
"result": { "kind": "String", "value": "120" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2e+2)",
|
||||
"result": { "kind": "String", "value": "120" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2E+2)",
|
||||
"result": { "kind": "String", "value": "120" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2e-2)",
|
||||
"result": { "kind": "String", "value": "0.012" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', 1.2E-2)",
|
||||
"result": { "kind": "String", "value": "0.012" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', Infinity)",
|
||||
"result": { "kind": "String", "value": "Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -Infinity)",
|
||||
"result": { "kind": "String", "value": "-Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', NaN)",
|
||||
"result": { "kind": "String", "value": "NaN" }
|
||||
}
|
||||
],
|
||||
"string": [
|
||||
{
|
||||
"expr": "format('{0}', 'string value')",
|
||||
"result": { "kind": "String", "value": "string value" }
|
||||
}
|
||||
],
|
||||
"array": [
|
||||
{
|
||||
"expr": "format('{0}', context)",
|
||||
"contexts": {
|
||||
"context": []
|
||||
},
|
||||
"result": { "kind": "String", "value": "Array" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', context)",
|
||||
"contexts": {
|
||||
"context": [1, 2, 3]
|
||||
},
|
||||
"result": { "kind": "String", "value": "Array" }
|
||||
}
|
||||
],
|
||||
"object": [
|
||||
{
|
||||
"expr": "format('{0}', context)",
|
||||
"contexts": {
|
||||
"context": {}
|
||||
},
|
||||
"result": { "kind": "String", "value": "Object" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', context)",
|
||||
"contexts": {
|
||||
"context": { "a": 1, "b": 2, "c": 3 }
|
||||
},
|
||||
"result": { "kind": "String", "value": "Object" }
|
||||
}
|
||||
]
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"string": [
|
||||
{
|
||||
"expr": "contains('abcdef', 'cde')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('abCdEf', 'cDe')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('abcdef', '')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('1234', 23)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('true', true)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('false', false)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('asdf', null)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "contains('true', false)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"array": [
|
||||
{
|
||||
"expr": "contains(test, 'hello')",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": ["hello"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, 'world')",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": ["hello", "world"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, 'asdf')",
|
||||
"result": { "kind": "Boolean", "value": false },
|
||||
"contexts": { "test": ["hello"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, 123)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [123.0] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, 123)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [123.0] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, null)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [""] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, false)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [""] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test, false)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [0] }
|
||||
}
|
||||
],
|
||||
|
||||
"filtered_array": [
|
||||
{
|
||||
"expr": "contains(test.*, 'hello')",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": ["hello"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, 'world')",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": ["hello", "world"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, 'asdf')",
|
||||
"result": { "kind": "Boolean", "value": false },
|
||||
"contexts": { "test": ["hello"] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, 123)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [123.0] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, 123)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [123.0] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, null)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [""] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, false)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [""] }
|
||||
},
|
||||
{
|
||||
"expr": "contains(test.*, false)",
|
||||
"result": { "kind": "Boolean", "value": true },
|
||||
"contexts": { "test": [0] }
|
||||
}
|
||||
]
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"basics": [
|
||||
{
|
||||
"expr": "endsWith('abcdef', 'def')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('abcdef', 'de')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('abcdef', '')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('1234', 34)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('true', true)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('false', false)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf', null)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('1234', 23)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('true', false)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-a-thru-z": [
|
||||
{
|
||||
"expr": "endsWith('asdf_ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||
{
|
||||
"expr": "endsWith('asdf_ς', 'Σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_ς', 'σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_Σ', 'ς')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_Σ', 'σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_σ', 'ς')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_σ', 'Σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ü-Ü": [
|
||||
{
|
||||
"expr": "endsWith('asdf_ü', 'Ü')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_Ü', 'ü')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ç-Ç": [
|
||||
{
|
||||
"expr": "endsWith('asdf_ç', 'Ç')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_Ç', 'ç')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-i-İ": [
|
||||
{
|
||||
"expr": "endsWith('asdf_i', 'İ')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_İ', 'i')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-ı-I": [
|
||||
{
|
||||
"expr": "endsWith('asdf_ı', 'I')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "endsWith('asdf_I', 'ı')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"cyrillic-letters": [
|
||||
{
|
||||
"expr": "endsWith('asdf_АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ', 'абвгдежзийклмнопрстуфхцчшщьэюя')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"format": [
|
||||
{
|
||||
"expr": "format(null)",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "format(null, 'some arg')",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "format('')",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "format('', 'some arg')",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "format('123{0}456', 'abc')",
|
||||
"result": { "kind": "String", "value": "123abc456" }
|
||||
},
|
||||
{
|
||||
"expr": "format('123{0}456{0}789', 'abc')",
|
||||
"result": { "kind": "String", "value": "123abc456abc789" }
|
||||
},
|
||||
{
|
||||
"expr": "format('123{0}456{1}789', 'abc', 'def')",
|
||||
"result": { "kind": "String", "value": "123abc456def789" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}123', 'abc')",
|
||||
"result": { "kind": "String", "value": "abc123" }
|
||||
},
|
||||
{
|
||||
"expr": "format('123{0}', 'abc')",
|
||||
"result": { "kind": "String", "value": "123abc" }
|
||||
},
|
||||
{
|
||||
"expr": "format('123{0}{1}456', 'abc', 'def')",
|
||||
"result": { "kind": "String", "value": "123abcdef456" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{{0}}', 'abc')",
|
||||
"result": { "kind": "String", "value": "{0}" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{{{{0}}}}', 'abc')",
|
||||
"result": { "kind": "String", "value": "{{0}}" }
|
||||
},
|
||||
{
|
||||
"expr": "format('}}', 'abc')",
|
||||
"result": { "kind": "String", "value": "}" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{{', 'abc')",
|
||||
"result": { "kind": "String", "value": "{" }
|
||||
},
|
||||
{
|
||||
"expr": "format('}}{{', 'abc')",
|
||||
"result": { "kind": "String", "value": "}{" }
|
||||
},
|
||||
{
|
||||
"expr": "format('}}{{}}', 'abc')",
|
||||
"result": { "kind": "String", "value": "}{}" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', Infinity)",
|
||||
"result": { "kind": "String", "value": "Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -Infinity)",
|
||||
"result": { "kind": "String", "value": "-Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', NaN)",
|
||||
"result": { "kind": "String", "value": "NaN" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}', -0.0)",
|
||||
"result": { "kind": "String", "value": "0" }
|
||||
},
|
||||
{
|
||||
"expr": "format('{0')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {0}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}}}}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {0}}}}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('0}')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: 0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('0}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: 0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{{0}')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{{0}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{{{{0}')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {{{{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{{{{0}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {{{{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('}0{')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: }0{"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('}0{', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: }0{"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('}{0}')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: }{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('}{0}', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: }{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}{', '')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string is invalid: {0}{"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string references more arguments than were supplied: {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('{0}{1}', 'abc')",
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The following format string references more arguments than were supplied: {0}{1}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"basics": [
|
||||
{ "expr": "fromJSON('null')", "result": { "kind": "Null", "value": null } },
|
||||
{ "expr": "fromJSON('true')", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "fromJSON('false')", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "fromJSON('0')", "result": { "kind": "Number", "value": 0 } },
|
||||
{ "expr": "fromJSON('-0')", "result": { "kind": "Number", "value": -0 } },
|
||||
{ "expr": "fromJSON('123456789')", "result": { "kind": "Number", "value": 123456789 } },
|
||||
{ "expr": "fromJSON('-123456789')", "result": { "kind": "Number", "value": -123456789 } },
|
||||
{ "expr": "fromJSON('1234.5')", "result": { "kind": "Number", "value": 1234.5 } },
|
||||
{ "expr": "fromJSON('-1234.5')", "result": { "kind": "Number", "value": -1234.5 } },
|
||||
{ "expr": "fromJSON('\"\"')", "result": { "kind": "String", "value": "" } },
|
||||
{ "expr": "fromJSON('\"abc\"')", "result": { "kind": "String", "value": "abc" } },
|
||||
{ "expr": "fromJSON('\"abc''def\"')", "result": { "kind": "String", "value": "abc'def" } },
|
||||
{ "expr": "fromJSON('\"abc\\\\\\\"def\"')", "result": { "kind": "String", "value": "abc\\\"def" } }
|
||||
],
|
||||
"array": [
|
||||
{ "expr": "fromJSON('[]')", "result": { "kind": "Array", "value": [] } },
|
||||
{ "expr": "fromJSON('[1, 2, 3]')", "result": { "kind": "Array", "value": [1, 2, 3] } },
|
||||
{
|
||||
"expr": "fromJSON('[[1, 2, 3], [\"abc\",\"def\",\"ghi\"], [true, false, null, [], {}]]')",
|
||||
"result": {
|
||||
"kind": "Array",
|
||||
"value": [[1, 2, 3], ["abc", "def", "ghi"], [true, false, null, [], {}]]
|
||||
}
|
||||
}
|
||||
],
|
||||
"object": [
|
||||
{ "expr": "fromJSON('{}')", "result": { "kind": "Object", "value": {} } },
|
||||
{
|
||||
"expr": "fromJSON('{\"one\": \"value one\", \"two\": \"value two\", \"three\": \"value three\"}')",
|
||||
"result": {
|
||||
"kind": "Object",
|
||||
"value": {
|
||||
"one": "value one",
|
||||
"two": "value two",
|
||||
"three": "value three"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "fromJSON('{\"nested-one\": {\"one\": 1,\"two\": 2,\"three\": 3},\"nested-two\": {\"string one\": \"value one\",\"string two\": \"value two\",\"string three\": \"value three\"},\"nested-three\": {\"true\": true,\"false\": false,\"null\": null,\"array\": [],\"object\": {}}\n}')",
|
||||
"result": {
|
||||
"kind": "Object",
|
||||
"value": {
|
||||
"nested-one": {
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3
|
||||
},
|
||||
"nested-two": {
|
||||
"string one": "value one",
|
||||
"string two": "value two",
|
||||
"string three": "value three"
|
||||
},
|
||||
"nested-three": {
|
||||
"true": true,
|
||||
"false": false,
|
||||
"null": null,
|
||||
"array": [],
|
||||
"object": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
{
|
||||
"join": [
|
||||
{
|
||||
"expr": "join(github.event.issue.labels.*.name, ', ')",
|
||||
"contexts": {
|
||||
"github": {
|
||||
"event": {
|
||||
"issue": {
|
||||
"labels": [
|
||||
{
|
||||
"name": "bug"
|
||||
},
|
||||
{
|
||||
"name": "enhancement"
|
||||
},
|
||||
{
|
||||
"name": "help wanted"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "bug, enhancement, help wanted" }
|
||||
},
|
||||
{
|
||||
"expr": "join(null)",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "join(true)",
|
||||
"result": { "kind": "String", "value": "true" }
|
||||
},
|
||||
{
|
||||
"expr": "join(123.456)",
|
||||
"result": { "kind": "String", "value": "123.456" }
|
||||
},
|
||||
{
|
||||
"expr": "join('abc')",
|
||||
"result": { "kind": "String", "value": "abc" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray)",
|
||||
"contexts": { "myArray": [] },
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myObject)",
|
||||
"contexts": { "myObject": {} },
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myObject)",
|
||||
"contexts": { "myObject": { "key1": "value1" } },
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray)",
|
||||
"contexts": { "myArray": [ null, true, 123.456, "abc", [], ["def"], {}, { "key1": "value1" } ] },
|
||||
"result": { "kind": "String", "value": ",true,123.456,abc,Array,Array,Object,Object" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, null)",
|
||||
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||
"result": { "kind": "String", "value": "_ITEM-1__ITEM-2__ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, true)",
|
||||
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||
"result": { "kind": "String", "value": "_ITEM-1_true_ITEM-2_true_ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, 123.456)",
|
||||
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||
"result": { "kind": "String", "value": "_ITEM-1_123.456_ITEM-2_123.456_ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, ' | ')",
|
||||
"contexts": { "myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ] },
|
||||
"result": { "kind": "String", "value": "_ITEM-1_ | _ITEM-2_ | _ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, mySeparator)",
|
||||
"contexts": {
|
||||
"myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ],
|
||||
"mySeparator": []
|
||||
},
|
||||
"result": { "kind": "String", "value": "_ITEM-1_,_ITEM-2_,_ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join(myArray, mySeparator)",
|
||||
"contexts": {
|
||||
"myArray": [ "_ITEM-1_", "_ITEM-2_", "_ITEM-3_" ],
|
||||
"mySeparator": {}
|
||||
},
|
||||
"result": { "kind": "String", "value": "_ITEM-1_,_ITEM-2_,_ITEM-3_" }
|
||||
},
|
||||
{
|
||||
"expr": "join()",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Too few parameters supplied: "
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "join(1, 2, 3)",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Too many parameters supplied: "
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"number": [
|
||||
{ "expr": "1", "result": { "kind": "Number", "value": 1.0 } },
|
||||
{ "expr": ".5", "result": { "kind": "Number", "value": 0.5 } },
|
||||
{ "expr": "0.5", "result": { "kind": "Number", "value": 0.5 } },
|
||||
{ "expr": "2", "result": { "kind": "Number", "value": 2.0 } },
|
||||
{ "expr": "-1", "result": { "kind": "Number", "value": -1.0 } },
|
||||
{ "expr": "+1", "result": { "kind": "Number", "value": 1.0 } },
|
||||
{ "expr": "-.5", "result": { "kind": "Number", "value": -0.5 } },
|
||||
{ "expr": "-2", "result": { "kind": "Number", "value": -2.0 } },
|
||||
{ "expr": "format('{0}', -Infinity)", "result": { "kind": "String", "value": "-Infinity" } },
|
||||
{ "expr": "format('{0}', Infinity)", "result": { "kind": "String", "value": "Infinity" } },
|
||||
{ "expr": "format('{0}', +Infinity)", "result": { "kind": "String", "value": "Infinity" } },
|
||||
{ "expr": "format('{0}', NaN)", "result": { "kind": "String", "value": "NaN" } },
|
||||
{ "expr": "0", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{ "expr": "0.0", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{ "expr": "-0", "result": { "kind": "Number", "value": -0.0 } },
|
||||
{ "expr": "-0.0", "result": { "kind": "Number", "value": -0.0 } },
|
||||
{ "expr": "0x0", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{ "expr": "0x00", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{ "expr": "0xf", "result": { "kind": "Number", "value": 15.0 } },
|
||||
{ "expr": "0xfF", "result": { "kind": "Number", "value": 255.0 } },
|
||||
{ "expr": "0xfFf", "result": { "kind": "Number", "value": 4095.0 } },
|
||||
{ "expr": "0o0", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{ "expr": "0o7", "result": { "kind": "Number", "value": 7.0 } },
|
||||
{ "expr": "0o77", "result": { "kind": "Number", "value": 63.0 } },
|
||||
{ "expr": "0o777", "result": { "kind": "Number", "value": 511.0 } },
|
||||
{ "expr": "1e1", "result": { "kind": "Number", "value": 10.0 } },
|
||||
{ "expr": "1e2", "result": { "kind": "Number", "value": 100.0 } },
|
||||
{ "expr": "1E1", "result": { "kind": "Number", "value": 10.0 } },
|
||||
{ "expr": "1E+1", "result": { "kind": "Number", "value": 10.0 } },
|
||||
{ "expr": "1e-1", "result": { "kind": "Number", "value": 0.1 } },
|
||||
{ "expr": "1E-1", "result": { "kind": "Number", "value": 0.1 } },
|
||||
{
|
||||
"expr": "0x01p2",
|
||||
"err": { "kind": "lexing", "value": "Unexpected symbol: '0x01p2'. Located at position 1 within expression: 0x01p2" }
|
||||
},
|
||||
{
|
||||
"expr": "-Inf",
|
||||
"err": { "kind": "lexing", "value": "Unexpected symbol: '-Inf'. Located at position 1 within expression: -Inf" }
|
||||
},
|
||||
{
|
||||
"expr": "-0xFF",
|
||||
"err": { "kind": "lexing", "value": "Unexpected symbol: '-0xFF'. Located at position 1 within expression: -0xFF" }
|
||||
},
|
||||
{
|
||||
"expr": "0xFZ",
|
||||
"err": { "kind": "lexing", "value": "Unexpected symbol: '0xFZ'. Located at position 1 within expression: 0xFZ" }
|
||||
}
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"operator_and": [
|
||||
{
|
||||
"expr": "true && true && true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "true && true", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "true && true && false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true && false && true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false && true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false && false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "true && 1 && 2", "result": { "kind": "Number", "value": 2.0 } },
|
||||
{ "expr": "true && 0 && 2", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{
|
||||
"expr": "true && 'a' && 'b'",
|
||||
"result": { "kind": "String", "value": "b" }
|
||||
},
|
||||
{
|
||||
"expr": "true && '' && 'asdf'",
|
||||
"result": { "kind": "String", "value": "" }
|
||||
},
|
||||
{ "expr": "null && true", "result": { "kind": "Null", "value": null } },
|
||||
{
|
||||
"expr": "test && 123",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Number", "value": 123.0 }
|
||||
},
|
||||
{
|
||||
"expr": "test && 456",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Number", "value": 456.0 }
|
||||
},
|
||||
{
|
||||
"expr": "test && 789",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Number", "value": 789.0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
{
|
||||
"property-basics": [
|
||||
{
|
||||
"expr": "foo.bar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.Bar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.b",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"b": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo._",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"_": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo._bar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"_bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.b_ar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"b_ar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.b-ar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"b-ar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "fromJson('{\"one\": \"one val\"}').one",
|
||||
"result": { "kind": "String", "value": "one val" }
|
||||
},
|
||||
{
|
||||
"expr": "(fromJson('{\"one\": \"one val\"}')).one",
|
||||
"result": { "kind": "String", "value": "one val" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"one": "one val",
|
||||
"two": "two val"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one val", "two val"] }
|
||||
}
|
||||
],
|
||||
"property-case-insensitive": [
|
||||
{
|
||||
"expr": "foo.bar",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"BAR": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.BAR",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
}
|
||||
],
|
||||
"property-matches-const": [
|
||||
{
|
||||
"expr": "foo.true",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"true": "it's true"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's true" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.false",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"false": "it's false"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's false" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.Infinity",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Infinity": "it's Infinity"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.NaN",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"NaN": "it's NaN"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's NaN" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.null",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"null": "it's null"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's null" }
|
||||
},
|
||||
{
|
||||
"expr": "foo.format",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"format": "it's format"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's format" }
|
||||
}
|
||||
],
|
||||
"property-errors": [
|
||||
{
|
||||
"expr": "foo.b@r",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"b@r": "baz"
|
||||
}
|
||||
},
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: 'b@r'. Located at position 5 within expression: foo.b@r"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo.1",
|
||||
"contexts": {
|
||||
"foo": {}
|
||||
},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '1'. Located at position 5 within expression: foo.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "fromjson('').1",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '1'. Located at position 14 within expression: fromjson('').1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo[1].2",
|
||||
"contexts": {
|
||||
"foo": {}
|
||||
},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 8 within expression: foo[1].2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+430
@@ -0,0 +1,430 @@
|
||||
{
|
||||
"boolean": [
|
||||
{ "expr": "true == true", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "false == false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false == true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"number": [
|
||||
{ "expr": "0 == -0", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "2 == 2", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "1.001 == 1.002", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "120 == 1.2e2", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "1 == 2", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "NaN == NaN", "result": { "kind": "Boolean", "value": false } },
|
||||
{
|
||||
"expr": "Infinity == Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity == Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 == Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 == Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"string": [
|
||||
{ "expr": "'a' == 'a'", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "'a' == 'b'", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"null": [
|
||||
{ "expr": "null == null", "result": { "kind": "Boolean", "value": true } }
|
||||
],
|
||||
|
||||
"object": [
|
||||
{
|
||||
"expr": "object1 == object1",
|
||||
"contexts": { "object1": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "object1 == object2",
|
||||
"contexts": { "object1": {}, "object2": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"array": [
|
||||
{
|
||||
"expr": "array1 == array1",
|
||||
"contexts": { "array1": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "array1 == array2",
|
||||
"contexts": { "array1": [], "array2": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_number": [
|
||||
{ "expr": "false == 0", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "true == 1", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "false == NaN", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "true == 2", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_bool_string": [
|
||||
{ "expr": "false == ''", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "false == ' '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false == ' 0.0 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true == ' 1.0 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true == ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false == '-1'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "true == '2'", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_bool_null": [
|
||||
{
|
||||
"expr": "false == null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true == null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_object-array": [
|
||||
{
|
||||
"expr": "false == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_bool": [
|
||||
{ "expr": "0 == false", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "1 == true", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "NaN == false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "2 == true", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_number_string": [
|
||||
{
|
||||
"expr": "0 == ' -0.0 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "0 == ''", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "0 == ' '", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "120 == ' +1.2e2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "120.0 == ' 1.2E2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "120 == ' 1.2e+2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "-120 == ' -1.2E+2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0.012 == ' 1.2e-2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0.012 == ' 1.2E-2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "255.0 == ' 0xff '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "8.0 == ' 0o10 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity == ' Infinity '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity == ' -Infinity '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "NaN == 'NaN'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "1 == '0'", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "1 == 'a'", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_number_null": [
|
||||
{ "expr": "0 == null", "result": { "kind": "Boolean", "value": true } }
|
||||
],
|
||||
|
||||
"coerce_number_object-array": [
|
||||
{
|
||||
"expr": "0 == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_bool": [
|
||||
{ "expr": "'' == false", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "' ' == false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0.0 ' == false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.0 ' == true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' == true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'-1' == false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "'2' == true", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_string_number": [
|
||||
{
|
||||
"expr": "' -0.0 ' == 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "'' == 0", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "' ' == 0", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "' +1.2e2 ' == 120",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2E2 ' == 120.0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2e+2 ' == 120",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' -1.2E+2 ' == -120",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2e-2 ' == 0.012",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2E-2 ' == 0.012",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0xff ' == 255.0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0o10 ' == 8.0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' Infinity ' == Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' -Infinity ' == -Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'NaN' == NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{ "expr": "'0' == 1", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "'a' == 1", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_string_null": [
|
||||
{ "expr": "'' == null", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "' ' == null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "'0' == null", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "'false' == null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'null' == null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_object": [
|
||||
{
|
||||
"expr": "'' == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'false' == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'0' == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'null' == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Object' == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_array": [
|
||||
{
|
||||
"expr": "'' == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'false' == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'0' == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'null' == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Array' == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_bool": [
|
||||
{
|
||||
"expr": "null == false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "null == true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_number": [
|
||||
{ "expr": "null == 0", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "false == 1", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"coerce_null_string": [
|
||||
{ "expr": "null == ''", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "null == ' '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "null == '0'", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "null == 'false'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null == 'null'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_object-array": [
|
||||
{
|
||||
"expr": "null == test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null == test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"bool": [
|
||||
{
|
||||
"expr": "false > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"number": [
|
||||
{
|
||||
"expr": "1 > 2",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 > 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 > 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1.002 > 1.001",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"string": [
|
||||
{
|
||||
"expr": "'def' > 'abc'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'a' > 'b'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"array": [
|
||||
{
|
||||
"expr": "test > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"object": [
|
||||
{
|
||||
"expr": "test > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_number": [
|
||||
{
|
||||
"expr": "false > 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false > -11",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true > 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false > Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false > -Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true > -Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_string": [
|
||||
{
|
||||
"expr": "false > '0'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false > '-1'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true > '1'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > '0'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_null": [
|
||||
{
|
||||
"expr": "false > null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_object": [
|
||||
{
|
||||
"expr": "false > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_array": [
|
||||
{
|
||||
"expr": "false > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_bool": [
|
||||
{
|
||||
"expr": "1 > false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 > true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "NaN > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "NaN > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity > false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity > true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_string": [
|
||||
{
|
||||
"expr": "0 > ' 0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 > ' -1 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 > ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 > ' 0 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_null": [
|
||||
{
|
||||
"expr": "1 > null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 > null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_object": [
|
||||
{
|
||||
"expr": "0 > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_array": [
|
||||
{
|
||||
"expr": "0 > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_bool": [
|
||||
{
|
||||
"expr": "'0' > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'1' > false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'1' > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'2' > true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_number": [
|
||||
{
|
||||
"expr": "' 0 ' > -1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0 ' > 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' > 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' > 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_null": [
|
||||
{
|
||||
"expr": "'' > null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'1' > null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_object": [
|
||||
{
|
||||
"expr": "'' > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_array": [
|
||||
{
|
||||
"expr": "'' > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_bool": [
|
||||
{
|
||||
"expr": "null > false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null > true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_number": [
|
||||
{
|
||||
"expr": "null > 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null > -1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_string": [
|
||||
{
|
||||
"expr": "null > ' 0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null > ' -1 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_object": [
|
||||
{
|
||||
"expr": "null > test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_array": [
|
||||
{
|
||||
"expr": "null > test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"bool": [
|
||||
{
|
||||
"expr": "false >= true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false >= false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true >= false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true >= true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
{
|
||||
"array-index-coercion": [
|
||||
{
|
||||
"expr": "foo[null]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "String", "value": "zero" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[false]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "String", "value": "zero" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[true]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "String", "value": "one" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[' 0.0 ']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "String", "value": "zero" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[' 0x02 ']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "String", "value": "two" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[' asdf ']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[fromjson('[]')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[fromjson('{}')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
}
|
||||
],
|
||||
"array-index-out-of-range": [
|
||||
{
|
||||
"expr": "foo[-1]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[3]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[-Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[NaN]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
}
|
||||
],
|
||||
"object-case-insensitive": [
|
||||
{
|
||||
"expr": "foo['bAr']",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"BaR": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "baz" }
|
||||
}
|
||||
],
|
||||
"object-index-coercion": [
|
||||
{
|
||||
"expr": "foo[5]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"5": "it's 5"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's 5" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[.5]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"0.5": "it's 0.5"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's 0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[-.5]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"-0.5": "it's -0.5"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's -0.5" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[0x0f]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"15": "it's 15"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's 15" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[NaN]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"NaN": "it's NaN"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's NaN" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[Infinity]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Infinity": "it's Infinity"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[-Infinity]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"-Infinity": "it's -Infinity"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's -Infinity" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[null]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"": "it's null"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's null" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[true]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"true": "it's true"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's true" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[false]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"false": "it's false"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "it's false" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[fromJson('{}')]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Object": "It's Object"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[format('{0}', fromJson('{}'))]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Object": "It's Object"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "It's Object" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[fromJson('[]')]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Array": "It's Array"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo[format('{0}', fromJson('[]'))]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"Array": "It's Array"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "It's Array" }
|
||||
}
|
||||
],
|
||||
"object-index-out-of-range": [
|
||||
{
|
||||
"expr": "foo['']",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo['asdf']",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"bar": "baz"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Null", "value": null }
|
||||
}
|
||||
],
|
||||
"index-following-function": [
|
||||
{
|
||||
"expr": "fromJson('[\"one\", \"two\"]')[1]",
|
||||
"result": { "kind": "String", "value": "two" }
|
||||
}
|
||||
],
|
||||
"index-following-group": [
|
||||
{
|
||||
"expr": "(fromJson('[\"one\", \"two\"]'))[1]",
|
||||
"result": { "kind": "String", "value": "two" }
|
||||
}
|
||||
],
|
||||
"index-star": [
|
||||
{
|
||||
"expr": "foo[*]",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"one": "one val",
|
||||
"two": "two val",
|
||||
"*": "star val"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one val", "two val", "star val"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo['*']",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"one": "one val",
|
||||
"two": "two val",
|
||||
"*": "star val"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "String", "value": "star val" }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one", "two"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
+556
@@ -0,0 +1,556 @@
|
||||
{
|
||||
"filtered-array-from-literal": [
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": null
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": false
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": true
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": 123
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": "abc"
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
}
|
||||
],
|
||||
"filtered-array-from-array": [
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
"zero",
|
||||
"one"
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["zero", "one"] }
|
||||
}
|
||||
],
|
||||
"filtered-array-from-object": [
|
||||
{
|
||||
"expr": "foo.*",
|
||||
"contexts": {
|
||||
"foo": {
|
||||
"one": "one val",
|
||||
"two": "two val"
|
||||
}
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one val", "two val"] }
|
||||
}
|
||||
],
|
||||
"nested-object": [
|
||||
{
|
||||
"expr": "foo.*.one",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"one": "obj_1_prop_1",
|
||||
"two": "obj_1_prop_2"
|
||||
},
|
||||
{
|
||||
"one": "obj_2_prop_1",
|
||||
"two": "obj_2_prop_2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["obj_1_prop_1", "obj_2_prop_1"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*]['']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"": "empty string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["empty string"] }
|
||||
}
|
||||
],
|
||||
"nested-object-property-case-insensitive": [
|
||||
{
|
||||
"expr": "foo.*.thePROPERTY",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"THEproperty": "the property"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["the property"] }
|
||||
}
|
||||
],
|
||||
"nested-object-property-not-found": [
|
||||
{
|
||||
"expr": "foo.*.one",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"one": "obj_1_prop_1",
|
||||
"two": "obj_1_prop_2"
|
||||
},
|
||||
{
|
||||
"zero": "obj_2_prop_0",
|
||||
"three": "obj_2_prop_3"
|
||||
},
|
||||
{
|
||||
"one": "obj_3_prop_1",
|
||||
"two": "obj_3_prop_2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["obj_1_prop_1", "obj_3_prop_1"] }
|
||||
}
|
||||
],
|
||||
"nested-object-property-coercion": [
|
||||
{
|
||||
"expr": "foo[*][null]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"": "empty string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["empty string"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][false]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"false": "It's false"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["It's false"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][true]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"true": "It's true"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["It's true"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][0x0f]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"15": "fifteen"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["fifteen"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][NaN]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"NaN": "It's NaN"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["It's NaN"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"Infinity": "It's Infinity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["It's Infinity"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][-Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"-Infinity": "It's -Infinity"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["It's -Infinity"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][fromjson('[]')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"": "empty string",
|
||||
"Array": "It's array"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][fromjson('{}')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"": "empty string",
|
||||
"Object": "It's object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
}
|
||||
],
|
||||
"nested-array": [
|
||||
{
|
||||
"expr": "foo[*][1]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"ary_0_idx_0",
|
||||
"ary_0_idx_1",
|
||||
"ary_0_idx_2"
|
||||
],
|
||||
[
|
||||
"ary_1_idx_0",
|
||||
"ary_1_idx_1",
|
||||
"ary_1_idx_2"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["ary_0_idx_1", "ary_1_idx_1"] }
|
||||
}
|
||||
],
|
||||
"nested-array-index-not-found": [
|
||||
{
|
||||
"expr": "foo[*][1]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"ary_0_idx_0",
|
||||
"ary_0_idx_1",
|
||||
"ary_0_idx_2"
|
||||
],
|
||||
[
|
||||
"ary_1_idx_0"
|
||||
],
|
||||
[
|
||||
"ary_2_idx_0",
|
||||
"ary_2_idx_1",
|
||||
"ary_2_idx_2"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["ary_0_idx_1", "ary_2_idx_1"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][-1]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][NaN]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][Infinity]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
}
|
||||
],
|
||||
"nested-array-index-coercion": [
|
||||
{
|
||||
"expr": "foo[*][null]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["zero"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][false]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["zero"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][true]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*]['']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["zero"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*]['abc']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][' 0x01 ']",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["one"] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][fromjson('[]')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][fromjson('{}')]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
}
|
||||
],
|
||||
"double-star": [
|
||||
{
|
||||
"expr": "foo.*.*",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
123,
|
||||
"hello",
|
||||
[
|
||||
"zero",
|
||||
"one",
|
||||
"two"
|
||||
],
|
||||
{
|
||||
"p1": "v1",
|
||||
"p2": "v2",
|
||||
"p3": "v3"
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["zero", "one", "two", "v1", "v2", "v3"] }
|
||||
}
|
||||
],
|
||||
"double-property": [
|
||||
{
|
||||
"expr": "foo.*.p.nestedp",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
{
|
||||
"p": {
|
||||
"nestedp": "val 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"p": {
|
||||
"nestedp": "val 2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["val 1", "val 2"] }
|
||||
}
|
||||
],
|
||||
"double-index": [
|
||||
{
|
||||
"expr": "foo[*][1][2]",
|
||||
"contexts": {
|
||||
"foo": [
|
||||
[
|
||||
[
|
||||
"0_0_0",
|
||||
"0_0_1",
|
||||
"0_0_2",
|
||||
"0_0_3"
|
||||
],
|
||||
[
|
||||
"0_1_0",
|
||||
"0_1_1",
|
||||
"0_1_2",
|
||||
"0_1_3"
|
||||
],
|
||||
[
|
||||
"0_2_0",
|
||||
"0_2_1",
|
||||
"0_2_2",
|
||||
"0_2_3"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"1_0_0",
|
||||
"1_0_1",
|
||||
"1_0_2",
|
||||
"1_0_3"
|
||||
],
|
||||
[
|
||||
"1_1_0",
|
||||
"1_1_1",
|
||||
"1_1_2",
|
||||
"1_1_3"
|
||||
],
|
||||
[
|
||||
"1_2_0",
|
||||
"1_2_1",
|
||||
"1_2_2",
|
||||
"1_2_3"
|
||||
]
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": { "kind": "Array", "value": ["0_1_2", "1_1_2"] }
|
||||
}
|
||||
],
|
||||
"not-found-always-returns-empty-array": [
|
||||
{
|
||||
"expr": "foo[*][1]",
|
||||
"contexts": {
|
||||
"foo": []
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[*][1][2]",
|
||||
"contexts": {
|
||||
"foo": []
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*.bar",
|
||||
"contexts": {
|
||||
"foo": {}
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo.*.bar.baz",
|
||||
"contexts": {
|
||||
"foo": []
|
||||
},
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
{
|
||||
"bool": [
|
||||
{
|
||||
"expr": "false < true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"number": [
|
||||
{
|
||||
"expr": "1 < 2",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 < 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 < 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1.001 < 1.002",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"string": [
|
||||
{
|
||||
"expr": "'abc' < 'def'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'b' < 'a'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"array": [
|
||||
{
|
||||
"expr": "test < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"object": [
|
||||
{
|
||||
"expr": "test < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_number": [
|
||||
{
|
||||
"expr": "false < 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true < 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < 2",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < NaN",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false < Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true < Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false < -Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < -Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_string": [
|
||||
{
|
||||
"expr": "false < '0'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false < '1'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true < '1'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < '2'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_null": [
|
||||
{
|
||||
"expr": "false < null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_object": [
|
||||
{
|
||||
"expr": "false < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_array": [
|
||||
{
|
||||
"expr": "false < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_bool": [
|
||||
{
|
||||
"expr": "-1 < false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 < true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 < true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "NaN < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "NaN < true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity < true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity < false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity < true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_string": [
|
||||
{
|
||||
"expr": "0 < ' 0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 < ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 < ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 < ' 2 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_null": [
|
||||
{
|
||||
"expr": "-1 < null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 < null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_object": [
|
||||
{
|
||||
"expr": "0 < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-1 < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_array": [
|
||||
{
|
||||
"expr": "0 < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-1 < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_bool": [
|
||||
{
|
||||
"expr": "'0' < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'-1' < false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'1' < true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'0' < true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_number": [
|
||||
{
|
||||
"expr": "' 0 ' < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 0 ' < 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' < 2",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' < 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_null": [
|
||||
{
|
||||
"expr": "'' < null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'-1' < null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_object": [
|
||||
{
|
||||
"expr": "'' < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_array": [
|
||||
{
|
||||
"expr": "'' < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_bool": [
|
||||
{
|
||||
"expr": "null < false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null < true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_number": [
|
||||
{
|
||||
"expr": "null < 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_string": [
|
||||
{
|
||||
"expr": "null < ' 0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null < ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_object": [
|
||||
{
|
||||
"expr": "null < test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_array": [
|
||||
{
|
||||
"expr": "null < test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"bool": [
|
||||
{
|
||||
"expr": "false <= true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false <= false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true <= false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true <= true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
+597
@@ -0,0 +1,597 @@
|
||||
{
|
||||
"boolean": [
|
||||
{
|
||||
"expr": "true != true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true != true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"number": [
|
||||
{
|
||||
"expr": "0 != -0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 != 2",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "120 != 1.2e2",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 != 2",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1.001 != 1.002",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "NaN != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity != Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity != Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 != Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != Infinity",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"string": [
|
||||
{
|
||||
"expr": "'a' != 'a'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'a' != 'b'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"null": [
|
||||
{ "expr": "null != null", "result": { "kind": "Boolean", "value": false } }
|
||||
],
|
||||
|
||||
"object": [
|
||||
{
|
||||
"expr": "object1 != object1",
|
||||
"contexts": { "object1": {} },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "object1 != object2",
|
||||
"contexts": { "object1": {}, "object2": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"array": [
|
||||
{
|
||||
"expr": "array1 != array1",
|
||||
"contexts": { "array1": [] },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "array1 != array2",
|
||||
"contexts": { "array1": [], "array2": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_number": [
|
||||
{
|
||||
"expr": "false != 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != 2",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_string": [
|
||||
{
|
||||
"expr": "false != ''",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != ' '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != ' 0.0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != ' 1.0 '",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != ' 1.0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true != ' 1 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "false != '-1'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != '2'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_null": [
|
||||
{
|
||||
"expr": "false != null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "true != null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_bool_object-array": [
|
||||
{
|
||||
"expr": "false != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "false != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "true != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_bool": [
|
||||
{
|
||||
"expr": "0 != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 != false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 != true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "NaN != false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_string": [
|
||||
{
|
||||
"expr": "0 != ' -0.0 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 != ''",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 != ' '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "120 != ' +1.2e2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "120.0 != ' 1.2E2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "120 != ' 1.2e+2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-120 != ' -1.2E+2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0.012 != ' 1.2e-2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0.012 != ' 1.2E-2 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "255.0 != ' 0xff '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "8.0 != ' 0o10 '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "Infinity != ' Infinity '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "-Infinity != ' -Infinity '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "NaN != 'NaN'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != '0'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != '1'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 != 'a'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 != 'a'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_null": [
|
||||
{
|
||||
"expr": "0 != null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 != null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_number_object-array": [
|
||||
{
|
||||
"expr": "0 != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "1 != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_bool": [
|
||||
{
|
||||
"expr": "'' != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' ' != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 0.0 ' != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.0 ' != false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.0 ' != true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1 ' != true",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'-1' != true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'2' != true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_number": [
|
||||
{
|
||||
"expr": "' -0.0 ' != 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'' != 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' ' != 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' +1.2e2 ' != 120",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2E2 ' != 120.0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2e+2 ' != 120",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' -1.2E+2 ' != -120",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2e-2 ' != 0.012",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 1.2E-2 ' != 0.012",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 0xff ' != 255.0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' 0o10 ' != 8.0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' Infinity ' != Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' -Infinity ' != -Infinity",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'NaN' != NaN",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'0' != 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'1' != 1",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'a' != 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'a' != 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_null": [
|
||||
{
|
||||
"expr": "'' != null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "' ' != null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'0' != null",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'1' != null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'false' != null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'null' != null",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_object": [
|
||||
{
|
||||
"expr": "'' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'false' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'0' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'1' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'null' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Object' != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_string_array": [
|
||||
{
|
||||
"expr": "'' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'false' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'0' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'0' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'null' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Array' != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_bool": [
|
||||
{
|
||||
"expr": "null != false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null != true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_number": [
|
||||
{
|
||||
"expr": "null != 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null != 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_string": [
|
||||
{
|
||||
"expr": "null != ''",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null != ' '",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null != '0'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "null != '1'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "null != 'false'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "null != 'null'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"coerce_null_object-array": [
|
||||
{
|
||||
"expr": "null != test",
|
||||
"contexts": { "test": {} },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "null != test",
|
||||
"contexts": { "test": [] },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"not": [
|
||||
{ "expr": "!!true", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!false", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!1", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!.5", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!0.5", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!2", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!-1", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!-.5", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!-2", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!-Infinity", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!Infinity", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!NaN", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!0", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!0.0", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!-0", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!-0.0", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!'a'", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!'false'", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!'0'", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!' '", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!''", "result": { "kind": "Boolean", "value": false } },
|
||||
{ "expr": "!!fromJson('[]')", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!fromJson('{}')", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "!!null", "result": { "kind": "Boolean", "value": false } }
|
||||
]
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"or": [
|
||||
{
|
||||
"expr": "true || true || true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "true || true", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "true || true || false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{ "expr": "true || false", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "false || true", "result": { "kind": "Boolean", "value": true } },
|
||||
{
|
||||
"expr": "false || false",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "0 || 0 || 2 || 3",
|
||||
"result": { "kind": "Number", "value": 2.0 }
|
||||
},
|
||||
{ "expr": "false || 0", "result": { "kind": "Number", "value": 0.0 } },
|
||||
{
|
||||
"expr": "false || '' || 'a' || 'b'",
|
||||
"result": { "kind": "String", "value": "a" }
|
||||
},
|
||||
{ "expr": "false || ''", "result": { "kind": "String", "value": "" } },
|
||||
{ "expr": "null || true", "result": { "kind": "Boolean", "value": true } },
|
||||
{ "expr": "false || null", "result": { "kind": "Null", "value": null } }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
{
|
||||
"lower-vs-upper": [
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' == 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' != 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' < 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' <= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' > 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'abcdefghijklmnopqrstuvwxyz' >= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"upper-vs-lower": [
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' == 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' != 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' < 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' <= 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' > 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ABCDEFGHIJKLMNOPQRSTUVWXYZ' >= 'abcdefghijklmnopqrstuvwxyz'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"a-z-equivalent-to-A-Z-wrt-chars-between-Z-and-a": [
|
||||
{
|
||||
"expr": "'A' < '['",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'A' <= '['",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'A' > '['",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'A' >= '['",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'a' < '['",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'a' <= '['",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'a' > '['",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'a' >= '['",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||
{
|
||||
"expr": "'ς' == 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' != 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' < 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' <= 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' > 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' >= 'Σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' == 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' != 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' < 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' <= 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' > 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ς' >= 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' == 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' != 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' < 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' <= 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' > 'σ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Σ' >= 'σ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ü-Ü": [
|
||||
{
|
||||
"expr": "'ü' == 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ü' != 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ü' < 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ü' <= 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ü' > 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ü' >= 'Ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' == 'ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' != 'ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' < 'ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' <= 'ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' > 'ü'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ü' >= 'ü'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ç-Ç": [
|
||||
{
|
||||
"expr": "'ç' == 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ç' != 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ç' < 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ç' <= 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ç' > 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ç' >= 'Ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' == 'ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' != 'ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' < 'ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' <= 'ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' > 'ç'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'Ç' >= 'ç'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-i-İ": [
|
||||
{
|
||||
"expr": "'i' == 'İ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'i' != 'İ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'i' < 'İ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'i' <= 'İ'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'i' > 'İ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'i' >= 'İ'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' == 'i'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' != 'i'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' < 'i'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' <= 'i'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' > 'i'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'İ' >= 'i'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-ı-I": [
|
||||
{
|
||||
"expr": "'ı' == 'I'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ı' != 'I'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ı' < 'I'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ı' <= 'I'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'ı' > 'I'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'ı' >= 'I'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'I' == 'ı'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'I' != 'ı'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'I' < 'ı'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'I' <= 'ı'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "'I' > 'ı'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'I' >= 'ı'",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"cyrillic-letters": [
|
||||
{
|
||||
"expr": "'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ' == 'абвгдежзийклмнопрстуфхцчшщьэюя'",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"operator_precedence_._and_._evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "foo.bar.baz",
|
||||
"contexts": { "foo": { "bar": { "baz": 2 } } },
|
||||
"result": { "kind": "Number", "value": 2.0 }
|
||||
}
|
||||
],
|
||||
"operator_precedence_._is_higher_than_!": [
|
||||
{
|
||||
"expr": "!foo.bar",
|
||||
"contexts": { "foo": { "bar": true } },
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "(!foo).bar",
|
||||
"contexts": { "foo": { "bar": true } },
|
||||
"result": { "kind": "Null", "value": null }
|
||||
}
|
||||
],
|
||||
"operator_precedence_._is_higher_than_>": [
|
||||
{
|
||||
"expr": "3 > foo.bar",
|
||||
"contexts": { "foo": { "bar": 2 } },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "foo.bar > 2",
|
||||
"contexts": { "foo": { "bar": 3 } },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"operator_precedence_[]_is_higher_than_>": [
|
||||
{
|
||||
"expr": "3 > foo['bar']",
|
||||
"contexts": { "foo": { "bar": 2 } },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "foo['bar'] > 2",
|
||||
"contexts": { "foo": { "bar": 3 } },
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"operator_precedence_!_is_higher_than_<": [
|
||||
{
|
||||
"expr": "!2 < 3",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "!(2 < 3)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_<_and_<_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "3 < 2 < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "3 < (2 < 1)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_<_and_<=_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "3 < 2 <= 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "3 < (2 <= 1)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_<=_and_<_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "3 <= 2 < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "3 <= (2 < 1)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_>_and_<_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "0 > 0 < 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "0 > (0 < 1)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_>_and_>_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "2 > 2 > 0",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "2 > (2 > 0)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
"operator_precedence_<=_is_higher_than_==": [
|
||||
{
|
||||
"expr": "2 <= 3 == true",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "2 <= (3 == true)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_==_and_==_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "2 == 2 == 1",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "2 == (2 == 1)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_==_and_!=_evaluate_left_to_right": [
|
||||
{
|
||||
"expr": "2 == 2 != 0",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "2 == (2 != 0)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_==_is_higher_than_&&": [
|
||||
{
|
||||
"expr": "1 == 1 && 2",
|
||||
"result": { "kind": "Number", "value": 2.0 }
|
||||
},
|
||||
{
|
||||
"expr": "1 == (1 && 2)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "'a' == 'a' && 'b'",
|
||||
"result": { "kind": "String", "value": "b" }
|
||||
},
|
||||
{
|
||||
"expr": "'a' == ('a' && 'b')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
"operator_precedence_&&_is_higher_than_||": [
|
||||
{
|
||||
"expr": "false && 0 || null",
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "false && (0 || null)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "1 || 2 && 3",
|
||||
"result": { "kind": "Number", "value": 1.0 }
|
||||
},
|
||||
{
|
||||
"expr": "(1 || 2) && 3",
|
||||
"result": { "kind": "Number", "value": 3.0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"basics": [
|
||||
{
|
||||
"expr": "startsWith('abcdef', 'abc')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('abcdef', 'bc')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('abcdef', '')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('1234', 12)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('true', true)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('false', false)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('asdf', null)",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('1234', 23)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('true', false)",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('true', false, true)",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Too many parameters supplied: "
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('true')",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Too few parameters supplied: "
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-a-thru-z": [
|
||||
{
|
||||
"expr": "startsWith('ABCDEFGHIJKLMNOPQRSTUVWXYZ_asdf', 'abcdefghijklmnopqrstuvwxyz')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('abcdefghijklmnopqrstuvwxyz_asdf', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ς-(final-lowercase-sigma)-Σ-(capital-sigma)-σ-(non-final-sigma)": [
|
||||
{
|
||||
"expr": "startsWith('ς_asdf', 'Σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('ς_asdf', 'σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('Σ_asdf', 'ς')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('Σ_asdf', 'σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('σ_asdf', 'ς')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('σ_asdf', 'Σ')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ü-Ü": [
|
||||
{
|
||||
"expr": "startsWith('ü_asdf', 'Ü')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('Ü_asdf', 'ü')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-insensitive-ç-Ç": [
|
||||
{
|
||||
"expr": "startsWith('ç_asdf', 'Ç')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('Ç_asdf', 'ç')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-i-İ": [
|
||||
{
|
||||
"expr": "startsWith('i_asdf', 'İ')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('İ_asdf', 'i')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"case-sensitive-ı-I": [
|
||||
{
|
||||
"expr": "startsWith('ı_asdf', 'I')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
},
|
||||
{
|
||||
"expr": "startsWith('I_asdf', 'ı')",
|
||||
"result": { "kind": "Boolean", "value": false }
|
||||
}
|
||||
],
|
||||
|
||||
"cyrillic-letters": [
|
||||
{
|
||||
"expr": "startsWith('АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЭЮЯ_asdf', 'абвгдежзийклмнопрстуфхцчшщьэюя')",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
}
|
||||
]
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
{
|
||||
"parsing-errors": [
|
||||
{
|
||||
"expr": "1 <",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected end of expression: '<'. Located at position 3 within expression: 1 <"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 < )",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: ')'. Located at position 5 within expression: 1 < )"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "> 1",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '>'. Located at position 1 within expression: > 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github.()",
|
||||
"contexts": { "github": { "sha": "abc123" } },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '('. Located at position 8 within expression: github.()"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "endswith(1, 2 a)",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: 'a'. Located at position 15 within expression: endswith(1, 2 a)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "(1,",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: ','. Located at position 3 within expression: (1,"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 2",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 3 within expression: 1 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "(1)2",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 4 within expression: (1)2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(1)2",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 10 within expression: tojson(1)2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo 2",
|
||||
"contexts": { "foo": "bar" },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 5 within expression: foo 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo.bar 2",
|
||||
"contexts": { "foo": "bar" },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 9 within expression: foo.bar 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo['bar']2",
|
||||
"contexts": { "foo": "bar" },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected symbol: '2'. Located at position 11 within expression: foo['bar']2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "nonExistentFunction()",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized function: 'nonExistentFunction'. Located at position 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "false && nonExistentFunction()",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized function: 'nonExistentFunction'. Located at position 1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 = < 2",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '='. Located at position 3 within expression: 1 = < 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 & < 2",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '&'. Located at position 3 within expression: 1 & < 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 | < 2",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '|'. Located at position 3 within expression: 1 | < 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 =2 =3",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '=2'. Located at position 3 within expression: 1 =2 =3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 &2 &3",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '&2'. Located at position 3 within expression: 1 &2 &3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 |2 |3",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '|2'. Located at position 3 within expression: 1 |2 |3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 @abc 2",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '@abc'. Located at position 3 within expression: 1 @abc 2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "'foo",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: ''foo'. Located at position 1 within expression: 'foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "fromjson(a,b ",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'a'. Located at position 10 within expression: fromjson(a,b "
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": " true ||( false && false",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unexpected end of expression: 'false'. Located at position 20 within expression: true ||( false && false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "😊 Emoji!! (unicode)",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '😊'. Located at position 1 within expression: 😊 Emoji!! (unicode)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github.event_name == 'push'\n && !contains(env.COMMIT_MESSAGES)",
|
||||
"contexts": { "github": { "event_name": "push" }},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'env'. Located at position 43 within expression: github.event_name == 'push'\n && !contains(env.COMMIT_MESSAGES)"
|
||||
},
|
||||
"options": {
|
||||
"skip": ["typescript"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github.event_name == 'push' \n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||
"contexts": { "github": { "event_name": "push" }},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'env'. Located at position 51 within expression: github.event_name == 'push' \n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||
},
|
||||
"options": {
|
||||
"skip": ["typescript"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github.event_name == 'push' \n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||
"contexts": { "github": { "event_name": "push" }},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'env'. Located at position 59 within expression: github.event_name == 'push' \n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||
},
|
||||
"options": {
|
||||
"skip": ["typescript"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github.event_name == 'push' \n&& true\n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)",
|
||||
"contexts": { "github": { "event_name": "push" }},
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Unrecognized named-value: 'env'. Located at position 67 within expression: github.event_name == 'push' \n&& true\n&& true\n&& true\n&& !contains(env.COMMIT_MESSAGES)"
|
||||
},
|
||||
"options": {
|
||||
"skip": ["typescript"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "1 && ..github/workflow.md",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 6 within expression: 1 && ..github/workflow.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "..github/workflow.md",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 1 within expression: ..github/workflow.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": ".github/workflow.md",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '.github/workflow.md'. Located at position 1 within expression: .github/workflow.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": ".github/workflow",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '.github/workflow'. Located at position 1 within expression: .github/workflow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "github/workflow.md",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: 'github/workflow'. Located at position 1 within expression: github/workflow.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "..github/workflow.md && 1",
|
||||
"err": {
|
||||
"kind": "lexing",
|
||||
"value": "Unexpected symbol: '..github/workflow.md'. Located at position 1 within expression: ..github/workflow.md && 1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"depth-errors": [
|
||||
{
|
||||
"expr": "!!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! false",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "!!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!!! !!!! false",
|
||||
"result": { "kind": "Boolean", "value": true }
|
||||
},
|
||||
{
|
||||
"expr": "foo._1._2._3._4._5._6._7._8._9._10._11._12._13._14._15._16._17._18._19._20._21._22._23._24._25._26._27._28._29._30._31._32._33._34._35._36._37._38._39._40._41._42._43._44._45._46._47._48._49._50",
|
||||
"contexts": { "foo": null },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo._1._2._3._4._5._6._7._8._9._10._11._12._13._14._15._16._17._18._19._20._21._22._23._24._25._26._27._28._29._30._31._32._33._34._35._36._37._38._39._40._41._42._43._44._45._46._47._48._49",
|
||||
"contexts": { "foo": null },
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.*",
|
||||
"contexts": { "foo": null },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*.* .*.*.*.*",
|
||||
"contexts": { "foo": null },
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "foo[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33][34][35][36][37][38][39][40][41][42][43][44][45][46][47][48][49][50]",
|
||||
"contexts": { "foo": null },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo[1][2][3][4][5][6][7][8][9][10][11][12][13][14][15][16][17][18][19][20][21][22][23][24][25][26][27][28][29][30][31][32][33][34][35][36][37][38][39][40][41][42][43][44][45][46][47][48][49]",
|
||||
"contexts": { "foo": null },
|
||||
"result": { "kind": "Null", "value": null }
|
||||
},
|
||||
{
|
||||
"expr": "foo [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*]",
|
||||
"contexts": { "foo": null },
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "foo [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*][*] [*][*][*][*]",
|
||||
"contexts": { "foo": null },
|
||||
"result": { "kind": "Array", "value": [] }
|
||||
},
|
||||
{
|
||||
"expr": "fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( 1 )))))))))) )))))))))) )))))))))) )))))))))) ))))))))))",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson( fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson(tojson(fromjson( '1' ))))))))) )))))))))) )))))))))) )))))))))) ))))))))))",
|
||||
"result": { "kind": "Number", "value": 1 }
|
||||
},
|
||||
{
|
||||
"expr": "format('depth3'||format('depth5'||format('depth7'||format('depth9'||format('depth11'||format('depth13'||format('depth15'||format('depth17'||format('depth19'||format('depth21'||format('depth23'||format('depth25'||format('depth27'||format('depth29'||format('depth31'||format('depth33'||format('depth35'||format('depth37'||format('depth39'||format('depth41'||format('depth43'||format('depth45'||format('depth47'||format('depth49'||format('depth51'||'depth51')))))))))) )))))))))) )))))",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('depth3'||format('depth5'||format('depth7'||format('depth9'||format('depth11'||format('depth13'||format('depth15'||format('depth17'||format('depth19'||format('depth21'||format('depth23'||format('depth25'||format('depth27'||format('depth29'||format('depth31'||format('depth33'||format('depth35'||format('depth37'||format('depth39'||format('depth41'||format('depth43'||format('depth45'||format('depth47'||format('depth49'||format('depth50')))))))))) )))))))))) )))))",
|
||||
"result": { "kind": "String", "value": "depth3" }
|
||||
},
|
||||
{
|
||||
"expr": "format('depth3'&&format('depth5'&&format('depth7'&&format('depth9'&&format('depth11'&&format('depth13'&&format('depth15'&&format('depth17'&&format('depth19'&&format('depth21'&&format('depth23'&&format('depth25'&&format('depth27'&&format('depth29'&&format('depth31'&&format('depth33'&&format('depth35'&&format('depth37'&&format('depth39'&&format('depth41'&&format('depth43'&&format('depth45'&&format('depth47'&&format('depth49'&&format('depth51'&&'depth51')))))))))) )))))))))) )))))",
|
||||
"err": {
|
||||
"kind": "parsing",
|
||||
"value": "Exceeded max expression depth 50"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "format('depth3'&&format('depth5'&&format('depth7'&&format('depth9'&&format('depth11'&&format('depth13'&&format('depth15'&&format('depth17'&&format('depth19'&&format('depth21'&&format('depth23'&&format('depth25'&&format('depth27'&&format('depth29'&&format('depth31'&&format('depth33'&&format('depth35'&&format('depth37'&&format('depth39'&&format('depth41'&&format('depth43'&&format('depth45'&&format('depth47'&&format('depth49'&&format('depth50')))))))))) )))))))))) )))))",
|
||||
"result": { "kind": "String", "value": "depth50" }
|
||||
},
|
||||
{
|
||||
"expr": "1 || 2 || 3 || 4 || 5 || 6 || 7 || 8 || 9 || 10 || 11 || 12 || 13 || 14 || 15 || 16 || 17 || 18 || 19 || 20 || 21 || 22 || 23 || 24 || 25 || 26 || 27 || 28 || 29 || 30 || 31 || 32 || 33 || 34 || 35 || 36 || 37 || 38 || 39 || 40 || 41 || 42 || 43 || 44 || 45 || 46 || 47 || 48 || 49 || 50 || 51 || 52 || 53 || 54 || 55 || 56 || 57 || 58 || 59 || 60",
|
||||
"result": { "kind": "Number", "value": 1 }
|
||||
},
|
||||
{
|
||||
"expr": "1 && 2 && 3 && 4 && 5 && 6 && 7 && 8 && 9 && 10 && 11 && 12 && 13 && 14 && 15 && 16 && 17 && 18 && 19 && 20 && 21 && 22 && 23 && 24 && 25 && 26 && 27 && 28 && 29 && 30 && 31 && 32 && 33 && 34 && 35 && 36 && 37 && 38 && 39 && 40 && 41 && 42 && 43 && 44 && 45 && 46 && 47 && 48 && 49 && 50 && 51 && 52 && 53 && 54 && 55 && 56 && 57 && 58 && 59 && 60",
|
||||
"result": { "kind": "Number", "value": 60 }
|
||||
},
|
||||
{
|
||||
"expr": "1 && 2 || 3 && 4 || 5 && 6 || 7 && 8 || 9 && 10 || 11 && 12 || 13 && 14 || 15 && 16 || 17 && 18 || 19 && 20 || 21 && 22 || 23 && 24 || 25 && 26 || 27 && 28 || 29 && 30 || 31 && 32 || 33 && 34 || 35 && 36 || 37 && 38 || 39 && 40 || 41 && 42 || 43 && 44 || 45 && 46 || 47 && 48 || 49 && 50 || 51 && 52 || 53 && 54 || 55 && 56 || 57 && 58 || 59 && 60",
|
||||
"result": { "kind": "Number", "value": 2 }
|
||||
}
|
||||
],
|
||||
"memory-errors": [
|
||||
{
|
||||
"options": {
|
||||
"skip": ["typescript"]
|
||||
},
|
||||
"expr": "startswith(format('{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}', tojson(github)), format('{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}{0}', tojson(github)))",
|
||||
"contexts": {
|
||||
"github": {
|
||||
"ref": "refs/heads/main",
|
||||
"sha": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||
"repository": "monalisa/testing",
|
||||
"repository_owner": "monalisa",
|
||||
"repository_owner_id": "12102068",
|
||||
"repositoryUrl": "git://github.com/monalisa/testing.git",
|
||||
"run_id": "2827197126",
|
||||
"run_number": "845",
|
||||
"retention_days": "90",
|
||||
"run_attempt": "1",
|
||||
"artifact_cache_size_limit": "10",
|
||||
"repository_visibility": "public",
|
||||
"repository_id": "1",
|
||||
"actor_id": "1",
|
||||
"actor": "monalisa",
|
||||
"triggering_actor": "monalisa",
|
||||
"workflow": "CI",
|
||||
"head_ref": "",
|
||||
"base_ref": "",
|
||||
"event_name": "push",
|
||||
"event": {
|
||||
"after": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||
"base_ref": null,
|
||||
"before": "bbe29778fdad33232cd67bd84205d334b94412d7",
|
||||
"commits": [
|
||||
{
|
||||
"author": {
|
||||
"email": "monalisa@users.noreply.github.com",
|
||||
"name": "mona",
|
||||
"username": "monalisa"
|
||||
},
|
||||
"committer": {
|
||||
"email": "noreply@github.com",
|
||||
"name": "GitHub",
|
||||
"username": "web-flow"
|
||||
},
|
||||
"distinct": true,
|
||||
"id": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||
"message": "Update main.yml",
|
||||
"timestamp": "2022-08-09T12:40:12-05:00",
|
||||
"tree_id": "0dfd49aedd0cc103865d00587df3d23aa6f571f7",
|
||||
"url": "https://github.com/monalisa/testing/commit/2ce6b095f4cceb616efed52266e3e3c0367ba795"
|
||||
}
|
||||
],
|
||||
"compare": "https://github.com/monalisa/testing/compare/bbe29778fdad...2ce6b095f4cc",
|
||||
"created": false,
|
||||
"deleted": false,
|
||||
"forced": false,
|
||||
"head_commit": {
|
||||
"author": {
|
||||
"email": "monalisa@users.noreply.github.com",
|
||||
"name": "mona",
|
||||
"username": "monalisa"
|
||||
},
|
||||
"committer": {
|
||||
"email": "noreply@github.com",
|
||||
"name21": "GitHub",
|
||||
"username": "web-flow"
|
||||
},
|
||||
"distinct": true,
|
||||
"id": "2ce6b095f4cceb616efed52266e3e3c0367ba795",
|
||||
"message": "Update main.yml",
|
||||
"timestamp": "2022-08-09T12:40:12-05:00",
|
||||
"tree_id": "0dfd49aedd0cc103865d00587df3d23aa6f571f7",
|
||||
"url": "https://github.com/monalisa/testing/commit/2ce6b095f4cceb616efed52266e3e3c0367ba795"
|
||||
},
|
||||
"pusher": {
|
||||
"email": "monalisa@users.noreply.github.com",
|
||||
"name": "monalisa"
|
||||
},
|
||||
"ref": "refs/heads/main",
|
||||
"repository": {
|
||||
"allow_forking": true,
|
||||
"archive_url": "https://api.github.com/repos/monalisa/testing/{archive_format}{/ref}",
|
||||
"archived": false,
|
||||
"assignees_url": "https://api.github.com/repos/monalisa/testing/assignees{/user}",
|
||||
"blobs_url": "https://api.github.com/repos/monalisa/testing/git/blobs{/sha}",
|
||||
"branches_url": "https://api.github.com/repos/monalisa/testing/branches{/branch}",
|
||||
"clone_url": "https://github.com/monalisa/testing.git",
|
||||
"collaborators_url": "https://api.github.com/repos/monalisa/testing/collaborators{/collaborator}",
|
||||
"comments_url": "https://api.github.com/repos/monalisa/testing/comments{/number}",
|
||||
"commits_url": "https://api.github.com/repos/monalisa/testing/commits{/sha}",
|
||||
"compare_url": "https://api.github.com/repos/monalisa/testing/compare/{base}...{head}",
|
||||
"contents_url": "https://api.github.com/repos/monalisa/testing/contents/{+path}",
|
||||
"contributors_url": "https://api.github.com/repos/monalisa/testing/contributors",
|
||||
"created_at": 1516230842,
|
||||
"default_branch": "main",
|
||||
"deployments_url": "https://api.github.com/repos/monalisa/testing/deployments",
|
||||
"description": null,
|
||||
"disabled": false,
|
||||
"downloads_url": "https://api.github.com/repos/monalisa/testing/downloads",
|
||||
"events_url": "https://api.github.com/repos/monalisa/testing/events",
|
||||
"fork": false,
|
||||
"forks": 1,
|
||||
"forks_count": 1,
|
||||
"forks_url": "https://api.github.com/repos/monalisa/testing/forks",
|
||||
"full_name": "monalisa/testing",
|
||||
"git_commits_url": "https://api.github.com/repos/monalisa/testing/git/commits{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/monalisa/testing/git/refs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/monalisa/testing/git/tags{/sha}",
|
||||
"git_url": "git://github.com/monalisa/testing.git",
|
||||
"has_downloads": true,
|
||||
"has_issues": true,
|
||||
"has_pages": false,
|
||||
"has_projects": true,
|
||||
"has_wiki": true,
|
||||
"homepage": null,
|
||||
"hooks_url": "https://api.github.com/repos/monalisa/testing/hooks",
|
||||
"html_url": "https://github.com/monalisa/testing",
|
||||
"id": 117904191,
|
||||
"is_template": false,
|
||||
"issue_comment_url": "https://api.github.com/repos/monalisa/testing/issues/comments{/number}",
|
||||
"issue_events_url": "https://api.github.com/repos/monalisa/testing/issues/events{/number}",
|
||||
"issues_url": "https://api.github.com/repos/monalisa/testing/issues{/number}",
|
||||
"keys_url": "https://api.github.com/repos/monalisa/testing/keys{/key_id}",
|
||||
"labels_url": "https://api.github.com/repos/monalisa/testing/labels{/name}",
|
||||
"language": "Shell",
|
||||
"languages_url": "https://api.github.com/repos/monalisa/testing/languages",
|
||||
"license": null,
|
||||
"master_branch": "main",
|
||||
"merges_url": "https://api.github.com/repos/monalisa/testing/merges",
|
||||
"milestones_url": "https://api.github.com/repos/monalisa/testing/milestones{/number}",
|
||||
"mirror_url": null,
|
||||
"name": "testing",
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkxMTc5MDQxOTE=",
|
||||
"notifications_url": "https://api.github.com/repos/monalisa/testing/notifications{?since,all,participating}",
|
||||
"open_issues": 5,
|
||||
"open_issues_count": 5,
|
||||
"owner": {
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12102068?v=4",
|
||||
"email": "monalisa@users.noreply.github.com",
|
||||
"events_url": "https://api.github.com/users/monalisa/events{/privacy}",
|
||||
"followers_url": "https://api.github.com/users/monalisa/followers",
|
||||
"following_url": "https://api.github.com/users/monalisa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}",
|
||||
"gravatar_id": "",
|
||||
"html_url": "https://github.com/monalisa",
|
||||
"id": 12102068,
|
||||
"login": "monalisa",
|
||||
"name": "monalisa",
|
||||
"node_id": "MDQ6VXNlcjEyMTAyMDY4",
|
||||
"organizations_url": "https://api.github.com/users/monalisa/orgs",
|
||||
"received_events_url": "https://api.github.com/users/monalisa/received_events",
|
||||
"repos_url": "https://api.github.com/users/monalisa/repos",
|
||||
"site_admin": true,
|
||||
"starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/monalisa/subscriptions",
|
||||
"type": "User",
|
||||
"url": "https://api.github.com/users/monalisa"
|
||||
},
|
||||
"private": false,
|
||||
"pulls_url": "https://api.github.com/repos/monalisa/testing/pulls{/number}",
|
||||
"pushed_at": 1660066812,
|
||||
"releases_url": "https://api.github.com/repos/monalisa/testing/releases{/id}",
|
||||
"size": 611,
|
||||
"ssh_url": "git@github.com:monalisa/testing.git",
|
||||
"stargazers": 1,
|
||||
"stargazers_count": 1,
|
||||
"stargazers_url": "https://api.github.com/repos/monalisa/testing/stargazers",
|
||||
"statuses_url": "https://api.github.com/repos/monalisa/testing/statuses/{sha}",
|
||||
"subscribers_url": "https://api.github.com/repos/monalisa/testing/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/monalisa/testing/subscription",
|
||||
"svn_url": "https://github.com/monalisa/testing",
|
||||
"tags_url": "https://api.github.com/repos/monalisa/testing/tags",
|
||||
"teams_url": "https://api.github.com/repos/monalisa/testing/teams",
|
||||
"topics": [],
|
||||
"trees_url": "https://api.github.com/repos/monalisa/testing/git/trees{/sha}",
|
||||
"updated_at": "2022-01-05T02:14:32Z",
|
||||
"url": "https://github.com/monalisa/testing",
|
||||
"visibility": "public",
|
||||
"watchers": 1,
|
||||
"watchers_count": 1,
|
||||
"web_commit_signoff_required": false
|
||||
},
|
||||
"sender": {
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/12102068?v=4",
|
||||
"events_url": "https://api.github.com/users/monalisa/events{/privacy}",
|
||||
"followers_url": "https://api.github.com/users/monalisa/followers",
|
||||
"following_url": "https://api.github.com/users/monalisa/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/monalisa/gists{/gist_id}",
|
||||
"gravatar_id": "",
|
||||
"html_url": "https://github.com/monalisa",
|
||||
"id": 12102068,
|
||||
"login": "monalisa",
|
||||
"node_id": "MDQ6VXNlcjEyMTAyMDY4",
|
||||
"organizations_url": "https://api.github.com/users/monalisa/orgs",
|
||||
"received_events_url": "https://api.github.com/users/monalisa/received_events",
|
||||
"repos_url": "https://api.github.com/users/monalisa/repos",
|
||||
"site_admin": true,
|
||||
"starred_url": "https://api.github.com/users/monalisa/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/monalisa/subscriptions",
|
||||
"type": "User",
|
||||
"url": "https://api.github.com/users/monalisa"
|
||||
}
|
||||
},
|
||||
"server_url": "https://github.com",
|
||||
"api_url": "https://api.github.com",
|
||||
"graphql_url": "https://api.github.com/graphql",
|
||||
"ref_name": "main",
|
||||
"ref_protected": false,
|
||||
"ref_type": "branch",
|
||||
"secret_source": "Actions",
|
||||
"workspace": "/home/runner/work/testing/testing",
|
||||
"action": "__run_2",
|
||||
"event_path": "/home/runner/work/_temp/_github_workflow/event.json",
|
||||
"action_repository": "",
|
||||
"action_ref": "",
|
||||
"path": "/home/runner/work/_temp/_runner_file_commands/add_path_04146e5b-5a26-490d-b886-886641aa5651",
|
||||
"env": "/home/runner/work/_temp/_runner_file_commands/set_env_04146e5b-5a26-490d-b886-886641aa5651",
|
||||
"step_summary": "/home/runner/work/_temp/_runner_file_commands/step_summary_04146e5b-5a26-490d-b886-886641aa5651"
|
||||
}
|
||||
},
|
||||
"err": {
|
||||
"kind": "evaluation",
|
||||
"value": "The maximum allowed memory size was exceeded"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"basics": [
|
||||
{
|
||||
"expr": "tojson(null)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "null"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(true)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(false)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(0)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(-0)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(123456789)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "123456789"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(-123456789)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "-123456789"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(1234.5)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "1234.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(-1234.5)",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "-1234.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson('')",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "\"\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson('abc')",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "\"abc\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson('abc''def')",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "\"abc'def\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson('abc\\\"def')",
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "\"abc\\\\\\\"def\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(emptyArray)",
|
||||
"contexts": {
|
||||
"emptyArray": []
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "[]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(emptyObject)",
|
||||
"contexts": {
|
||||
"emptyObject": {}
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "{}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"arrays": [
|
||||
{
|
||||
"expr": "tojson(myArray)",
|
||||
"contexts": {
|
||||
"myArray": []
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "[]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(myArray)",
|
||||
"contexts": {
|
||||
"myArray": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "[\n 1,\n 2,\n 3\n]"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(myArray)",
|
||||
"contexts": {
|
||||
"myArray": [
|
||||
[
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
[
|
||||
"abc",
|
||||
"def",
|
||||
"ghi"
|
||||
],
|
||||
[
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
[],
|
||||
{}
|
||||
]
|
||||
]
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "[\n [\n 1,\n 2,\n 3\n ],\n [\n \"abc\",\n \"def\",\n \"ghi\"\n ],\n [\n true,\n false,\n null,\n [],\n {}\n ]\n]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"object": [
|
||||
{
|
||||
"expr": "tojson(myObject)",
|
||||
"contexts": {
|
||||
"myObject": {}
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "{}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(myObject)",
|
||||
"contexts": {
|
||||
"myObject": {
|
||||
"one": "value one",
|
||||
"two" : "value two",
|
||||
"three": "value three"
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "{\n \"one\": \"value one\",\n \"two\": \"value two\",\n \"three\": \"value three\"\n}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"expr": "tojson(myObject)",
|
||||
"contexts": {
|
||||
"myObject": {
|
||||
"nested-one": {
|
||||
"one": 1,
|
||||
"two": 2,
|
||||
"three": 3
|
||||
},
|
||||
"nested-two": {
|
||||
"string one": "value one",
|
||||
"string two": "value two",
|
||||
"string three": "value three"
|
||||
},
|
||||
"nested-three": {
|
||||
"true": true,
|
||||
"false": false,
|
||||
"null": null,
|
||||
"array": [],
|
||||
"object": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"result": {
|
||||
"kind": "String",
|
||||
"value": "{\n \"nested-one\": {\n \"one\": 1,\n \"two\": 2,\n \"three\": 3\n },\n \"nested-two\": {\n \"string one\": \"value one\",\n \"string two\": \"value two\",\n \"string three\": \"value three\"\n },\n \"nested-three\": {\n \"true\": true,\n \"false\": false,\n \"null\": null,\n \"array\": [],\n \"object\": {}\n }\n}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"include": ["src"],
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"watchOptions": {
|
||||
"watchFile": "useFsEvents",
|
||||
"watchDirectory": "useFsEvents",
|
||||
"synchronousWatchDirectory": true,
|
||||
"excludeDirectories": ["**/node_modules", "dist"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"root": true,
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/dist
|
||||
/node_modules
|
||||
@@ -0,0 +1,2 @@
|
||||
@github:registry=https://npm.pkg.github.com
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/dist
|
||||
/node_modules
|
||||
/src/workflows/workflow-v1.0.json
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"semi": false
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Jest Tests",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"runtimeArgs": [
|
||||
"--inspect-brk",
|
||||
"${workspaceRoot}/node_modules/jest/bin/jest.js",
|
||||
"--runInBand"
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"internalConsoleOptions": "neverOpen",
|
||||
"env": {
|
||||
"NODE_OPTIONS": "--experimental-vm-modules"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
export default {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
moduleNameMapper: {
|
||||
"^(\\.{1,2}/.*)\\.js$": "$1",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"name": "@github/actions-workflow-parser",
|
||||
"version": "0.0.42",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./*": {
|
||||
"import": "./dist/*.js"
|
||||
}
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
".": [
|
||||
"dist/index.d.ts"
|
||||
],
|
||||
"*": [
|
||||
"dist/*.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/github/actions-workflow-parser/"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --build tsconfig.build.json",
|
||||
"clean": "rimraf dist",
|
||||
"format": "prettier --write '**/*.ts'",
|
||||
"format-check": "prettier --check '**/*.ts'",
|
||||
"lint": "eslint **/*.ts",
|
||||
"lint-fix": "eslint --fix **/*.ts",
|
||||
"prepublishOnly": "npm run build && npm run test",
|
||||
"test": "NODE_OPTIONS=\"--experimental-vm-modules\" jest",
|
||||
"test-xlang": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --testPathPattern xlang",
|
||||
"test-watch": "NODE_OPTIONS=\"--experimental-vm-modules\" jest --watch",
|
||||
"watch": "tsc --build tsconfig.build.json --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/actions-expressions": "*",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"eslint": "7.32.0",
|
||||
"jest": "^29.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.0.3",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import { StringToken } from "./templates/tokens"
|
||||
import { isBasicExpression, isString } from "./templates/tokens/type-guards"
|
||||
import { nullTrace } from "./test-utils/null-trace"
|
||||
import { parseWorkflow } from "./workflows/workflow-parser"
|
||||
|
||||
describe("Workflow Expression Parsing", () => {
|
||||
it("preserves original expressions when building format", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
run-name: Test \${{ github.event_name }} \${{ github.ref }}
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo 'hello'`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||
|
||||
const run = result.value!.assertMapping("run")!
|
||||
const runNameMapping = run.get(1)!
|
||||
expect(runNameMapping?.key?.assertString("run-name key").value).toBe(
|
||||
"run-name"
|
||||
)
|
||||
|
||||
const v = runNameMapping.value!
|
||||
expect(v).not.toBeUndefined()
|
||||
|
||||
if (!isBasicExpression(v)) {
|
||||
throw new Error("expected run-name to be a basic expression")
|
||||
}
|
||||
|
||||
expect(v.originalExpressions).toHaveLength(2)
|
||||
expect(v.originalExpressions!.map((x) => x.toDisplayString())).toEqual([
|
||||
"${{ github.event_name }}",
|
||||
"${{ github.ref }}",
|
||||
])
|
||||
})
|
||||
|
||||
it("preserves original expressions when building format for multi-line strings", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo \${{ github.event_name }}
|
||||
echo 'hello' \${{ github.ref }}`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||
|
||||
const run = result.value!.assertMapping("run")!
|
||||
const jobsMapping = run.get(1)!
|
||||
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs")
|
||||
|
||||
const job = jobsMapping
|
||||
.value!.assertMapping("jobs")!
|
||||
.get(0)!
|
||||
.value!.assertMapping("job")
|
||||
const stepRun = job
|
||||
.get(1)
|
||||
.value!.assertSequence("steps")
|
||||
.get(0)
|
||||
.assertMapping("step")
|
||||
.get(0)
|
||||
.value!.assertScalar("step-run")
|
||||
|
||||
if (!isBasicExpression(stepRun)) {
|
||||
throw new Error("expected run-name to be a basic expression")
|
||||
}
|
||||
|
||||
expect(stepRun.originalExpressions).toHaveLength(2)
|
||||
expect(
|
||||
stepRun.originalExpressions!.map((x) => [x.toDisplayString(), x.range])
|
||||
).toEqual([
|
||||
[
|
||||
"${{ github.event_name }}",
|
||||
{
|
||||
start: [7, 16],
|
||||
end: [7, 40],
|
||||
},
|
||||
],
|
||||
[
|
||||
"${{ github.ref }}",
|
||||
{
|
||||
start: [8, 24],
|
||||
end: [8, 41],
|
||||
},
|
||||
],
|
||||
])
|
||||
})
|
||||
|
||||
it("return errors and string token with preserved expressions for (multiple) expression errors", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo \${{ abc }}
|
||||
echo 'hello' \${{ gith }}`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(2)
|
||||
|
||||
const run = result.value!.assertMapping("run")!
|
||||
const jobsMapping = run.get(1)!
|
||||
expect(jobsMapping?.key?.assertString("jobs").value).toBe("jobs")
|
||||
|
||||
const job = jobsMapping
|
||||
.value!.assertMapping("jobs")!
|
||||
.get(0)!
|
||||
.value!.assertMapping("job")
|
||||
const stepRun = job
|
||||
.get(1)
|
||||
.value!.assertSequence("steps")
|
||||
.get(0)
|
||||
.assertMapping("step")
|
||||
.get(0)
|
||||
.value!.assertScalar("step-run")
|
||||
|
||||
expect(isString(stepRun)).toBe(true)
|
||||
expect((stepRun as StringToken).value).toContain("${{")
|
||||
})
|
||||
|
||||
it("reports all errors for multi-line expressions at the correct locations", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo \${{ fromJSON2('test') }}
|
||||
echo 'hello' \${{ toJSON2(inputs.test) }}`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toEqual([
|
||||
{
|
||||
prefix: "test.yaml (Line: 6, Col: 14)",
|
||||
range: {
|
||||
start: [7, 16],
|
||||
end: [7, 40],
|
||||
},
|
||||
rawMessage: "Unrecognized function: 'fromJSON2'",
|
||||
},
|
||||
{
|
||||
prefix: "test.yaml (Line: 6, Col: 14)",
|
||||
range: {
|
||||
start: [8, 24],
|
||||
end: [8, 51],
|
||||
},
|
||||
rawMessage: "Unrecognized function: 'toJSON2'",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it("parses isExpression strings into expression tokens", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
steps:
|
||||
- run: echo 'hello'`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||
|
||||
const workflowRoot = result.value!.assertMapping("root")!
|
||||
const jobs = workflowRoot.get(1).value.assertMapping("jobs")
|
||||
const build = jobs.get(0).value.assertMapping("job")
|
||||
const ifToken = build.get(1).value
|
||||
expect(ifToken.toString()).toEqual("${{ github.event_name == 'push' }}")
|
||||
|
||||
if (!isBasicExpression(ifToken)) {
|
||||
throw new Error("expected if to be a basic expression")
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,121 @@
|
||||
import { TemplateValidationError } from "./templates/template-validation-error"
|
||||
import { nullTrace } from "./test-utils/null-trace"
|
||||
import { parseWorkflow } from "./workflows/workflow-parser"
|
||||
|
||||
describe("parseWorkflow", () => {
|
||||
it("parses valid workflow", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content:
|
||||
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'",
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("contains range for error", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content:
|
||||
"on: push\njobs:\n build:\n steps:\n - run: echo 'hello'",
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toEqual([
|
||||
new TemplateValidationError(
|
||||
"Required property is missing: runs-on",
|
||||
"test.yaml (Line: 4, Col: 5)",
|
||||
undefined,
|
||||
{
|
||||
start: [4, 5],
|
||||
end: [5, 24],
|
||||
}
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
it("error range for expression is constrained to scalar node", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: \${{ github.event = 12 }}
|
||||
run: echo 'hello'`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toEqual([
|
||||
new TemplateValidationError(
|
||||
"Unexpected symbol: '='. Located at position 14 within expression: github.event = 12",
|
||||
"test.yaml (Line: 6, Col: 13)",
|
||||
undefined,
|
||||
{
|
||||
start: [6, 13],
|
||||
end: [6, 37],
|
||||
}
|
||||
),
|
||||
])
|
||||
})
|
||||
|
||||
it("tokens contain descriptions", () => {
|
||||
const result = parseWorkflow(
|
||||
"test.yaml",
|
||||
[
|
||||
{
|
||||
name: "test.yaml",
|
||||
content:
|
||||
"on: push\nname: hello\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - run: echo 'hello'",
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0)
|
||||
expect(result.value).not.toBeUndefined()
|
||||
const root = result.value!.assertMapping("root")
|
||||
expect(root.description).toBe("Workflow file with strict validation")
|
||||
for (const pair of root) {
|
||||
const key = pair.key.assertString("key").value
|
||||
switch (key) {
|
||||
case "name": {
|
||||
const nameKey = pair.key.assertString("name")
|
||||
expect(nameKey.definition).not.toBeUndefined()
|
||||
expect(nameKey.description).toBe("The name of the workflow.")
|
||||
break
|
||||
}
|
||||
case "on": {
|
||||
const onKey = pair.key.assertString("on")
|
||||
const onValue = pair.value.assertString("push")
|
||||
expect(onKey.definition).not.toBeUndefined()
|
||||
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."
|
||||
)
|
||||
expect(onValue.definition).not.toBeUndefined()
|
||||
expect(onValue.description).toBe(
|
||||
"Runs your workflow when you push a commit or tag."
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
export { convertWorkflowTemplate } from "./model/convert"
|
||||
export { WorkflowTemplate } from "./model/workflow-template"
|
||||
export * from "./templates/tokens/type-guards"
|
||||
export { NoOperationTraceWriter, TraceWriter } from "./templates/trace-writer"
|
||||
export { parseWorkflow, ParseWorkflowResult } from "./workflows/workflow-parser"
|
||||
@@ -0,0 +1,348 @@
|
||||
import { nullTrace } from "../test-utils/null-trace"
|
||||
import { parseWorkflow } from "../workflows/workflow-parser"
|
||||
import { convertWorkflowTemplate, ErrorPolicy } from "./convert"
|
||||
|
||||
function serializeTemplate(template: unknown): unknown {
|
||||
return JSON.parse(JSON.stringify(template))
|
||||
}
|
||||
|
||||
describe("convertWorkflowTemplate", () => {
|
||||
it("converts workflow with one job", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
push: {},
|
||||
},
|
||||
jobs: [
|
||||
{
|
||||
id: "build",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "build",
|
||||
needs: undefined,
|
||||
outputs: undefined,
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts workflow if expressions", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
if: \${{ true }}
|
||||
runs-on: ubuntu-latest
|
||||
deploy:
|
||||
if: true
|
||||
runs-on: ubuntu-latest`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
events: {
|
||||
push: {},
|
||||
},
|
||||
jobs: [
|
||||
{
|
||||
id: "build",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "build",
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
{
|
||||
id: "deploy",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "deploy",
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts workflow with empty needs", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:
|
||||
needs: # comment to preserve whitespace in test
|
||||
runs-on: ubuntu-latest`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
{
|
||||
Message: "wf.yaml (Line: 4, Col: 12): Unexpected value ''",
|
||||
},
|
||||
],
|
||||
events: {
|
||||
push: {},
|
||||
},
|
||||
jobs: [
|
||||
{
|
||||
id: "build",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "build",
|
||||
needs: [],
|
||||
outputs: undefined,
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts workflow with needs errors", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
job1:
|
||||
needs: [unknown-job, job3]
|
||||
runs-on: ubuntu-latest
|
||||
job2:
|
||||
runs-on: ubuntu-latest
|
||||
job3:
|
||||
needs: job1
|
||||
runs-on: ubuntu-latest`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
{
|
||||
Message:
|
||||
"wf.yaml (Line: 4, Col: 13): Job 'job1' depends on unknown job 'unknown-job'.",
|
||||
},
|
||||
{
|
||||
Message:
|
||||
"wf.yaml (Line: 4, Col: 26): Job 'job1' depends on job 'job3' which creates a cycle in the dependency graph.",
|
||||
},
|
||||
{
|
||||
Message:
|
||||
"wf.yaml (Line: 9, Col: 12): Job 'job3' depends on job 'job1' which creates a cycle in the dependency graph.",
|
||||
},
|
||||
],
|
||||
events: {
|
||||
push: {},
|
||||
},
|
||||
jobs: [
|
||||
{
|
||||
id: "job1",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "job1",
|
||||
needs: ["unknown-job", "job3"],
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
{
|
||||
id: "job2",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "job2",
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
{
|
||||
id: "job3",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "job3",
|
||||
needs: ["job1"],
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [],
|
||||
type: "job",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts workflow with invalid on", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test:
|
||||
options: 123
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo hello`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(template.jobs).not.toBeUndefined()
|
||||
expect(template.jobs).toHaveLength(1)
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
{
|
||||
Message: "wf.yaml (Line: 5, Col: 18): Unexpected value '123'",
|
||||
},
|
||||
{
|
||||
Message:
|
||||
"wf.yaml (Line: 5, Col: 18): Unexpected type 'NumberToken' encountered while reading 'input options'. The type 'SequenceToken' was expected.",
|
||||
},
|
||||
],
|
||||
events: {},
|
||||
jobs: [
|
||||
{
|
||||
id: "build",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
name: "build",
|
||||
needs: undefined,
|
||||
outputs: undefined,
|
||||
"runs-on": "ubuntu-latest",
|
||||
steps: [
|
||||
{
|
||||
id: "__run",
|
||||
if: {
|
||||
expr: "success()",
|
||||
type: 3,
|
||||
},
|
||||
run: "echo hello",
|
||||
},
|
||||
],
|
||||
type: "job",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("converts workflow with invalid jobs", () => {
|
||||
const result = parseWorkflow(
|
||||
"wf.yaml",
|
||||
[
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on: push
|
||||
jobs:
|
||||
build:`,
|
||||
},
|
||||
],
|
||||
nullTrace
|
||||
)
|
||||
|
||||
const template = convertWorkflowTemplate(
|
||||
result.context,
|
||||
result.value!,
|
||||
ErrorPolicy.TryConversion
|
||||
)
|
||||
|
||||
expect(template.jobs).not.toBeUndefined()
|
||||
expect(template.jobs).toHaveLength(0)
|
||||
expect(serializeTemplate(template)).toEqual({
|
||||
errors: [
|
||||
{
|
||||
Message: "wf.yaml (Line: 3, Col: 9): Unexpected value ''",
|
||||
},
|
||||
{
|
||||
Message:
|
||||
"wf.yaml (Line: 3, Col: 9): Unexpected type 'NullToken' encountered while reading 'job build'. The type 'MappingToken' was expected.",
|
||||
},
|
||||
],
|
||||
events: {
|
||||
push: {},
|
||||
},
|
||||
jobs: [],
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,80 @@
|
||||
import { TemplateContext } from "../templates/template-context"
|
||||
import {
|
||||
TemplateToken,
|
||||
TemplateTokenError,
|
||||
} from "../templates/tokens/template-token"
|
||||
import { convertConcurrency } from "./converter/concurrency"
|
||||
import { convertOn } from "./converter/events"
|
||||
import { handleTemplateTokenErrors } from "./converter/handle-errors"
|
||||
import { convertJobs } from "./converter/jobs"
|
||||
import { WorkflowTemplate } from "./workflow-template"
|
||||
|
||||
export enum ErrorPolicy {
|
||||
ReturnErrorsOnly,
|
||||
TryConversion,
|
||||
}
|
||||
|
||||
export function convertWorkflowTemplate(
|
||||
context: TemplateContext,
|
||||
root: TemplateToken,
|
||||
errorPolicy: ErrorPolicy = ErrorPolicy.ReturnErrorsOnly
|
||||
): WorkflowTemplate {
|
||||
const result = {} as WorkflowTemplate
|
||||
|
||||
if (
|
||||
context.errors.getErrors().length > 0 &&
|
||||
errorPolicy === ErrorPolicy.ReturnErrorsOnly
|
||||
) {
|
||||
result.errors = context.errors.getErrors().map((x) => ({
|
||||
Message: x.message,
|
||||
}))
|
||||
return result
|
||||
}
|
||||
|
||||
try {
|
||||
const rootMapping = root.assertMapping("root")
|
||||
|
||||
for (const item of rootMapping) {
|
||||
const key = item.key.assertString("root key")
|
||||
|
||||
switch (key.value) {
|
||||
case "on":
|
||||
result.events = handleTemplateTokenErrors(root, context, {}, () =>
|
||||
convertOn(context, item.value)
|
||||
)
|
||||
break
|
||||
|
||||
case "jobs":
|
||||
result.jobs = handleTemplateTokenErrors(root, context, [], () =>
|
||||
convertJobs(context, item.value)
|
||||
)
|
||||
break
|
||||
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(root, context, {}, () =>
|
||||
convertConcurrency(context, item.value)
|
||||
)
|
||||
result.concurrency = item.value
|
||||
break
|
||||
case "env":
|
||||
result.env = item.value
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof TemplateTokenError) {
|
||||
context.error(err.token, err)
|
||||
} else {
|
||||
// Report error for the root node
|
||||
context.error(root, err)
|
||||
}
|
||||
} finally {
|
||||
if (context.errors.getErrors().length > 0) {
|
||||
result.errors = context.errors.getErrors().map((x) => ({
|
||||
Message: x.message,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||
import { isString } from "../../templates/tokens/type-guards"
|
||||
import { ConcurrencySetting } from "../workflow-template"
|
||||
|
||||
export function convertConcurrency(
|
||||
context: TemplateContext,
|
||||
token: TemplateToken
|
||||
): ConcurrencySetting {
|
||||
const result: ConcurrencySetting = {}
|
||||
|
||||
if (token.isExpression) {
|
||||
return result
|
||||
}
|
||||
if (isString(token)) {
|
||||
result.group = token
|
||||
return result
|
||||
}
|
||||
const concurrencyProperty = token.assertMapping("concurrency group")
|
||||
for (const property of concurrencyProperty) {
|
||||
const propertyName = property.key.assertString("concurrency group key")
|
||||
if (property.key.isExpression || property.value.isExpression) {
|
||||
continue
|
||||
}
|
||||
switch (propertyName.value) {
|
||||
case "group":
|
||||
result.group = property.value.assertString("concurrency group")
|
||||
break
|
||||
case "cancel-in-progress":
|
||||
result.cancelInProgress =
|
||||
property.value.assertBoolean("cancel-in-progress").value
|
||||
break
|
||||
default:
|
||||
context.error(
|
||||
propertyName,
|
||||
`Invalid property name: ${propertyName.value}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import {
|
||||
MappingToken,
|
||||
SequenceToken,
|
||||
StringToken,
|
||||
TemplateToken,
|
||||
} from "../../templates/tokens"
|
||||
import { isString } from "../../templates/tokens/type-guards"
|
||||
import { Container, Credential } from "../workflow-template"
|
||||
|
||||
export function convertToJobContainer(
|
||||
context: TemplateContext,
|
||||
container: TemplateToken
|
||||
): Container | undefined {
|
||||
let image: StringToken | undefined
|
||||
let env: MappingToken | undefined
|
||||
let ports: SequenceToken | undefined
|
||||
let volumes: SequenceToken | undefined
|
||||
let options: StringToken | undefined
|
||||
|
||||
// Skip validation for expressions for now to match
|
||||
// behavior of the other parsers
|
||||
for (const [_, token, __] of TemplateToken.traverse(container)) {
|
||||
if (token.isExpression) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isString(container)) {
|
||||
// Workflow uses shorthand syntax `container: image-name`
|
||||
image = container.assertString("container item")
|
||||
return { image: image }
|
||||
}
|
||||
|
||||
const mapping = container.assertMapping("container item")
|
||||
if (mapping)
|
||||
for (const item of mapping) {
|
||||
const key = item.key.assertString("container item key")
|
||||
const value = item.value
|
||||
|
||||
switch (key.value) {
|
||||
case "image":
|
||||
image = value.assertString("container image")
|
||||
break
|
||||
case "credentials":
|
||||
convertToJobCredentials(context, value)
|
||||
break
|
||||
case "env":
|
||||
env = value.assertMapping("container env")
|
||||
for (const envItem of env) {
|
||||
envItem.key.assertString("container env value")
|
||||
}
|
||||
break
|
||||
case "ports":
|
||||
ports = value.assertSequence("container ports")
|
||||
for (const port of ports) {
|
||||
port.assertString("container port")
|
||||
}
|
||||
break
|
||||
case "volumes":
|
||||
volumes = value.assertSequence("container volumes")
|
||||
for (const volume of volumes) {
|
||||
volume.assertString("container volume")
|
||||
}
|
||||
break
|
||||
case "options":
|
||||
options = value.assertString("container options")
|
||||
break
|
||||
default:
|
||||
context.error(key, `Unexpected container item key: ${key.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (!image) {
|
||||
context.error(container, "Container image cannot be empty")
|
||||
} else {
|
||||
return { image, env, ports, volumes, options }
|
||||
}
|
||||
}
|
||||
|
||||
export function convertToJobServices(
|
||||
context: TemplateContext,
|
||||
services: TemplateToken
|
||||
): Container[] | undefined {
|
||||
const serviceList: Container[] = []
|
||||
|
||||
const mapping = services.assertMapping("services")
|
||||
for (const service of mapping) {
|
||||
service.key.assertString("service key")
|
||||
const container = convertToJobContainer(context, service.value)
|
||||
if (container) {
|
||||
serviceList.push(container)
|
||||
}
|
||||
}
|
||||
return serviceList
|
||||
}
|
||||
|
||||
function convertToJobCredentials(
|
||||
context: TemplateContext,
|
||||
value: TemplateToken
|
||||
): Credential | undefined {
|
||||
const mapping = value.assertMapping("credentials")
|
||||
|
||||
let username: StringToken | undefined
|
||||
let password: StringToken | undefined
|
||||
|
||||
for (const item of mapping) {
|
||||
const key = item.key.assertString("credentials item")
|
||||
const value = item.value
|
||||
|
||||
switch (key.value) {
|
||||
case "username":
|
||||
username = value.assertString("credentials username")
|
||||
break
|
||||
case "password":
|
||||
password = value.assertString("credentials password")
|
||||
break
|
||||
default:
|
||||
context.error(key, `credentials key ${key.value}`)
|
||||
}
|
||||
}
|
||||
|
||||
return { username, password }
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import { MappingToken } from "../../templates/tokens/mapping-token"
|
||||
import { SequenceToken } from "../../templates/tokens/sequence-token"
|
||||
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||
import {
|
||||
isLiteral,
|
||||
isMapping,
|
||||
isSequence,
|
||||
isString,
|
||||
} from "../../templates/tokens/type-guards"
|
||||
import { TokenType } from "../../templates/tokens/types"
|
||||
import {
|
||||
BranchFilterConfig,
|
||||
EventsConfig,
|
||||
PathFilterConfig,
|
||||
ScheduleConfig,
|
||||
TagFilterConfig,
|
||||
TypesFilterConfig,
|
||||
WorkflowFilterConfig,
|
||||
} from "../workflow-template"
|
||||
import { convertStringList } from "./string-list"
|
||||
import { convertEventWorkflowDispatchInputs } from "./workflow-dispatch"
|
||||
|
||||
export function convertOn(
|
||||
context: TemplateContext,
|
||||
token: TemplateToken
|
||||
): EventsConfig {
|
||||
if (isLiteral(token)) {
|
||||
const event = token.assertString("on")
|
||||
|
||||
return {
|
||||
[event.value]: {},
|
||||
} as EventsConfig
|
||||
}
|
||||
|
||||
if (isSequence(token)) {
|
||||
const result = {} as EventsConfig
|
||||
|
||||
for (const item of token) {
|
||||
const event = item.assertString("on")
|
||||
result[event.value] = {}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
if (isMapping(token)) {
|
||||
const result = {} as EventsConfig
|
||||
|
||||
for (const item of token) {
|
||||
const eventKey = item.key.assertString("event name")
|
||||
const eventName = eventKey.value
|
||||
|
||||
if (item.value.templateTokenType === TokenType.Null) {
|
||||
result[eventName] = {}
|
||||
continue
|
||||
}
|
||||
|
||||
// Schedule is the only event that can be a sequence, handle that separately
|
||||
if (eventName === "schedule") {
|
||||
const scheduleToken = item.value.assertSequence(`event ${eventName}`)
|
||||
result.schedule = convertSchedule(context, scheduleToken)
|
||||
continue
|
||||
}
|
||||
|
||||
// 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.
|
||||
const eventToken = item.value.assertMapping(`event ${eventName}`)
|
||||
|
||||
result[eventName] = {
|
||||
...convertPatternFilter("branches", eventToken),
|
||||
...convertPatternFilter("tags", eventToken),
|
||||
...convertPatternFilter("paths", eventToken),
|
||||
...convertFilter("types", eventToken),
|
||||
...convertFilter("workflows", eventToken),
|
||||
// TODO - share input parsing for now, but workflow_call also needs outputs and secrets
|
||||
...convertEventWorkflowDispatchInputs(context, eventToken),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
context.error(token, "Invalid format for 'on'")
|
||||
return {}
|
||||
}
|
||||
|
||||
function convertPatternFilter<
|
||||
T extends BranchFilterConfig & TagFilterConfig & PathFilterConfig
|
||||
>(name: "branches" | "tags" | "paths", token: MappingToken): T {
|
||||
const result = {} as T
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString(`${name} filter key`)
|
||||
|
||||
switch (key.value) {
|
||||
case name:
|
||||
if (isString(item.value)) {
|
||||
result[name] = [item.value.value]
|
||||
} else {
|
||||
result[name] = convertStringList(
|
||||
name,
|
||||
item.value.assertSequence(`${name} list`)
|
||||
)
|
||||
}
|
||||
break
|
||||
|
||||
case `${name}-ignore`:
|
||||
if (isString(item.value)) {
|
||||
result[`${name}-ignore`] = [item.value.value]
|
||||
} else {
|
||||
result[`${name}-ignore`] = convertStringList(
|
||||
`${name}-ignore`,
|
||||
item.value.assertSequence(`${name}-ignore list`)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig>(
|
||||
name: "types" | "workflows",
|
||||
token: MappingToken
|
||||
): T {
|
||||
const result = {} as T
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString(`${name} filter key`)
|
||||
|
||||
switch (key.value) {
|
||||
case name:
|
||||
if (isString(item.value)) {
|
||||
result[name] = [item.value.value]
|
||||
} else {
|
||||
result[name] = convertStringList(
|
||||
name,
|
||||
item.value.assertSequence(`${name} list`)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function convertSchedule(
|
||||
context: TemplateContext,
|
||||
token: SequenceToken
|
||||
): ScheduleConfig[] | undefined {
|
||||
const result = [] as ScheduleConfig[]
|
||||
for (const item of token) {
|
||||
const mappingToken = item.assertMapping(`event schedule`)
|
||||
if (mappingToken.count == 1) {
|
||||
const schedule = mappingToken.get(0)
|
||||
const scheduleKey = schedule.key.assertString(`schedule key`)
|
||||
if (scheduleKey.value == "cron") {
|
||||
const cron = schedule.value.assertString(`schedule cron`)
|
||||
// Validate the cron string
|
||||
if (
|
||||
!cron.value.match(/((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})/)
|
||||
) {
|
||||
context.error(cron, "Invalid cron string")
|
||||
}
|
||||
result.push({ cron: cron.value })
|
||||
} else {
|
||||
context.error(scheduleKey, `Invalid schedule key`)
|
||||
}
|
||||
} else {
|
||||
context.error(mappingToken, "Invalid format for 'schedule'")
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import {
|
||||
TemplateToken,
|
||||
TemplateTokenError,
|
||||
} from "../../templates/tokens/template-token"
|
||||
|
||||
export function handleTemplateTokenErrors<TResult>(
|
||||
root: TemplateToken,
|
||||
context: TemplateContext,
|
||||
defaultValue: TResult,
|
||||
f: () => TResult
|
||||
): TResult {
|
||||
let r: TResult = defaultValue
|
||||
|
||||
try {
|
||||
r = f()
|
||||
} catch (err) {
|
||||
if (err instanceof TemplateTokenError) {
|
||||
context.error(err.token, err)
|
||||
} else {
|
||||
// Report error for the root node
|
||||
context.error(root, err)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { IdBuilder } from "./id-builder"
|
||||
|
||||
function build(...segments: string[]): string {
|
||||
const builder = new IdBuilder()
|
||||
for (const segment of segments) {
|
||||
builder.appendSegment(segment)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
describe("ID Builder", () => {
|
||||
it("builds IDs", () => {
|
||||
expect(build("one")).toEqual("one")
|
||||
|
||||
expect(build("one", "two")).toEqual("one_two")
|
||||
|
||||
expect(build("one", "two", "three")).toEqual("one_two_three")
|
||||
})
|
||||
|
||||
it("empty builder", () => {
|
||||
const builder = new IdBuilder()
|
||||
expect(builder.build()).toEqual("job")
|
||||
})
|
||||
|
||||
it("ignores empty segments", () => {
|
||||
expect(build("", "one")).toEqual("one")
|
||||
|
||||
expect(build("one", "", "two", "")).toEqual("one_two")
|
||||
})
|
||||
|
||||
it("handles illegal characters", () => {
|
||||
const builder = new IdBuilder()
|
||||
builder.appendSegment("hello world!")
|
||||
expect(builder.build()).toEqual("hello_world_")
|
||||
})
|
||||
|
||||
it("handles illegal leading characters", () => {
|
||||
expect(build("!hello")).toEqual("_hello")
|
||||
|
||||
expect(build("!hello", "!world")).toEqual("_hello__world")
|
||||
|
||||
expect(build("!@world", "!@world")).toEqual("__world___world")
|
||||
|
||||
expect(build("123")).toEqual("_123")
|
||||
|
||||
expect(build("123", "456")).toEqual("_123_456")
|
||||
|
||||
expect(build("-abc")).toEqual("_-abc")
|
||||
|
||||
expect(build("-abc", "-def")).toEqual("_-abc_-def")
|
||||
})
|
||||
|
||||
it("allows legal characters", () => {
|
||||
expect(build("abyzABYZ0189_-")).toEqual("abyzABYZ0189_-")
|
||||
})
|
||||
|
||||
it("allows legal leading characters", () => {
|
||||
expect(build("abc")).toEqual("abc")
|
||||
|
||||
expect(build("bcd")).toEqual("bcd")
|
||||
|
||||
expect(build("zyx")).toEqual("zyx")
|
||||
|
||||
expect(build("yxw")).toEqual("yxw")
|
||||
|
||||
expect(build("ABCD")).toEqual("ABCD")
|
||||
|
||||
expect(build("BCDE")).toEqual("BCDE")
|
||||
|
||||
expect(build("ZYXW")).toEqual("ZYXW")
|
||||
|
||||
expect(build("YXWV")).toEqual("YXWV")
|
||||
|
||||
expect(build("_abc")).toEqual("_abc")
|
||||
})
|
||||
|
||||
it("errors for max collisions", () => {
|
||||
const builder = new IdBuilder()
|
||||
builder.appendSegment("abc")
|
||||
builder.appendSegment("def")
|
||||
expect(builder.build()).toEqual("abc_def")
|
||||
|
||||
for (let i = 2; i < 1000; i++) {
|
||||
builder.appendSegment("abc")
|
||||
builder.appendSegment("def")
|
||||
expect(builder.build()).toEqual(`abc_def_${i}`)
|
||||
}
|
||||
|
||||
builder.appendSegment("abc")
|
||||
builder.appendSegment("def")
|
||||
expect(() => builder.build()).toThrowError("Unable to create a unique name")
|
||||
})
|
||||
|
||||
it("takes suffix into account for max length", () => {
|
||||
const builder = new IdBuilder()
|
||||
|
||||
const name =
|
||||
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(name)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_2"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_3"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_4"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_5"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_6"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_7"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_8"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_2345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678_9"
|
||||
)
|
||||
|
||||
builder.appendSegment(name)
|
||||
expect(builder.build()).toEqual(
|
||||
"_234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567_10"
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
const SEPARATOR = "_"
|
||||
const MAX_ATTEMPTS = 1000
|
||||
const MAX_LENGTH = 100
|
||||
|
||||
export class IdBuilder {
|
||||
private name: string[] = []
|
||||
private readonly distinctNames: Set<string> = new Set()
|
||||
|
||||
public appendSegment(value: string) {
|
||||
if (value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.name.length == 0) {
|
||||
const first = value[0]
|
||||
if (this.isAlpha(first) || first == "_") {
|
||||
// Legal first char
|
||||
} else if (this.isNumeric(first) || first == "-") {
|
||||
// Illegal first char, but legal char.
|
||||
// Prepend "_".
|
||||
this.name.push("_")
|
||||
} else {
|
||||
// Illegal char
|
||||
}
|
||||
} else {
|
||||
// Separator
|
||||
this.name.push(SEPARATOR)
|
||||
}
|
||||
|
||||
for (const c of value) {
|
||||
{
|
||||
if (this.isAlphaNumeric(c) || c == "_" || c == "-") {
|
||||
// Legal
|
||||
this.name.push(c)
|
||||
} else {
|
||||
// Illegal
|
||||
this.name.push(SEPARATOR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public build(): string {
|
||||
const original = this.name.length > 0 ? this.name.join("") : "job"
|
||||
let suffix = ""
|
||||
for (let attempt = 1; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
if (attempt === 1) {
|
||||
suffix = ""
|
||||
} else {
|
||||
suffix = "_" + attempt
|
||||
}
|
||||
|
||||
const candidate =
|
||||
original.substring(
|
||||
0,
|
||||
Math.min(original.length, MAX_LENGTH - suffix.length)
|
||||
) + suffix
|
||||
|
||||
if (!this.distinctNames.has(candidate)) {
|
||||
this.distinctNames.add(candidate)
|
||||
this.name = []
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("Unable to create a unique name")
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a known identifier to the set of distinct ids.
|
||||
* @param value The value to add
|
||||
* @returns An error if the value is invalid, otherwise undefined
|
||||
*/
|
||||
public tryAddKnownId(value: string): string | undefined {
|
||||
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.`
|
||||
}
|
||||
|
||||
if (value.startsWith(SEPARATOR + SEPARATOR)) {
|
||||
return `The identifier '${value}' is invalid. IDs starting with '__' are reserved.`
|
||||
}
|
||||
|
||||
if (this.distinctNames.has(value)) {
|
||||
return `The identifier '${value}' may not be used more than once within the same scope.`
|
||||
}
|
||||
|
||||
this.distinctNames.add(value)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* A name is valid if it starts with a letter or underscore, and contains only
|
||||
* letters, numbers, underscores, and hyphens.
|
||||
* @param name The string name to validate
|
||||
* @returns Whether the name is valid
|
||||
*/
|
||||
private isValid(name: string): boolean {
|
||||
let first = true
|
||||
for (const c of name) {
|
||||
if (first) {
|
||||
first = false
|
||||
if (!this.isAlpha(c) && c != "_") {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!this.isAlphaNumeric(c) && c != "_" && c != "-") {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private isAlphaNumeric(c: string): boolean {
|
||||
return this.isAlpha(c) || this.isNumeric(c)
|
||||
}
|
||||
|
||||
private isNumeric(c: string): boolean {
|
||||
return c >= "0" && c <= "9"
|
||||
}
|
||||
|
||||
private isAlpha(c: string): boolean {
|
||||
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { TemplateContext } from "../../../templates/template-context"
|
||||
import { TemplateToken } from "../../../templates/tokens/template-token"
|
||||
import { isScalar } from "../../../templates/tokens/type-guards"
|
||||
import { ActionsEnvironmentReference } from "../../workflow-template"
|
||||
|
||||
export function convertToActionsEnvironmentRef(
|
||||
context: TemplateContext,
|
||||
token: TemplateToken
|
||||
): ActionsEnvironmentReference {
|
||||
const result: ActionsEnvironmentReference = {}
|
||||
|
||||
if (token.isExpression) {
|
||||
return result
|
||||
}
|
||||
|
||||
if (isScalar(token)) {
|
||||
result.name = token
|
||||
return result
|
||||
}
|
||||
|
||||
const environmentMapping = token.assertMapping("job environment")
|
||||
|
||||
for (const property of environmentMapping) {
|
||||
const propertyName = property.key.assertString("job environment key")
|
||||
if (property.key.isExpression || property.value.isExpression) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch (propertyName.value) {
|
||||
case "name":
|
||||
result.name = property.value.assertScalar("job environment name key")
|
||||
break
|
||||
|
||||
case "url":
|
||||
result.url = property.value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import {
|
||||
BasicExpressionToken,
|
||||
MappingToken,
|
||||
StringToken,
|
||||
} from "../../templates/tokens"
|
||||
import { TemplateToken } from "../../templates/tokens/template-token"
|
||||
import {
|
||||
isMapping,
|
||||
isSequence,
|
||||
isString,
|
||||
} 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 = {
|
||||
name: string
|
||||
needs: StringToken[]
|
||||
}
|
||||
|
||||
export function convertJobs(
|
||||
context: TemplateContext,
|
||||
token: TemplateToken
|
||||
): Job[] {
|
||||
if (isMapping(token)) {
|
||||
const result: Job[] = []
|
||||
const jobsWithSatisfiedNeeds: nodeInfo[] = []
|
||||
const alljobsWithUnsatisfiedNeeds: nodeInfo[] = []
|
||||
|
||||
for (const item of token) {
|
||||
const jobKey = item.key.assertString("job name")
|
||||
const jobDef = item.value.assertMapping(`job ${jobKey.value}`)
|
||||
|
||||
const job = handleTemplateTokenErrors(token, context, undefined, () =>
|
||||
convertJob(context, jobKey, jobDef)
|
||||
)
|
||||
if (job) {
|
||||
result.push(job)
|
||||
const node = {
|
||||
name: job.id.value,
|
||||
needs: Object.assign([], job.needs),
|
||||
}
|
||||
if (node.needs.length > 0) {
|
||||
alljobsWithUnsatisfiedNeeds.push(node)
|
||||
} else {
|
||||
jobsWithSatisfiedNeeds.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//validate job needs
|
||||
validateNeeds(
|
||||
token,
|
||||
context,
|
||||
result,
|
||||
jobsWithSatisfiedNeeds,
|
||||
alljobsWithUnsatisfiedNeeds
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
context.error(token, "Invalid format for jobs")
|
||||
return []
|
||||
}
|
||||
|
||||
function validateNeeds(
|
||||
token: TemplateToken,
|
||||
context: TemplateContext,
|
||||
result: Job[],
|
||||
jobsWithSatisfiedNeeds: nodeInfo[],
|
||||
alljobsWithUnsatisfiedNeeds: nodeInfo[]
|
||||
) {
|
||||
if (jobsWithSatisfiedNeeds.length == 0) {
|
||||
context.error(
|
||||
token,
|
||||
"The workflow must contain at least one job with no dependencies."
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Figure out which nodes would start after current completes
|
||||
while (jobsWithSatisfiedNeeds.length > 0) {
|
||||
const currentJob = jobsWithSatisfiedNeeds.shift()
|
||||
if (currentJob == undefined) {
|
||||
break
|
||||
}
|
||||
for (let i = alljobsWithUnsatisfiedNeeds.length - 1; i >= 0; i--) {
|
||||
const unsatisfiedJob = alljobsWithUnsatisfiedNeeds[i]
|
||||
for (let j = unsatisfiedJob.needs.length - 1; j >= 0; j--) {
|
||||
const need = unsatisfiedJob.needs[j]
|
||||
if (need.value == currentJob.name) {
|
||||
unsatisfiedJob.needs.splice(j, 1)
|
||||
if (unsatisfiedJob.needs.length == 0) {
|
||||
jobsWithSatisfiedNeeds.push(unsatisfiedJob)
|
||||
alljobsWithUnsatisfiedNeeds.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether some jobs will never execute
|
||||
if (alljobsWithUnsatisfiedNeeds.length > 0) {
|
||||
const jobNames = result.map((x) => x.id.value)
|
||||
for (const unsatisfiedJob of alljobsWithUnsatisfiedNeeds) {
|
||||
for (const need of unsatisfiedJob.needs) {
|
||||
if (jobNames.includes(need.value)) {
|
||||
context.error(
|
||||
need,
|
||||
`Job '${unsatisfiedJob.name}' depends on job '${need.value}' which creates a cycle in the dependency graph.`
|
||||
)
|
||||
} else {
|
||||
context.error(
|
||||
need,
|
||||
`Job '${unsatisfiedJob.name}' depends on unknown job '${need.value}'.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertJob(
|
||||
context: TemplateContext,
|
||||
jobKey: StringToken,
|
||||
token: MappingToken
|
||||
): Job {
|
||||
const error = new IdBuilder().tryAddKnownId(jobKey.value)
|
||||
if (error) {
|
||||
context.error(jobKey, error)
|
||||
}
|
||||
const result: Job = {
|
||||
type: "job",
|
||||
id: jobKey,
|
||||
name: undefined,
|
||||
needs: undefined,
|
||||
if: new BasicExpressionToken(
|
||||
undefined,
|
||||
undefined,
|
||||
"success()",
|
||||
undefined,
|
||||
undefined
|
||||
),
|
||||
env: undefined,
|
||||
concurrency: undefined,
|
||||
environment: undefined,
|
||||
strategy: undefined,
|
||||
"runs-on": undefined,
|
||||
container: undefined,
|
||||
services: undefined,
|
||||
outputs: undefined,
|
||||
steps: [],
|
||||
}
|
||||
|
||||
for (const item of token) {
|
||||
const propertyName = item.key.assertString("job property name")
|
||||
switch (propertyName.value) {
|
||||
case "concurrency":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertConcurrency(context, item.value)
|
||||
)
|
||||
result.concurrency = item.value
|
||||
break
|
||||
|
||||
case "container":
|
||||
// Do early validation, but don't convert
|
||||
convertToJobContainer(context, item.value)
|
||||
result.container = item.value
|
||||
break
|
||||
|
||||
case "env":
|
||||
result.env = item.value.assertMapping("job env")
|
||||
break
|
||||
|
||||
case "environment":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertToActionsEnvironmentRef(context, item.value)
|
||||
)
|
||||
result.environment = item.value
|
||||
break
|
||||
|
||||
case "name":
|
||||
result.name = item.value.assertScalar("job name")
|
||||
break
|
||||
|
||||
case "needs":
|
||||
result.needs = []
|
||||
if (isString(item.value)) {
|
||||
const jobNeeds = item.value.assertString("job needs id")
|
||||
result.needs.push(jobNeeds)
|
||||
}
|
||||
|
||||
if (isSequence(item.value)) {
|
||||
for (const seqItem of item.value) {
|
||||
const jobNeeds = seqItem.assertString("job needs id")
|
||||
result.needs.push(jobNeeds)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
case "outputs":
|
||||
result.outputs = item.value.assertMapping("job outputs")
|
||||
break
|
||||
|
||||
case "runs-on":
|
||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||
convertRunsOn(context, item.value)
|
||||
)
|
||||
result["runs-on"] = item.value
|
||||
break
|
||||
|
||||
case "services":
|
||||
// Do early validation, but don't convert
|
||||
convertToJobServices(context, item.value)
|
||||
result.services = item.value
|
||||
break
|
||||
|
||||
case "steps":
|
||||
result.steps = convertSteps(context, item.value)
|
||||
break
|
||||
|
||||
case "strategy":
|
||||
result.strategy = item.value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.name) {
|
||||
result.name = result.id
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type RunsOn = {
|
||||
labels: Set<string>
|
||||
group: string
|
||||
}
|
||||
|
||||
function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
||||
const labels = convertRunsOnLabels(token)
|
||||
|
||||
if (!isMapping(token)) {
|
||||
return {
|
||||
labels,
|
||||
group: "",
|
||||
}
|
||||
}
|
||||
|
||||
let group = ""
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString("job runs-on property name")
|
||||
switch (key.value) {
|
||||
case "group": {
|
||||
if (item.value.isExpression) {
|
||||
continue
|
||||
}
|
||||
|
||||
const groupName = item.value.assertString(
|
||||
"job runs-on group name"
|
||||
).value
|
||||
const names = groupName.split("/")
|
||||
switch (names.length) {
|
||||
case 1: {
|
||||
group = groupName
|
||||
break
|
||||
}
|
||||
case 2: {
|
||||
if (
|
||||
!["org", "organization", "ent", "enterprise"].includes(names[0])
|
||||
) {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (!names[1]) {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}'.`
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
group = groupName
|
||||
break
|
||||
}
|
||||
default: {
|
||||
context.error(
|
||||
item.value,
|
||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "labels": {
|
||||
const mapLabels = convertRunsOnLabels(item.value)
|
||||
for (const label of mapLabels) {
|
||||
labels.add(label)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
labels,
|
||||
group,
|
||||
}
|
||||
}
|
||||
|
||||
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
||||
const labels = new Set<string>()
|
||||
if (token.isExpression) {
|
||||
return labels
|
||||
}
|
||||
|
||||
if (isString(token)) {
|
||||
labels.add(token.value)
|
||||
return labels
|
||||
}
|
||||
|
||||
if (isSequence(token)) {
|
||||
for (const item of token) {
|
||||
if (item.isExpression) {
|
||||
continue
|
||||
}
|
||||
|
||||
const label = item.assertString("job runs-on label sequence item")
|
||||
labels.add(label.value)
|
||||
}
|
||||
}
|
||||
|
||||
return labels
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import {
|
||||
BasicExpressionToken,
|
||||
MappingToken,
|
||||
ScalarToken,
|
||||
StringToken,
|
||||
TemplateToken,
|
||||
} 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(
|
||||
context: TemplateContext,
|
||||
steps: TemplateToken
|
||||
): Step[] {
|
||||
if (!isSequence(steps)) {
|
||||
context.error(steps, "Invalid format for steps")
|
||||
return []
|
||||
}
|
||||
|
||||
const idBuilder = new IdBuilder()
|
||||
|
||||
const result: Step[] = []
|
||||
for (const item of steps) {
|
||||
const step = handleTemplateTokenErrors(steps, context, undefined, () =>
|
||||
convertStep(context, idBuilder, item)
|
||||
)
|
||||
if (step) {
|
||||
result.push(step)
|
||||
}
|
||||
}
|
||||
|
||||
for (const step of result) {
|
||||
if (step.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
let id = ""
|
||||
if (isActionStep(step)) {
|
||||
id = createActionStepId(step)
|
||||
}
|
||||
|
||||
if (!id) {
|
||||
id = "run"
|
||||
}
|
||||
|
||||
idBuilder.appendSegment(`__${id}`)
|
||||
step.id = idBuilder.build()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function convertStep(
|
||||
context: TemplateContext,
|
||||
idBuilder: IdBuilder,
|
||||
step: TemplateToken
|
||||
): Step | undefined {
|
||||
const mapping = step.assertMapping("steps item")
|
||||
|
||||
let run: ScalarToken | undefined
|
||||
let id: StringToken | undefined
|
||||
let name: ScalarToken | undefined
|
||||
let uses: StringToken | undefined
|
||||
let continueOnError: boolean | undefined
|
||||
let env: MappingToken | undefined
|
||||
const ifCondition = new BasicExpressionToken(
|
||||
undefined,
|
||||
undefined,
|
||||
"success()",
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
for (const item of mapping) {
|
||||
const key = item.key.assertString("steps item key")
|
||||
switch (key.value) {
|
||||
case "id":
|
||||
id = item.value.assertString("steps item id")
|
||||
if (id) {
|
||||
const error = idBuilder.tryAddKnownId(id.value)
|
||||
if (error) {
|
||||
context.error(id, error)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "name":
|
||||
name = item.value.assertScalar("steps item name")
|
||||
break
|
||||
case "run":
|
||||
run = item.value.assertScalar("steps item run")
|
||||
break
|
||||
case "uses":
|
||||
uses = item.value.assertString("steps item uses")
|
||||
break
|
||||
case "env":
|
||||
env = item.value.assertMapping("step env")
|
||||
break
|
||||
case "continue-on-error":
|
||||
continueOnError = item.value.assertBoolean(
|
||||
"steps item continue-on-error"
|
||||
).value
|
||||
}
|
||||
}
|
||||
|
||||
if (run) {
|
||||
return {
|
||||
id: id?.value || "",
|
||||
name,
|
||||
if: ifCondition,
|
||||
"continue-on-error": continueOnError,
|
||||
env,
|
||||
run,
|
||||
}
|
||||
}
|
||||
|
||||
if (uses) {
|
||||
return {
|
||||
id: id?.value || "",
|
||||
name,
|
||||
if: ifCondition,
|
||||
"continue-on-error": continueOnError,
|
||||
env,
|
||||
uses,
|
||||
}
|
||||
}
|
||||
context.error(step, "Expected uses or run to be defined")
|
||||
}
|
||||
|
||||
function createActionStepId(step: ActionStep): string {
|
||||
const uses = step.uses.value
|
||||
if (uses.startsWith("docker://")) {
|
||||
return uses.substring("docker://".length)
|
||||
}
|
||||
|
||||
if (uses.startsWith("./") || uses.startsWith(".\\")) {
|
||||
return "self"
|
||||
}
|
||||
|
||||
const segments = uses.split("@")
|
||||
if (segments.length != 2) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const pathSegments = segments[0].split(/[\\/]/).filter((s) => s.length > 0)
|
||||
const gitRef = segments[1]
|
||||
|
||||
if (
|
||||
pathSegments.length >= 2 &&
|
||||
pathSegments[0] &&
|
||||
pathSegments[1] &&
|
||||
gitRef
|
||||
) {
|
||||
return `${pathSegments[0]}/${pathSegments[1]}`
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { SequenceToken } from "../../templates/tokens/sequence-token"
|
||||
|
||||
export function convertStringList(
|
||||
name: string,
|
||||
token: SequenceToken
|
||||
): string[] {
|
||||
const result = [] as string[]
|
||||
|
||||
for (const item of token) {
|
||||
result.push(item.assertString(`${name} item`).value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { TemplateContext } from "../../templates/template-context"
|
||||
import { MappingToken } from "../../templates/tokens/mapping-token"
|
||||
import { ScalarToken } from "../../templates/tokens/scalar-token"
|
||||
import {
|
||||
InputConfig,
|
||||
InputType,
|
||||
WorkflowDispatchConfig,
|
||||
} from "../workflow-template"
|
||||
import { convertStringList } from "./string-list"
|
||||
|
||||
export function convertEventWorkflowDispatchInputs(
|
||||
context: TemplateContext,
|
||||
token: MappingToken
|
||||
): WorkflowDispatchConfig {
|
||||
const result: WorkflowDispatchConfig = {}
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString("workflow dispatch input key")
|
||||
|
||||
switch (key.value) {
|
||||
case "inputs":
|
||||
result.inputs = convertWorkflowDispatchInputs(
|
||||
context,
|
||||
item.value.assertMapping("workflow dispatch inputs")
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function convertWorkflowDispatchInputs(
|
||||
context: TemplateContext,
|
||||
token: MappingToken
|
||||
): {
|
||||
[inputName: string]: InputConfig
|
||||
} {
|
||||
const result: { [inputName: string]: InputConfig } = {}
|
||||
|
||||
for (const item of token) {
|
||||
const inputName = item.key.assertString("input name")
|
||||
const inputMapping = item.value.assertMapping("input configuration")
|
||||
|
||||
result[inputName.value] = convertWorkflowDispatchInput(
|
||||
context,
|
||||
inputMapping
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function convertWorkflowDispatchInput(
|
||||
context: TemplateContext,
|
||||
token: MappingToken
|
||||
): InputConfig {
|
||||
const result: InputConfig = {
|
||||
type: InputType.string, // Default to string
|
||||
}
|
||||
|
||||
let defaultValue: undefined | ScalarToken
|
||||
|
||||
for (const item of token) {
|
||||
const key = item.key.assertString("workflow dispatch input key")
|
||||
|
||||
switch (key.value) {
|
||||
case "description":
|
||||
result.description = item.value.assertString("input description").value
|
||||
break
|
||||
|
||||
case "required":
|
||||
result.required = item.value.assertBoolean("input required").value
|
||||
break
|
||||
|
||||
case "default":
|
||||
defaultValue = item.value.assertScalar("input default")
|
||||
break
|
||||
|
||||
case "type":
|
||||
result.type =
|
||||
InputType[
|
||||
item.value.assertString("input type")
|
||||
.value as keyof typeof InputType
|
||||
]
|
||||
break
|
||||
|
||||
case "options":
|
||||
result.options = convertStringList(
|
||||
"input options",
|
||||
item.value.assertSequence("input options")
|
||||
)
|
||||
break
|
||||
|
||||
default:
|
||||
context.error(item.key, `Invalid key '${key.value}'`)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate default value
|
||||
if (defaultValue !== undefined) {
|
||||
try {
|
||||
switch (result.type) {
|
||||
case InputType.boolean:
|
||||
result.default = defaultValue.assertBoolean("input default").value
|
||||
|
||||
break
|
||||
|
||||
case InputType.string:
|
||||
case InputType.choice:
|
||||
case InputType.environment:
|
||||
result.default = defaultValue.assertString("input default").value
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
context.error(defaultValue, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate `options` for `choice` type
|
||||
if (result.type === InputType.choice) {
|
||||
if (result.options === undefined || result.options.length === 0) {
|
||||
context.error(token, "Missing 'options' for choice input")
|
||||
}
|
||||
} else {
|
||||
if (result.options !== undefined) {
|
||||
context.error(
|
||||
token,
|
||||
"Input type is not 'choice', but 'options' is defined"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ActionStep, RunStep, Step } from "./workflow-template"
|
||||
|
||||
export function isRunStep(step: Step): step is RunStep {
|
||||
return (step as RunStep).run !== undefined
|
||||
}
|
||||
|
||||
export function isActionStep(step: Step): step is ActionStep {
|
||||
return (step as ActionStep).uses !== undefined
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
BasicExpressionToken,
|
||||
MappingToken,
|
||||
ScalarToken,
|
||||
SequenceToken,
|
||||
StringToken,
|
||||
TemplateToken,
|
||||
} from "../templates/tokens"
|
||||
|
||||
export type WorkflowTemplate = {
|
||||
events: EventsConfig
|
||||
jobs: Job[]
|
||||
concurrency: TemplateToken
|
||||
env: TemplateToken
|
||||
|
||||
errors?: {
|
||||
Message: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type ConcurrencySetting = {
|
||||
group?: StringToken
|
||||
cancelInProgress?: boolean
|
||||
}
|
||||
|
||||
export type ActionsEnvironmentReference = {
|
||||
name?: TemplateToken
|
||||
url?: TemplateToken
|
||||
}
|
||||
|
||||
export type Job = {
|
||||
type: string
|
||||
id: StringToken
|
||||
name?: ScalarToken
|
||||
needs?: StringToken[]
|
||||
if: BasicExpressionToken
|
||||
env?: MappingToken
|
||||
concurrency?: TemplateToken
|
||||
environment?: TemplateToken
|
||||
strategy?: TemplateToken
|
||||
"runs-on"?: TemplateToken
|
||||
container?: TemplateToken
|
||||
services?: TemplateToken
|
||||
outputs?: MappingToken
|
||||
steps: Step[]
|
||||
}
|
||||
|
||||
export type Container = {
|
||||
image: StringToken
|
||||
credentials?: Credential
|
||||
env?: MappingToken
|
||||
ports?: SequenceToken
|
||||
volumes?: SequenceToken
|
||||
options?: StringToken
|
||||
}
|
||||
|
||||
export type Credential = {
|
||||
username: StringToken | undefined
|
||||
password: StringToken | undefined
|
||||
}
|
||||
|
||||
export type Step = ActionStep | RunStep
|
||||
|
||||
type BaseStep = {
|
||||
id: string
|
||||
name?: ScalarToken
|
||||
if: BasicExpressionToken
|
||||
"continue-on-error"?: boolean
|
||||
env?: MappingToken
|
||||
}
|
||||
|
||||
export type RunStep = BaseStep & {
|
||||
run: ScalarToken
|
||||
}
|
||||
|
||||
export type ActionStep = BaseStep & {
|
||||
uses: StringToken
|
||||
}
|
||||
|
||||
export type EventsConfig = {
|
||||
schedule?: ScheduleConfig[]
|
||||
workflow_dispatch?: WorkflowDispatchConfig
|
||||
workflow_call?: WorkflowCallConfig
|
||||
|
||||
// Events that support filters
|
||||
pull_request?: BranchFilterConfig & PathFilterConfig & TypesFilterConfig
|
||||
pull_request_target?: BranchFilterConfig &
|
||||
PathFilterConfig &
|
||||
TypesFilterConfig
|
||||
push?: BranchFilterConfig & TagFilterConfig & PathFilterConfig
|
||||
workflow_run?: WorkflowFilterConfig & BranchFilterConfig & TypesFilterConfig
|
||||
|
||||
// Events that only support activity types
|
||||
branch_protection_rule?: TypesFilterConfig
|
||||
check_run?: TypesFilterConfig
|
||||
check_suite?: TypesFilterConfig
|
||||
disccusion?: TypesFilterConfig
|
||||
disccusion_comment?: TypesFilterConfig
|
||||
issue_comment?: TypesFilterConfig
|
||||
issues?: TypesFilterConfig
|
||||
label?: TypesFilterConfig
|
||||
merge_group?: TypesFilterConfig
|
||||
milestone?: TypesFilterConfig
|
||||
project?: TypesFilterConfig
|
||||
project_card?: TypesFilterConfig
|
||||
project_column?: TypesFilterConfig
|
||||
pull_request_review?: TypesFilterConfig
|
||||
pull_request_review_comment?: TypesFilterConfig
|
||||
registry_package?: TypesFilterConfig
|
||||
repository_dispatch?: TypesFilterConfig
|
||||
release?: TypesFilterConfig
|
||||
watch?: TypesFilterConfig
|
||||
|
||||
// Index signature to allow easier lookup
|
||||
[eventName: string]: unknown
|
||||
}
|
||||
|
||||
export type TypesFilterConfig = {
|
||||
types?: string[]
|
||||
}
|
||||
|
||||
export type BranchFilterConfig = {
|
||||
branches?: string[]
|
||||
"branches-ignore"?: string[]
|
||||
}
|
||||
|
||||
export type TagFilterConfig = {
|
||||
tags?: string[]
|
||||
"tags-ignore"?: string[]
|
||||
}
|
||||
|
||||
export type PathFilterConfig = {
|
||||
paths?: string[]
|
||||
"paths-ignore"?: string[]
|
||||
}
|
||||
|
||||
export type WorkflowDispatchConfig = {
|
||||
inputs?: { [inputName: string]: InputConfig }
|
||||
}
|
||||
|
||||
export type WorkflowCallConfig = {
|
||||
inputs: { [inputName: string]: InputConfig }
|
||||
// TODO - these are supported in C# and Go but not in TS yet
|
||||
// outputs: { [outputName: string]: OutputConfig }
|
||||
// secrets: { [secretName: string]: SecretConfig }
|
||||
}
|
||||
|
||||
export enum InputType {
|
||||
string = "string",
|
||||
choice = "choice",
|
||||
boolean = "boolean",
|
||||
environment = "environment",
|
||||
}
|
||||
|
||||
export type InputConfig = {
|
||||
type: InputType
|
||||
description?: string
|
||||
required?: boolean
|
||||
default?: string | boolean | number
|
||||
options?: string[]
|
||||
}
|
||||
|
||||
export type ScheduleConfig = {
|
||||
cron: string
|
||||
}
|
||||
|
||||
export type WorkflowFilterConfig = {
|
||||
workflows?: string[]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { FunctionInfo } from "@github/actions-expressions/funcs/info"
|
||||
import { MAX_CONSTANT } from "./template-constants"
|
||||
|
||||
export function splitAllowedContext(allowedContext: string[]): {
|
||||
namedContexts: string[]
|
||||
functions: FunctionInfo[]
|
||||
} {
|
||||
const FUNCTION_REGEXP = /^([a-zA-Z0-9_]+)\(([0-9]+),([0-9]+|MAX)\)$/
|
||||
|
||||
const namedContexts: string[] = []
|
||||
const functions: FunctionInfo[] = []
|
||||
if (allowedContext.length > 0) {
|
||||
for (const contextItem of allowedContext) {
|
||||
const match = contextItem.match(FUNCTION_REGEXP)
|
||||
if (match) {
|
||||
const functionName = match[1]
|
||||
const minParameters = Number.parseInt(match[2])
|
||||
const maxParametersRaw = match[3]
|
||||
const maxParameters =
|
||||
maxParametersRaw === MAX_CONSTANT
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: Number.parseInt(maxParametersRaw)
|
||||
functions.push(<FunctionInfo>{
|
||||
name: functionName,
|
||||
minArgs: minParameters,
|
||||
maxArgs: maxParameters,
|
||||
})
|
||||
} else {
|
||||
namedContexts.push(contextItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
namedContexts: namedContexts,
|
||||
functions: functions,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Evaluator, Lexer, Parser } from "@github/actions-expressions"
|
||||
import { Expr } from "@github/actions-expressions/ast"
|
||||
import {
|
||||
Dictionary,
|
||||
ExpressionData,
|
||||
Kind,
|
||||
} from "@github/actions-expressions/data/index"
|
||||
import { FunctionInfo } from "@github/actions-expressions/funcs/info"
|
||||
import { TemplateContext } from "./template-context"
|
||||
import {
|
||||
BasicExpressionToken,
|
||||
BooleanToken,
|
||||
LiteralToken,
|
||||
MappingToken,
|
||||
NullToken,
|
||||
NumberToken,
|
||||
SequenceToken,
|
||||
StringToken,
|
||||
TemplateToken,
|
||||
} from "./tokens"
|
||||
import { TokenType } from "./tokens/types"
|
||||
|
||||
export function evaluateStringToken(
|
||||
token: BasicExpressionToken,
|
||||
context: TemplateContext
|
||||
): TemplateToken {
|
||||
const expr = parseExpression(
|
||||
token.expression,
|
||||
context.expressionNamedContexts,
|
||||
context.expressionFunctions
|
||||
)
|
||||
if (!expr) {
|
||||
throw new Error("Unexpected empty expression")
|
||||
}
|
||||
// TODO: Pass in context
|
||||
const evaluator = new Evaluator(expr, new Dictionary())
|
||||
const result = evaluator.evaluate()
|
||||
if (!result.primitive) {
|
||||
context.error(token, "Expected a string")
|
||||
return new StringToken(
|
||||
token.file,
|
||||
token.range,
|
||||
token.expression,
|
||||
token.definitionInfo
|
||||
)
|
||||
}
|
||||
|
||||
return new StringToken(
|
||||
token.file,
|
||||
token.range,
|
||||
result.coerceString(),
|
||||
token.definitionInfo
|
||||
)
|
||||
}
|
||||
|
||||
export function evaluateSequenceToken(
|
||||
token: BasicExpressionToken,
|
||||
context: TemplateContext
|
||||
): TemplateToken {
|
||||
const expr = parseExpression(
|
||||
token.expression,
|
||||
context.expressionNamedContexts,
|
||||
context.expressionFunctions
|
||||
)
|
||||
if (!expr) {
|
||||
throw new Error("Unexpected empty expression")
|
||||
}
|
||||
// TODO: Pass in context
|
||||
const evaluator = new Evaluator(expr, new Dictionary())
|
||||
const result = evaluator.evaluate()
|
||||
const value = convertToTemplateToken(token, result)
|
||||
if (value.templateTokenType !== TokenType.Sequence) {
|
||||
context.error(token, "Expected a sequence")
|
||||
return new SequenceToken(token.file, token.range, token.definitionInfo)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
export function evaluateMappingToken(
|
||||
token: BasicExpressionToken,
|
||||
context: TemplateContext
|
||||
): MappingToken {
|
||||
const expr = parseExpression(
|
||||
token.expression,
|
||||
context.expressionNamedContexts,
|
||||
context.expressionFunctions
|
||||
)
|
||||
if (!expr) {
|
||||
throw new Error("Unexpected empty expression")
|
||||
}
|
||||
// TODO: Pass in context
|
||||
const evaluator = new Evaluator(expr, new Dictionary())
|
||||
const result = evaluator.evaluate()
|
||||
const value = convertToTemplateToken(token, result)
|
||||
if (value.templateTokenType !== TokenType.Mapping) {
|
||||
context.error(token, "Expected a mapping")
|
||||
return new MappingToken(token.file, token.range, token.definitionInfo)
|
||||
}
|
||||
return value as MappingToken
|
||||
}
|
||||
|
||||
export function evaluateTemplateToken(
|
||||
token: BasicExpressionToken,
|
||||
context: TemplateContext
|
||||
): TemplateToken {
|
||||
const expr = parseExpression(
|
||||
token.expression,
|
||||
context.expressionNamedContexts,
|
||||
context.expressionFunctions
|
||||
)
|
||||
if (!expr) {
|
||||
throw new Error("Unexpected empty expression")
|
||||
}
|
||||
// TODO: Pass in context
|
||||
const evaluator = new Evaluator(expr, new Dictionary())
|
||||
const result = evaluator.evaluate()
|
||||
return convertToTemplateToken(token, result)
|
||||
}
|
||||
|
||||
function convertToTemplateToken(
|
||||
token: BasicExpressionToken,
|
||||
result: ExpressionData
|
||||
): TemplateToken {
|
||||
// Literal
|
||||
const literal = convertToLiteralToken(token, result)
|
||||
if (literal) {
|
||||
return literal
|
||||
}
|
||||
|
||||
// TODO: Support expressions that return a sequence or mapping token
|
||||
|
||||
// Leverage the expression SDK to traverse the object
|
||||
switch (result.kind) {
|
||||
case Kind.Dictionary: {
|
||||
const mapping = new MappingToken(
|
||||
token.file,
|
||||
token.range,
|
||||
token.definitionInfo
|
||||
)
|
||||
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
|
||||
}
|
||||
case Kind.Array: {
|
||||
const sequence = new SequenceToken(
|
||||
token.file,
|
||||
token.range,
|
||||
token.definitionInfo
|
||||
)
|
||||
for (const value of result.values()) {
|
||||
const itemToken = convertToTemplateToken(token, value)
|
||||
sequence.add(itemToken)
|
||||
}
|
||||
return sequence
|
||||
}
|
||||
default:
|
||||
throw new Error("Unable to convert the object to a template token")
|
||||
}
|
||||
}
|
||||
|
||||
function convertToLiteralToken(
|
||||
token: BasicExpressionToken,
|
||||
result: ExpressionData
|
||||
): LiteralToken | undefined {
|
||||
switch (result.kind) {
|
||||
case Kind.Null:
|
||||
return new NullToken(token.file, token.range, token.definitionInfo)
|
||||
case Kind.Boolean:
|
||||
return new BooleanToken(
|
||||
token.file,
|
||||
token.range,
|
||||
result.value,
|
||||
token.definitionInfo
|
||||
)
|
||||
case Kind.Number:
|
||||
return new NumberToken(
|
||||
token.file,
|
||||
token.range,
|
||||
result.value,
|
||||
token.definitionInfo
|
||||
)
|
||||
case Kind.String:
|
||||
return new StringToken(
|
||||
token.file,
|
||||
token.range,
|
||||
result.value,
|
||||
token.definitionInfo
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function parseExpression(
|
||||
expression: string,
|
||||
contexts: string[],
|
||||
functions: FunctionInfo[]
|
||||
): Expr {
|
||||
const lexer = new Lexer(expression)
|
||||
const result = lexer.lex()
|
||||
const p = new Parser(result.tokens, contexts, functions)
|
||||
return p.parse()
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user