diff --git a/README.md b/README.md index 48600ec..6800298 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ npm install @camunda/feel-builtins ## Usage +This package exports multiple collections of FEEL builtins: + +* **`camundaBuiltins`**: Collection of builtins of camunda scala FEEL. +* **`feelBuiltins`**: List of standard FEEL built-in functions (excluding Camunda-specific extensions). +* **`camundaExtensions`**: List of FEEL camunda extensions. +* **`camundaReservedNameBuiltins`**: Functions using reserved keywords in their name and need to be added to the parser context during parsing. + +You can feed built-ins as context into your favorite [FEEL editor](#feel-editor) or [validator](#feel-lint). + ### Feel Editor In your [FEEL editor](https://github.com/bpmn-io/feel-editor) you can use these builtins to establish the Camunda context: @@ -29,6 +38,17 @@ const editor = new FeelEditor({ }); ``` +If you only want standard FEEL functions, use `feelBuiltins` instead: + +```js +import { feelBuiltins } from '@camunda/feel-builtins'; + +const editor = new FeelEditor({ + container, + builtins: feelBuiltins +}); +``` + ### Feel Lint With [@bpmn-io/feel-lint](https://github.com/bpmn-io/feel-lint) you can also use the builtins to lint expressions in the Camunda world: diff --git a/dist/index.d.ts b/dist/index.d.ts index b2de2e7..37eef58 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,7 +1,24 @@ /** - * A collection of builtin of FEEL. + * Collection of builtins of camunda scala FEEL. */ export const camundaBuiltins: Builtin[]; + +/** + * List of standard FEEL built-in functions (excluding Camunda-specific extensions). + */ +export const feelBuiltins: Builtin[]; + +/** + * List of FEEL camunda extensions. + */ +export const camundaExtensions: Builtin[]; + +/** + * Camunda built-ins that use reserved keywords in their name and thus must + * be explicitly declared when parsing FEEL. + */ +export const camundaReservedNameBuiltins: Builtin[]; + export type Builtin = { /** * The name of the builtin function. @@ -12,11 +29,11 @@ export type Builtin = { */ info: string; /** - * type of the builtin, always 'function' for builtin functions. + * Type of the builtin, always 'function' for builtin functions. */ - type?: "function"; + type?: 'function'; /** - * function parameters. + * Function parameters. */ params?: Array<{ name: string; diff --git a/package-lock.json b/package-lock.json index 74c775f..ed51392 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.3.0", "license": "MIT", "devDependencies": { + "@bpmn-io/lezer-feel": "^2.2.1", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", @@ -23,6 +24,21 @@ "typescript": "^5.8.3" } }, + "node_modules/@bpmn-io/lezer-feel": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@bpmn-io/lezer-feel/-/lezer-feel-2.2.1.tgz", + "integrity": "sha512-X/8DIoTIW+F9dI2Pr2M66tGT8q83ZXLAwVYqHVUWw9STXbbCs7Aluy6CxW8EpXD1rur15pbP1ZkXQDIFkx55rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/highlight": "^1.2.3", + "@lezer/lr": "^1.4.7", + "min-dash": "^5.0.0" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -297,6 +313,33 @@ "node": ">=12" } }, + "node_modules/@lezer/common": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", + "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz", + "integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -3009,6 +3052,13 @@ "node": ">= 0.10.0" } }, + "node_modules/min-dash": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/min-dash/-/min-dash-5.0.0.tgz", + "integrity": "sha512-EGuoBnVL7/Fnv2sqakpX5WGmZehZ3YMmLayT7sM8E9DRU74kkeyMg4Rik1lsOkR2GbFNeBca4/L+UfU6gF0Edw==", + "dev": true, + "license": "MIT" + }, "node_modules/minimatch": { "version": "10.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", diff --git a/package.json b/package.json index 25c18dd..ef2f1a6 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "compile-builtins": "node tasks/compileBuiltins.js" }, "devDependencies": { + "@bpmn-io/lezer-feel": "^2.2.1", "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", "@types/node": "^24.0.0", diff --git a/src/camundaBuiltins.js b/src/camundaBuiltins.js index aa16597..304d8fd 100644 --- a/src/camundaBuiltins.js +++ b/src/camundaBuiltins.js @@ -18,11 +18,11 @@ */ /** - * FEEL built-ins available with Camunda / feel-scala. + * List of standard FEEL built-in functions (excluding Camunda-specific extensions). * * @type { Builtin[] } */ -export const camundaBuiltins = [ +export const feelBuiltins = [ { "name": "not", "type": "function", @@ -33,58 +33,6 @@ export const camundaBuiltins = [ ], "info": "

Returns the logical negation of the given value.

\n

Function signature

\n
not(negand: boolean): boolean\n
\n

Examples

\n
not(true)\n// false\n\nnot(null)\n// null\n
\n" }, - { - "name": "is defined", - "type": "function", - "params": [ - { - "name": "value" - } - ], - "info": "

Camunda Extension

\n

Checks if a given value is not null. If the value is null then the function returns false.\nOtherwise, the function returns true.

\n

Function signature

\n
is defined(value: Any): boolean\n
\n

Examples

\n
is defined(1)\n// true\n\nis defined(null)\n// false\n\nis defined(x)\n// false - if no variable "x" exists\n\nis defined(x.y)\n// false - if no variable "x" exists or it doesn't have a property "y"\n
\n

:::caution Breaking change

\n

This function worked differently in previous versions. It returned true if the value was null.\nSince this version, the function returns false if the value is null.

\n

:::

\n" - }, - { - "name": "get or else", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "default" - } - ], - "info": "

Camunda Extension

\n

Return the provided value parameter if not null, otherwise return the default parameter

\n

Function signature

\n
get or else(value: Any, default: Any): Any\n
\n

Examples

\n
get or else("this", "default")\n// "this"\n\nget or else(null, "default")\n// "default"\n\nget or else(null, null)\n// null\n
\n" - }, - { - "name": "assert", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "condition" - } - ], - "info": "

Camunda Extension

\n

Verify that the given condition is met. If the condition is true, the function returns the value.\nOtherwise, the evaluation fails with an error.

\n

Function signature

\n
assert(value: Any, condition: Any)\n
\n

Examples

\n
assert(x, x != null)\n// "value" - if x is "value"\n// error - if x is null or doesn't exist\n\nassert(x, x >= 0)\n// 4 - if x is 4\n// error - if x is less than zero\n
\n" - }, - { - "name": "assert", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "condition" - }, - { - "name": "cause" - } - ], - "info": "

Camunda Extension

\n

Verify that the given condition is met. If the condition is true, the function returns the value.\nOtherwise, the evaluation fails with an error containing the given message.

\n

Function signature

\n
assert(value: Any, condition: Any, cause: String)\n
\n

Examples

\n
assert(x, x != null, "'x' should not be null")\n// "value" - if x is "value"\n// error('x' should not be null) - if x is null or doesn't exist\n\nassert(x, x >= 0, "'x' should be positive")\n// 4 - if x is 4\n// error('x' should be positive) - if x is less than zero\n
\n" - }, { "name": "get value", "type": "function", @@ -98,19 +46,6 @@ export const camundaBuiltins = [ ], "info": "

