unit XData.Aurelius.ModelBuilder;

{$I XData.inc}

interface

uses
  Generics.Collections, System.SysUtils, System.Rtti, System.TypInfo,
  System.Classes, System.Types,
  {$IFNDEF GENERICS}System.Contnrs,{$ENDIF}
  Aurelius.Mapping.Explorer,
  Aurelius.Mapping.Metadata,
  Aurelius.Mapping.Optimization,
  Bcl.Json.Binder,
  XData.Security.Attributes,
  XData.Security.Interfaces,
  XData.XmlDoc,
  XData.XmlDoc.Builder,
  XData.Aurelius.Model,
  XData.Json.Converters,
  XData.Model.Attributes,
  XData.Model.Classes,
  XData.Sys.Exceptions;

type
  TXDataModelBuilder = class
  strict private
    FModel: TXDataAureliusModel;
    FSchema: TXDataSchema;
    FContainer: TXDataEntityContainer;
    FEntityTypeMap: TDictionary<TEntityType, TXDataEntityType>;
    FInstanceTypeMap: TDictionary<TClass, TXDataInstanceType>;
    FUseOriginalClassNames: boolean;
    FUseOriginalFieldNames: boolean;
    FUseOriginalContractNames: boolean;
    FRtti: TRttiContext;
    FConverters: TXDataJsonConverters;
    FTypeMap: TDictionary<PTypeInfo, TXDataType>;
    FPrepared: Boolean;
    function Explorer: TMappingExplorer;
    function DefineEnumType(RttiType: TRttiOrdinalType): TXDataEnumType;
    function DefineEntityTypeFromClass(AClass: TClass): TXDataEntityType;
    function DefineEntityType(AEntityType: TEntityType): TXDataEntityType;
    function DefineInstanceType(AClass: TClass): TXDataInstanceType;
    function DefineStructuredType(AClass: TClass): TXDataStructuredType;
    procedure BuildEntityType(AType: TXDataEntityType; AEntityType: TEntityType);
    procedure BuildInstanceType(AType: TXDataInstanceType; AClass: TClass);
    procedure AddDeclaredProperties(AType: TXDataEntityType; AEntityType: TAbstractEntityType);
    function DefineEntitySet(AType: TXDataEntityType): TXDataEntitySet;
    procedure ImportServiceContracts;
    procedure ValidateParamDef(ParamDef: TXDataParamDef);
    procedure ValidateParamDefStructured(ParamDef: TXDataParamDef; AType: TXDataStructuredType);
    procedure ValidateAction(Action: TXDataAction);
    procedure DefineParamDefaultValue(ParamDef: TXDataParamDef; MethParam: TRttiParameter);
    function DetermineBindingMode(Action: TXDataAction; MethParam: TRttiParameter; RouteInfo: TXDataRouteInfo): TBindingMode;
    function AddAction(Controller: TXDataController; Meth: TRttiMethod): TXDataAction;
    procedure AddServiceContract(InterfaceType: TRttiInterfaceType);
    function AddNavigationProperty(AType: TXDataEntityType;
      ARttiInfo: TRttiOptimization): TXDataNavigationProperty;
    procedure TryAddProperty(AType: TXDataEntityType; ARttiMember: TRttiMember);
    function FindOrDefineScalarType(RttiType: TRttiType): TXDataScalarType;
    function FindOrDefineType(RttiType: TRttiType): TXDataType;
    procedure AddDefaultTypeMap;
    function GetAuthorizationRequirements(RttiObj: TRttiObject): TArray<IAuthorizationRequirement>;
    function AddInstanceSimpleProperty(AType: TXDataInstanceType; AField: TJsonBoundField;
      ARttiInfo: TRttiOptimization): TXDataInstanceSimpleProperty;
    function AddInstanceNavigationProperty(AType: TXDataInstanceType; AField: TJsonBoundField;
      ARttiInfo: TRttiOptimization): TXDataInstanceNavigationProperty;
    function GetCollectionType(AType: TXDataType): TXDataType;
  strict protected
    function EntityTypeName(AClass: TClass): string;
    function InstanceTypeName(AClass: TClass): string;
    function PropertyName(const AMember: TRttiOptimization): string;
    function EnumName(const ATypeName: string): string;
    function EntitySetName(const AType: TXDataEntityType): string;
    function ControllerName(const AType: TRttiInterfaceType): string;
  public
    constructor Create(AModel: TXDataAureliusModel);
    destructor Destroy; override;
    procedure Build;
    procedure LoadXmlDoc(const Folder: string = ''); overload;
    procedure Finish;

    function AddSimpleProperty(AType: TXDataEntityType;
      ARttiInfo: TRttiOptimization): TXDataSimpleProperty;

    function AddEntitySet(AClass: TClass): TXDataEntitySet;
    procedure AddService<T>;
    procedure RemoveEntitySet(AClass: TClass);
    class procedure BuildModel(AModel: TXDataAureliusModel);
    class procedure LoadXmlDoc(AModel: TXDataAureliusModel; const Folder: string = ''); overload;
    property UseOriginalClassNames: boolean read FUseOriginalClassNames write FUseOriginalClassNames;
    property UseOriginalFieldNames: boolean read FUseOriginalFieldNames write FUseOriginalFieldNames;
    property UseOriginalContractNames: boolean read FUseOriginalContractNames write FUseOriginalContractNames;
    property Model: TXDataAureliusModel read FModel;
  end;

  EMultipleColumnsMemberException = class(Exception)
  public
    constructor Create(AProp: TEntityProperty);
  end;

  ECannotFindPropertyKey = class(Exception)
  public
    constructor Create(AInfo: TRttiOptimization);
  end;

  EAssociationNotFound = class(Exception)
  public
    constructor Create(AInfo: TRttiOptimization);
  end;

  EAssociationPartnerNotFound = class(Exception)
  public
    constructor Create(A: TAssociation);
  end;

  EUnsupportedActionResultType = class(EXDataException)
  public
    constructor Create(AAction: TXDataAction; AMethod: TRttiMethod);
  end;

  EUnsupportedNavigationPropertyType = class(EXDataException)
  public
    constructor Create(ANavProp: TXDataStructuredProperty; const ATypeName: string);
  end;

  EParamByRefNotSupported = class(EXDataException)
  public
    constructor Create(AParamDef: TXDataParamDef);
  end;

  EOutputParamsNotAllowed = class(EXDataException)
  public
    constructor Create(AAction: TXDataAction);
  end;

  EInvalidParamBinding = class(EXDataException)
  public
    constructor Create(AParamDef: TXDataParamDef);
  end;

  EParamTypeNotSpecified = class(EXDataException)
  public
    constructor Create(AAction: TXDataAction; AParam: TRttiParameter);
  end;

  EUnsupportedActionParamType = class(EXDataException)
  public
    constructor Create(AParamDef: TXDataParamDef);
  end;

  EInvalidStreamParam = class(EXDataException)
  public
    constructor Create(AAction: TXDataAction);
  end;

  EInvalidDefaultvalue = class(EXDataException)
  public
    constructor Create(AAction: TXDataAction; AParam: TRttiParameter);
  end;

  EEntityTypeFromClassNotMapped = class(EXDataException)
  public
    constructor Create(AClass: TClass);
  end;

  EPrimitiveCollectionNotSupported = class(EXDataException)
  public
    constructor Create(const APrimitiveName: string);
  end;

  EModelBuilderException = class(EXDataException)
  end;

