THRIFT-4862 better ToString() support for enums and container types
Client: Delphi
Patch: Jens Geyer

This closes #1795
diff --git a/lib/delphi/src/Thrift.Collections.pas b/lib/delphi/src/Thrift.Collections.pas
index b2206cb..2e13724 100644
--- a/lib/delphi/src/Thrift.Collections.pas
+++ b/lib/delphi/src/Thrift.Collections.pas
@@ -22,7 +22,7 @@
 interface
 
 uses
-  Generics.Collections, Generics.Defaults, Thrift.Utils;
+  SysUtils, Generics.Collections, Generics.Defaults, Thrift.Utils;
 
 type
 
@@ -30,11 +30,11 @@
   TArray<T> = array of T;
 {$IFEND}
 
-  IThriftContainer = interface
-    ['{93DEF5A0-D162-461A-AB22-5B4EE0734050}']
-    function ToString: string;
+  IThriftContainer = interface( ISupportsToString)
+    ['{E05C0F9D-A4F5-491D-AADA-C926B4BDB6E4}']
   end;
 
+
   IThriftDictionary<TKey,TValue> = interface(IThriftContainer)
     ['{25EDD506-F9D1-4008-A40F-5940364B7E46}']
     function GetEnumerator: TEnumerator<TPair<TKey,TValue>>;
@@ -64,7 +64,7 @@
     property Values: TDictionary<TKey,TValue>.TValueCollection read GetValues;
   end;
 
-  TThriftDictionaryImpl<TKey,TValue> = class( TInterfacedObject, IThriftDictionary<TKey,TValue>)
+  TThriftDictionaryImpl<TKey,TValue> = class( TInterfacedObject, IThriftDictionary<TKey,TValue>, IThriftContainer, ISupportsToString)
   private
     FDictionaly : TDictionary<TKey,TValue>;
   protected
@@ -95,6 +95,7 @@
   public
     constructor Create(ACapacity: Integer = 0);
     destructor Destroy; override;
+    function ToString : string;  override;
   end;
 
   IThriftList<T> = interface(IThriftContainer)
@@ -140,7 +141,7 @@
     property Items[Index: Integer]: T read GetItem write SetItem; default;
   end;
 
-  TThriftListImpl<T> = class( TInterfacedObject, IThriftList<T>)
+  TThriftListImpl<T> = class( TInterfacedObject, IThriftList<T>, IThriftContainer, ISupportsToString)
   private
     FList : TList<T>;
   protected
@@ -186,6 +187,7 @@
   public
     constructor Create;
     destructor Destroy; override;
+    function ToString : string;  override;
   end;
 
   IHashSet<TValue> = interface(IThriftContainer)
@@ -202,7 +204,7 @@
     function Remove( const item: TValue ): Boolean;
   end;
 
-  THashSetImpl<TValue> = class( TInterfacedObject, IHashSet<TValue>)
+  THashSetImpl<TValue> = class( TInterfacedObject, IHashSet<TValue>, IThriftContainer, ISupportsToString)
   private
     FDictionary : IThriftDictionary<TValue,Integer>;
     FIsReadOnly: Boolean;
@@ -219,6 +221,7 @@
     function Remove( const item: TValue ): Boolean;
   public
     constructor Create;
+    function ToString : string;  override;
   end;
 
 implementation
@@ -287,6 +290,28 @@
   end;
 end;
 
