{ +-------------------------------------------------------------+ }
{ |                                                             | }
{ |   GM-Software                                               | }
{ |   ===========                                               | }
{ |                                                             | }
{ |   Project: All Projects                                     | }
{ |                                                             | }
{ |   Description: Simple JSON Path implementation.             | }
{ |                                                             | }
{ |                                                             | }
{ |   Copyright (C) - 2018 - Gerrit Moeller.                    | }
{ |                                                             | }
{ |   Source code dstributed under MIT license.                 | }
{ |                                                             | }
{ |   See: https://www.gm-software.de                           | }
{ |                                                             | }
{ +-------------------------------------------------------------+ }

{$INCLUDE GMCompilerSettings.inc}

unit GMJsonPath;

interface

uses //{$IFDEF JEDIAPI}JwaWinType,{$ENDIF}
     GMStrDef, GMCollections, GMIntf, GMCommon, GMUnionValue, GMJson;

type

  TJsonPathSelector = (jpsUnknown, jpsRoot, jpsSelf, jpsChildName, jpsChildIdx, jpsChildSlice, jpsDescendant, jpsWildcard);


  RGMJsonPathSelector = record
   Selector: TJsonPathSelector;
   MemberName: TGMString;
   StartIdx: Integer;
   EndIdx: Integer;
   IdxStep: Integer;
  end;


  //TGMJsonPathEvaluator = class;
  //
  //IGMJsonPathEvaluator = interface(IUnknown)
  //  ['{1A2FA727-ACCF-4E95-933F-29F96E966411}']
  //  function Obj: TGMJsonPathEvaluator;
  //end;
  //
  //TGMJsonPathEvaluator = class(TGMRefCountedObj, IGMJsonPathEvaluator)
  // protected
  //  FJsonRoot: IGMJsonValueBase;
  //
  //  procedure EvaluateSelector(constref ASelector: RGMJsonPathSelector; constref AInputNodes, AMatchingNodes: IGMJSONValuesCollection);
  //  function ParseSelectors(const AJsonPathExpression: TGMString): IGMJSONValuesCollection;
  //
  // public
  //  constructor Create(const ARefLifeTime: Boolean = True); overload; override;
  //  constructor Create(const AJsonRoot: IGMJsonValueBase; const ARefLifeTime: Boolean = True); reintroduce; overload;
  //  function Obj: TGMJsonPathEvaluator;
  //
  //  function EvaluateJsonPathExpression(const AJsonPathExpression: TGMString): IGMJSONValuesCollection;
  //end;


  EGMJsonPathException = class(EGMException);


function GMEvaluateJsonPathExpression(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase): IGMJSONValuesCollection;

function GMGetJsonTerminalValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const ACaller: TObject = nil): RGMJsonValueData;

function GMGetJsonTerminalValueDflt(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase;
                                    const ADefaultValue: RGMUnionValue; const AApplyDefaultOnNullValues: Boolean = True): RGMUnionValue;

function GMGetJsonArray(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const ACaller: TObject = nil): IGMJsonValueContainer;

function GMFindJsonValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase): IGMJsonValueBase; overload;
function GMFindJsonValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const AIID: TGUID; out AIntf): Boolean; overload;


function GMCreateJSONValueCollection: IGMJSONValuesCollection;



implementation

uses TypInfo;

resourcestring

  srNoValueMatchingExprFmt = 'No JSON %s value found matching the JSON-path expression "%s"';
  srUnexpectedExprChar = 'Invalid character "%s" in JSON path expression at position %d, excpeted %s';

  srTerminal = 'terminal';
  srArray = 'array';


{ ------------------------- }
{ ---- Global Routines ---- }
{ ------------------------- }

function GMCreateJSONValueCollection: IGMJSONValuesCollection;
begin
  Result := TGMGenericArrayCollection<IGMJsonValueBase>.Create;
end;