resourcestring
  STypeShouldBeScalar = 'Type "%s" should be a scalar type';

implementation

uses
  Aurelius.Mapping.MappedClasses,
  Aurelius.Types.Blob,
  Bcl.Collections,
  Bcl.Json.ObjectConverter,
  Bcl.Rtti.Utils,
  Bcl.Json.Attributes,
  Bcl.Json.Converters,
//  XData.XmlDoc.Builder,
//  XData.XmlDoc,
  XData.Model.Types,
  XData.Service.Common,
  XData.Service.Internal,
  XData.Types.Map;

{ EMultipleColumnsMemberException }

constructor EMultipleColumnsMemberException.Create(AProp: TEntityProperty);
begin
  inherited CreateFmt('Property "%s.%s" is mapped to more than one database columns.',
    [AProp.EntityType.Name, AProp.Name]);
end;

{ ECannotFindPropertyKey }

constructor ECannotFindPropertyKey.Create(
  AInfo: TRttiOptimization);
begin
  inherited CreateFmt('Property "%s.%s" could not be defined as key in entity type',
    [AInfo.MemberRef.Parent.Name, AInfo.MemberName]);
end;

{ EAssociationNotFound }

constructor EAssociationNotFound.Create(
  AInfo: TRttiOptimization);
begin
  inherited CreateFmt('Association not found for member "%s.%s"',
    [AInfo.MemberRef.Parent.Name, AInfo.MemberName]);
end;

{ EAssociationPartnerNotFound }

constructor EAssociationPartnerNotFound.Create(
  A: TAssociation);
begin
  inherited CreateFmt('Could not find member "%s.%s" mapped by association "%s.%s"',
    [A.Target.ClassName, A.MappedBy, A.Optimization.MemberRef.Parent.Name,
     A.Optimization.MemberName]);
end;

{ EParamByRefNotSupported }

constructor EParamByRefNotSupported.Create(AParamDef: TXDataParamDef);
begin
  inherited CreateFmt('Cannot define action "%s": Param "%s" cannot be passed as reference',
    [AParamDef.Parent.Name, AParamDef.Name]);
end;

constructor EParamTypeNotSpecified.Create(AAction: TXDataAction; AParam: TRttiParameter);
begin
  inherited CreateFmt('Cannot define action "%s": Param "%s" does not have a type', [AAction.Name, AParam.Name]);
end;

{ EUnsupportedNavigationPropertyType }

constructor EUnsupportedNavigationPropertyType.Create(ANavProp: TXDataStructuredProperty; const ATypeName: string);
begin
  inherited CreateFmt('Cannot define navigation property "%s": ' +
    'Invalid type "%s"', [ANavProp.QualifiedName, ATypeName]);
end;

{ EUnsupportedActionResultType }

constructor EUnsupportedActionResultType.Create(AAction: TXDataAction; AMethod: TRttiMethod);
var
  MethodName: string;
  ResultTypeName: string;
begin
  if AMethod.Parent <> nil then
    MethodName := AMethod.Parent.Name + '.' + AMethod.Name
  else
    MethodName := AMethod.Name;

  if AMethod.ReturnType <> nil then
    ResultTypeName := AMethod.ReturnType.Name
  else
    ResultTypeName := '';

  inherited CreateFmt('Cannot define action: Invalid result type "%s" in method "%s"', [ResultTypeName, MethodName]);
end;

{ EUnsupportedActionParamType }

constructor EUnsupportedActionParamType.Create(AParamDef: TXDataParamDef);
var
  ParamTypeName: string;
begin
  if AParamDef.RttiType <> nil then
    ParamTypeName := AParamDef.RttiType.Name
  else
    ParamTypeName := '';

  inherited CreateFmt('Cannot define action "%s": Invalid type "%s" for param "%s"',
    [AParamDef.Parent.Name, ParamTypeName, AParamDef.Name]);
end;

{ EInvalidDefaultValue }

constructor EInvalidDefaultValue.Create(AAction: TXDataAction;
  AParam: TRttiParameter);
begin
  inherited CreateFmt('Cannot define action "%s": Invalid default value for param "%s"',
    [AAction.Name, AParam.Name]);
end;

{ EEntityTypeFromClassNotMapped }

constructor EEntityTypeFromClassNotMapped.Create(AClass: TClass);
begin
  inherited CreateFmt('Cannot create entity type from class %s. Class is not mapped', [AClass.ClassName]);
end;

{ EPrimitiveCollectionNotSupported }

constructor EPrimitiveCollectionNotSupported.Create(const APrimitiveName: string);
begin
  inherited CreateFmt('Collection of primitive types (%s) not allowed', [APrimitiveName]);
end;

{ TXDataModelBuilder }

function TXDataModelBuilder.AddAction(Controller: TXDataController; Meth: TRttiMethod): TXDataAction;
var
  Action: TXDataAction;
  ActionInfo: TXDataAureliusActionInfo;
  RouteInfo: TXDataRouteInfo;
  A: TCustomAttribute;
  MethParam: TRttiParameter;
  ParamDef: TXDataParamDef;
