unit XData.Model.Classes;

{$I XData.Inc}
{$IFDEF PAS2JS}
  {$modeswitch ignoreattributes}
{$ENDIF}

interface

uses
  Classes, SysUtils, Types,
  {$IFDEF ISDELPHI}
  Rtti,
  Bcl.TypInfo.Common,
  Bcl.Json.Attributes,
  {$ELSE}
//  JS,
  {$ENDIF}
  {$IFDEF GENERICS}
  Generics.Collections,
  {$ELSE}
  Contnrs,
  {$ENDIF}
  Bcl.Collections.Common,
  Bcl.Rtti.Common;

type
  TXDataVersion = (v1, v2);
  TXDataEntityType = class;

  TXTypeKind = (
    xtUndefined,
    xtText,
    xtInt32,
    xtInt64,
    xtDouble,
    xtBoolean,
    xtDateTime,
    xtDate,
    xtTime,
    xtCurrency,
    xtGuid,
    xtVariant,
    xtBinary,
    xtStream,
    xtInt16,
    xtByte,
    xtSByte,
    xtEntity,
    xtEntityCollection,
    xtEnum,
    xtScalarCollection
  );

  TXDataSchema = class;
  TXDataModel = class;
  TXDataTypeConverter = class;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataModelObject = class
  strict private
    [JsonIgnore]
    FData: TValue;
    [JsonIgnore]
    FOwnsDataObject: Boolean;
    FDescription: string;
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    [JsonIgnore]
    FParent: TXDataModelObject;
  protected
    property Parent: TXDataModelObject read FParent write FParent;
  public
    destructor Destroy; override;
    property Data: TValue read FData write FData;
    property OwnsDataObject: boolean read FOwnsDataObject write FOwnsDataObject;
    property Description: string read FDescription write FDescription;
  end;

  {$IFDEF GENERICS}
  {$ELSE}
  TXDataOwnedList = class(TObjectList)
  private
    [JsonIgnore]
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    FOwner: TXDataModelObject;
  protected
    {$IFDEF PAS2JS}
    procedure Notify(Ptr: JSValue; Action: TListNotification); override;
    {$ELSE}
    procedure Notify(Ptr: Pointer; Action: TListNotification); override;
    {$ENDIF}
  public
    constructor Create(AOwner: TXDataModelObject); overload;
    constructor Create; reintroduce; overload;
  end;
  {$ENDIF}

  TXDataType = class(TXDataModelObject)
  strict private
    FName: string;
  strict protected
    function GetName: string; virtual;
    function GetIsCollection: boolean; virtual;
    function GetWrappedType: TXDataType; virtual;
    function GetTypeKind: TXTypeKind; virtual;
  public
    function QualifiedName: string; virtual;
    property Name: string read GetName write FName;
    property IsCollection: boolean read GetIsCollection;
    property WrappedType: TXDataType read GetWrappedType;
    property TypeKind: TXTypeKind read GetTypeKind;
  end;

  TXDataScalarCollectionType = class;

  TXDataScalarType = class(TXDataType)
  strict private
    [JsonIgnore]
    FCollection: TXDataScalarCollectionType;
  strict protected
    function GetCollection: TXDataScalarType;
  public
    destructor Destroy; override;
    property Collection: TXDataScalarType read GetCollection;
  end;

  TXDataPrimitiveType = class(TXDataScalarType)
  end;

  TXDataEnumMember = class(TXDataModelObject)
  private
    FName: string;
    FValue: integer;
  public
    property Name: string read FName write FName;
    property Value: integer read FValue write FValue;
  end;

  {$IFDEF GENERICS}
  TXDataEnumMemberList = TList<TXDataEnumMember>;
  {$ELSE}
  TXDataEnumMemberList = class(TObjectList)
  private
    function GetItem(Index: Integer): TXDataEnumMember; reintroduce;
  public
    property Items[Index: Integer]: TXDataEnumMember read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataEnumType = class(TXDataScalarType)
  strict private
    FMembers: TXDataEnumMemberList;
    function GetSchema: TXDataSchema;
  strict protected
    function GetTypeKind: TXTypeKind; override;
  public
    function QualifiedName: string; override;
    property Schema: TXDataSchema read GetSchema;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function AddMember(const AName: string; AValue: integer): TXDataEnumMember;
    property Members: TXDataEnumMemberList read FMembers;
  end;

  {$IFDEF GENERICS}
  TXDataEnumTypeList = TList<TXDataEnumType>;
  {$ELSE}
  TXDataEnumTypeList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataEnumType; reintroduce;
  public
    property Items[Index: Integer]: TXDataEnumType read GetItem; default;
  end;
  {$ENDIF}

  TXDataStructuredType = class(TXDataType)
  end;

  TXDataScalarCollectionType = class(TXDataScalarType)
  strict private
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    [JsonIgnore]
    FItemType: TXDataScalarType;
  strict protected
    function GetIsCollection: boolean; override;
    function GetName: string; override;
    function GetWrappedType: TXDataType; override;
    function GetTypeKind: TXTypeKind; override;
  public
    function QualifiedName: string; override;
    property ItemType: TXDataScalarType read FItemType write FItemType;
  end;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataProperty = class(TXDataModelObject)
  strict private
    FName: string;
    FRequired: Boolean;
    function GetParent: TXDataEntityType;
  public
    function IsSimple: Boolean; virtual; abstract;
    property Parent: TXDataEntityType read GetParent;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function QualifiedName: string;
    property Name: string read FName write FName;
    property Required: Boolean read FRequired write FRequired;
  end;

  {$IFDEF GENERICS}
  TXDataPropertyList = TList<TXDataProperty>;
  {$ELSE}
  TXDataPropertyList = class(TObjectList)
  private
    function GetItem(Index: Integer): TXDataProperty; reintroduce;
  public
    property Items[Index: Integer]: TXDataProperty read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataSimpleProperty = class(TXDataProperty)
  strict private
    [JsonIgnore]
    FType: TXDataScalarType;
    FLength: Integer;
    FPrecision: Integer;
    FScale: Integer;
  private
    [JsonIgnore]
    FUrlConverter: TXDataTypeConverter;
  private
    [JsonIgnore]
    FTypeRef: string;
    function GetTypeRef: string;
  public
    [JsonProperty('Type')]
    property TypeRef: string read GetTypeRef write FTypeRef;
  private
    procedure Prepare;
  protected
    procedure SetType(const Value: TXDataScalarType);
    function GetTypeKind: TXTypeKind;
