unit Neslib.Json.Path; (*< XPath-like query language for JSON. There is no official JSONPath specification, but the most widely used version seems to be one developed by Stefan Goessner: http://goessner.net/articles/JsonPath/ @bold(About JSONPath) A JSONPath looks like: $.store.book[0].title or $['store']['book'][0]['title'] Both representations are identical: you can use either dot (.) or bracket ([]) notation to denote children of a dictionary. Brackets can also be used with numerical indices to denote children of an array by index. NOTE: JSONPath only uses single quotes (') within brackets. We also allow for double quotes (") since these are easier to use in Delphi strings. * Every path starts with a $ indicating the root, followed by zero or more child operators (. or []). A $ by itself matches the entire document. * A child name can be an identifier string or the asterisk (* or '*') wildcard to match all children. For example, $.store.book[*].author matches the authors of all books in the store. * In addition to a single dot (.), a double dot (..) can be used to search for any descendants instead of immediate children. For example, $..author matches all authors, regardless of depth. This is called recursive descent. * Children can also be accessed by one or more indices between brackets. These indices are 0-based and are only used with arrays. You can separate multiple indices with comma's. For example, $.store.book[0,2,3] matches the first, third and fourth books. * You can use the slice notation [Start:End:Step] to match a slice (range) of children. This matches all children from index Start up to (but not including) End, using a given Step size (usually 1). All are optional, but at least one value (and colon) must be given: * If Start is omitted, it is implied to be 0. A negative value indicates an offset from the end of the array. * If End is omitted, the slice extracts through the end of the array. A negative value indicates and offset from the end of the array. * If Step is omitted, is is implied to be 1. * Examples: * List[2:] matches the third and all following elements. * List[-2:] matches the last two elements. * List[:2] matches the first two elements. * List[:-2] matches all but the last two elements. * List[2:-2] matches all elements but the first two and last two. * List[-4:-2] matches the 3rd and 4rd elements from the end. * List[::2] matches all elements with an even index. NOTE: JSONPath also has an @ operator to allow custom script expressions. We do not support this operator. @Bold(Examples) Example document: { "store": { "book": [ { "category": "reference", "author": "Nigel Rees", "title": "Sayings of the Century", "price": 8.95 }, { "category": "fiction", "author": "J. R. R. Tolkien", "title": "The Lord of the Rings", "isbn": "0-395-19395-8", "price": 22.99 } ], "bicycle": { "color": "red", "price": 19.95 } } } Example paths: $ Matches the root document (a single value) $..* Matches all members in the document (lots of values) $.store.book[*].author The authors of all books in the store $..author All authors $.store.* All things in store (2 books and a bicycle) $.store..price The price of everything in the store $..book[2] The third book $..book[-1:] The last book in order $..book[:2] The first two books @bold(JSONPath in Delphi) The JSONPath API is short and simple. It consists of a TJsonPath record with only a couple of methods. For one-off matching, use the static Match method: var Doc: IJsonDocument; Matches: TArray; begin Doc := TJsonDocument.Load(...); Matches := TJsonPath.Match(Doc, '$.store.book[*].author'); end; If you plan to use the same path on multiple (sub)documents, then it is faster to parse the path once, and then apply it multiple times: var Doc1, Doc2: IJsonDocument; Path: TJsonPath; Matches1, Matches2: TArray; begin Doc1 := TJsonDocument.Load(...); Doc2 := TJsonDocument.Load(...); Path := TJsonPath.Create('$.store.book[*].author'); Matches1 := Path.Match(Doc1); Matches2 := Path.Match(Doc2); end; You can also run the path on sub-trees: var Doc: IJsonDocument; Store: TJsonValue; Matches: TArray; begin Doc := TJsonDocument.Load(...); Store := Doc.Root.Values['store']; Matches := TJsonPath.Match(Store, '$.book[*].author'); end; If you are only interested in a single (or the first) match, then you can use MatchSingle instead: var Doc: IJsonDocument; Match: TJsonValue; begin Doc := TJsonDocument.Load(...); if (TJsonPath.MatchSingle(Store, '$.book[*]', Match)) then ... end; *) {$INCLUDE 'Neslib.inc'} interface uses System.SysUtils, Neslib.Json.Types, Neslib.Json; type { Exception type that is raised for invalid JSONPath expressions. } EJsonPathError = class(Exception); type { Creating and executing JSONPath expressions. } TJsonPath = record {$REGION 'Internal Declarations'} private type TOperatorType = ( ChildName, // Name of a child element (as in .store or ['store']) ChildIndex, // Index of a child element (as in [3]) RecursiveDescent, // .. operator Wildcard, // * operator IndexList, // [a,b,c,...] Slice); // [start:end:step] private type POperator = ^TOperator; TOperator = record public procedure Init(const AType: TOperatorType); public OperatorType: TOperatorType; Next: POperator; Name: JsonString; // For ChildName Indices: TArray; // For IndexList case Byte of 0: (Index: Integer); // For ChildIndex 1: (Start, Stop, Step: Integer); // For Slice end; private FOperators: TArray; FOperatorCount: Integer; FMatches: TArray; FMatchCount: Integer; FSingleMatch: Boolean; private class procedure SkipWhitespace(var ACur: PJsonChar); inline; static; class function ParseInteger(var ACur: PJsonChar; out AValue: Integer): Boolean; static; class function IsQuote(const AChar: JsonChar): Boolean; inline; static; private procedure AddOperator(const AOperator: TOperator); procedure AddMatch(const AMatch: TJsonValue); procedure ParseDotOperator(var ACur: PJsonChar); procedure ParseBracketOperator(var ACur: PJsonChar); function Match(const ARoot: TJsonValue; const AMatchSingle: Boolean): TArray; overload; procedure VisitOperator(const AOp: POperator; const ARoot: TJsonValue); {$ENDREGION 'Internal Declarations'} public { Parses a JSONPath expression that can be used for matching later. Parameters: AExpression: the JSONPath expression to parse. Raises: EJsonPathError if AExpression is invalid. If you plan to use the same JSONPath expression multiple times, then it is faster to parse it just once using this constructor, and execute it multiple times using one of the (non-static) Match* methods. } constructor Create(const AExpression: JsonString); { Executes this JSONPath expression on a document. Parameters: ADoc: the document to use this JSONPath on. Returns: An array of JSON values that match this JSONPath expression. } function Match(const ADoc: IJsonDocument): TArray; overload; inline; { Executes this JSONPath expression on a JSON value. Parameters: ARoot: the root TJsonValue to use this JSONPath on. Returns: An array of JSON values that match this JSONPath expression. } function Match(const ARoot: TJsonValue): TArray; overload; inline; { Executes this JSONPath expression on a document and returns the first match. Parameters: ADoc: the document to use this JSONPath on. AMatch: is set to the first match found, or a Null value if no match is found. Returns: True if a match is found or False otherwise. } function MatchSingle(const ADoc: IJsonDocument; out AMatch: TJsonValue): Boolean; overload; inline; { Executes this JSONPath expression on a JSON value and returns the first match. Parameters: ARoot: the root TJsonValue to use this JSONPath on. AMatch: is set to the first match found, or a Null value if no match is found. Returns: True if a match is found or False otherwise. } function MatchSingle(const ARoot: TJsonValue; out AMatch: TJsonValue): Boolean; overload; inline; { Executes a JSONPath expression on a document. Parameters: ADoc: the document to use the expression on. AExpression: the JSONPath expression to run. Returns: An array of JSON values that match this JSONPath expression. Raises: EJsonPathError if AExpression is invalid. If you plan to use the same expression multiple times, then it is faster to parse it just once using the constructor, and execute it multiple times using one of the (non-static) Match* methods. } class function Match(const ADoc: IJsonDocument; const AExpression: JsonString): TArray; overload; inline; static; { Executes a JSONPath expression on JSON value. Parameters: ARoot: the root TJsonValue to use the expression on. AExpression: the JSONPath expression to run. Returns: An array of JSON values that match this JSONPath expression. Raises: EJsonPathError if AExpression is invalid. If you plan to use the same expression multiple times, then it is faster to parse it just once using the constructor, and execute it multiple times using one of the (non-static) Match* methods. } class function Match(const ARoot: TJsonValue; const AExpression: JsonString): TArray; overload; static; { Executes a JSONPath expression on a document and returns the first match. Parameters: ADoc: the document to use the expression on. AExpression: the JSONPath expression to run. AMatch: is set to the first match found, or a Null value if no match is found. Returns: True if a match is found or False otherwise. Raises: EJsonPathError if AExpression is invalid. If you plan to use the same expression multiple times, then it is faster to parse it just once using the constructor, and execute it multiple times using one of the (non-static) Match* methods. } class function MatchSingle(const ADoc: IJsonDocument; const AExpression: JsonString; out AMatch: TJsonValue): Boolean; overload; inline; static; { Executes a JSONPath expression on a JSON value and returns the first match. Parameters: ARoot: the root TJsonValue to use the expression on. AExpression: the JSONPath expression to run. AMatch: is set to the first match found, or a Null value if no match is found. Returns: True if a match is found or False otherwise. Raises: EJsonPathError if AExpression is invalid. If you plan to use the same expression multiple times, then it is faster to parse it just once using the constructor, and execute it multiple times using one of the (non-static) Match* methods. } class function MatchSingle(const ARoot: TJsonValue; const AExpression: JsonString; out AMatch: TJsonValue): Boolean; overload; static; end; resourcestring RS_JSON_PATH_INVALID_ROOT = 'A JSON path must start with a root ($) operator.'; RS_JSON_PATH_DUPLICATE_ROOT = 'Multiple root operators ($) in JSON path.'; RS_JSON_PATH_INVALID_OPERATOR = 'Operator in JSON path must start with dot (.) or bracket ([).'; RS_JSON_PATH_MISSING_MEMBER_NAME = 'Child operator in JSON path is missing a member name.'; RS_JSON_PATH_QUOTE_EXPECTED = 'Missing end quote in JSON path.'; RS_JSON_PATH_QUOTE_MISMATCH = 'Quote mismatch in JSON path.'; RS_JSON_PATH_MISSING_CLOSE_BRACKET = 'Missing close bracket (]) in JSON path.'; RS_JSON_PATH_TOO_MANY_SLICE_ARGUMENTS = 'Too many slice arguments in JSON path.'; RS_JSON_PATH_INVALID_SLICE_END = 'Invalid slice end value in JSON path.'; RS_JSON_PATH_INVALID_SLICE_STEP = 'Invalid slice step value in JSON path.'; RS_JSON_PATH_INVALID_BRACKET_OPERATOR = 'Invalid text between brackets in JSON path.'; RS_JSON_PATH_INVALID_INDEX = 'Invalid index in JSON path.'; RS_JSON_PATH_NEGATIVE_ARRAY_INDEX = 'Negative array index in JSON path not allowed.'; RS_JSON_PATH_INVALID_RECURSIVE_DESCENT = 'Recursive descent operator (..) in JSON path must be followed by another operator.'; implementation uses System.Classes; { TJsonPath } procedure TJsonPath.AddMatch(const AMatch: TJsonValue); begin if (FMatchCount >= Length(FMatches)) then begin if (FMatchCount = 0) then SetLength(FMatches, 4) else SetLength(FMatches, FMatchCount * 2); end; FMatches[FMatchCount] := AMatch; Inc(FMatchCount); end; procedure TJsonPath.AddOperator(const AOperator: TOperator); var I: Integer; begin if (FOperatorCount >= Length(FOperators)) then begin if (FOperatorCount = 0) then SetLength(FOperators, 4) else begin SetLength(FOperators, FOperatorCount * 2); { Issue #1: adjust the Next pointers after a resize } for I := 0 to FOperatorCount - 1 do FOperators[I].Next := @FOperators[I + 1]; end; end; FOperators[FOperatorCount] := AOperator; if (FOperatorCount > 0) then FOperators[FOperatorCount - 1].Next := @FOperators[FOperatorCount]; Inc(FOperatorCount); end; constructor TJsonPath.Create(const AExpression: JsonString); var P: PJsonChar; begin FOperators := nil; FOperatorCount := 0; if (AExpression = '') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_ROOT); P := PJsonChar(AExpression); SkipWhitespace(P); if (P^ <> '$') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_ROOT); Inc(P); while (P^ <> #0) do begin SkipWhitespace(P); if (P^ = '.') then ParseDotOperator(P) else if (P^ = '[') then ParseBracketOperator(P) else if (P^ = '$') then raise EJsonPathError.Create(RS_JSON_PATH_DUPLICATE_ROOT) else raise EJsonPathError.Create(RS_JSON_PATH_INVALID_OPERATOR); end; if (FOperatorCount > 0) and (FOperators[FOperatorCount - 1].OperatorType = TOperatorType.RecursiveDescent) then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_RECURSIVE_DESCENT); end; class function TJsonPath.IsQuote(const AChar: JsonChar): Boolean; begin Result := (AChar = '''') or (AChar = '"'); end; function TJsonPath.Match(const ARoot: TJsonValue): TArray; begin Result := Match(ARoot, False); end; function TJsonPath.Match(const ADoc: IJsonDocument): TArray; begin if (ADoc <> nil) then Result := Match(ADoc.Root); end; class function TJsonPath.Match(const ADoc: IJsonDocument; const AExpression: JsonString): TArray; begin if (ADoc <> nil) then Result := Match(ADoc.Root, AExpression); end; class function TJsonPath.Match(const ARoot: TJsonValue; const AExpression: JsonString): TArray; var Path: TJsonPath; begin if (AExpression = '') then Exit; Path := TJsonPath.Create(AExpression); Result := Path.Match(ARoot, False); end; function TJsonPath.Match(const ARoot: TJsonValue; const AMatchSingle: Boolean): TArray; begin if (FOperatorCount = 0) then Exit(TArray.Create(ARoot)); FMatches := nil; FMatchCount := 0; FSingleMatch := AMatchSingle; VisitOperator(@FOperators[0], ARoot); SetLength(FMatches, FMatchCount); Result := FMatches; end; function TJsonPath.MatchSingle(const ADoc: IJsonDocument; out AMatch: TJsonValue): Boolean; begin if (ADoc = nil) then begin AMatch := TJsonValue.Null; Exit(False); end; Result := MatchSingle(ADoc.Root, AMatch); end; function TJsonPath.MatchSingle(const ARoot: TJsonValue; out AMatch: TJsonValue): Boolean; var Matches: TArray; begin Matches := Match(ARoot, True); if (Matches = nil) then begin AMatch := TJsonValue.Null; Exit(False); end; AMatch := Matches[0]; Result := True; end; class function TJsonPath.MatchSingle(const ADoc: IJsonDocument; const AExpression: JsonString; out AMatch: TJsonValue): Boolean; begin if (ADoc <> nil) then Result := MatchSingle(ADoc.Root, AExpression, AMatch) else begin AMatch := TJsonValue.Null; Result := False; end; end; class function TJsonPath.MatchSingle(const ARoot: TJsonValue; const AExpression: JsonString; out AMatch: TJsonValue): Boolean; var Path: TJsonPath; begin if (AExpression = '') then begin AMatch := TJsonValue.Null; Exit(False); end; Path := TJsonPath.Create(AExpression); Result := Path.MatchSingle(ARoot, AMatch); end; procedure TJsonPath.ParseBracketOperator(var ACur: PJsonChar); var P, Start, Stop: PJsonChar; QuoteChar: JsonChar; Op: TOperator; I, Count: Integer; begin // Initial '[' has already been parsed Assert(ACur^ = '['); P := ACur + 1; SkipWhitespace(P); if IsQuote(P^) then begin // ['ident'] or ["ident"] QuoteChar := P^; Inc(P); if (P^ = '*') then begin // ['*'] or ["*"] if (not IsQuote(P[1])) then raise EJsonPathError.Create(RS_JSON_PATH_QUOTE_EXPECTED); if (P[1] <> QuoteChar) then raise EJsonPathError.Create(RS_JSON_PATH_QUOTE_MISMATCH); Inc(P, 2); SkipWhitespace(P); if (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_CLOSE_BRACKET); Op.Init(TOperatorType.Wildcard); AddOperator(Op); Inc(P); end else begin // ['ident'] or ["ident"] Start := P; // Scan for end quote while (P^ <> #0) and (not IsQuote(P^)) do Inc(P); if (P^ = #0) then raise EJsonPathError.Create(RS_JSON_PATH_QUOTE_EXPECTED); if (P = Start) then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_MEMBER_NAME); if (P^ <> QuoteChar) then raise EJsonPathError.Create(RS_JSON_PATH_QUOTE_MISMATCH); Stop := P; Inc(P); SkipWhitespace(P); if (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_CLOSE_BRACKET); Op.Init(TOperatorType.ChildName); SetString(Op.Name, Start, Stop - Start); AddOperator(Op); Inc(P); end; end else if (P^ = '*') then begin // [*] Inc(P); SkipWhitespace(P); if (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_CLOSE_BRACKET); Op.Init(TOperatorType.Wildcard); AddOperator(Op); Inc(P); end else begin // [index] // [index, index, ...] // [start:end:step] Op.Init(TOperatorType.Wildcard); // Temporary if (not ParseInteger(P, I)) then begin // [:end:step] SkipWhitespace(P); if (P^ <> ':') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_BRACKET_OPERATOR); Op.Init(TOperatorType.Slice); end else begin // [index] // [index, index, ...] SkipWhitespace(P); if (P^ = ']') then begin // [index] if (I < 0) then raise EJsonPathError.Create(RS_JSON_PATH_NEGATIVE_ARRAY_INDEX); Op.Init(TOperatorType.ChildIndex); Op.Index := I; end else if (P^ = ',') then begin // [index, index, ...] if (I < 0) then raise EJsonPathError.Create(RS_JSON_PATH_NEGATIVE_ARRAY_INDEX); Op.Init(TOperatorType.IndexList); SetLength(Op.Indices, 4); Op.Indices[0] := I; Count := 1; while True do begin Inc(P); SkipWhitespace(P); if (not ParseInteger(P, I)) then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_INDEX); if (I < 0) then raise EJsonPathError.Create(RS_JSON_PATH_NEGATIVE_ARRAY_INDEX); if (Count >= Length(Op.Indices)) then SetLength(Op.Indices, Count * 2); Op.Indices[Count] := I; Inc(Count); if (P^ = ']') then Break; if (P^ <> ',') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_INDEX); end; SetLength(Op.Indices, Count); end else begin if (P^ <> ':') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_BRACKET_OPERATOR); // [start:end:step] Op.Init(TOperatorType.Slice); Op.Start := I; end; end; if (Op.OperatorType = TOperatorType.Slice) and (P^ = ':') then begin // Parse :end part of slice Inc(P); SkipWhitespace(P); if (not ParseInteger(P, I)) then begin if (P^ <> ':') and (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_SLICE_END); end else Op.Stop := I; Op.Step := 1; if (P^ = ':') then begin // Parse :step part of slice Inc(P); SkipWhitespace(P); if (not ParseInteger(P, I)) and (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_INVALID_SLICE_STEP); Op.Step := I; end; if (P^ = ':') then raise EJsonPathError.Create(RS_JSON_PATH_TOO_MANY_SLICE_ARGUMENTS); if (P^ <> ']') then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_CLOSE_BRACKET); end; AddOperator(Op); Inc(P); end; ACur := P; end; procedure TJsonPath.ParseDotOperator(var ACur: PJsonChar); var P, Start: PJsonChar; Op: TOperator; begin // Initial '.' has already been parsed Assert(ACur^ = '.'); P := ACur + 1; if (P^ = '.') then begin // .. Op.Init(TOperatorType.RecursiveDescent); AddOperator(Op); end else if (P^ = '*') then begin // .* Op.Init(TOperatorType.Wildcard); AddOperator(Op); Inc(P); end else begin // .ident Start := P; // Scan for start of next operator while (P^ <> #0) and (P^ <> '.') and (P^ <> '[') do Inc(P); if (P = Start) then raise EJsonPathError.Create(RS_JSON_PATH_MISSING_MEMBER_NAME); Op.Init(TOperatorType.ChildName); SetString(Op.Name, Start, P - Start); AddOperator(Op); end; ACur := P; end; class function TJsonPath.ParseInteger(var ACur: PJsonChar; out AValue: Integer): Boolean; var P: PJsonChar; C: JsonChar; IsNegative: Boolean; Value: Integer; begin P := ACur; SkipWhitespace(P); IsNegative := False; if (P^ = '-') then begin IsNegative := True; Inc(P); end; C := P^; if (C < '0') or (C > '9') then Exit(False); Value := Ord(C) - Ord('0'); Inc(P); while True do begin C := P^; if (C < '0') or (C > '9') then Break; Value := (Value * 10) + (Ord(C) - Ord('0')); Inc(P); end; if IsNegative then Value := -Value; SkipWhitespace(P); ACur := P; AValue := Value; Result := True; end; class procedure TJsonPath.SkipWhitespace(var ACur: PJsonChar); begin while (ACur^ <= ' ') and (ACur^ <> #0) do Inc(ACur); end; procedure TJsonPath.VisitOperator(const AOp: POperator; const ARoot: TJsonValue); var I, Index, Start, Stop: Integer; Value: TJsonValue; Element: PJsonElement; NextOp: POperator; begin Assert(Assigned(AOp)); if (FSingleMatch) and (FMatchCount <> 0) then Exit; case AOp.OperatorType of TOperatorType.ChildName: if ARoot.IsDictionary and ARoot.TryGetValue(AOp.Name, Value) then begin if (AOp.Next = nil) then AddMatch(Value) else VisitOperator(AOp.Next, Value); end; TOperatorType.ChildIndex: if ARoot.IsArray and (AOp.Index < ARoot.Count) then begin Assert(AOp.Index >= 0); Value := ARoot.Items[AOp.Index]; if (AOp.Next = nil) then AddMatch(Value) else VisitOperator(AOp.Next, Value); end; TOperatorType.RecursiveDescent: begin NextOp := AOp.Next; Assert(Assigned(NextOp)); case ARoot.ValueType of TJsonValueType.&Array: begin for I := 0 to ARoot.Count - 1 do begin if (NextOp.OperatorType = TOperatorType.ChildIndex) and (NextOp.Index = I) then VisitOperator(NextOp, ARoot) else VisitOperator(AOp, ARoot.Items[I]); end; end; TJsonValueType.Dictionary: begin for I := 0 to ARoot.Count - 1 do begin Element := ARoot.Elements[I]; if(NextOp.OperatorType = TOperatorType.ChildName) and (NextOp.Name = Element.Name) then VisitOperator(NextOp, ARoot) else VisitOperator(AOp, Element.Value); end; end; end; end; TOperatorType.Wildcard: case ARoot.ValueType of TJsonValueType.&Array: begin for I := 0 to ARoot.Count - 1 do begin Value := ARoot.Items[I]; if (AOp.Next = nil) then AddMatch(Value) else VisitOperator(AOp.Next, Value); end; end; TJsonValueType.Dictionary: begin for I := 0 to ARoot.Count - 1 do begin Element := ARoot.Elements[I]; if (AOp.Next = nil) then AddMatch(Element.Value) else VisitOperator(AOp.Next, Element.Value); end; end; end; TOperatorType.IndexList: if ARoot.IsArray then begin for I := 0 to Length(AOp.Indices) - 1 do begin Index := AOp.Indices[I]; Assert(Index >= 0); if (Index < ARoot.Count) then begin Value := ARoot.Items[Index]; if (AOp.Next = nil) then AddMatch(Value) else VisitOperator(AOp.Next, Value); end; end; end; TOperatorType.Slice: if ARoot.IsArray then begin if (AOp.Start < 0) then begin Start := ARoot.Count + AOp.Start; Stop := ARoot.Count + AOp.Stop; end else begin Start := AOp.Start; Stop := AOp.Stop; end; if (Stop > ARoot.Count) then Stop := ARoot.Count; I := Start; Assert(AOp.Step > 0); while (I < Stop) do begin Value := ARoot.Items[I]; if (AOp.Next = nil) then AddMatch(Value) else VisitOperator(AOp.Next, Value); Inc(I, AOp.Step); end; end else Assert(False); end; end; { TJsonPath.TOperator } procedure TJsonPath.TOperator.Init(const AType: TOperatorType); begin OperatorType := AType; Next := nil; Name := ''; Indices := []; Start := 0; Stop := 0; Step := 0; end; end.