begin
  // create action object
  Action := TXDataAction.Create;
  Controller.Actions.Add(Action);

  // Set extra info
  ActionInfo := TXDataAureliusActionInfo.Create(Meth);
  Action.Data := ActionInfo;
  Action.OwnsDataObject := True;
  Action.IsResultStream := (Meth.ReturnType <> nil) and (Meth.ReturnType.Handle = TypeInfo(TStream));
  Action.IsResultCriteria := (Meth.ReturnType <> nil) and (Meth.ReturnType.QualifiedName = 'Aurelius.Criteria.Base.TCriteria');

  // set authorization requirements
  ActionInfo.Requirements.AddRange(GetAuthorizationRequirements(Meth));

  // Set action name and operation id
  Action.Name := Meth.Name;
  if Assigned(Meth.Parent) then
    Action.OperationId := Meth.Parent.Name + '.';
  Action.OperationId := Action.OperationId + Action.Name;
  RouteInfo := TXDataRouteInfo.Create;
  try
    // Override action settings if attributes are present
    for A in Meth.GetAttributes do
    begin
      if A is URIPathSegmentAttribute then
        Action.Name := URIPathSegmentAttribute(A).Path;
      if A is HttpMethodAttribute then
        Action.HttpMethod := HttpMethodAttribute(A).HttpMethod;
    end;

    // Update RouteInfo after action name is loaded (from URIPath attribute)
    RouteInfo.Process(Controller.Name + '/' + Action.Name);

    // add params
    for MethParam in Meth.GetParameters do
    begin
      // RTTI type must be available
      if MethParam.ParamType = nil then
        raise EParamTypeNotSpecified.Create(Action, MethParam);

      // Create parameter and set needed properties
      ParamDef := TXDataParamDef.Create;
      Action.Parameters.Add(ParamDef);
      ParamDef.Name := MethParam.Name;
      ParamDef.BindingMode := DetermineBindingMode(Action, MethParam, RouteInfo);
      ParamDef.RttiType := MethParam.ParamType;
      ParamDef.Output := (MethParam.Flags * [pfVar, pfOut]) <> [];
      ParamDef.Input := not (pfOut in MethParam.Flags);
      ParamDef.ParamType := FindOrDefineType(MethParam.ParamType);
      if (ParamDef.ParamType <> nil) and (ParamDef.ParamType.TypeKind = TXTypeKind.xtRaw) then
        Action.IsParamStream := True;
      DefineParamDefaultValue(ParamDef, MethParam);

      // Perform several validations to make sure the parameter is properly configured
      ValidateParamDef(ParamDef);
    end;

    // Define return type
    if (Meth.ReturnType <> nil) then
    begin
      if not Action.IsResultStream and not Action.IsResultCriteria then
      begin
        Action.ReturnType := FindOrDefineType(Meth.ReturnType);

        // check JSON
        if FConverters.Find(Meth.ReturnType.Handle) = nil then
          raise EUnsupportedActionResultType.Create(Action, Meth);

        // check JSON Schema
//          if FGenerators.Find(Meth.ReturnType.Handle) = nil then
//            raise EUnsupportedActionResultType.Create(Action, Meth);
      end;
    end;
    Action.IsResultSingleObject := (Meth.ReturnType <> nil) and not Action.HasOutputParams and Meth.ReturnType.IsInstance;

    // final checks and setup for the action
    ValidateAction(Action);
  finally
    RouteInfo.Free;
  end;
  Result := Action;
end;

procedure TXDataModelBuilder.AddDeclaredProperties(AType: TXDataEntityType; AEntityType: TAbstractEntityType);
var
  Col: TColumn;
  Prop: TXDataSimpleProperty;
  NavProp: TXDataNavigationProperty;
  RttiField: TRttiField;
  RttiProp: TRttiProperty;
  EntityProp: TEntityProperty;
begin
  // Add properties from abstract entities, since they are "declared" in base class
  if (AType.BaseType = nil) and (AEntityType.ParentType <> nil) then
    AddDeclaredProperties(AType, AEntityType.ParentType);

  for EntityProp in AEntityType.DeclaredProperties do
  begin
    if EntityProp.RttiInfo <> nil then
      if TRttiUtils.GetInstance.GetAttribute(EntityProp.RttiInfo.MemberRef, XDataExcludePropertyAttribute) <> nil then
        Continue;

    if EntityProp.IsAssociation then
    begin
      Assert(EntityProp.Association <> nil);
      NavProp := AddNavigationProperty(AType, EntityProp.RttiInfo);

      // Set specific properties based on Aurelius mapping
      NavProp.Required := TAssociationProp.Required in EntityProp.Association.Properties;
    end else
    begin
      // scalar property
      if EntityProp.Columns.Count <> 1 then
        raise EMultipleColumnsMemberException.Create(EntityProp);
      Col := EntityProp.Columns[0];

      Prop := AddSimpleProperty(AType, EntityProp.RttiInfo);

      // Set specific properties based on Aurelius mapping
      Prop.Length := Col.Length;
      Prop.Precision := Col.Precision;
      Prop.Scale := Col.Scale;
      Prop.Required := TColumnProp.Required in Col.Properties;
    end;
  end;

  // Iterate through regular fields/properties to add members mapped
  // with XDataProperty attribute
  for RttiField in FRtti.GetType(AEntityType.OriginClass).GetDeclaredFields do
    if TRttiUtils.GetInstance.GetAttribute(RttiField, XDataPropertyAttribute) <> nil then
      TryAddProperty(AType, RttiField);
  for RttiProp in FRtti.GetType(AEntityType.OriginClass).GetDeclaredProperties do
    if TRttiUtils.GetInstance.GetAttribute(RttiProp, XDataPropertyAttribute) <> nil then
      TryAddProperty(AType, RttiProp);
end;

procedure TXDataModelBuilder.AddDefaultTypeMap;
var
  Defaults: TXDataPrimitiveTypes;
begin
  XData.Types.Map.FillDefaultTypes(FTypeMap);

  Defaults := TXDataPrimitiveTypes.Instance;
  FTypeMap.AddOrSetValue(TypeInfo(TBlob), Defaults.StreamType);
end;

function TXDataModelBuilder.AddEntitySet(AClass: TClass): TXDataEntitySet;
begin
  FPrepared := False;
  Result := DefineEntitySet(DefineEntityTypeFromClass(AClass));
end;

function TXDataModelBuilder.AddNavigationProperty(AType: TXDataEntityType;
  ARttiInfo: TRttiOptimization): TXDataNavigationProperty;
begin
  Result := TXDataNavigationProperty.Create;
  AType.NavigationProperties.Add(Result);
  Result.Data := TXDataAureliusNavPropInfo.Create(ARttiInfo.Clone);
  Result.OwnsDataObject := True;
  Result.Name := PropertyName(ARttiInfo);
  Result.Description := Explorer.GetMemberDescription(ARttiInfo.MemberRef);
  if Explorer.IsObjectList(ARttiInfo.MemberClass) then
    Result.Target := DefineEntityTypeFromClass(ARttiInfo.SurroundedClass).Collection
  else
    Result.Target := DefineEntityTypeFromClass(ARttiInfo.MemberClass);
end;

procedure TXDataModelBuilder.AddService<T>;
var
  Info: PTypeInfo;
  RttiType: TRttiType;
begin
  FPrepared := False;
  Info := TypeInfo(T);
  if Info.Kind <> tkInterface then
    raise EXDataInvalidServiceType.Create(Info);

  RttiType := FRtti.GetType(Info);
  if not (RttiType is TRttiInterfaceType) then
    raise EXDataInvalidServiceType.Create(Info);

  AddServiceContract(TRttiInterfaceType(RttiType));
end;