//    procedure SetTypeKind(const Value: TOTypeKind);
  public
    property UrlConverter: TXDataTypeConverter read FUrlConverter write FUrlConverter;
  public
    function IsSimple: Boolean; override;
    property PropertyType: TXDataScalarType read FType write SetType;
    property TypeKind: TXTypeKind read GetTypeKind; // write SetTypeKind;

    property Length: Integer read FLength write FLength;
    property Precision: Integer read FPrecision write FPrecision;
    property Scale: Integer read FScale write FScale;
  end;

  {$IFDEF GENERICS}
  TXDataSimplePropertyList = TList<TXDataSimpleProperty>;
  {$ELSE}
  TXDataSimplePropertyList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataSimpleProperty; reintroduce;
  public
    property Items[Index: Integer]: TXDataSimpleProperty read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataNavigationProperty = class(TXDataProperty)
  strict private
    [JsonIgnore]
    FTarget: TXDataEntityType;
  private
    [JsonIgnore]
    FTargetRef: string;
    function GetTargetRef: string;
  public
    [JsonProperty('Target')]
    property TargetRef: string read GetTargetRef write FTargetRef;
  private
    procedure Prepare;
  public
    function IsSimple: Boolean; override;
    property Target: TXDataEntityType read FTarget write FTarget;
  end;

  {$IFDEF GENERICS}
  TXDataNavigationPropertyList = TList<TXDataNavigationProperty>;
  {$ELSE}
  TXDataNavigationPropertyList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataNavigationProperty; reintroduce;
  public
    property Items[Index: Integer]: TXDataNavigationProperty read GetItem; default;
  end;
  {$ENDIF}

  TXDataEntityCollectionType = class;

  TXDataEntitySet = class;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataEntityType = class(TXDataStructuredType)
  strict private
    [JsonIgnore]
    FKey: TXDataPropertyList;
    [JsonIgnore]
    FBaseType: TXDataEntityType;
    FProperties: TXDataSimplePropertyList;
    FNavigationProperties: TXDataNavigationPropertyList;
    [JsonIgnore]
    FCollection: TXDataEntityCollectionType;
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    [JsonIgnore]
    FDefaultEntitySet: TXDataEntitySet;
    function FindDeclaredProperty(const AName: string): TXDataSimpleProperty;
    function FindDeclaredNavigationProperty(const AName: string): TXDataNavigationProperty;
    function GetSchema: TXDataSchema;
  private
    [JsonIgnore]
    FBaseTypeRef: string;
    [JsonIgnore]
    FKeyRef: TStringDynArray;
    function GetBaseTypeRef: string;
    function GetKeyRef: TStringDynArray;
  public
    [JsonProperty('BaseType')]
    property BaseTypeRef: string read GetBaseTypeRef write FBaseTypeRef;
    [JsonProperty('Key')]
    property KeyRef: TStringDynArray read GetKeyRef write FKeyRef;
  private
    procedure Prepare;
  strict protected
    function GetCollection: TXDataEntityCollectionType;
    function GetTypeKind: TXTypeKind; override;
    procedure SetSchema(Value: TXDataSchema);
  private
    property DefaultEntitySet: TXDataEntitySet read FDefaultEntitySet write FDefaultEntitySet;
  public
    function QualifiedName: string; override;
    function FindProperty(const AName: string): TXDataSimpleProperty;
    function FindNavigationProperty(const AName: string): TXDataNavigationProperty;
    function IsInheritedFrom(ABase: TXDataEntityType): boolean;
    property Schema: TXDataSchema read GetSchema;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    property BaseType: TXDataEntityType read FBaseType write FBaseType;
    property Key: TXDataPropertyList read FKey;
    property Properties: TXDataSimplePropertyList read FProperties;
    property NavigationProperties: TXDataNavigationPropertyList read FNavigationProperties;
    property Collection: TXDataEntityCollectionType read GetCollection;
  end;

  {$IFDEF GENERICS}
  TXDataEntityTypeList = TList<TXDataEntityType>;
  {$ELSE}
  TXDataEntityTypeList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataEntityType; reintroduce;
  public
    property Items[Index: Integer]: TXDataEntityType read GetItem; default;
  end;
  {$ENDIF}

  TXDataEntityCollectionType = class(TXDataEntityType)
  strict private
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    [JsonIgnore]
    FItemType: TXDataEntityType;
  strict protected
    function GetName: string; override;
    function GetIsCollection: boolean; override;
    function GetWrappedType: TXDataType; override;
    function GetTypeKind: TXTypeKind; override;
  public
    function QualifiedName: string; override;
    property ItemType: TXDataEntityType read FItemType write FItemType;
  end;

  TXDataEntityContainer = class;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataEntitySet = class(TXDataModelObject)
  strict private
    FName: string;
    [JsonIgnore]
    FEntityType: TXDataEntityType;
    function GetContainer: TXDataEntityContainer;
  private
    [JsonIgnore]
    FEntityTypeRef: string;
    function GetEntityTypeRef: string;
  public
    [JsonProperty('EntityType')]
    property EntityTypeRef: string read GetEntityTypeRef write FEntityTypeRef;
  private
    procedure Prepare;
  public
    property Name: string read FName write FName;
    property EntityType: TXDataEntityType read FEntityType write FEntityType;
    property Container: TXDataEntityContainer read GetContainer;
  end;

  {$IFDEF GENERICS}
  TXDataEntitySetList = TList<TXDataEntitySet>;
  {$ELSE}
  TXDataEntitySetList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataEntitySet; reintroduce;
  public
    property Items[Index: Integer]: TXDataEntitySet read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataEntityContainer = class(TXDataModelObject)
  strict private
    FName: string;
    FEntitySets: TXDataEntitySetList;
    function GetSchema: TXDataSchema;
  public
    property Schema: TXDataSchema read GetSchema;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function FindEntitySet(const AName: string): TXDataEntitySet;
    property Name: string read FName write FName;
    property EntitySets: TXDataEntitySetList read FEntitySets;
  end;

  {$IFDEF GENERICS}
  TXDataEntityContainerList = TList<TXDataEntityContainer>;
  {$ELSE}
  TXDataEntityContainerList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataEntityContainer; reintroduce;
  public
    property Items[Index: Integer]: TXDataEntityContainer read GetItem; default;
  end;
  {$ENDIF}

  TXDataAction = class;

  TBindingMode = (FromBody, FromURI);

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataParamDef = class(TXDataModelObject)
  strict private
    FName: string;
  private
    [JsonIgnore]
    FTypeRef: string;
    function GetTypeRef: string;
  public
    [JsonProperty('Type')]
    property TypeRef: string read GetTypeRef write FTypeRef;
  private
    procedure Prepare;
  private
    [JsonIgnore]
    FUrlConverter: TXDataTypeConverter;
    [JsonIgnore]
    FType: TXDataType;
    FBindingMode: TBindingMode;
    FOutput: Boolean;
    FInput: Boolean;
    [JsonIgnore]
    FDefaultValue: TValue;
    function GetParent: TXDataAction;
  {$IFDEF ISDELPHI}
  strict private
    [JsonIgnore]
    FRttiType: TRttiType;
  public
    property RttiType: TRttiType read FRttiType write FRttiType;
  {$ENDIF}
  public
    property Parent: TXDataAction read GetParent;
  public
    property UrlConverter: TXDataTypeConverter read FUrlConverter write FUrlConverter;
    property Name: string read FName write FName;
    property BindingMode: TBindingMode read FBindingMode write FBindingMode;
    property Output: Boolean read FOutput write FOutput;
    property Input: Boolean read FInput write FInput;
    property DefaultValue: TValue read FDefaultValue write FDefaultValue;
    property ParamType: TXDataType read FType write FType;
  end;

  {$IFDEF GENERICS}
  TXDataParamDefList = TList<TXDataParamDef>;
  {$ELSE}
  TXDataParamDefList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataParamDef; reintroduce;
  public
    property Items[Index: Integer]: TXDataParamDef read GetItem; default;
  end;
  {$ENDIF}

  EFromURIBindingNotSupported = class(Exception)
  public
    constructor Create(AParam: TXDataParamDef); reintroduce;
  end;

  TXDataController = class;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataAction = class(TXDataModelObject)
  strict private
    FName: string;
    FParameters: TXDataParamDefList;
    FHttpMethod: string;
    FIsParamStream: Boolean;
    FIsResultStream: Boolean;
    FIsResultCriteria: Boolean;
    FIsResultSingleObject: Boolean;
    FOperationId: string;
    {$IFDEF ISDELPHI}
    [JsonIgnore]
    FRttiMethod: TRttiMethod;
    procedure SetRttiMethod(ARttiMethod: TRttiMethod);
    {$ENDIF}
  private
    procedure Prepare;
  protected
    function GetController: TXDataController;
  public
    property Controller: TXDataController read GetController;
  public
    property IsParamStream: Boolean read FIsParamStream write FIsParamStream;
    property IsResultStream: Boolean read FIsResultStream write FIsResultStream;
    property IsResultCriteria: Boolean read FIsResultCriteria write FIsResultCriteria;
    property IsResultSingleObject: Boolean read FIsResultSingleObject write FIsResultSingleObject;
    property OperationId: string read FOperationId write FOperationId;
    function HasOutputParams: Boolean;
    {$IFDEF ISDELPHI}
    function ReturnType: TRttiType;
    property RttiMethod: TRttiMethod read FRttiMethod write SetRttiMethod;
    {$ENDIF}
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function FindParameter(const AName: string): TXDataParamDef;
    function IndexOfParameter(const AName: string): Integer;
    function HasBinding(ABindingMode: TBindingMode): boolean;
    property Name: string read FName write FName;
    property Parameters: TXDataParamDefList read FParameters;
    property HttpMethod: string read FHttpMethod write FHttpMethod;
  end;

  {$IFDEF GENERICS}
  TXDataActionList = TList<TXDataAction>;
  {$ELSE}
  TXDataActionList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataAction; reintroduce;
  public
    property Items[Index: Integer]: TXDataAction read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataController = class(TXDataModelObject)
  strict private
    FName: string;
    FActions: TXDataActionList;
    [JsonIgnore]
    FServiceClass: TClass;
    {$IFDEF ISDELPHI}
    [JsonIgnore]
    FInterfaceType: PTypeInfo;
    {$ENDIF}
    function GetSchema: TXDataSchema;
  public
    property Schema: TXDataSchema read GetSchema;
  public
    {$IFDEF ISDELPHI}
    function InterfaceTypeName: string;
    property InterfaceType: PTypeInfo read FInterfaceType write FInterfaceType;
    {$ENDIF}
    property ServiceClass: TClass read FServiceClass write FServiceClass;
  private
    procedure Prepare;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    procedure FindActions(const AName: string; AActions: TXDataActionList);
    {$IFDEF ISDELPHI}
    function FindActionFromRttiMethod(const AMethodName: string): TXDataAction;
    {$ENDIF}
    property Name: string read FName write FName;
    property Actions: TXDataActionList read FActions;
  end;

  {$IFDEF GENERICS}
  TXDataControllerList = TList<TXDataController>;
  {$ELSE}
  TXDataControllerList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataController; reintroduce;
  public
    property Items[Index: Integer]: TXDataController read GetItem; default;
  end;
  {$ENDIF}

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataSchema = class(TXDataModelObject)
  private
    FNamespace: string;
    FAlias: string;
    function GetModel: TXDataModel;
    function FindOrDefineConverter(AType: TXDataType): TXDataTypeConverter;
  strict private
    FEntityTypes: TXDataEntityTypeList;
    FEntityContainers: TXDataEntityContainerList;
    FEnumTypes: TXDataEnumTypeList;
    FControllers: TXDataControllerList;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function FindEntityType(const AName: string): TXDataEntityType;
    function FindEnumType(const AName: string): TXDataEnumType;
    function FindContainer(const AName: string): TXDataEntityContainer;
    function FindController(const AName: string): TXDataController;
  public
    property Namespace: string read FNamespace write FNamespace;
    property Alias: string read FAlias write FAlias;
    property EntityTypes: TXDataEntityTypeList read FEntityTypes;
    property EntityContainers: TXDataEntityContainerList read FEntityContainers;
    property EnumTypes: TXDataEnumTypeList read FEnumTypes;
    property Controllers: TXDataControllerList read FControllers;
    property Model: TXDataModel read GetModel;
  end;

  {$IFDEF GENERICS}
  TXDataSchemaList = TList<TXDataSchema>;
  {$ELSE}
  TXDataSchemaList = class(TXDataOwnedList)
  private
    function GetItem(Index: Integer): TXDataSchema; reintroduce;
  public
    property Items[Index: Integer]: TXDataSchema read GetItem; default;
  end;
  {$ENDIF}

  TXDataUrlConverters = class
  strict private
    FConverterMap: TStringMap;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function Find(const ATypeName: string): TXDataTypeConverter;
    procedure RegisterConverter(const ATypeName: string; AConverter: TXDataTypeConverter);
  end;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataBaseModel = class(TXDataModelObject)
  strict private
    FTitle: string;
    FVersion: string;
    FSchemas: TXDataSchemaList;
  private
    [JsonIgnore]
    FUrlConverters: TXDataUrlConverters;
  strict private
    function GetDefaultEntityContainer: TXDataEntityContainer;
  strict protected
    function FindRootType(const AType: TXDataEntityType): TXDataEntityType;
  public
    // This method must be called before using a model. It does the initialization of several
    // parts of the model, like solving references and settings converters
    procedure Prepare;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
    function FindSchema(const APrefix: string): TXDataSchema;
    property Schemas: TXDataSchemaList read FSchemas;
    function GetEntityType(const QualifiedName: string): TXDataEntityType;
    function FindEntityType(const QualifiedName: string): TXDataEntityType;
    function GetEntitySet(const QualifiedName: string): TXDataEntitySet;
    function FindEntitySet(const QualifiedName: string): TXDataEntitySet;
    function FindController(const AName: string): TXDataController;
    function FindActionByOperationId(const AOperationId: string): TXDataAction;
    function DefaultEntitySet(AType: TXDataEntityType): TXDataEntitySet;
    procedure DefineDefaultEntitySet(AType: TXDataEntityType; EntitySet: TXDataEntitySet);
    property DefaultEntityContainer: TXDataEntityContainer read GetDefaultEntityContainer;
    property Title: string read FTitle write FTitle;
    property Version: string read FVersion write FVersion;
  end;

  TXDataSimplePropertyArray = array of TXDataSimpleProperty;
  TXDataGetPropValueProc = reference to function(Entity: TValue; Prop: TXDataSimpleProperty): TValue;
  TXDataGetNavPropValueProc = reference to function(Entity: TValue; NavProp: TXDataNavigationProperty): TValue;

  [JsonInclude(TInclusionMode.NonDefault)]
  TXDataModel = class(TXDataBaseModel)
  strict protected
    ///	<summary>
    ///	  Returns the key predicate of an object instance, to be used in Url.
    ///	</summary>
    ///	<param name="AObject">
    ///	  The object instance for which the key predicate will be generated
    ///	</param>
    ///	<param name="AType">
    ///	  The entity type associated with the object. Just for optimization purposes
    ///	</param>
    ///	<returns>
    ///	  The key predicate of the object
    ///	</returns>
    ///	<remarks>
    ///	  This function returns a string containing the key predicate of an
    ///	  object instance. The key predicate is a string representation of the
    ///	  object id, to be used in an Url to identify a web resource relate to
    ///	  the object. For example, in the url
    ///	  <see href="http://localhost/Customers('ABC'" />), the key predicate
    ///	  is 'ABC' (with the quotes).
    ///	</remarks>
    function IdToKeyPredicate(const Entity: TValue; AType: TXDataEntityType;
      GetPropValue: TXDataGetPropValueProc; GetNavPropValue: TXDataGetNavPropValueProc): string; overload;
    /// <summary>
    ///   Returns the key predicate for an entity of the specified class, given a specified value
    /// </summary>
    ///	<remarks>
    ///	  This function returns a string containing the key predicate of a class given the specified id.
    ///	  The key predicate is a string representation of the
    ///	  object id, to be used in an Url to identify a web resource relate to
    ///	  the object. For example, in the url
    ///	  <see href="http://localhost/Customers('ABC'" />), the key predicate
    ///	  is 'ABC' (with the quotes).
    ///   If the class has multiple keys (composite key), TValue must be an array of TValue elements
    ///   with the same number of elements as the entity key properties
    ///	</remarks>
    function IdToKeyPredicate(AType: TXDataEntityType; const Id: TValue): string; overload;
  public
    function GetKeyScalarProperties(AType: TXDataEntityType): TXDataSimplePropertyArray;
    {$REGION 'Documentation'}
    ///	<summary>
    ///	  Retrieves the canonical relative Url for an object instance.
    ///	</summary>
    ///	<param name="AObject">
    ///	  The object instance which the canonical Url will be generated for
    ///	</param>
    ///	<param name="AType">
    ///	  The TXDataEntityType object which relates to the AObject instance.
    ///   It's used only for optimization and can be nil. If AType doesn't match
    ///   the exact type associated with the object, the function will find
    ///   the correct type. If type matches, a search is avoided for performance reasons
    ///	</param>
    ///	<returns>
    ///	  The canonical Url, relative to the server base path, without trailing
    ///	  slash
    ///	</returns>
    ///	<remarks>
    ///	  Use this method to build the Url to the resource that represents the
    ///	  object instance. The canonical Url returned is relative to the server
    ///	  base path.
    ///	</remarks>
    ///	<example>
    ///	  <para>
    ///	    CanonicalUrl := Model.CanonicalUrl(Customer, CustomerType);
    ///	  </para>
    ///	  <para>
    ///	    // returns "Customers(15)" if Customer.Id is equal to 15
    ///	  </para>
    ///	</example>
    {$ENDREGION}
    function CanonicalUrlFromId(EntityType: TXDataEntityType; Id: TValue): string; overload;
    function CanonicalUrlFromEntity(EntityType: TXDataEntityType; const Entity: TValue;
      GetPropValue: TXDataGetPropValueProc; GetNavPropValue: TXDataGetNavPropValueProc): string;
    function EntitySetUrl(const AEntitySet: TXDataEntitySet): string;
    function DefaultSchema: TXDataSchema;
  end;

  ECannotSetTypeKind = class(Exception)
  public
    constructor Create(Prop: TXDataProperty; Value: TXTypeKind); reintroduce;
  end;

  TXDataTypeConverter = class
  strict protected
    function GetXDataType: TXDataScalarType; virtual; abstract;
    function TryValueToUrl(const Value: TValue; out Literal: string; Prop: TXDataSimpleProperty): boolean; virtual;
    function TryUrlToValue(const Literal: string; out Value: TValue; Prop: TXDataSimpleProperty): boolean; virtual;
  public
    function ValueToUrl(const Value: TValue; Prop: TXDataSimpleProperty): string;
    function UrlToValue(const Literal: string; Prop: TXDataSimpleProperty): TValue;
    property XDataType: TXDataScalarType read GetXDataType;
  end;

  EConvertToUrlError = class(Exception)
  public
    constructor Create(Prop: TXDataSimpleProperty; Value: TValue); reintroduce;
  end;
  EConvertFromUrlError = class(Exception)
  public
    constructor Create(const Prop: TXDataSimpleProperty; const Literal: string); reintroduce;
  end;

  EModelTypeNotFound = class(Exception);

  EModelReferenceNotFound = class(Exception);

  EXDataEntityTypeNotFound = class(Exception)
  public
    constructor Create(const AName: string); reintroduce;
  end;

  EXDataEntitySetNotFound = class(Exception)
  public
    constructor Create(const AName: string); reintroduce;
  end;

  ECannotBuildKeyPredicateWrongValueLength = class(Exception)
  public
    constructor Create(AType: TXDataEntityType); reintroduce;
  end;

  ECannotBuildCanonicalUrl = class(Exception)
  public
    constructor Create(AType: TXDataEntityType); reintroduce;
  end;

  EUrlConverterNotFound = class(Exception)
  public
    constructor Create(AType: TXDataType); reintroduce;
  end;