Returns the value of the context entry with the given key.

\n

Function signature

\n
get value(context: context, key: string): Any\n
\n

Examples

\n
get value({foo: 123}, "foo")\n// 123\n\nget value({a: 1}, "b")\n// null\n
\n" }, - { - "name": "get value", - "type": "function", - "params": [ - { - "name": "context" - }, - { - "name": "keys" - } - ], - "info": "

Camunda Extension

\n

Returns the value of the context entry for a context path defined by the given keys.

\n

If keys contains the keys [k1, k2] then it returns the value at the nested entry k1.k2 of the context.

\n

If keys are empty or the nested entry defined by the keys doesn't exist in the context, it returns null.

\n

Function signature

\n
get value(context: context, keys: list<string>): Any\n
\n

Examples

\n
get value({x:1, y: {z:0}}, ["y", "z"])\n// 0\n\nget value({x: {y: {z:0}}}, ["x", "y"])\n// {z:0}\n\nget value({a: {b: 3}}, ["b"])\n// null\n
\n" - }, { "name": "get entries", "type": "function", @@ -121,22 +56,6 @@ export const camundaBuiltins = [ ], "info": "

Returns the entries of the context as a list of key-value-pairs.

\n

Function signature

\n
get entries(context: context): list<context>\n
\n

The return value is a list of contexts. Each context contains two entries for "key" and "value".

\n

Examples

\n
get entries({foo: 123})\n// [{key: "foo", value: 123}]\n
\n" }, - { - "name": "context put", - "type": "function", - "params": [ - { - "name": "context" - }, - { - "name": "key" - }, - { - "name": "value" - } - ], - "info": "

Adds a new entry with the given key and value to the context. Returns a new context that includes the entry.

\n

If an entry for the same key already exists in the context, it overrides the value.

\n

Function signature

\n
context put(context: context, key: string, value: Any): context\n
\n

Examples

\n
context put({x:1}, "y", 2)\n// {x:1, y:2}\n
\n

:::info\nThe function context put() replaced the previous function put() (Camunda Extension). The\nprevious function is deprecated and should not be used anymore.\n:::

\n" - }, { "name": "context put", "type": "function", @@ -153,16 +72,6 @@ export const camundaBuiltins = [ ], "info": "

Adds a new entry with the given value to the context. The path of the entry is defined by the keys. Returns a new context that includes the entry.

\n

If keys contains the keys [k1, k2] then it adds the nested entry k1.k2 = value to the context.

\n

If an entry for the same keys already exists in the context, it overrides the value.

\n

If keys are empty, it returns null.

\n

Function signature

\n
context put(context: context, keys: list<string>, value: Any): context\n
\n

Examples

\n
context put({x:1}, ["y"], 2)\n// {x:1, y:2}\n\ncontext put({x:1, y: {z:0}}, ["y", "z"], 2)\n// {x:1, y: {z:2}}\n\ncontext put({x:1}, ["y", "z"], 2)\n// {x:1, y: {z:2}}\n
\n" }, - { - "name": "context merge", - "type": "function", - "params": [ - { - "name": "contexts" - } - ], - "info": "

Union the given contexts. Returns a new context that includes all entries of the given contexts.

\n

If an entry for the same key already exists in a context, it overrides the value. The entries are overridden in the same order as in the list of contexts.

\n

Function signature

\n
context merge(contexts: list<context>): context\n
\n

Examples

\n
context merge([{x:1}, {y:2}])\n// {x:1, y:2}\n\ncontext merge([{x:1, y: 0}, {y:2}])\n// {x:1, y:2}\n
\n

:::info\nThe function context merge() replaced the previous function put all() (Camunda Extension). The\nprevious function is deprecated and should not be used anymore.\n:::

\n" - }, { "name": "string", "type": "function", @@ -316,19 +225,6 @@ export const camundaBuiltins = [ ], "info": "

Returns a date and time from the given components.

\n

Function signature

\n
date and time(date: date, time: time): date and time\n
\n
date and time(date: date and time, time: time): date and time\n
\n

Returns a date and time value that consists of the date component of date combined with time.

\n

Examples

\n
date and time(date("2012-12-24"),time("T23:59:00"))\n// date and time("2012-12-24T23:59:00")\n\ndate and time(date and time("2012-12-25T11:00:00"),time("T23:59:00"))\n// date and time("2012-12-25T23:59:00")\n
\n" }, - { - "name": "date and time", - "type": "function", - "params": [ - { - "name": "date" - }, - { - "name": "timezone" - } - ], - "info": "

Camunda Extension

\n

Returns the given date and time value at the given timezone.

\n

If date has a different timezone than timezone then it adjusts the time to match the local time of timezone.

\n

Function signature

\n
date and time(date: date and time, timezone: string): date and time\n
\n

Examples

\n
date and time(@"2020-07-31T14:27:30@Europe/Berlin", "America/Los_Angeles")\n// date and time("2020-07-31T05:27:30@America/Los_Angeles")\n\ndate and time(@"2020-07-31T14:27:30", "Z")\n// date and time("2020-07-31T12:27:30Z")\n
\n" - }, { "name": "duration", "type": "function", @@ -619,16 +515,6 @@ export const camundaBuiltins = [ ], "info": "

Returns the given list without duplicates.

\n

Function signature

\n
distinct values(list: list): list\n
\n

Examples

\n
distinct values([1,2,3,2,1])\n// [1,2,3]\n
\n" }, - { - "name": "duplicate values", - "type": "function", - "params": [ - { - "name": "list" - } - ], - "info": "

Camunda Extension

\n

Returns all duplicate values of the given list.

\n

Function signature

\n
duplicate values(list: list): list\n
\n

Examples

\n
duplicate values([1,2,3,2,1])\n// [1,2]\n
\n" - }, { "name": "flatten", "type": "function", @@ -675,128 +561,6 @@ export const camundaBuiltins = [ ], "info": "

Joins a list of strings into a single string. This is similar to\nJava's joining\nfunction.

\n

If an item of the list is null, the item is ignored for the result string. If an item is\nneither a string nor null, the function returns null instead of a string.

\n

The resulting string contains a delimiter between each element.

\n

Function signature

\n
string join(list: list<string>, delimiter: string): string\n
\n

Examples

\n
string join(["a"], "X")\n// "a"\n\nstring join(["a","b","c"], ", ")\n// "a, b, c"\n
\n" }, - { - "name": "string join", - "type": "function", - "params": [ - { - "name": "list" - }, - { - "name": "delimiter" - }, - { - "name": "prefix" - }, - { - "name": "suffix" - } - ], - "info": "

Camunda Extension

\n

Joins a list of strings into a single string. This is similar to\nJava's joining\nfunction.

\n

If an item of the list is null, the item is ignored for the result string. If an item is\nneither a string nor null, the function returns null instead of a string.

\n

The resulting string starts with prefix, contains a delimiter between each element, and ends\nwith suffix.

