unit XData.Module.QueryInfo;

{$I XData.inc}

interface

uses
  Generics.Collections, System.Classes, System.Rtti,
  Bcl.Collections,
  XData.Model.Classes,
  XData.Query.Expression,
  XData.Query;

type
  TQueryPayloadFormat = (None, Atom, Xml, VerboseJson, Json, Custom);

  TQueryInlineCount = (None, AllPages);

  TXDataQueryInfo = class
  strict private
    FContextType: TXDataStructuredType;
    FQueryString: string;
    FQuery: TXDataQuery;
    FOwnsQuery: Boolean;
    FCustom: TStrings;
    FFilters: TList<TQueryExpression>;
    FOrders: TList<TQueryOrderInfo>;
    FExpandItems: TList<TQueryExpandInfo>;
    FExpandProps: TObjectSet<TXDataProperty>;
    FIsEmpty: boolean;
    FSkip: integer;
    FHasSkip: boolean;
    FTop: integer;
    FHasTop: boolean;
    FFormat: TQueryPayloadFormat;
    FFormatValue: string;
    FSolved: boolean;
    FInlineCount: TQueryInlineCount;
    procedure SolveQuery(Query: TXDataQuery);
    procedure SolveQueryString(const Query: string);
    procedure SolveFilter(const Value: string);
    procedure SolveOrderBy(const Value: string);
    procedure SolveExpand(const Value: string);
    procedure SolveTop(const Value: string);
    procedure SolveSkip(const Value: string);
    procedure SolveFormat(const Value: string);
    procedure SolveCustomQuery(const Value: string);
    procedure SolveInlineCount(const Value: string);
    constructor Create(const AContextType: TXDataStructuredType); overload;
  public
    constructor Create(const AContextType: TXDataStructuredType; const AQueryString: string); overload;
    constructor Create(const AContextType: TXDataStructuredType; AQuery: TXDataQuery;
      AOwnsQuery: Boolean = False); overload;
    destructor Destroy; override;
    procedure Solve;
    property Solved: boolean read FSolved;
    property IsEmpty: boolean read FIsEmpty;
    property ContextType: TXDataStructuredType read FContextType;
    //
    property Custom: TStrings read FCustom;
    property Filters: TList<TQueryExpression> read FFilters;
    property Orders: TList<TQueryOrderInfo> read FOrders;
    property ExpandItems: TList<TQueryExpandInfo> read FExpandItems;
    property ExpandProps: TObjectSet<TXDataProperty> read FExpandProps;
    property Skip: integer read FSkip;
    property Top: integer read FTop;
    property HasSkip: boolean read FHasSkip;
    property HasTop: boolean read FHasTop;
    property Format: TQueryPayloadFormat read FFormat;
    property FormatValue: string read FFormatValue;
    property InlineCount: TQueryInlineCount read FInlineCount;
  end;

implementation

uses
  SysUtils, StrUtils,
  Sparkle.Utils,
  XData.Module.Exceptions,
  XData.Query.Exceptions,
  XData.Query.Parser;

{ TXDataQueryInfo }

constructor TXDataQueryInfo.Create(const AContextType: TXDataStructuredType;
  const AQueryString: string);
begin
  Create(AContextType);
  FIsEmpty := (AQueryString = '') or (AQueryString = '?');
  FQueryString := AQueryString;
  FSolved := FIsEmpty;
end;

constructor TXDataQueryInfo.Create(const AContextType: TXDataStructuredType; AQuery: TXDataQuery; AOwnsQuery: Boolean);
begin
  Create(AContextType);
  FIsEmpty := AQuery.IsEmpty;
  FQuery := AQuery;
  FOwnsQuery := AOwnsQuery;
  FSolved := FIsEmpty;
end;

constructor TXDataQueryInfo.Create(const AContextType: TXDataStructuredType);
begin
  inherited Create;
  FContextType := AContextType;
  FCustom := TStringList.Create;
  FFilters := TObjectList<TQueryExpression>.Create(true);
  FOrders := TObjectList<TQueryOrderInfo>.Create(true);
  FExpandItems := TObjectList<TQueryExpandInfo>.Create(true);
  FExpandProps := TObjectSet<TXDataProperty>.Create(false);
  FFormat := TQueryPayloadFormat.None;
end;

destructor TXDataQueryInfo.Destroy;
begin
  FExpandItems.Free;
  FExpandProps.Free;
  FOrders.Free;
  FCustom.Free;
  FFilters.Free;
  if FOwnsQuery then
    FQuery.Free;
  inherited;
end;