implementation

uses
  XData.Model.Types,
  XData.Types.Converters,
  XData.Utils;

{ TXDataTypeConverter }

function TXDataTypeConverter.UrlToValue(const Literal: string;
  Prop: TXDataSimpleProperty): TValue;
begin
  if not TryUrlToValue(Literal, Result, Prop) then
    raise EConvertFromUrlError.Create(Prop, Literal);
end;

function TXDataTypeConverter.TryUrlToValue(const Literal: string;
  out Value: TValue; Prop: TXDataSimpleProperty): boolean;
begin
  Result := False;
end;

function TXDataTypeConverter.TryValueToUrl(const Value: TValue;
  out Literal: string; Prop: TXDataSimpleProperty): boolean;
begin
  Result := false;
end;

function TXDataTypeConverter.ValueToUrl(const Value: TValue;
  Prop: TXDataSimpleProperty): string;
begin
  {$IFDEF ISDELPHI}
  if Value.IsEmpty then
    Exit('null')
  else
  {$ELSE}
  if Value = nil then
    Exit('null')
  else
  {$ENDIF}
  begin
    if not TryValueToUrl(Value, Result, Prop) then
      raise EConvertToUrlError.Create(Prop, Value);
  end;
end;

{ EConvertToUrlError }

constructor EConvertToUrlError.Create(
  Prop: TXDataSimpleProperty; Value: TValue);