procedure TXDataModelBuilder.AddServiceContract(InterfaceType: TRttiInterfaceType);
var
  Controller: TXDataController;
  Meth: TRttiMethod;
  TypeData: PTypeData;
  ControllerRequirements: TArray<IAuthorizationRequirement>;
  Action: TXDataAction;
  ActionInfo: TXDataAureliusActionInfo;
begin
  if Model.FindControllerByInterface(InterfaceType.Handle) <> nil then
    Exit;

  TypeData := GetTypeData(InterfaceType.Handle);
  if not (ifHasGuid in TypeData.IntfFlags) then
    raise EXDataServiceInterfaceWithoutGuid.Create(InterfaceType.Handle);

  Controller := TXDataController.Create;
  FSchema.Controllers.Add(Controller);
  Controller.Name := ControllerName(InterfaceType);
  Controller.Data := TXDataAureliusControllerInfo.Create(InterfaceType.Handle);
  Controller.OwnsDataObject := True;
  ControllerRequirements := GetAuthorizationRequirements(InterfaceType);
  for Meth in InterfaceType.GetMethods do
  begin
    // no need to check method kind or visibility here because it's an interface, but let's add extra checks anyway
    // in case we move/copy this code somewhere else
    if not (Meth.Visibility in [TMemberVisibility.mvPublic, TMemberVisibility.mvPublished]) then
      Continue;
    if not (Meth.MethodKind in [TMethodKind.mkProcedure, TMethodKind.mkFunction]) then
      Continue;

    Action := AddAction(Controller, Meth);
    ActionInfo := Model.GetActionInfo(Action);
    ActionInfo.Requirements.InsertRange(0, ControllerRequirements);
  end;
end;

function TXDataModelBuilder.AddInstanceNavigationProperty(AType: TXDataInstanceType; AField: TJsonBoundField;
  ARttiInfo: TRttiOptimization): TXDataInstanceNavigationProperty;
var
  CandidateType: TXDataType;
begin
  Result := TXDataInstanceNavigationProperty.Create;
  AType.NavigationProperties.Add(Result);
  Result.Data := TXDataAureliusInstanceNavPropInfo.Create(ARttiInfo.Clone);
  Result.OwnsDataObject := True;
  Result.Name := AField.Name;
  Result.Description := Explorer.GetMemberDescription(ARttiInfo.MemberRef);
  CandidateType := FindOrDefineType(ARttiInfo.RealType);
  if (CandidateType <> nil) and (CandidateType is TXDataStructuredType) then
    Result.Target := TXDataStructuredType(CandidateType);

// do not raise exception yet - not all types are supported
//  raise EUnsupportedNavigationPropertyType.Create(Result, ARttiInfo.RealType.Name);
end;

function TXDataModelBuilder.AddInstanceSimpleProperty(AType: TXDataInstanceType;
  AField: TJsonBoundField; ARttiInfo: TRttiOptimization): TXDataInstanceSimpleProperty;
var
  PropInfo: TXDataAureliusPropInfo;
  ConverterAttr: JsonConverterAttribute;
begin
  Result := TXDataInstanceSimpleProperty.Create;
  AType.Properties.Add(Result);
  Result.Name := AField.Name;
  Result.Description := Explorer.GetMemberDescription(ARttiInfo.MemberRef);
  Result.PropertyType := FindOrDefineScalarType(ARttiInfo.RealType);
  PropInfo := TXDataAureliusInstancePropInfo.Create(ARttiInfo.Clone, AField);
  Result.Data := PropInfo;
  Result.OwnsDataObject := true;
  if TRttiUtils.GetInstance.TryGetAttribute<JsonConverterAttribute>(ARttiInfo.MemberRef, ConverterAttr) then
    PropInfo.ConverterClass := ConverterAttr.ConverterClass;

  // Set specific properties
  if Result.TypeKind = TXTypeKind.xtText then
    Result.Length := 255;

  FPrepared := False;
end;

function TXDataModelBuilder.AddSimpleProperty(AType: TXDataEntityType;
  ARttiInfo: TRttiOptimization): TXDataSimpleProperty;
var
  PropInfo: TXDataAureliusPropInfo;
  ConverterAttr: JsonConverterAttribute;
begin
  Result := TXDataSimpleProperty.Create;
  AType.Properties.Add(Result);
  Result.Name := PropertyName(ARttiInfo);
  Result.Description := Explorer.GetMemberDescription(ARttiInfo.MemberRef);
  Result.PropertyType := FindOrDefineScalarType(ARttiInfo.RealType);
  PropInfo := TXDataAureliusPropInfo.Create(ARttiInfo.Clone);
  Result.Data := PropInfo;
  Result.OwnsDataObject := true;
  if TRttiUtils.GetInstance.TryGetAttribute<JsonConverterAttribute>(ARttiInfo.MemberRef, ConverterAttr) then
    PropInfo.ConverterClass := ConverterAttr.ConverterClass;
  FPrepared := False;
end;

procedure TXDataModelBuilder.Build;
var
  C: TClass;
  EntityType: TXDataEntityType;
begin
  FPrepared := False;
  for C in Explorer.Hierarchy.Classes do
    DefineEntityTypeFromClass(C);
  for EntityType in FSchema.EntityTypes do
    DefineEntitySet(EntityType);
  ImportServiceContracts;
  Finish;
end;

procedure TXDataModelBuilder.BuildEntityType(AType: TXDataEntityType;
  AEntityType: TEntityType);
var
  KeyProp: TXDataProperty;
  KeyMember: TRttiOptimization;
begin
  AType.Name := EntityTypeName(AEntityType.OriginClass);

  if AEntityType.Parent <> nil then
    AType.BaseType := DefineEntityType(AEntityType.Parent);

  // create properties and navigation properties
  AddDeclaredProperties(AType, AEntityType);

  // set keys - not for inherited types
  if AType.BaseType = nil then
    for KeyMember in AEntityType.SafeId.Members do
    begin
      KeyProp := AType.FindProperty(PropertyName(KeyMember));
      if KeyProp = nil then
        KeyProp := AType.FindNavigationProperty(PropertyName(KeyMember));

      if KeyProp = nil then
        raise ECannotFindPropertyKey.Create(KeyMember);
      AType.Key.Add(KeyProp);
    end;
end;

procedure TXDataModelBuilder.BuildInstanceType(AType: TXDataInstanceType; AClass: TClass);
var
  Fields: TList<TJsonBoundField>;
  Field: TJsonBoundField;
  Binder: TJsonFieldBinder;
  RttiInfo: TRttiOptimization;
begin
  AType.Name := InstanceTypeName(AClass);

  // create properties and navigation properties
  Binder := TJsonFieldBinder.Create(FConverters);
  try
    Fields := Model.GetInstanceTypeInfo(AType).Fields;
    Binder.LoadBoundFields(AClass, Fields);
    for Field in Fields do
    begin
      RttiInfo := Explorer.GetOptimization(Field.Member);
      try
        if Field.MemberType.IsInstance then
          AddInstanceNavigationProperty(AType, Field, RttiInfo)
        else
          AddInstanceSimpleProperty(AType, Field, RttiInfo);
      finally
        RttiInfo.Free;
      end;
    end;
  finally
    Binder.Free;
  end;
