unit XData.Web.Client;

{$I XData.inc}
{$SCOPEDENUMS OFF} // to keep backward compatibility
{$IF PAS2JS_FULLVERSION >= 20000}
  {$DEFINE JSASYNC}
{$ENDIF}

interface

uses
  JS, SysUtils, Classes, Types,
  Bcl.Rtti.Common,
  Bcl.Json.Common,
  XData.Model.Classes,
  XData.Web.Connection,
  XData.Web.Request,
  XData.Web.Response;

type
  TXDataWebClient = class;
  TXDataClientRequestType = (rtGet, rtList, rtPut, rtPost, rtDelete, rtRawInvoke);

  TXDataClientResponse = class
  strict private
    FResponse: IHttpResponse;
    FRequestType: TXDataClientRequestType;
    FResult: JSValue;
    FRequestId: string;
    FCount: Integer;
    FClient: TXDataWebClient;
    function GetResponseText: string;
    function GetStatusCode: Integer;
  strict private
    function GetResultAsArray: TJSArray;
    function GetResultAsObject: TJSObject; protected
    property RequestType: TXDataClientRequestType read FRequestType;
  public
    constructor Create(AClient: TXDataWebClient; AResponse: IHttpResponse; AResult: JSValue;
      ARequestType: TXDataClientRequestType; ARequestId: string; ACount: Integer = 0); reintroduce;
    property StatusCode: Integer read GetStatusCode;
    property ResponseText: string read GetResponseText;
    property Result: JSValue read FResult;
    property ResultAsObject: TJSObject read GetResultAsObject;
    property ResultAsArray: TJSArray read GetResultAsArray;
    property RequestId: string read FRequestId;
    property Count: Integer read FCount;
    property Client: TXDataWebClient read FClient;
    property Response: IHttpResponse read FResponse;
  end;

  TXDataClientRequest = class
  strict private
    FRequest: IHttpRequest;
    FRequestType: TXDataClientRequestType;
    FRequestId: string;
    FClient: TXDataWebClient;
  strict protected
    property RequestType: TXDataClientRequestType read FRequestType;
  public
    constructor Create(AClient: TXDataWebClient; ARequest: IHttpRequest;
      ARequestType: TXDataClientRequestType; ARequestId: string); reintroduce;
    property RequestId: string read FRequestId;
    property Client: TXDataWebClient read FClient;
    property Request: IHttpRequest read FRequest;
  end;

  TXDataClientError = class
  private
    FResponse: IHttpResponse;
    FRequestId: string;
    FErrorCode: string;
    FErrorMessage: string;
    FRequestUrl: string;
    FClient: TXDataWebClient;
    function GetStatusCode: Integer;
  public
    constructor Create(AClient: TXDataWebClient; AResponse: IHttpResponse;
      const ARequestUrl, AErrorCode, AErrorMessage, ARequestId: string); reintroduce;
    property StatusCode: Integer read GetStatusCode;
    property RequestUrl: string read FRequestUrl;
    property RequestId: string read FRequestId;
    property ErrorCode: string read FErrorCode;
    property ErrorMessage: string read FErrorMessage;
    property Client: TXDataWebClient read FClient;
    property Response: IHttpResponse read FResponse;
  end;

  TXDataClientLoadEvent = procedure(Response: TXDataClientResponse) of object;
  TXDataClientLoadProc = reference to procedure(Response: TXDataClientResponse);
  TXDataClientErrorEvent = procedure(Error: TXDataClientError) of object;
  TXDataClientErrorProc = reference to procedure(Error: TXDataClientError);
  TXDataClientRequestEvent = procedure(Request: TXDataClientRequest) of object;
  THttpResponseProc = reference to procedure(Response: IHttpResponse);

  TReferenceSolvingMode = (rsAll, rsNone);

  TXDataWebClient = class(TComponent)
  strict private
    FOnLoad: TXDataClientLoadEvent;
    FOnError: TXDataClientErrorEvent;
    FOnRequest: TXDataClientRequestEvent;
//    FMaxEagerFetchDepth: integer;
    FConnection: TXDataWebConnection;
    FReferenceSolvingMode: TReferenceSolvingMode;
    function EntityTypeFromEntitySet(const EntitySetName: string): TXDataEntityType;
    procedure SetConnection(const Value: TXDataWebConnection);
    function Model: TXDataModel;
    function Url: string;
    procedure CheckConnection;
    procedure ParseError(Response: IHttpResponse; var ErrorCode, ErrorMessage: string);
    procedure SolveReferences(json: JSValue);