begin
  inherited CreateFmt('Cannot serialize value "%s" to Url format',
    {$IFDEF ISDELPHI}
    [Value.ToString]
    {$ELSE}
    [TValue_ToString(Value)]
    {$ENDIF}
  );
end;

{ EConvertFromUrlError }

constructor EConvertFromUrlError.Create(
  const Prop: TXDataSimpleProperty; const Literal: string);
begin
  inherited CreateFmt('Cannot deserialize value "%s" from Url format',
    [Literal]);
end;

{ TOwnedList }

{$IFDEF GENERICS}
type
  TOwnedList<T: TXDataModelObject; TOwner: TXDataModelObject> = class(TObjectList<T>)
  strict private
    {$IFDEF AUTOREFCOUNT}[Weak]{$ENDIF}
    FOwner: {$IFDEF DELPHIXE6_LVL}TOwner{$ELSE}TObject{$ENDIF};
  protected
    procedure Notify(const Value: T; Action: TCollectionNotification); override;
  public
    constructor Create(AOwner: TOwner);
  end;

constructor TOwnedList<T, TOwner>.Create(AOwner: TOwner);
begin
  inherited Create(true);
  FOwner := AOwner;
end;

procedure TOwnedList<T, TOwner>.Notify(const Value: T;
  Action: TCollectionNotification);
begin
  if Action = cnAdded then
    {$IFDEF DELPHIXE6_LVL}
    Value.Parent := FOwner
    {$ELSE}
    Value.Parent := TXDataModelObject(FOwner)
    {$ENDIF}
  else
    Value.Parent := nil;
  inherited;
end;
{$ENDIF}

{ TXDataSchema }

constructor TXDataSchema.Create;
begin
  inherited;
  {$IFDEF GENERICS}
  FEntityTypes := TOwnedList<TXDataEntityType, TXDataSchema>.Create(Self);
  FEnumTypes := TOwnedList<TXDataEnumType, TXDataSchema>.Create(Self);
  FEntityContainers := TOwnedList<TXDataEntityContainer, TXDataSchema>.Create(Self);
  FControllers := TOwnedList<TXDataController, TXDataSchema>.Create(Self);
  {$ELSE}
  FEntityTypes := TXDataEntityTypeList.Create(Self);
  FEnumTypes := TXDataEnumTypeList.Create(Self);
  FEntityContainers := TXDataEntityContainerList.Create(Self);
  FControllers := TXDataControllerList.Create(Self);
  {$ENDIF}
end;

destructor TXDataSchema.Destroy;
begin
  FEntityTypes.Free;
  FEnumTypes.Free;
  FEntityContainers.Free;
  FControllers.Free;
  inherited;
end;

function TXDataSchema.FindController(const AName: string): TXDataController;
var
  Controller: TXDataController;
  I: Integer;
begin
  for I := 0 to Controllers.Count - 1 do
  begin
    Controller := Controllers[I];
    if SameText(Controller.Name, AName) then
      Exit(Controller);
  end;
  Result := nil;
end;

function TXDataSchema.FindContainer(const AName: string): TXDataEntityContainer;
var
  Container: TXDataEntityContainer;
  I: Integer;
begin
  for I := 0 to EntityContainers.Count - 1 do
  begin
    Container := EntityContainers[I];
    if SameText(Container.Name, AName) then
      Exit(Container);
  end;
  Result := nil;
end;

function TXDataSchema.FindEntityType(const AName: string): TXDataEntityType;
var
  EntityType: TXDataEntityType;
  Wrapper, SubType: string;
  I: Integer;
begin
  // Check if it's a collection type
  if TXDataUtils.GetNameAndKey(AName, Wrapper, SubType) and
    (Wrapper = 'Collection') then
  begin
    EntityType := FindEntityType(SubType);
    Result := EntityType.Collection;
  end
  else
  begin
    for I := 0 to EntityTypes.Count - 1 do
    begin
      EntityType := EntityTypes[I];
      if SameText(EntityType.Name, AName) then
        Exit(EntityType);
    end;

    Result := nil;
  end;
end;

function TXDataSchema.FindEnumType(const AName: string): TXDataEnumType;
var
  EnumType: TXDataEnumType;
  I: Integer;
begin
  for I := 0 to EnumTypes.Count - 1 do
  begin
    EnumType := EnumTypes[I];
    if SameText(EnumType.Name, AName) then
      Exit(EnumType);
  end;
  Result := nil;
end;

function TXDataSchema.FindOrDefineConverter(
  AType: TXDataType): TXDataTypeConverter;
begin
  if AType = nil then Exit(nil);
  
  Result := Model.FUrlConverters.Find(AType.Name);
  if Result = nil then
  begin
    if AType is TXDataEnumType then
    begin
      Result := TXDataEnumConverter.Create(TXDataEnumType(AType));
      Model.FUrlConverters.RegisterConverter(AType.Name, Result);
    end;
  end;
end;

function TXDataSchema.GetModel: TXDataModel;
begin
  Result := TXDataModel(inherited Parent);
end;

{ TXDataProperty }

constructor TXDataProperty.Create;
begin
  inherited;
end;

destructor TXDataProperty.Destroy;
begin
  inherited;
end;

function TXDataProperty.GetParent: TXDataEntityType;
begin
  Result := TXDataEntityType(inherited Parent);
end;

function TXDataProperty.QualifiedName: string;
begin
  if Parent <> nil then
    Result := Parent.QualifiedName + '.' + Self.Name
  else
    Result := Self.Name;
end;

{ TXDataEntityType }

constructor TXDataEntityType.Create;
begin
  inherited;
  {$IFDEF GENERICS}
  FKey := TList<TXDataProperty>.Create;
  FProperties := TOwnedList<TXDataSimpleProperty, TXDataEntityType>.Create(Self);
  FNavigationProperties := TOwnedList<TXDataNavigationProperty, TXDataEntityType>.Create(Self);
  {$ELSE}
  FKey := TXDataPropertyList.Create(False);
  FProperties := TXDataSimplePropertyList.Create(Self);
  FNavigationProperties := TXDataNavigationPropertyList.Create(Self);
  {$ENDIF}
end;

destructor TXDataEntityType.Destroy;
begin
  FKey.Free;
  FNavigationProperties.Free;
  FProperties.Free;
  FCollection.Free;
  inherited;
end;

function TXDataEntityType.FindDeclaredNavigationProperty(
  const AName: string): TXDataNavigationProperty;
var
  Prop: TXDataNavigationProperty;
  I: Integer;
begin
  for I := 0 to NavigationProperties.Count - 1 do
  begin
    Prop := NavigationProperties[I];
    if SameText(AName, Prop.Name) then
      Exit(Prop);
  end;
  Result := nil;
end;

function TXDataEntityType.FindDeclaredProperty(
  const AName: string): TXDataSimpleProperty;
var
  Prop: TXDataSimpleProperty;
  I: Integer;
begin
  for I := 0 to Properties.Count - 1 do
  begin
    Prop := Properties[I];
    if SameText(AName, Prop.Name) then
      Exit(Prop);
  end;
  Result := nil;
end;

function TXDataEntityType.FindNavigationProperty(const AName: string): TXDataNavigationProperty;
begin
  Result := FindDeclaredNavigationProperty(AName);
  if (Result = nil) and (BaseType <> nil) then
    Result := BaseType.FindNavigationProperty(AName);
end;

function TXDataEntityType.FindProperty(const AName: string): TXDataSimpleProperty;
begin
  Result := FindDeclaredProperty(AName);
  if (Result = nil) and (BaseType <> nil) then
    Result := BaseType.FindProperty(AName);
end;

function TXDataEntityType.GetBaseTypeRef: string;
begin
  if FBaseType <> nil then
    Result := FBaseType.Name
  else
    Result := '';
end;