procedure NotImplemented(const AName: TGMString);
begin
  raise EGMJsonPathException.ObjError('JSON path ' + AName + ' not implemented yet', nil, 'NotImplemented')
end;


{ ------------------------------ }
{ ---- TGMJsonPathEvaluator ---- }
{ ------------------------------ }

//constructor TGMJsonPathEvaluator.Create(const ARefLifeTime: Boolean);
//begin
//  inherited Create(ARefLifeTime);
//end;
//
//constructor TGMJsonPathEvaluator.Create(const AJsonRoot: IGMJsonValueBase; const ARefLifeTime: Boolean);
//begin
//  Create(ARefLifeTime);
//  FJsonRoot := AJsonRoot;
//end;
//
//function TGMJsonPathEvaluator.Obj: TGMJsonPathEvaluator;
//begin
//  Result := Self;
//end;

procedure EvaluateSelector(constref ASelector: RGMJsonPathSelector; const AJsonRoot: IGMJsonValueBase; const AInputNodes, AMatchingNodes: IGMJSONValuesCollection);
var inputValueIt, subValueIt: IGMJSONValuesIterator; ctxNode, subNode: IGMJsonValueBase; containerNode: IGMJsonValueContainer;
    idx, startIdx, endIdx, len: PtrInt;
  procedure AddDescendants(const ANode: IGMJsonValueBase);
  var it: IGMJSONValuesIterator; subNode: IGMJsonValueBase;
  begin
    if ANode = nil then Exit;
    AMatchingNodes.Add(ANode);
    it := ANode.CreateSubValueIterator;
    if it <> nil then
     while it.NextEntry(subNode) do AddDescendants(subNode);
  end;

  function NormalizeIdx(AIndex, AArrayLength: PtrInt): PtrInt;
  begin
    if AIndex >= 0 then Exit(AIndex) else Exit(AArrayLength + AIndex);
  end;
begin
  if AMatchingNodes = nil then Exit;

  if ASelector.Selector = jpsRoot then AMatchingNodes.Add(AJsonRoot) else
   if AInputNodes <> nil then
    begin
     inputValueIt := AInputNodes.CreateIterator;
     while inputValueIt.NextEntry(ctxNode) do
      case ASelector.Selector of
       //jpsUnknown,
       //jpsRoot: AMatchingNodes.Add(FJsonRoot);
       jpsSelf: AMatchingNodes.Add(ctxNode);
       jpsChildName: begin
                      if Length(ASelector.MemberName) <= 0 then
                       begin
                        if GMQueryInterface(ctxNode, IGMJsonValueContainer, containerNode) and
                           (containerNode.FirstValue <> nil) then AMatchingNodes.Add(containerNode.FirstValue);
                       end
                      else
                       begin
                        subValueIt := ctxNode.CreateSubValueIterator;
                        if subValueIt <> nil then
                         while subValueIt.NextEntry(subNode) do
                           if GMsameText(subNode.ValueName, ASelector.MemberName) then AMatchingNodes.Add(subNode);
                       end;
                     end;

       jpsChildIdx: if GMQueryInterface(ctxNode, IGMJsonValueContainer, containerNode) then
                      begin
                       len := containerNode.ArrayLength;
                       idx := NormalizeIdx(ASelector.StartIdx, len);
                       if GMIsInrange(idx, 0, len-1) then AMatchingNodes.Add(containerNode[idx]);
                      end;

       jpsChildSlice: if GMQueryInterface(ctxNode, IGMJsonValueContainer, containerNode) then
                       begin
                        len := containerNode.ArrayLength;
                        startIdx := NormalizeIdx(ASelector.StartIdx, len);
                        endIdx := NormalizeIdx(ASelector.EndIdx, len);

                        if ASelector.IdxStep > 0 then
                         begin
                          endIdx := Min(endIdx, len);
                          idx := Max(0, startIdx);
                          while idx < endIdx do
                           begin
                            AMatchingNodes.Add(containerNode[idx]);
                            Inc(idx, ASelector.IdxStep);
                           end;
                         end;

                        if ASelector.IdxStep < 0 then
                         begin
                          startIdx := Max(0, startIdx);
                          idx := Min(endIdx, len-1);
                          while startIdx < idx do
                           begin
                            AMatchingNodes.Add(containerNode[idx]);
                            Inc(idx, ASelector.IdxStep);
                           end;
                         end;
                       end;

       jpsDescendant: AddDescendants(ctxNode);

       jpsWildcard: begin
                     subValueIt := ctxNode.CreateSubValueIterator;
                     if subValueIt <> nil then
                       while subValueIt.NextEntry(subNode) do AMatchingNodes.Add(subNode);
                    end;
      end;
    end;
