(*
* Copyright (c) 2011, Ciobanu Alexandru
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the <organization> nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*)
unit Collections.Dynamic;
interface
uses
  SysUtils,
  Generics.Collections,
  Rtti,
  TypInfo;
type
  ///  <summary>Alias for the Rtti <c>TValue</c> type. The compiler seems to have a hard
  ///  time differentiating <c>TValue</c> from the generic <c>TValue</c> type argument.</summary>
  TAny = TValue;
  ///  <summary>An alias to the <v>Variant</c> type. Its main purpose is to serve as a reminder that
  ///  it contains a part of an object. It is to be considered a "dynamic record".</summary>
  TView = Variant;
  ///  <summary>A special purpose record type that exposes a number of methods that generate
  ///  selector methods for fields and properties of a class or record type.</summary>
  Member = record
  private class var
    FViewVariantType: Word;
  private type
    {$REGION 'Internal Types'}
    TViewPair = TPair<string, TValue>;
    TViewArray = TArray<TViewPair>;
    TSelector<T, K> = class(TInterfacedObject, TFunc<T, K>, TFunc<T, TValue>)
    private
      FContext: TRttiContext;
      FType: TRttiType;
      FMember: TRttiMember;
    protected
      function TFunc<T, K>.Invoke = GenericInvoke;
      function TFunc<T, TValue>.Invoke = TValueInvoke;
    public
      function TValueInvoke(AFrom: T): TValue; virtual; abstract;
      function GenericInvoke(AFrom: T): K;
    end;
    TRecordFieldSelector<K> = class(TSelector<Pointer, K>)
    public
      function TValueInvoke(AFrom: Pointer): TValue; override;
    end;
    TClassFieldSelector<K> = class(TSelector<TObject, K>)
    public
      function TValueInvoke(AFrom: TObject): TValue; override;
    end;
    TClassPropertySelector<K> = class(TSelector<TObject, K>)
    public
      function TValueInvoke(AFrom: TObject): TValue; override;
    end;
    TViewSelector<T> = class(TInterfacedObject, TFunc<T, TView>)
    private
      FNames: TArray<string>;
      FFuncs: TArray<TFunc<T, TValue>>;
    public
      function Invoke(AFrom: T): TView;
    end;
    {$ENDREGION}
  public
    ///  <summary>Generates a selector for a given member name.</summary>
    ///  <param name="AName">The field or property name to select from <c>T</c>.</param>
    ///  <returns>A selector function that retrieves the field/property from a class or record.
    ///  <c>nil</c> is returned in case of error.</returns>
    class function Name<T, K>(const AName: string): TFunc<T, K>; overload; static;
    ///  <summary>Generates a selector for a given member name.</summary>
    ///  <param name="AName">The field or property name to select from <c>T</c>.</param>
    ///  <returns>A selector function that retrieves the field/property from a class or record. The selected value is a <c>TValue</c> type.
    ///  <c>nil</c> is returned in case of error.</returns>
    class function Name<T>(const AName: string): TFunc<T, TValue>; overload; static;
    ///  <summary>Generates a selector for the given member names.</summary>
    ///  <param name="ANames">The field or property names to select from <c>T</c>.</param>
    ///  <returns>A selector function that retrieves the fields/properties from a class or record. The selected value is a <c>TView</c> type.
    ///  <c>nil</c> is returned in case of error.</returns>
    class function Name<T>(const ANames: array of string): TFunc<T, TView>; overload; static;
  end;
implementation
uses
  Variants;
type
  { Mapping the TSVDictionary into TVarData structure }
  TViewDictionaryVarData = packed record
    { Var type; will be assigned at run time }
    VType: TVarType;
    { Reserved stuff }
    Reserved1, Reserved2, Reserved3: Word;
    { A reference to the enclosed dictionary }
    FArray: Member.TViewArray;
    { Reserved stuff }
    Reserved4: LongWord;
  end;
  { Manager for our variant type }
  TViewDictionaryVariantType = class(TInvokeableVariantType)
  public
    procedure Clear(var V: TVarData); override;
    procedure Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); override;
    function GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; override;
  end;
{ TViewDictionaryVariantType }
procedure TViewDictionaryVariantType.Clear(var V: TVarData);
begin
  { Clear the variant type }
  V.VType := varEmpty;
  { And dispose the value }
  TViewDictionaryVarData(V).FArray := nil;
end;
procedure TViewDictionaryVariantType.Copy(var Dest: TVarData;
  const Source: TVarData; const Indirect: Boolean);
begin
  if Indirect and VarDataIsByRef(Source) then
    VarDataCopyNoInd(Dest, Source)
  else
  begin
    with TViewDictionaryVarData(Dest) do
    begin
      { Copy the variant type }
      VType := VarType;
      { Copy the reference }
      FArray := TViewDictionaryVarData(Source).FArray;
    end;
  end;
end;
function TViewDictionaryVariantType.GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean;
var
  LPair: Member.TViewPair;
  LAsVar: Variant;