function TXDataEntityType.GetCollection: TXDataEntityCollectionType;
begin
  if FCollection = nil then
  begin
    FCollection := TXDataEntityCollectionType.Create;
    FCollection.ItemType := Self;
    FCollection.SetSchema(Self.Schema);
  end;
  Result := FCollection;
end;

function TXDataEntityType.GetKeyRef: TStringDynArray;
var
  I: Integer;
begin
  SetLength(Result, Key.Count);
  for I := 0 to Key.Count - 1 do
    Result[I] := Key[I].Name;
end;

function TXDataEntityType.GetSchema: TXDataSchema;
begin
  Result := TXDataSchema(inherited Parent);
end;

//function TXDataEntityType.GetOpenType: boolean;
//begin
//  if (BaseType <> nil) then
//    Result := BaseType.OpenType
//  else
//    Result := FOpenType;
//end;

function TXDataEntityType.GetTypeKind: TXTypeKind;
begin
  Result := TXTypeKind.xtEntity;
end;

function TXDataEntityType.IsInheritedFrom(ABase: TXDataEntityType): boolean;
begin
  if ABase = Self then
    Result := true
  else
    Result := (BaseType <> nil) and BaseType.IsInheritedFrom(ABase);
end;

function TXDataEntityType.QualifiedName: string;
begin
  Result := Self.Name;
  if Self.Schema <> nil then
  begin
    if Self.Schema.Alias <> '' then
      Result := Self.Schema.Alias + '.' + Self.Name
    else
    if Self.Schema.Namespace <> '' then
      Result := Self.Schema.Namespace + '.' + Self.Name;
  end;
end;

procedure TXDataEntityType.SetSchema(Value: TXDataSchema);
begin
  inherited Parent := Value;
  if FCollection <> nil then
    FCollection.SetSchema(Value);
end;

procedure TXDataEntityType.Prepare;
var
  KeyItem: string;
  TempProp: TXDataProperty;
  I: Integer;
begin
  if FBaseTypeRef <> '' then
  begin
    BaseType := Schema.FindEntityType(FBaseTypeRef);
    if BaseType = nil then
      raise EModelReferenceNotFound.CreateFmt('BaseType %s not found for entity type %s', [FBaseTypeRef, Name]);
    FBaseTypeRef := '';
  end;
  if BaseType <> nil then
    BaseType.Prepare;

  if Length(FKeyRef) > 0 then
  begin
    Key.Clear;
    for I := 0 to Length(FKeyRef) - 1 do
    begin
      KeyItem := FKeyRef[I];
      TempProp := FindProperty(KeyItem);
      if TempProp = nil then
        TempProp := FindNavigationProperty(KeyItem);
      if TempProp = nil then
        raise EModelReferenceNotFound.CreateFmt('Key property %s not found on entity type %s', [KeyItem, Name]);
      Key.Add(TempProp);
    end;
    SetLength(FKeyRef, 0);
  end;

  for I := 0 to Properties.Count - 1 do
    Properties[I].Prepare;
  for I := 0 to NavigationProperties.Count - 1 do
    NavigationProperties[I].Prepare;
end;

{ TXDataSimpleProperty }

function TXDataSimpleProperty.GetTypeKind: TXTypeKind;
begin
  if PropertyType = nil then
    Result := TXTypeKind.xtUndefined
  else
    Result := PropertyType.TypeKind;
end;

function TXDataSimpleProperty.GetTypeRef: string;
begin
  if FType <> nil then
    Result := FType.Name
  else
    Result := '';
end;

function TXDataSimpleProperty.IsSimple: Boolean;
begin
  Result := True;
end;

procedure TXDataSimpleProperty.SetType(const Value: TXDataScalarType);
begin
  // add a restriction here, property cannot be entity type
  // (but can be complex type so we can't restrict by class)

  // Also, type must be in scope (same namespace as property). I.e, property can be
  // either a scalar type, a collection or a complex type in scope
  FType := Value;
end;

procedure TXDataSimpleProperty.Prepare;
begin
  // Resolve type reference
  if FTypeRef <> '' then
  begin
    PropertyType := Parent.Schema.FindEnumType(FTypeRef);
    if PropertyType = nil then
      PropertyType := TXDataPrimitiveTypes.Instance.Find(FTypeRef);
    if PropertyType = nil then
      raise EModelReferenceNotFound.CreateFmt('Property type %s not found for property %s.%s',
        [FTypeRef, Parent.Name, Name]);
    FTypeRef := '';
  end;

  // Set converter
  UrlConverter := Parent.Schema.FindOrDefineConverter(PropertyType);
  if UrlConverter = nil then
    raise EUrlConverterNotFound.Create(PropertyType);
end;

//procedure TXDataSimpleProperty.SetTypeKind(const Value: TOTypeKind);
//begin
//  raise ECannotSetTypeKind.Create(Self, Value);
//end;

{ TXDataEntityContainer }

constructor TXDataEntityContainer.Create;
begin
  inherited;
  {$IFDEF GENERICS}
  FEntitySets := TOwnedList<TXDataEntitySet, TXDataEntityContainer>.Create(Self);
  {$ELSE}
  FEntitySets := TXDataEntitySetList.Create(Self);
  {$ENDIF}
end;

destructor TXDataEntityContainer.Destroy;
begin
  FEntitySets.Free;
  inherited;
end;

function TXDataEntityContainer.FindEntitySet(
  const AName: string): TXDataEntitySet;
var
  EntitySet: TXDataEntitySet;
  I: Integer;
begin
  for I := 0 to EntitySets.Count - 1 do
  begin
    EntitySet := EntitySets[I];
    if SameText(EntitySet.Name, AName) then
      Exit(EntitySet);
  end;
  Result := nil;
end;

function TXDataEntityContainer.GetSchema: TXDataSchema;
begin
  Result := TXDataSchema(inherited Parent);
end;

{ TXDataEnumType }

function TXDataEnumType.AddMember(const AName: string; AValue: integer): TXDataEnumMember;
begin
  Result := TXDataEnumMember.Create;
  Members.Add(Result);
  Result.Name := AName;
  Result.Value := AValue;
end;

constructor TXDataEnumType.Create;
begin
  {$IFDEF GENERICS}
  FMembers := TObjectList<TXDataEnumMember>.Create(True);
  {$ELSE}
  FMembers := TXDataEnumMemberList.Create;
  {$ENDIF}
end;

destructor TXDataEnumType.Destroy;
begin
  FMembers.Free;
  inherited;
end;

function TXDataEnumType.GetSchema: TXDataSchema;
begin
  Result := TXDataSchema(inherited Parent);
end;

function TXDataEnumType.GetTypeKind: TXTypeKind;
begin
  Result := TXTypeKind.xtEnum;
end;

function TXDataEnumType.QualifiedName: string;
begin
  Result := Self.Name;
  if Self.Schema <> nil then
  begin
    if Self.Schema.Alias <> '' then
      Result := Self.Schema.Alias + '.' + Self.Name
    else
    if Self.Schema.Namespace <> '' then
      Result := Self.Schema.Namespace + '.' + Self.Name;
  end;
end;

{ TXDataModel }

constructor TXDataBaseModel.Create;
begin
  FTitle := 'Server API';
  FVersion := '1.0';
  {$IFDEF GENERICS}
  FSchemas := TOwnedList<TXDataSchema, TXDataBaseModel>.Create(Self);
  {$ELSE}
  FSchemas := TXDataSchemaList.Create(Self);
  {$ENDIF}
  FUrlConverters := TXDataUrlConverters.Create;
end;

function TXDataBaseModel.DefaultEntitySet(AType: TXDataEntityType): TXDataEntitySet;
var
  Schema: TXDataSchema;
  EntitySet: TXDataEntitySet;
  Container: TXDataEntityContainer;
  SchemaIndex, ContainerIndex, EntitySetIndex: Integer;
begin
  if AType.DefaultEntitySet <> nil then
    Exit(AType.DefaultEntitySet);

  for SchemaIndex := 0 to Schemas.Count - 1 do
  begin
    Schema := Schemas[SchemaIndex];
    for ContainerIndex := 0 to Schema.EntityContainers.Count - 1 do
    begin
      Container := Schema.EntityContainers[ContainerIndex];
      for EntitySetIndex := 0 to Container.EntitySets.Count - 1 do
      begin
        EntitySet := Container.EntitySets[EntitySetIndex];
        if EntitySet.EntityType = AType then
        begin
          DefineDefaultEntitySet(AType, EntitySet);
          Exit(EntitySet);
        end;
      end;
    end;
  end;
  Result := nil;
end;

procedure TXDataBaseModel.DefineDefaultEntitySet(AType: TXDataEntityType;
  EntitySet: TXDataEntitySet);
begin
  AType.DefaultEntitySet := EntitySet;
end;

destructor TXDataBaseModel.Destroy;
begin
  FSchemas.Free;
  FUrlConverters.Free;
  inherited;
end;

function TXDataBaseModel.FindActionByOperationId(
  const AOperationId: string): TXDataAction;