end;

class procedure TXDataModelBuilder.BuildModel(AModel: TXDataAureliusModel);
var
  Builder: TXDataModelBuilder;
begin
  Builder := TXDataModelBuilder.Create(AModel);
  try
    Builder.Build;
  finally
    Builder.Free;
  end;
end;

function TXDataModelBuilder.ControllerName(const AType: TRttiInterfaceType): string;
var
  A: URIPathSegmentAttribute;
begin
  Result := AType.Name;
  if not UseOriginalContractNames then
  begin
    if (Length(Result) > 1) and (Result[1] = 'I') then
      Result := Copy(Result, 2, MaxInt)
  end;

  if TRttiUtils.GetInstance.TryGetAttribute<URIPathSegmentAttribute>(AType, A, True) then
    Result := A.Path;
end;

constructor TXDataModelBuilder.Create(AModel: TXDataAureliusModel);
begin
  FEntityTypeMap := TDictionary<TEntityType, TXDataEntityType>.Create;
  FInstanceTypeMap := TDictionary<TClass, TXDataInstanceType>.Create;
  FTypeMap := TDictionary<PTypeInfo, TXDataType>.Create;
  AddDefaultTypeMap;
  FModel := AModel;
  FRtti := TRttiContext.Create;
  FEntityTypeMap.Clear;
  if FModel.Schemas.Count = 0 then
    FModel.Schemas.Add(TXDataSchema.Create);
  FSchema := FModel.Schemas[0];
  FSchema.Namespace := 'XData.Default';
  FSchema.Alias := '';
  if FSchema.EntityContainers.Count = 0 then
    FSchema.EntityContainers.Add(TXDataEntityContainer.Create);
  FContainer := FSchema.EntityContainers[0];
  FContainer.Name := 'Default';
  FConverters := TXDataJsonConverters.Create(AModel);
  FPrepared := False;
end;

function TXDataModelBuilder.DefineEntitySet(
  AType: TXDataEntityType): TXDataEntitySet;
var
  Info: TXDataAureliusEntitySetInfo;
  A: TCustomAttribute;
  SecurityAttr: EntitySecurityAttribute;
  Clazz: TClass;
  RttiType: TRttiType;
  Permission: TEntitySetPermission;
begin
  Result := FModel.DefaultEntitySet(AType);
  if Result = nil then
  begin
    Result := TXDataEntitySet.Create;
    FContainer.EntitySets.Add(Result);
    Result.EntityType := AType;
    FModel.DefineDefaultEntitySet(AType, Result);
    Result.Name := EntitySetName(AType);

    Info := TXDataAureliusEntitySetInfo.Create;
    Result.Data := Info;
    Result.OwnsDataObject := True;

    // Add requirements from attributes
    Clazz := Model.GetEntityTypeInfo(AType).Clazz;
    if Clazz <> nil then
    begin
      RttiType := FRtti.GetType(Clazz);
      if RttiType <> nil then
        for A in RttiType.GetAttributes do
          if A is EntitySecurityAttribute then
          begin
            SecurityAttr := EntitySecurityAttribute(A);
            for Permission in SecurityAttr.GetPermissions do
              Info.Requirements[Permission].Add(SecurityAttr.GetRequirement);
          end;
    end;
  end;
end;

function TXDataModelBuilder.DefineEntityType(
  AEntityType: TEntityType): TXDataEntityType;
begin
  if FEntityTypeMap.TryGetValue(AEntityType, Result) then Exit;
  Result := TXDataEntityType.Create;
  Result.Data := TXDataAureliusEntityTypeInfo.Create(AEntityType);
  Result.OwnsDataObject := True;
  FSchema.EntityTypes.Add(Result);
  FEntityTypeMap.Add(AEntityType, Result);
  BuildEntityType(Result, AEntityType);
end;

function TXDataModelBuilder.DefineEntityTypeFromClass(AClass: TClass): TXDataEntityType;
var
  EntityType: TEntityType;
begin
  EntityType := Explorer.FindEntityTypeFromClass(AClass);
  if EntityType = nil then
    raise EEntityTypeFromClassNotMapped.Create(AClass);
  Result := DefineEntityType(EntityType);
end;

function TXDataModelBuilder.DefineEnumType(RttiType: TRttiOrdinalType): TXDataEnumType;
var
  I: integer;
  Attr: JsonEnumValuesAttribute;
begin
  Result := FSchema.FindEnumType(EnumName(RttiType.Name));
  if Result <> nil then
    Exit;
  Result := TXDataEnumType.Create;
  FSchema.EnumTypes.Add(Result);
  Result.Name := EnumName(RttiType.Name);
  Result.Data := RttiType;

  if TRttiUtils.GetInstance.TryGetAttribute<JsonEnumValuesAttribute>(RttiType, Attr) then
  begin
    for I := 0 to Attr.EnumValues.Count - 1 do
      Result.AddMember(Attr.EnumValues[I], I);
  end
  else
    for I := RttiType.MinValue to RttiType.MaxValue do
      Result.AddMember(GetEnumName(RttiType.Handle, I), I);
end;

function TXDataModelBuilder.DefineInstanceType(AClass: TClass): TXDataInstanceType;
var
  Converter: IJsonTypeConverter;
begin
  if FInstanceTypeMap.TryGetValue(AClass, Result) then Exit;

  // This is a way to avoid special class types like TStrings and others, which
  // are serialized via JSON in a different way. We will only add as instance type
  // those classes that are actually serialize using bound fields.
  Converter :=  FConverters.Find(AClass);
  if not (Assigned(Converter) and (Converter is TJsonObjectConverter)) then
    Exit(nil);

  Result := TXDataInstanceType.Create;
  Result.Data := TXDataAureliusInstanceTypeInfo.Create(AClass);
  Result.OwnsDataObject := True;
  FSchema.InstanceTypes.Add(Result);
  FInstanceTypeMap.Add(AClass, Result);
  BuildInstanceType(Result, AClass);
end;

procedure TXDataModelBuilder.DefineParamDefaultValue(ParamDef: TXDataParamDef; MethParam: TRttiParameter);
var
  A: TCustomAttribute;
  TempDefaultValue: TValue;
begin
  // Check if a default value is provided
  for A in MethParam.GetAttributes do
    if A is BaseDefaultAttribute then
    begin
      if not TValue.FromVariant(BaseDefaultAttribute(A).Value).TryCast(
        MethParam.ParamType.Handle, TempDefaultValue) then
        raise EInvalidDefaultValue.Create(ParamDef.Parent, MethParam);
      ParamDef.DefaultValue := TempDefaultValue;
    end;
