Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,44 @@ parser.evaluate('toUpper(trim(left(" hello world ", 10)))'); // "HELLO WOR"

> **Note:** All string functions return `undefined` if any of their required arguments are `undefined`, allowing for safe chaining and conditional logic.

## Object Manipulation Functions

The parser includes functions for working with objects.

| Function | Description |
|:--------------------- |:----------- |
| merge(obj1, obj2, ...)| Merges two or more objects together. Duplicate keys are overwritten by later arguments. |
| keys(obj) | Returns an array of strings containing the keys of the object. |
| values(obj) | Returns an array containing the values of the object. |
| flatten(obj, sep?) | Flattens a nested object's keys using an optional separator (default: `_`). For example, `{foo: {bar: 1}}` becomes `{foo_bar: 1}`. |

### Object Function Examples

```js
const parser = new Parser();

// Merge objects
parser.evaluate('merge({a: 1}, {b: 2})'); // {a: 1, b: 2}
parser.evaluate('merge({a: 1, b: 2}, {b: 3, c: 4})'); // {a: 1, b: 3, c: 4}
parser.evaluate('merge({a: 1}, {b: 2}, {c: 3})'); // {a: 1, b: 2, c: 3}

// Get keys
parser.evaluate('keys({a: 1, b: 2, c: 3})'); // ["a", "b", "c"]

// Get values
parser.evaluate('values({a: 1, b: 2, c: 3})'); // [1, 2, 3]

// Flatten nested objects
parser.evaluate('flatten(obj)', { obj: { foo: { bar: 1 } } }); // {foo_bar: 1}
parser.evaluate('flatten(obj)', { obj: { a: { b: { c: 1 } } } }); // {a_b_c: 1}
parser.evaluate('flatten(obj, ".")', { obj: { foo: { bar: 1 } } }); // {"foo.bar": 1}

// Mixed nested and flat keys
parser.evaluate('flatten(obj)', { obj: { a: 1, b: { c: 2 } } }); // {a: 1, b_c: 2}
```

> **Note:** All object functions return `undefined` if any of their required arguments are `undefined`, allowing for safe chaining and conditional logic.

## Array Literals

Arrays can be created by including the elements inside square `[]` brackets, separated by commas. For example:
Expand Down
1 change: 1 addition & 0 deletions src/functions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './math';
export * from './array';
export * from './utility';
export * from './string';
export * from './object';
6 changes: 6 additions & 0 deletions src/functions/object/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Object functions exports
* Re-exports all object manipulation functions
*/

export * from './operations.js';
102 changes: 102 additions & 0 deletions src/functions/object/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Object manipulation functions
* Provides comprehensive object operations for the expression parser
*/

import { Value, ValueObject } from '../../types/values.js';

/**
* Merges two or more objects together.
* Duplicate keys will be overwritten by later arguments.
* @param objects - Objects to merge
* @returns Merged object or undefined if any argument is undefined
*/
export function merge(...objects: (ValueObject | undefined)[]): ValueObject | undefined {
if (objects.length === 0) {
return {};
}

for (const obj of objects) {
if (obj === undefined) {
return undefined;
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error('Arguments to merge must be objects');
}
}

return Object.assign({}, ...objects);
}

/**
* Returns an array of the keys of an object.
* @param obj - The object to get keys from
* @returns Array of string keys or undefined if input is undefined
*/
export function keys(obj: ValueObject | undefined): string[] | undefined {
if (obj === undefined) {
return undefined;
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error('Argument to keys must be an object');
}
return Object.keys(obj);
}

/**
* Returns an array of the values of an object.
* @param obj - The object to get values from
* @returns Array of values or undefined if input is undefined
*/
export function values(obj: ValueObject | undefined): Value[] | undefined {
if (obj === undefined) {
return undefined;
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error('Argument to values must be an object');
}
return Object.values(obj);
}

/**
* Flattens a nested object's keys using underscore as separator.
* For example: { foo: { bar: 1 } } becomes { foo_bar: 1 }
* @param obj - The object to flatten
* @param separator - The separator to use (default: '_')
* @returns Flattened object or undefined if input is undefined
*/
export function flatten(
obj: ValueObject | undefined,
separator: string = '_'
): ValueObject | undefined {
if (obj === undefined) {
return undefined;
}
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
throw new Error('First argument to flatten must be an object');
}
if (typeof separator !== 'string') {
throw new Error('Second argument to flatten must be a string');
}

const result: ValueObject = {};

function flattenHelper(current: Value, prefix: string): void {
if (
current !== null &&
typeof current === 'object' &&
!Array.isArray(current)
) {
for (const key of Object.keys(current as ValueObject)) {
const newKey = prefix ? `${prefix}${separator}${key}` : key;
flattenHelper((current as ValueObject)[key], newKey);
}
} else {
result[prefix] = current;
}
}

flattenHelper(obj, '');

return result;
}
32 changes: 32 additions & 0 deletions src/language-service/language-service.documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,38 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
params: [
{ name: 'values', description: 'Values to check.', isVariadic: true }
]
},
/**
* Object functions
*/
merge: {
name: 'merge',
description: 'Merge two or more objects together. Duplicate keys are overwritten by later arguments.',
params: [
{ name: 'objects', description: 'Objects to merge.', isVariadic: true }
]
},
keys: {
name: 'keys',
description: 'Return an array of strings containing the keys of the object.',
params: [
{ name: 'obj', description: 'Input object.' }
]
},
values: {
name: 'values',
description: 'Return an array containing the values of the object.',
params: [
{ name: 'obj', description: 'Input object.' }
]
},
flatten: {
name: 'flatten',
description: 'Flatten a nested object\'s keys using an optional separator (default: _). For example, {foo: {bar: 1}} becomes {foo_bar: 1}.',
params: [
{ name: 'obj', description: 'Input object.' },
{ name: 'separator', description: 'Key separator (default: _).', optional: true }
]
}
};