var
  Schema: TXDataSchema;
  Controller: TXDataController;
  Action: TXDataAction;
  I, J, L: Integer;
begin
  Result := nil;
  for I := 0 to Schemas.Count - 1 do
  begin
    Schema := Schemas[I];
    for J := 0 to Schema.Controllers.Count - 1 do
    begin
      Controller := Schema.Controllers[J];
      for L := 0 to Controller.Actions.Count - 1 do
      begin
        Action := Controller.Actions[L];

        if SameText(Action.OperationId, AOperationId) then
          Exit(Action);
      end;
    end;
  end;
end;

function TXDataBaseModel.FindController(const AName: string): TXDataController;
var
  Schema: TXDataSchema;
  I: Integer;
begin
  Result := nil;
  for I := 0 to Schemas.Count - 1 do
  begin
    Schema := Schemas[I];
    Result := Schema.FindController(AName);
    if Result <> nil then
      break;
  end;
end;

function TXDataBaseModel.FindEntitySet(const QualifiedName: string): TXDataEntitySet;
var
  Prefix, LocalName: string;
  Schema: TXDataSchema;
  Container: TXDataEntityContainer;
  I: Integer;
begin
  Result := nil;
  TXDataUtils.GetQualifiedName(QualifiedName, Prefix, LocalName);
  Container := nil;
  if Prefix = '' then
  begin
    Container := DefaultEntityContainer;
  end else
  begin
    for I := 0 to Schemas.Count - 1 do
    begin
      Schema := Schemas[I];
      Container := Schema.FindContainer(Prefix);
      if Container <> nil then
        break;
    end;
  end;
  if Container <> nil then
    Result := Container.FindEntitySet(LocalName);
end;

function TXDataBaseModel.FindEntityType(
  const QualifiedName: string): TXDataEntityType;
var
  Prefix, LocalName: string;
  Schema: TXDataSchema;
begin
  Result := nil;
  TXDataUtils.GetQualifiedName(QualifiedName, Prefix, LocalName);
  Schema := FindSchema(Prefix);
  if Schema <> nil then
    Result := Schema.FindEntityType(LocalName);
end;

function TXDataBaseModel.FindRootType(
  const AType: TXDataEntityType): TXDataEntityType;
begin
  Result := AType;
  while Result.BaseType <> nil do
    Result := Result.BaseType;
end;

function TXDataBaseModel.FindSchema(const APrefix: string): TXDataSchema;
var
  LocalSchema: TXDataSchema;
  I: Integer;
begin
  for I := 0 to Schemas.Count - 1 do
  begin
    LocalSchema := Schemas[I];
    if SameText(APrefix, LocalSchema.Namespace) or SameText(APrefix, LocalSchema.Alias) then
      Exit(LocalSchema);
  end;
  Result := nil;
end;

function TXDataBaseModel.GetDefaultEntityContainer: TXDataEntityContainer;
begin
  if (Schemas.Count > 0) and (Schemas[0].EntityContainers.Count > 0) then
    Result := Schemas[0].EntityContainers[0]
  else
    Result := nil;
end;

function TXDataBaseModel.GetEntitySet(
  const QualifiedName: string): TXDataEntitySet;
begin
  Result := FindEntitySet(QualifiedName);
  if Result = nil then
    raise EXDataEntitySetNotFound.Create(QualifiedName);
end;

function TXDataBaseModel.GetEntityType(
  const QualifiedName: string): TXDataEntityType;
begin
  Result := FindEntityType(QualifiedName);
  if Result = nil then
    raise EXDataEntityTypeNotFound.Create(QualifiedName);
end;

procedure TXDataBaseModel.Prepare;
var
  Schema: TXDataSchema;
  Container: TXDataEntityContainer;
  EntitySet: TXDataEntitySet;
  EntityType: TXDataEntityType;
  SchemaIndex: Integer;
  ContainerIndex: Integer;
  EntitySetIndex: Integer;
  EntityTypeIndex: Integer;
  ControllerIndex: Integer;
  Controller: TXDataController;
begin
  for SchemaIndex := 0 to Schemas.Count - 1 do
  begin
    Schema := Schemas[SchemaIndex];

    for ContainerIndex := 0 to Schema.EntityContainers.Count - 1 do
    begin
      Container := Schema.EntityContainers[ContainerIndex];
      for EntitySetIndex := 0 to Container.EntitySets.Count - 1 do
      begin
        EntitySet := Container.EntitySets[EntitySetIndex];
        EntitySet.Prepare;
      end;
    end;

    for EntityTypeIndex := 0 to Schema.EntityTypes.Count - 1 do
    begin
      EntityType := Schema.EntityTypes[EntityTypeIndex];
      EntityType.Prepare;
    end;

    for ControllerIndex := 0 to Schema.Controllers.Count - 1 do
    begin
      Controller := Schema.Controllers[ControllerIndex];
      Controller.Prepare;
    end;
  end;
end;

{ TXDataModel }

type
  TValueArray = array of TValue;

function TXDataModel.IdToKeyPredicate(const Entity: TValue; AType: TXDataEntityType;
  GetPropValue: TXDataGetPropValueProc; GetNavPropValue: TXDataGetNavPropValueProc): string;

  procedure FillKeyValues(Entity: TValue; AType: TXDataEntityType; var Values: TValueArray);
  var
    Key: TXDataProperty;
    I: Integer;
  begin
    AType := FindRootType(AType);
    for I := 0 to AType.Key.Count - 1 do
    begin
      Key := AType.Key[I];
      if Key.IsSimple then
      begin
        SetLength(Values, Length(Values) + 1);
        Values[Length(Values) - 1] := GetPropValue(Entity, TXDataSimpleProperty(Key));
      end
      else
        FillKeyValues(
          GetNavPropValue(Entity, TXDataNavigationProperty(Key)),
          TXDataNavigationProperty(Key).Target,
          Values
        );
    end;
  end;

var
  ValueList: TValueArray;
  SimpleKey: TXDataSimpleProperty;
begin
  AType := FindRootType(AType);
  if AType.Key.Count = 0 then
    Exit('');

  if (AType.Key.Count = 1) and AType.Key[0].IsSimple then
  begin
    SimpleKey := TXDataSimpleProperty(AType.Key[0]);
    Result := SimpleKey.UrlConverter.ValueToUrl(
      GetPropValue(Entity, SimpleKey),
      nil
      )
  end
  else
  begin
    SetLength(ValueList, 0);
    FillKeyValues(Entity, AType, ValueList);
    {$IFDEF PAS2JS}
    Result := IdToKeyPredicate(AType, ValueList);
    {$ELSE}
    Result := IdToKeyPredicate(AType, TValue.From<TValueArray>(ValueList));
    {$ENDIF}
  end;
end;

function TXDataModel.CanonicalUrlFromEntity(EntityType: TXDataEntityType;
  const Entity: TValue; GetPropValue: TXDataGetPropValueProc;
  GetNavPropValue: TXDataGetNavPropValueProc): string;
var
  EntitySet: TXDataEntitySet;
begin
  EntitySet := DefaultEntitySet(EntityType);
  if EntitySet = nil then
    raise ECannotBuildCanonicalUrl.Create(EntityType);
  Result := Format('%s(%s)', [
    EntitySetUrl(EntitySet),
    IdToKeyPredicate(Entity, EntityType, GetPropValue, GetNavPropValue)
  ]);
end;

function TXDataModel.CanonicalUrlFromId(EntityType: TXDataEntityType; Id: TValue): string;
var
  EntitySet: TXDataEntitySet;
begin
  EntitySet := DefaultEntitySet(EntityType);
  if EntitySet = nil then
    raise ECannotBuildCanonicalUrl.Create(EntityType);
  Result := Format('%s(%s)', [EntitySetUrl(EntitySet), IdToKeyPredicate(EntityType, Id)]);
end;

function TXDataModel.DefaultSchema: TXDataSchema;
begin
  if Schemas.Count >= 0 then
    Result := Schemas[0]
  else
    Result := nil;
end;

function TXDataModel.EntitySetUrl(const AEntitySet: TXDataEntitySet): string;
begin
  // todo: use prefixed, qualified name when model has more than one entity container
  // for now, we will consider there is only one default entity container
  Result := AEntitySet.Name;
end;

function TXDataModel.GetKeyScalarProperties(
  AType: TXDataEntityType): TXDataSimplePropertyArray;

  procedure FillKeyConverters(AType: TXDataEntityType; var KeyProps: TXDataSimplePropertyArray);
  var
    Key: TXDataProperty;
    I: Integer;
  begin
    AType := FindRootType(AType);
    for I := 0 to AType.Key.Count - 1 do
    begin
      Key := AType.Key[I];
      if Key.IsSimple then
      begin
        SetLength(KeyProps, Length(KeyProps) + 1);
        KeyProps[Length(KeyProps) - 1] := TXDataSimpleProperty(Key);
      end
      else
        FillKeyConverters(TXDataNavigationProperty(Key).Target, KeyProps);
    end;
  end;

var
  KeyProps: TXDataSimplePropertyArray;
