unit XData.Module.ResourceInfo;

interface

uses
  Generics.Collections,
  Sparkle.Uri,
  XData.Model.Classes;

type
  TNavigationPathItem = class
  strict private
    FNavProp: TXDataNavigationProperty;
  public
    constructor Create(const ANavProp: TXDataNavigationProperty);
    property NavProp: TXDataNavigationProperty read FNavProp;
  end;

  TXDataResourceType = (Error, EntitySet, Entity, SimpleProperty, CollectionProperty,
    EntityProperty, StreamProperty, PropertyValue, Metadata, ServiceDocument, Reference,
    Action, EntitySchema, Model);

  TResourcePath = class
  strict private
    FNavPath: TList<TNavigationPathItem>;
    FKey: string;
  public
    constructor Create; overload;
    destructor Destroy; override;
    property Key: string read FKey write FKey;
    property NavPath: TList<TNavigationPathItem> read FNavPath;
  end;

  TXDataResourceInfo = class
  strict private
    FSegments: TUriSegmentStack;
    FPath: TList<TResourcePath>;
    FModel: TXDataModel;
    FResourceType: TXDataResourceType;
    FEntitySet: TXDataEntitySet;
    FReferenceNavProp: TXDataNavigationProperty;
    FFinalProp: TXDataSimpleProperty;
//    FFinalType: TXDataEntityType;
    FFinalCollection: TXDataNavigationProperty;
    FActions: TXDataActionList;
    FErrorMessage: string;
    FErrorCode: integer;
    FSolved: boolean;
    FIsCount: Boolean;
    property Segments: TUriSegmentStack read FSegments;
  strict private
    procedure ErrorInvalidEntitySetOperation;
    procedure ErrorTooManyReferenceSegments;
    procedure ErrorTooManyPropertySegments;
    procedure ErrorUnknownResource(const PropName: string);
    procedure ErrorUnknownEntitySet(const Name: string);
    procedure ErrorUnknownPath(const Name: string);
    procedure ErrorNoDefaultAction(AController: TXDataController);
    procedure ErrorActionNotComposable(AAction: TXDataAction);
    procedure ErrorActionNotFound(const Name: string);
  strict private
    procedure DoEntitySet;
    procedure DoEntity;
    procedure DoProperty;
    procedure DoPropertyValue;
    procedure DoStreamProperty;
    procedure DoCollectionProperty;
    procedure DoCollectionPropertyCount;
    procedure DoMetadata;
    procedure DoModel;
    procedure DoServiceDocument;
    procedure DoReference;
    procedure DoEntitySetCount;
    procedure DoEntitySchema;
    procedure DoAction;
  strict private
    procedure PushPath(const APath: TResourcePath);
    function PeekPath: TResourcePath;
  strict private
    procedure SolveEntitySet(AEntitySet: TXDataEntitySet);
    procedure SolveEntity(AType: TXDataEntityType);
    procedure SolveProperty(AProp: TXDataSimpleProperty);
    procedure SolveNavigationProperty(ANavProp: TXDataNavigationProperty);
    procedure SolveRef(ANavProp: TXDataNavigationProperty);
    procedure SolveController(AController: TXDataController);
  public
    constructor Create(const AModel: TXDataModel; const ASegments: TArray<string>);
    destructor Destroy; override;
    procedure Solve;
    property Solved: boolean read FSolved;
    property Path: TList<TResourcePath> read FPath;
    property IsCount: Boolean read FIsCount;
    property ErrorCode: integer read FErrorCode;
    property ErrorMessage: string read FErrorMessage;
    // result properties
    property EntitySet: TXDataEntitySet read FEntitySet;
    property ResourceType: TXDataResourceType read FResourceType;
    property ReferenceNavProp: TXDataNavigationProperty read FReferenceNavProp;
    property FinalProp: TXDataSimpleProperty read FFinalProp;
    property FinalCollection: TXDataNavigationProperty read FFinalCollection;
    property Actions: TXDataActionList read FActions;
//    property FinalType: TXDataEntityType read FFinalType;
  end;

implementation

uses
  SysUtils,
  XData.Utils;

{ TXDataResourceInfo }

constructor TXDataResourceInfo.Create(const AModel: TXDataModel; const ASegments: TArray<string>);
begin
  FModel := AModel;
  FSegments := TUriSegmentStack.Create(ASegments);
  FPath := TObjectList<TResourcePath>.Create(true);
  FActions := TXDataActionList.Create;
end;

destructor TXDataResourceInfo.Destroy;
begin
  FActions.Free;
  FPath.Free;
  FSegments.Free;
  inherited;
end;

procedure TXDataResourceInfo.DoAction;
begin
  FResourceType := TXDataResourceType.Action;
end;

procedure TXDataResourceInfo.DoCollectionProperty;
begin
  FResourceType := TXDataResourceType.CollectionProperty;
end;

procedure TXDataResourceInfo.DoCollectionPropertyCount;
begin
  FResourceType := TXDataResourceType.CollectionProperty;
  FIsCount := True;