end;


function ParseSelectors(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase): IGMJSONValuesCollection;
var selectorData: RGMJsonPathSelector; inputNodes, matchingNodes: IGMJSONValuesCollection; pExprCh: PGMChar; ch: TGMChar;
    isUnion: Boolean;

  function MsgUnexpectedChar(const AExpectedChars: TGMString): TGMString; // ; AFoundChr: TGMChar; ACharPos: Integer
  begin
    Result := GMFormat(srUnexpectedExprChar, [pExprCh^, pExprCh - PGMChar(AJsonPathExpression) + 1, GMStrAsQuotedChars(AExpectedChars)]);
  end;

  procedure ExcpectedChars(const AExpectedChars: TGMString);
  begin
    raise EGMJsonPathException.ObjError(MsgUnexpectedChar(AExpectedChars), nil, 'ParseSelectors')
  end;

  procedure SkipWhiteSpace;
  begin
    while pExprCh^ <> #0 do
     case pExprCh^ of
      ' ', #9, #10, #13: Inc(pExprCh);
      else Break;
     end;
  end;

  function ParseDotName: TGMString;
  var pStartCh: PGMChar;
  begin
    pStartCh := pExprCh;

    while pExprCh^ <> #0 do
     case pExprCh^ of
      '0' .. '9', 'a' ..'z', 'A' .. 'Z', '_': Inc(pExprCh);
      else if Ord(pExprCh^) < 128 then Break;
     end;

    SetLength(Result, pExprCh - pStartCh);
    if Length(Result) > 0 then System.Move(pStartCh^, Result[1], Length(Result) * SizeOf(TGMChar));
  end;

  function ParseNumber: Integer;
  var pStartCh: PGMChar; numStr: TGMString;
  begin
    pStartCh := pExprCh;

    while pExprCh^ <> #0 do
     case pExprCh^ of
      '0' .. '9': Inc(pExprCh);
      else Break;
     end;

    SetLength(numStr, pExprCh - pStartCh);
    if Length(numStr) > 0 then System.Move(pStartCh^, numStr[1], Length(numStr) * SizeOf(TGMChar));
    Result := GMStrToInt(numStr);
  end;

  function ParseQuotedString(AQuoteCh: TGMChar): TGMString;
  var pStartCh: PGMChar;
  begin
    {ToDo: Properly parse escape sequences and unicode characters in quoted strings}
    pStartCh := pExprCh;
    while (pExprCh^ <> #0) and (pExprCh^ <> AQuoteCh) do Inc(pExprCh);
    SetLength(Result, pExprCh - pStartCh);
    if Length(Result) > 0 then System.Move(pStartCh^, Result[1], Length(Result) * SizeOf(TGMChar));
    if pExprCh^ = AQuoteCh then Inc(pExprCh);
  end;

  procedure ParseFilterSelector;
  begin
    {ToDo: Implement jpsDescendant filter selectors}
    NotImplemented('filter selectors');
    //raise EGMJsonPathException.ObjError('Filter selectors not implemented yet', Self, 'ParseSelectors')
  end;


begin
  //if (AInputNodes = nil) or AInputNodes.IsEmpty then Exit(GMCreateJSONValueCollection);

  pExprCh := PGMChar(AJsonPathExpression);

  //inputNodes := GMCreateJSONValueCollection;

  while pExprCh^ <> #0 do
   begin
    selectorData := Default(RGMJsonPathSelector); // <- always reset selector at the beginning of each loop
    matchingNodes := GMCreateJSONValueCollection;

    case pExprCh^ of
     '$': begin selectorData.Selector := jpsRoot; Inc(pExprCh); end;
     '.': begin
           if (pExprCh + 1)^ = '.' then
            begin
             selectorData.Selector := jpsDescendant;
             if (pExprCh + 2)^ = '[' then Inc(pExprCh, 2) else Inc(pExprCh, 1); // <- use the last dot as member selector for the next round
            end
           else
           if (pExprCh + 1)^ = '*' then begin selectorData.Selector := jpsWildcard; Inc(pExprCh, 2); end
           else
            begin
             Inc(pExprCh);
             selectorData.Selector := jpschildName;
             selectorData.MemberName := ParseDotName;
            end;
          end;

     '[': begin
           Inc(pExprCh);
           repeat
            SkipWhiteSpace;
            isUnion := False;
            case pExprCh^ of
             '''', '"': begin
                         ch := pExprCh^;
                         Inc(pExprCh);
                         selectorData.Selector := jpschildName;
                         selectorData.MemberName := ParseQuotedString(ch);
                         SkipWhiteSpace;
                         case pExprCh^ of
                          ']': Inc(pExprCh);
                          ',': begin isUnion := True; Inc(pExprCh); end;
                          else ExcpectedChars('],');
                         end;
                        end;

             '*': begin
                   selectorData.Selector := jpsWildcard;
                   Inc(pExprCh);
                   SkipWhiteSpace;
                   if pExprCh^ <> ']' then ExcpectedChars(']');
                   Inc(pExprCh);
                  end;

             ']': begin selectorData.Selector := jpschildName; Inc(pExprCh); end;

             '?': ParseFilterSelector; {ToDo: Parse filter selector}

             ':': NotImplemented('array slice selectors');

             '0' .. '9': begin
                          selectorData.Selector := jpsChildIdx;
                          selectorData.StartIdx := ParseNumber;
                          SkipWhiteSpace;
                          case pExprCh^ of
                           ']': Inc(pExprCh);
                           ',': begin isUnion := True; Inc(pExprCh); end;
                           ':': NotImplemented('array slice selectors');  {ToDo: Parse array slice selector}
                           else ExcpectedChars('],:');
                          end;
                         end;
             else ExcpectedChars('''"*]?:0123456789');
            end;
            if isUnion then
              begin
               EvaluateSelector(selectorData, AJsonRoot, inputNodes, matchingNodes);
               selectorData := Default(RGMJsonPathSelector);
              end;
           until not isUnion;
          end;

     else ExcpectedChars('$.[');
    end;

    if selectorData.Selector <> jpsUnknown then
     begin
      EvaluateSelector(selectorData, AJsonRoot, inputNodes, matchingNodes);
      inputNodes := matchingNodes;
     end;

    SkipWhiteSpace;
   end;

  if matchingNodes = nil then Result := GMCreateJSONValueCollection else Result := matchingNodes; // <- always return a valid collection object
end;

//function TGMJsonPathEvaluator.EvaluateJsonPathExpression(const AJsonPathExpression: TGMString): IGMJSONValuesCollection;
//begin
//  if (FJsonRoot = nil) or (Length(AJsonPathExpression) <= 0) then Exit(GMCreateJSONValueCollection);
//  Result := ParseSelectors(AJsonPathExpression);
//end;


{ ---------------- }
{ ---- Global ---- }
{ ---------------- }

function GMEvaluateJsonPathExpression(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase): IGMJSONValuesCollection;
//var evaluator: IGMJsonPathEvaluator;
begin
  //evaluator := TGMJsonPathEvaluator.Create(AJsonRoot);
  //Result := evaluator.Obj.EvaluateJsonPathExpression(AJsonPathExpression);

  if (AJsonRoot = nil) or (Length(AJsonPathExpression) <= 0) then Exit(GMCreateJSONValueCollection);
  Result := ParseSelectors(AJsonPathExpression, AJsonRoot);
end;
                                                                        
function GMGetJsonTerminalValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const ACaller: TObject): RGMJsonValueData;
var matchingValues: IGMJSONValuesCollection; termVal: IGMJsonTerminalValue; it: IGMJSONValuesIterator; // unkVal: IUnknown;
    jsonEntry: IGMJsonValueBase;