begin
  SetLength(KeyProps, 0);
  FillKeyConverters(AType, KeyProps);
  Result := KeyProps;
end;

function TXDataModel.IdToKeyPredicate(AType: TXDataEntityType;
  const Id: TValue): string;
var
  I: integer;
  Value, CastValue: TValue;
  KeyProps: TXDataSimplePropertyArray;
begin
  AType := FindRootType(AType);
  if AType.Key.Count = 0 then
    Exit('');
  if (AType.Key.Count = 1) and AType.Key[0].IsSimple then
    Result := TXDataSimpleProperty(AType.Key[0]).UrlConverter.ValueToUrl(Id, nil)
  else
  begin
    // complex predicate (composite or using associations)
    KeyProps := GetKeyScalarProperties(AType);
    if not (TValue_IsArray(Id) and (TValue_GetArrayLength(Id) = Length(KeyProps))) then
      raise ECannotBuildKeyPredicateWrongValueLength.Create(AType);

    for I := 0 to Length(KeyProps) - 1 do
    begin
      Value := TValue_GetArrayElement(Id, I);
      {$IFDEF PAS2JS}
      CastValue := Value;
      {$ELSE}
      if Value.TypeInfo = TypeInfo(TValue) then
        CastValue := Value.AsType<TValue>
      else
        CastValue := Value;
      {$ENDIF}

      if Result <> '' then
        Result := Result + ',';
      Result := Result + KeyProps[I].UrlConverter.ValueToUrl(CastValue, nil);
    end;
  end;
end;

{ TXDataModelObject }

destructor TXDataModelObject.Destroy;
begin
  {$IFDEF ISDELPHI}
  {$IFNDEF AUTOREFCOUNT}
  if OwnsDataObject and FData.IsObject then
    FData.AsObject.Free;
  {$ENDIF}
  {$ENDIF}
  inherited;
end;

{ TXDataType }

function TXDataType.GetIsCollection: boolean;
begin
  Result := false;
end;

function TXDataType.GetName: string;
begin
  Result := FName;
end;


function TXDataType.GetTypeKind: TXTypeKind;
begin
  Result := TXTypeKind.xtUndefined;
end;

function TXDataType.GetWrappedType: TXDataType;
begin
  Result := nil;
end;

function TXDataType.QualifiedName: string;
begin
  Result := Name;
end;

{ TXDataScalarType }

destructor TXDataScalarType.Destroy;
begin
  FCollection.Free;
  inherited;
end;

function TXDataScalarType.GetCollection: TXDataScalarType;
begin
  if FCollection = nil then
  begin
    FCollection := TXDataScalarCollectionType.Create;
    FCollection.ItemType := Self;
  end;
  Result := FCollection;
end;

{ TDataScalarCollectionType }

function TXDataScalarCollectionType.GetIsCollection: boolean;
begin
  Result := true;
end;

{ TXDataEntityCollectionType }

function TXDataEntityCollectionType.GetIsCollection: boolean;
begin
  Result := true;
end;

function TXDataScalarCollectionType.GetName: string;
begin
  if ItemType <> nil then
    Result := Format('Collection(%s)', [ItemType.Name])
  else
    Result := 'Collection()';
end;

function TXDataScalarCollectionType.GetTypeKind: TXTypeKind;
begin
  Result := TXTypeKind.xtScalarCollection;
end;

function TXDataScalarCollectionType.GetWrappedType: TXDataType;
begin
  Result := ItemType;
end;

function TXDataScalarCollectionType.QualifiedName: string;
begin
  if ItemType <> nil then
    Result := Format('Collection(%s)', [ItemType.QualifiedName])
  else
    Result := 'Collection()';
end;

function TXDataEntityCollectionType.GetName: string;
begin
  if ItemType <> nil then
    Result := Format('Collection(%s)', [ItemType.Name])
  else
    Result := 'Collection()';
end;

function TXDataEntityCollectionType.GetTypeKind: TXTypeKind;
begin
  Result := TXTypeKind.xtEntityCollection;
end;

function TXDataEntityCollectionType.GetWrappedType: TXDataType;
begin
  Result := ItemType;
end;

function TXDataEntityCollectionType.QualifiedName: string;
begin
  if ItemType <> nil then
    Result := Format('Collection(%s)', [ItemType.QualifiedName])
  else
    Result := 'Collection()';
end;

{ TXDataEntitySet }

function TXDataEntitySet.GetContainer: TXDataEntityContainer;
begin
  Result := TXDataEntityContainer(inherited Parent);
end;

function TXDataEntitySet.GetEntityTypeRef: string;
begin
  if EntityType <> nil then
    Result := EntityType.Name
  else
    Result := '';
end;

procedure TXDataEntitySet.Prepare;
begin
  if FEntityTypeRef <> '' then
  begin
    EntityType := Container.Schema.FindEntityType(FEntityTypeRef);
    if EntityType = nil then
      raise EModelReferenceNotFound.CreateFmt('EntityType not found: %s', [FEntityTypeRef]);
    FEntityTypeRef := '';
  end;
end;

{ ECannotSetTypeKind }

constructor ECannotSetTypeKind.Create(Prop: TXDataProperty; Value: TXTypeKind);
begin
  inherited CreateFmt('Cannot set property "%s" type to type kind %s',
    [Prop.QualifiedName, IntToStr(Ord(Value))]);
end;

{ TXDataAction }

constructor TXDataAction.Create;
begin
  FHttpMethod := 'POST';
  {$IFDEF GENERICS}
  FParameters := TOwnedList<TXDataParamDef, TXDataAction>.Create(Self);
  {$ELSE}
  FParameters := TXDataParamDefList.Create(Self);
  {$ENDIF}
end;

destructor TXDataAction.Destroy;
begin
  FParameters.Free;
  inherited;
end;

function TXDataAction.FindParameter(const AName: string): TXDataParamDef;
var
  I: Integer;
begin
  I := IndexOfParameter(AName);
  if I >= 0 then
    Result := Parameters[I]
  else
    Result := nil;
end;

function TXDataAction.HasBinding(ABindingMode: TBindingMode): boolean;
var
  Parameter: TXDataParamDef;
  I: Integer;
begin
  Result := false;
  for I := 0 to Self.Parameters.Count - 1 do
  begin
    Parameter := Self.Parameters[I];
    if Parameter.BindingMode = ABindingMode then
      Exit(true);
  end;
end;

function TXDataAction.HasOutputParams: Boolean;
var
  ParamDef: TXDataParamDef;
  I: Integer;
begin
  for I := 0 to Parameters.Count - 1 do
  begin
    ParamDef := Parameters[I];
    if ParamDef.Output then
      Exit(True);
  end;
  Result := False;
end;

function TXDataAction.IndexOfParameter(const AName: string): Integer;
var
  I: Integer;
begin
  for I := 0 to Parameters.Count - 1 do
    if SameText(Parameters[I].Name, AName) then
      Exit(I);
  Result := -1;
end;

procedure TXDataAction.Prepare;
var
  ParamDefIndex: Integer;
  ParamDef: TXDataParamDef;
begin
  for ParamDefIndex := 0 to Parameters.Count - 1 do
  begin
    ParamDef := Parameters[ParamDefIndex];
    ParamDef.Prepare;
  end;
end;

{$IFDEF ISDELPHI}
function TXDataAction.ReturnType: TRttiType;
begin
  if (FRttiMethod <> nil) then
    Result := FRttiMethod.ReturnType
  else
    Result := nil;
end;
{$ENDIF}

{$IFDEF ISDELPHI}
procedure TXDataAction.SetRttiMethod(ARttiMethod: TRttiMethod);
var
  IsSingleResult: Boolean;
begin
  FRttiMethod := ARttiMethod;

  FIsParamStream := (FRttiMethod <> nil) and (Length(FRttiMethod.GetParameters) = 1) and
    (FRttiMethod.GetParameters[0].ParamType.Handle = TypeInfo(TStream));

  IsSingleResult := (ReturnType <> nil) and not HasOutputParams;
  FIsResultSingleObject := IsSingleResult and ReturnType.IsInstance;
  FIsResultStream := IsSingleResult and (ReturnType.Handle = TypeInfo(TStream));
  FIsResultCriteria := IsSingleResult and (ReturnType.QualifiedName = 'Aurelius.Criteria.Base.TCriteria');

  FOperationId := '';
  if Assigned(RttiMethod.Parent) then
    FOperationId := RttiMethod.Parent.Name + '.';
  FOperationId := FOperationId + RttiMethod.Name;
end;
{$ENDIF}

function TXDataAction.GetController: TXDataController;
begin
  Result := TXDataController(inherited Parent);
end;

{ TXDataParamDef }

function TXDataParamDef.GetParent: TXDataAction;
begin
  Result := TXDataAction(inherited Parent);
end;

function TXDataParamDef.GetTypeRef: string;
begin
  if FType <> nil then
    Result := FType.Name
  else
    Result := '';
end;