begin
  { Iterate over our internal array and search for the requested property by name }
  with TViewDictionaryVarData(V) do
  begin
    for LPair in FArray do
      if AnsiSameStr(LPair.Key, Name) then
      begin
        LAsVar := LPair.Value.AsVariant;
        Dest := TVarData(LAsVar);
        Exit(True);
      end;
  end;
  { Key not found, means error }
  Clear(Dest);
  Result := False;
end;
var
  { Our singleton that manages our variant type }
  FViewDictionaryVariantType: TViewDictionaryVariantType;
{ Member.TSelector<T, K> }
function Member.TSelector<T, K>.GenericInvoke(AFrom: T): K;
begin
  Result := TValueInvoke(AFrom).AsType<K>();
end;
{ Member.TRecordFieldSelector<T, K> }
function Member.TRecordFieldSelector<K>.TValueInvoke(AFrom: Pointer): TValue;
begin
  ASSERT(Assigned(FMember));
  ASSERT(FMember is TRttiField);
  Result := TRttiField(FMember).GetValue(AFrom);
end;
{ Member.TClassFieldSelector<K> }
function Member.TClassFieldSelector<K>.TValueInvoke(AFrom: TObject): TValue;
begin
  ASSERT(Assigned(FMember));
  ASSERT(FMember is TRttiField);
  Result := TRttiField(FMember).GetValue(AFrom);
end;
{ Member.TClassPropertySelector<K> }
function Member.TClassPropertySelector<K>.TValueInvoke(AFrom: TObject): TValue;
begin
  ASSERT(Assigned(FMember));
  ASSERT(FMember is TRttiProperty);
  Result := TRttiProperty(FMember).GetValue(AFrom);
end;
{ Member.TViewSelector<T> }
function Member.TViewSelector<T>.Invoke(AFrom: T): TView;
var
  I, L: NativeInt;
  LCalc: TViewArray;
begin
  { Initialize a view }
  VarClear(Result);
  L := Length(FFuncs);
  SetLength(LCalc, L);
  { Copy selected fields over }
  for I := 0 to Length(FFuncs) - 1 do
  begin
    LCalc[I].Key := FNames[I];
    LCalc[I].Value := FFuncs[I](AFrom);
  end;
  { Give the result to the guy standing on the chair ... }
  with TVarData(Result) do
  begin
    VType := Member.FViewVariantType;
    Member.TViewArray(VPointer) := LCalc;
  end;
end;
{ Member }
class function Member.Name<T, K>(const AName: string): TFunc<T, K>;
var
  LT, LK: PTypeInfo;
  LContext: TRttiContext;
  LType: TRttiType;
  LMember: TRttiMember;
  LSelector: TSelector<T, K>;
begin
  Result := nil;
  { Get the type }
  LT := TypeInfo(T);
  LK := TypeInfo(K);
  LType := LContext.GetType(LT);
  { Check for correctness }
  if not Assigned(LType) or not (LType.TypeKind in [tkClass, tkRecord]) then
    Exit;
  if LType.TypeKind = tkRecord then
  begin
    LMember := LType.GetField(AName);
    if not Assigned(LMember) then
      Exit;
    LSelector := TSelector<T, K>(TRecordFieldSelector<K>.Create());
  end else
  if LType.TypeKind = tkClass then
  begin
    LMember := LType.GetField(AName);
    if Assigned(LMember) then
      LSelector := TSelector<T, K>(TClassFieldSelector<K>.Create())
    else begin
      LMember := LType.GetProperty(AName);
      if not Assigned(LMember) then
        Exit;
      LSelector := TSelector<T, K>(TClassPropertySelector<K>.Create());
    end;
  end;
  { Upload selector }
  LSelector.FContext := LContext;
  LSelector.FType := LType;
  LSelector.FMember := LMember;
  Result := LSelector;
end;
class function Member.Name<T>(const AName: string): TFunc<T, TValue>;
begin
  Result := Member.Name<T, TValue>(AName);
end;
class function Member.Name<T>(const ANames: array of string): TFunc<T, TView>;
var
  LSelector: TViewSelector<T>;
  I, L: NativeInt;
begin
  Result := nil;
  L := Length(ANames);
  if L = 0 then
    Exit;
  LSelector := TViewSelector<T>.Create;
  { Prepare the array of selectors }
  SetLength(LSelector.FNames, L);
  SetLength(LSelector.FFuncs, L);
  { Create the array }
  for I := 0 to L - 1 do
  begin
    LSelector.FNames[I] := AnsiUpperCase(ANames[I]);
    LSelector.FFuncs[I] := Member.Name<T, TValue>(ANames[I]);
    if not Assigned(LSelector.FFuncs[I]) then
    begin
      LSelector.Free;
      Exit;
    end;
  end;
  Result := LSelector;
end;
initialization
  { Register our custom variant type }
  FViewDictionaryVariantType := TViewDictionaryVariantType.Create();
  Member.FViewVariantType := FViewDictionaryVariantType.VarType;
finalization
  { Uregister our custom variant }
  FreeAndNil(FViewDictionaryVariantType);
end.