begin
  matchingValues := GMEvaluateJsonPathExpression(AJsonPathExpression, AJsonRoot);

  it := matchingValues.CreateIterator;
  if it.NextEntry(jsonEntry) and GMQueryInterface(jsonEntry, IGMJsonTerminalValue, termVal) then
    Result := termVal.ValueData
  else
    raise EGMJsonPathException.ObjError(GMFormat(srNoValueMatchingExprFmt, [srTerminal, AJsonPathExpression]), ACaller, {$I %CurrentRoutine%});
end;


function GMGetJsonTerminalValueDflt(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const ADefaultValue: RGMUnionValue; const AApplyDefaultOnNullValues: Boolean): RGMUnionValue;
var matchingValues: IGMJSONValuesCollection; jsonEntry: IGMJsonValueBase; termVal: IGMJsonTerminalValue; it: IGMJSONValuesIterator;
begin
  matchingValues := GMEvaluateJsonPathExpression(AJsonPathExpression, AJsonRoot);

  it := matchingValues.CreateIterator;

  if it.NextEntry(jsonEntry) and GMQueryInterface(jsonEntry, IGMJsonTerminalValue, termVal) and not (termVal.ValueData.IsNullOrUnassigned and AApplyDefaultOnNullValues) then
    Result := termVal.ValueData
  else
    Result := ADefaultValue;