+function THashSetImpl<TValue>.ToString : string;
+var elm : TValue;
+    sb : TThriftStringBuilder;
+    first : Boolean;
+begin
+  sb := TThriftStringBuilder.Create('{');
+  try
+    first := TRUE;
+    for elm in FDictionary.Keys do begin
+      if first
+      then first := FALSE
+      else sb.Append(', ');
+
+      sb.Append( StringUtils<TValue>.ToString(elm));
+    end;
+    sb.Append('}');
+    Result := sb.ToString;
+  finally
+    sb.Free;
+  end;
+end;
+
 { TThriftDictionaryImpl<TKey, TValue> }
 
 procedure TThriftDictionaryImpl<TKey, TValue>.Add(const Key: TKey;
@@ -393,6 +418,32 @@
 {$IFEND}
 end;
 
+function TThriftDictionaryImpl<TKey, TValue>.ToString : string;
+var pair : TPair<TKey, TValue>;
+    sb : TThriftStringBuilder;
+    first : Boolean;
+begin
+  sb := TThriftStringBuilder.Create('{');
+  try
+    first := TRUE;
+    for pair in FDictionaly do begin
+      if first
+      then first := FALSE
+      else sb.Append(', ');
+
+      sb.Append( '(');
+      sb.Append( StringUtils<TKey>.ToString(pair.Key));
+      sb.Append(' => ');
+      sb.Append( StringUtils<TValue>.ToString(pair.Value));
+      sb.Append(')');
+    end;
+    sb.Append('}');
+    Result := sb.ToString;
+  finally
+    sb.Free;
+  end;
+end;
+
 procedure TThriftDictionaryImpl<TKey, TValue>.TrimExcess;
 begin
   FDictionaly.TrimExcess;
@@ -611,6 +662,28 @@
 {$IFEND}
 end;
 
+function TThriftListImpl<T>.ToString : string;
+var elm : T;
+    sb : TThriftStringBuilder;
+    first : Boolean;
+begin
+  sb := TThriftStringBuilder.Create('{');
+  try
+    first := TRUE;
+    for elm in FList do begin
+      if first
+      then first := FALSE
+      else sb.Append(', ');
+
+      sb.Append( StringUtils<T>.ToString(elm));
+    end;
+    sb.Append('}');
+    Result := sb.ToString;
+  finally
+    sb.Free;
+  end;
+end;
+
 procedure TThriftListImpl<T>.TrimExcess;
 begin
   FList.TrimExcess;
diff --git a/lib/delphi/src/Thrift.Protocol.pas b/lib/delphi/src/Thrift.Protocol.pas
index 36509ca..609dfc6 100644
--- a/lib/delphi/src/Thrift.Protocol.pas
+++ b/lib/delphi/src/Thrift.Protocol.pas
@@ -29,6 +29,7 @@
   Contnrs,
   Thrift.Exception,
   Thrift.Stream,
+  Thrift.Utils,
   Thrift.Collections,
   Thrift.Transport;
 
@@ -111,12 +112,6 @@
     function GetProtocol( const trans: ITransport): IProtocol;
   end;
 
-  TThriftStringBuilder = class( TStringBuilder)
-  public
-    function Append(const Value: TBytes): TStringBuilder; overload;
-    function Append(const Value: IThriftContainer): TStringBuilder; overload;
-  end;
-
   TProtocolException = class( TException)
   public
     const // TODO(jensg): change into enum
@@ -292,9 +287,8 @@
     constructor Create( trans: ITransport );
   end;
 
-  IBase = interface
-    ['{08D9BAA8-5EAA-410F-B50B-AC2E6E5E4155}']
-    function ToString: string;
+  IBase = interface( ISupportsToString)
+    ['{AFF6CECA-5200-4540-950E-9B89E0C1C00C}']
     procedure Read( const iprot: IProtocol);
     procedure Write( const iprot: IProtocol);
   end;
@@ -1034,19 +1028,6 @@
   inherited HiddenCreate(Msg);
 end;
 
-{ TThriftStringBuilder }
-
-function TThriftStringBuilder.Append(const Value: TBytes): TStringBuilder;
-begin
-  Result := Append( string( RawByteString(Value)) );
-end;
-
-function TThriftStringBuilder.Append(
-  const Value: IThriftContainer): TStringBuilder;
-begin
-  Result := Append( Value.ToString );
-end;
-
 { TBinaryProtocolImpl.TFactory }
 
 constructor TBinaryProtocolImpl.TFactory.Create(AStrictRead, AStrictWrite: Boolean);
diff --git a/lib/delphi/src/Thrift.Utils.pas b/lib/delphi/src/Thrift.Utils.pas
index 7e57863..46e238c 100644
--- a/lib/delphi/src/Thrift.Utils.pas
+++ b/lib/delphi/src/Thrift.Utils.pas
@@ -25,12 +25,19 @@
 
 uses
   {$IFDEF OLD_UNIT_NAMES}
-  Classes, Windows, SysUtils, Character, SyncObjs;
+  Classes, Windows, SysUtils, Character, SyncObjs, TypInfo, Rtti;
   {$ELSE}
-  System.Classes, Winapi.Windows, System.SysUtils, System.Character, System.SyncObjs;
+  System.Classes, Winapi.Windows, System.SysUtils, System.Character,
+  System.SyncObjs, System.TypInfo, System.Rtti;
   {$ENDIF}
 
 type
+  ISupportsToString = interface
+    ['{AF71C350-E0CD-4E94-B77C-0310DC8227FF}']
+    function ToString : string;
+  end;
+
+
   IOverlappedHelper = interface
     ['{A1832EFA-2E02-4884-8F09-F0A0277157FA}']
     function Overlapped : TOverlapped;
@@ -55,6 +62,13 @@
   end;
 
 
+  TThriftStringBuilder = class( TStringBuilder)
+  public
+    function Append(const Value: TBytes): TStringBuilder; overload;
+    function Append(const Value: ISupportsToString): TStringBuilder; overload;
+  end;
+
+
   Base64Utils = class sealed
   public
     class function Encode( const src : TBytes; srcOff, len : Integer; dst : TBytes; dstOff : Integer) : Integer; static;
@@ -68,6 +82,16 @@
     class function IsLowSurrogate( const c : Char) : Boolean; static; inline;
   end;
 
+  EnumUtils<T> = class sealed
+  public
+    class function ToString(const value : Integer) : string;  reintroduce; static; inline;
+  end;
+
+  StringUtils<T> = class sealed
+  public
+    class function ToString(const value : T) : string;  reintroduce; static; inline;
+  end;
+
 
 {$IFDEF Win64}
 function InterlockedExchangeAdd64( var Addend : Int64; Value : Int64) : Int64;  
@@ -256,4 +280,51 @@
 {$ENDIF}
 
 
+{ EnumUtils<T> }
+
+class function EnumUtils<T>.ToString(const value : Integer) : string;
+var pType : PTypeInfo;
+begin
+  pType := PTypeInfo(TypeInfo(T));
+  if Assigned(pType) and (pType^.Kind = tkEnumeration)
+  then result := GetEnumName(pType,value)
+  else result := IntToStr(Ord(value));
+end;
+
+
+{ StringUtils<T> }
+
+class function StringUtils<T>.ToString(const value : T) : string;
+var pType : PTypeInfo;
+    base  : ISupportsToString;
+begin
+  pType := PTypeInfo(TypeInfo(T));
+  if Assigned(pType) then begin
+    case pType^.Kind of
+      tkInterface : begin
+        if Supports(IInterface(value), ISupportsToString, base) then begin
+          result := base.toString;
+          Exit;
+        end;
+      end;
+    end;
+  end;
+
+  result := TValue.From<T>(value).ToString;
+end;
+
+
+{ TThriftStringBuilder }
+
+function TThriftStringBuilder.Append(const Value: TBytes): TStringBuilder;
+begin
+  Result := Append( string( RawByteString(Value)) );
+end;
+
+function TThriftStringBuilder.Append( const Value: ISupportsToString): TStringBuilder;
+begin
+  Result := Append( Value.ToString );
+end;
+
+
 end.
diff --git a/lib/delphi/test/TestServer.pas b/lib/delphi/test/TestServer.pas
index 374472c..4cb0090 100644
--- a/lib/delphi/test/TestServer.pas
+++ b/lib/delphi/test/TestServer.pas
@@ -150,7 +150,7 @@
 
 function TTestServer.TTestHandlerImpl.testEnum(thing: TNumberz): TNumberz;
 begin
-  Console.WriteLine('testEnum(' + IntToStr( Integer( thing)) + ')');
+  Console.WriteLine('testEnum(' + EnumUtils<TNumberz>.ToString(Ord(thing)) + ')');
   Result := thing;
 end;
 
@@ -191,7 +191,10 @@
   insane : IThriftDictionary<Int64, IThriftDictionary<TNumberz, IInsanity>>;
 
 begin
-  Console.WriteLine('testInsanity()');
+  Console.Write('testInsanity(');
+  if argument <> nil then Console.Write(argument.ToString);
+  Console.WriteLine(')');
+
 
   (**
    * So you think you've got this all worked, out eh?
@@ -222,49 +225,20 @@
   Result := insane;
 end;
 
-function TTestServer.TTestHandlerImpl.testList(
-  const thing: IThriftList<Integer>): IThriftList<Integer>;
-var
-  first : Boolean;
-  elem : Integer;
+function TTestServer.TTestHandlerImpl.testList( const thing: IThriftList<Integer>): IThriftList<Integer>;
 begin
-  Console.Write('testList({');
-  first := True;
-  for elem in thing do
-  begin
-    if first then
-    begin
-      first := False;
-    end else
-    begin
-      Console.Write(', ');
-    end;
-    Console.Write( IntToStr( elem));
-  end;
-  Console.WriteLine('})');
+  Console.Write('testList(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');
   Result := thing;
 end;
 
 function TTestServer.TTestHandlerImpl.testMap(
   const thing: IThriftDictionary<Integer, Integer>): IThriftDictionary<Integer, Integer>;
-var
-  first : Boolean;
-  key : Integer;
 begin
-  Console.Write('testMap({');
-  first := True;
-  for key in thing.Keys do
-  begin
-    if (first) then
-    begin
-      first := false;
-    end else
-    begin
-      Console.Write(', ');
-    end;
-    Console.Write(IntToStr(key) + ' => ' + IntToStr( thing[key]));
-  end;
-  Console.WriteLine('})');
+  Console.Write('testMap(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');
   Result := thing;
 end;
 
@@ -313,12 +287,11 @@
   x2 : TXception2;
 begin
   Console.WriteLine('testMultiException(' + arg0 + ', ' + arg1 + ')');
-  if ( arg0 = 'Xception') then
-  begin
+  if ( arg0 = 'Xception') then begin
     raise TXception.Create( 1001, 'This is an Xception');  // test the new rich CTOR
-  end else
-  if ( arg0 = 'Xception2') then
-  begin
+  end;
+
+  if ( arg0 = 'Xception2') then begin
     x2 := TXception2.Create;  // the old way still works too?
     x2.ErrorCode := 2002;
     x2.Struct_thing := TXtructImpl.Create;
@@ -332,17 +305,11 @@
 end;
 
 function TTestServer.TTestHandlerImpl.testNest( const thing: IXtruct2): IXtruct2;
-var
-  temp : IXtruct;
 begin
-  temp := thing.Struct_thing;
-  Console.WriteLine('testNest({' +
-         IntToStr( thing.Byte_thing) + ', {' +
-         '"' + temp.String_thing + '", ' +
-         IntToStr( temp.Byte_thing) + ', ' +
-         IntToStr( temp.I32_thing) + ', ' +
-         IntToStr( temp.I64_thing) + '}, ' +
-         IntToStr( temp.I32_thing) + '})');
+  Console.Write('testNest(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');
+
   Result := thing;
 end;
 
@@ -353,34 +320,18 @@
   Console.WriteLine('testOneway finished');
 end;
 
-function TTestServer.TTestHandlerImpl.testSet(
-  const thing: IHashSet<Integer>):IHashSet<Integer>;
-var
-  first : Boolean;
-  elem : Integer;
+function TTestServer.TTestHandlerImpl.testSet( const thing: IHashSet<Integer>):IHashSet<Integer>;
 begin
-  Console.Write('testSet({');
-  first := True;
+  Console.Write('testSet(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');;
 
-  for elem in thing do
-  begin
-    if first then
-    begin
-      first := False;
-    end else
-    begin
-      Console.Write( ', ');
-    end;
-    Console.Write( IntToStr( elem));
-  end;
-  Console.WriteLine('})');
   Result := thing;
 end;
 
 procedure TTestServer.TTestHandlerImpl.testStop;
 begin
-  if FServer <> nil then
-  begin
+  if FServer <> nil then begin
     FServer.Stop;
   end;
 end;
@@ -399,24 +350,11 @@
 
 function TTestServer.TTestHandlerImpl.testStringMap(
   const thing: IThriftDictionary<string, string>): IThriftDictionary<string, string>;
-var
-  first : Boolean;
-  key : string;
 begin
-  Console.Write('testStringMap({');
-  first := True;
-  for key in thing.Keys do
-  begin
-    if (first) then
-    begin
-      first := false;
-    end else
-    begin
-      Console.Write(', ');
-    end;
-    Console.Write(key + ' => ' + thing[key]);
-  end;
-  Console.WriteLine('})');
+  Console.Write('testStringMap(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');
+
   Result := thing;
 end;
 
@@ -433,11 +371,10 @@
 
 function TTestServer.TTestHandlerImpl.testStruct( const thing: IXtruct): IXtruct;
 begin
-  Console.WriteLine('testStruct({' +
-    '"' + thing.String_thing + '", ' +
-      IntToStr( thing.Byte_thing) + ', ' +
-      IntToStr( thing.I32_thing) + ', ' +
-      IntToStr( thing.I64_thing));
+  Console.Write('testStruct(');
+  if thing <> nil then Console.Write(thing.ToString);
+  Console.WriteLine(')');
+
   Result := thing;
 end;