procedure TXDataParamDef.Prepare;
begin
  // Solve type reference, if any
  if FTypeRef <> '' then
  begin
    ParamType := Parent.Controller.Schema.FindEnumType(FTypeRef);
    if ParamType = nil then
      ParamType := TXDataPrimitiveTypes.Instance.Find(FTypeRef);
    if ParamType = nil then
      raise EModelReferenceNotFound.CreateFmt('Param type %s not found for property %s.%s',
        [FTypeRef, Parent.Name, Name]);
    FTypeRef := '';
  end;

  // Find the type converter for the param def
  if (BindingMode = TBindingMode.FromURI) then
  begin
    UrlConverter := Parent.Controller.Schema.FindOrDefineConverter(ParamType);
    if UrlConverter = nil then
      raise EFromURIBindingNotSupported.Create(Self);
  end;
end;

{ TXDataController }

constructor TXDataController.Create;
begin
  {$IFDEF GENERICS}
  FActions := TOwnedList<TXDataAction, TXDataController>.Create(Self);
  {$ELSE}
  FActions := TXDataActionList.Create(Self);
  {$ENDIF}
end;

destructor TXDataController.Destroy;
begin
  FActions.Free;
  inherited;
end;

{$IFDEF ISDELPHI}
function TXDataController.FindActionFromRttiMethod(const AMethodName: string): TXDataAction;
var
  Action: TXDataAction;
begin
  for Action in Self.Actions do
    if SameText(Action.RttiMethod.Name, AMethodName) then
      Exit(Action);
  Result := nil;
end;
{$ENDIF}

procedure TXDataController.FindActions(const AName: string; AActions: TXDataActionList);
var
  Action: TXDataAction;
  I: Integer;
begin
  AActions.Clear;
  for I := 0 to Self.Actions.Count - 1 do
  begin
    Action := Self.Actions[I];
    if SameText(Action.Name, AName) then
      AActions.Add(Action);
  end;
end;

{$IFDEF ISDELPHI}
function TXDataController.InterfaceTypeName: string;
begin
  Result := GetTypeName_(InterfaceType);
end;
{$ENDIF}

function TXDataController.GetSchema: TXDataSchema;
begin
  Result := TXDataSchema(inherited Parent);
end;

procedure TXDataController.Prepare;
var
  ActionIndex: Integer;
  Action: TXDataAction;
begin
  for ActionIndex := 0 to Actions.Count - 1 do
  begin
    Action := Actions[ActionIndex];
    Action.Prepare;
  end;
end;

{ TXDataNavigationProperty }

function TXDataNavigationProperty.GetTargetRef: string;
begin
  if FTarget <> nil then
    Result := FTarget.Name
  else
    Result := FTargetRef;
end;

function TXDataNavigationProperty.IsSimple: Boolean;
begin
  Result := False;
end;

procedure TXDataNavigationProperty.Prepare;
begin
  if FTargetRef <> '' then
  begin
    Target := Parent.Schema.FindEntityType(FTargetRef);
    if Target = nil then
      raise EModelReferenceNotFound.CreateFmt('Target %s not found for navigation property %s.%s',
        [FTargetRef, Parent.Name, Name]);
    FTargetRef := '';
  end;
end;

{ TXDataOwnedList }

{$IFNDEF GENERICS}
constructor TXDataOwnedList.Create;
begin
  Create(nil);
end;

constructor TXDataOwnedList.Create(AOwner: TXDataModelObject);
begin
  inherited Create(AOwner <> nil);
  FOwner := AOwner;
end;

{$IFDEF PAS2JS}
procedure TXDataOwnedList.Notify(Ptr: JSValue; Action: TListNotification);
{$ELSE}
procedure TXDataOwnedList.Notify(Ptr: Pointer; Action: TListNotification);
{$ENDIF}
begin
  if FOwner = nil then Exit; // not managed
  
  if Action = lnAdded then
    TXDataModelObject(Ptr).Parent := FOwner
  else
    TXDataModelObject(Ptr).Parent := nil;
  inherited;
end;
{$ENDIF}

{ TXDataParamDefList }

{$IFNDEF GENERICS}
function TXDataParamDefList.GetItem(Index: Integer): TXDataParamDef;
begin
  Result := TXDataParamDef(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataEntityContainerList }

{$IFNDEF GENERICS}
function TXDataEntityContainerList.GetItem(
  Index: Integer): TXDataEntityContainer;
begin
  Result := TXDataEntityContainer(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataSchemaList }

{$IFNDEF GENERICS}
function TXDataSchemaList.GetItem(Index: Integer): TXDataSchema;
begin
  Result := TXDataSchema(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataPropertyList }

{$IFNDEF GENERICS}
function TXDataPropertyList.GetItem(Index: Integer): TXDataProperty;
begin
  Result := TXDataProperty(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataEnumTypeList }

{$IFNDEF GENERICS}
function TXDataEnumTypeList.GetItem(Index: Integer): TXDataEnumType;
begin
  Result := TXDataEnumType(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataEnumMemberList }

{$IFNDEF GENERICS}
function TXDataEnumMemberList.GetItem(Index: Integer): TXDataEnumMember;
begin
  Result := TXDataEnumMember(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataEntityTypeList }

{$IFNDEF GENERICS}
function TXDataEntityTypeList.GetItem(Index: Integer): TXDataEntityType;
begin
  Result := TXDataEntityType(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataControllerList }

{$IFNDEF GENERICS}
function TXDataControllerList.GetItem(Index: Integer): TXDataController;
begin
  Result := TXDataController(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataOwnedActionList }

{$IFNDEF GENERICS}
function TXDataActionList.GetItem(Index: Integer): TXDataAction;
begin
  Result := TXDataAction(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataSimplePropertyList }

{$IFNDEF GENERICS}
function TXDataSimplePropertyList.GetItem(Index: Integer): TXDataSimpleProperty;
begin
  Result := TXDataSimpleProperty(inherited Items[Index]);
end;
{$ENDIF}

{ EXDataEntityTypeNotFound }

constructor EXDataEntityTypeNotFound.Create(const AName: string);
begin
  inherited CreateFmt('Entity type not found for type "%s"', [AName]);
end;

{ EXDataEntitySetNotFound }

constructor EXDataEntitySetNotFound.Create(const AName: string);
begin
  inherited CreateFmt('Entity set "%s" not found', [AName]);
end;

{ ECannotBuildKeyPredicateWrongValueLength }

constructor ECannotBuildKeyPredicateWrongValueLength.Create(
  AType: TXDataEntityType);
begin
  inherited CreateFmt('Cannot create id predicate for type "%s". ' +
    'The length of id value does not match number of elements in composite key', [AType.QualifiedName]);
end;

{ ECannotBuildCanonicalUrl }

constructor ECannotBuildCanonicalUrl.Create(AType: TXDataEntityType);
begin
  inherited CreateFmt('Cannot create canonical url for type "%s"', [AType.QualifiedName]);
end;

{ TXDataEntitySetList }

{$IFNDEF GENERICS}
function TXDataEntitySetList.GetItem(Index: Integer): TXDataEntitySet;
begin
  Result := TXDataEntitySet(inherited Items[Index]);
end;
{$ENDIF}

{ TXDataNavigationPropertyList }

{$IFNDEF GENERICS}
function TXDataNavigationPropertyList.GetItem(
  Index: Integer): TXDataNavigationProperty;
begin
  Result := TXDataNavigationProperty(inherited Items[Index]);
end;
{$ENDIF}

{ EFromURIBindingNotSupported }

constructor EFromURIBindingNotSupported.Create(AParam: TXDataParamDef);
var
  ParamTypeName: string;
begin
  if AParam.ParamType <> nil then
    ParamTypeName := AParam.ParamType.Name
  else
    ParamTypeName := '(undefined)';

  inherited CreateFmt('Cannot define action "%s": Type "%s" for param "%s" cannot be bound from URI',
    [AParam.Parent.Name, ParamTypeName, AParam.Name]);
end;

{ EUrlConverterNotFound }

constructor EUrlConverterNotFound.Create(AType: TXDataType);
var
  ATypeName: string;
begin
  if AType = nil then
    ATypeName := '(null)'
  else
    ATypeName := AType.Name;
  inherited CreateFmt('Could not find URL converter for type "%s"', [ATypeName]);
end;

{ TXDataUrlConverters }

constructor TXDataUrlConverters.Create;
begin
  FConverterMap := TStringMap.Create(True);
  AddDefaultUrlConverters(Self);
end;

destructor TXDataUrlConverters.Destroy;
begin
  FConverterMap.Free;
  inherited;
end;

function TXDataUrlConverters.Find(const ATypeName: string): TXDataTypeConverter;
begin
  if FConverterMap.ContainsKey(ATypeName) then
    Result := TXDataTypeConverter(FConverterMap[ATypeName])
  else
    Result := nil;
end;

procedure TXDataUrlConverters.RegisterConverter(const ATypeName: string;
  AConverter: TXDataTypeConverter);
begin
  FConverterMap.AddOrSetValue(ATypeName, AConverter);
end;

end.