Expand Down
9 changes: 7 additions & 2 deletions src/parsing/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Expression } from '../core/expression.js';
import type { Value, VariableResolveResult, Values } from '../types/values.js';
import type { Instruction } from './instruction.js';
import type { OperatorFunction } from '../types/parser.js';
import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesceString } from '../functions/index.js';
import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth, slice, urlEncode, base64Encode, base64Decode, coalesceString, merge, keys, values, flatten } from '../functions/index.js';
import {
add,
sub,
Expand Down Expand Up @@ -224,7 +224,12 @@ export class Parser {
urlEncode: urlEncode,
base64Encode: base64Encode,
base64Decode: base64Decode,
coalesce: coalesceString
coalesce: coalesceString,
// Object manipulation functions
merge: merge,
keys: keys,
values: values,
flatten: flatten
};

this.numericConstants = {
Expand Down
166 changes: 166 additions & 0 deletions test/functions/functions-object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/* global describe, it */

import assert from 'assert';
import { Parser } from '../../index';

describe('Object Functions TypeScript Test', function () {
describe('merge(obj1, obj2, ...)', function () {
it('should return empty object when called with no arguments', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('merge()'), {});
});

it('should return the object when called with one argument', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('merge({a: 1})'), { a: 1 });
});

it('should merge two objects', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('merge({a: 1}, {b: 2})'), { a: 1, b: 2 });
});

it('should override duplicate keys with later arguments', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('merge({a: 1, b: 2}, {b: 3, c: 4})'), { a: 1, b: 3, c: 4 });
});

it('should merge three or more objects', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('merge({a: 1}, {b: 2}, {c: 3})'),
{ a: 1, b: 2, c: 3 }
);
});

it('should return undefined if any argument is undefined', function () {
const parser = new Parser();
assert.strictEqual(parser.evaluate('merge({a: 1}, undefined)'), undefined);
assert.strictEqual(parser.evaluate('merge(undefined, {a: 1})'), undefined);
});

it('should work with variables', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('merge(obj1, obj2)', { obj1: { x: 10 }, obj2: { y: 20 } }),
{ x: 10, y: 20 }
);
});
});

describe('keys(obj)', function () {
it('should return empty array for empty object', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('keys({})'), []);
});

it('should return keys of an object', function () {
const parser = new Parser();
const result = parser.evaluate('keys({a: 1, b: 2, c: 3})') as string[];
assert.strictEqual(result.length, 3);
assert(result.includes('a'));
assert(result.includes('b'));
assert(result.includes('c'));
});

it('should return undefined if argument is undefined', function () {
const parser = new Parser();
assert.strictEqual(parser.evaluate('keys(undefined)'), undefined);
});

it('should work with variables', function () {
const parser = new Parser();
const result = parser.evaluate('keys(obj)', { obj: { foo: 'bar', baz: 42 } }) as string[];
assert.strictEqual(result.length, 2);
assert(result.includes('foo'));
assert(result.includes('baz'));
});
});

describe('values(obj)', function () {
it('should return empty array for empty object', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('values({})'), []);
});

it('should return values of an object', function () {
const parser = new Parser();
const result = parser.evaluate('values({a: 1, b: 2, c: 3})') as number[];
assert.strictEqual(result.length, 3);
assert(result.includes(1));
assert(result.includes(2));
assert(result.includes(3));
});

it('should return undefined if argument is undefined', function () {
const parser = new Parser();
assert.strictEqual(parser.evaluate('values(undefined)'), undefined);
});

it('should work with variables', function () {
const parser = new Parser();
const result = parser.evaluate('values(obj)', { obj: { foo: 'bar', baz: 42 } });
assert(Array.isArray(result));
assert.strictEqual((result as any[]).length, 2);
assert((result as any[]).includes('bar'));
assert((result as any[]).includes(42));
});
});

describe('flatten(obj)', function () {
it('should return empty object for empty object', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('flatten({})'), {});
});

it('should return same object for flat object', function () {
const parser = new Parser();
assert.deepStrictEqual(parser.evaluate('flatten({a: 1, b: 2})'), { a: 1, b: 2 });
});

it('should flatten nested object', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('flatten(obj)', { obj: { foo: { bar: 1 } } }),
{ foo_bar: 1 }
);
});

it('should flatten deeply nested object', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('flatten(obj)', { obj: { a: { b: { c: 1 } } } }),
{ a_b_c: 1 }
);
});

it('should handle mixed nested and flat keys', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('flatten(obj)', { obj: { a: 1, b: { c: 2, d: 3 }, e: 4 } }),
{ a: 1, b_c: 2, b_d: 3, e: 4 }
);
});

it('should preserve arrays as values', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('flatten(obj)', { obj: { a: { b: [1, 2, 3] } } }),
{ a_b: [1, 2, 3] }
);
});

it('should return undefined if argument is undefined', function () {
const parser = new Parser();
assert.strictEqual(parser.evaluate('flatten(undefined)'), undefined);
});

it('should use custom separator when provided', function () {
const parser = new Parser();
assert.deepStrictEqual(
parser.evaluate('flatten(obj, ".")', { obj: { foo: { bar: 1 } } }),
{ 'foo.bar': 1 }
);
});
});
});