\n

Function signature

\n
string join(list: list<string>, delimiter: string, prefix: string, suffix: string): string\n
\n

Examples

\n
string join(["a","b","c"], ", ", "[", "]")\n// "[a, b, c]"\n
\n" - }, - { - "name": "is empty", - "type": "function", - "params": [ - { - "name": "list" - } - ], - "info": "

Camunda Extension

\n

Returns true if the given list is empty. Otherwise, returns false.

\n

Function signature

\n
is empty(list: list): boolean\n
\n

Examples

\n
is empty([])\n// true\n\nis empty([1,2,3])\n// false\n
\n" - }, - { - "name": "partition", - "type": "function", - "params": [ - { - "name": "list" - }, - { - "name": "size" - } - ], - "info": "

Camunda Extension

\n

Returns consecutive sublists of a list, each of the same size (the final list may be smaller).

\n

If size is less than 0, it returns null.

\n

Function signature

\n
partition(list: list, size: number): list\n
\n

Examples

\n
partition([1,2,3,4,5], 2)\n// [[1,2], [3,4], [5]]\n\npartition([], 2)\n// []\n\npartition([1,2], 0)\n// null\n
\n" - }, - { - "name": "fromAi", - "type": "function", - "params": [ - { - "name": "value" - } - ], - "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n\n

The main use case of this function is for tool definitions used by the AI Agent connector.

\n

See the following function overloads for additional function parameters.

\n

Function signature

\n
fromAi(value: Any): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery)\n// toolCall.searchQuery contents\n\nfromAi(toolCall.userId)\n// toolCall.userId contents\n
\n" - }, - { - "name": "fromAi", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "description" - } - ], - "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional description parameter to provide a textual description of the value. The description must be null or a string constant.

\n

Function signature

\n
fromAi(value: Any, description: string): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery, "The search query used to find the best match.")\n// toolCall.searchQuery contents\n\nfromAi(toolCall.searchQuery, null)\n// toolCall.searchQuery contents\n
\n" - }, - { - "name": "fromAi", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "description" - }, - { - "name": "type" - } - ], - "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional type parameter to provide type information about the value. The type must be null or a string constant.

\n

Function signature

\n
fromAi(value: Any, description: string, type: string): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery, "The search query used to find the best match.", "string")\n// toolCall.searchQuery contents\n\nfromAi(toolCall.userId, "The user's ID", "number")\n// toolCall.userId contents\n\nfromAi(toolCall.userId, null, "number")\n// toolCall.userId contents\n\nfromAi(value: toolCall.userId, type: "number")\n// toolCall.userId contents\n
\n" - }, - { - "name": "fromAi", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "description" - }, - { - "name": "type" - }, - { - "name": "schema" - } - ], - "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional schema parameter to provide a (partial) JSON schema for the value.

\n\n

Function signature

\n
fromAi(value: Any, description: string, type: string, schema: context): Any\n
\n

Examples

\n
fromAi(toolCall.documentType, "The document type to provide", "string", {\n  enum: ["invoice", "receipt", "contract"]\n})\n// toolCall.documentType contents\n\nfromAi(value: toolCall.documentType, description: "The document type to provide", schema: {\n  type: "string",\n  enum: ["invoice", "receipt", "contract"]\n})\n// toolCall.documentType contents\n\nfromAi(toolCall.tags, "Tags to apply to the blog post", "array", {\n  items: {\n    type: "string"\n  }\n})\n// toolCall.tags contents\n
\n" - }, - { - "name": "fromAi", - "type": "function", - "params": [ - { - "name": "value" - }, - { - "name": "description" - }, - { - "name": "type" - }, - { - "name": "schema" - }, - { - "name": "options" - } - ], - "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional options parameter to provide additional options for the integration handling the value definition.

\n\n

Function signature

\n
fromAi(value: Any, description: string, type: string, schema: context, options: context): Any\n
\n

Examples

\n
fromAi(toolCall.documentType, "The document type to provide", "string", null, {\n  required: false\n})\n// toolCall.documentType contents\n\nfromAi(value: toolCall.documentType, options: {\n  required: false\n})\n// toolCall.documentType contents\n
\n" - }, { "name": "decimal", "type": "function", @@ -981,12 +745,6 @@ export const camundaBuiltins = [ ], "info": "

Returns true if the given is even. Otherwise, returns false.

\n

Function signature

\n
even(number: number): boolean\n
\n

Examples

\n
even(5)\n// false\n\neven(2)\n// true\n
\n" }, - { - "name": "random number", - "type": "function", - "params": [], - "info": "

Camunda Extension

\n

Returns a random number between 0 and 1.

\n

Function signature

\n
random number(): number\n
\n

Examples

\n
random number()\n// 0.9701618132579795\n
\n" - }, { "name": "before", "type": "function", @@ -1540,115 +1298,365 @@ export const camundaBuiltins = [ "info": "

Splits the given value into a list of substrings, breaking at each occurrence of the delimiter pattern.

\n

Function signature

\n
split(string: string, delimiter: string): list<string>\n
\n

The delimiter is a string that contains a regular expression.

\n

Examples

\n
split("John Doe", "\\s" )\n// ["John", "Doe"]\n\nsplit("a;b;c;;", ";")\n// ["a", "b", "c", "", ""]\n
\n" }, { - "name": "extract", + "name": "now", "type": "function", - "params": [ - { - "name": "string" - }, - { - "name": "pattern" - } - ], - "info": "

Camunda Extension

\n

Returns all matches of the pattern in the given string. Returns an empty list if the pattern doesn't\nmatch.

\n

Function signature

\n
extract(string: string, pattern: string): list<string>\n
\n

The pattern is a string that contains a regular expression.

\n

Examples

\n
extract("references are 1234, 1256, 1378", "12[0-9]*")\n// ["1234","1256"]\n
\n" + "params": [], + "info": "

Returns the current date and time including the timezone.

\n

Function signature

\n
now(): date and time\n
\n

Examples

\n
now()\n// date and time("2020-07-31T14:27:30@Europe/Berlin")\n
\n" }, { - "name": "trim", + "name": "today", + "type": "function", + "params": [], + "info": "

Returns the current date.

\n

Function signature

\n
today(): date\n
\n

Examples

\n
today()\n// date("2020-07-31")\n
\n" + }, + { + "name": "day of week", "type": "function", "params": [ { - "name": "string" + "name": "date" } ], - "info": "

Camunda Extension

\n

Returns the given string without leading and trailing spaces.

\n

Function signature

\n
trim(string: string): string\n
\n

Examples

\n
trim("  hello world  ")\n// "hello world"\n\ntrim("hello   world ")\n// "hello   world"\n
\n" + "info": "

Returns the day of the week according to the Gregorian calendar. Note that it always returns the English name of the day.

\n

Function signature

\n
day of week(date: date): string\n
\n
day of week(date: date and time): string\n
\n

Examples

\n
day of week(date("2019-09-17"))\n// "Tuesday"\n\nday of week(date and time("2019-09-17T12:00:00"))\n// "Tuesday"\n
\n" }, { - "name": "uuid", + "name": "day of year", "type": "function", - "params": [], - "info": "