end;

procedure TXDataModelBuilder.ValidateAction(Action: TXDataAction);
begin
  // If result is stream or criteria, it's a specific output format.
  // In this case, output parameters (var, out) are not allowed
  if Action.HasOutputParams and (Action.IsResultStream or Action.IsResultCriteria) then
    raise EOutputParamsNotAllowed.Create(Action);

  // If the action is supposed to receieve a stream parameter, then the whole request
  // body will be in the stream. Other body parameters are not allowed
  if Action.IsParamStream and (Action.BodyParamCount <> 1) then
    raise EInvalidStreamParam.Create(Action);
end;

procedure TXDataModelBuilder.ValidateParamDef(ParamDef: TXDataParamDef);
var
  Action: TXDataAction;
begin
  Action := ParamDef.Parent;

  // Instance types cannot be returned as "var" parameters
  if ParamDef.Output and ParamDef.RttiType.IsInstance then
    raise EParamByRefNotSupported.Create(ParamDef);

  // The param must be serializable via JSON, no matter what
  if FConverters.Find(ParamDef.RttiType.Handle) = nil then
    raise EUnsupportedActionParamType.Create(ParamDef);

  // check JSON Schema
//        if FGenerators.Find(MethParam.ParamType.Handle) = nil then
//          raise EUnsupportedActionParamType.Create(Action, MethParam);

  // Checks
  case ParamDef.BindingMode of
    TBindingMode.FromPath:
      begin
        // An url converter must exist for the scalar type. This already check for ParamType nil
        if FSchema.FindOrDefineConverter(ParamDef.ParamType) = nil then
          raise EUnsupportedActionParamType.Create(ParamDef);

        // for now, only accept scalar types from path
        if not (ParamDef.ParamType is TXDataScalarType) then
          raise EUnsupportedActionParamType.Create(ParamDef);
      end;
    TBindingMode.FromURI:
      begin
        // We must have XData types for parameters received via URI
        if ParamDef.ParamType = nil then
          raise EUnsupportedActionParamType.Create(ParamDef);

        // Treat it differently between scalar and structured types
        if ParamDef.ParamType is TXDataScalarType then
        begin
          // An url converter must exist for the scalar type. This already check for ParamType nil
          if FSchema.FindOrDefineConverter(ParamDef.ParamType) = nil then
            raise EUnsupportedActionParamType.Create(ParamDef);
        end
        else
        begin
          Assert(ParamDef.ParamType is TXDataStructuredType);
          ValidateParamDefStructured(ParamDef, TXDataStructuredType(ParamDef.ParamType));
        end;
      end;
    TBindingMode.FromBody:
      begin
        // Body parameter can't be received via GET
        if Action.HttpMethod = 'GET' then
          raise EInvalidParamBinding.Create(ParamDef);
      end;
  end;

  // Special check for streams: a TStream (raw) type can only be received via Body
  if (ParamDef.ParamType <> nil) and (ParamDef.ParamType.TypeKind = TXTypeKind.xtRaw) then
  begin
    if ParamDef.BindingMode <> TBindingMode.FromBody then
      raise EInvalidParamBinding.Create(ParamDef);
  end;
end;

procedure TXDataModelBuilder.ValidateParamDefStructured(ParamDef: TXDataParamDef; AType: TXDataStructuredType);
var
  I: Integer;
  Prop: TXDataScalarProperty;
begin
  if AType.StructuredPropertyCount > 0 then
    raise EUnsupportedActionParamType.Create(ParamDef);

  // All properties of the structured type must be scalar and have a converter
  for I := 0 to AType.ScalarPropertyCount - 1 do
  begin
    Prop := AType.GetScalarProperty(I);
    if not (Prop is TXDataScalarProperty) then
      raise EUnsupportedActionParamType.Create(ParamDef);

    // Ensure a converter exists
    if FSchema.FindOrDefineConverter(Prop.PropertyType) = nil then
      raise EUnsupportedActionParamType.Create(ParamDef);
  end;
end;

function TXDataModelBuilder.DefineStructuredType(AClass: TClass): TXDataStructuredType;
var
  EntityType: TEntityType;
begin
  EntityType := Explorer.FindEntityTypeFromClass(AClass);
  if EntityType <> nil then
    Result := DefineEntityType(EntityType)
  else
    Result := DefineInstanceType(AClass);
end;

destructor TXDataModelBuilder.Destroy;
begin
  try
    if not FPrepared then
      Finish;
  finally
    FEntityTypeMap.Free;
    FInstanceTypeMap.Free;
    FRtti.Free;
    FConverters.Free;
    FTypeMap.Free;
    inherited;
  end;
end;

function TXDataModelBuilder.DetermineBindingMode(Action: TXDataAction;
  MethParam: TRttiParameter; RouteInfo: TXDataRouteInfo): TBindingMode;
var
  SegmentIndex: Integer;
  Segment: TXDataRouteSegment;
  A: TCustomAttribute;
begin
  // default param binding mode: FromURI if it's a GET request, FromBody otherwise
  if Action.HttpMethod = 'GET' then
    Result := TBindingMode.FromURI
  else
    Result := TBindingMode.FromBody;

  // But, if the parameter is present in the route, then it's FromPath
  for SegmentIndex := 0 to RouteInfo.SegmentCount - 1 do
  begin
    Segment := RouteInfo.Segments[SegmentIndex];
    if Segment.IsParam and SameText(Segment.ParamName, MethParam.Name) then
      Result := TBindingMode.FromPath;
  end;

  // Finally if there is an attribute, it overrides everything
  for A in MethParam.GetAttributes do
  begin
    if A is FromQueryAttribute then
      Result := TBindingMode.FromURI;
    if A is FromPathAttribute then
      Result := TBindingMode.FromPath;
    if A is FromBodyAttribute then
      Result := TBindingMode.FromBody;
  end;
end;

function TXDataModelBuilder.EnumName(const ATypeName: string): string;
begin
  Result := ATypeName;
end;

function TXDataModelBuilder.Explorer: TMappingExplorer;
begin
  Result := FModel.Explorer;
end;

function TXDataModelBuilder.FindOrDefineScalarType(
  RttiType: TRttiType): TXDataScalarType;
var
  FoundType: TXDataType;
begin
  FoundType := nil;
  if not FTypeMap.TryGetValue(RttiType.Handle, FoundType) then
  begin
    if RttiType.TypeKind = tkEnumeration then
    begin
      FoundType := DefineEnumType(RttiType.AsOrdinal);
      FTypeMap.Add(RttiType.Handle, FoundType);
    end;
  end;

  if (FoundType <> nil) and not (FoundType is TXDataScalarType) then
    raise EModelBuilderException.CreateFmt(STypeShouldBeScalar, [FoundType.Name]);
  Result := TXDataScalarType(FoundType);
