diff --git a/expressions/src/errors.ts b/expressions/src/errors.ts index 967113fa..cfccfe7a 100644 --- a/expressions/src/errors.ts +++ b/expressions/src/errors.ts @@ -12,6 +12,7 @@ export enum ErrorType { ErrorExceededMaxLength, ErrorTooFewParameters, ErrorTooManyParameters, + ErrorEvenParameters, ErrorUnrecognizedContext, ErrorUnrecognizedFunction } @@ -42,6 +43,8 @@ function errorDescription(typ: ErrorType): string { return "Too few parameters supplied"; case ErrorType.ErrorTooManyParameters: return "Too many parameters supplied"; + case ErrorType.ErrorEvenParameters: + return "Even number of parameters supplied, requires an odd number of parameters"; case ErrorType.ErrorUnrecognizedContext: return "Unrecognized named-value"; case ErrorType.ErrorUnrecognizedFunction: diff --git a/expressions/src/funcs.ts b/expressions/src/funcs.ts index 1adad75f..a0df579c 100644 --- a/expressions/src/funcs.ts +++ b/expressions/src/funcs.ts @@ -1,4 +1,5 @@ import {ErrorType, ExpressionError} from "./errors.js"; +import {caseFunc} from "./funcs/case.js"; import {contains} from "./funcs/contains.js"; import {endswith} from "./funcs/endswith.js"; import {format} from "./funcs/format.js"; @@ -16,6 +17,7 @@ export type ParseContext = { }; export const wellKnownFunctions: {[name: string]: FunctionDefinition} = { + case: caseFunc, contains: contains, endswith: endswith, format: format, @@ -53,4 +55,9 @@ export function validateFunction(context: ParseContext, identifier: Token, argCo if (argCount > f.maxArgs) { throw new ExpressionError(ErrorType.ErrorTooManyParameters, identifier); } + + // case function requires an odd number of arguments + if (name === "case" && argCount % 2 === 0) { + throw new ExpressionError(ErrorType.ErrorEvenParameters, identifier); + } } diff --git a/expressions/src/funcs/case.ts b/expressions/src/funcs/case.ts new file mode 100644 index 00000000..4ec5fe1d --- /dev/null +++ b/expressions/src/funcs/case.ts @@ -0,0 +1,29 @@ +import {ExpressionData, Kind} from "../data/index.js"; +import {FunctionDefinition} from "./info.js"; + +export const caseFunc: FunctionDefinition = { + name: "case", + description: + "`case( pred1, val1, pred2, val2, ..., default )`\n\nEvaluates predicates in order and returns the value corresponding to the first predicate that evaluates to `true`. If no predicate matches, returns the default value (the last argument).", + minArgs: 3, + maxArgs: Number.MAX_SAFE_INTEGER, + call: (...args: ExpressionData[]): ExpressionData => { + // Evaluate predicate-result pairs + for (let i = 0; i < args.length - 1; i += 2) { + const predicate = args[i]; + + // Predicate must be a boolean + if (predicate.kind !== Kind.Boolean) { + throw new Error("case predicate must evaluate to a boolean value"); + } + + // If predicate is true, return the corresponding result + if (predicate.value) { + return args[i + 1]; + } + } + + // No predicate matched, return default (last argument) + return args[args.length - 1]; + } +}; diff --git a/expressions/testdata/case.json b/expressions/testdata/case.json new file mode 100644 index 00000000..1dc12ff3 --- /dev/null +++ b/expressions/testdata/case.json @@ -0,0 +1,157 @@ +{ + "case": [ + { + "expr": "case(true, 'first', 'default')", + "result": { "kind": "String", "value": "first" } + }, + { + "expr": "case(false, 'first', 'default')", + "result": { "kind": "String", "value": "default" } + }, + { + "expr": "case(true, 'first', false, 'second', 'default')", + "result": { "kind": "String", "value": "first" } + }, + { + "expr": "case(false, 'first', true, 'second', 'default')", + "result": { "kind": "String", "value": "second" } + }, + { + "expr": "case(false, 'first', false, 'second', 'default')", + "result": { "kind": "String", "value": "default" } + }, + { + "expr": "case(1 == 1, 'equal', 'not equal')", + "result": { "kind": "String", "value": "equal" } + }, + { + "expr": "case(1 == 2, 'equal', 'not equal')", + "result": { "kind": "String", "value": "not equal" } + }, + { + "expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')", + "contexts": { + "github": { + "ref": "refs/heads/main", + "event_name": "push" + } + }, + "result": { "kind": "String", "value": "main" } + }, + { + "expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')", + "contexts": { + "github": { + "ref": "refs/heads/develop", + "event_name": "pull_request" + } + }, + "result": { "kind": "String", "value": "pr" } + }, + { + "expr": "case(github.ref == 'refs/heads/main', 'main', github.event_name == 'pull_request', 'pr', 'other')", + "contexts": { + "github": { + "ref": "refs/heads/develop", + "event_name": "push" + } + }, + "result": { "kind": "String", "value": "other" } + }, + { + "expr": "case(true, 123, 456)", + "result": { "kind": "Number", "value": 123 } + }, + { + "expr": "case(false, 123, 456)", + "result": { "kind": "Number", "value": 456 } + }, + { + "expr": "case(github.event == 'pull_request', 0, 1)", + "contexts": { + "github": { + "event": "pull_request" + } + }, + "result": { "kind": "Number", "value": 0 } + }, + { + "expr": "case(false, 0, 1)", + "result": { "kind": "Number", "value": 1 } + }, + { + "expr": "case(true, false, true)", + "result": { "kind": "Boolean", "value": false } + }, + { + "expr": "case(false, false, true)", + "result": { "kind": "Boolean", "value": true } + }, + { + "expr": "case(true, '', 'default')", + "result": { "kind": "String", "value": "" } + }, + { + "expr": "case(false, 'first', '')", + "result": { "kind": "String", "value": "" } + }, + { + "expr": "case(true, fromJSON('[1,2,3]'), 'default')", + "result": { "kind": "Array", "value": [1, 2, 3] } + }, + { + "expr": "case(true, fromJSON('{\"key\":\"value\"}'), 'default')", + "result": { "kind": "Object", "value": { "key": "value" } } + }, + { + "expr": "case(false, 'first', false, 'second', false, 'third', false, 'fourth', 'default')", + "result": { "kind": "String", "value": "default" } + }, + { + "expr": "case(false, 'first', false, 'second', true, 'third', false, 'fourth', 'default')", + "result": { "kind": "String", "value": "third" } + }, + { + "expr": "case('not a boolean', 'first', 'default')", + "err": { + "kind": "evaluation", + "value": "case predicate must evaluate to a boolean value" + } + }, + { + "expr": "case(1, 'first', 'default')", + "err": { + "kind": "evaluation", + "value": "case predicate must evaluate to a boolean value" + } + }, + { + "expr": "case(null, 'first', 'default')", + "err": { + "kind": "evaluation", + "value": "case predicate must evaluate to a boolean value" + } + }, + { + "expr": "case(fromJSON('[]'), 'first', 'default')", + "err": { + "kind": "evaluation", + "value": "case predicate must evaluate to a boolean value" + } + }, + { + "expr": "case(fromJSON('{}'), 'first', 'default')", + "err": { + "kind": "evaluation", + "value": "case predicate must evaluate to a boolean value" + } + }, + { + "expr": "case(true, 'first', false, 'second')", + "err": { + "kind": "parsing", + "value": "Even number of parameters supplied, requires an odd number of parameters: 'case'. Located at position 1 within expression: case(true, 'first', false, 'second')" + } + } + ] +} diff --git a/languageservice/src/complete.expressions.test.ts b/languageservice/src/complete.expressions.test.ts index 3496b1e5..f1bf8f5d 100644 --- a/languageservice/src/complete.expressions.test.ts +++ b/languageservice/src/complete.expressions.test.ts @@ -74,6 +74,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -114,6 +115,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -132,6 +134,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -150,6 +153,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -168,6 +172,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -186,6 +191,7 @@ describe("expressions", () => { "github", "inputs", "vars", + "case", "contains", "endsWith", "format", @@ -1139,6 +1145,7 @@ jobs: "steps", "strategy", "vars", + "case", "contains", "endsWith", "format",