Camunda Extension

\n

Returns a UUID (Universally Unique Identifier) with 36 characters.

\n

Function signature

\n
uuid(): string\n
\n

Examples

\n
uuid()\n// "7793aab1-d761-4d38-916b-b7270e309894"\n
\n" + "params": [ + { + "name": "date" + } + ], + "info": "

Returns the Gregorian number of the day within the year.

\n

Function signature

\n
day of year(date: date): number\n
\n
day of year(date: date and time): number\n
\n

Examples

\n
day of year(date("2019-09-17"))\n// 260\n\nday of year(date and time("2019-09-17T12:00:00"))\n// 260\n
\n" }, { - "name": "to base64", + "name": "week of year", + "type": "function", + "params": [ + { + "name": "date" + } + ], + "info": "

Returns the Gregorian number of the week within the year, according to ISO 8601.

\n

Function signature

\n
week of year(date: date): number\n
\n
week of year(date: date and time): number\n
\n

Examples

\n
week of year(date("2019-09-17"))\n// 38\n\nweek of year(date and time("2019-09-17T12:00:00"))\n// 38\n
\n" + }, + { + "name": "month of year", + "type": "function", + "params": [ + { + "name": "date" + } + ], + "info": "

Returns the month of the year according to the Gregorian calendar. Note that it always returns the English name of the month.

\n

Function signature

\n
month of year(date: date): string\n
\n
month of year(date: date and time): string\n
\n

Examples

\n
month of year(date("2019-09-17"))\n// "September"\n\nmonth of year(date and time("2019-09-17T12:00:00"))\n// "September"\n
\n" + }, + { + "name": "abs", + "type": "function", + "params": [ + { + "name": "n" + } + ], + "info": "

Returns the absolute value of a given duration.

\n

Function signature

\n
abs(n: days and time duration): days and time duration\n
\n
abs(n: years and months duration): years and months duration\n
\n

Examples

\n
abs(duration("-PT5H"))\n// "duration("PT5H")"\n\nabs(duration("PT5H"))\n// "duration("PT5H")"\n\nabs(duration("-P2M"))\n// duration("P2M")\n
\n" + } +]; + +/** + * List of FEEL camunda extensions. + * + * @type { Builtin[] } + */ +export const camundaExtensions = [ + { + "name": "is defined", "type": "function", "params": [ { "name": "value" } ], - "info": "

Camunda Extension

\n

Returns the given string encoded in Base64 format.

\n

Function signature

\n
to base64(value: string): string\n
\n

Examples

\n
to base64("FEEL")\n// "RkVFTA=="\n
\n" + "info": "

Camunda Extension

\n

Checks if a given value is not null. If the value is null then the function returns false.\nOtherwise, the function returns true.

\n

Function signature

\n
is defined(value: Any): boolean\n
\n

Examples

\n
is defined(1)\n// true\n\nis defined(null)\n// false\n\nis defined(x)\n// false - if no variable "x" exists\n\nis defined(x.y)\n// false - if no variable "x" exists or it doesn't have a property "y"\n
\n

:::caution Breaking change

\n

This function worked differently in previous versions. It returned true if the value was null.\nSince this version, the function returns false if the value is null.

\n

:::

\n" }, { - "name": "is blank", + "name": "get or else", "type": "function", "params": [ { - "name": "string" + "name": "value" + }, + { + "name": "default" } ], - "info": "

Camunda Extension

\n

Returns true if the given string is blank (empty or contains only whitespaces).

\n

Function signature

\n
is blank(string: string): boolean\n
\n

Examples

\n
is blank("")\n// true\n\nis blank(" ")\n// true\n\nis blank("hello world")\n// false\n
\n" + "info": "

Camunda Extension

\n

Return the provided value parameter if not null, otherwise return the default parameter

\n

Function signature

\n
get or else(value: Any, default: Any): Any\n
\n

Examples

\n
get or else("this", "default")\n// "this"\n\nget or else(null, "default")\n// "default"\n\nget or else(null, null)\n// null\n
\n" }, { - "name": "now", + "name": "assert", "type": "function", - "params": [], - "info": "

Returns the current date and time including the timezone.

\n

Function signature

\n
now(): date and time\n
\n

Examples

\n
now()\n// date and time("2020-07-31T14:27:30@Europe/Berlin")\n
\n" + "params": [ + { + "name": "value" + }, + { + "name": "condition" + } + ], + "info": "

Camunda Extension

\n

Verify that the given condition is met. If the condition is true, the function returns the value.\nOtherwise, the evaluation fails with an error.

\n

Function signature

\n
assert(value: Any, condition: Any)\n
\n

Examples

\n
assert(x, x != null)\n// "value" - if x is "value"\n// error - if x is null or doesn't exist\n\nassert(x, x >= 0)\n// 4 - if x is 4\n// error - if x is less than zero\n
\n" }, { - "name": "today", + "name": "assert", "type": "function", - "params": [], - "info": "

Returns the current date.

\n

Function signature

\n
today(): date\n
\n

Examples

\n
today()\n// date("2020-07-31")\n
\n" + "params": [ + { + "name": "value" + }, + { + "name": "condition" + }, + { + "name": "cause" + } + ], + "info": "

Camunda Extension

\n

Verify that the given condition is met. If the condition is true, the function returns the value.\nOtherwise, the evaluation fails with an error containing the given message.

\n

Function signature

\n
assert(value: Any, condition: Any, cause: String)\n
\n

Examples

\n
assert(x, x != null, "'x' should not be null")\n// "value" - if x is "value"\n// error('x' should not be null) - if x is null or doesn't exist\n\nassert(x, x >= 0, "'x' should be positive")\n// 4 - if x is 4\n// error('x' should be positive) - if x is less than zero\n
\n" }, { - "name": "day of week", + "name": "get value", "type": "function", "params": [ { - "name": "date" + "name": "context" + }, + { + "name": "keys" } ], - "info": "

Returns the day of the week according to the Gregorian calendar. Note that it always returns the English name of the day.

\n

Function signature

\n
day of week(date: date): string\n
\n
day of week(date: date and time): string\n
\n

Examples

\n
day of week(date("2019-09-17"))\n// "Tuesday"\n\nday of week(date and time("2019-09-17T12:00:00"))\n// "Tuesday"\n
\n" + "info": "

Camunda Extension

\n

Returns the value of the context entry for a context path defined by the given keys.

\n

If keys contains the keys [k1, k2] then it returns the value at the nested entry k1.k2 of the context.

\n

If keys are empty or the nested entry defined by the keys doesn't exist in the context, it returns null.

\n

Function signature

\n
get value(context: context, keys: list<string>): Any\n
\n

Examples

\n
get value({x:1, y: {z:0}}, ["y", "z"])\n// 0\n\nget value({x: {y: {z:0}}}, ["x", "y"])\n// {z:0}\n\nget value({a: {b: 3}}, ["b"])\n// null\n
\n" }, { - "name": "day of year", + "name": "context put", "type": "function", "params": [ { - "name": "date" + "name": "context" + }, + { + "name": "key" + }, + { + "name": "value" } ], - "info": "