end;

function TXDataModelBuilder.FindOrDefineType(RttiType: TRttiType): TXDataType;
begin
  if RttiType = nil then Exit(nil);

  if RttiType.IsInstance then
  begin
    // special cases of classes
    if Bcl.Collections.IsValueList(RttiType.AsInstance.MetaclassType) then
    begin
      Result := FindOrDefineType(Bcl.Collections.ValueListType(RttiType.AsInstance.MetaclassType));
      if Result <> nil then
        Result := GetCollectionType(Result);
    end
    else
    if RttiType.AsInstance.MetaclassType.InheritsFrom(TStream) then
      Result := TXDataPrimitiveTypes.Instance.RawType
    else
      Result := DefineStructuredType(RttiType.AsInstance.MetaclassType);
  end
  else
    Result := FindOrDefineScalarType(RttiType);
end;

procedure TXDataModelBuilder.Finish;
begin
  if not FPrepared then
  begin
    FPrepared := True;
    FModel.Prepare;
  end;
end;

function TXDataModelBuilder.GetAuthorizationRequirements(
  RttiObj: TRttiObject): TArray<IAuthorizationRequirement>;
var
  A: TCustomAttribute;
  Len: Integer;
begin
  Len := 0;
  SetLength(Result, Len);
  for A in RttiObj.GetAttributes do
    if A is SecurityAttribute then
    begin
      SetLength(Result, Len + 1);
      Result[Len] := SecurityAttribute(A).GetRequirement;
      Inc(Len);
    end;
end;

function TXDataModelBuilder.GetCollectionType(AType: TXDataType): TXDataType;
begin
  if AType is TXDataScalarType then
    Result := TXDataScalarType(AType).Collection
  else
  if AType is TXDataEntityType then
    Result := TXDataEntityType(AType).Collection
  else
  if AType is TXDataInstanceType then
    Result := TXDataInstanceType(AType).Collection
  else
    Result := nil;
end;

procedure TXDataModelBuilder.ImportServiceContracts;
var
  RttiType: TRttiType;
  A: TCustomAttribute;
begin
  for RttiType in FRtti.GetTypes do
    if RttiType is TRttiInterfaceType then
      for A in RttiType.GetAttributes do
        if A is ServiceContractAttribute then
          if not Explorer.CustomSetup and TMappedClasses.BelongsToModel(RttiType, Explorer.ModelName) then
          begin
            AddServiceContract(TRttiInterfaceType(RttiType));
            break;
          end;
end;

function TXDataModelBuilder.InstanceTypeName(AClass: TClass): string;
begin
  Result := AClass.ClassName;
//  if not UseOriginalClassNames then
//  begin
//    if (Length(Result) > 1) and (Result[1] = 'T') then
//      Result := Copy(Result, 2, MaxInt)
//  end;
end;

class procedure TXDataModelBuilder.LoadXmlDoc(AModel: TXDataAureliusModel;
  const Folder: string);
var
  Builder: TXDataModelBuilder;
begin
  Builder := TXDataModelBuilder.Create(AModel);
  try
    Builder.LoadXmlDoc(Folder);
  finally
    Builder.Free;
  end;
end;

procedure TXDataModelBuilder.LoadXmlDoc(const Folder: string = '');
var
  Builder: TXmlDocBuilder;

  function TextPreference(const S1, S2, S3: string; const S4: string = ''): string;
  begin
    if S1 <> '' then
      Exit(S1);
    if S2 <> '' then
      Exit(S2);
    if S3 <> '' then
      Exit(S3);
    Result := S4;
  end;

  procedure LoadActionXmlDoc(Action: TXDataAction; XmlMethod: TXmlDocMethod);
  var
    Param: TXDataParamDef;
  begin
    if Model.GetActionInfo(Action).Method = nil then
      Exit;
    if XmlMethod = nil then
      Exit;

    Action.Description := TextPreference(
      Action.Description,
      XmlMethod.Notes.SwaggerContent(''),
      XmlMethod.Notes.Summary
    );
    Action.Remarks := TextPreference(
      Action.Remarks,
      XmlMethod.Notes.SwaggerContent('remarks'),
      XmlMethod.Notes.Remarks
    );

    for Param in Action.Parameters do
      Param.Description := TextPreference(
        Param.Description,
        XmlMethod.Notes.SwaggerContent('param-' + Param.Name),
        XmlMethod.Notes.ParamContent(Param.Name)
      );
  end;

  procedure LoadControllerXmlDoc(Controller: TXDataController);
  var
    TempType: TRttiType;
    IntfType: TRttiInterfaceType;
    XmlUnit: TXmlDocUnit;
    XmlIntf: TXmlDocInterface;
    Action: TXDataAction;
  begin
    TempType := FRtti.GetType(Model.GetControllerInfo(Controller).InterfaceType);
    if (TempType = nil) or not (TempType is TRttiInterfaceType) then
      Exit;
    IntfType := TRttiInterfaceType(TempType);
    XmlUnit := Builder.AddUnit(IntfType.DeclaringUnitName);
    XmlIntf := XmlUnit.InterfaceByName(IntfType.Name);
    if XmlIntf = nil then
      Exit;
    Controller.Description := TextPreference(
      Controller.Description,
      XmlIntf.Notes.SwaggerContent('tag-description'),
      XmlIntf.Notes.SwaggerContent(''),
      XmlIntf.Notes.Summary
    );
    Controller.TagName := TextPreference(
      Controller.TagName,
      XmlIntf.Notes.SwaggerContent('tag-name'),
      ''
    );

    for Action in Controller.Actions do
      LoadActionXmlDoc(Action, XmlIntf.MethodByName(Model.GetActionInfo(Action).Method.Name));
  end;

  function XmlClassFromStructuredType(StructuredType: TXDataStructuredType): TXmlDocClass;
  var
    TempType: TRttiType;
    Clazz: TClass;
    RttiType: TRttiInstanceType;
    XmlUnit: TXmlDocUnit;
  begin
    Result := nil;
    if StructuredType is TXDataEntityType then
      Clazz := Model.GetEntityTypeInfo(TXDataEntityType(StructuredType)).Clazz
    else
      Clazz := Model.GetInstanceTypeInfo(TXDataInstanceType(StructuredType)).Clazz;

    TempType := FRtti.GetType(Clazz);
    if (TempType = nil) or not (TempType is TRttiInstanceType) then
      Exit;
    RttiType := TRttiInstanceType(TempType);
    XmlUnit := Builder.AddUnit(RttiType.DeclaringUnitName);
    Result := XmlUnit.ClassByName(RttiType.Name);
  end;

  procedure LoadStructuredTypeXmlDoc(StructuredType: TXDataStructuredType);
  var
    Prop: TXDataScalarProperty;
    NavProp: TXDataStructuredProperty;
    XmlClass: TXmlDocClass;
    PropInfo: TXDataAureliusPropInfo;
    Member: TXmlDocMember;
    RttiInfo: TRttiOptimization;
    I: Integer;
  begin
    XmlClass := XmlClassFromStructuredType(StructuredType);
    if XmlClass = nil then
      Exit;

    StructuredType.Description := TextPreference(
      StructuredType.Description,
      XmlClass.Notes.SwaggerContent('definition'),
      XmlClass.Notes.SwaggerContent(''),
      XmlClass.Notes.Summary
    );

    for I := 0 to StructuredType.ScalarPropertyCount - 1 do
    begin
      Prop := StructuredType.GetScalarProperty(I);
      PropInfo := Model.GetScalarPropertyInfo(Prop);
      if (PropInfo = nil) or (PropInfo.RttiInfo = nil) then
        Continue;
      Member := XmlClass.MemberByName(PropInfo.RttiInfo.MemberName);
      if Member = nil then
        Continue;
      Prop.Description := TextPreference(
        Prop.Description,
        Member.Notes.SwaggerContent(''),
        Member.Notes.Summary
      );
    end;

    for I := 0 to StructuredType.StructuredPropertyCount - 1 do
    begin
      NavProp := StructuredType.GetStructuredProperty(I);
      RttiInfo := Model.GetStructuredPropInfo(NavProp).RttiInfo;
      if RttiInfo = nil then
        Continue;
      Member := XmlClass.MemberByName(RttiInfo.MemberName);
      if Member = nil then
        Continue;
      NavProp.Description := TextPreference(
        NavProp.Description,
        Member.Notes.SwaggerContent(''),
        Member.Notes.Summary
      )
    end;
  end;

  procedure LoadEntitySetXmlDoc(EntitySet: TXDataEntitySet);
  var
    XmlClass: TXmlDocClass;
  begin
    XmlClass := XmlClassFromStructuredType(EntitySet.EntityType);
    if XmlClass = nil then
      Exit;
    EntitySet.Description := TextPreference(
      EntitySet.Description,
      XmlClass.Notes.SwaggerContent('tag-description'),
      XmlClass.Notes.SwaggerContent(''),
      XmlClass.Notes.Summary
    );
    EntitySet.TagName := TextPreference(EntitySet.TagName,
      XmlClass.Notes.SwaggerContent('tag-name'),
      ''
    );
  end;

