Skip to content

Commit

Permalink
feat(json-expression): 🎸 add JSON Patch add "jp.add" implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 17, 2024
1 parent 5059463 commit 2bbe3cd
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 3 deletions.
2 changes: 0 additions & 2 deletions src/json-expression/__tests__/codegen.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ const check = (
operators: operatorsMap,
});
const fn = codegen.run().compile();
// console.log(codegen.generate().js);
// console.log(fn.toString());
const result = fn({vars: new Vars(data)});
expect(result).toStrictEqual(expected);
};
Expand Down
19 changes: 19 additions & 0 deletions src/json-expression/__tests__/jsonExpressionUnitTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2198,4 +2198,23 @@ export const jsonExpressionUnitTests = (
});
});
});

describe('JSON Patch operators', () => {
describe('jp.add', () => {
test('can set an object property', () => {
check(['jp.add', {}, '/foo', 'bar'], {foo: 'bar'});
});

test('can set two properties, one computed', () => {
const expression: Expr = ['jp.add', {},
'/foo', 'bar',
'/baz', ['+', ['$', ''], 3],
];
check(expression, {
foo: 'bar',
baz: 5,
}, 2);
});
});
});
};
2 changes: 2 additions & 0 deletions src/json-expression/operators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {objectOperators} from './object';
import {branchingOperators} from './branching';
import {inputOperators} from './input';
import {bitwiseOperators} from './bitwise';
import {patchOperators} from './patch';

export const operators = [
...arithmeticOperators,
Expand All @@ -25,6 +26,7 @@ export const operators = [
...branchingOperators,
...inputOperators,
...bitwiseOperators,
...patchOperators,
];

export const operatorsMap = operatorsToMap(operators);
108 changes: 108 additions & 0 deletions src/json-expression/operators/patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {Expression, ExpressionResult} from '../codegen-steps';
import {OpAdd, applyOp} from '../../json-patch';
import type * as types from '../types';
import {Path, toPath} from '../../json-pointer';
import {JavaScript, JavaScriptLinked, compileClosure} from '@jsonjoy.com/util/lib/codegen';
import {$findRef} from '../../json-pointer/codegen/findRef';

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

const validateAddPath = (path: unknown) => {
if (typeof path !== 'string') {
throw new Error('The "path" argument for "jp.add" must be a const string.');
}
};

type AddFn = (doc: unknown, value: unknown) => unknown;

export const $$add = (path: Path): JavaScriptLinked<AddFn> => {
const find = $findRef(path);
const js = /* js */ `
(function(find, path){
return function(doc, value){
var f = find(doc);
var obj = f.obj, key = f.key, val = f.val;
if (!obj) doc = value;
else if (typeof key === 'string') obj[key] = value;
else {
var length = obj.length;
if (key < length) obj.splice(key, 0, value);
else if (key > length) throw new Error('INVALID_INDEX');
else obj.push(value);
}
return doc;
};
})`;

return {
deps: [find] as unknown[],
js: js as JavaScript<(...deps: unknown[]) => AddFn>,
};
};

export const $add = (path: Path): AddFn => compileClosure($$add(path));


export const patchOperators: types.OperatorDefinition<any>[] = [
[
'jp.add',
[],
-1,
/**
* Applies JSON Patch "add" operations to the input value.
*
* ```
* ['add', {},
* '/a', 1,
* '/b', ['+', 2, 3],
* ]
* ```
*
* Results in:
*
* ```
* {
* a: 1,
* b: 5,
* }
* ```
*/
(expr: types.JsonPatchAdd, ctx) => {
let i = 1;
const length = expr.length;
validateAddOperandCount(length);
let doc = ctx.eval(expr[i++], ctx);
while (i < length) {
const path = expr[i++];
validateAddPath(path);
const value = ctx.eval(expr[i++], ctx);
const res = applyOp(doc, new OpAdd(toPath(path), value), true);
doc = res.doc;
}
return doc;
},
(ctx: types.OperatorCodegenCtx<types.JsonPatchAdd>): ExpressionResult => {
const expr = ctx.expr;
const length = ctx.operands.length;
validateAddOperandCount(length + 1);
let i = 0;
let curr = ctx.operands[i++];
while (i < length) {
const path = expr[1 + i++];
validateAddPath(path);
const value = ctx.operands[i++];
const addCompiled = $add(toPath(path));
const dAdd = ctx.link(addCompiled);
curr = new Expression(`${dAdd}(${curr}, ${value})`);
}
return curr;
},
] as types.OperatorDefinition<types.JsonPatchAdd>,
];
8 changes: 7 additions & 1 deletion src/json-expression/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ export type InputExpression = ExprGet | ExprDefined;
export type ExprGet = UnaryExpression<'get' | '$'> | BinaryExpression<'get' | '$'>;
export type ExprDefined = UnaryExpression<'get?' | '$?'>;

// JSON Patch expressions
export type JsonPatchExpression = JsonPatchAdd;

export type JsonPatchAdd = VariadicExpression<'jp.add'>;

export type Expr =
| ArithmeticExpression
| ComparisonExpression
Expand All @@ -264,7 +269,8 @@ export type Expr =
| ObjectExpression
| BitwiseExpression
| BranchingExpression
| InputExpression;
| InputExpression
| JsonPatchExpression;

export interface JsonExpressionExecutionContext {
vars: Vars;
Expand Down

0 comments on commit 2bbe3cd

Please sign in to comment.