end;

procedure TXDataResourceInfo.DoEntitySetCount;
begin
  FResourceType := TXDataResourceType.EntitySet;
  FIsCount := True;
end;

procedure TXDataResourceInfo.DoEntity;
begin
  Assert(PeekPath <> nil);
  if PeekPath.NavPath.Count = 0 then
    FResourceType := TXDataResourceType.Entity
  else
    FResourceType := TXDataResourceType.EntityProperty;
end;

procedure TXDataResourceInfo.DoEntitySchema;
begin
  FResourceType := TXDataResourceType.EntitySchema;
end;

procedure TXDataResourceInfo.DoEntitySet;
begin
  FResourceType := TXDataResourceType.EntitySet;
end;

procedure TXDataResourceInfo.DoReference;
begin
  FResourceType := TXDataResourceType.Reference;
end;

procedure TXDataResourceInfo.DoMetadata;
begin
  FResourceType := TXDataResourceType.Metadata;
end;

procedure TXDataResourceInfo.DoModel;
begin
  FResourceType := TXDataResourceType.Model;
end;

procedure TXDataResourceInfo.DoProperty;
begin
  FResourceType := TXDataResourceType.SimpleProperty;
end;

procedure TXDataResourceInfo.DoPropertyValue;
begin
  FResourceType := TXDataResourceType.PropertyValue;
end;

procedure TXDataResourceInfo.DoServiceDocument;
begin
  FResourceType := TXDataResourceType.ServiceDocument;
end;

procedure TXDataResourceInfo.DoStreamProperty;
begin
  FResourceType := TXDataResourceType.StreamProperty;
end;

procedure TXDataResourceInfo.ErrorUnknownPath(const Name: string);
begin
  FErrorCode := 404;
  FErrorMessage := Format('Unknown path "%s"', [Name]);
end;

procedure TXDataResourceInfo.ErrorActionNotComposable(AAction: TXDataAction);
begin
  FErrorCode := 404;
  FErrorMessage := Format('Action "%s" not composable. Too many path segments.', [AAction.Name]);
end;

procedure TXDataResourceInfo.ErrorActionNotFound(const Name: string);
begin
  FErrorCode := 404;
  FErrorMessage := Format('Action not found: %s', [Name]);
end;

procedure TXDataResourceInfo.ErrorInvalidEntitySetOperation;
begin
  FErrorCode := 404;
  FErrorMessage := 'Unknown entity set operation';
end;

procedure TXDataResourceInfo.ErrorNoDefaultAction(AController: TXDataController);
begin
  FErrorCode := 400;
  FErrorMessage := Format('No default action for path "%s"', [AController.Name]);
end;

procedure TXDataResourceInfo.ErrorTooManyReferenceSegments;
begin
  FErrorCode := 400;
  FErrorMessage := 'Too many path segments after $ref';
end;

procedure TXDataResourceInfo.ErrorTooManyPropertySegments;
begin
  FErrorCode := 400;
  FErrorMessage := 'Too many path segments after property resource';
end;

procedure TXDataResourceInfo.ErrorUnknownEntitySet(const Name: string);
begin
  FErrorCode := 404;
  FErrorMessage := Format('Unknown entity set "%s"', [Name]);
end;

procedure TXDataResourceInfo.ErrorUnknownResource(const PropName: string);
begin
  FErrorCode := 404;
  FErrorMessage := Format('Unknown path resource "%s"', [PropName]);
end;

function TXDataResourceInfo.PeekPath: TResourcePath;
begin
  if FPath.Count > 0 then
    Result := FPath.Last
  else
    Result := nil;
end;

procedure TXDataResourceInfo.PushPath(const APath: TResourcePath);
begin
  FPath.Add(APath);
end;

procedure TXDataResourceInfo.Solve;
var
  Name, Key: string;
  Path: string;
  LocalController: TXDataController;
begin
  FResourceType := TXDataResourceType.Error;
  FSolved := true;
  if Segments.Count = 0 then
    DoServiceDocument
  else
  if SameText(Segments.Peek, '$metadata') and (Segments.Count = 1) then
    DoMetadata
  else
  if SameText(Segments.Peek, '$model') and (Segments.Count = 1) then
    DoModel
  else
  begin
    // check if path is in format Name(key)
    Path := Segments.Pop;
    if TXDataUtils.GetNameAndKey(Path, Name, Key) then
    begin
      Self.FEntitySet := FModel.FindEntitySet(Name);
      if Self.EntitySet <> nil then
      begin
        // support composite key later
        PushPath(TResourcePath.Create);
        PeekPath.Key := Key;
        SolveEntity(EntitySet.EntityType);
      end else
        ErrorUnknownEntitySet(Name);
    end
    else
    begin
      // check for simple name in path.
      // Search for entity sets or controllers
      Self.FEntitySet := FModel.FindEntitySet(Path);
      if Self.EntitySet <> nil then
      begin
        PushPath(TResourcePath.Create);
        SolveEntitySet(Self.EntitySet);
      end
      else
      begin
        LocalController := FModel.FindController(Path);
        if LocalController <> nil then
        begin
          SolveController(LocalController);
        end
        else
        begin
          LocalController := FModel.FindController('');
          if LocalController <> nil then
          begin
            Segments.Push;
            SolveController(LocalController);
          end
          else
            ErrorUnknownPath(Path);
        end;
      end;
    end;
  end;
