Skip to content

Commit

Permalink
feat(json-expression): 🎸 implement "o.set" operator
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 17, 2024
1 parent 84c5aeb commit bd05779
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 16 deletions.
54 changes: 43 additions & 11 deletions src/json-expression/__tests__/jsonExpressionUnitTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,49 @@ export const jsonExpressionUnitTests = (
);
});
});

describe('o.set', () => {
test('can set an object property', () => {
check(['o.set', {}, 'foo', 'bar'], {foo: 'bar'});
});

test('can set two properties, one computed', () => {
const expression: Expr = ['o.set', {},
'foo', 'bar',
'baz', ['+', ['$', ''], 3],
];
check(expression, {
foo: 'bar',
baz: 5,
}, 2);
});

test('can retrieve object from input', () => {
const expression: Expr = ['o.set', ['$', '/obj'],
'foo', 123,
];
check(expression, {
type: 'the-obj',
foo: 123,
}, {
obj: {
type: 'the-obj'
},
});
});

test('can compute prop from expression', () => {
const expression: Expr = ['o.set', {a: 'b'},
['.', ['$', '/name'], '_test'], ['+', 5, 5],
];
check(expression, {
a: 'b',
'Mac_test': 10,
}, {
name: 'Mac'
});
});
});
});

describe('Branching operators', () => {
Expand Down Expand Up @@ -2215,17 +2258,6 @@ export const jsonExpressionUnitTests = (
baz: 5,
}, 2);
});

test('can set two properties, one computed', () => {
const expression: Expr = ['jp.add', {},
'/foo', 'bar',
'/baz', ['+', ['$', ''], 3],
];
check(expression, {
foo: 'bar',
baz: 5,
}, 2);
});
});
});
};
1 change: 1 addition & 0 deletions src/json-expression/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class JsonExpressionCodegen {

public compile() {
const fn = this.codegen.compile();
// console.log('fn', fn.toString());
return (ctx: types.JsonExpressionExecutionContext) => {
try {
return fn(ctx);
Expand Down
73 changes: 72 additions & 1 deletion src/json-expression/operators/object.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import * as util from '../util';
import {Expression, ExpressionResult} from '../codegen-steps';
import {Expression, ExpressionResult, Literal} from '../codegen-steps';
import type * as types from '../types';

const validateSetOperandCount = (count: number) => {
if (count < 3) {
throw new Error('Not enough operands for "o.set" operand.');
}
if (count % 2 !== 0) {
throw new Error('Invalid number of operands for "o.set" operand.');
}
};

export const objectOperators: types.OperatorDefinition<any>[] = [
[
'keys',
Expand Down Expand Up @@ -47,4 +56,66 @@ export const objectOperators: types.OperatorDefinition<any>[] = [
return new Expression(js);
},
] as types.OperatorDefinition<types.ExprEntries>,

[
'o.set',
[],
-1,
/**
* Set one or more properties on an object.
*
* ```
* ['o.set', {},
* 'a', 1,
* 'b', ['+', 2, 3],
* ]
* ```
*
* Results in:
*
* ```
* {
* a: 1,
* b: 5,
* }
* ```
*/
(expr: types.ExprObjectSet, ctx) => {
let i = 1;
const length = expr.length;
validateSetOperandCount(length);
let doc = util.asObj(ctx.eval(expr[i++], ctx)) as Record<string, unknown>;
while (i < length) {
const prop = util.str(expr[i++]) as string;
const value = ctx.eval(expr[i++], ctx);
doc[prop] = value;
}
return doc;
},
(ctx: types.OperatorCodegenCtx<types.ExprObjectSet>): ExpressionResult => {
const length = ctx.operands.length;
validateSetOperandCount(length + 1);
let i = 0;
let curr = ctx.operands[i++];
if (curr instanceof Literal) {
curr = new Literal(util.asObj(curr.val));
} else if (curr instanceof Expression) {
ctx.link(util.asObj, 'asObj');
curr = new Expression(`asObj(${curr})`);
}
ctx.link(util.str, 'str');
ctx.link(util.setRaw, 'setRaw');
while (i < length) {
let prop = ctx.operands[i++];
if (prop instanceof Literal) {
prop = new Literal(util.str(prop.val));
} else if (prop instanceof Expression) {
prop = new Expression(`str(${prop})`);
}
const value = ctx.operands[i++];
curr = new Expression(`setRaw(${curr}, ${prop}, ${value})`);
}
return curr;
},
] as types.OperatorDefinition<types.ExprObjectSet>,
];
6 changes: 3 additions & 3 deletions src/json-expression/operators/patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {find} from '../../json-pointer/find';

const validateAddOperandCount = (count: number) => {
if (count < 3) {
throw new Error('Not enough arguments for "jp.add" operand.');
throw new Error('Not enough operands for "jp.add" operand.');
}
if (count % 2 !== 0) {
throw new Error('Invalid number of arguments for "jp.add" operand.');
throw new Error('Invalid number of operands for "jp.add" operand.');
}
};

Expand Down Expand Up @@ -83,7 +83,7 @@ export const patchOperators: types.OperatorDefinition<any>[] = [
const path = expr[i++];
validateAddPath(path);
const value = ctx.eval(expr[i++], ctx);
const {obj, key, val} = find(doc, toPath(path));
const {obj, key} = find(doc, toPath(path));
if (!obj) doc = value;
else if (typeof key === 'string') (obj as any)[key] = value;
else if (obj instanceof Array) {
Expand Down
3 changes: 2 additions & 1 deletion src/json-expression/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,11 +226,12 @@ export type ExprMap = TernaryExpression<'map'>;
export type ExprReduce = QuinaryExpression<'reduce'>;

// Object expressions
export type ObjectExpression = ExprKeys | ExprValues | ExprEntries;
export type ObjectExpression = ExprKeys | ExprValues | ExprEntries | ExprObjectSet;

export type ExprKeys = UnaryExpression<'keys'>;
export type ExprValues = UnaryExpression<'values'>;
export type ExprEntries = UnaryExpression<'entries'>;
export type ExprObjectSet = VariadicExpression<'o.set'>;

// Bitwise expressions
export type BitwiseExpression = ExprBitAnd | ExprBitOr | ExprBitXor | ExprBitNot;
Expand Down
11 changes: 11 additions & 0 deletions src/json-expression/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,17 @@ export const entries = (value: unknown): [key: string, value: unknown][] => {
return entries;
};

export const set = (obj: unknown, key: unknown, value: unknown): Record<string, unknown> => {
const obj2 = asObj(obj) as Record<string, unknown>;
obj2[str(key)] = value;
return obj2 as Record<string, unknown>;
};

export const setRaw = (obj: Record<string, unknown>, key: string, value: unknown): Record<string, unknown> => {
obj[str(key)] = value;
return obj;
};

// -------------------------------------------------------------------- Various

export const isLiteral = (value: unknown): boolean => {
Expand Down

0 comments on commit bd05779

Please sign in to comment.