Returns the Gregorian number of the day within the year.

\n

Function signature

\n
day of year(date: date): number\n
\n
day of year(date: date and time): number\n
\n

Examples

\n
day of year(date("2019-09-17"))\n// 260\n\nday of year(date and time("2019-09-17T12:00:00"))\n// 260\n
\n" + "info": "

Adds a new entry with the given key and value to the context. Returns a new context that includes the entry.

\n

If an entry for the same key already exists in the context, it overrides the value.

\n

Function signature

\n
context put(context: context, key: string, value: Any): context\n
\n

Examples

\n
context put({x:1}, "y", 2)\n// {x:1, y:2}\n
\n

:::info\nThe function context put() replaced the previous function put() (Camunda Extension). The\nprevious function is deprecated and should not be used anymore.\n:::

\n" }, { - "name": "week of year", + "name": "context merge", "type": "function", "params": [ { - "name": "date" + "name": "contexts" } ], - "info": "

Returns the Gregorian number of the week within the year, according to ISO 8601.

\n

Function signature

\n
week of year(date: date): number\n
\n
week of year(date: date and time): number\n
\n

Examples

\n
week of year(date("2019-09-17"))\n// 38\n\nweek of year(date and time("2019-09-17T12:00:00"))\n// 38\n
\n" + "info": "

Union the given contexts. Returns a new context that includes all entries of the given contexts.

\n

If an entry for the same key already exists in a context, it overrides the value. The entries are overridden in the same order as in the list of contexts.

\n

Function signature

\n
context merge(contexts: list<context>): context\n
\n

Examples

\n
context merge([{x:1}, {y:2}])\n// {x:1, y:2}\n\ncontext merge([{x:1, y: 0}, {y:2}])\n// {x:1, y:2}\n
\n

:::info\nThe function context merge() replaced the previous function put all() (Camunda Extension). The\nprevious function is deprecated and should not be used anymore.\n:::

\n" }, { - "name": "month of year", + "name": "date and time", "type": "function", "params": [ { "name": "date" + }, + { + "name": "timezone" } ], - "info": "

Returns the month of the year according to the Gregorian calendar. Note that it always returns the English name of the month.

\n

Function signature

\n
month of year(date: date): string\n
\n
month of year(date: date and time): string\n
\n

Examples

\n
month of year(date("2019-09-17"))\n// "September"\n\nmonth of year(date and time("2019-09-17T12:00:00"))\n// "September"\n
\n" + "info": "

Camunda Extension

\n

Returns the given date and time value at the given timezone.

\n

If date has a different timezone than timezone then it adjusts the time to match the local time of timezone.

\n

Function signature

\n
date and time(date: date and time, timezone: string): date and time\n
\n

Examples

\n
date and time(@"2020-07-31T14:27:30@Europe/Berlin", "America/Los_Angeles")\n// date and time("2020-07-31T05:27:30@America/Los_Angeles")\n\ndate and time(@"2020-07-31T14:27:30", "Z")\n// date and time("2020-07-31T12:27:30Z")\n
\n" }, { - "name": "abs", + "name": "duplicate values", "type": "function", "params": [ { - "name": "n" + "name": "list" } ], - "info": "

Returns the absolute value of a given duration.

\n

Function signature

\n
abs(n: days and time duration): days and time duration\n
\n
abs(n: years and months duration): years and months duration\n
\n

Examples

\n
abs(duration("-PT5H"))\n// "duration("PT5H")"\n\nabs(duration("PT5H"))\n// "duration("PT5H")"\n\nabs(duration("-P2M"))\n// duration("P2M")\n
\n" + "info": "

Camunda Extension

\n

Returns all duplicate values of the given list.

\n

Function signature

\n
duplicate values(list: list): list\n
\n

Examples

\n
duplicate values([1,2,3,2,1])\n// [1,2]\n
\n" + }, + { + "name": "string join", + "type": "function", + "params": [ + { + "name": "list" + }, + { + "name": "delimiter" + }, + { + "name": "prefix" + }, + { + "name": "suffix" + } + ], + "info": "

Camunda Extension

\n

Joins a list of strings into a single string. This is similar to\nJava's joining\nfunction.

\n

If an item of the list is null, the item is ignored for the result string. If an item is\nneither a string nor null, the function returns null instead of a string.

\n

The resulting string starts with prefix, contains a delimiter between each element, and ends\nwith suffix.

\n

Function signature

\n
string join(list: list<string>, delimiter: string, prefix: string, suffix: string): string\n
\n

Examples

\n
string join(["a","b","c"], ", ", "[", "]")\n// "[a, b, c]"\n
\n" + }, + { + "name": "is empty", + "type": "function", + "params": [ + { + "name": "list" + } + ], + "info": "

Camunda Extension

\n

Returns true if the given list is empty. Otherwise, returns false.

\n

Function signature

\n
is empty(list: list): boolean\n
\n

Examples

\n
is empty([])\n// true\n\nis empty([1,2,3])\n// false\n
\n" + }, + { + "name": "partition", + "type": "function", + "params": [ + { + "name": "list" + }, + { + "name": "size" + } + ], + "info": "

Camunda Extension

\n

Returns consecutive sublists of a list, each of the same size (the final list may be smaller).

\n

If size is less than 0, it returns null.

\n

Function signature

\n
partition(list: list, size: number): list\n
\n

Examples

\n
partition([1,2,3,4,5], 2)\n// [[1,2], [3,4], [5]]\n\npartition([], 2)\n// []\n\npartition([1,2], 0)\n// null\n
\n" + }, + { + "name": "fromAi", + "type": "function", + "params": [ + { + "name": "value" + } + ], + "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n\n

The main use case of this function is for tool definitions used by the AI Agent connector.

\n

See the following function overloads for additional function parameters.

\n

Function signature

\n
fromAi(value: Any): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery)\n// toolCall.searchQuery contents\n\nfromAi(toolCall.userId)\n// toolCall.userId contents\n
\n" + }, + { + "name": "fromAi", + "type": "function", + "params": [ + { + "name": "value" + }, + { + "name": "description" + } + ], + "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional description parameter to provide a textual description of the value. The description must be null or a string constant.

\n

Function signature

\n
fromAi(value: Any, description: string): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery, "The search query used to find the best match.")\n// toolCall.searchQuery contents\n\nfromAi(toolCall.searchQuery, null)\n// toolCall.searchQuery contents\n
\n" + }, + { + "name": "fromAi", + "type": "function", + "params": [ + { + "name": "value" + }, + { + "name": "description" + }, + { + "name": "type" + } + ], + "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional type parameter to provide type information about the value. The type must be null or a string constant.

\n

Function signature

\n
fromAi(value: Any, description: string, type: string): Any\n
\n

Examples