end;

procedure TXDataResourceInfo.SolveController(AController: TXDataController);
var
  ActionName: string;
begin
  if Segments.Count = 0 then
  begin
    AController.FindActions('', FActions);
    if FActions.Count > 0 then
      DoAction
    else
      ErrorNoDefaultAction(AController);
  end
  else
  begin
    ActionName := Segments.Pop;
    AController.FindActions(ActionName, FActions);
    if FActions.Count > 0 then
    begin
      if Segments.Count = 0 then
        DoAction
      else
        ErrorActionNotComposable(FActions[0]);
    end else
      ErrorActionNotFound(ActionName);
  end
end;

procedure TXDataResourceInfo.SolveEntitySet(AEntitySet: TXDataEntitySet);
begin
  if Segments.Count = 0 then
    DoEntitySet
  else
  if SameText(Segments.Peek, '$count') and (Segments.Count = 1) then
    DoEntitySetCount
  else
  if SameText(Segments.Peek, '$schema') and (Segments.Count = 1) then
    DoEntitySchema
  else
    ErrorInvalidEntitySetOperation; // support entity set operation later here
end;

procedure TXDataResourceInfo.SolveRef(ANavProp: TXDataNavigationProperty);
begin
  if Segments.Count = 0 then
  begin
//    Self.FFinalType := ANavProp.Parent;
    Self.FReferenceNavProp := ANavProp;
    DoReference;
  end
  else
    ErrorTooManyReferenceSegments;
end;

procedure TXDataResourceInfo.SolveNavigationProperty(ANavProp: TXDataNavigationProperty);
begin
  if SameText(Segments.Peek, '$ref') then
  begin
    Segments.Pop; // remove the "$ref" segment
    SolveRef(ANavProp);
  end
  else
  if not ANavProp.Target.IsCollection then
  begin
    PeekPath.NavPath.Add(TNavigationPathItem.Create(ANavProp));

//    SolveEntity(ANavProp.Target);
    // replaced SolveEntity by code here (copied and adapted from SolveEntity) to avoid, for now,
    // accessing multilevel of navigation properties (Order.Customer.City.Country for example)
    // for now, we will only allow one level (Order.Customer).
    if Segments.Count = 0 then
      DoEntity
    else
      ErrorUnknownResource(Segments.Peek);


  end else
  begin
    // collection property
    // for now, the collection property cannot have sub paths. It just retrieves the collection
    if Segments.Count = 0 then
    begin
      Self.FFinalCollection := ANavProp;
      DoCollectionProperty;
    end
    else
    if SameText(Segments.Peek, '$count') and (Segments.Count = 1) then
    begin
      Self.FFinalCollection := ANavProp;
      DoCollectionPropertyCount;
    end
    else
      ErrorTooManyPropertySegments;
  end;
end;

procedure TXDataResourceInfo.SolveProperty(AProp: TXDataSimpleProperty);
begin
  if Segments.Count = 0 then
  begin
    Self.FFinalProp := AProp;
    if AProp.TypeKind = TXTypeKind.xtStream then
      DoStreamProperty
    else
      DoProperty;
  end
  else
  if (Segments.Count = 1) and SameText(Segments.Peek, '$value') and (AProp.TypeKind <> TXTypeKind.xtStream) then
  begin
    Self.FFinalProp := AProp;
    DoPropertyValue;
  end
  else
    ErrorTooManyPropertySegments;
end;

procedure TXDataResourceInfo.SolveEntity(AType: TXDataEntityType);
var
  Prop: TXDataSimpleProperty;
  NavProp: TXDataNavigationProperty;
  PathName: string;
begin
  if Segments.Count = 0 then
  begin
//    Self.FFinalType := AType;
    DoEntity;
  end
  else
  begin
    PathName := Segments.Pop;
    Prop := AType.FindProperty(PathName);
    if Prop <> nil then
      SolveProperty(Prop)
    else
    begin
      NavProp := AType.FindNavigationProperty(PathName);
      if NavProp <> nil then
        SolveNavigationProperty(NavProp)
      else
        ErrorUnknownResource(PathName);
    end;
  end;
end;

{ TResourcePath }

constructor TResourcePath.Create;
begin
  FNavPath := TObjectList<TNavigationPathItem>.Create(true);
end;

destructor TResourcePath.Destroy;
begin
  FNavPath.Free;
  inherited;
end;

{ TNavigationPathItem }

constructor TNavigationPathItem.Create(
  const ANavProp: TXDataNavigationProperty);
begin
  FNavProp := ANavProp;
end;

end.