var
  Info: TXmlDocInfo;
  Controller: TXDataController;
  EntityType: TXDataEntityType;
  EntitySet: TXDataEntitySet;
  InstanceType: TXDataInstanceType;
begin
  Builder := nil;
  Info := TXmlDocInfo.Create;
  try
    Builder := TXmlDocBuilder.Create(Folder, Info);
    for Controller in FSchema.Controllers do
      LoadControllerXmlDoc(Controller);

    for EntityType in FSchema.EntityTypes do
      LoadStructuredTypeXmlDoc(EntityType);

    for InstanceType in FSchema.InstanceTypes do
      LoadStructuredTypeXmlDoc(InstanceType);

    for EntitySet in Model.DefaultEntityContainer.EntitySets do
      LoadEntitySetXmlDoc(EntitySet);
  finally
    Builder.Free;
    Info.Free
  end;
end;

function TXDataModelBuilder.PropertyName(const AMember: TRttiOptimization): string;
begin
  Result := AMember.MemberName;
  if (AMember.MemberKind = TMemberKind.Field) and not UseOriginalFieldNames and not AMember.IsDynamic then
  begin
    if (Length(Result) > 1) and (Result[1] = 'F') then
      Result := Copy(Result, 2, MaxInt)
  end;
end;

procedure TXDataModelBuilder.RemoveEntitySet(AClass: TClass);
var
  EntityType: TXDataEntityType;
  EntitySet: TXDataEntitySet;
  AureliusEntityType: TEntityType;
begin
  FPrepared := False;
  AureliusEntityType := Explorer.FindEntityTypeFromClass(AClass);
  if not FEntityTypeMap.TryGetValue(AureliusEntityType, EntityType) then Exit;
  EntitySet := FModel.DefaultEntitySet(EntityType);
  if EntitySet <> nil then
    FContainer.EntitySets.Remove(EntitySet);
end;

procedure TXDataModelBuilder.TryAddProperty(AType: TXDataEntityType;
  ARttiMember: TRttiMember);
var
  RttiInfo: TRttiOptimization;
begin
  if not (ARttiMember is TRttiField) and not (ARttiMember is TRttiProperty) then Exit;
  RttiInfo := Explorer.GetOptimization(ARttiMember);
  try
    if AType.FindProperty(PropertyName(RttiInfo)) <> nil then Exit;
    if RttiInfo.RealType.IsInstance then
      AddNavigationProperty(AType, RttiInfo)
    else
      AddSimpleProperty(AType, RttiInfo);
  finally
    RttiInfo.Free;
  end;
end;

function TXDataModelBuilder.EntitySetName(
  const AType: TXDataEntityType): string;
var
  Clazz: TClass;
  RttiType: TRttiType;
  A: URIPathSegmentAttribute;
begin
  Result := AType.Name;

  Clazz := Model.GetEntityTypeInfo(AType).Clazz;
  if Clazz = nil then Exit;
  
  RttiType := FRtti.GetType(Clazz);
  if RttiType = nil then Exit;

  if TRttiUtils.GetInstance.TryGetAttribute<URIPathSegmentAttribute>(RttiType, A, False) then
    Result := A.Path;
end;

function TXDataModelBuilder.EntityTypeName(AClass: TClass): string;
begin
  Result := AClass.ClassName;
  if not UseOriginalClassNames then
  begin
    if (Length(Result) > 1) and (Result[1] = 'T') then
      Result := Copy(Result, 2, MaxInt)
  end;
end;

{ EInvalidParamBinding }

constructor EInvalidParamBinding.Create(AParamDef: TXDataParamDef);
begin
  inherited CreateFmt('Cannot define action "%s": Invalid binding in param "%s" ',
    [AParamDef.Parent.Name, AParamDef.Name]);
end;

{ EInvalidStreamParam }

constructor EInvalidStreamParam.Create(AAction: TXDataAction);
begin
  Assert(Assigned(AAction));
  inherited CreateFmt('Cannot define action "%s": TStream param must be the only one set as FromBody',
    [AAction.Name]);
end;

{ EOutputParamsNotAllowed }

constructor EOutputParamsNotAllowed.Create(AAction: TXDataAction);
begin
  inherited CreateFmt('Output parameters not allowed in action "%s"', [AAction.Name]);
end;

end.