\n
fromAi(toolCall.searchQuery, "The search query used to find the best match.", "string")\n// toolCall.searchQuery contents\n\nfromAi(toolCall.userId, "The user's ID", "number")\n// toolCall.userId contents\n\nfromAi(toolCall.userId, null, "number")\n// toolCall.userId contents\n\nfromAi(value: toolCall.userId, type: "number")\n// toolCall.userId contents\n
\n" + }, + { + "name": "fromAi", + "type": "function", + "params": [ + { + "name": "value" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "schema" + } + ], + "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional schema parameter to provide a (partial) JSON schema for the value.

\n\n

Function signature

\n
fromAi(value: Any, description: string, type: string, schema: context): Any\n
\n

Examples

\n
fromAi(toolCall.documentType, "The document type to provide", "string", {\n  enum: ["invoice", "receipt", "contract"]\n})\n// toolCall.documentType contents\n\nfromAi(value: toolCall.documentType, description: "The document type to provide", schema: {\n  type: "string",\n  enum: ["invoice", "receipt", "contract"]\n})\n// toolCall.documentType contents\n\nfromAi(toolCall.tags, "Tags to apply to the blog post", "array", {\n  items: {\n    type: "string"\n  }\n})\n// toolCall.tags contents\n
\n" + }, + { + "name": "fromAi", + "type": "function", + "params": [ + { + "name": "value" + }, + { + "name": "description" + }, + { + "name": "type" + }, + { + "name": "schema" + }, + { + "name": "options" + } + ], + "info": "

Camunda Extension

\n

Returns the unmodified value parameter.

\n

In addition to the previous overload, it also accepts an optional options parameter to provide additional options for the integration handling the value definition.

\n\n

Function signature

\n
fromAi(value: Any, description: string, type: string, schema: context, options: context): Any\n
\n

Examples

\n
fromAi(toolCall.documentType, "The document type to provide", "string", null, {\n  required: false\n})\n// toolCall.documentType contents\n\nfromAi(value: toolCall.documentType, options: {\n  required: false\n})\n// toolCall.documentType contents\n
\n" + }, + { + "name": "random number", + "type": "function", + "params": [], + "info": "

Camunda Extension

\n

Returns a random number between 0 and 1.

\n

Function signature

\n
random number(): number\n
\n

Examples

\n
random number()\n// 0.9701618132579795\n
\n" + }, + { + "name": "extract", + "type": "function", + "params": [ + { + "name": "string" + }, + { + "name": "pattern" + } + ], + "info": "

Camunda Extension

\n

Returns all matches of the pattern in the given string. Returns an empty list if the pattern doesn't\nmatch.

\n

Function signature

\n
extract(string: string, pattern: string): list<string>\n
\n

The pattern is a string that contains a regular expression.

\n

Examples

\n
extract("references are 1234, 1256, 1378", "12[0-9]*")\n// ["1234","1256"]\n
\n" + }, + { + "name": "trim", + "type": "function", + "params": [ + { + "name": "string" + } + ], + "info": "

Camunda Extension

\n

Returns the given string without leading and trailing spaces.

\n

Function signature

\n
trim(string: string): string\n
\n

Examples

\n
trim("  hello world  ")\n// "hello world"\n\ntrim("hello   world ")\n// "hello   world"\n
\n" + }, + { + "name": "uuid", + "type": "function", + "params": [], + "info": "

Camunda Extension

\n

Returns a UUID (Universally Unique Identifier) with 36 characters.

\n

Function signature

\n
uuid(): string\n
\n

Examples

\n
uuid()\n// "7793aab1-d761-4d38-916b-b7270e309894"\n
\n" + }, + { + "name": "to base64", + "type": "function", + "params": [ + { + "name": "value" + } + ], + "info": "

Camunda Extension

\n

Returns the given string encoded in Base64 format.

\n

Function signature

\n
to base64(value: string): string\n
\n

Examples

\n
to base64("FEEL")\n// "RkVFTA=="\n
\n" + }, + { + "name": "is blank", + "type": "function", + "params": [ + { + "name": "string" + } + ], + "info": "

Camunda Extension

\n

Returns true if the given string is blank (empty or contains only whitespaces).

\n

Function signature

\n
is blank(string: string): boolean\n
\n

Examples

\n
is blank("")\n// true\n\nis blank(" ")\n// true\n\nis blank("hello world")\n// false\n
\n" }, { "name": "last day of month", @@ -1660,4 +1668,32 @@ export const camundaBuiltins = [ ], "info": "

Camunda Extension

\n

Takes the month of the given date or date-time value and returns the last day of this month.

\n

Function signature

\n
last day of month(date: date): date\n
\n
last day of month(date: date and time): date\n
\n

Examples

\n
last day of month(date("2022-10-01"))\n// date("2022-10-31"))\n\nlast day of month(date and time("2022-10-16T12:00:00"))\n// date("2022-10-31"))\n
\n" } -]; \ No newline at end of file +]; + +/** + * Collection of builtins of camunda scala FEEL. + * + * @type { Builtin[] } + */ +export const camundaBuiltins = [ ...feelBuiltins, ...camundaExtensions ]; + +/** + * Functions using reserved keywords in their name and need to be added to the parser context. + * + * @type { Builtin[] } + */ +export const camundaReservedNameBuiltins = [ + { + "name": "get or else", + "type": "function", + "params": [ + { + "name": "value" + }, + { + "name": "default" + } + ], + "info": "

Camunda Extension

\n

Return the provided value parameter if not null, otherwise return the default parameter

\n

Function signature

\n
get or else(value: Any, default: Any): Any\n
\n

Examples