//    procedure SetIdFromResponse(Entity: TObject; Req: THttpRequest; Resp: THttpResponse);
  private
    { callback functions to build canonical url's }
    procedure DoLoad(Response: TXDataClientResponse; SuccessProc: TXDataClientLoadProc; AsyncMode: Boolean);
    procedure DoError(Response: IHttpResponse; RequestUrl, RequestId: string;
      ErrorMessage: string; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean);
    function RequestUrl(const RelativeUrl: string): string;
    function GetNavPropValue(Entity: JSValue; Prop: TXDataNavigationProperty): JSValue;
    function GetPropValue(Entity: JSValue; Prop: TXDataSimpleProperty): JSValue;
  private
    procedure InternalGet(const EntitySet, QueryString: string; Id: JSValue; const RequestId: string;
      SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
    procedure InternalPut(const EntitySet: string; Entity: TJObject; const RequestId: string;
      SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
    procedure InternalPost(const EntitySet: string; Entity: TJObject; const RequestId: string;
      SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
    procedure InternalDelete(const EntitySet: string; Entity: TJObject; const RequestId: string;
      SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
    procedure InternalList(const EntitySet: string; const Query: string; const RequestId: string;
      SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
    procedure InternalRawInvoke(const OperationId: string; Args: array of JSValue;
      RequestId: string; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
  protected
    procedure DoRequest(Request: TXDataClientRequest);
    procedure Send(Req: IHttpRequest; SuccessProc: THttpResponseProc; ErrorProc: THttpErrorProc);
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
  public
    procedure Get(const EntitySet: string; Id: JSValue; const RequestId: string = 'get'); overload;
    procedure Get(const EntitySet: string; Id: JSValue; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    procedure Get(const EntitySet, QueryString: string; Id: JSValue; const RequestId: string = 'get'); overload;
    procedure Get(const EntitySet, QueryString: string; Id: JSValue; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function GetAsync(const EntitySet, QueryString: string; Id: JSValue): TXDataClientResponse; async; overload;
    function GetAsync(const EntitySet: string; Id: JSValue): TXDataClientResponse; async; overload;
    {$ENDIF}

    procedure Put(const EntitySet: string; Entity: TJObject; const RequestId: string = 'put'); overload;
    procedure Put(const EntitySet: string; Entity: TJObject; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function PutAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse; async;
    {$ENDIF}

    procedure Post(const EntitySet: string; Entity: TJObject; const RequestId: string = 'post'); overload;
    procedure Post(const EntitySet: string; Entity: TJObject; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function PostAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse; async;
    {$ENDIF}

    procedure Delete(const EntitySet: string; Entity: TJObject; const RequestId: string = 'delete'); overload;
    procedure Delete(const EntitySet: string; Entity: TJObject; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function DeleteAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse; async;
    {$ENDIF}

    procedure List(const EntitySet: string; const Query: string = ''; const RequestId: string = 'list'); overload;
    procedure List(const EntitySet: string; const Query: string; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function ListAsync(const EntitySet: string; const Query: string = ''): TXDataClientResponse; async;
    {$ENDIF}

    procedure RawInvoke(const OperationId: string; Args: array of JSValue; RequestId: string = ''); overload;
    procedure RawInvoke(const OperationId: string; Args: array of JSValue; SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc = nil); overload;
    {$IFDEF JSASYNC}
    function RawInvokeAsync(const OperationId: string; Args: array of JSValue): TXDataClientResponse; async;
    {$ENDIF}

//    function Count(Clazz: TClass; const Query: string = ''): Integer;
//    function Service<T>: T;
  published
    property Connection: TXDataWebConnection read FConnection write SetConnection;
    property OnLoad: TXDataClientLoadEvent read FOnLoad write FOnLoad;
    property OnError: TXDataClientErrorEvent read FOnError write FOnError;
    property OnRequest: TXDataClientRequestEvent read FOnRequest write FOnRequest;
    property ReferenceSolvingMode: TReferenceSolvingMode read FReferenceSolvingMode write FReferenceSolvingMode default rsAll;
  end;

  TXDataWebInvoker = class
  strict private
    FClient: TXDataWebClient;
    FAction: TXDataAction;
    FRequestId: string;
    FSuccessProc: TXDataClientLoadProc;
    FErrorProc: TXDataClientErrorProc;
    FAsyncMode: Boolean;
  protected
    procedure DoLoad(Response: TXDataClientResponse);
    procedure DoError(Response: IHttpResponse; RequestUrl, RequestId: string;
      ErrorMessage: string);
    property Action: TXDataAction read FAction;
    property RequestId: string read FRequestId write FRequestId;
  strict private
    function CreateHttpRequest: IHttpRequest;
    function RequestUrl(const RelativeUrl: string): string;
  strict private
    function BuildQueryString(Args: array of JSValue): string;
    function GetPathParamValues(Args: array of JSValue): TStringDynArray;
    function BuildUrlRoute(Args: array of JSValue): string;
    procedure SetParamStreamBody(Req: IHttpRequest; const Args: array of JSValue; ArgIndex: Integer);
    procedure SetJsonParamsBody(Req: IHttpRequest; const Args: array of JSValue);
    procedure SetJsonObjectBody(Req: IHttpRequest; const Args: array of JSValue);
    procedure TransformParam(Param: TXDataParamDef; var Value: JSValue);
  public
    constructor Create(AClient: TXDataWebClient; AAction: TXDataAction;
      ASuccessProc: TXDataClientLoadProc; AErrorProc: TXDataClientErrorProc;
      AAsyncMode: Boolean); reintroduce;
    procedure Execute(const Args: array of JSValue);
  end;

  EXDataClientException = class(Exception)
  end;

  EXDataClientRequestException = class(EXDataClientException)
  private
    FErrorResult: TXDataClientError;
  public
    constructor Create(AClientError: TXDataClientError); reintroduce;
    property ErrorResult: TXDataClientError read FErrorResult;
  end;

  EXDataOperationRequestException = class(EXDataClientRequestException)
  end;

  EXDataWrongParameterCount = class(EXDataClientException)
  public
    constructor Create(Action: TXDataAction; PassedParameters: integer); reintroduce;
  end;

  EXDataWrongRouteParams = class(EXDataClientException)
  public
    constructor Create(Action: TXDataAction);
  end;

implementation

uses
  Bcl.Utils,
  XData.Bind.Converter,
  XData.Utils;

{ TXDataWebClient }

procedure TXDataWebClient.CheckConnection;
begin
  if not Assigned(FConnection) then
    raise EXDataClientException.Create('Missing XData Connection');
end;

procedure TXDataWebClient.Delete(const EntitySet: string; Entity: TJObject;
  const RequestId: string = 'delete');
begin
  InternalDelete(EntitySet, Entity, RequestId, nil, nil);
end;

procedure TXDataWebClient.Delete(const EntitySet: string; Entity: TJObject;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalDelete(EntitySet, Entity, 'delete', SuccessProc, ErrorProc);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.DeleteAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalDelete(EntitySet, Entity, 'delete',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

procedure TXDataWebClient.DoError(Response: IHttpResponse; RequestUrl, RequestId: string;
  ErrorMessage: string; ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean);
var
  ErrorCode: string;
  ClientError: TXDataClientError;
begin
  if Response = nil then
    Response := TDummyHttpResponse.Create;

  ErrorCode := '';
  if ErrorMessage = '' then
    ParseError(Response, ErrorCode, ErrorMessage);
  ClientError := TXDataClientError.Create(Self, Response, RequestUrl, ErrorCode, ErrorMessage, RequestId);

  if AsyncMode then
  begin
    if Assigned(FOnError) then
      FOnError(ClientError);
    if Assigned(ErrorProc) then
      ErrorProc(ClientError)
    else
      raise EXDataClientRequestException.Create(ClientError);
  end
  else
  begin
    if Assigned(ErrorProc) then
    begin
      ErrorProc(ClientError);
      Exit;
    end;

    if Assigned(FOnError) then
      FOnError(ClientError)
    else
      raise EXDataClientRequestException.Create(ClientError);
  end;
end;

procedure TXDataWebClient.DoLoad(Response: TXDataClientResponse;
  SuccessProc: TXDataClientLoadProc; AsyncMode: Boolean);
begin
  try
    case ReferenceSolvingMode of
      rsAll: SolveReferences(Response.Result);
    end;

    if AsyncMode then
    begin
      if Assigned(FOnLoad) then
        FOnLoad(Response);
      if Assigned(SuccessProc) then
        SuccessProc(Response);
    end
    else
    begin
      if Assigned(SuccessProc) then
      begin
        SuccessProc(Response);
        Exit;
      end;
      if Assigned(FOnLoad) then
        FOnLoad(Response);
    end;
  finally
    if not AsyncMode then
      Response.Free;
  end;
end;

procedure TXDataWebClient.DoRequest(Request: TXDataClientRequest);
begin
  try
    if Assigned(FOnRequest) then
      FOnRequest(Request);
  finally
    Request.Free;
  end;
end;

function TXDataWebClient.EntityTypeFromEntitySet(
  const EntitySetName: string): TXDataEntityType;
begin
  Result := nil;
  Result := Model.GetEntitySet(EntitySetName).EntityType;

  if Result = nil then
    raise EXDataClientException.CreateFmt('Invalid entity set "%s"', [EntitySetName]);
end;

procedure TXDataWebClient.Get(const EntitySet: string; Id: JSValue;
  const RequestId: string = 'get');
begin
  Get(EntitySet, '', id, RequestId);
end;

procedure TXDataWebClient.Get(const EntitySet, QueryString: string; Id: JSValue;
  const RequestId: string);
begin
  InternalGet(EntitySet, QueryString, Id, RequestId, nil, nil);
end;

procedure TXDataWebClient.Get(const EntitySet: string; Id: JSValue;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalGet(EntitySet, '', Id, 'get', SuccessProc, ErrorProc);
end;

function TXDataWebClient.GetNavPropValue(Entity: JSValue;
  Prop: TXDataNavigationProperty): JSValue;
begin
  Result := TJSObject(Entity)[Prop.Name];
end;

function TXDataWebClient.GetPropValue(Entity: JSValue;
  Prop: TXDataSimpleProperty): JSValue;
begin
  Result := TJSObject(Entity)[Prop.Name];
end;

procedure TXDataWebClient.InternalDelete(const EntitySet: string;
  Entity: TJObject; const RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
var
  Req: IHttpRequest;
  RequestUri: string;

  procedure LocalSuccess(Resp: IHttpResponse);
  begin
    if Resp.StatusCode = 204 then
      DoLoad(TXDataClientResponse.Create(Self, Resp, JS.NULL, rtDelete, RequestId), SuccessProc, AsyncMode)
    else
      DoError(Resp, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

begin
  RequestUri := RequestUrl(TBclUtils.PercentEncode(
    Model.CanonicalUrlFromEntity(EntityTypeFromEntitySet(EntitySet), Entity,
      @GetPropValue, @GetNavPropValue)));

  Req := THttpRequest.Create(RequestUri, 'DELETE');
  DoRequest(TXDataClientRequest.Create(Self, Req, rtDelete, RequestId));
  Send(Req, @LocalSuccess, @LocalError);
end;

procedure TXDataWebClient.InternalGet(const EntitySet, QueryString: string;
  Id: JSValue; const RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
var
  RequestUri: string;

  procedure LocalSuccess(Resp: IHttpResponse);
  var
    JsonResult: JSValue;
  begin
    if Resp.StatusCode = 200 then
    begin
      JsonResult := TJSJSON.parse(Resp.ContentAsText);
      if isObject(JsonResult) then
        DoLoad(TXDataClientResponse.Create(Self, Resp, JsonResult, rtGet, RequestId), SuccessProc, AsyncMode)
      else
        DoError(Resp, RequestUri, RequestId, 'Invalid JSON object from GET request', ErrorProc, AsyncMode);
    end
    else
    if Resp.StatusCode = 404 then
      DoLoad(TXDataClientResponse.Create(Self, Resp, JS.NULL, rtGet, RequestId), SuccessProc, AsyncMode)
    else
      DoError(Resp, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

var
  Req: IHttpRequest;
begin
  RequestUri := RequestUrl(TBclUtils.PercentEncode(
    Model.CanonicalUrlFromId(EntityTypeFromEntitySet(EntitySet), Id)));
  if QueryString <> '' then
    RequestUri := RequestUri + '?' + QueryString;

  Req := THttpRequest.Create(RequestUri);
  DoRequest(TXDataClientRequest.Create(Self, Req, rtGet, RequestId));
  Send(Req, @LocalSuccess, @LocalError);
end;

procedure TXDataWebClient.InternalList(const EntitySet, Query,
  RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean);
var
  RequestUri: string;

  procedure LocalSuccess(Resp: IHttpResponse);
  var
    JsonResult: JSValue;
    ObjResult: TJSObject;
    Count: Integer;
  begin
    if Resp.StatusCode = 200 then
    begin
      JsonResult := TJSJSON.parse(Resp.ContentAsText);
      if isObject(JsonResult) and isArray(TJSObject(JsonResult)['value']) then
      begin
        ObjResult := TJSObject(JsonResult);
        if ObjResult.hasOwnProperty('@xdata.count') then
          Count := Integer(ObjResult['@xdata.count'])
        else
          Count := 0;
        DoLoad(TXDataClientResponse.Create(Self, Resp, ObjResult['value'], rtList, RequestId, Count), SuccessProc, AsyncMode);
      end
      else
        DoError(Resp, RequestUri, RequestId, 'Invalid JSON array from LIST request', ErrorProc, AsyncMode);
    end
    else
      DoError(Resp, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

var
  Req: IHttpRequest;
begin
  RequestUri := RequestUrl(TBclUtils.PercentEncode(
    Model.EntitySetUrl(Model.GetEntitySet(EntitySet))));
  if Query <> '' then
    RequestUri := RequestUri + '?' + Query;

  Req := THttpRequest.Create(RequestUri);
  DoRequest(TXDataClientRequest.Create(Self, Req, rtList, RequestId));
  Send(Req, @LocalSuccess, @LocalError);
end;

procedure TXDataWebClient.InternalPost(const EntitySet: string;
  Entity: TJObject; const RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
var
  RequestUri: string;

  procedure LocalSuccess(Resp: IHttpResponse);
  var
    Value: JSValue;
    Obj: TJSObject;
    EntityType: TXDataEntityType;
    I: Integer;
    Prop: string;
  begin
    // SetIdFromResponse
    if Resp.StatusCode = 201 then
    begin
      // Get the resulted object and set the id from the result
      Value := TJSJSON.parse(Resp.ContentAsText);
      if IsObject(Value) then
      begin
        Obj := ToObject(Value);
        EntityType := EntityTypeFromEntitySet(EntitySet);
        for I := 0 to EntityType.Key.Count - 1 do
        begin
          Prop := EntityType.Key[I].Name;
          if not IsNull(Obj[Prop]) then
            Entity[Prop] := Obj[Prop];
        end;
      end;

      DoLoad(TXDataClientResponse.Create(Self, Resp, TJSJSON.parse(Resp.ContentAsText), rtPost, RequestId), SuccessProc, AsyncMode);
    end
    else
      DoError(Resp, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

var
  Req: IHttpRequest;
begin
  RequestUri := RequestUrl(TBclUtils.PercentEncode(
    Model.EntitySetUrl(Model.GetEntitySet(EntitySet))
  ));

  Req := THttpRequest.Create(RequestUri, 'POST');
  Req.Headers.SetValue('content-type', 'application/json');
  Req.Content := TJSJson.stringify(Entity);
  DoRequest(TXDataClientRequest.Create(Self, Req, rtPost, RequestId));
  Send(Req, @LocalSuccess, @LocalError);
end;

procedure TXDataWebClient.InternalPut(const EntitySet: string; Entity: TJObject;
  const RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
var
  RequestUri: string;

  procedure LocalSuccess(Resp: IHttpResponse);
  begin
    if Resp.StatusCode = 200 then
      DoLoad(TXDataClientResponse.Create(Self, Resp, JS.NULL, rtPut, RequestId), SuccessProc, AsyncMode)
    else
      DoError(Resp, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUri, RequestId, '', ErrorProc, AsyncMode);
  end;

var
  Req: IHttpRequest;
begin
  RequestUri := RequestUrl(TBclUtils.PercentEncode(
    Model.CanonicalUrlFromEntity(EntityTypeFromEntitySet(EntitySet), Entity,
      @GetPropValue, @GetNavPropValue)));

  Req := THttpRequest.Create(RequestUri, 'PUT');
  Req.Headers.SetValue('content-type', 'application/json');
  Req.Content := TJSJson.stringify(Entity);
  DoRequest(TXDataClientRequest.Create(Self, Req, rtPut, RequestId));
  Send(Req, @LocalSuccess, @LocalError);
end;

procedure TXDataWebClient.InternalRawInvoke(const OperationId: string;
  Args: array of JSValue; RequestId: string; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc; AsyncMode: Boolean = False);
var
  Invoker: TXDataWebInvoker;
  Action: TXDataAction;
begin
  if RequestId = '' then
    RequestId := OperationId;

  Action := Model.FindActionByOperationId(OperationId);
  if Action = nil then
    raise EXDataClientException.CreateFmt('Operation "%s" not found', [OperationId]);

  Invoker := TXDataWebInvoker.Create(Self, Action, SuccessProc, ErrorProc, AsyncMode);
  Invoker.RequestId := RequestId;
  Invoker.Execute(Args);
end;

procedure TXDataWebClient.List(const EntitySet, Query: string;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalList(EntitySet, Query, 'list', SuccessProc, ErrorProc);
end;

procedure TXDataWebClient.List(const EntitySet, Query: string; const RequestId: string = 'list');
begin
  InternalList(EntitySet, Query, RequestId, nil, nil);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.ListAsync(const EntitySet: string; const Query: string = ''): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalList(EntitySet, Query, 'list',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

function TXDataWebClient.Model: TXDataModel;
begin
  CheckConnection;
  if not Assigned(Connection.Model) then
    raise EXDataClientException.Create('TXDataWebConnection is not connected');
  Result := Connection.Model;
end;

procedure TXDataWebClient.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited;
  if (Operation = opRemove) and (AComponent = FConnection) then
    Connection := nil;
end;

procedure TXDataWebClient.ParseError(Response: IHttpResponse; var ErrorCode,
  ErrorMessage: string);
var
  ErrorResult: JSValue;
  ErrorObject: TJSObject;
begin
  ErrorCode := '';
  ErrorMessage := Response.StatusReason;
  if SameText(Response.Headers.Get('content-type'), 'application/json') then
  begin
    ErrorResult := JS.Null;
    try
      ErrorResult := TJSJson.parse(Response.ContentAsText);
    except
    end;
    if IsObject(ErrorResult) and IsObject(TJSObject(ErrorResult)['error']) then
    begin
      ErrorObject := TJSObject(TJSObject(ErrorResult)['error']);
      ErrorCode := JS.ToString(ErrorObject['code']);
      ErrorMessage := JS.ToString(ErrorObject['message']);
    end;
  end;
end;

procedure TXDataWebClient.Post(const EntitySet: string; Entity: TJObject;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalPost(EntitySet, Entity, 'post', SuccessProc, ErrorProc);
end;

procedure TXDataWebClient.Post(const EntitySet: string; Entity: TJObject;
  const RequestId: string = 'post');
begin
  InternalPost(EntitySet, Entity, RequestId, nil, nil);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.PostAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalPost(EntitySet, Entity, 'post',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

procedure TXDataWebClient.Put(const EntitySet: string; Entity: TJObject;
  const RequestId: string = 'put');
begin
  InternalPut(EntitySet, Entity, RequestId, nil, nil);
end;

procedure TXDataWebClient.RawInvoke(const OperationId: string;
  Args: array of JSValue; RequestId: string);
begin
  InternalRawInvoke(OperationId, Args, RequestId, nil, nil);
end;

procedure TXDataWebClient.RawInvoke(const OperationId: string;
  Args: array of JSValue; SuccessProc: TXDataClientLoadProc;
  ErrorProc: TXDataClientErrorProc);
begin
  InternalRawInvoke(OperationId, Args, '', SuccessProc, ErrorProc);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.RawInvokeAsync(const OperationId: string;
  Args: array of JSValue): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalRawInvoke(OperationId, Args, '',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

function TXDataWebClient.RequestUrl(const RelativeUrl: string): string;
begin
  Result := TXDataUtils.CombineUrlFast(Self.Url, RelativeUrl);
end;

procedure TXDataWebClient.Send(Req: IHttpRequest; SuccessProc: THttpResponseProc;
  ErrorProc: THttpErrorProc);
begin
  Connection.SendRequest(Req, SuccessProc, ErrorProc);
end;

procedure TXDataWebClient.SetConnection(const Value: TXDataWebConnection);
begin
  if Value <> FConnection then
  begin
    if FConnection <> nil then
      FConnection.RemoveFreeNotification(Self);
    FConnection := Value;
    if FConnection <> nil then
      FConnection.FreeNotification(Self);
  end;
end;

//procedure TXDataWebClient.SetIdFromResponse(Entity: TObject; Req: THttpRequest;
//  Resp: THttpResponse);
//var
//  Uri: TUri;
//  Len: integer;
//  IdValue: Variant;
//  Clazz: TClass;
//begin
//  if not Resp.Headers.Exists('location') then
//    raise EXDataMissingLocationHeader.Create(Req.Uri);
//  Uri := TUri.Create(Resp.Headers.Get('location'));
//  try
//    Len := Length(Uri.Segments);
//    if Len = 0 then
//      raise EXDataInvalidLocationHeader.Create(Req.Uri, Resp.Headers.Get('location'));
//    Clazz := Entity.ClassType;
//    Model.ParseCanonicalUrl(Uri.Segments[Len - 1], Clazz, IdValue);
//  finally
//    Uri.Free;
//  end;
//
//  // If the server generates the id, the set it now
//  if not FModel.Explorer.GetId(Clazz).IsUserAssignedId then
//    FModel.Explorer.SetIdValue(Entity, IdValue);
//end;

procedure TXDataWebClient.SolveReferences(json: JSValue);
asm
    var byid = {}, // all objects by id
            refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
        if (typeof obj !== 'object' || !obj) // a primitive value
            return obj;
        if (Object.prototype.toString.call(obj) === '[object Array]') {
            for (var i = 0; i < obj.length; i++)
                // check also if the array element is not a primitive value
                if (typeof obj[i] !== 'object' || !obj[i]) // a primitive value
                    continue;
                else if ("$ref" in obj[i])
                    obj[i] = recurse(obj[i], i, obj);
                else
                    obj[i] = recurse(obj[i], prop, obj);
            return obj;
        }
        if ("$ref" in obj) { // a reference
            var ref = obj.$ref;
            if (ref in byid)
                return byid[ref];
            // else we have to make it lazy:
            refs.push([parent, prop, ref]);
            return;
        } else if ("$id" in obj) {
            var id = obj.$id;
            delete obj.$id;
            if ("$values" in obj) // an array
                obj = obj.$values.map(recurse);
            else // a plain object
                for (var p in obj)
                    obj[p] = recurse(obj[p], p, obj);
            byid[id] = obj;
        }
        return obj;
    })(json); // run it!

    for (var i = 0; i < refs.length; i++) { // resolve previously unknown references
        var ref = refs[i];
        ref[0][ref[1]] = byid[ref[2]];
        // Notice that this throws if you put in a reference at top-level
    }
end;

function TXDataWebClient.Url: string;
begin
  CheckConnection;
  Result := Connection.URL;
end;

procedure TXDataWebClient.Get(const EntitySet, QueryString: string; Id: JSValue;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalGet(EntitySet, QueryString, Id, 'get', SuccessProc, ErrorProc);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.GetAsync(const EntitySet, QueryString: string;
  Id: JSValue): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalGet(EntitySet, QueryString, Id, 'get',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

{$IFDEF JSASYNC}
function TXDataWebClient.GetAsync(const EntitySet: string;
  Id: JSValue): TXDataClientResponse;
begin
  Result := await(GetAsync(EntitySet, '', Id));
end;
{$ENDIF}

procedure TXDataWebClient.Put(const EntitySet: string; Entity: TJObject;
  SuccessProc: TXDataClientLoadProc; ErrorProc: TXDataClientErrorProc);
begin
  InternalPut(EntitySet, Entity, 'put', SuccessProc, ErrorProc);
end;

{$IFDEF JSASYNC}
function TXDataWebClient.PutAsync(const EntitySet: string; Entity: TJObject): TXDataClientResponse;
var
  Promise: TJSPromise;
begin
  Promise := TJSPromise.new(
    procedure(resolve, reject: TJSPromiseResolver)
    begin
      InternalPut(EntitySet, Entity, 'put',
        procedure(Response: TXDataClientResponse)
        begin
          resolve(Response);
        end,
        procedure(Error: TXDataClientError)
        begin
          reject(EXDataClientRequestException.Create(Error));
        end,
        True
      );
    end
  );
  Exit(Promise);
end;
{$ENDIF}

{ TXDataClientResponse }

constructor TXDataClientResponse.Create(AClient: TXDataWebClient; AResponse: IHttpResponse;
  AResult: JSValue; ARequestType: TXDataClientRequestType; ARequestId: string; ACount: Integer = 0);
begin
  FResponse := AResponse;
  FResult := AResult;
  FRequestType := ARequestType;
  FRequestId := ARequestId;
  FCount := ACount;
  FClient := AClient;
end;

function TXDataClientResponse.GetResponseText: string;
begin
  Result := Response.ContentAsText;
end;

function TXDataClientResponse.GetStatusCode: Integer;
begin
  Result := Response.StatusCode;
end;

function TXDataClientResponse.GetResultAsArray: TJSArray;
begin
  Result := JS.ToArray(Self.FResult);
end;

function TXDataClientResponse.GetResultAsObject: TJSObject;
begin
  Result := JS.ToObject(Self.FResult);
end;

{ EXDataClientRequestException }

constructor EXDataClientRequestException.Create(AClientError: TXDataClientError);
var
  Msg: string;
begin
  FErrorResult := AClientError;
  Msg := 'XData server request error.' + sLineBreak + 'Uri: ' + AClientError.RequestUrl;
  Msg := Msg + sLineBreak + 'Status code: ' + IntToStr(AClientError.StatusCode);
  if AClientError.ErrorCode <> '' then
    Msg := Msg + sLineBreak + 'Error Code: ' + AClientError.ErrorCode;
  if AClientError.ErrorMessage <> '' then
    Msg := Msg + sLineBreak + AClientError.ErrorMessage;
  inherited Create(Msg);
end;

{ TXDataClientError }

constructor TXDataClientError.Create(AClient: TXDataWebClient; AResponse: IHttpResponse;
  const ARequestUrl, AErrorCode, AErrorMessage, ARequestId: string);
begin
  FResponse := AResponse;
  FErrorCode := AErrorCode;
  FErrorMessage := AErrorMessage;
  FRequestId := ARequestId;
  FRequestUrl := ARequestUrl;
  FClient := AClient;
end;

function TXDataClientError.GetStatusCode: Integer;
begin
  Result := Response.StatusCode;
end;

{ TXDataClientRequest }

constructor TXDataClientRequest.Create(AClient: TXDataWebClient;
  ARequest: IHttpRequest; ARequestType: TXDataClientRequestType;
  ARequestId: string);
begin
  FRequest := ARequest;
  FRequestType := ARequestType;
  FRequestId := ARequestId;
  FClient := AClient;
end;

{ TXDataWebInvoker }

function TXDataWebInvoker.GetPathParamValues(Args: array of JSValue): TStringDynArray;
var
  I: Integer;
  Param: TXDataParamDef;
  ParamIndex: Integer;
begin
  ParamIndex := 0;
  SetLength(Result, Action.Parameters.Count);
  for I := 0 to Action.Parameters.Count - 1 do
    if Action.Parameters[I].BindingMode = TBindingMode.FromPath then
    begin
      Param := Action.Parameters[I];
      if Param.Input and (I <= Length(Args)) then
      begin
        Result[ParamIndex] :=
          TBclUtils.PercentEncode(
            BindValueToUrl(Args[I], nil {Param.RttiType.Handle}, Param.UrlConverter)
          );
        Inc(ParamIndex);
      end;
    end;
  Result := Copy(Result, 0, ParamIndex);
end;

function TXDataWebInvoker.BuildUrlRoute(Args: array of JSValue): string;
var
  PathParamValues: TStringDynArray;
  UsedParamValues: array of Boolean;

  procedure AddRouteSegments(RouteInfo: TXDataRouteInfo);
  var
    I: Integer;
  begin
    for I := 0 to RouteInfo.SegmentCount - 1 do
    begin
      if RouteInfo.Segments[I].IsParam then
      begin
        // Segment is a path param
        if RouteInfo.Segments[I].ParamIndex < Length(PathParamValues) then
          Result := Result + PathParamValues[RouteInfo.Segments[I].ParamIndex]
        else
          raise EXDataWrongRouteParams.Create(Action);
        UsedParamValues[RouteInfo.Segments[I].ParamIndex] := True;
      end
      else
        Result := Result + RouteInfo.Segments[I].Value;
      Result := Result + '/';
    end;
  end;

var
  I: Integer;
begin
  PathParamValues := GetPathParamValues(Args);
  SetLength(UsedParamValues, Length(PathParamValues));
  Result := '';

  AddRouteSegments(Action.RouteInfo);

  for I := 0 to Length(UsedParamValues) - 1 do
    if not UsedParamValues[I] then
      Result := Result + PathParamValues[I] + '/';
end;

function TXDataWebInvoker.BuildQueryString(Args: array of JSValue): string;
var
  QueryString: string;

  procedure WriteQueryParam(const Name, Value: string);
  begin
    if QueryString <> '' then
      QueryString := QueryString + '&';
    QueryString := QueryString + TBclUtils.PercentEncode(Name) +
      '=' + TBclUtils.PercentEncode(Value);
  end;

  procedure WriteObjectToQuery(Obj: TJSObject; AType: TXDataStructuredType);
  var
    I: Integer;
    Prop: TXDataScalarProperty;
    Value: JSValue;
  begin
    if Obj = nil then Exit;

    for I := 0 to AType.ScalarPropertyCount - 1 do
    begin
      Prop := AType.GetScalarProperty(I);
      Value := Obj[Prop.Name];
      if not JS.IsUndefined(Value) then
        WriteQueryParam(Prop.Name,
          BindValueToUrl(Value, nil {PropInfo.RttiInfo.RealType.Handle}, Prop.UrlConverter));
    end;
  end;

var
  I: Integer;
  Param: TXDataParamDef;
begin
  // set query string (parameters that are defined to be in uri query string)
  QueryString := '';
  for I := 0 to Action.Parameters.Count - 1 do
    if Action.Parameters[I].BindingMode = TBindingMode.FromURI then
    begin
      Param := Action.Parameters[I];
      if Param.Input and (I <= Length(Args)) then
      begin
        if Param.ParamType is TXDataStructuredType then
        begin
          Assert(JS.IsObject(Args[I]));
          WriteObjectToQuery(Js.ToObject(Args[I]), TXDataStructuredType(Param.ParamType));
        end
        else
          WriteQueryParam(Param.Name, BindValueToUrl(Args[I], nil {Param.RttiType.Handle}, Param.UrlConverter));
      end;
    end;
  if QueryString <> '' then
    QueryString := '?' + QueryString;

  Result := QueryString;
end;

constructor TXDataWebInvoker.Create(AClient: TXDataWebClient; AAction: TXDataAction;
  ASuccessProc: TXDataClientLoadProc; AErrorProc: TXDataClientErrorProc; AAsyncMode: Boolean);
begin
  FClient := AClient;
  FAction := AAction;
  FSuccessProc := ASuccessProc;
  FErrorProc := AErrorProc;
  FAsyncMode := AAsyncMode;
end;

function TXDataWebInvoker.CreateHttpRequest: IHttpRequest;
begin
  Result := THttpRequest.Create;
end;

procedure TXDataWebInvoker.DoError(Response: IHttpResponse; RequestUrl,
  RequestId, ErrorMessage: string);
begin
  FClient.DoError(Response, RequestUrl, RequestId, ErrorMessage, FErrorProc, FAsyncMode);
end;

procedure TXDataWebInvoker.DoLoad(Response: TXDataClientResponse);
begin
  FClient.DoLoad(Response, FSuccessProc, FAsyncMode);
end;

procedure TXDataWebInvoker.Execute(const Args: array of JSValue);
var
  RequestUrl: string;

  procedure OnSuccess(Resp: IHttpResponse);
  var
    ResultValue: JSValue;
    JsonResult: JSValue;
  begin
    // check errors
    if (Resp.StatusCode <> 200) and (Resp.StatusCode <> 204) and (Resp.StatusCode <> 404) then
    begin
      DoError(Resp, RequestUrl, RequestId, '');
      Exit;
    end;

    // has content
    if Action.IsResultStream then
    begin
      if Resp.StatusCode = 404 then
        DoError(Resp, RequestUrl, RequestId, '')
      else
      begin
        ResultValue := Resp.ContentAsText; //TBytesStream.Create(Resp.ContentAsBytes);
        DoLoad(TXDataClientResponse.Create(FClient, Resp, ResultValue, rtRawInvoke, RequestId));
      end;
    end
    else
    if Action.IsResultCriteria then
    begin
      DoError(Resp, RequestUrl, RequestId, 'TCriteria result yet not supported in TXDataClient');
    end
    else
    begin
      if (Resp.StatusCode <> 204) then
      begin
        JsonResult := TJSJSON.parse(Resp.ContentAsText)
      end
      else
        JsonResult := JS.Null;
      DoLoad(TXDataClientResponse.Create(FClient, Resp, JsonResult, rtRawInvoke, RequestId));
    end;
  end;

  procedure LocalError;
  begin
    DoError(nil, RequestUrl, RequestId, '');
  end;

var
  Req: IHttpRequest;
  QueryString: string;
  SingleBodyParamIndex: Integer;
begin
  // check number of parameters
  // note that first item in Args array is not the parameter, but instead always the interface pointer
//  if Action.Parameters.Count <> Length(Args) then
//    raise EXDataWrongParameterCount.Create(Action, Length(Args));

//  Resp := nil;
  Req := CreateHttpRequest;

  // Set request uri, method and other parameters before setting message body
  // TODO: Later Uri must be defined based on a more advanced routing system
  QueryString := BuildQueryString(Args);
  RequestUrl := BuildUrlRoute(Args);
  RequestUrl := RequestUrl + QueryString;
  RequestUrl := Self.RequestUrl(RequestUrl);
  Req.Uri := RequestUrl;
  Req.Method := Action.HttpMethod;

  if Action.IsParamStream then
    SetParamStreamBody(Req, Args, Action.ParamStreamIndex)
  else
  if Action.BodyParamCount > 0 then
  begin
    // Set content (parameters that are defined to be in the message body)
    // Check for special case: single parameter object and list. Server-side equivalent code for this is
    // in XData.Payload.Json.Reader unit: TJsonParamsReader.ReadParams method
    // Check there if you change something here.
//    if (Action.Parameters.Count = 1) and (Action.Parameters[0].ParamType is TXDataEntityType) then
    SingleBodyParamIndex := Action.BodyParams[0].Index;
    if (Action.BodyParamCount = 1) and (Length(Args) > SingleBodyParamIndex) and (JS.IsObject(Args[SingleBodyParamIndex])) then
      SetJsonObjectBody(Req, Args)
    else
      SetJsonParamsBody(Req, Args);
    Req.Headers.SetValue('content-type', 'application/json');
  end;

  FClient.DoRequest(TXDataClientRequest.Create(FClient, Req, rtRawInvoke, FRequestId));
  FClient.Send(Req, @OnSuccess, @LocalError);
end;

function TXDataWebInvoker.RequestUrl(const RelativeUrl: string): string;
begin
  Result := FClient.RequestUrl(RelativeUrl);
end;

procedure TXDataWebInvoker.SetJsonObjectBody(Req: IHttpRequest;
  const Args: array of JSValue);
var
  ParamIndex: Integer;
begin
  // Review this when Aurelius entities are supported
  // For now, just set the raw Json content
  ParamIndex := Action.BodyParams[0].Index;
  if Length(Args) <= ParamIndex then
    raise EXDataWrongParameterCount.Create(Action, ParamIndex + 1);
  Req.Content := TJSJson.stringify(Args[ParamIndex]);
end;

procedure TXDataWebInvoker.SetJsonParamsBody(Req: IHttpRequest;
  const Args: array of JSValue);
var
  I: Integer;
  Param: TXDataParamDef;
  ParamObj: TJSObject;
  ParamValue: JSValue;
begin
  ParamObj := TJSObject.new;
  for I := 0 to Action.Parameters.Count - 1 do
    if Action.Parameters[I].BindingMode = TBindingMode.FromBody then
    begin
      Param := Action.Parameters[I];
      if Param.Input and (I < Length(Args)) then
      begin
        ParamValue := Args[I];
        TransformParam(Param, ParamValue);
        ParamObj[Param.Name] := ParamValue;
      end;
    end;
  Req.COntent := TJSJson.stringify(ParamObj);
end;

procedure TXDataWebInvoker.SetParamStreamBody(Req: IHttpRequest;
  const Args: array of JSValue; ArgIndex: Integer);
begin
  if ArgIndex >= Length(Args) then
    raise EXDataWrongParameterCount.Create(Action, ArgIndex + 1);
  Req.Content := Args[ArgIndex];
end;

procedure TXDataWebInvoker.TransformParam(Param: TXDataParamDef;
  var Value: JSValue);
begin
//  case Param.ParamType.TypeKind of
////    xtDateTime,
////    xtDate,
////    xtTime,
//    TXTypeKind.xtGuid:
//      begin
//        // Must be TGuid type
//        if JS.IsObject(Value) then
//        begin
//          Value := Copy(GuidToString(TGuid(Value)), 2, 36);
//        end;
//      end;
////    xtBinary,
////    xtEnum,
////    xtScalarCollection
//  end;
end;

{ EXDataWrongParameterCount }

constructor EXDataWrongParameterCount.Create(Action: TXDataAction;
  PassedParameters: integer);
begin
  inherited CreateFmt('Incorrect parameter count for Operation "%s". Expected at least: %d, actual: %d',
    [Action.OperationId, Action.Parameters.Count, PassedParameters]);
end;

{ EXDataWrongRouteParams }

constructor EXDataWrongRouteParams.Create(Action: TXDataAction);
begin
  inherited CreateFmt('Invalid number of route params for Operation "%s". ' +
    'Route params: %d, Path params: %d',
    [Action.OperationId, Action.RouteInfo.ParamCount, Action.PathParamCount]);
end;

end.