end;


function GMGetJsonArray(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const ACaller: TObject = nil): IGMJsonValueContainer;
var matchingValues: IGMJSONValuesCollection; jsonEntry: IGMJsonValueBase; it: IGMJSONValuesIterator;
begin
  matchingValues := GMEvaluateJsonPathExpression(AJsonPathExpression, AJsonRoot);

  it := matchingValues.CreateIterator;
  if not it.NextEntry(jsonEntry) or not GMQueryInterface(jsonEntry, IGMJsonValueContainer, Result) then
     raise EGMJsonPathException.ObjError(GMFormat(srNoValueMatchingExprFmt, [srArray]) +': '+ AJsonPathExpression, ACaller, {$I %CurrentRoutine%});
end;


function GMFindJsonValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase): IGMJsonValueBase;
var matchingValues: IGMJSONValuesCollection; jsonEntry: IGMJsonValueBase; it: IGMJSONValuesIterator;
begin
  matchingValues := GMEvaluateJsonPathExpression(AJsonPathExpression, AJsonRoot);

  it := matchingValues.CreateIterator;
  if not it.NextEntry(jsonEntry) or not GMQueryInterface(jsonEntry, IGMJsonValueBase, Result) then Result := nil;
end;


function GMFindJsonValue(const AJsonPathExpression: TGMString; const AJsonRoot: IGMJsonValueBase; const AIID: TGUID; out AIntf): Boolean;
var matchingValues: IGMJSONValuesCollection; jsonEntry: IGMJsonValueBase; it: IGMJSONValuesIterator;
begin
  matchingValues := GMEvaluateJsonPathExpression(AJsonPathExpression, AJsonRoot);

  it := matchingValues.CreateIterator;
  Result := it.NextEntry(jsonEntry) and GMQueryInterface(jsonEntry, AIID, AIntf);
end;


end.