procedure TXDataQueryInfo.SolveQuery(Query: TXDataQuery);
begin
  if Query.Filter <> '' then
    SolveFilter(Query.Filter);

  if Query.OrderBy <> '' then
    SolveOrderBy(Query.OrderBy);

  FHasTop := Query.Top > 0;
  FTop := Query.Top;

  FHasSkip := Query.Skip > 0;
  FSkip := Query.Skip;
end;

procedure TXDataQueryInfo.SolveQueryString(const Query: string);
var
  QueryItem: TPair<string, string>;
  LowerName: string;
begin
  for QueryItem in TSparkleUtils.GetQueryParams(Query) do
  begin
    if (QueryItem.Key = '') and (QueryItem.Value = '') then Continue;
    LowerName := LowerCase(QueryItem.Key);
    if LowerName = '' then
      LowerName := LowerCase(QueryItem.Value);

    if LowerName = '$filter' then
      SolveFilter(QueryItem.Value)
    else
    if LowerName = '$orderby' then
      SolveOrderBy(QueryItem.Value)
    else
    if LowerName = '$expand' then
      SolveExpand(QueryItem.Value)
    else
    if LowerName = '$top' then
      SolveTop(QueryItem.Value)
    else
    if LowerName = '$skip' then
      SolveSkip(QueryItem.Value)
    else
    if LowerName = '$format' then
      SolveFormat(QueryItem.Value)
    else
    if LowerName = '$inlinecount' then
      SolveInlineCount(QueryItem.Value)
    else
    if LowerName[1] = '$' then
      raise EXDataHttpUnsupportedQuery.Create(QueryItem.Key)
    else
      SolveCustomQuery(
        TSparkleUtils.PercentEncode(QueryItem.Key) + '=' +
        TSparkleUtils.PercentEncode(QueryItem.Value));
  end;
end;

procedure TXDataQueryInfo.Solve;
begin
  if IsEmpty then Exit;

  // Decide if we should solve using FQuery or FQueryString
  if FQuery <> nil then
    SolveQuery(FQuery)
  else
  begin
    if FQueryString[1] = '?' then
      SolveQueryString(Copy(FQueryString, 2, MaxInt))
    else
      SolveQueryString(FQueryString);
  end;
  FSolved := True;
end;

procedure TXDataQueryInfo.SolveCustomQuery(const Value: string);
begin
  FCustom.Add(Value);
end;

procedure TXDataQueryInfo.SolveExpand(const Value: string);
var
  Parser: TQueryExpandParser;
begin
  Parser := TQueryExpandParser.Create(Value, FContextType);
  try
    FExpandItems.AddRange(Parser.ParseItems);
  finally
    Parser.Free;
  end;
end;

procedure TXDataQueryInfo.SolveFilter(const Value: string);
var
  Parser: TQueryParser;
begin
  Parser := TQueryParser.Create(Value, FContextType);
  try
    FFilters.Add(Parser.Parse);
  finally
    Parser.Free;
  end;
end;

procedure TXDataQueryInfo.SolveFormat(const Value: string);
var
  CompValue: string;
begin
  CompValue := Trim(Lowercase(Value));
  if CompValue = 'json' then
    FFormat := TQueryPayloadFormat.Json
  else
  if CompValue = 'verbosejson' then
    FFormat := TQueryPayloadFormat.VerboseJson
  else
  if CompValue = 'atom' then
    FFormat := TQueryPayloadFormat.Atom
  else
  if CompValue = 'xml' then
    FFormat := TQueryPayloadFormat.Xml
  else
    FFormat := TQueryPayloadFormat.Custom;
  FFormatValue := Value;
end;

procedure TXDataQueryInfo.SolveInlineCount(const Value: string);
begin
  if SameText(Value, 'allpages') then
    FInlineCount := TQueryInlineCount.AllPages
  else
  if SameText(Value, 'none') then
    FInlineCount := TQueryInlineCount.None
  else
    raise EQueryInvalidInlineCount.Create(Value);
end;

procedure TXDataQueryInfo.SolveOrderBy(const Value: string);
var
  Parser: TQueryOrderParser;
begin
  Parser := TQueryOrderParser.Create(Value, FContextType);
  try
    FOrders.AddRange(Parser.ParseOrders);
  finally
    Parser.Free;
  end;
end;

procedure TXDataQueryInfo.SolveSkip(const Value: string);
begin
  if not TryStrToInt(Value, FSkip) then
    raise EQueryInvalidSkip.Create(Value);
  FHasSkip := true;
end;

procedure TXDataQueryInfo.SolveTop(const Value: string);
begin
  if not TryStrToInt(Value, FTop) then
    raise EQueryInvalidTop.Create(Value);
  FHasTop := true;
end;

end.
