Skip to content

Commit 61bea73

Browse files
authored
Merge pull request #943 from streamich/string-diff-2
Add normalization and overlap detection in string diffing
2 parents 944e74d + 6f5fe06 commit 61bea73

File tree

5 files changed

+646
-36
lines changed

5 files changed

+646
-36
lines changed

src/json-path/JsonPathEval.ts

Lines changed: 56 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import type * as types from './types';
55
/**
66
* Function signature for JSONPath functions
77
*/
8-
type JSONPathFunction = (args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval) => any;
8+
type JSONPathFunction = (
9+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
10+
currentNode: Value,
11+
evaluator: JsonPathEval,
12+
) => any;
913

1014
export class JsonPathEval {
1115
public static run = (path: string | types.JSONPath, data: unknown): Value[] => {
@@ -355,23 +359,23 @@ export class JsonPathEval {
355359

356360
private evalFunctionExpression(expression: types.FunctionExpression, currentNode: Value): boolean {
357361
const result = this.evaluateFunction(expression, currentNode);
358-
362+
359363
// Functions in test expressions should return LogicalType
360364
// If the function returns a value, convert it to boolean
361365
if (typeof result === 'boolean') {
362366
return result;
363367
}
364-
368+
365369
// For count() and length() returning numbers, convert to boolean (non-zero is true)
366370
if (typeof result === 'number') {
367371
return result !== 0;
368372
}
369-
373+
370374
// For nodelists, true if non-empty
371375
if (Array.isArray(result)) {
372376
return result.length > 0;
373377
}
374-
378+
375379
// For other values, check if they exist (not null/undefined)
376380
return result != null;
377381
}
@@ -386,17 +390,17 @@ export class JsonPathEval {
386390
return expression.value;
387391
case 'path': {
388392
if (!expression.path.segments) return undefined;
389-
393+
390394
// Evaluate path segments starting from current node
391395
let currentResults = [currentNode];
392-
396+
393397
for (const segment of expression.path.segments) {
394398
currentResults = this.evalSegment(currentResults, segment);
395399
if (currentResults.length === 0) break; // No results, early exit
396400
}
397-
401+
398402
// For function arguments, we want the actual data, not Value objects
399-
return currentResults.map(v => v.data);
403+
return currentResults.map((v) => v.data);
400404
}
401405
case 'function':
402406
return this.evaluateFunction(expression, currentNode);
@@ -477,13 +481,13 @@ export class JsonPathEval {
477481
* Uses the function registry for extensible function support
478482
*/
479483
private evaluateFunction(expression: types.FunctionExpression, currentNode: Value): any {
480-
const { name, args } = expression;
481-
484+
const {name, args} = expression;
485+
482486
const func = this.funcs.get(name);
483487
if (func) {
484488
return func(args, currentNode, this);
485489
}
486-
490+
487491
// Unknown function - return false for logical context, undefined for value context
488492
return false;
489493
}
@@ -493,12 +497,16 @@ export class JsonPathEval {
493497
* Parameters: ValueType
494498
* Result: ValueType (unsigned integer or Nothing)
495499
*/
496-
private lengthFunction(args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval): number | undefined {
500+
private lengthFunction(
501+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
502+
currentNode: Value,
503+
evaluator: JsonPathEval,
504+
): number | undefined {
497505
if (args.length !== 1) return undefined;
498506

499507
const [arg] = args;
500508
const result = this.getValueFromArg(arg, currentNode, evaluator);
501-
509+
502510
// For length() function, we need the single value
503511
let value: any;
504512
if (Array.isArray(result)) {
@@ -517,7 +525,7 @@ export class JsonPathEval {
517525
if (value && typeof value === 'object' && value !== null) {
518526
return Object.keys(value).length;
519527
}
520-
528+
521529
return undefined; // Nothing for other types
522530
}
523531

@@ -526,12 +534,16 @@ export class JsonPathEval {
526534
* Parameters: NodesType
527535
* Result: ValueType (unsigned integer)
528536
*/
529-
private countFunction(args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval): number {
537+
private countFunction(
538+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
539+
currentNode: Value,
540+
evaluator: JsonPathEval,
541+
): number {
530542
if (args.length !== 1) return 0;
531543

532544
const [arg] = args;
533545
const result = this.getValueFromArg(arg, currentNode, evaluator);
534-
546+
535547
// Count logic based on RFC 9535:
536548
// For count(), we count the number of nodes selected by the expression
537549
if (Array.isArray(result)) {
@@ -549,14 +561,18 @@ export class JsonPathEval {
549561
* Parameters: ValueType (string), ValueType (string conforming to RFC9485)
550562
* Result: LogicalType
551563
*/
552-
private matchFunction(args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval): boolean {
564+
private matchFunction(
565+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
566+
currentNode: Value,
567+
evaluator: JsonPathEval,
568+
): boolean {
553569
if (args.length !== 2) return false;
554570

555571
const [stringArg, regexArg] = args;
556-
572+
557573
const strResult = this.getValueFromArg(stringArg, currentNode, evaluator);
558574
const regexResult = this.getValueFromArg(regexArg, currentNode, evaluator);
559-
575+
560576
// Handle array results (get single value)
561577
const str = Array.isArray(strResult) ? (strResult.length === 1 ? strResult[0] : undefined) : strResult;
562578
const regex = Array.isArray(regexResult) ? (regexResult.length === 1 ? regexResult[0] : undefined) : regexResult;
@@ -579,14 +595,18 @@ export class JsonPathEval {
579595
* Parameters: ValueType (string), ValueType (string conforming to RFC9485)
580596
* Result: LogicalType
581597
*/
582-
private searchFunction(args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval): boolean {
598+
private searchFunction(
599+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
600+
currentNode: Value,
601+
evaluator: JsonPathEval,
602+
): boolean {
583603
if (args.length !== 2) return false;
584604

585605
const [stringArg, regexArg] = args;
586-
606+
587607
const strResult = this.getValueFromArg(stringArg, currentNode, evaluator);
588608
const regexResult = this.getValueFromArg(regexArg, currentNode, evaluator);
589-
609+
590610
// Handle array results (get single value)
591611
const str = Array.isArray(strResult) ? (strResult.length === 1 ? strResult[0] : undefined) : strResult;
592612
const regex = Array.isArray(regexResult) ? (regexResult.length === 1 ? regexResult[0] : undefined) : regexResult;
@@ -609,12 +629,16 @@ export class JsonPathEval {
609629
* Parameters: NodesType
610630
* Result: ValueType
611631
*/
612-
private valueFunction(args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[], currentNode: Value, evaluator: JsonPathEval): any {
632+
private valueFunction(
633+
args: (types.ValueExpression | types.FilterExpression | types.JSONPath)[],
634+
currentNode: Value,
635+
evaluator: JsonPathEval,
636+
): any {
613637
if (args.length !== 1) return undefined;
614638

615639
const [nodeArg] = args;
616640
const result = this.getValueFromArg(nodeArg, currentNode, evaluator);
617-
641+
618642
// For value() function, return single value if exactly one result,
619643
// otherwise undefined (following RFC 9535 value() semantics)
620644
if (Array.isArray(result)) {
@@ -627,7 +651,11 @@ export class JsonPathEval {
627651
/**
628652
* Helper to get value from function argument
629653
*/
630-
private getValueFromArg(arg: types.ValueExpression | types.FilterExpression | types.JSONPath, currentNode: Value, evaluator: JsonPathEval): any {
654+
private getValueFromArg(
655+
arg: types.ValueExpression | types.FilterExpression | types.JSONPath,
656+
currentNode: Value,
657+
evaluator: JsonPathEval,
658+
): any {
631659
if (this.isValueExpression(arg)) {
632660
return evaluator.evalValueExpression(arg, currentNode);
633661
} else if (this.isJSONPath(arg)) {
@@ -643,15 +671,13 @@ export class JsonPathEval {
643671
* Type guard for ValueExpression
644672
*/
645673
private isValueExpression(arg: any): arg is types.ValueExpression {
646-
return arg && typeof arg === 'object' &&
647-
['current', 'root', 'literal', 'path', 'function'].includes(arg.type);
674+
return arg && typeof arg === 'object' && ['current', 'root', 'literal', 'path', 'function'].includes(arg.type);
648675
}
649676

650677
/**
651678
* Type guard for JSONPath
652679
*/
653680
private isJSONPath(arg: any): arg is types.JSONPath {
654-
return arg && typeof arg === 'object' &&
655-
Array.isArray(arg.segments);
681+
return arg && typeof arg === 'object' && Array.isArray(arg.segments);
656682
}
657683
}

src/json-path/__tests__/JsonPathEval.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ describe('JsonPathEval', () => {
368368
});
369369

370370
test('length function with Unicode characters', () => {
371-
const unicodeData = { text: 'Hello 🌍 World' };
371+
const unicodeData = {text: 'Hello 🌍 World'};
372372
const expr = '$[?length(@.text) == 13]';
373373
const result = JsonPathEval.run(expr, unicodeData);
374374
expect(result.length).toBe(1);
@@ -541,10 +541,10 @@ describe('JsonPathEval', () => {
541541
test('match and search difference', () => {
542542
const matchExpr = '$.store.book[?match(@.title, "Lord")]';
543543
const searchExpr = '$.store.book[?search(@.title, "Lord")]';
544-
544+
545545
const matchResult = JsonPathEval.run(matchExpr, testData);
546546
const searchResult = JsonPathEval.run(searchExpr, testData);
547-
547+
548548
expect(matchResult.length).toBe(0); // Exact match fails
549549
expect(searchResult.length).toBe(1); // Substring search succeeds
550550
});
@@ -582,7 +582,7 @@ describe('JsonPathEval', () => {
582582
});
583583

584584
test('function with null values', () => {
585-
const nullData = { items: [null, '', 0, false] };
585+
const nullData = {items: [null, '', 0, false]};
586586
const expr = '$.items[?length(@) == 0]';
587587
const result = JsonPathEval.run(expr, nullData);
588588
expect(result.length).toBe(1); // Only empty string has length 0

src/json-path/__tests__/JsonPathParser-descendant.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ describe('JsonPathParser - @.. descendant parsing tests', () => {
3535
const result = JsonPathParser.parse('$[?count(@..[0]) > 0]');
3636
expect(result.success).toBe(true);
3737
});
38-
});
38+
});

0 commit comments

Comments
 (0)