From 8242f18b8a150b61bf8332ffd69a028491f1de84 Mon Sep 17 00:00:00 2001 From: spastorelli Date: Mon, 13 Apr 2026 12:19:52 +0200 Subject: [PATCH] Extend gitbook/expr std lib with some additional methods (#4173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Samy Pessé --- .changeset/few-pandas-crash.md | 5 ++ .../expr/src/__tests__/autocomplete.test.ts | 45 +++++++++++++- packages/expr/src/__tests__/runtime.test.ts | 42 ++++++++++++++ .../src/symbols/__tests__/symbols.test.ts | 12 ++++ packages/expr/src/symbols/symbols.ts | 58 +++++++++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 .changeset/few-pandas-crash.md diff --git a/.changeset/few-pandas-crash.md b/.changeset/few-pandas-crash.md new file mode 100644 index 000000000..7c8d3445d --- /dev/null +++ b/.changeset/few-pandas-crash.md @@ -0,0 +1,5 @@ +--- +"@gitbook/expr": patch +--- + +Extend gitbook/expr std lib with some additional methods diff --git a/packages/expr/src/__tests__/autocomplete.test.ts b/packages/expr/src/__tests__/autocomplete.test.ts index 321407c4d..64c7647ec 100644 --- a/packages/expr/src/__tests__/autocomplete.test.ts +++ b/packages/expr/src/__tests__/autocomplete.test.ts @@ -25,6 +25,7 @@ describe('autocomplete', () => { description: 'An array of string', items: SymbolString(), }); + const visitorClaimsKeyPropStringSymbol = SymbolString({ name: 'key' }); const symbols = { visitor: SymbolObject({ name: 'visitor', @@ -33,7 +34,7 @@ describe('autocomplete', () => { name: 'claims', description: 'The claims contained in the visitor JWT token', properties: { - key: SymbolString({ name: 'key' }), + key: visitorClaimsKeyPropStringSymbol, flags: SymbolObject({ name: 'flags', properties: { @@ -168,8 +169,12 @@ describe('autocomplete', () => { childrenRefs: [ 'visitor.claims.key.length', 'visitor.claims.key.at', + 'visitor.claims.key.startsWith', 'visitor.claims.key.endsWith', 'visitor.claims.key.includes', + 'visitor.claims.key.toLowerCase', + 'visitor.claims.key.toUpperCase', + 'visitor.claims.key.trim', ], }, }, @@ -227,8 +232,12 @@ describe('autocomplete', () => { childrenRefs: [ 'visitor.claims.role.length', 'visitor.claims.role.at', + 'visitor.claims.role.startsWith', 'visitor.claims.role.endsWith', 'visitor.claims.role.includes', + 'visitor.claims.role.toLowerCase', + 'visitor.claims.role.toUpperCase', + 'visitor.claims.role.trim', ], }, }, @@ -246,13 +255,47 @@ describe('autocomplete', () => { childrenRefs: [ 'visitor.claims.key.length', 'visitor.claims.key.at', + 'visitor.claims.key.startsWith', 'visitor.claims.key.endsWith', 'visitor.claims.key.includes', + 'visitor.claims.key.toLowerCase', + 'visitor.claims.key.toUpperCase', + 'visitor.claims.key.trim', ], }, }, ], }, + { + expressionWithCursor: 'visitor.claims.key.', + expectedSuggestions: [ + { + type: 'symbol', + symbol: { + definition: SymbolNumber({ + name: 'length', + description: + 'The length data property of a String value contains the length of the string in UTF-16 code units.', + link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length', + }), + ref: 'visitor.claims.key.length', + parentRef: 'visitor.claims.key', + childrenRefs: [], + }, + }, + ...visitorClaimsKeyPropStringSymbol.methods.map( + (method) => ({ + type: 'symbol', + symbol: { + definition: method, + ref: `visitor.claims.key.${method.name}`, + parentRef: 'visitor.claims.key', + childrenRefs: [], + }, + }) + ), + ], + }, { expressionWithCursor: 'visitor.claims.h', expectedSuggestions: [ diff --git a/packages/expr/src/__tests__/runtime.test.ts b/packages/expr/src/__tests__/runtime.test.ts index fbc648be1..756484469 100644 --- a/packages/expr/src/__tests__/runtime.test.ts +++ b/packages/expr/src/__tests__/runtime.test.ts @@ -54,12 +54,54 @@ describe('ExpressionRuntime', () => { inputs: { reviews: [{ status: 'approved' }, { status: 'approved' }] }, expectedResult: true, }, + { + scenario: 'array includes', + condition: 'reviews.includes("approved")', + inputs: { reviews: ['pending', 'approved'] }, + expectedResult: true, + }, { scenario: 'array map', condition: '[1, 2, 3].map(n => n * x)', inputs: { x: 2 }, expectedResult: [2, 4, 6], }, + { + scenario: 'string startsWith', + condition: 'user.role.startsWith("ad")', + inputs: { user: { role: 'admin' } }, + expectedResult: true, + }, + { + scenario: 'string endsWith', + condition: 'user.role.endsWith("min")', + inputs: { user: { role: 'admin' } }, + expectedResult: true, + }, + { + scenario: 'string includes', + condition: 'user.role.includes("dm")', + inputs: { user: { role: 'admin' } }, + expectedResult: true, + }, + { + scenario: 'string toLowerCase', + condition: 'user.role.toLowerCase() === "admin"', + inputs: { user: { role: 'ADMIN' } }, + expectedResult: true, + }, + { + scenario: 'string toUpperCase', + condition: 'user.role.toUpperCase() === "ADMIN"', + inputs: { user: { role: 'admin' } }, + expectedResult: true, + }, + { + scenario: 'string trim', + condition: 'user.role.trim() === "admin"', + inputs: { user: { role: ' admin ' } }, + expectedResult: true, + }, ])( 'should properly evaluate/safeEvaluate a valid conditional expression: $scenario', ({ condition, inputs, expectedResult }) => { diff --git a/packages/expr/src/symbols/__tests__/symbols.test.ts b/packages/expr/src/symbols/__tests__/symbols.test.ts index 16a4abeef..4ca601788 100644 --- a/packages/expr/src/symbols/__tests__/symbols.test.ts +++ b/packages/expr/src/symbols/__tests__/symbols.test.ts @@ -255,8 +255,12 @@ describe('ExpressionRuntime', () => { childrenRefs: [ 'visitor.claims.key.length', 'visitor.claims.key.at', + 'visitor.claims.key.startsWith', 'visitor.claims.key.endsWith', 'visitor.claims.key.includes', + 'visitor.claims.key.toLowerCase', + 'visitor.claims.key.toUpperCase', + 'visitor.claims.key.trim', ], }); @@ -301,8 +305,12 @@ describe('ExpressionRuntime', () => { childrenRefs: [ 'visitor.claims.flags.FLAG1.length', 'visitor.claims.flags.FLAG1.at', + 'visitor.claims.flags.FLAG1.startsWith', 'visitor.claims.flags.FLAG1.endsWith', 'visitor.claims.flags.FLAG1.includes', + 'visitor.claims.flags.FLAG1.toLowerCase', + 'visitor.claims.flags.FLAG1.toUpperCase', + 'visitor.claims.flags.FLAG1.trim', ], }); @@ -318,8 +326,12 @@ describe('ExpressionRuntime', () => { childrenRefs: [ 'visitor.claims.flags.FLAG2.length', 'visitor.claims.flags.FLAG2.at', + 'visitor.claims.flags.FLAG2.startsWith', 'visitor.claims.flags.FLAG2.endsWith', 'visitor.claims.flags.FLAG2.includes', + 'visitor.claims.flags.FLAG2.toLowerCase', + 'visitor.claims.flags.FLAG2.toUpperCase', + 'visitor.claims.flags.FLAG2.trim', ], }); diff --git a/packages/expr/src/symbols/symbols.ts b/packages/expr/src/symbols/symbols.ts index 78ab9d2f0..22dc35f2a 100644 --- a/packages/expr/src/symbols/symbols.ts +++ b/packages/expr/src/symbols/symbols.ts @@ -156,6 +156,31 @@ const StandardLibrary: Partial< members: [SymbolString(), SymbolUndefined()], }), }), + SymbolFunction({ + name: 'startsWith', + description: `Returns true if the given characters are found at the beginning of the string, including when searchString + is an empty string. Otherwise returns false.`, + link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith', + args: [ + SymbolString({ + name: 'searchString', + description: `The characters to be searched for at the start of this string. Cannot be a regex. + All values that are not regexes are coerced to strings, so omitting it or passing undefined causes startsWith() to search for + the string "undefined", which is rarely what you want.`, + }), + OptionalFunctionArg( + SymbolNumber({ + name: 'position', + description: `The start position at which searchString is expected to be found + (the index of searchString's first character). Defaults to 0.`, + }) + ), + ], + returns: SymbolBoolean({ + description: `true if the given characters are found at the beginning of the string, including when searchString is an empty string; + otherwise, false.`, + }), + }), SymbolFunction({ name: 'endsWith', description: `Returns true if the sequence of elements of searchString converted to a String is the same as the corresponding @@ -205,6 +230,39 @@ const StandardLibrary: Partial< otherwise, false.`, }), }), + SymbolFunction({ + name: 'toLowerCase', + description: + 'Returns the value of the string converted to lower case. toLowerCase() does not affect the value of the string str itself.', + link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase', + args: [], + returns: SymbolString({ + description: + 'A new string representing the calling string converted to lower case.', + }), + }), + SymbolFunction({ + name: 'toUpperCase', + description: + 'Returns the value of the string converted to uppercase. toUpperCase() does not affect the value of the string str itself.', + link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase', + args: [], + returns: SymbolString({ + description: + 'A new string representing the calling string converted to upper case.', + }), + }), + SymbolFunction({ + name: 'trim', + description: + 'Returns new string representing str stripped of whitespace from both its beginning and end.', + link: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim', + args: [], + returns: SymbolString({ + description: + 'A new string representing str stripped of whitespace from both its beginning and end.', + }), + }), ], }, [SymbolType.Array]: (arraySymbolDef: ArraySymbolDef) => ({