\n
get or else("this", "default")\n// "this"\n\nget or else(null, "default")\n// "default"\n\nget or else(null, null)\n// null\n
\n" + } +]; diff --git a/src/index.js b/src/index.js index 503e70e..ecb3331 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1 @@ -export { camundaBuiltins } from './camundaBuiltins.js'; +export { camundaBuiltins, feelBuiltins, camundaExtensions, camundaReservedNameBuiltins } from './camundaBuiltins.js'; diff --git a/tasks/camundaBuiltins.template.js b/tasks/camundaBuiltins.template.js index ef2018c..dd9af18 100644 --- a/tasks/camundaBuiltins.template.js +++ b/tasks/camundaBuiltins.template.js @@ -18,8 +18,29 @@ */ /** - * FEEL built-ins available with Camunda / feel-scala. + * List of standard FEEL built-in functions (excluding Camunda-specific extensions). * * @type { Builtin[] } */ -export const camundaBuiltins = /** CAMUNDA_BUILTINS_PLACEHOLDER */ []; \ No newline at end of file +export const feelBuiltins = /** FEEL_BUILTINS_PLACEHOLDER */ []; + +/** + * List of FEEL camunda extensions. + * + * @type { Builtin[] } + */ +export const camundaExtensions = /** CAMUNDA_EXTENSIONS_PLACEHOLDER */ []; + +/** + * Collection of builtins of camunda scala FEEL. + * + * @type { Builtin[] } + */ +export const camundaBuiltins = [ ...feelBuiltins, ...camundaExtensions ]; + +/** + * Functions using reserved keywords in their name and need to be added to the parser context. + * + * @type { Builtin[] } + */ +export const camundaReservedNameBuiltins = /** RESERVED_NAME_BUILTINS_PLACEHOLDER */ []; diff --git a/tasks/compileBuiltins.js b/tasks/compileBuiltins.js index 7c15d80..b5a9c4d 100644 --- a/tasks/compileBuiltins.js +++ b/tasks/compileBuiltins.js @@ -1,71 +1,28 @@ import { glob } from 'glob'; -import { marked } from 'marked'; -import { readFile, writeFile } from 'node:fs/promises'; -import { parseBuiltins } from './utils/parseBuiltins.js'; +import { categorizeBuiltins, logStatistics, parseBuiltins, parseMarkdownFile, writeBuiltinsFromTemplate } from './utils/index.js'; // paths relative to CWD const MARKDOWN_SRC = './camunda-docs/docs/components/modeler/feel/builtin-functions/*.md'; const JS_SRC = './tasks/camundaBuiltins.template.js'; const JS_DEST = './src/camundaBuiltins.js'; -const CAMUNDA_BUILTINS_PLACEHOLDER = '/** CAMUNDA_BUILTINS_PLACEHOLDER */ []'; - -/** - * @typedef { import('./utils/parseBuiltins.js').BuiltinDescriptor } BuiltinDescriptor - */ - -/** - * @param {string} fileName - * - * @return { Promise } - */ -async function parseFile(fileName) { - - const fileContent = await readFile(fileName, 'utf-8'); - - const [ _heading, ...contents ] = fileContent.split('## '); - - const descriptions = await Promise.all( - contents.flatMap(async string => { - const name = string.split('\n')[0]; - let description = await Promise.resolve( - marked.parse(string.split('\n').slice(1).join('\n')) - ); - - description = description.replace('', 'Camunda Extension'); - - // e.g. "and() / all()" - if (name.includes('/')) { - throw new Error(`unsupported built-in name <${ name }>`); - } - - return { name, description }; - }) - ); - - return descriptions; -} - async function run() { - const files = await glob(MARKDOWN_SRC); - const descriptors = ( - await Promise.all( - files.sort().map(parseFile) - ) - ).flat(); + const descriptors = (await Promise.all(files.sort().map(parseMarkdownFile))).flat(); const builtins = parseBuiltins(descriptors); - const template = await readFile(JS_SRC, 'utf-8'); - const content = template.replace(CAMUNDA_BUILTINS_PLACEHOLDER, JSON.stringify(builtins, null, 2)); + // Categorize into FEEL builtins and Camunda extensions + const categorized = categorizeBuiltins(builtins); + + logStatistics(categorized); - await writeFile(JS_DEST, content); + await writeBuiltinsFromTemplate(JS_SRC, JS_DEST, categorized); } -run().catch(err => { +run().catch((err) => { console.error('Failed to compile built-ins', err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/tasks/utils/categorizeBuiltins.js b/tasks/utils/categorizeBuiltins.js new file mode 100644 index 0000000..417147c --- /dev/null +++ b/tasks/utils/categorizeBuiltins.js @@ -0,0 +1,89 @@ +import { parser } from '@bpmn-io/lezer-feel'; + +/** + * Test if a function is parsable + * @param {import('@camunda/feel-builtins').Builtin} builtin + * @returns {boolean} + */ +function isParsable(builtin) { + try { + const paramsList = builtin.params?.map((p) => 'null').join(', ') || ''; + const expression = `${builtin.name}(${paramsList})`; + + const tree = parser.parse(expression); + let hasError = false; + tree.iterate({ + enter: (nodeRef) => { + if (nodeRef.type.isError) { + hasError = true; + return false; // stop iterating deeper on first error + } + } + }); + return !hasError; + } catch (error) { + return false; + } +} + +/** + * Check if a builtin is a Camunda extension + * @param {import('@camunda/feel-builtins').Builtin} builtin + * @returns {boolean} + */ +function isCamundaExtension(builtin) { + return builtin.info?.includes('Camunda Extension') || false; +} + +/** + * Categorize builtins into FEEL standard functions Camunda extensions and list ones using reserved keywords. + * @param {import('@camunda/feel-builtins').Builtin[]} builtins + * @returns {{ + * feelBuiltins: import('@camunda/feel-builtins').Builtin[], + * camundaExtensions: import('@camunda/feel-builtins').Builtin[], + * camundaReservedNameBuiltins: import('@camunda/feel-builtins').Builtin[] + * }} + */ +export function categorizeBuiltins(builtins) { + const feelBuiltins = []; + const camundaExtensions = []; + const camundaReservedNameBuiltins = []; + + for (const builtin of builtins) { + if (!isParsable(builtin)) { + camundaReservedNameBuiltins.push(builtin); + } + + if (isCamundaExtension(builtin)) { + camundaExtensions.push(builtin); + } else { + feelBuiltins.push(builtin); + } + } + + return { + feelBuiltins, + camundaExtensions, + camundaReservedNameBuiltins, + }; +} + +/** + * Log categorization statistics + * @param {{ + * feelBuiltins: import('@camunda/feel-builtins').Builtin[], + * camundaExtensions: import('@camunda/feel-builtins').Builtin[], + * camundaReservedNameBuiltins: import('@camunda/feel-builtins').Builtin[] + * }} categorized + */ +export function logStatistics(categorized) { + const { feelBuiltins, camundaExtensions, camundaReservedNameBuiltins } = categorized; + + console.log(`FEEL built-ins: ${feelBuiltins.length}`); + console.log(`Camunda extensions: ${camundaExtensions.length}`); + console.log(`Camunda extensions with reserved names: ${camundaReservedNameBuiltins.length}`); + + if (camundaReservedNameBuiltins.length > 0) { + console.log('Reserved names:', camundaReservedNameBuiltins.map((b) => b.name).join(', ')); + } +} diff --git a/tasks/utils/index.js b/tasks/utils/index.js new file mode 100644 index 0000000..352bab7 --- /dev/null +++ b/tasks/utils/index.js @@ -0,0 +1,4 @@ +export * from './categorizeBuiltins.js'; +export * from './markdownParser.js'; +export * from './parseBuiltins.js'; +export * from './templateWriter.js'; diff --git a/tasks/utils/markdownParser.js b/tasks/utils/markdownParser.js new file mode 100644 index 0000000..5cd21b6 --- /dev/null +++ b/tasks/utils/markdownParser.js @@ -0,0 +1,35 @@ +import { marked } from 'marked'; +import { readFile } from 'node:fs/promises'; + +/** + * @typedef { { name: string, description: string } } BuiltinDescriptor + */ + +/** + * Parse a markdown file to extract builtin function descriptors + * @param {string} fileName + * @return {Promise} + */ +export async function parseMarkdownFile(fileName) { + const fileContent = await readFile(fileName, 'utf-8'); + + const [ _heading, ...contents ] = fileContent.split('## '); + + const descriptions = await Promise.all( + contents.flatMap(async (string) => { + const name = string.split('\n')[0]; + let description = await Promise.resolve(marked.parse(string.split('\n').slice(1).join('\n'))); + + description = description.replace('', 'Camunda Extension'); + + // e.g. "and() / all()" + if (name.includes('/')) { + throw new Error(`unsupported built-in name <${name}>`); + } + + return { name, description }; + }), + ); + + return descriptions; +} diff --git a/tasks/utils/parseBuiltins.js b/tasks/utils/parseBuiltins.js index 8be7301..c537a9c 100644 --- a/tasks/utils/parseBuiltins.js +++ b/tasks/utils/parseBuiltins.js @@ -1,9 +1,5 @@ /** - * @typedef { { name: string, description: string } } BuiltinDescriptor - */ - -/** - * @param { BuiltinDescriptor[] } descriptors + * @param { import('./markdownParser.js').BuiltinDescriptor[] } descriptors * * @returns {import('@camunda/feel-builtins').Builtin[] } */ @@ -12,7 +8,7 @@ export function parseBuiltins(descriptors) { } /** - * @param { BuiltinDescriptor } descriptor + * @param { import('./markdownParser.js').BuiltinDescriptor } descriptor * * @returns { import('@camunda/feel-builtins').Builtin } */ diff --git a/tasks/utils/templateWriter.js b/tasks/utils/templateWriter.js new file mode 100644 index 0000000..f52e84e --- /dev/null +++ b/tasks/utils/templateWriter.js @@ -0,0 +1,26 @@ +import { readFile, writeFile } from 'node:fs/promises'; + +const FEEL_BUILTINS_PLACEHOLDER = '/** FEEL_BUILTINS_PLACEHOLDER */ []'; +const CAMUNDA_EXTENSIONS_PLACEHOLDER = '/** CAMUNDA_EXTENSIONS_PLACEHOLDER */ []'; +const RESERVED_NAME_BUILTINS_PLACEHOLDER = '/** RESERVED_NAME_BUILTINS_PLACEHOLDER */ []'; + +/** + * Write builtins to the destination file using the template + * @param {string} templatePath + * @param {string} destinationPath + * @param {Object} categorized + * @param {import('@camunda/feel-builtins').Builtin[]} categorized.feelBuiltins + * @param {import('@camunda/feel-builtins').Builtin[]} categorized.camundaExtensions + * @param {import('@camunda/feel-builtins').Builtin[]} categorized.camundaReservedNameBuiltins + */ +export async function writeBuiltinsFromTemplate(templatePath, destinationPath, categorized) { + const { feelBuiltins, camundaExtensions, camundaReservedNameBuiltins } = categorized; + + const template = await readFile(templatePath, 'utf-8'); + let content = template + .replace(FEEL_BUILTINS_PLACEHOLDER, JSON.stringify(feelBuiltins, null, 2)) + .replace(CAMUNDA_EXTENSIONS_PLACEHOLDER, JSON.stringify(camundaExtensions, null, 2)) + .replace(RESERVED_NAME_BUILTINS_PLACEHOLDER, JSON.stringify(camundaReservedNameBuiltins, null, 2)); + + await writeFile(destinationPath, content); +} diff --git a/test/integration/bundle.spec.cjs b/test/integration/bundle.spec.cjs index 414eb1b..33df591 100644 --- a/test/integration/bundle.spec.cjs +++ b/test/integration/bundle.spec.cjs @@ -1,12 +1,12 @@ const { camundaBuiltins } = require('@camunda/feel-builtins'); +const { expect } = require('chai'); + describe('integration - bundle', function() { it('should export CJS export', async function() { - const { expect } = await import('chai'); - // then expect(camundaBuiltins).not.to.be.empty; }); diff --git a/test/spec/lib/camundaBuiltins.spec.js b/test/spec/lib/camundaBuiltins.spec.js index bea8008..4a0c14d 100644 --- a/test/spec/lib/camundaBuiltins.spec.js +++ b/test/spec/lib/camundaBuiltins.spec.js @@ -1,21 +1,58 @@ import { expect } from 'chai'; -import { camundaBuiltins } from '@camunda/feel-builtins'; +import { + camundaBuiltins, + feelBuiltins, + camundaExtensions, + camundaReservedNameBuiltins +} from '@camunda/feel-builtins'; -describe('lib/camundaBuiltins', function() { +describe('camundaBuiltins', function() { - it('should export ALL built-ins', function() { + it('should export ALL builtins', function() { + + // testing if the count of builtin changes, if it does we have + // to adjust from chore to feat and do a minor release // then expect(camundaBuiltins).to.be.an('array').with.length(135); + expect(camundaReservedNameBuiltins).to.be.an('array').with.length(1); + }); + + + it('should export feelBuiltins', function() { + + // then + expectBuiltin(feelBuiltins, 'not'); + }); + + + it('should export camundaExtensions', function() { + + // then + expectBuiltin(camundaExtensions, 'get or else'); }); - it('should export parameterized built-in', function() { + it('should export camundaReservedNameBuiltins', function() { // then - expectBuiltin('get or else', { + expectBuiltin(camundaReservedNameBuiltins, 'get or else'); + }); + + + it('should export camundaBuiltins', function() { + + // then + expect(camundaBuiltins).to.have.length(feelBuiltins.length + camundaExtensions.length); + }); + + + it('should export parameterized builtin', function() { + + // then + expectBuiltinProperties(camundaBuiltins, 'get or else', { name: 'get or else', type: 'function', params: [ { name: 'value' }, { name: 'default' } ], @@ -23,10 +60,10 @@ describe('lib/camundaBuiltins', function() { }); - it('should export parameterless built-in', function() { + it('should export parameterless builtin', function() { // then - expectBuiltin('random number', { + expectBuiltinProperties(camundaBuiltins, 'random number', { name: 'random number', type: 'function', params: [] @@ -39,12 +76,13 @@ describe('lib/camundaBuiltins', function() { // helpers ///////// /** + * @param {import('@camunda/feel-builtins').Builtin[]} builtins * @param {string} name * * @return {import('@camunda/feel-builtins').Builtin} */ -function findBuiltin(name) { - const builtin = camundaBuiltins.find(builtin => builtin.name === name); +function expectBuiltin(builtins, name) { + const builtin = builtins.find(builtin => builtin.name === name); if (!builtin) { throw expect(builtin, `builtin with name <${name}>`).to.exist; @@ -54,11 +92,13 @@ function findBuiltin(name) { } /** + * @param {import('@camunda/feel-builtins').Builtin[]} builtins * @param {string} name + * * @param {Record} expectedProperties */ -function expectBuiltin(name, expectedProperties) { - const builtin = findBuiltin(name); +function expectBuiltinProperties(builtins, name, expectedProperties) { + const builtin = expectBuiltin(builtins, name); expect(builtin).to.deep.include(expectedProperties); expect(builtin).to.have.property('info'); diff --git a/tsconfig.json b/tsconfig.json index 6a37f6b..e070081 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,8 @@ "strict": true, "checkJs": true, "module": "nodenext", - "moduleResolution": "nodenext" + "moduleResolution": "nodenext", + "skipLibCheck": true }, "include": [ "src",