Browse Source

--- Merging r35292 into '.':
U packages/fcl-web/examples/echo/cgi/echo.lpi
U packages/fcl-web/examples/echo/cgi/echo.res
U packages/fcl-web/examples/httpclient/httpget.lpi
U packages/fcl-web/examples/httpapp/testhttp.pp
U packages/fcl-web/examples/httpapp/testhttp.lpi
U packages/fcl-web/examples/httpserver/simplehttpserver.lpi
A packages/fcl-web/examples/routing
A packages/fcl-web/examples/routing/demorouting.lpr
A packages/fcl-web/examples/routing/README
A packages/fcl-web/examples/routing/demorouting.lpi
A packages/fcl-web/examples/routing/routes.pp
A packages/fcl-web/examples/routing/sample.ini
U packages/fcl-web/fpmake.pp
A packages/fcl-web/tests/testfpweb.lpr
A packages/fcl-web/tests/tchttproute.pp
A packages/fcl-web/tests/testfpweb.lpi
U packages/fcl-web/src/base/fphttp.pp
A packages/fcl-web/src/base/tcwebmodule.pp
U packages/fcl-web/src/base/custweb.pp
U packages/fcl-web/src/base/httpdefs.pp
U packages/fcl-web/src/base/README.txt
A packages/fcl-web/src/base/httproute.pp
--- Recording mergeinfo for merge of r35292 into '.':
U .
--- Merging r35296 into '.':
U packages/fcl-json/fpmake.pp
A packages/fcl-json/tests/testjson2code.lpr
A packages/fcl-json/tests/testjson2code.lpi
A packages/fcl-json/tests/tcjsontocode.pp
A packages/fcl-json/src/fpjsontopas.pp
--- Recording mergeinfo for merge of r35296 into '.':
G .
--- Merging r35343 into '.':
U packages/fcl-json/tests/testjsondata.pp
U packages/fcl-json/src/fpjson.pp
--- Recording mergeinfo for merge of r35343 into '.':
G .
--- Merging r35384 into '.':
A utils/pas2js/dist
A utils/pas2js/dist/rtl.js
--- Recording mergeinfo for merge of r35384 into '.':
G .
--- Merging r35416 into '.':
U packages/fcl-js/src/jsbase.pp
U packages/fcl-js/src/jswriter.pp
--- Recording mergeinfo for merge of r35416 into '.':
G .
--- Merging r35417 into '.':
U packages/pastojs/src/fppas2js.pp
U packages/pastojs/tests/tcconverter.pp
U packages/pastojs/tests/tcmodules.pas
--- Recording mergeinfo for merge of r35417 into '.':
G .
--- Merging r35418 into '.':
U utils/pas2js/dist/rtl.js
--- Recording mergeinfo for merge of r35418 into '.':
G .
--- Merging r35429 into '.':
G utils/pas2js/dist/rtl.js
--- Recording mergeinfo for merge of r35429 into '.':
G .
--- Merging r35471 into '.':
G packages/fcl-js/src/jsbase.pp
G packages/fcl-js/src/jswriter.pp
--- Recording mergeinfo for merge of r35471 into '.':
G .

# revisions: 35292,35296,35343,35384,35416,35417,35418,35429,35471

git-svn-id: branches/fixes_3_0@35983 -

marco 8 years ago
parent
commit
be53a5754e
35 changed files with 8662 additions and 521 deletions
  1. 15 0
      .gitattributes
  2. 83 1
      packages/fcl-js/src/jsbase.pp
  3. 28 11
      packages/fcl-js/src/jswriter.pp
  4. 32 21
      packages/fcl-json/fpmake.pp
  5. 87 0
      packages/fcl-json/src/fpjson.pp
  6. 1279 0
      packages/fcl-json/src/fpjsontopas.pp
  7. 2422 0
      packages/fcl-json/tests/tcjsontocode.pp
  8. 70 0
      packages/fcl-json/tests/testjson2code.lpi
  9. 52 0
      packages/fcl-json/tests/testjson2code.lpr
  10. 23 0
      packages/fcl-json/tests/testjsondata.pp
  11. 3 11
      packages/fcl-web/examples/echo/cgi/echo.lpi
  12. BIN
      packages/fcl-web/examples/echo/cgi/echo.res
  13. 22 172
      packages/fcl-web/examples/httpapp/testhttp.lpi
  14. 1 1
      packages/fcl-web/examples/httpapp/testhttp.pp
  15. 1 9
      packages/fcl-web/examples/httpclient/httpget.lpi
  16. 2 11
      packages/fcl-web/examples/httpserver/simplehttpserver.lpi
  17. 22 0
      packages/fcl-web/examples/routing/README
  18. 69 0
      packages/fcl-web/examples/routing/demorouting.lpi
  19. 34 0
      packages/fcl-web/examples/routing/demorouting.lpr
  20. 203 0
      packages/fcl-web/examples/routing/routes.pp
  21. 8 0
      packages/fcl-web/examples/routing/sample.ini
  22. 21 3
      packages/fcl-web/fpmake.pp
  23. 75 0
      packages/fcl-web/src/base/README.txt
  24. 99 37
      packages/fcl-web/src/base/custweb.pp
  25. 67 20
      packages/fcl-web/src/base/fphttp.pp
  26. 21 0
      packages/fcl-web/src/base/httpdefs.pp
  27. 778 0
      packages/fcl-web/src/base/httproute.pp
  28. 346 0
      packages/fcl-web/src/base/tcwebmodule.pp
  29. 971 0
      packages/fcl-web/tests/tchttproute.pp
  30. 71 0
      packages/fcl-web/tests/testfpweb.lpi
  31. 28 0
      packages/fcl-web/tests/testfpweb.lpr
  32. 562 205
      packages/pastojs/src/fppas2js.pp
  33. 3 3
      packages/pastojs/tests/tcconverter.pp
  34. 873 16
      packages/pastojs/tests/tcmodules.pas
  35. 291 0
      utils/pas2js/dist/rtl.js

+ 15 - 0
.gitattributes

@@ -2470,13 +2470,17 @@ packages/fcl-json/fpmake.pp svneol=native#text/plain
 packages/fcl-json/src/README.txt svneol=native#text/plain
 packages/fcl-json/src/fpjson.pp svneol=native#text/plain
 packages/fcl-json/src/fpjsonrtti.pp svneol=native#text/plain
+packages/fcl-json/src/fpjsontopas.pp svneol=native#text/plain
 packages/fcl-json/src/jsonconf.pp svneol=native#text/plain
 packages/fcl-json/src/jsonparser.pp svneol=native#text/plain
 packages/fcl-json/src/jsonscanner.pp svneol=native#text/plain
 packages/fcl-json/tests/jsonconftest.pp svneol=native#text/plain
+packages/fcl-json/tests/tcjsontocode.pp svneol=native#text/plain
 packages/fcl-json/tests/testcomps.pp svneol=native#text/plain
 packages/fcl-json/tests/testjson.lpi svneol=native#text/plain
 packages/fcl-json/tests/testjson.pp svneol=native#text/plain
+packages/fcl-json/tests/testjson2code.lpi svneol=native#text/plain
+packages/fcl-json/tests/testjson2code.lpr svneol=native#text/plain
 packages/fcl-json/tests/testjsonconf.lpi svneol=native#text/plain
 packages/fcl-json/tests/testjsonconf.pp svneol=native#text/plain
 packages/fcl-json/tests/testjsondata.pp svneol=native#text/plain
@@ -3096,6 +3100,11 @@ packages/fcl-web/examples/jsonrpc/extdirect/extdemo.lpr svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/extdirect.in svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/wmext.lfm svneol=native#text/plain
 packages/fcl-web/examples/jsonrpc/extdirect/wmext.pp svneol=native#text/plain
+packages/fcl-web/examples/routing/README svneol=native#text/plain
+packages/fcl-web/examples/routing/demorouting.lpi svneol=native#text/plain
+packages/fcl-web/examples/routing/demorouting.lpr svneol=native#text/plain
+packages/fcl-web/examples/routing/routes.pp svneol=native#text/plain
+packages/fcl-web/examples/routing/sample.ini svneol=native#text/plain
 packages/fcl-web/examples/session/sessiondemo.lpi svneol=native#text/plain
 packages/fcl-web/examples/session/sessiondemo.lpr svneol=native#text/plain
 packages/fcl-web/examples/session/wmsession.lfm svneol=native#text/plain
@@ -3197,9 +3206,11 @@ packages/fcl-web/src/base/fpwebclient.pp svneol=native#text/plain
 packages/fcl-web/src/base/fpwebfile.pp svneol=native#text/plain
 packages/fcl-web/src/base/httpdefs.pp svneol=native#text/plain
 packages/fcl-web/src/base/httpprotocol.pp svneol=native#text/plain
+packages/fcl-web/src/base/httproute.pp svneol=native#text/plain
 packages/fcl-web/src/base/iniwebsession.pp svneol=native#text/plain
 packages/fcl-web/src/base/restbase.pp svneol=native#text/plain
 packages/fcl-web/src/base/restcodegen.pp svneol=native#text/plain
+packages/fcl-web/src/base/tcwebmodule.pp svneol=native#text/plain
 packages/fcl-web/src/base/webpage.pp svneol=native#text/plain
 packages/fcl-web/src/base/websession.pp svneol=native#text/plain
 packages/fcl-web/src/base/webutil.pp svneol=native#text/plain
@@ -3226,8 +3237,11 @@ packages/fcl-web/tests/cgigateway.lpi svneol=native#text/plain
 packages/fcl-web/tests/cgigateway.pp svneol=native#text/plain
 packages/fcl-web/tests/fpcunithpack.lpi svneol=native#text/plain
 packages/fcl-web/tests/fpcunithpack.lpr svneol=native#text/plain
+packages/fcl-web/tests/tchttproute.pp svneol=native#text/plain
 packages/fcl-web/tests/testcgiapp.lpi svneol=native#text/plain
 packages/fcl-web/tests/testcgiapp.pp svneol=native#text/plain
+packages/fcl-web/tests/testfpweb.lpi svneol=native#text/plain
+packages/fcl-web/tests/testfpweb.lpr svneol=native#text/plain
 packages/fcl-web/tests/uhpacktest1.pas svneol=native#text/plain
 packages/fcl-xml/Makefile svneol=native#text/plain
 packages/fcl-xml/Makefile.fpc svneol=native#text/plain
@@ -15704,6 +15718,7 @@ utils/pas2jni/readme.txt svneol=native#text/plain
 utils/pas2jni/writer.pas svneol=native#text/plain
 utils/pas2js/Makefile svneol=native#text/plain
 utils/pas2js/Makefile.fpc svneol=native#text/plain
+utils/pas2js/dist/rtl.js svneol=native#text/plain
 utils/pas2js/fpmake.pp svneol=native#text/plain
 utils/pas2js/pas2js.lpi svneol=native#text/plain
 utils/pas2js/pas2js.pp svneol=native#text/plain

+ 83 - 1
packages/fcl-js/src/jsbase.pp

@@ -26,6 +26,8 @@ Type
   TJSType = (jstUNDEFINED,jstNull,jstBoolean,jstNumber,jstString,jstObject,jstReference,JSTCompletion);
 
   TJSString = UnicodeString;
+  TJSChar = WideChar;
+  TJSPChar = PWideChar;
   TJSNumber = Double;
 
   { TJSValue }
@@ -76,10 +78,90 @@ Type
     Property AsCompletion : TObject Read GetAsCompletion Write SetAsCompletion;
   end;
 
+function IsValidJSIdentifier(Name: TJSString; AllowEscapeSeq: boolean = false): boolean;
+
 implementation
 
-{ TJSValue }
+function IsValidJSIdentifier(Name: TJSString; AllowEscapeSeq: boolean): boolean;
+var
+  p: TJSPChar;
+  i: Integer;
+begin
+  Result:=false;
+  if Name='' then exit;
+  p:=TJSPChar(Name);
+  repeat
+    case p^ of
+    #0:
+      if p-TJSPChar(Name)=length(Name) then
+        exit(true)
+      else
+        exit;
+    '0'..'9':
+      if p=TJSPChar(Name) then
+        exit
+      else
+        inc(p);
+    'a'..'z','A'..'Z','_','$': inc(p);
+    '\':
+      begin
+      if not AllowEscapeSeq then exit;
+      inc(p);
+      if p^='x' then
+        begin
+        // \x00
+        for i:=1 to 2 do
+          begin
+          inc(p);
+          if not (p^ in ['0'..'9','a'..'f','A'..'F']) then exit;
+          end;
+        end
+      else if p^='u' then
+        begin
+        inc(p);
+        if p^='{' then
+          begin
+          // \u{00000}
+          i:=0;
+          repeat
+            inc(p);
+            case p^ of
+            '}': break;
+            '0'..'9': i:=i*16+ord(p^)-ord('0');
+            'a'..'f': i:=i*16+ord(p^)-ord('a')+10;
+            'A'..'F': i:=i*16+ord(p^)-ord('A')+10;
+            else exit;
+            end;
+            if i>$10FFFF then exit;
+          until false;
+          inc(p);
+          end
+        else
+          begin
+          // \u0000
+          for i:=1 to 4 do
+            begin
+            inc(p);
+            if not (p^ in ['0'..'9','a'..'f','A'..'F']) then exit;
+            end;
+          end;
+        end
+      else
+        exit; // unknown sequence
+      end;
+    #$200C,#$200D: inc(p); // zero width non-joiner/joiner
+    #$00AA..#$2000,
+    #$200E..#$D7FF:
+      inc(p); // ToDo: only those with ID_START/ID_CONTINUE see https://codepoints.net/search?IDC=1
+    #$D800..#$DBFF:
+      inc(p,2); // see above
+    else
+      exit;
+    end;
+  until false;
+end;
 
+{ TJSValue }
 
 function TJSValue.GetAsBoolean: Boolean;
 begin

+ 28 - 11
packages/fcl-js/src/jswriter.pp

@@ -83,8 +83,14 @@ Type
     Property AsUnicodeString : UnicodeString Read GetUnicodeString;
   end;
 
+  TJSEscapeQuote = (
+    jseqSingle,
+    jseqDouble,
+    jseqBoth
+    );
 
   { TJSWriter }
+
   TWriteOption = (woCompact,
                   woUseUTF8,
                   woTabIndent,
@@ -151,7 +157,7 @@ Type
     Procedure WritePrimaryExpression(El: TJSPrimaryExpression);virtual;
     Procedure WriteBinary(El: TJSBinary);virtual;
   Public
-    Function EscapeString(const S: TJSString): String;
+    Function EscapeString(const S: TJSString; Quote: TJSEscapeQuote = jseqDouble): String;
     Function JSStringToStr(const S: TJSString): string;
     Constructor Create(AWriter : TTextWriter);
     Constructor Create(Const AFileName : String);
@@ -164,7 +170,7 @@ Type
     Property IndentSize : Byte Read FIndentSize Write FIndentSize;
     Property UseUTF8 : Boolean Read GetUseUTF8;
   end;
-  EJSWriter = CLass(Exception);
+  EJSWriter = Class(Exception);
 
 implementation
 
@@ -380,27 +386,30 @@ begin
     end;
 end;
 
-function TJSWriter.EscapeString(const S: TJSString): String;
+function TJSWriter.EscapeString(const S: TJSString; Quote: TJSEscapeQuote
+  ): String;
 
 Var
   I,J,L : Integer;
-  P : PWideChar;
+  P : TJSPChar;
 
 begin
   I:=1;
   J:=1;
   Result:='';
   L:=Length(S);
-  P:=PWideChar(S);
+  P:=TJSPChar(S);
   While I<=L do
     begin
-    if (P^ in ['"','/','\',#8,#9,#10,#12,#13]) then
+    if (P^ in [#0..#31,'"','''','/','\']) then
       begin
       Result:=Result+JSStringToStr(Copy(S,J,I-J));
       Case P^ of
         '\' : Result:=Result+'\\';
         '/' : Result:=Result+'\/';
-        '"' : Result:=Result+'\"';
+        '"' : if Quote=jseqSingle then Result:=Result+'"' else Result:=Result+'\"';
+        '''': if Quote=jseqDouble then Result:=Result+'''' else Result:=Result+'\''';
+        #0..#7,#11,#14..#31: Result:=Result+'\x'+hexStr(ord(P^),2);
         #8  : Result:=Result+'\b';
         #9  : Result:=Result+'\t';
         #10 : Result:=Result+'\n';
@@ -427,6 +436,7 @@ procedure TJSWriter.WriteValue(V: TJSValue);
 
 Var
   S : String;
+  JS: TJSString;
 begin
   if V.CustomValue<>'' then
     S:=JSStringToStr(V.CustomValue)
@@ -435,7 +445,14 @@ begin
       jstUNDEFINED : S:='undefined';
       jstNull : s:='null';
       jstBoolean : if V.AsBoolean then s:='true' else s:='false';
-      jstString : S:='"'+EscapeString(V.AsString)+'"';
+      jstString :
+        begin
+        JS:=V.AsString;
+        if Pos('"',JS)>0 then
+          S:=''''+EscapeString(JS,jseqSingle)+''''
+        else
+          S:='"'+EscapeString(JS,jseqDouble)+'"';
+        end;
       jstNumber :
         if Frac(V.AsNumber)=0 then // this needs to be improved
           Str(Round(V.AsNumber),S)
@@ -544,10 +561,10 @@ procedure TJSWriter.WriteRegularExpressionLiteral(
 
 begin
   Write('/');
-  Write(EscapeString(El.Pattern.AsString));
+  Write(EscapeString(El.Pattern.AsString,jseqBoth));
   Write('/');
   If Assigned(El.PatternFlags) then
-    Write(EscapeString(El.PatternFlags.AsString));
+    Write(EscapeString(El.PatternFlags.AsString,jseqBoth));
 end;
 
 procedure TJSWriter.WriteLiteral(El: TJSLiteral);
@@ -642,7 +659,7 @@ begin
   For I:=0 to C do
    begin
    S:=El.Elements[i].Name;
-   if QE then
+   if QE or not IsValidJSIdentifier(S) then
      S:='"'+S+'"';
    Write(S+': ');
    Indent;

+ 32 - 21
packages/fcl-json/fpmake.pp

@@ -31,31 +31,42 @@ begin
     P.SourcePath.Add('src');
 
     T:=P.Targets.AddUnit('fpjson.pp');
-      T.ResourceStrings:=true;
+    T.ResourceStrings:=true;
+
     T:=P.Targets.AddUnit('jsonconf.pp');
-      T.ResourceStrings:=true;
-      with T.Dependencies do
-        begin
-          AddUnit('fpjson');
-          AddUnit('jsonparser');
-        end;
+    T.ResourceStrings:=true;
+    with T.Dependencies do
+      begin
+      AddUnit('fpjson');
+      AddUnit('jsonparser');
+     end;
+        
     T:=P.Targets.AddUnit('jsonparser.pp');
-      T.ResourceStrings:=true;
-      with T.Dependencies do
-        begin
-          AddUnit('fpjson');
-          AddUnit('jsonscanner');
-        end;
+    T.ResourceStrings:=true;
+    with T.Dependencies do
+      begin
+      AddUnit('fpjson');
+      AddUnit('jsonscanner');
+      end;
+        
     T:=P.Targets.AddUnit('jsonscanner.pp');
-      T.ResourceStrings:=true;
+    T.ResourceStrings:=true;
+    
     T:=P.Targets.AddUnit('fpjsonrtti.pp');
-      T.ResourceStrings:=true;
-      with T.Dependencies do
-        begin
-          AddUnit('fpjson');
-          AddUnit('jsonparser');
-        end;
-      T.ResourceStrings:=true;
+    T.ResourceStrings:=true;
+    with T.Dependencies do
+      begin
+      AddUnit('fpjson');
+      AddUnit('jsonparser');
+      end;
+      
+    T:=P.Targets.AddUnit('fpjsontopas.pp');
+    T.ResourceStrings:=true;
+    with T.Dependencies do
+      begin
+      AddUnit('fpjson');
+      AddUnit('jsonparser');
+      end;
 
     P.ExamplePath.Add('examples');
     T:=P.Targets.AddExampleProgram('confdemo.pp');

+ 87 - 0
packages/fcl-json/src/fpjson.pp

@@ -537,6 +537,12 @@ Type
     Function IndexOfName(const AName: TJSONStringType; CaseInsensitive : Boolean = False): Integer;
     Function Find(Const AName : String) : TJSONData; overload;
     Function Find(Const AName : String; AType : TJSONType) : TJSONData; overload;
+    function Find(const key: TJSONStringType; out AValue: TJSONData): boolean;
+    function Find(const key: TJSONStringType; out AValue: TJSONObject): boolean;
+    function Find(const key: TJSONStringType; out AValue: TJSONArray): boolean;
+    function Find(const key: TJSONStringType; out AValue: TJSONString): boolean;
+    function Find(const key: TJSONStringType; out AValue: TJSONBoolean): boolean;
+    function Find(const key: TJSONStringType; out AValue: TJSONNumber): boolean;
     Function Get(Const AName : String) : Variant;
     Function Get(Const AName : String; ADefault : TJSONFloat) : TJSONFloat;
     Function Get(Const AName : String; ADefault : Integer) : Integer;
@@ -3155,6 +3161,87 @@ begin
     Result:=Nil;
 end;
 
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONData): boolean;
+begin
+  AValue := Find(key);
+  result := assigned(AValue);
+end;
+
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONObject): boolean;
+var
+  v: TJSONData;
+begin
+  v := Find(key);
+  if assigned(v) then
+  begin
+    result := v.JSONType = jtObject;
+    if result then
+      AValue := TJSONObject(v);
+  end
+  else
+    result := false;
+end;
+
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONArray): boolean;
+var
+  v: TJSONData;
+begin
+  v := Find(key);
+  if assigned(v) then
+  begin
+    result := v.JSONType = jtArray;
+    if result then
+      AValue := TJSONArray(v);
+  end
+  else
+    result := false;
+end;
+
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONString): boolean;
+var
+  v: TJSONData;
+begin
+  v := Find(key);
+  if assigned(v) then
+  begin
+    result := v.JSONType = jtString;
+    if result then
+      AValue := TJSONString(v);
+  end
+  else
+    result := false;
+end;
+
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONBoolean): boolean;
+var
+  v: TJSONData;
+begin
+  v := Find(key);
+  if assigned(v) then
+  begin
+    result := v.JSONType = jtBoolean;
+    if result then
+      AValue := TJSONBoolean(v);
+  end
+  else
+    result := false;
+end;
+
+function TJSONObject.Find(const key: TJSONStringType; out AValue: TJSONNumber): boolean;
+var
+  v: TJSONData;
+begin
+  v := Find(key);
+  if assigned(v) then
+  begin
+    result := v.JSONType = jtNumber;
+    if result then
+      AValue := TJSONNumber(v);
+  end
+  else
+    result := false;
+end;
+
 initialization
   // Need to force initialization;
   TJSONData.DetermineElementSeparators;

+ 1279 - 0
packages/fcl-json/src/fpjsontopas.pp

@@ -0,0 +1,1279 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2016 by Michael Van Canneyt
+
+    Converter unit to convert JSON object to object pascal classes.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************}
+
+unit fpjsontopas;
+
+// TODO : Array of Array LoadFromJSON/SaveToJSON
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpjson, jsonparser;
+
+Type
+  EJSONToPascal = Class(EJSON);
+
+  { TPropertyMapItem }
+  TPropertyMapItem = Class(TCollectionItem)
+  private
+    FGenerated: Boolean;
+    FJSONType: TJSONType;
+    FParentTypeName: String;
+    FPath: String;
+    FPropertyName: String;
+    FSkipType: Boolean;
+    FTypeName: String;
+  Public
+    Procedure Assign(Source: TPersistent); override;
+    Property Generated : Boolean Read FGenerated;
+  Published
+    Property Path : String Read FPath Write FPath;
+    Property TypeName : String Read FTypeName Write FTypeName;
+    Property ParentTypeName : String Read FParentTypeName Write FParentTypeName;
+    Property PropertyName : String Read FPropertyName Write FPropertyName;
+    Property JSONType : TJSONType Read FJSONType write FJSONType;
+    // Set this to true if no class/array should be generated
+    Property SkipType : Boolean Read FSkipType Write FSkipType;
+  end;
+
+  TPropertyMap = Class(TCollection)
+  private
+    function GetM(Aindex : Integer): TPropertyMapItem;
+    procedure SetM(Aindex : Integer; AValue: TPropertyMapItem);
+  Public
+    Function AddPath(Const APath,ATypeName : String) : TPropertyMapItem;
+    Function IndexOfPath(Const APath : String) : Integer;
+    Function FindPath(Const APath : String) : TPropertyMapItem;
+    Property Map[Aindex : Integer] : TPropertyMapItem Read GetM Write SetM; Default;
+  end;
+
+  { TJSONToPascal }
+  TJSONToPascalOption = (jpoUseSetter,jpoGenerateLoad,jpoUnknownLoadPropsError,jpoDelphiJSON, jpoLoadCaseInsensitive,jpoGenerateSave);
+  TJSONToPascalOptions = set of TJSONToPascalOption;
+
+  TJSONToPascal = Class(TComponent)
+  private
+    FExtraUnitNames: String;
+    FFieldPrefix: String;
+    FIndent : String;
+    FActive : Boolean;
+    FCode : TStrings;
+    FDefaultParentName : String;
+    FDestUnitName : String;
+    FIndentSize : Integer;
+    FJSON : TJSONStringType;
+    FJSONData: TJSONData;
+    FJSONStream: TStream;
+    FObjectConstructorArguments: String;
+    FOptions: TJSONToPascalOptions;
+    FPropertyMap: TPropertyMap;
+    FPropertyTypeSuffix: String;
+    FinType : Boolean; //  State
+    procedure GenerateSaveFunctionForm(M: TPropertyMapItem);
+    function GetObjectConstructorArguments: String;
+    function JSONDataName: String;
+    procedure MaybeEmitType;
+    procedure SetActive(AValue: Boolean);
+    procedure SetCode(AValue: TStrings);
+    procedure SetJSON(AValue: TJSONStringType);
+    procedure SetPropertyMap(AValue: TPropertyMap);
+  Protected
+    Procedure AddSemiColonToLastLine;
+    Procedure Indent;
+    Procedure Undent;
+    Procedure AddLn(Const Line : String);
+    Procedure AddLn(Const Fmt : String; Const Args : Array of const);
+    Procedure AddIndented(Const Line : String);
+    Procedure AddIndented(Const Fmt : String; Const Args : Array of const);
+    Function CreatePropertyMap : TPropertyMap; virtual;
+    Function GetJSONData(Out FreeResult : Boolean) : TJSONData; virtual;
+    function IsDateTimeValue(const AValue: String): Boolean; virtual;
+    Function GetDefaultParentName : String;
+    function GetPropertyTypeName(const APath, AName: String; AValue: TJSONData): String; virtual;
+    function PathToTypeName(const APath: String): String; virtual;
+    function AddToPath(const APath, AName: String): String;
+    class function CleanPropertyName(const AName: String): string;
+    function GetPropertyName(const APath, AName: String): String;
+
+    // Called for each type
+    function  GenerateAssign(IM: TPropertyMapItem; AVarName, AJSONName: String ): String;
+    function  GenerateAssignDelphi(IM: TPropertyMapItem; AVarName, AJSONName: String; AddSemiColon : Boolean ): String;
+    procedure GenerateCreateArray(M: TPropertyMapItem);
+    procedure GenerateSaveArray(M: TPropertyMapItem);
+    procedure GenerateCreateObjectfpJSON(M: TPropertyMapItem);
+    procedure GenerateLoadJSONDelphi(M: TPropertyMapItem; J: TJSONObject);
+    procedure GenerateLoadJSONfpJSON(M: TPropertyMapItem; J: TJSONObject);
+    procedure GenerateSaveJSONDelphi(M: TPropertyMapItem; J: TJSONObject);
+    procedure GenerateSaveJSONfpJSON(M: TPropertyMapItem; J: TJSONObject);
+    Function  GenerateArrayDeclaration(M: TPropertyMapItem; J: TJSONArray) : Boolean; virtual;
+    procedure GenerateObjectDeclaration(M: TPropertyMapItem;  J: TJSONObject); virtual;
+    procedure GenerateArrayImplementation(M : TPropertyMapItem; J: TJSONArray); virtual;
+    procedure GenerateObjectImplementation(M : TPropertyMapItem; J: TJSONObject); virtual;
+    // Top level routines
+    Function  GetExtraUnitNames : String; virtual;
+    Procedure ClearGeneratedTypes;virtual;
+    Procedure GenerateInterfaceHeader;virtual;
+    procedure GenerateDeclaration(const APath : String; J: TJSONData);  virtual;
+    Procedure GenerateImplementationHeader;virtual;
+    Procedure GenerateImplementation(const APath: String; J: TJSONData); virtual;
+    Procedure GenerateImplementationEnd;virtual;
+  Public
+    Constructor Create(AOwner : TComponent); override;
+    Destructor Destroy; override;
+    Procedure Execute;
+    // JSON Data to generate code from.
+    Property JSONData : TJSONData Read FJSONData Write FJSONData;
+    // JSON Data (in stream form) to generate code from. JSONData takes prioroty over this property.
+    Property JSONStream : TStream Read FJSONStream Write FJSONStream;
+  Published
+    // Setting this to true will call execute. Can be used to generate code in the IDE.
+    Property Active : Boolean Read FActive Write SetActive;
+    // Options to use.
+    Property Options : TJSONToPascalOptions Read FOptions Write FOptions;
+    // The JSON to use. JSONData/JSONStream take priority over this property.
+    Property JSON : TJSONStringType Read FJSON Write SetJSON;
+    // This string
+    Property Code : TStrings Read FCode Write SetCode;
+    // Type information for generated types. After Execute, this will contain generated/detected types for all properties.
+    Property PropertyMap : TPropertyMap Read FPropertyMap Write SetPropertyMap;
+    // Generated unit name.
+    Property DestUnitName : String Read FDestUnitName Write FDestUnitName;
+    // Default Parent class name when declaring objects. Can be overridden per property.
+    Property DefaultParentName: String Read FDefaultParentName Write FDefaultParentName;
+    // Indent size
+    Property IndentSize : Integer Read FIndentSize Write FIndentSize default 2;
+    // These units (comma separated list) will be added to the interface uses clause.
+    Property ExtraUnitNames : String Read FExtraUnitNames Write FExtraUnitNames;
+    // This will be suffixed to an object/array type name when the propert map is constructed.
+    Property PropertyTypeSuffix : String Read FPropertyTypeSuffix Write FPropertyTypeSuffix;
+    // First letter for field name.
+    Property FieldPrefix : String Read FFieldPrefix Write FFieldPrefix;
+    // What are the arguments to a constructor ? This property is inserted literally in the code between ().
+    Property ObjectConstructorArguments : String Read FObjectConstructorArguments Write FObjectConstructorArguments;
+  end;
+
+
+
+implementation
+
+{$IFDEF VER2_6_4}
+Const
+  StructuredJSONTypes  = [jtArray,jtObject];
+{$ENDIF}
+
+{ TPropertyMap }
+
+function TPropertyMap.GetM(Aindex : Integer): TPropertyMapItem;
+begin
+  Result:=Items[AIndex] as TPropertyMapItem;
+end;
+
+procedure TPropertyMap.SetM(Aindex : Integer; AValue: TPropertyMapItem);
+begin
+  Items[AIndex]:=AValue;
+end;
+
+function TPropertyMap.AddPath(const APath, ATypeName: String): TPropertyMapItem;
+begin
+  Result:=Add as TPropertyMapItem;
+  Result.Path:=APath;
+  Result.TypeName:=ATypeName;
+end;
+
+function TPropertyMap.IndexOfPath(const APath: String): Integer;
+begin
+  Result:=Count-1;
+  While (Result>=0) and (GetM(Result).Path<>APath) do
+    Dec(Result);
+end;
+
+function TPropertyMap.FindPath(const APath: String): TPropertyMapItem;
+
+Var
+  I : Integer;
+
+begin
+  I:=IndexOfPath(APath);
+  If I=-1 then
+    Result:=Nil
+  else
+    Result:=GetM(I);
+end;
+
+{ TJSONToPascal }
+
+class function TJSONToPascal.CleanPropertyName(const AName: String): string;
+
+Const
+   KW=';absolute;and;array;asm;begin;case;const;constructor;destructor;div;do;'+
+       'downto;else;end;file;for;function;goto;if;implementation;in;inherited;'+
+       'inline;interface;label;mod;nil;not;object;of;on;operator;or;packed;'+
+       'procedure;program;record;reintroduce;repeat;self;set;shl;shr;string;then;'+
+       'to;type;unit;until;uses;var;while;with;xor;dispose;exit;false;new;true;'+
+       'as;class;dispinterface;except;exports;finalization;finally;initialization;'+
+       'inline;is;library;on;out;packed;property;raise;resourcestring;threadvar;try;'+
+       'private;published;length;setlength;';
+Var
+  I : Integer;
+
+begin
+  Result:=Aname;
+  For I:=Length(Result) downto 1 do
+    If Not ((Upcase(Result[i]) in ['_','A'..'Z'])
+             or ((I>1) and (Result[i] in (['0'..'9'])))) then
+     Delete(Result,i,1);
+  if Pos(';'+lowercase(Result)+';',KW)<>0 then
+   Result:='_'+Result
+end;
+
+procedure TJSONToPascal.SetActive(AValue: Boolean);
+begin
+  if (FActive=AValue) then Exit;
+  if AValue then
+    Execute;
+end;
+
+procedure TJSONToPascal.SetCode(AValue: TStrings);
+begin
+  if FCode=AValue then Exit;
+  FCode.Assign(AValue);
+end;
+
+procedure TJSONToPascal.SetJSON(AValue: TJSONStringType);
+begin
+  if FJSON=AValue then Exit;
+  FJSON:=AValue;
+end;
+
+procedure TJSONToPascal.SetPropertyMap(AValue: TPropertyMap);
+begin
+  if FPropertyMap=AValue then Exit;
+  FPropertyMap.Assign(AValue);
+end;
+
+procedure TJSONToPascal.AddSemiColonToLastLine;
+
+Var
+  I : Integer;
+
+begin
+  I:=FCode.Count-1;
+  FCode[I]:=FCode[I]+';'
+end;
+
+procedure TJSONToPascal.Indent;
+begin
+  FIndent:=Findent+StringOfChar(' ',FIndentSize);
+end;
+
+procedure TJSONToPascal.Undent;
+
+Var
+  L : Integer;
+
+begin
+  L:=Length(FIndent);
+  Dec(L,FIndentSize);
+  if L<0 then L:=0;
+  FIndent:=Copy(FIndent,1,L);
+end;
+
+procedure TJSONToPascal.AddLn(const Line: String);
+begin
+  FCode.Add(FIndent+Line);
+end;
+
+procedure TJSONToPascal.AddLn(const Fmt: String; const Args: array of const);
+begin
+  AddLn(Format(Fmt,Args));
+end;
+
+procedure TJSONToPascal.AddIndented(const Line: String);
+begin
+  Indent;
+  AddLn(Line);
+  Undent;
+end;
+
+procedure TJSONToPascal.AddIndented(const Fmt: String;
+  const Args: array of const);
+begin
+  Indent;
+  AddLn(Fmt,Args);
+  Undent;
+end;
+
+constructor TJSONToPascal.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+  FCode:=TStringList.Create;
+  FPropertyMap:=CreatePropertyMap;
+  FIndentSize:=2;
+  FFieldPrefix:='F';
+end;
+
+destructor TJSONToPascal.Destroy;
+begin
+  FreeAndNil(FCode);
+  FreeAndNil(FPropertyMap);
+  inherited Destroy;
+end;
+
+function TJSONToPascal.CreatePropertyMap: TPropertyMap;
+
+begin
+  Result:=TPropertyMap.Create(TPropertyMapItem);
+end;
+
+function TJSONToPascal.GetJSONData(out FreeResult: Boolean): TJSONData;
+
+Var
+  D : TJSONData;
+
+begin
+  FreeResult:=not Assigned(FJSONData);
+  if Not FreeResult then
+    Exit(FJSONData);
+  Result:=Nil;
+  If Assigned(JSONStream) then
+    D:=GetJSON(JSONStream)
+  else if (JSON<>'') then
+    D:=GetJSON(JSON)
+  else
+    Raise EJSONToPascal.Create('Need one of JSONObject, JSONStream or JSON to be set');
+  If Not (D.JSONType in [jtObject,jtArray]) then
+    begin
+    FreeAndNil(D);
+    Raise EJSONToPascal.Create('Provided JSONStream or JSON is not a JSON Object or array');
+    end;
+  Result:=D;
+end;
+
+function TJSONToPascal.GetExtraUnitNames: String;
+begin
+  Result:=FExtraUnitNames;
+end;
+
+procedure TJSONToPascal.ClearGeneratedTypes;
+
+Var
+  I : integer;
+
+begin
+  For i:=FPropertyMap.Count-1 downto 0 do
+    if FPropertyMap[i].Generated then
+      FPropertyMap.Delete(I);
+end;
+
+procedure TJSONToPascal.GenerateInterfaceHeader;
+
+Var
+  S: string;
+begin
+  AddLn('unit %s;',[DestUnitName]);
+  Addln('');
+  Addln('interface');
+  Addln('');
+  S:=Trim(GetExtraUnitNames);
+  if (S<>'') and (S[1]<>',') then
+    S:=', '+S;
+  if jpoDelphiJSON in Options then
+    S:='JSON'+S
+  else
+    S:='fpJSON'+S;
+  S:='SysUtils, Classes, '+S;
+  Addln('uses %s;',[s]);
+  Addln('');
+end;
+
+
+function TJSONToPascal.PathToTypeName(const APath: String): String;
+
+begin
+  Result:=StringReplace(Apath,'.','',[rfReplaceAll]);
+  Result:=StringReplace(Result,'[0]','Item',[rfReplaceAll]);
+  Result:=StringReplace(Result,'[]','Item',[rfReplaceAll]);
+  if Result='' then
+    Result:='TMyObject'
+  else
+    Result:='T'+Result+PropertyTypeSuffix;
+end;
+
+function TJSONToPascal.IsDateTimeValue(const AValue: String): Boolean;
+
+Var
+  D : TDateTime;
+
+begin
+  Result:=TryStrToDate(AValue,D);
+  if Not Result then
+    Result:=TryStrToTime(AValue,D);
+  if Not Result then
+    Result:=TryStrToDateTime(AValue,D);
+end;
+
+function TJSONToPascal.GetDefaultParentName: String;
+begin
+  Result:=FDefaultParentName;
+  if Result='' then
+    Result:='TObject';
+end;
+
+Resourcestring
+  SErrCannotDetermineType = 'Cannot determine type for %s : Not in type map';
+  SErrCannotDeterminePropertyType = 'Cannot determine property type for %s';
+  SErrCannotGenerateArrayDeclaration = 'Cannot generate array declaration from empty array at "%s"';
+
+function TJSONToPascal.GetPropertyTypeName(const APath, AName: String; AValue: TJSONData): String;
+
+Var
+  M : TPropertyMapItem;
+  IP : String;
+
+begin
+  Case AValue.JSONType of
+    jtBoolean : Result:='Boolean';
+    jtNull : Result:='Boolean';
+    jtNumber :
+      Case TJSONNumber(AValue).NumberType of
+        ntFloat : Result:='Double';
+        ntInt64 : Result:='Int64';
+        ntInteger : Result:='Integer';
+      end;
+    jtString :
+      if not IsDateTimeValue(AValue.AsString) then
+        Result:='String'
+      else
+        Result:='TDateTime';
+    jtArray:
+      begin
+      IP:=AddToPath(APath,AName);
+      M:=FPropertyMap.FindPath(IP);
+      If (M=Nil) then
+        raise EJSONToPascal.CreateFmt(SErrCannotDetermineType, [IP]);
+      if M.TypeName='' then
+        M.TypeName:='Array of '+GetPropertyTypeName(AddToPath(APath,AName)+'[0]','Item',TJSONArray(AValue)[0]);
+      Result:=M.TypeName;
+      end;
+    jtObject :
+      begin
+      M:=FPropertyMap.FindPath(AddToPath(APath,AName));
+      If (M=Nil) then // Can happen in case of [ [ {} ] ]
+        M:=FPropertyMap.AddPath(AddToPath(APath,AName),'');
+//        Raise EJSONToPascal.CreateFmt('Cannot determine type for %s.%s : Not in type map',[APath,AName]);
+      if M.TypeName='' then
+        M.TypeName:=PathToTypeName(AddToPath(APath,AName));
+      if M.ParentTypeName='' then
+         M.ParentTypeName:=GetDefaultParentName;
+      Result:=M.TypeName;
+      end;
+  end;
+end;
+
+function TJSONToPascal.GetPropertyName(const APath, AName: String): String;
+
+begin
+  Result:=CleanPropertyName(AName);
+end;
+
+function TJSONToPascal.JSONDataName: String;
+
+begin
+  if jpoDelphiJSON in options then
+    Result:='TJSONValue'
+  else
+    Result:='TJSONData';
+end;
+
+function TJSONToPascal.GenerateArrayDeclaration(M: TPropertyMapItem;
+  J: TJSONArray): Boolean;
+
+Var
+  IP : String;
+  IM : TPropertyMapItem;
+  B : Boolean;
+
+begin
+  Result:=False;
+  IP:=AddToPath(M.Path,'[0]');
+  IM:=FPropertyMap.FindPath(IP);
+  AddLn('%s = Array of %s;',[M.TypeName,IM.TypeName]);
+  B:=([jpoGenerateLoad,jpoGenerateSave] * options)<>[];
+  if B then
+    begin
+    Undent;
+    AddLn('');
+    end;
+  if jpoGenerateLoad in options then
+    AddLn('Function Create%s(AJSON : %s) : %s;',[M.TypeName,JSONDataName,M.TypeName]);
+  if jpoGenerateSave in options then
+    begin
+    AddLn('Procedure Save%sToJSON(AnArray : %s; AJSONArray : TJSONArray); overload;',[M.TypeName,M.TypeName]);
+    AddLn('Function Save%sToJSON(AnArray : %s) : TJSONArray; overload;',[M.TypeName,M.TypeName]);
+    end;
+  AddLn('');
+  if B then
+    begin
+    Indent;
+    FinType:=False;
+    Result:=True;
+    end;
+end;
+
+procedure TJSONToPascal.GenerateObjectDeclaration(M : TPropertyMapItem; J: TJSONObject);
+
+Var
+  E : TJSONEnum;
+  IM :  TPropertyMapItem;
+  IP, FRN,FWN : String;
+  HaveObj : Boolean;
+
+begin
+  HaveObj:=False;
+  Addln('');
+  AddLn('{ -----------------------------------------------------------------------');
+  Addln('  '+M.TypeName);
+  AddLn('  -----------------------------------------------------------------------}');
+  Addln('');
+  AddLn('%s = class(%s)',[M.TypeName,M.ParentTypeName]);
+  Addln('Private');
+  Indent;
+  For E in J do
+    begin
+    IM:=FPropertyMap.FindPath(AddToPath(M.Path,E.Key));
+    If IM=Nil then
+      begin
+      IM:=FPropertyMap.Add as TPropertyMapItem;
+      IM.Path:=AddToPath(M.Path,E.Key);
+      IM.FGenerated:=True;
+      end;
+    if IM.TypeName='' then
+      IM.TypeName:=GetPropertyTypeName(M.Path,E.Key,E.Value);
+    if IM.PropertyName='' then
+      IM.PropertyName:=GetPropertyName(M.Path,E.Key);
+    IM.JSONType:=E.Value.JSONtype;
+    AddLn('F%s : %s;',[IM.PropertyName,IM.TypeName]);
+    HaveObj:=HaveObj or (IM.JSONType=jtObject);
+    end;
+  Undent;
+  if jpoUseSetter in Options then
+    begin
+    Addln('Protected');
+    Indent;
+    For E in J do
+      begin
+      IM:=FPropertyMap.FindPath(AddToPath(M.Path,E.Key));
+      If IM=Nil then
+        raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [AddToPath(M.Path, E.Key)]);
+      FRN:=FieldPrefix+IM.PropertyName;
+      AddLn('Procedure Set%s(AValue : %s); virtual;',[IM.PropertyName,IM.TypeName]);
+      end;
+    Undent;
+    end;
+  Addln('Public');
+  Indent;
+  if HaveObj then
+    AddLn('Destructor Destroy; override;');
+  if jpoGenerateLoad in options then
+    begin
+    AddLn('Constructor CreateFromJSON(AJSON : %s); virtual;',[JSONDataName]);
+    AddLn('Procedure LoadFromJSON(AJSON : %s); virtual;',[JSONDataName]);
+    end;
+  if jpoGenerateSave in options then
+    begin
+    AddLn('Function SaveToJSON : TJSONObject; overload;');
+    AddLn('Procedure SaveToJSON(AJSON : TJSONObject); overload; virtual;');
+    end;
+
+  For E in J do
+    begin
+    IP:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(IP);
+    If IM=Nil then
+      raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [IP]);
+    FRN:=FieldPrefix+IM.PropertyName;
+    if jpoUseSetter in Options then
+      FWN:='Set'+IM.PropertyName
+    else
+      FWN:=FRN;
+    AddLn('Property %s : %s Read %s Write %s;',[IM.PropertyName,IM.TypeName,FRN, FWN]);
+    end;
+  Undent;
+  AddLn('end;');
+end;
+
+function TJSONToPascal.AddToPath(const APath, AName: String): String;
+
+begin
+  Result:=APath;
+  if (AName<>'') then
+    begin
+    if (Result<>'') and (AName[1]<>'[') then
+      Result:=Result+'.';
+    Result:=Result+AName;
+    end;
+end;
+
+procedure TJSONToPascal.MaybeEmitType;
+
+begin
+  if FinType then exit;
+  Undent;
+  AddLn('Type');
+  Indent;
+  FinType:=True;
+end;
+
+procedure TJSONToPascal.GenerateDeclaration(const APath: String;J: TJSONData);
+
+Var
+  M :  TPropertyMapItem;
+  O : TJSONEnum;
+  IP : String;
+
+begin
+  AddLn('');
+  MaybeEmitType;
+  M:=FPropertyMap.FindPath(APath);
+  If M=Nil then
+    begin
+    M:=FPropertyMap.Add as TPropertyMapItem;
+    M.Path:=APath;
+    M.FGenerated:=True;
+    end
+  else if M.SkipType then
+    exit;
+  if (M.TypeName='') then
+    if J.JSONType in StructuredJSONtypes then
+      M.TypeName:=PathToTypeName(APath)
+    else
+      M.TypeName:=GetPropertyTypeName(APath,'',J);
+  M.JSONType:=J.JSONType;
+  if J is TJSONArray then
+    begin
+    M.ParentTypeName:='';
+    if J.Count=0 then
+      raise EJSONToPascal.CreateFmt(SErrCannotGenerateArrayDeclaration, [APath]);
+    IP:=AddToPath(M.Path,'[0]');
+    GenerateDeclaration(IP,J.Items[0]);
+    MaybeEmitType;
+    GenerateArrayDeclaration(M,TJSONarray(J));
+    end
+  else if J is TJSONObject then
+    begin
+    For O in TJSONOBject(J) do
+      begin
+      IP:=AddToPath(APath,O.Key);
+      GenerateDeclaration(IP,O.Value);
+      end;
+    M.ParentTypeName:=GetDefaultParentName;
+    MaybeEmitType;
+    GenerateObjectDeclaration(M,TJSONObject(J));
+    end;
+end;
+
+procedure TJSONToPascal.GenerateImplementationHeader;
+begin
+  Addln('');
+  Addln('implementation');
+  Addln('');
+end;
+
+procedure TJSONToPascal.GenerateArrayImplementation(M : TPropertyMapItem; J: TJSONArray);
+
+Var
+  IM : TPropertyMapItem;
+  P : String;
+
+begin
+  P:=AddToPath(M.Path,'[0]');
+  IM:=FPropertyMap.FindPath(P);
+  if J.Items[0] is TJSONObject then
+    GenerateObjectImplementation(IM,J.Items[0] as TJSONObject)
+  else if J.Items[0] is TJSONArray then
+    GenerateArrayImplementation(IM,J.Items[0] as TJSONArray);
+  if jpoGenerateLoad in Options then
+    GenerateCreateArray(M);
+  if jpoGenerateSave in Options then
+    GenerateSaveArray(M)
+  // Do nothing yet
+end;
+
+procedure TJSONToPascal.GenerateCreateArray(M : TPropertyMapItem);
+
+Var
+  IP : String;
+  IM : TPropertyMapItem;
+
+begin
+  IP:=AddToPath(M.Path,'[0]');
+  IM:=FPropertyMap.FindPath(IP);
+  AddLn('');
+  AddLn('Function Create%s(AJSON : %s) : %s;',[M.TypeName,JSONDataName,M.TypeName]);
+  AddLn('');
+  AddLn('var');
+  AddIndented('I : integer;');
+  if (jpoDelphiJSON in Options) then
+    AddIndented('A : TJSONArray;');
+  AddLn('');
+  AddLn('begin');
+  Indent;
+  if not (jpoDelphiJSON in Options) then
+    begin
+    AddLn('SetLength(Result,AJSON.Count);');
+    AddLn('For I:=0 to AJSON.Count-1 do');
+    AddIndented(GenerateAssign(IM,'Result[i]','AJSON.Items[i]'));
+    end
+  else
+    begin
+    AddLn('A:=AJSON as TJSONArray;');
+    AddLn('SetLength(Result,A.Count);');
+    AddLn('For I:=0 to A.Count-1 do');
+    AddIndented(GenerateAssignDelphi(IM,'Result[i]','A.Items[i]',True));
+    end;
+  Undent;
+  Addln('End;');
+  AddLn('');
+end;
+
+procedure TJSONToPascal.GenerateSaveArray(M : TPropertyMapItem);
+
+Var
+  IP : String;
+  IM : TPropertyMapItem;
+
+begin
+  IP:=AddToPath(M.Path,'[0]');
+  IM:=FPropertyMap.FindPath(IP);
+  AddLn('');
+  AddLn('Function Save%sToJSON(AnArray : %s) : TJSONArray;',[M.TypeName,M.TypeName]);
+  AddLn('begin');
+  Indent;
+  Addln('Result:=TJSONArray.Create;');
+  Addln('Try');
+  AddIndented('Save%sToJSON(AnArray,Result);',[M.TypeName]);
+  Addln('Except');
+  Indent;
+  Addln('FreeAndNil(Result);');
+  Addln('Raise;');
+  Undent;
+  Addln('end;');
+  Undent;
+  Addln('end;');
+  AddLn('');
+  AddLn('');
+  AddLn('Procedure Save%sToJSON(AnArray : %s; AJSONArray : TJSONArray);',[M.TypeName,M.TypeName]);
+  AddLn('');
+  AddLn('var');
+  AddIndented('I : integer;');
+  AddLn('');
+  AddLn('begin');
+  Indent;
+  AddLn('For I:=0 to Length(AnArray)-1 do');
+  Case IM.JSONType of
+    jtObject : AddIndented('AJSONArray.Add(AnArray[i].SaveToJSON);');
+    jtArray :  AddIndented('AJSONArray.Add(Save%sToJSON(AnArray[i]));',[IM.TypeName]);
+  else
+    AddIndented('AJSONArray.Add(AnArray[i]);');
+  end;
+  Undent;
+  Addln('end;');
+  AddLn('');
+end;
+
+function TJSONToPascal.GetObjectConstructorArguments: String;
+
+begin
+  Result:=ObjectConstructorArguments
+end;
+
+procedure TJSONToPascal.GenerateCreateObjectfpJSON(M : TPropertyMapItem);
+
+Var
+  IP : String;
+  IM : TPropertyMapItem;
+
+begin
+  IP:=AddToPath(M.Path,'[0]');
+  IM:=FPropertyMap.FindPath(IP);
+  AddLn('');
+  Indent;
+  AddLn('Function CreateObject%s(AnObject : TJSONData) : %s;',[M.TypeName,M.TypeName]);
+  AddLn('');
+  AddLn('begin');
+  Indent;
+  AddLn('Result:='+M.TypeName+'.Create('+GetObjectConstructorArguments+');');
+  AddLn('Result.LoadFromJSON(AnObject);');
+  Undent;
+  Addln('End;');
+  Undent;
+  AddLn('');
+end;
+
+procedure TJSONToPascal.GenerateLoadJSONDelphi(M: TPropertyMapItem;
+  J: TJSONObject);
+Var
+  IM :  TPropertyMapItem;
+  E : TJSONEnum;
+  P,K : String;
+  SElse : String;
+
+begin
+  AddLn('Procedure %s.LoadFromJSON(AJSON : TJSONValue);',[M.TypeName]);
+  Addln('');
+  Addln('var');
+  AddIndented('P : TJSONPair;');
+  AddIndented('O : TJSONObject;');
+  AddIndented('PN : String;');
+  Addln('');
+  Addln('begin');
+  Indent;
+  if (jpoUnknownLoadPropsError in options) then
+    begin
+    Addln('if not (AJSON is TJSONObject) then');
+    AddIndented('Raise EJSONException.CreateFmt(''"%s" : Cannot load from : "%s"'',[ClassName,AJSON.ClassName]);');
+    end
+  else
+    Addln('if not (AJSON is TJSONObject) then exit;');
+  Addln('O:=AJSON as TJSONObject;');
+  Addln('for P in O do');
+  Indent;
+  Addln('begin');
+  if jpoLoadCaseInsensitive in Options then
+    Addln('PN:=LowerCase(P.JSONString.Value);')
+  else
+    Addln('PN:=P.JSONString.Value;');
+  SElse:='';
+  For E in J do
+    begin
+    P:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(P);
+    If IM=Nil then
+      raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [P]);
+    K:=E.Key;
+    If jpoLoadCaseInsensitive in Options then
+      K:=LowerCase(K);
+    Addln(SElse+'If (PN=''%s'') then',[K]);
+    IM.JSONType:=E.Value.JSONType;
+    AddIndented(GenerateAssignDelphi(IM,IM.PropertyName,'P.JSONValue',False));
+    if SElse='' then
+      SElse:='else '
+    end;
+  if (jpoUnknownLoadPropsError in options) then
+    begin
+    Addln('else');
+    AddIndented('Raise EJSONException.CreateFmt(''"%s" : Unknown property : "%s"'',[ClassName,PN]);');
+    end
+  else
+    AddSemiColonToLastLine;
+  Addln('end;'); // For loop
+  Undent;
+  Undent;
+  Addln('end;');
+end;
+
+function TJSONToPascal.GenerateAssign(IM: TPropertyMapItem; AVarName, AJSONName: String): String;
+
+Var
+  T : String;
+  C : Boolean;
+
+begin
+  T:='';
+  Case LowerCase(IM.TypeName) of
+    'boolean' : T:='AsBoolean';
+    'string'  : T:='AsString';
+    'double'  : T:='AsFloat';
+    'integer' : T:='AsInteger';
+    'int64'   : T:='AsInt64';
+    'qword'   : T:='AsQWord';
+  else
+    if IM.JSONType=jtArray then
+      Result:=Format('%s:=Create%s(%s);',[AVarName,IM.TypeName,AJSONName])
+    else if IM.JSONType=jtObject then
+      Result:=Format('%s:=%s.CreateFromJSON(%s);',[AVarName,IM.TypeName,AJSONName])
+    else
+      Result:=Format('Raise EJSON.CreateFmt(''"%%s": Cannot handle property of type "%%s"''),[ClassName,''%s'']);',[IM.TypeName]);
+  end;
+  if T<>'' then
+    Result:=Format('%s:=%s.%s;',[AVarName,AJSONName,T]);
+end;
+
+function TJSONToPascal.GenerateAssignDelphi(IM: TPropertyMapItem; AVarName,
+  AJSONName: String; AddSemiColon: Boolean): String;
+
+Var
+  T : String;
+
+begin
+  T:='';
+  Case LowerCase(IM.TypeName) of
+    'boolean' : T:='Boolean';
+    'string'  : T:='String';
+    'double'  : T:='Double';
+    'integer' : T:='Integer';
+    'int64'   : T:='Int64';
+    'qword'   : T:='Int64';
+  else
+    if IM.JSONType=jtArray then
+      Result:=Format('%s:=Create%s(%s)',[AVarName,IM.TypeName,AJSONName])
+    else if IM.JSONType=jtObject then
+      Result:=Format('%s:=%s.CreateFromJSON(%s)',[AVarName,IM.TypeName,AJSONName])
+    else
+      Result:=Format('Raise EJSON.CreateFmt(''"%%s": Cannot handle property of type "%%s"''),[ClassName,''%s'']);',[IM.TypeName]);
+  end;
+  if T<>'' then
+    Result:=Format('%s:=%s.GetValue<%s>',[AVarName,AJSONName,T]);
+  If AddSemicolon then
+    Result:=Result+';'
+end;
+
+procedure TJSONToPascal.GenerateLoadJSONfpJSON(M : TPropertyMapItem; J: TJSONObject);
+
+Var
+  IM :  TPropertyMapItem;
+  E : TJSONEnum;
+  P : String;
+
+begin
+  AddLn('Procedure %s.LoadFromJSON(AJSON : TJSONData);',[M.TypeName]);
+  Addln('');
+  Addln('var');
+  AddIndented('E : TJSONEnum;');
+  Addln('');
+  Addln('begin');
+  Indent;
+  Addln('for E in AJSON do');
+  Indent;
+  Addln('begin');
+  if jpoLoadCaseInsensitive in Options then
+    Addln('case lowercase(E.Key) of')
+  else
+    Addln('case E.Key of');
+  For E in J do
+    begin
+    P:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(P);
+    If IM=Nil then
+      raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [P]);
+    if jpoLoadCaseInsensitive in Options then
+      Addln('''%s'':',[LowerCase(E.Key)])
+    else
+      Addln('''%s'':',[E.Key]);
+    IM.JSONType:=E.Value.JSONType;
+    AddIndented(GenerateAssign(IM,IM.PropertyName,'E.Value'));
+    end;
+  if (jpoUnknownLoadPropsError in options) then
+    begin
+    Addln('else');
+    AddIndented('Raise EJSON.CreateFmt(''"%s" : Unknown property : "%s"'',[ClassName,E.Key]);');
+    end;
+  Addln('end;'); // Case
+  Addln('end;'); // For loop
+  Undent;
+  Undent;
+  Addln('end;');
+end;
+
+procedure TJSONToPascal.GenerateSaveFunctionForm(M: TPropertyMapItem);
+
+begin
+  AddLn('Function  %s.SaveToJSON : TJSONObject;',[M.TypeName]);
+  AddLn('begin');
+  Indent;
+  AddLn('Result:=TJSONObject.Create;');
+  AddLn('Try');
+  AddIndented('SaveToJSON(Result);');
+  AddLn('except');
+  Indent;
+    Addln('FreeAndNil(Result);');
+    AddLn('Raise;');
+  Undent;
+  AddLn('end;');
+  Undent;
+  AddLn('end;');
+  AddLn('');
+end;
+
+procedure TJSONToPascal.GenerateSaveJSONDelphi(M: TPropertyMapItem;  J: TJSONObject);
+
+Var
+  IM :  TPropertyMapItem;
+  E : TJSONEnum;
+  T,P : String;
+  B,C : Boolean; // B : Indent called. C : Need to create value
+
+begin
+  GenerateSaveFunctionForm(M);
+  AddLn('');
+  AddLn('Procedure %s.SaveToJSON(AJSON : TJSONObject);',[M.TypeName]);
+  Addln('');
+  Addln('begin');
+  Indent;
+  For E in J do
+    begin
+    B:=False;
+    C:=True;
+    P:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(P);
+    If IM=Nil then
+      raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [P]);
+    Case LowerCase(IM.TypeName) of
+      'boolean' : T:='Boolean';
+      'string'  : T:='String';
+      'double'  : T:='Number';
+      'integer' : T:='Number';
+      'int64'   : T:='Number';
+      'qword'   : T:='Number';
+    else
+      C:=False;
+      if IM.JSONType=jtArray then
+        T:=Format('Save%sToJSON(%s)',[IM.TypeName,IM.PropertyName])
+      else if IM.JSONType=jtObject then
+        begin
+        Addln('If Assigned(%s) then',[IM.PropertyName]);
+        T:=Format('%s.SaveToJSON',[IM.PropertyName]);
+        B:=True; // Indent called
+        Indent;
+        end;
+    end;
+    if C then
+      T:='TJSON'+T+'.Create('+IM.PropertyName+')';
+    if (T<>'') then
+      AddLn('AJSON.AddPair(''%s'',%s);',[E.Key,T]);
+    if B then
+      Undent;
+    end;
+  Undent;
+  Addln('end;');
+end;
+
+procedure TJSONToPascal.GenerateSaveJSONfpJSON(M: TPropertyMapItem; J: TJSONObject);
+
+Var
+  IM :  TPropertyMapItem;
+  E : TJSONEnum;
+  T,P : String;
+  B : Boolean;
+
+begin
+  GenerateSaveFunctionForm(M);
+  AddLn('');
+  AddLn('Procedure %s.SaveToJSON(AJSON : TJSONObject);',[M.TypeName]);
+  Addln('');
+  Addln('begin');
+  Indent;
+  For E in J do
+    begin
+    B:=False;
+    P:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(P);
+    If IM=Nil then
+      raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [P]);
+    Case LowerCase(IM.TypeName) of
+      'boolean' : T:=IM.PropertyName;
+      'string'  : T:=IM.PropertyName;
+      'double'  : T:=IM.PropertyName;
+      'integer' : T:=IM.PropertyName;
+      'int64'   : T:=IM.PropertyName;
+      'qword'   : T:=IM.PropertyName;
+    else
+      if IM.JSONType=jtArray then
+        t:=Format('Save%sToJSON(%s)',[IM.TypeName,IM.PropertyName])
+      else if IM.JSONType=jtObject then
+        begin
+        Addln('If Assigned(%s) then',[IM.PropertyName]);
+        T:=Format('%s.SaveToJSON',[IM.PropertyName]);
+        B:=True; // Indent called
+        Indent;
+        end;
+    end;
+    if (T<>'') then
+      AddLn('AJSON.Add(''%s'',%s);',[E.Key,T]);
+    if B then
+      Undent;
+    end;
+  Undent;
+  Addln('end;');
+end;
+
+procedure TJSONToPascal.GenerateObjectImplementation(M : TPropertyMapItem; J: TJSONObject);
+
+Var
+  IM :  TPropertyMapItem;
+  E : TJSONEnum;
+  P,FRN : String;
+  HaveObj : Boolean;
+
+begin
+  HaveObj:=False;
+  For E in J do
+    begin
+    P:=AddToPath(M.Path,E.Key);
+    IM:=FPropertyMap.FindPath(P);
+    If IM<>Nil then
+      HaveObj:=HaveObj or (IM.JSONType=jtObject);
+    end;
+  Addln('');
+  AddLn('{ -----------------------------------------------------------------------');
+  Addln('  '+M.TypeName);
+  AddLn('  -----------------------------------------------------------------------}');
+  Addln('');
+  if HaveObj then
+    begin
+    AddLn('Destructor %s.Destroy;',[M.TypeName]);
+    Addln('');
+    Addln('begin');
+    Indent;
+    For E in J do
+      begin
+      P:=AddToPath(M.Path,E.Key);
+      IM:=FPropertyMap.FindPath(P);
+      If (IM<>Nil) and (IM.JSONType=jtObject) then
+        AddLn('FreeAndNil('+FieldPrefix+IM.PropertyName+');');
+      end;
+    Addln('inherited;');
+    Undent;
+    Addln('end;');
+    Addln('');
+    end;
+  Addln('');
+  if jpoUseSetter in Options then
+    For E in J do
+      begin
+      P:=AddToPath(M.Path,E.Key);
+      IM:=FPropertyMap.FindPath(P);
+      If IM=Nil then
+        raise EJSONToPascal.CreateFmt(SErrCannotDeterminePropertyType, [P]);
+      FRN:=FieldPrefix+IM.PropertyName;
+      AddLn('Procedure %s.Set%s(AValue : %s);',[M.TypeName,IM.PropertyName,IM.TypeName]);
+      Addln('');
+      Addln('begin');
+      Indent;
+      AddLn('if ('+FieldPrefix+IM.PropertyName+'=AValue) then exit;');
+      If IM.JSONType=jtObject then
+        AddLn('FreeAndNil('+FieldPrefix+IM.PropertyName+');');
+      AddLn(FieldPrefix+IM.PropertyName+':=AValue;');
+      Undent;
+      Addln('end;');
+      Addln('');
+      end;
+  if jpoGenerateLoad in Options then
+    begin
+    AddLn('Constructor %s.CreateFromJSON(AJSON : %s);',[M.TypeName,JSONDataName]);
+    Addln('');
+    Addln('begin');
+    Indent;
+    AddLn('Create(%s);',[GetObjectConstructorArguments]);
+    AddLn('LoadFromJSON(AJSON);');
+    Undent;
+    Addln('end;');
+    Addln('');
+    if jpoDelphiJSON in options then
+      GenerateLoadJSONDelphi(M,J)
+    else
+      GenerateLoadJSONfpJSON(M,J);
+    end;
+  if jpoGenerateSave in Options then
+    if jpoDelphiJSON in options then
+      GenerateSaveJSONDelphi(M,J)
+    else
+      GenerateSaveJSONfpJSON(M,J);
+end;
+
+procedure TJSONToPascal.GenerateImplementation(const APath: String; J: TJSONData);
+
+Var
+  M ,IM :  TPropertyMapItem;
+  O : TJSONEnum;
+  P : String;
+
+begin
+  Addln('');
+  M:=FPropertyMap.FindPath(APath);
+  if M.SkipType then
+    exit;
+  if J is TJSONArray then
+    GenerateArrayImplementation(M,TJSONarray(J))
+  else if J is TJSONObject then
+    begin
+    For O in TJSONOBject(J) do
+      begin
+      P:=AddToPath(APath,O.Key);
+      IM:=FPropertyMap.FindPath(P);
+      If (O.Value.JSONType in StructuredJSONTypes) then
+        GenerateImplementation(P,O.Value);
+      end;
+    GenerateObjectImplementation(M,TJSONObject(J));
+    end;
+  Addln('');
+end;
+
+procedure TJSONToPascal.GenerateImplementationEnd;
+begin
+  Addln('end.');
+end;
+
+procedure TJSONToPascal.Execute;
+
+Var
+  J : TJSONData;
+  DoFree : Boolean;
+
+begin
+  J:=Nil;
+  DoFree:=False;
+  Factive:=True;
+  try
+    ClearGeneratedTypes;
+    J:=GetJSONData(DoFree);
+    GenerateInterfaceHeader;
+    FInType:=False;
+    GenerateDeclaration('',J);
+    Undent;
+    GenerateImplementationHeader;
+    GenerateImplementation('',J);
+    GenerateImplementationEnd;
+  finally
+    if DoFree then
+      FreeAndNil(J);
+    Factive:=False;
+  end;
+end;
+
+{ TPropertyMapItem }
+
+procedure TPropertyMapItem.Assign(Source: TPersistent);
+
+Var
+  M : TPropertyMapItem;
+
+begin
+  if Source is TPropertyMapItem then
+    begin
+    M:=Source as TPropertyMapItem;
+    FPath:=M.Path;
+    FTypeName:=M.TypeName;
+    FParentTypeName:=M.ParentTypeName;
+    FGenerated:=M.Generated;
+    end
+  else
+    inherited Assign(Source);
+end;
+
+end.
+

+ 2422 - 0
packages/fcl-json/tests/tcjsontocode.pp

@@ -0,0 +1,2422 @@
+unit tcjsontocode;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testutils, testregistry, fpjsontopas;
+
+type
+
+  { TTestGenCode }
+
+  TTestGenCode= class(TTestCase)
+  private
+    FPos : Integer;
+    FGen: TJSONToPascal;
+    procedure AssertDelphiLoadArray(AElementType, AJSONtype: String);
+    procedure AssertDelphiPropertyAssignmentLoop;
+    procedure AssertDestructorImplementation(AClassName: String; ObjectFields: array of string);
+    procedure AssertLine(Msg: String; AExpected: String);
+    procedure GenCode(AJSON: String);
+    class function GetDataName(IsDelphi: Boolean): string;
+    function NextLine: String;
+    function Pos(const What, Where: String): Integer;
+  protected
+    procedure SetUp; override;
+    procedure TearDown; override;
+    procedure AssertArrayCreator(const ArrayTypeName, AElementType: String; IsDelphi: Boolean=False);
+    procedure AssertArraySaver(const ArrayTypeName, AElementType: String; IsDelphi: Boolean=False);
+    procedure AssertArrayCreatorImplementation(const ArrayTypeName, AElementType: String; AObjectName: String=''; IsDelphi: Boolean=False);
+    procedure AssertArraySaverImplementation(const ArrayTypeName, AElementType: String; AObjectName: String=''; IsDelphi: Boolean=False);
+    procedure AssertLoadArray(AElementType, AJSONtype: String; IsDelphi : Boolean = False);
+    procedure AssertSaveArray(AElementType, AJSONtype: String; IsDelphi: Boolean = False);
+    procedure AssertPropertyAssignmentLoop;
+    procedure AssertType;
+    procedure AssertClassComment(const Msg, AName: String);
+    procedure AssertLoadConstructorDeclaration(AType: String);
+    procedure AssertLoaderDeclaration(AType: String);
+    procedure AssertSaverDeclaration;
+    procedure AssertLoaderImplementationEnd(IsDelphi : Boolean = False);
+    procedure AssertLoadConstructorImplementationStart(Const ATypeName, ADataName: String);
+    procedure AssertLoaderImplementationStart(Const ATypeName, ADataName: String; IsDelphi : Boolean = False);
+    procedure AssertSaverImplementationStart(Const ATypeName: String; IsDelphi : Boolean = False);
+    procedure AssertArrayLoaderImplementationStart(Const ATypeName, ADataName, ArrayName, ArrayTypeName, ArrayElementType : String; IsDelphi : Boolean = False);
+    procedure AssertObjectLoaderImplementationStart(Const ATypeName, ADataName, ArrayName, ArrayTypeName, ArrayElementType : String; IsDelphi : Boolean = False);
+    Procedure AssertUnitHeader;
+    Procedure AssertBegin;
+    Procedure AssertEnd(Const Msg : String = '');
+    Procedure AssertUnitEnd;
+    Procedure AssertImplementation;
+    procedure AssertProperty(const AName, AType: String; Setter : Boolean = False);
+    procedure AssertSetter(const AName, AType: String);
+    Procedure AssertClassHeader(Const AName : String; AParentName : String);
+    Procedure AssertSetterImplementation(Const AClassType,AName,AType : String; IsObject : Boolean = False);
+    Procedure AssertVisibility(Const AVisibility : String);
+    Procedure AssertDestructor;
+    Procedure AssertField(Const AName,AType : String; Prefix : String = '');
+    Procedure AssertArrayType(Const AName,AItemType : String);
+    Procedure AssertPropertyMap(Const APath,ATypeName,APropertyName,AParentTypeName : String);
+    Property Gen : TJSONToPascal Read FGen;
+  published
+    procedure TestEmpty;
+    Procedure TestSimple;
+    Procedure TestClassName;
+    Procedure TestParentClassName;
+    Procedure TestIntegerProperty;
+    Procedure Test2IntegersProperty;
+    Procedure TestBooleanProperty;
+    Procedure TestStringProperty;
+    Procedure TestFloatProperty;
+    Procedure TestInt64Property;
+    Procedure TestPropertySetter;
+    Procedure TestObjectProperty;
+    Procedure TestObjectPropertySetter;
+    Procedure TestObjectPropertySuffix;
+    Procedure TestObjectPropertySkip;
+    Procedure TestObjectPropertyRecurse;
+    Procedure TestObjectPropertyRecurseSuffix;
+    Procedure TestObjectPropertyRecurseSkip;
+    Procedure TestObjectPropertyRecurseSkipB;
+    Procedure TestStringArrayProperty;
+    Procedure TestIntegerArrayProperty;
+    Procedure TestBooleanArrayProperty;
+    Procedure TestFloatArrayProperty;
+    Procedure TestInt64ArrayProperty;
+    Procedure TestStringArrayPropertySuffix;
+    Procedure TestObjectArrayProperty;
+    procedure TestObjectArrayPropertySuffix;
+    procedure TestArrayArrayProperty;
+    procedure TestObjectArrayArrayProperty;
+    Procedure TestLoadIntegerProperty;
+    Procedure TestLoad2IntegersProperty;
+    Procedure TestLoadIntegerWithErrorProperty;
+    Procedure TestLoadIntegerCaseInsensitiveProperty;
+    Procedure TestLoadStringProperty;
+    Procedure TestLoadBooleanProperty;
+    Procedure TestLoadInt64Property;
+    Procedure TestLoadFloatProperty;
+    Procedure TestLoadObjectProperty;
+    Procedure TestLoadStringArrayProperty;
+    Procedure TestLoadBooleanArrayProperty;
+    Procedure TestLoadIntegerArrayProperty;
+    Procedure TestLoadInt64ArrayProperty;
+    Procedure TestLoadFloatArrayProperty;
+    Procedure TestLoadObjectArrayProperty;
+    Procedure TestLoadDelphiIntegerProperty;
+    Procedure TestLoadDelphi2IntegersProperty;
+    Procedure TestLoadDelphiIntegerWithErrorProperty;
+    Procedure TestLoadDelphiIntegerCaseInsensitiveProperty;
+    Procedure TestLoadDelphiStringProperty;
+    Procedure TestLoadDelphiBooleanProperty;
+    Procedure TestLoadDelphiInt64Property;
+    Procedure TestLoadDelphiFloatProperty;
+    procedure TestLoadDelphiObjectProperty;
+    Procedure TestLoadDelphiStringArrayProperty;
+    Procedure TestLoadDelphiBooleanArrayProperty;
+    Procedure TestLoadDelphiIntegerArrayProperty;
+    Procedure TestLoadDelphiInt64ArrayProperty;
+    Procedure TestLoadDelphiFloatArrayProperty;
+    procedure TestLoadDelphiObjectArrayProperty;
+    Procedure TestSaveIntegerProperty;
+    Procedure TestSave2IntegersProperty;
+    Procedure TestSaveStringProperty;
+    Procedure TestSaveBooleanProperty;
+    Procedure TestSaveInt64Property;
+    Procedure TestSaveFloatProperty;
+    Procedure TestSaveObjectProperty;
+    Procedure TestSaveStringArrayProperty;
+    Procedure TestSaveBooleanArrayProperty;
+    Procedure TestSaveIntegerArrayProperty;
+    Procedure TestSaveInt64ArrayProperty;
+    Procedure TestSaveFloatArrayProperty;
+    Procedure TestSaveObjectArrayProperty;
+    Procedure TestSaveDelphiIntegerProperty;
+    Procedure TestSaveDelphi2IntegersProperty;
+    Procedure TestSaveDelphiStringProperty;
+    Procedure TestSaveDelphiBooleanProperty;
+    Procedure TestSaveDelphiInt64Property;
+    Procedure TestSaveDelphiFloatProperty;
+    Procedure TestSaveDelphiObjectProperty;
+    Procedure TestSaveDelphiStringArrayProperty;
+    Procedure TestSaveDelphiBooleanArrayProperty;
+    Procedure TestSaveDelphiIntegerArrayProperty;
+    Procedure TestSaveDelphiInt64ArrayProperty;
+    Procedure TestSaveDelphiFloatArrayProperty;
+    Procedure TestSaveDelphiObjectArrayProperty;
+  end;
+
+Var
+  TestUnitDir : String;
+
+implementation
+
+procedure TTestGenCode.SetUp;
+begin
+  FGen:=TJSONToPascal.Create(Nil);
+end;
+
+procedure TTestGenCode.TearDown;
+begin
+  FreeAndNil(FGen)
+end;
+
+function TTestGenCode.NextLine: String;
+
+begin
+  Result:='';
+  While (Result='') do
+    begin
+    Inc(FPos);
+    AssertTrue('In scope',FPos<FGen.Code.Count);
+    Result:=Trim(FGen.Code[FPos]);
+    end;
+end;
+
+procedure TTestGenCode.AssertUnitHeader;
+
+Var
+  S: String;
+
+begin
+  S:=NextLine;
+  AssertTrue('Have unit',Pos('unit ',S)=1);
+  S:=NextLine;
+  AssertTrue('Have interface',Pos('interface',S)=1);
+  S:=NextLine;
+  AssertTrue('Have uses',Pos('uses ',S)=1);
+  S:=NextLine;
+  AssertTrue('Type line',Pos('Type',S)=1);
+end;
+
+procedure TTestGenCode.AssertBegin;
+begin
+  AssertTrue('Have begin',pos('begin',nextline)>0);
+end;
+
+procedure TTestGenCode.AssertEnd(const Msg: String);
+begin
+  AssertTrue('Have end:'+Msg,pos('end;',nextline)>0);
+end;
+
+procedure TTestGenCode.AssertUnitEnd;
+begin
+  AssertTrue('Have end.',pos('end.',nextline)>0);
+end;
+
+procedure TTestGenCode.AssertImplementation;
+begin
+  AssertTrue('Have implementation',CompareText(NextLine,'implementation')=0);
+end;
+
+function TTestGenCode.Pos(const What, Where: String): Integer;
+
+begin
+  Result:=system.Pos(lowercase(what),lowercase(where));
+end;
+
+procedure TTestGenCode.AssertClassComment(const Msg,AName: String);
+
+Var
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue(Msg+' ('+AName+'): Class header comment start',Pos('{ --',S)>0);
+  S:=NextLine;
+  AssertTrue(Msg+' ('+AName+'): Class header comment class nam',Pos(AName,S)>0);
+  S:=NextLine;
+  AssertTrue(Msg+' ('+AName+'): Class header comment end',Pos('}',S)>0);
+end;
+
+procedure TTestGenCode.AssertClassHeader(const AName: String; AParentName: String);
+
+Var
+  P : Integer;
+  S : String;
+
+begin
+  AssertClassComment('Class declarationheader for '+AName,AName);
+  S:=NextLine;
+  P:=Pos(AName+' = class(',S);
+  AssertTrue('class type ',P>0);
+  P:=Pos(AParentName+')',S);
+  AssertTrue('Class parent type ',P>0);
+  AssertVisibility('private');
+end;
+
+procedure TTestGenCode.AssertSetterImplementation(const AClassType, AName,
+  AType: String; IsObject: Boolean);
+
+Var
+  S,PS : String;
+  P : Integer;
+
+begin
+  S:=NextLine;
+  PS:='Procedure '+AClassType+'.Set'+Aname+'(AValue';
+  AssertTrue('Have declaration start',Pos(PS,S)>0);
+  Delete(S,1,Length(PS));
+  P:=Pos(':',S);
+  AssertTrue('Have colon' ,p>0);
+  Delete(S,1,P);
+  AssertTrue('Have type',Pos(AType,S)>0);
+  AssertTrue('Have );',Pos(');',S)>0);
+  AssertTrue('Terminated on semicolon',S[Length(S)]=';');
+  AssertBegin;
+  AssertTrue('Have change check',Pos('if ('+Gen.FieldPrefix+AName+'=AValue) then exit;',NextLine)>0);
+  if IsObject then
+    AssertTrue('Have free of previous value',Pos('FreeAndNil('+Gen.FieldPrefix+AName+');',NextLine)>0);
+  AssertTrue('Have Assignment',Pos(Gen.FieldPrefix+AName+':=AValue;',NextLine)>0);
+  AssertEnd;
+end;
+
+procedure TTestGenCode.AssertVisibility(const AVisibility: String);
+
+begin
+  AssertTrue('Have visibility section '+AVisibility,Pos(AVisibility,NextLine)>0);
+end;
+
+procedure TTestGenCode.AssertDestructor;
+begin
+  AssertTrue('Have destructor declaration',Pos('Destructor Destroy; override;',NextLine)>0);
+end;
+
+
+procedure TTestGenCode.AssertDestructorImplementation(AClassName: String;
+  ObjectFields: array of string);
+
+Var
+  F : String;
+
+begin
+  AssertTrue('Have destructor implementation',Pos(Format('Destructor %s.Destroy;',[AClassName]),NextLine)>0);
+  AssertBegin;
+  For F in ObjectFields do
+    AssertTrue('Have destructor for F'+F,Pos('FreeAndNil(F'+F+');',NextLine)>0);
+  AssertTrue('Have inherited call'+F,Pos('Inherited;',NextLine)>0);
+  AssertEnd;
+end;
+
+procedure TTestGenCode.AssertField(const AName, AType: String; Prefix : String = '');
+
+Var
+  F,S : String;
+  P : Integer;
+
+begin
+  F:=Prefix;
+  if F='' then
+    F:='F';
+  S:=NextLine;
+  AssertTrue('Field Name',Pos(F+AName,S)=1);
+  P:=Pos(':',S);
+  AssertTrue('Colon after field name',P>Length(F+AName));
+  AssertTrue('Field type after colon',Pos(AType,S)>P);
+  AssertTrue('Terminated on semicolon',S[Length(S)]=';');
+end;
+
+procedure TTestGenCode.AssertSetter(const AName, AType: String);
+
+Var
+  N,S,PD : String;
+  P,p2 : Integer;
+
+begin
+  S:=NextLine;
+  N:='Setter declaration for '+AName+' : ';
+  PD:='Procedure Set'+AName;
+  AssertTrue(N+'Setter name',Pos(PD,S)=1);
+  P:=Pos('(',S);
+  AssertTrue(N+'( after parameter name',P>Length(PD));
+  P:=Pos(':',S);
+  AssertTrue(N+'Colon after parameter name',P>Length(PD));
+  Delete(S,1,P);
+  P2:=Pos(AType,S);
+  AssertTrue(N+'Field type after colon '+AType+' : '+S,P2>0);
+  P:=Pos(');',S);
+  AssertTrue(N+'); type after parameter type',P>P2);
+  P2:=Pos('virtual',S);
+  AssertTrue(N+'virtual after ); ',P2>P);
+  AssertTrue(N+'Terminated on semicolon',S[Length(S)]=';');
+end;
+
+procedure TTestGenCode.AssertArrayType(const AName, AItemType: String);
+
+Var
+  P,p2 : Integer;
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue('Type Name',Pos(AName,S)=1);
+  P:=Pos('=',S);
+  AssertTrue('Equal token after type Name',P>Pos(AName,S));
+  P2:=Pos('Array of',S);
+  AssertTrue('Array of after Equal token after type Name',P2>P);
+  P:=Pos(AItemType,S);
+  AssertTrue('Item type name after array of',P>P2);
+  AssertTrue('Terminated on semicolon',S[Length(S)]=';');
+end;
+
+procedure TTestGenCode.AssertPropertyMap(const APath, ATypeName, APropertyName,
+  AParentTypeName: String);
+
+Var
+  M : TPropertyMapItem;
+
+begin
+  M:=Gen.PropertyMap.FindPath(APath);
+  AssertNotNull('Have property map "'+APath+'"',M);
+  AssertEquals('Have type name ',ATypeName,M.TypeName);
+  AssertEquals('Have property name ',APropertyName,M.PropertyName);
+  AssertEquals('Have parent type name ',AParentTypeName,M.ParentTypeName);
+end;
+
+procedure TTestGenCode.AssertProperty(const AName, AType: String; Setter : Boolean = False);
+
+Var
+  S : String;
+  P,P2 : Integer;
+
+begin
+  S:=NextLine;
+  AssertTrue('Property Name',Pos('Property '+AName,S)=1);
+  P:=Pos(':',S);
+  AssertTrue('Colon after property name',P>Length('Property '+AName));
+  P2:=Pos(AType,S);
+  AssertTrue('Field type after colon',P2>P);
+  P:=pos(' read ',S);
+  AssertTrue('Read specifier after type ',P>P2);
+  P2:=Pos('F'+AName,S);
+  AssertTrue('Field name for read specifier',P2>P);
+  P:=pos(' write ',S);
+  AssertTrue('Write specifier after type ',P>P2);
+  if Setter Then
+    P2:=Pos('write Set'+AName,S)
+  else
+    P2:=Pos('write F'+AName,S);
+  AssertTrue('Field name for write specifier',P2>P);
+
+  AssertTrue('Terminated on semicolon',S[Length(S)]=';');
+end;
+
+
+procedure TTestGenCode.GenCode(AJSON : String);
+
+Var
+  F : Text;
+
+begin
+  Gen.JSON:=AJSON;
+  Gen.DestUnitName:='u'+TestName;
+  Gen.Execute;
+  if (TestUnitDir<>'') then
+    begin
+    Assign(F,IncludeTrailingPathDelimiter(TestUnitDir)+Gen.DestUnitName+'.pp');
+    Rewrite(F);
+    Writeln(F,'// ',Self.TestName);
+    Writeln(F,Gen.Code.Text);
+    Close(F);
+    Assign(F,IncludeTrailingPathDelimiter(TestUnitDir)+Gen.DestUnitName+'.json');
+    Rewrite(F);
+    Writeln(F,AJSON);
+    Close(F);
+    end
+  else
+    begin
+    Writeln('// ',Self.TestName);
+    Writeln('(* JSON: '+AJSON+' *)');
+    Writeln(Gen.Code.Text);
+    end;
+
+  FPos:=-1;
+end;
+
+procedure TTestGenCode.TestEmpty;
+begin
+  AssertNotNull('Have generator',Gen);
+  AssertNotNull('Generator property map exists',Gen.PropertyMap);
+  AssertNotNull('Generator property code exists',Gen.Code);
+  AssertNull('Generator JSON empty',Gen.JSONData);
+  AssertNull('Generator JSON stream empty',Gen.JSONStream);
+  AssertEquals('Generator JSON empty','',Gen.JSON);
+  AssertEquals('Generator property map empty',0,Gen.PropertyMap.Count);
+end;
+
+procedure TTestGenCode.TestSimple;
+begin
+  GenCode('{}');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+end;
+
+procedure TTestGenCode.TestClassName;
+begin
+  Gen.PropertyMap.AddPath('','TSomeObject');
+  GenCode('{}');
+  AssertUnitHeader;
+  AssertClassHeader('TSomeObject','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertPropertyMap('','TSomeObject','','TObject');
+end;
+
+procedure TTestGenCode.TestParentClassName;
+begin
+  Gen.PropertyMap.AddPath('','TSomeObject');
+  Gen.DefaultParentName:='TMyObject';
+  GenCode('{}');
+  AssertUnitHeader;
+  AssertClassHeader('TSomeObject','TMyObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertPropertyMap('','TSomeObject','','TMyObject');
+end;
+
+procedure TTestGenCode.TestIntegerProperty;
+begin
+  GenCode('{ "a" : 1 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertVisibility('public');
+  AssertProperty('a','integer');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+end;
+
+procedure TTestGenCode.Test2IntegersProperty;
+begin
+  GenCode('{ "a" : 1, "b" : 2 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertProperty('a','integer');
+  AssertProperty('b','integer');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestBooleanProperty;
+begin
+  GenCode('{ "a" : true }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','boolean');
+  AssertVisibility('public');
+  AssertProperty('a','boolean');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Boolean','a','');
+end;
+
+procedure TTestGenCode.TestStringProperty;
+begin
+  GenCode('{ "a" : "abce" }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','string');
+  AssertVisibility('public');
+  AssertProperty('a','string');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','String','a','');
+end;
+
+procedure TTestGenCode.TestFloatProperty;
+begin
+  GenCode('{ "a" : 1.1 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','double');
+  AssertVisibility('public');
+  AssertProperty('a','double');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Double','a','');
+end;
+
+procedure TTestGenCode.TestInt64Property;
+begin
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','int64');
+  AssertVisibility('public');
+  AssertProperty('a','int64');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestPropertySetter;
+begin
+  Gen.Options:=[jpoUseSetter];
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','int64');
+  AssertVisibility('protected');
+  AssertSetter('A','int64');
+  AssertVisibility('public');
+  AssertProperty('a','int64',True);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSetterImplementation('TMyObject','a','int64');
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestObjectProperty;
+begin
+  GenCode('{ "a" : {} }');
+  AssertUnitHeader;
+  AssertClassHeader('TA','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Comment for class TA','Ta');
+  AssertClassComment('Comment for class TMyObject','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.TestObjectPropertySetter;
+begin
+  Gen.Options:=[jpoUseSetter];
+  GenCode('{ "a" : {} }');
+  AssertUnitHeader;
+  AssertClassHeader('TA','TObject');
+  AssertVisibility('protected');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('protected');
+  AssertSetter('a','Ta');
+  AssertVisibility('Public');
+  AssertDestructor;
+  AssertProperty('a','Ta',True);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Comment for class TA','Ta');
+  AssertClassComment('Comment for class TMyObject','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertSetterImplementation('TMyObject','a','Ta',True);
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.TestObjectPropertySuffix;
+begin
+  Gen.PropertyTypeSuffix:='Type';
+  GenCode('{ "a" : {} }');
+  AssertUnitHeader;
+  AssertClassHeader('TAType','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','TaType');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','TaType');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','TaType','a','TObject');
+end;
+
+procedure TTestGenCode.TestObjectPropertySkip;
+begin
+  Gen.PropertyTypeSuffix:='Type';
+  Gen.PropertyMap.AddPath('a','me').SkipType:=true;
+  GenCode('{ "a" : {} }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','me');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','me');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','me','a','');
+end;
+
+procedure TTestGenCode.TestObjectPropertyRecurse;
+begin
+  GenCode('{ "a" : { "b" : {} } }');
+  AssertUnitHeader;
+  AssertClassHeader('TAB','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertClassHeader('TA','TObject');
+  AssertField('b','TaB');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('b','TaB');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+  AssertPropertyMap('a.b','Tab','b','TObject');
+end;
+
+procedure TTestGenCode.TestObjectPropertyRecurseSuffix;
+begin
+  Gen.PropertyTypeSuffix:='Type';
+  GenCode('{ "a" : { "b" : {} } }');
+  AssertUnitHeader;
+  AssertClassHeader('TABType','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertClassHeader('TAType','TObject');
+  AssertField('b','TaBType');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('b','TaBType');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','TaType');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','TaType');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','TaType','a','TObject');
+  AssertPropertyMap('a.b','TabType','b','TObject');
+end;
+
+procedure TTestGenCode.TestObjectPropertyRecurseSkip;
+begin
+  Gen.PropertyMap.AddPath('a','me').SkipType:=true;
+  GenCode('{ "a" : { "b" : {} } }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','me');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','me');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','me','a','');
+end;
+
+procedure TTestGenCode.TestObjectPropertyRecurseSkipB;
+begin
+  Gen.PropertyMap.AddPath('a.b','me').SkipType:=true;
+  GenCode('{ "a" : { "b" : {} } }');
+  AssertUnitHeader;
+  AssertClassHeader('TA','TObject');
+  AssertField('b','me');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('b','me');
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+  AssertPropertyMap('a.b','me','b','');
+end;
+
+procedure TTestGenCode.TestStringArrayProperty;
+begin
+  GenCode('{ "a" : [ "" ] }');
+  AssertUnitHeader;
+  AssertArrayType('Ta','string');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','String','','');
+end;
+
+procedure TTestGenCode.TestIntegerArrayProperty;
+begin
+  GenCode('{ "a" : [ 1 ] }');
+  AssertUnitHeader;
+  AssertArrayType('Ta','integer');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','Integer','','');
+end;
+
+procedure TTestGenCode.TestBooleanArrayProperty;
+begin
+  GenCode('{ "a" : [ true ] }');
+  AssertUnitHeader;
+  AssertArrayType('Ta','Boolean');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','Boolean','','');
+end;
+
+procedure TTestGenCode.TestFloatArrayProperty;
+begin
+  GenCode('{ "a" : [ 1.2 ] }');
+  AssertUnitHeader;
+  AssertArrayType('Ta','Double');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','Double','','');
+end;
+
+procedure TTestGenCode.TestInt64ArrayProperty;
+begin
+  GenCode('{ "a" : [ 1234567890123 ] }');
+  AssertUnitHeader;
+  AssertArrayType('Ta','Int64');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','Int64','','');
+end;
+
+procedure TTestGenCode.TestStringArrayPropertySuffix;
+begin
+  Gen.PropertyTypeSuffix:='Type';
+  GenCode('{ "a" : [ "" ] }');
+  AssertUnitHeader;
+  AssertArrayType('TaType','string');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','TaType');
+  AssertVisibility('public');
+  AssertProperty('a','TaType');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','TaType','a','');
+  AssertPropertyMap('a[0]','String','','');
+end;
+
+procedure TTestGenCode.TestObjectArrayProperty;
+begin
+  GenCode('{ "a" : [ {} ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItem','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertArrayType('Ta','TaItem');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','TaItem','','TObject');
+end;
+
+procedure TTestGenCode.TestObjectArrayPropertySuffix;
+
+begin
+  Gen.PropertyTypeSuffix:='Type';
+  GenCode('{ "a" : [ {} ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItemType','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertArrayType('TaType','TaItemType');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','TaType');
+  AssertVisibility('public');
+  AssertProperty('a','TaType');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','TaType','a','');
+  AssertPropertyMap('a[0]','TaItemType','','TObject');
+end;
+
+procedure TTestGenCode.TestArrayArrayProperty;
+begin
+  GenCode('{ "a" : [ [ "" ] ] }');
+  AssertUnitHeader;
+  AssertArrayType('TaItem','String');
+  AssertArrayType('Ta','TaItem');
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertProperty('a','Ta');
+  AssertEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','TaItem','','');
+  AssertPropertyMap('a[0][0]','String','','');
+end;
+
+procedure TTestGenCode.TestObjectArrayArrayProperty;
+begin
+  GenCode('{ "a" : [ [ {} ] ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItemItem','TObject');
+  AssertVisibility('public');
+  AssertEnd;
+  AssertArrayType('TaItem','TaItemItem');
+  AssertArrayType('Ta','TaItem');
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+  AssertPropertyMap('a[0]','TaItem','','');
+  AssertPropertyMap('a[0][0]','TaItemItem','','TObject');
+end;
+
+procedure TTestGenCode.AssertLoadConstructorDeclaration(AType: String);
+
+Var
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue('Load Constructor declaration in '+S,Pos('Constructor CreateFromJSON(AJSON : '+AType+'); virtual;',S)>0);
+end;
+
+procedure TTestGenCode.AssertLoaderDeclaration(AType : String);
+
+Var
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue('LoadFromJSON declaration in '+S,Pos('Procedure LoadFromJSON(AJSON : '+AType+'); virtual;',S)>0);
+end;
+
+procedure TTestGenCode.AssertSaverDeclaration;
+
+Var
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue('SaveToJSON function declaration in '+S,Pos('Function SaveToJSON : TJSONObject;',S)>0);
+  S:=NextLine;
+  AssertTrue('SaveToJSON procedure declaration in '+S,Pos('Procedure SaveToJSON(AJSON : TJSONObject)',S)>0);
+end;
+
+procedure TTestGenCode.AssertLoaderImplementationEnd(IsDelphi : Boolean = False);
+
+begin
+  if Not IsDelphi then
+    AssertEnd('Case');// Case
+  AssertEnd('for');// For
+  AssertEnd('procedure');// Routine
+end;
+
+procedure TTestGenCode.AssertArrayLoaderImplementationStart(const ATypeName,
+  ADataName, ArrayName, ArrayTypeName, ArrayElementType: String; IsDelphi : Boolean = False);
+
+Var
+  S : String;
+begin
+  S:=NextLine;
+  AssertTrue('Have loader start: '+ATypeName+','+ADataName,Pos('Procedure '+ATypeName+'.LoadFromJSON(AJSON : '+ADataName+');',S)>0);
+  if isDelphi then
+    AssertDelphiPropertyAssignmentLoop
+  else
+    AssertPropertyAssignmentLoop;
+end;
+
+procedure TTestGenCode.AssertPropertyAssignmentLoop;
+
+begin
+  AssertTrue('Have var',Pos('var',NextLine)>0);
+  AssertTrue('Have P enum',Pos('E : TJSONEnum;',NextLine)>0);
+  AssertBegin;
+  AssertTrue('Have E for enum',Pos('For E in AJSON do',NextLine)>0);
+  AssertBegin;
+  if (jpoLoadCaseInsensitive in Gen.Options) then
+    AssertTrue('Have E for enum',Pos('case LowerCase(E.key) of',NextLine)>0)
+  else
+    AssertTrue('Have E for enum',Pos('case E.key of',NextLine)>0);
+end;
+
+procedure TTestGenCode.AssertDelphiPropertyAssignmentLoop;
+
+Var
+  S : String;
+
+begin
+  AssertTrue('Have var',Pos('var',NextLine)>0);
+  AssertTrue('Have pair',Pos('P : TJSONPair;',NextLine)>0);
+  AssertTrue('Have obj',Pos('O : TJSONObject;',NextLine)>0);
+  AssertTrue('Have Propertyname var',Pos('PN : String;',NextLine)>0);
+  AssertBegin;
+  S:=NextLine;
+  AssertTrue('Have JSONObject check in '+S,Pos('not (AJSON is TJSONObject)',S)>0);
+  if jpoUnknownLoadPropsError in gen.Options then
+    AssertTrue('Have raise statement',Pos('Raise EJSONException',NextLine)>0);
+  AssertTrue('Have typecast',Pos('O:=AJSON as TJSONObject',NextLine)>0);
+  AssertTrue('Have P for enum',Pos('For P in O do',NextLine)>0);
+  AssertBegin;
+  if jpoLoadCaseInsensitive in Gen.Options then
+    AssertTrue('Have case insensitive propertyname assign',Pos('PN:=LowerCase(P.JSONString.Value)',NextLine)>0)
+  else
+    AssertTrue('Have propertyname assign',Pos('PN:=P.JSONString.Value',NextLine)>0);
+end;
+
+procedure TTestGenCode.AssertObjectLoaderImplementationStart(const ATypeName,
+  ADataName, ArrayName, ArrayTypeName, ArrayElementType: String; IsDelphi : Boolean = False);
+Var
+  S : String;
+begin
+  S:=NextLine;
+  AssertTrue('Have loader start: '+ATypeName+','+ADataName,Pos('Procedure '+ATypeName+'.LoadFromJSON(AJSON : '+ADataName+');',S)>0);
+  if isDelphi then
+    AssertDelphiPropertyAssignmentLoop
+  else
+    AssertPropertyAssignmentLoop;
+end;
+
+procedure TTestGenCode.AssertSaverImplementationStart(const ATypeName: String;
+  IsDelphi: Boolean);
+
+Var
+  S,N : String;
+
+begin
+  N:='SaveToJSONFunction '+ATypeName+' : ';
+  S:=NextLine;
+  AssertTrue(N+'header',Pos('Function  '+ATypeName+'.SaveToJSON : TJSONObject;',S)>0);
+  AssertBegin;
+  AssertTrue(N+'Create',Pos('Result:=TJSONObject.Create',NextLine)>0);
+  AssertTrue(N+'Try',Pos('Try',NextLine)>0);
+  AssertTrue(N+'Save',Pos('SaveToJSON(Result);',NextLine)>0);
+  AssertTrue(N+'except',Pos('except',NextLine)>0);
+  AssertTrue(N+'FreeAndNil',Pos('FreeAndNil(Result);',NextLine)>0);
+  AssertTrue(N+'Reraise',Pos('Raise;',NextLine)>0);
+  AssertTrue(N+'end;',Pos('End;',NextLine)>0);
+  AssertTrue(N+'end;',Pos('End;',NextLine)>0);
+  AssertTrue(N+'proc header',Pos('Procedure '+ATypeName+'.SaveToJSON(AJSON : TJSONObject);',NextLine)>0);
+  AssertBegin;
+end;
+
+
+procedure TTestGenCode.AssertLoaderImplementationStart(const ATypeName,
+  ADataName: String; IsDelphi : Boolean = False);
+
+begin
+  AssertTrue(Pos('Procedure '+ATypeName+'.LoadFromJSON(AJSON : '+ADataName+');',NextLine)>0);
+  if isDelphi then
+    AssertDelphiPropertyAssignmentLoop
+  else
+    AssertPropertyAssignmentLoop;
+end;
+
+procedure TTestGenCode.AssertLoadConstructorImplementationStart(const ATypeName,
+  ADataName: String);
+
+begin
+  AssertTrue('Have constructor call',Pos('Constructor '+ATypeName+'.CreateFromJSON(AJSON : '+ADataName+');',NextLine)>0);
+  AssertBegin;
+  AssertTrue('Call create constructor',Pos('create();',NextLine)>0);
+  AssertTrue('Call LoadFromJSON',Pos('LoadFromJSON(AJSON);',NextLine)>0);
+  AssertEnd;
+end;
+
+procedure TTestGenCode.TestLoadIntegerProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" integer property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('a:=E.Value.AsInteger;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+end;
+
+procedure TTestGenCode.TestLoad2IntegersProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" integer property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('a:=E.Value.AsInteger;',NextLine)>0);
+  AssertTrue('Have "b" integer property case',Pos('''b'':',NextLine)>0);
+  AssertTrue('Have "b" integer property set', Pos('b:=E.Value.AsInteger;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestLoadIntegerWithErrorProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoUnknownLoadPropsError];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" integer property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('a:=E.Value.AsInteger;',NextLine)>0);
+  AssertTrue('Have "b" integer property case',Pos('''b'':',NextLine)>0);
+  AssertTrue('Have "b" integer property set', Pos('b:=E.Value.AsInteger;',NextLine)>0);
+  AssertTrue('Have case else',Pos('else',NextLine)>0);
+  AssertTrue('Have raise statement', Pos('Raise EJSON.CreateFmt',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestLoadIntegerCaseInsensitiveProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoLoadCaseInsensitive];
+  GenCode('{ "A" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('A','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('A','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData',False);
+  AssertTrue('Have "a" integer property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('A:=E.Value.AsInteger;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('A','Integer','A','');
+end;
+
+procedure TTestGenCode.TestLoadStringProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : "1234" }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','string');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','string',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" string property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" string property set', Pos('a:=E.Value.AsString;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','String','a','');
+end;
+
+procedure TTestGenCode.TestLoadBooleanProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : true }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','boolean');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','boolean',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" boolean property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" boolean property set', Pos('a:=E.Value.AsBoolean;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Boolean','a','');
+end;
+
+procedure TTestGenCode.TestLoadInt64Property;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Int64');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','Int64',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" Int64 property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" Int64 property set', Pos('a:=E.Value.AsInt64;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestLoadFloatProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : 1.1 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Double');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','Double',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertLoaderImplementationStart('TMyObject','TJSONData');
+  AssertTrue('Have "a" Double property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" Double property set', Pos('a:=E.Value.AsFloat;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Double','a','');
+end;
+
+procedure TTestGenCode.TestLoadObjectProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : { "b" : "abc" } }');
+  AssertUnitHeader;
+  AssertClassHeader('Ta','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','Ta');
+  AssertLoadConstructorImplementationStart('Ta','TJSONData');
+  AssertLoaderImplementationStart('Ta','TJSONData');
+  AssertTrue('Have "b" string property case',Pos('''b'':',NextLine)>0);
+  AssertTrue('Have "b" string property set', Pos('b:=E.Value.AsString;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertObjectLoaderImplementationStart('TMyObject','TJSONData','a','Ta','');
+  AssertTrue('Have "a" object property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" object create createfromjson', Pos('a:=ta.CreateFromJSON(E.Value);',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.AssertArrayCreator(const ArrayTypeName,
+  AElementType: String; IsDelphi: Boolean);
+
+Var
+  S : String;
+
+begin
+  S:=NextLine;
+  AssertTrue('Have array creator in '+S,Pos('Function Create'+ArrayTypeName+'(AJSON : '+GetDataName(IsDelphi)+') : '+ArrayTypeName,S)>0);
+end;
+
+procedure TTestGenCode.AssertArraySaver(const ArrayTypeName,
+  AElementType: String; IsDelphi: Boolean);
+
+Var
+  E,S : String;
+
+begin
+  S:=NextLine;
+  E:='Procedure Save'+ArrayTypeName+'ToJSON(AnArray : '+ArrayTypeName+'; AJSONArray : TJSONArray);';
+  AssertTrue('Have proc array saver in '+S,Pos(E,S)>0);
+  S:=NextLine;
+  E:='Function Save'+ArrayTypeName+'ToJSON(AnArray : '+ArrayTypeName+') : TJSONArray;';
+  AssertTrue('Have func array saver in '+S,Pos(E,S)>0);
+end;
+
+procedure TTestGenCode.AssertArrayCreatorImplementation(const ArrayTypeName,
+  AElementType: String; AObjectName: String; IsDelphi: Boolean);
+
+Var
+  S,E,AN : String;
+
+begin
+  S:=NextLine;
+  E:='Function Create'+ARrayTypeName+'(AJSON : '+GetDataName(IsDelphi)+') : '+ArrayTypeName;
+  AssertTrue('Have array creator header '+S+'Expected : '+E ,Pos(E,S)>0);
+  AssertTrue('Have var',Pos('var',NextLine)>0);
+  AssertTrue('Have loop var',Pos('I : Integer;',NextLine)>0);
+  if IsDelphi then
+    begin
+    AssertTrue('Have Array var',Pos('A : TJSONArray;',NextLine)>0);
+    AN:='A'
+    end
+  else
+    AN:='AJSON';
+  AssertBegin;
+  if IsDelphi then
+    AssertTrue('Have Array assignnment',Pos('A:=AJSON as TJSONArray;',NextLine)>0);
+  AssertTrue('Have array setlength ',Pos('SetLength(Result,'+AN+'.Count);',NextLine)>0);
+  AssertTrue('Have loop ',Pos('for i:=0 to '+AN+'.Count-1 do',NextLine)>0);
+  if AObjectName='' then
+    begin
+    if IsDelphi then
+      AssertTrue('Have element assignment : '+AElementType,Pos('Result[i]:='+AN+'.Items[i].GetValue<'+AElementType+'>;',NextLine)>0)
+    else
+      AssertTrue('Have element assignment : '+AElementType,Pos('Result[i]:='+AN+'.Items[i].'+AElementType+';',NextLine)>0)
+    end
+  else
+    AssertTrue('Have element assignment : '+AElementType,Pos('Result[i]:='+AObjectName+'.CreateFromJSON('+AN+'.Items[i]);',NextLine)>0);
+  AssertEnd;
+end;
+
+procedure TTestGenCode.AssertLine(Msg : String; AExpected : String);
+
+Var
+  N,DMsg : String;
+
+begin
+  N:=NextLine;
+  DMsg:=Msg+', Expected: "'+AExpected+'", Actual: "'+N+'"';
+  AssertTrue(Dmsg,Pos(AExpected,N)>0);
+end;
+
+procedure TTestGenCode.AssertArraySaverImplementation(const ArrayTypeName,
+  AElementType: String; AObjectName: String; IsDelphi: Boolean);
+Var
+  N,S,E,AN : String;
+
+begin
+  N:=ArrayTypeName+'Saver : ';
+  S:=NextLine;
+  E:='Function Save'+ArrayTypeName+'ToJSON(AnArray : '+ArrayTypeName+') : TJSONArray;';
+  AssertTrue(N+'header',Pos(E,S)>0);
+  AssertBegin;
+  AssertTrue(N+'Create',Pos('Result:=TJSONArray.Create',NextLine)>0);
+  AssertTrue(N+'Try',Pos('Try',NextLine)>0);
+  S:=NextLine;
+  E:='Save'+ArrayTypeName+'ToJSON(AnArray,Result);';
+  AssertTrue(N+'Save',Pos(E,S)>0);
+  AssertTrue(N+'except',Pos('except',NextLine)>0);
+  AssertTrue(N+'FreeAndNil',Pos('FreeAndNil(Result);',NextLine)>0);
+  AssertTrue(N+'Reraise',Pos('Raise;',NextLine)>0);
+  AssertTrue(N+'end;',Pos('End;',NextLine)>0);
+  AssertTrue(N+'end;',Pos('End;',NextLine)>0);
+  S:=NextLine;
+  E:='Procedure Save'+ArrayTypeName+'ToJSON(AnArray : '+ArrayTypeName+'; AJSONArray : TJSONArray);';
+  AssertTrue('Have array saver header '+S+'Expected : '+E ,Pos(E,S)>0);
+  AssertTrue('Have var',Pos('var',NextLine)>0);
+  AssertTrue('Have loop var',Pos('I : Integer;',NextLine)>0);
+  AssertBegin;
+  AssertTrue('Have loop ',Pos('for i:=0 to Length(AnArray)-1 do',NextLine)>0);
+  if AObjectName='' then
+    AssertLine('Have element assignment : '+AElementType,'AJSONArray.Add(AnArray[i]);')
+{  else if AObjectName='' then
+    AssertLine('Have element assignment : '+AElementType,'AJSONArray.Add('+AN+'[i]);')}
+  else
+     AssertTrue('Have element assignment : '+AElementType,Pos('AJSONArray.Add(AnArray[i].SaveToJSON);',NextLine)>0);
+  AssertEnd;
+end;
+
+procedure TTestGenCode.AssertType;
+
+begin
+  AssertTrue('Have Type keyword',Pos('Type',NextLine)>0);
+end;
+
+procedure TTestGenCode.AssertDelphiLoadArray(AElementType, AJSONtype : String);
+
+begin
+  AssertUnitHeader;
+  AssertArrayType('Ta',AElementType);
+  AssertArrayCreator('Ta',AElementType,true);
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertArrayCreatorImplementation('Ta',AJSONType,'',True);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertArrayLoaderImplementationStart('TMyObject','TJSONValue','a','Ta',AJSONType);
+  AssertTrue('Have "a" property if',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" property set with createarray', Pos('a:=CreateTa(P.Value);',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+class function TTestGenCode.GetDataName(IsDelphi: Boolean): string;
+
+begin
+  if IsDelphi then
+    Result:='TJSONValue'
+  else
+    Result:='TJSONData';
+end;
+
+procedure TTestGenCode.AssertLoadArray(AElementType, AJSONtype: String;
+  IsDelphi: Boolean = False);
+
+Var
+  DN : String;
+
+begin
+  AssertUnitHeader;
+  DN:=GetDataName(IsDelphi);
+  AssertArrayType('Ta',AElementType);
+  AssertArrayCreator('Ta',AElementType,IsDelphi);
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration(DN);
+  AssertLoaderDeclaration(DN);
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertArrayCreatorImplementation('Ta',AJSONType,'',IsDelphi);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject',DN);
+  AssertArrayLoaderImplementationStart('TMyObject',DN,'a','Ta',AJSONType,isDelphi);
+  if IsDelphi then
+    begin
+    AssertTrue('Have "a" property if',Pos('If (PN=''a'') then',NextLine)>0);
+    AssertTrue('Have "a" property set with createarray', Pos('a:=CreateTa(P.JSONValue);',NextLine)>0);
+    end
+  else
+    begin
+    AssertTrue('Have "a" array property case',Pos('''a'':',NextLine)>0);
+    AssertTrue('Have "a" property set with createarray', Pos('a:=CreateTa(E.Value);',NextLine)>0);
+    end;
+  AssertLoaderImplementationEnd(IsDelphi);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+procedure TTestGenCode.AssertSaveArray(AElementType, AJSONtype: String; IsDelphi: Boolean = False);
+
+Var
+  DN : String;
+
+begin
+  AssertUnitHeader;
+  DN:=GetDataName(IsDelphi);
+  AssertArrayType('Ta',AElementType);
+  AssertArraySaver('Ta',AElementType,IsDelphi);
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertArraySaverImplementation('Ta',AJSONType,'',IsDelphi);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  if IsDelphi then
+    AssertTrue('Array save statement', Pos('AJSON.AddPair(''a'',SaveTaToJSON(a));',NextLine)>0)
+  else
+    AssertTrue('Array save statement', Pos('AJSON.Add(''a'',SaveTaToJSON(a));',NextLine)>0);
+  AssertEnd('Saver');
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+procedure TTestGenCode.TestLoadStringArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : [ "abc" ] }');
+  AssertLoadArray('string','AsString');
+end;
+
+procedure TTestGenCode.TestLoadBooleanArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : [ true ] }');
+  AssertLoadArray('boolean','AsBoolean');
+end;
+
+procedure TTestGenCode.TestLoadIntegerArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : [ 123 ] }');
+  AssertLoadArray('Integer','AsInteger');
+end;
+
+procedure TTestGenCode.TestLoadInt64ArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : [ 1234567890123 ] }');
+  AssertLoadArray('Int64','AsInt64');
+end;
+
+procedure TTestGenCode.TestLoadFloatArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" : [ 12.34 ] }');
+  AssertLoadArray('Double','AsFloat');
+end;
+
+procedure TTestGenCode.TestLoadObjectArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad];
+  GenCode('{ "a" :  [ { "b" : "abc" } ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItem','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertArrayType('Ta','TaItem');
+  AssertArrayCreator('Ta','TaItem');
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONData');
+  AssertLoaderDeclaration('TJSONData');
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TaItem');
+  AssertLoadConstructorImplementationStart('TAItem','TJSONData');
+  AssertLoaderImplementationStart('TaItem','TJSONData');
+  AssertTrue('Have "b" string property case',Pos('''b'':',NextLine)>0);
+  AssertTrue('Have "b" string property set', Pos('b:=E.Value.AsString;',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertArrayCreatorImplementation('Ta','','TaItem');
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONData');
+  AssertObjectLoaderImplementationStart('TMyObject','TJSONData','a','Ta','');
+  AssertTrue('Have "a" stringarray property case',Pos('''a'':',NextLine)>0);
+  AssertTrue('Have "a" property set with createarray', Pos('a:=CreateTa(E.Value);',NextLine)>0);
+  AssertLoaderImplementationEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+
+procedure TTestGenCode.TestLoadDelphiIntegerProperty;
+
+Var
+  S : String;
+
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case ',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('a:=P.JSONValue.GetValue<Integer>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphi2IntegersProperty;
+
+Var
+  S : String;
+
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case ',Pos('If (PN=''a'') then',NextLine)>0);
+  S:=NextLine;
+  AssertTrue('Have "a" integer property set', Pos('a:=P.JSONValue.GetValue<Integer>',S)>0);
+  AssertTrue('Have no semicolon', Pos(';',S)=0);
+  AssertTrue('Have else  "b" integer property case ',Pos('Else If (PN=''b'') then',NextLine)>0);
+  AssertTrue('Have "b" integer property set', Pos('b:=P.JSONValue.GetValue<Integer>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiIntegerWithErrorProperty;
+
+Var
+  S : String;
+
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON,jpoUnknownLoadPropsError];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case ',Pos('If (PN=''a'') then',NextLine)>0);
+  S:=NextLine;
+  AssertTrue('Have "a" integer property set', Pos('a:=P.JSONValue.GetValue<Integer>',S)>0);
+  AssertTrue('Have no semicolon for a', Pos(';',S)=0);
+  AssertTrue('Have "b" integer property case ',Pos('If (PN=''b'') then',NextLine)>0);
+  S:=NextLine;
+  AssertTrue('Have "b" integer property set', Pos('b:=P.JSONValue.GetValue<Integer>',S)>0);
+  AssertTrue('Have no semicolon for b', Pos(';',S)=0);
+  AssertTrue('Have case else',Pos('else',NextLine)>0);
+  AssertTrue('Have raise statement', Pos('Raise EJSONException.CreateFmt',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiIntegerCaseInsensitiveProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON,jpoLoadCaseInsensitive];
+  GenCode('{ "A" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('A','integer');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('A','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "A" integer property set', Pos('A:=P.JSONValue.GetValue<Integer>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('A','Integer','A','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiStringProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : "1234" }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','String');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','string',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" integer property set', Pos('a:=P.JSONValue.GetValue<String>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','String','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiBooleanProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : true }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','boolean');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','boolean',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" integer property set',Pos('a:=P.JSONValue.GetValue<Boolean>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Boolean','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiInt64Property;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Int64');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','Int64',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" integer property set',Pos('a:=P.JSONValue.GetValue<Int64>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiFloatProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : 1.1 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Double');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','Double',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertLoaderImplementationStart('TMyObject','TJSONValue',True);
+  AssertTrue('Have "a" integer property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" integer property set',Pos('a:=P.JSONValue.GetValue<Double>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Double','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiObjectProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : { "b" : "abc" } }');
+  AssertUnitHeader;
+  AssertClassHeader('Ta','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','Ta');
+  AssertLoadConstructorImplementationStart('Ta','TJSONValue');
+  AssertLoaderImplementationStart('Ta','TJSONValue',True);
+  AssertTrue('Have "b" string property case',Pos('If (PN=''b'') then',NextLine)>0);
+  AssertTrue('Have "b" string property set', Pos('b:=P.JSONValue.GetValue<String>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertObjectLoaderImplementationStart('TMyObject','TJSONValue','a','Ta','',True);
+  AssertTrue('Have "a" object property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" object create createfromjson', Pos('a:=ta.CreateFromJSON(P.JSONValue);',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.TestLoadDelphiObjectArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad,jpoDelphiJSON];
+  GenCode('{ "a" : [ { "b" : "abc" } ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItem','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertArrayType('Ta','TaItem');
+  AssertArrayCreator('Ta','TaItem',True);
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertLoadConstructorDeclaration('TJSONValue');
+  AssertLoaderDeclaration('TJSONValue');
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TaItem');
+  AssertLoadConstructorImplementationStart('TAItem','TJSONValue');
+  AssertLoaderImplementationStart('TaItem','TJSONValue',True);
+  AssertTrue('Have "b" object property case',Pos('If (PN=''b'') then',NextLine)>0);
+  AssertTrue('Have "b" object property set', Pos('b:=P.JSONValue.GetValue<String>;',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertArrayCreatorImplementation('Ta','','TaItem',True);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertLoadConstructorImplementationStart('TMyObject','TJSONValue');
+  AssertObjectLoaderImplementationStart('TMyObject','TJSONValue','a','Ta','',True);
+  AssertTrue('Have "a" object property case',Pos('If (PN=''a'') then',NextLine)>0);
+  AssertTrue('Have "a" property set with createarray', Pos('a:=CreateTa(P.JSONValue);',NextLine)>0);
+  AssertLoaderImplementationEnd(True);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+procedure TTestGenCode.TestSaveIntegerProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+end;
+
+procedure TTestGenCode.TestSave2IntegersProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('Have "b" integer property save', Pos('AJSON.Add(''b'',b);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestSaveStringProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : "1234" }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','string');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','string',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','String','a','');
+end;
+
+procedure TTestGenCode.TestSaveBooleanProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : true }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Boolean');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','Boolean',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" boolean property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Boolean','a','');
+end;
+
+procedure TTestGenCode.TestSaveInt64Property;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Int64');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','Int64',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" int64 property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestSaveFloatProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : 1.2 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','double');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','double',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.Add(''a'',a);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Double','a','');
+
+end;
+
+procedure TTestGenCode.TestSaveObjectProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : { "b" : "abc" } }');
+  AssertUnitHeader;
+  AssertClassHeader('Ta','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertSaverDeclaration;
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','Ta');
+  AssertSaverImplementationStart('Ta');
+  AssertTrue('Have "b" property save', Pos('AJSON.Add(''b'',b);',NextLine)>0);
+  AssertEnd;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have check for assigned object property save', Pos('if Assigned(a) then',NextLine)>0);
+  AssertTrue('Have "a" object property save', Pos('AJSON.Add(''a'',a.SaveToJSON);',NextLine)>0);
+  AssertEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.TestSaveStringArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : [ "abc" ] }');
+  AssertSaveArray('string','');
+end;
+
+procedure TTestGenCode.TestSaveBooleanArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : [ true ] }');
+  AssertSaveArray('boolean','');
+end;
+
+procedure TTestGenCode.TestSaveIntegerArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : [ 123 ] }');
+  AssertSaveArray('Integer','');
+end;
+
+procedure TTestGenCode.TestSaveInt64ArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : [ 1234567890123 ] }');
+  AssertSaveArray('Int64','');
+end;
+
+procedure TTestGenCode.TestSaveFloatArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" : [ 1.23] }');
+  AssertSaveArray('Double','');
+end;
+
+procedure TTestGenCode.TestSaveObjectArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave];
+  GenCode('{ "a" :  [ { "b" : "abc" } ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItem','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertArrayType('Ta','TaItem');
+  AssertArraySaver('Ta','TaItem');
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TaItem');
+  AssertSaverImplementationStart('TaItem');
+  AssertTrue('Have "b" string property save', Pos('AJSON.Add(''b'',b);',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertArraySaverImplementation('Ta','','TaItem');
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" array property save', Pos('AJSON.Add(''a'',SaveTaToJSON(a));',NextLine)>0);
+  AssertEnd('Loader TMyObject');
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiIntegerProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : 1234 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.AddPair(''a'',TJSONNumber.Create(a));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphi2IntegersProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : 1234, "b" : 5678 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','integer');
+  AssertField('b','integer');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','integer',False);
+  AssertProperty('b','integer',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" integer property save', Pos('AJSON.AddPair(''a'',TJSONNumber.Create(a));',NextLine)>0);
+  AssertTrue('Have "b" integer property save', Pos('AJSON.AddPair(''b'',TJSONNumber.Create(b));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Integer','a','');
+  AssertPropertyMap('b','Integer','b','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiStringProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : "1234" }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','string');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','string',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" string property save', Pos('AJSON.AddPair(''a'',TJSONString.Create(a));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','String','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiBooleanProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : true }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Boolean');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','Boolean',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" Boolean property save', Pos('AJSON.AddPair(''a'',TJSONBoolean.Create(a));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Boolean','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiInt64Property;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : 1234567890123 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Int64');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','Int64',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" int64 property save', Pos('AJSON.AddPair(''a'',TJSONNumber.Create(a));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Int64','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiFloatProperty;
+Var
+  S : String;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : 1.2 }');
+  AssertUnitHeader;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','double');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','double',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  S:=NextLine;
+  AssertTrue('Have "a" float property save', Pos('AJSON.AddPair(''a'',TJSONNumber.Create(a));',S)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Double','a','');
+end;
+
+procedure TTestGenCode.TestSaveDelphiObjectProperty;
+Var
+  S : String;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : { "b" : "abc" } }');
+  AssertUnitHeader;
+  AssertClassHeader('Ta','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertDestructor;
+  AssertSaverDeclaration;
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','Ta');
+  AssertSaverImplementationStart('Ta');
+  AssertTrue('Have "b" string property save', Pos('AJSON.AddPair(''b'',TJSONString.Create(b));',NextLine)>0);
+  AssertEnd;
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertDestructorImplementation('TMyObject',['a']);
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have check for assigned object property save', Pos('if Assigned(a) then',NextLine)>0);
+  S:=NextLine;
+  AssertTrue('Have "a" object property save', Pos('AJSON.AddPair(''a'',a.SaveToJSON);',S)>0);
+  AssertEnd;
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','TObject');
+end;
+
+procedure TTestGenCode.TestSaveDelphiStringArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : [ "abc" ] }');
+  AssertSaveArray('string','',True);
+end;
+
+procedure TTestGenCode.TestSaveDelphiBooleanArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : [ true ] }');
+  AssertSaveArray('boolean','',True);
+end;
+
+procedure TTestGenCode.TestSaveDelphiIntegerArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : [ 123 ] }');
+  AssertSaveArray('Integer','',True);
+end;
+
+procedure TTestGenCode.TestSaveDelphiInt64ArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : [ 1234567890123 ] }');
+  AssertSaveArray('Int64','',True);
+end;
+
+procedure TTestGenCode.TestSaveDelphiFloatArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" : [ 1.23] }');
+  AssertSaveArray('Double','',True);
+end;
+
+procedure TTestGenCode.TestSaveDelphiObjectArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateSave,jpoDelphiJSON];
+  GenCode('{ "a" :  [ { "b" : "abc" } ] }');
+  AssertUnitHeader;
+  AssertClassHeader('TaItem','TObject');
+  AssertField('b','String');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('b','String',False);
+  AssertEnd;
+  AssertArrayType('Ta','TaItem');
+  AssertArraySaver('Ta','TaItem',True);
+  AssertType;
+  AssertClassHeader('TMyObject','TObject');
+  AssertField('a','Ta');
+  AssertVisibility('public');
+  AssertSaverDeclaration;
+  AssertProperty('a','ta',False);
+  AssertEnd;
+  AssertImplementation;
+  AssertClassComment('Object Implementation','TaItem');
+  AssertSaverImplementationStart('TaItem',True);
+  AssertTrue('Have "b" string property save', Pos('AJSON.AddPair(''b'',TJSONString.Create(b));',NextLine)>0);
+  AssertTrue('end',Pos('end;',NextLine)>0);
+  AssertArraySaverImplementation('Ta','','TaItem',True);
+  AssertClassComment('Object Implementation','TMyObject');
+  AssertSaverImplementationStart('TMyObject');
+  AssertTrue('Have "a" array property save', Pos('AJSON.AddPair(''a'',SaveTaToJSON(a));',NextLine)>0);
+  AssertEnd('Loader TMyObject');
+  AssertUnitEnd;
+  AssertPropertyMap('','TMyObject','','TObject');
+  AssertPropertyMap('a','Ta','a','');
+end;
+
+procedure TTestGenCode.TestLoadDelphiStringArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad, jpoDelphiJSON];
+  GenCode('{ "a" : [ "abc" ] }');
+  AssertLoadArray('string','String',True);
+end;
+
+procedure TTestGenCode.TestLoadDelphiBooleanArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad, jpoDelphiJSON];
+  GenCode('{ "a" : [ true ] }');
+  AssertLoadArray('boolean','Boolean',True);
+end;
+
+procedure TTestGenCode.TestLoadDelphiIntegerArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad, jpoDelphiJSON];
+  GenCode('{ "a" : [ 12 ] }');
+  AssertLoadArray('integer','Integer',True);
+end;
+
+procedure TTestGenCode.TestLoadDelphiInt64ArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad, jpoDelphiJSON];
+  GenCode('{ "a" : [ 1234567890123 ] }');
+  AssertLoadArray('int64','Int64',True);
+end;
+
+procedure TTestGenCode.TestLoadDelphiFloatArrayProperty;
+begin
+  Gen.Options:=[jpoGenerateLoad, jpoDelphiJSON];
+  GenCode('{ "a" : [ 1.1 ] }');
+  AssertLoadArray('double','Double',True);
+end;
+
+
+initialization
+
+  RegisterTest(TTestGenCode);
+end.
+

+ 70 - 0
packages/fcl-json/tests/testjson2code.lpi

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="9"/>
+    <General>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="testjson2code"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <i18n>
+      <EnableI18N LFM="False"/>
+    </i18n>
+    <VersionInfo>
+      <StringTable ProductVersion=""/>
+    </VersionInfo>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <FormatVersion Value="1"/>
+        <CommandLineParams Value="--suite=TestLoadObjectProperty"/>
+      </local>
+    </RunParams>
+    <RequiredPackages Count="1">
+      <Item1>
+        <PackageName Value="FCL"/>
+      </Item1>
+    </RequiredPackages>
+    <Units Count="3">
+      <Unit0>
+        <Filename Value="testjson2code.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="tcjsontocode.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="../src/fpjsontopas.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit2>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 52 - 0
packages/fcl-json/tests/testjson2code.lpr

@@ -0,0 +1,52 @@
+program testjson2code;
+
+{$mode objfpc}{$H+}
+
+uses
+  Classes, consoletestrunner, tcjsontocode, fpjsontopas;
+
+type
+
+  { TLazTestRunner }
+
+  { TMyTestRunner }
+
+  TMyTestRunner = class(TTestRunner)
+  protected
+    function GetShortOpts: string; override;
+    procedure AppendLongOpts; override;
+    procedure DoRun; override;
+  end;
+
+var
+  Application: TMyTestRunner;
+
+{ TMyTestRunner }
+
+function TMyTestRunner.GetShortOpts: string;
+begin
+  Result:=inherited GetShortOpts;
+  Result:=Result+'t:';
+end;
+
+procedure TMyTestRunner.AppendLongOpts;
+begin
+  inherited AppendLongOpts;
+  LongOpts.Add('testunitdir:');
+end;
+
+procedure TMyTestRunner.DoRun;
+begin
+  TestUnitDir:=GetOptionValue('t','testunitdir');
+  inherited DoRun;
+end;
+
+begin
+  DefaultFormat:=fPlain;
+  DefaultRunAllTests:=True;
+  Application := TMyTestRunner.Create(nil);
+  Application.Initialize;
+  Application.Title := 'FPCUnit Console test runner';
+  Application.Run;
+  Application.Free;
+end.

+ 23 - 0
packages/fcl-json/tests/testjsondata.pp

@@ -256,6 +256,7 @@ type
     Procedure TestFormat;
     Procedure TestFormatNil;
     Procedure TestFind;
+    Procedure TestIfFind;
   end;
 
   { TTestJSONPath }
@@ -3553,6 +3554,28 @@ begin
   end;
 end;
 
+Procedure TTestObject.TestIfFind;
+Var
+  J: TJSONObject;
+  B: TJSONBoolean;
+  S: TJSONString;
+  N: TJSONNumber;
+  D: TJSONData;
+begin
+  J:=TJSONObject.Create(['s', 'astring', 'b', true, 'n', 1]);
+  try
+    TestJSONType(J,jtObject);
+    TestIsNull(J,False);
+    TestItemCount(J,3);
+    AssertEquals('boolean found', true, j.Find('b', B));
+    AssertEquals('string found', true, j.Find('s', S));
+    AssertEquals('number found', true, j.Find('n', N));
+    AssertEquals('data found', true, j.Find('s', D));
+  finally
+    FreeAndNil(J);
+  end;
+end;
+
 
 procedure TTestObject.TestCreateString;
 

+ 3 - 11
packages/fcl-web/examples/echo/cgi/echo.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -31,7 +31,6 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="\usr\bin\xterm -T 'Lazarus Run Output' -e $(LazarusDir)\tools\runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <RequiredPackages Count="1">
@@ -43,29 +42,22 @@
       <Unit0>
         <Filename Value="echo.lpr"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="echo"/>
       </Unit0>
       <Unit1>
         <Filename Value="..\webmodule\wmecho.pas"/>
         <IsPartOfProject Value="True"/>
         <ComponentName Value="EchoModule"/>
         <ResourceBaseClass Value="DataModule"/>
-        <UnitName Value="wmecho"/>
       </Unit1>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <PathDelim Value="\"/>
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="..\webmodule"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

BIN
packages/fcl-web/examples/echo/cgi/echo.res


+ 22 - 172
packages/fcl-web/examples/httpapp/testhttp.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -6,13 +6,10 @@
       <Flags>
         <MainUnitHasCreateFormStatements Value="False"/>
         <MainUnitHasTitleStatement Value="False"/>
-        <UseDefaultCompilerOptions Value="True"/>
       </Flags>
       <MainUnit Value="0"/>
       <ResourceType Value="res"/>
       <UseXPManifest Value="True"/>
-      <Icon Value="0"/>
-      <ActiveWindowIndexAtStart Value="0"/>
     </General>
     <i18n>
       <EnableI18N LFM="False"/>
@@ -31,269 +28,122 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="12">
       <Unit0>
         <Filename Value="testhttp.pp"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="testhttp"/>
-        <EditorIndex Value="0"/>
-        <WindowIndex Value="0"/>
-        <TopLine Value="1"/>
-        <CursorPos X="16" Y="5"/>
+        <IsVisibleTab Value="True"/>
+        <CursorPos X="41" Y="6"/>
         <UsageCount Value="20"/>
         <Loaded Value="True"/>
       </Unit0>
       <Unit1>
         <Filename Value="fpwebfile.pp"/>
-        <UnitName Value="fpwebfile"/>
-        <EditorIndex Value="10"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="8"/>
         <CursorPos X="22" Y="14"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit1>
       <Unit2>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttp.pp"/>
-        <UnitName Value="fphttp"/>
-        <EditorIndex Value="11"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="188"/>
-        <CursorPos X="1" Y="196"/>
+        <CursorPos Y="196"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit2>
       <Unit3>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpapp.pp"/>
-        <UnitName Value="fphttpapp"/>
-        <EditorIndex Value="6"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="14"/>
         <CursorPos X="31" Y="20"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit3>
       <Unit4>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/custhttpapp.pp"/>
-        <UnitName Value="custhttpapp"/>
-        <EditorIndex Value="7"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="35"/>
         <CursorPos X="30" Y="39"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit4>
       <Unit5>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpserver.pp"/>
-        <UnitName Value="fphttpserver"/>
-        <EditorIndex Value="8"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="18"/>
         <CursorPos X="24" Y="39"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit5>
       <Unit6>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
         <UnitName Value="HTTPDefs"/>
-        <EditorIndex Value="9"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="1005"/>
         <CursorPos X="42" Y="1038"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit6>
       <Unit7>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <UnitName Value="reglazwebextra"/>
-        <IsVisibleTab Value="True"/>
-        <EditorIndex Value="1"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="218"/>
         <CursorPos X="29" Y="235"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit7>
       <Unit8>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/weblazideintf.pp"/>
         <UnitName Value="WebLazIDEIntf"/>
-        <EditorIndex Value="5"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="623"/>
-        <CursorPos X="1" Y="642"/>
+        <CursorPos Y="642"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit8>
       <Unit9>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
         <ComponentName Value="NewHTTPApplicationForm"/>
         <ResourceBaseClass Value="Form"/>
-        <UnitName Value="frmnewhttpapp"/>
-        <EditorIndex Value="3"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="25"/>
         <CursorPos X="34" Y="104"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
         <LoadedDesigner Value="True"/>
       </Unit9>
       <Unit10>
         <Filename Value="../../../../../projects/lazarus/components/fpweb/fpwebstrconsts.pas"/>
         <UnitName Value="fpWebStrConsts"/>
-        <EditorIndex Value="4"/>
-        <WindowIndex Value="0"/>
+        <EditorIndex Value="-1"/>
         <TopLine Value="92"/>
         <CursorPos X="22" Y="121"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit10>
       <Unit11>
         <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <UnitName Value="fpwebdata"/>
-        <EditorIndex Value="2"/>
-        <WindowIndex Value="0"/>
-        <TopLine Value="1"/>
+        <EditorIndex Value="-1"/>
         <CursorPos X="14" Y="15"/>
         <UsageCount Value="10"/>
-        <Loaded Value="True"/>
       </Unit11>
     </Units>
-    <JumpHistory Count="30" HistoryIndex="29">
+    <JumpHistory Count="3" HistoryIndex="2">
       <Position1>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="23" Column="3" TopLine="1"/>
+        <Caret Line="23" Column="3"/>
       </Position1>
       <Position2>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="8" Column="44" TopLine="1"/>
+        <Caret Line="8" Column="44"/>
       </Position2>
       <Position3>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="1" Column="1" TopLine="1"/>
-      </Position3>
-      <Position4>
         <Filename Value="testhttp.pp"/>
-        <Caret Line="29" Column="44" TopLine="1"/>
-      </Position4>
-      <Position5>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="63" Column="22" TopLine="48"/>
-      </Position5>
-      <Position6>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/custhttpapp.pp"/>
-        <Caret Line="39" Column="30" TopLine="35"/>
-      </Position6>
-      <Position7>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/fphttpserver.pp"/>
-        <Caret Line="35" Column="38" TopLine="18"/>
-      </Position7>
-      <Position8>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="623" Column="33" TopLine="613"/>
-      </Position8>
-      <Position9>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="1012" Column="7" TopLine="1009"/>
-      </Position9>
-      <Position10>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="281" Column="71" TopLine="263"/>
-      </Position10>
-      <Position11>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="1014" Column="21" TopLine="1010"/>
-      </Position11>
-      <Position12>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/base/httpdefs.pp"/>
-        <Caret Line="660" Column="21" TopLine="627"/>
-      </Position12>
-      <Position13>
-        <Filename Value="fpwebfile.pp"/>
-        <Caret Line="77" Column="60" TopLine="48"/>
-      </Position13>
-      <Position14>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="86" Column="29" TopLine="58"/>
-      </Position14>
-      <Position15>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/weblazideintf.pp"/>
-        <Caret Line="549" Column="14" TopLine="547"/>
-      </Position15>
-      <Position16>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="54" Column="17" TopLine="36"/>
-      </Position16>
-      <Position17>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="41" Column="20" TopLine="41"/>
-      </Position17>
-      <Position18>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="27" Column="29" TopLine="1"/>
-      </Position18>
-      <Position19>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="93" Column="28" TopLine="76"/>
-      </Position19>
-      <Position20>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="39" Column="43" TopLine="21"/>
-      </Position20>
-      <Position21>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="97" Column="29" TopLine="68"/>
-      </Position21>
-      <Position22>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <Caret Line="1" Column="1" TopLine="1"/>
-      </Position22>
-      <Position23>
-        <Filename Value="../../../../../FPC/trunk/packages/fcl-web/src/webdata/fpwebdata.pp"/>
-        <Caret Line="15" Column="14" TopLine="1"/>
-      </Position23>
-      <Position24>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="203" Column="23" TopLine="184"/>
-      </Position24>
-      <Position25>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="66" Column="10" TopLine="59"/>
-      </Position25>
-      <Position26>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="71" Column="24" TopLine="39"/>
-      </Position26>
-      <Position27>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/frmnewhttpapp.pp"/>
-        <Caret Line="75" Column="18" TopLine="58"/>
-      </Position27>
-      <Position28>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="111" Column="66" TopLine="95"/>
-      </Position28>
-      <Position29>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="186" Column="15" TopLine="160"/>
-      </Position29>
-      <Position30>
-        <Filename Value="../../../../../projects/lazarus/components/fpweb/reglazwebextra.pp"/>
-        <Caret Line="200" Column="3" TopLine="184"/>
-      </Position30>
+        <Caret Line="29" Column="44"/>
+      </Position3>
     </JumpHistory>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <Parsing>
       <SyntaxOptions>
         <UseAnsiStrings Value="False"/>
       </SyntaxOptions>
     </Parsing>
-    <Other>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 1 - 1
packages/fcl-web/examples/httpapp/testhttp.pp

@@ -3,7 +3,7 @@
 program testhttp;
 
 uses
-  SysUtils, fphttpapp, fpwebfile;
+  SysUtils, fphttpapp, fpwebfile, wmecho;
 
 Procedure Usage;
 

+ 1 - 9
packages/fcl-web/examples/httpclient/httpget.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -32,14 +32,12 @@
       <local>
         <FormatVersion Value="1"/>
         <CommandLineParams Value="http://home/~michael/redirect.cgi out"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="1">
       <Unit0>
         <Filename Value="httpget.pas"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="httpget"/>
       </Unit0>
     </Units>
   </ProjectOptions>
@@ -48,12 +46,6 @@
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 2 - 11
packages/fcl-web/examples/httpserver/simplehttpserver.lpi

@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <ProjectOptions>
     <Version Value="9"/>
@@ -13,7 +13,6 @@
       <Title Value="Simple HTTP server demo"/>
       <ResourceType Value="res"/>
       <UseXPManifest Value="True"/>
-      <Icon Value="0"/>
     </General>
     <i18n>
       <EnableI18N LFM="False"/>
@@ -32,28 +31,20 @@
     <RunParams>
       <local>
         <FormatVersion Value="1"/>
-        <LaunchingApplication PathPlusParams="/usr/bin/xterm -T 'Lazarus Run Output' -e $(LazarusDir)/tools/runwait.sh $(TargetCmdLine)"/>
       </local>
     </RunParams>
     <Units Count="1">
       <Unit0>
         <Filename Value="simplehttpserver.pas"/>
         <IsPartOfProject Value="True"/>
-        <UnitName Value="simplehttpserver"/>
       </Unit0>
     </Units>
   </ProjectOptions>
   <CompilerOptions>
-    <Version Value="10"/>
+    <Version Value="11"/>
     <SearchPaths>
       <IncludeFiles Value="$(ProjOutDir)"/>
     </SearchPaths>
-    <Other>
-      <CompilerMessages>
-        <UseMsgFile Value="True"/>
-      </CompilerMessages>
-      <CompilerPath Value="$(CompPath)"/>
-    </Other>
   </CompilerOptions>
   <Debugging>
     <Exceptions Count="3">

+ 22 - 0
packages/fcl-web/examples/routing/README

@@ -0,0 +1,22 @@
+This demo demonstrates the routing mechanism of fpWeb.
+
+It can be run as a CGI or as a HTTP standalone server program.
+
+In order to get a correct set of routes in the demo, demorouting.ini file
+must be configured correctly and placed next to the binary.
+
+There is a different section for each type of binary: (CGI or Standalone)
+
+Each section needs at least the BaseURL key, this is the URL where the
+application can be reached.
+
+Example:
+
+[CGI]
+; Assuming the demo is in cgi-bin
+BaseURL=http://localhost/cgi-bin/demorouting.cgi
+
+[Standalone]
+Port=8080
+; Optional, the following is the default.
+;BaseURL=http://localhost:8080/

+ 69 - 0
packages/fcl-web/examples/routing/demorouting.lpi

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="9"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="demorouting"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <VersionInfo>
+      <StringTable ProductVersion=""/>
+    </VersionInfo>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <FormatVersion Value="1"/>
+      </local>
+    </RunParams>
+    <Units Count="3">
+      <Unit0>
+        <Filename Value="demorouting.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="routes.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="../../src/base/httproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit2>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="demorouting"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../../src/base"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 34 - 0
packages/fcl-web/examples/routing/demorouting.lpr

@@ -0,0 +1,34 @@
+program demorouting;
+
+{$DEFINE STANDALONE}
+
+uses
+  sysutils,
+  routes,
+{$IFDEF STANDALONE}
+  fphttpapp,
+{$ENDIF}
+{$IFDEF CGI}
+  fpcgi,
+{$ENDIF}
+  inifiles;
+
+
+begin
+  With TInifile.Create(ChangeFileExt(ParamStr(0),'.ini')) do
+    try
+      {$IFDEF CGI}
+      BaseURL:=ReadString('CGI','BaseURL','');
+      {$ENDIF CGI}
+      {$IFDEF STANDALONE}
+      Application.Port:=ReadInteger('Standalone','Port',8080);
+      BaseURL:=ReadString('Standalone','BaseURL','http://localhost:'+IntToStr(Application.Port));
+      {$ENDIF STANDALONE}
+    finally
+      Free;
+    end;
+  RegisterRoutes;
+  Application.Initialize;
+  Application.Run;
+end.
+

+ 203 - 0
packages/fcl-web/examples/routing/routes.pp

@@ -0,0 +1,203 @@
+unit routes;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+ sysutils, classes, httpdefs, httproute;
+
+Var
+  BaseURL : String;
+
+Procedure RegisterRoutes;
+
+implementation
+
+uses webutil, fphttp;
+
+Type
+
+  { TMyModule }
+
+  TMyModule = Class(TCustomHTTPModule)
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+  
+  { TMyIntf }
+
+  TMyIntf = Class(TObject,IRouteInterface)
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+  end;
+
+  { TMyHandler }
+
+  TMyHandler = Class(TRouteObject)
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);override;
+  end;
+
+Var
+  C1,C2 : TComponent;
+  MyIntf : TMyIntf;
+
+
+Procedure DumpRoutes(L : TStrings; AURL : String);
+
+  Function DefaultReps(S : String) : string;
+
+  begin
+    Result:=StringReplace(S,'*path','somepath',[]);
+    Result:=StringReplace(Result,':param1','theparam1',[]);
+    Result:=StringReplace(Result,':param2','theparam2',[]);
+    Result:=StringReplace(Result,':param','theparam',[]);
+    If (Result<>'') and (Result[1]='/') then
+      Delete(Result,1,1);
+  end;
+
+Var
+  I : Integer;
+  P : String;
+
+begin
+  THTTPRouter.SanitizeRoute(AURL);
+  L.Add('<A NAME="routes"/>');
+  L.Add('<H1>Try these routes:</H1>');
+  For I:=0 to HTTPRouter.RouteCount-1 do
+    begin
+    P:=DefaultReps(HTTPRouter[i].URLPattern);
+    L.Add('<A HREF="'+BaseURL+'/'+P+'">'+P+'</a><br>');
+    end;
+end;
+
+Procedure RequestToResponse(ATitle : String; ARequest : TRequest; AResponse : TResponse; RouteParams : Array of String);
+
+Var
+  L : TStrings;
+  S : String;
+
+begin
+  L:=TStringList.Create;
+  try
+    L.Add('<HTML>');
+    L.Add('<HEAD>');
+    L.Add('<TITLE>'+ATitle+'</TITLE>');
+    L.Add('</HEAD>');
+    L.Add('<BODY>');
+    L.Add('<H1>'+ATitle+'</H1>');
+    L.Add('<A HREF="#routes">Jump to routes overview</A>');
+    if (Length(RouteParams)>0) then
+      begin
+      L.Add('<H2>Routing parameters:</H2>');
+      L.Add('<table>');
+      L.Add('<tr><th>Param</th><th>Value</th></tr>');
+      for S in RouteParams do
+        L.Add('<tr><td>'+S+'</th><th>'+ARequest.RouteParams[S]+'</th></tr>');
+      L.Add('</table>');
+      end;
+    DumpRequest(ARequest,L,False);
+    DumpRoutes(L,ARequest.URL);
+    L.Add('</BODY>');
+    L.Add('</HTML>');
+    AResponse.Content:=L.Text;
+    AResponse.SendResponse;
+  finally
+    L.Free;
+  end;
+end;
+
+Procedure RequestToResponse(ATitle : String; ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse(ATitle,ARequest,AResponse,[]);
+end;
+
+Procedure SimpleCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Simple callback',ARequest,AResponse);
+end;
+
+Procedure DefaultCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Default callback (*path)',ARequest,AResponse,['path']);
+end;
+
+Procedure ParamPathMiddle(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Path in the middle (onepath/*path/new)',ARequest,AResponse,['path']);
+end;
+
+Procedure ParamPath(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Parametrized path (onepath/:param)',ARequest,AResponse,['param']);
+end;
+
+Procedure ParamPaths2(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Parametrized path (onepath/:param)',ARequest,AResponse,['param1','param2']);
+end;
+
+Procedure ComponentPath(AData : Pointer; ARequest : TRequest; AResponse : TResponse);
+
+begin
+  RequestToResponse('Component path (component: '+TComponent(AData).Name+')',ARequest,AResponse);
+end;
+
+
+
+{ TMyModule }
+
+procedure TMyModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Old-fashioned Module',ARequest,AResponse);
+end;
+
+{ TMyHandler }
+
+procedure TMyHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Route object',ARequest,AResponse);
+end;
+
+{ TMyIntf }
+
+procedure TMyIntf.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  RequestToResponse('Interface object',ARequest,AResponse);
+end;
+
+Procedure RegisterRoutes;
+
+begin
+  if (C1=Nil) then
+    begin
+    C1:=TComponent.Create(Nil);
+    C1.Name:='ComponentRoute1';
+    C2:=TComponent.Create(Nil);
+    C2.Name:='ComponentRoute2';
+    MyIntf:=TMyIntf.Create;
+    end;
+  HTTPRouter.RegisterRoute('simple',rmall,@SimpleCallBack);
+  HTTPRouter.RegisterRoute('onepath/:param',rmall,@ParamPath);
+  HTTPRouter.RegisterRoute('twopaths/:param1/:param2',rmall,@ParamPaths2);
+  HTTPRouter.RegisterRoute('onepath/*path/new',rmall,@ParamPathMiddle);
+  RegisterHTTPModule('module',TMyModule,True);
+  HTTPRouter.RegisterRoute('/component/1',C1,rmall,@ComponentPath);
+  HTTPRouter.RegisterRoute('/component/2',C2,rmall,@ComponentPath);
+  HTTPRouter.RegisterRoute('/interfaced',rmall,MyIntf);
+  HTTPRouter.RegisterRoute('/routed/object',rmall,TMyHandler);
+  // This will catch all other paths
+  HTTPRouter.RegisterRoute('*path',rmall,@DefaultCallBack,True);
+end;
+
+begin
+  FreeAndNil(C1);
+  FreeAndNil(C2);
+end.
+

+ 8 - 0
packages/fcl-web/examples/routing/sample.ini

@@ -0,0 +1,8 @@
+[CGI]
+; Assuming the demo is in cgi-bin
+BaseURL=http://localhost/cgi-bin/demorouting.cgi
+
+[Standalone]
+Port=8080
+; Optional, the following is the default.
+;BaseURL=http://localhost:8080/

+ 21 - 3
packages/fcl-web/fpmake.pp

@@ -27,6 +27,7 @@ begin
     P.Dependencies.Add('fcl-json');
     P.Dependencies.Add('fcl-net');
     P.Dependencies.Add('fcl-process');
+    P.Dependencies.Add('fcl-fpcunit');
     P.Dependencies.Add('fastcgi');
     P.Dependencies.Add('httpd22', AllOses - [amiga,aros,morphos]);
     P.Dependencies.Add('httpd24', AllOses - [amiga,aros,morphos]);
@@ -43,6 +44,14 @@ begin
     P.SourcePath.Add('src/base');
     P.SourcePath.Add('src/webdata');
     P.SourcePath.Add('src/jsonrpc');
+    P.SourcePath.Add('src/hpack');
+
+    T:=P.Targets.AddUnit('httpdefs.pp');
+    T.ResourceStrings:=true;
+    T.Dependencies.AddUnit('httpprotocol');
+
+    T:=P.Targets.AddUnit('httproute.pp');
+    T.Dependencies.AddUnit('httpdefs');
 
     T:=P.Targets.AddUnit('cgiapp.pp');
     T.ResourceStrings:=true;
@@ -90,10 +99,7 @@ begin
     T:=P.Targets.AddUnit('httpprotocol.pp');
     T:=P.Targets.AddUnit('cgiprotocol.pp');
 
-    T:=P.Targets.AddUnit('httpdefs.pp');
-    T.Dependencies.AddUnit('httpprotocol');
     
-    T.ResourceStrings:=true;
     T:=P.Targets.AddUnit('iniwebsession.pp');
     T.ResourceStrings:=true;
       with T.Dependencies do
@@ -115,6 +121,7 @@ begin
       begin
         ResourceStrings:=true;
         Dependencies.AddUnit('httpdefs');
+        Dependencies.AddUnit('httproute');
         Dependencies.AddUnit('fphttp');
       end;
     with P.Targets.AddUnit('webpage.pp') do
@@ -253,6 +260,17 @@ begin
     T.Dependencies.AddUnit('fpwebclient');
     T:=P.Targets.AddUnit('restbase.pp');
     T:=P.Targets.AddUnit('restcodegen.pp');
+
+    T:=P.Targets.AddUnit('uhpacktables.pp');
+    T:=P.Targets.AddUnit('uhpackimp.pp');
+    With T.Dependencies do  
+      AddUnit('uhpacktables');
+    T:=P.Targets.AddUnit('uhpack.pp');
+    With T.Dependencies do  
+      begin
+      AddUnit('uhpackimp');
+      end;
+    
 {$ifndef ALLPACKAGES}
     Run;
     end;

+ 75 - 0
packages/fcl-web/src/base/README.txt

@@ -6,6 +6,12 @@ fcl-base. See the fcl-base/texts/fptemplate.txt file.
 
 Architecture:
 
+httpprotocol:
+------------
+
+Mostly standard HTTP header definitions, and some auxiliary routines.
+
+
 httpdefs
 --------
 contains the basic HTTP system definitions: 
@@ -25,6 +31,75 @@ TResponse:
 TCustomSession: 
  Base for all session components.
 
+httproute
+---------
+
+The old Delphi style routing worked with Datamodules only. The pattern was
+strictly /modulename/actionname or through query variables: ?module=xyz&Action=nmo
+
+This old routing is still available by setting the LegacyRouting property of
+webhandler or webapplication (custweb) to true. (the new routing described
+below is then disabled)
+
+The new routing is more flexible in 3 ways.
+
+- It is no longer required to use datamodules, but this is still supported.
+  There are now 4 methods that can be used to register a route:
+
+  - Using a callback procedure
+    TRouteCallback = Procedure(ARequest: TRequest; AResponse);
+ 
+  - Using a callback event:
+    TRouteEvent = Procedure(ARequest: TRequest; AResponse) of object;
+
+  - Using an interface
+    IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
+      Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+    end;
+    Note that this is a CORBA interface, so no reference counting.
+
+  - Using a router object:
+    TRouteObject = Class(TObject,IRouteInterface)
+    Public
+      Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); virtual; abstract;
+    end;
+    TRouteObjectClass = Class of TRouteObject;
+  The object class needs to be registered. The router will instantiate the
+  object and release it once the request was handled.
+ 
+  More methods can be added, if need be.
+  All routes are registered using the HTTPRouter.RegisterRoute method.
+  it is overloaded to accept any of the above parameters.
+
+- The router can now match more complex, parametrized routes.
+
+  A route is the path part of an URL; query parameters are not examined.
+
+  /path1/path2/path3/path4
+
+  In these paths, parameters and wildcards are recognized:
+  :param means that it will match any request with a single part in this location
+  *paramm means that it will match any request with zero or more path parts in this location
+
+  examples:
+
+  /path1  
+  /REST/:Resource/:ID
+  /REST/:Resource
+  /*/something
+  /*path/somethiingelse
+  /*path  
+ 
+  The parameters will be added to TRequest, they are available in the (new) RouteParams array property of TRequest.
+  
+  Paths are matched case sensitively by default, and the first matching pattern is used.
+ 
+  HTTP Modules are registered in the router using classname/* or defaultmodulename/*
+
+- A set of methods can be added to the route registration (default is to  accept all methods). 
+  The router will  match the request method. If the method does not match, it will raise an
+  exception which will result in a 405 HTTP error.
+
 fphttp:
 -------
 Basic web system components/classes

+ 99 - 37
packages/fcl-web/src/base/custweb.pp

@@ -37,6 +37,7 @@ Type
   TWebHandler = class(TComponent)
   private
     FDefaultModuleName: String;
+    FLegacyRouting: Boolean;
     FOnIdle: TNotifyEvent;
     FOnInitModule: TInitModuleEvent;
     FOnUnknownRequestEncoding: TOnUnknownEncodingEvent;
@@ -55,6 +56,9 @@ Type
     FOnTerminate : TNotifyEvent;
     FOnLog : TLogEvent;
     FPreferModuleName : Boolean;
+    procedure DoCallModule(AModule: TCustomHTTPModule; AModuleName: String; ARequest: TRequest; AResponse: TResponse);
+    procedure HandleModuleRequest(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+    procedure OldHandleRequest(ARequest: TRequest; AResponse: TResponse);
   protected
     Class Procedure DoError(Msg : String; AStatusCode : Integer = 0; AStatusText : String = '');
     Class Procedure DoError(Fmt : String; Const Args : Array of const;AStatusCode : Integer = 0; AStatusText : String = '');
@@ -73,6 +77,7 @@ Type
     property Terminated: boolean read FTerminated;
   Public
     constructor Create(AOwner: TComponent); override;
+    Destructor Destroy; override;
     Procedure Run; virtual;
     Procedure Log(EventType : TEventType; Const Msg : String);
     Procedure DoHandleRequest(ARequest : TRequest; AResponse : TResponse);
@@ -94,6 +99,7 @@ Type
     Property OnUnknownRequestEncoding : TOnUnknownEncodingEvent Read FOnUnknownRequestEncoding Write FOnUnknownRequestEncoding;
     Property OnInitModule: TInitModuleEvent Read FOnInitModule write FOnInitModule;
     Property PreferModuleName : Boolean Read FPreferModuleName Write FPreferModuleName;
+    Property LegacyRouting : Boolean Read FLegacyRouting Write FLegacyRouting;
   end;
 
   TCustomWebApplication = Class(TCustomApplication)
@@ -107,6 +113,7 @@ Type
     function GetEmail: String;
     function GetEventLog: TEventLog;
     function GetHandleGetOnPost: Boolean;
+    function GetLegacyRouting: Boolean;
     function GetModuleVar: String;
     function GetOnGetModule: TGetModuleEvent;
     function GetOnShowRequestException: TOnShowRequestException;
@@ -120,6 +127,7 @@ Type
     procedure SetDefaultModuleName(AValue: String);
     procedure SetEmail(const AValue: String);
     procedure SetHandleGetOnPost(const AValue: Boolean);
+    procedure SetLegacyRouting(AValue: Boolean);
     procedure SetModuleVar(const AValue: String);
     procedure SetOnGetModule(const AValue: TGetModuleEvent);
     procedure SetOnShowRequestException(const AValue: TOnShowRequestException);
@@ -155,6 +163,7 @@ Type
     Property OnUnknownRequestEncoding : TOnUnknownEncodingEvent Read GetOnUnknownRequestEncoding Write SetOnUnknownRequestEncoding;
     Property EventLog: TEventLog read GetEventLog;
     Property PreferModuleName : Boolean Read GetPreferModuleName Write SetPreferModuleName;
+    Property LegacyRouting : Boolean Read GetLegacyRouting Write SetLegacyRouting;
   end;
 
   EFPWebError = Class(EFPHTTPError);
@@ -163,10 +172,12 @@ procedure ExceptionToHTML(S: TStrings; const E: Exception; const Title, Email, A
 
 Implementation
 
-{$ifdef CGIDEBUG}
+
 uses
-  dbugintf;
-{$endif}
+  {$ifdef CGIDEBUG}
+  dbugintf,
+  {$endif}
+  httproute;
 
 resourcestring
   SErrNoModuleNameForRequest = 'Could not determine HTTP module name for request';
@@ -302,54 +313,89 @@ begin
   Result := FAdministrator;
 end;
 
-Procedure TWebHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+Procedure TWebHandler.DoCallModule(AModule : TCustomHTTPModule; AModuleName : String ; ARequest: TRequest; AResponse: TResponse);
+
+begin
+  SetBaseURL(AModule,AModuleName,ARequest);
+  if (OnInitModule<>Nil) then
+    OnInitModule(Self,AModule);
+  AModule.DoAfterInitModule(ARequest);
+  if AModule.Kind=wkOneShot then
+    begin
+    try
+      AModule.HandleRequest(ARequest,AResponse);
+    finally
+      AModule.Free;
+    end;
+    end
+  else
+    AModule.HandleRequest(ARequest,AResponse);
+end;
+
+Procedure TWebHandler.HandleModuleRequest(Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse);
+
 Var
   MC : TCustomHTTPModuleClass;
   M  : TCustomHTTPModule;
   MN : String;
-  MI : TModuleItem;
+
+begin
+  MC:=Sender.ModuleClass;
+  MN:=Sender.ModuleName;
+  // Modules expect the path info to contain the action name as the first part. (See getmodulename);
+  ARequest.GetNextPathInfo;
+  if Sender.SkipStreaming then
+    M:=MC.CreateNew(Self)
+  else
+    M:=MC.Create(Self);
+  DoCallModule(M,MN,ARequest,AResponse);
+end;
+
+Procedure TWebHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
 
 begin
   try
-    MC:=Nil;
-    M:=NIL;
-    MI:=Nil;
-    If (OnGetModule<>Nil) then
-      OnGetModule(Self,ARequest,MC);
-    If (MC=Nil) then
-      begin
-      MN:=GetModuleName(ARequest);
-      MI:=ModuleFactory.FindModule(MN);
-      if (MI=Nil) then
-        DoError(SErrNoModuleForRequest,[MN],400,'Not found');
-      MC:=MI.ModuleClass;
-      end;
-    M:=FindModule(MC); // Check if a module exists already
-    If (M=Nil) then
-      if assigned(MI) and Mi.SkipStreaming then
-        M:=MC.CreateNew(Self)
-      else
-        M:=MC.Create(Self);
-    SetBaseURL(M,MN,ARequest);
-    if (OnInitModule<>Nil) then
-      OnInitModule(Self,M);
-    M.DoAfterInitModule(ARequest);
-    if M.Kind=wkOneShot then
-      begin
-      try
-        M.HandleRequest(ARequest,AResponse);
-      finally
-        M.Free;
-      end;
-      end
+    if LegacyRouting then
+      OldHandleRequest(ARequest,AResponse)
     else
-      M.HandleRequest(ARequest,AResponse);
+      HTTPRouter.RouteRequest(ARequest,AResponse);
   except
     On E : Exception do
       ShowRequestException(AResponse,E);
   end;
 end;
 
+Procedure TWebHandler.OldHandleRequest(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  MC : TCustomHTTPModuleClass;
+  M  : TCustomHTTPModule;
+  MN : String;
+  MI : TModuleItem;
+
+begin
+  MC:=Nil;
+  M:=NIL;
+  MI:=Nil;
+  If (OnGetModule<>Nil) then
+    OnGetModule(Self,ARequest,MC);
+  If (MC=Nil) then
+    begin
+    MN:=GetModuleName(ARequest);
+    MI:=ModuleFactory.FindModule(MN);
+    if (MI=Nil) then
+      DoError(SErrNoModuleForRequest,[MN],400,'Not found');
+    MC:=MI.ModuleClass;
+    end;
+  M:=FindModule(MC); // Check if a module exists already
+  If (M=Nil) then
+    if assigned(MI) and Mi.SkipStreaming then
+      M:=MC.CreateNew(Self)
+    else
+      M:=MC.Create(Self);
+   DoCallModule(M,MN,ARequest,AResponse);
+end;
+
 function TWebHandler.GetApplicationURL(ARequest: TRequest): String;
 begin
   Result:=FApplicationURL;
@@ -482,6 +528,12 @@ begin
   FHandleGetOnPost := True;
   FRedirectOnError := False;
   FRedirectOnErrorURL := '';
+  ModuleFactory.OnModuleRequest:=@HandleModuleRequest;
+end;
+
+destructor TWebHandler.Destroy;
+begin
+  ModuleFactory.OnModuleRequest:=@HandleModuleRequest;
 end;
 
 { TCustomWebApplication }
@@ -537,6 +589,11 @@ begin
   result := FWebHandler.HandleGetOnPost;
 end;
 
+function TCustomWebApplication.GetLegacyRouting: Boolean;
+begin
+  Result:=FWebHandler.LegacyRouting;
+end;
+
 function TCustomWebApplication.GetModuleVar: String;
 begin
   result := FWebHandler.ModuleVariable;
@@ -602,6 +659,11 @@ begin
   FWebHandler.HandleGetOnPost := AValue;
 end;
 
+procedure TCustomWebApplication.SetLegacyRouting(AValue: Boolean);
+begin
+  FWebHandler.LegacyRouting:=AValue;
+end;
+
 procedure TCustomWebApplication.SetModuleVar(const AValue: String);
 begin
   FWebHandler.ModuleVariable := AValue;

+ 67 - 20
packages/fcl-web/src/base/fphttp.pp

@@ -17,7 +17,7 @@ unit fphttp;
 
 Interface
 
-uses sysutils,classes,httpdefs;
+uses sysutils,classes,httpdefs, httproute;
 
 Type
 { TODO : Implement wkSession }
@@ -188,28 +188,40 @@ Type
 
   { TModuleItem }
 
-  TModuleItem = Class(TCollectionItem)
+  TModuleItem = Class(TCollectionItem, IRouteInterface)
   private
     FModuleClass: TCustomHTTPModuleClass;
     FModuleName: String;
     FSkipStreaming: Boolean;
+    FRouteID : Integer;
+  Protected
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+    Property RouteID : Integer Read FRouteID;
   Public
+    Destructor Destroy; override;
     Property ModuleClass : TCustomHTTPModuleClass Read FModuleClass Write FModuleClass;
     Property ModuleName : String Read FModuleName Write FModuleName;
     Property SkipStreaming : Boolean Read FSkipStreaming Write FSkipStreaming;
   end;
 
   { TModuleFactory }
+  TOnModuleRequest = Procedure (Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse) of object;
 
   TModuleFactory = Class(TCollection)
   private
+    FOnModuleRequest: TOnModuleRequest;
     function GetModule(Index : Integer): TModuleItem;
     procedure SetModule(Index : Integer; const AValue: TModuleItem);
+  Protected
+    procedure DoHandleRequest(Sender : TModuleItem; ARequest: TRequest; AResponse: TResponse);
   Public
+    Procedure RegisterHTTPModule(Const ModuleName : String; ModuleClass : TCustomHTTPModuleClass; SkipStreaming : Boolean = False);virtual;
+    Procedure RegisterHTTPModule(ModuleClass : TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
     Function FindModule(const AModuleName : String) : TModuleItem;
     Function ModuleByName(const AModuleName : String) : TModuleItem;
     Function IndexOfModule(const AModuleName : String) : Integer;
     Property Modules [Index : Integer]: TModuleItem Read GetModule Write SetModule;default;
+    Property OnModuleRequest : TOnModuleRequest Read FOnModuleRequest Write FOnModuleRequest;
   end;
 
   { EFPHTTPError }
@@ -237,9 +249,9 @@ Resourcestring
 
 Implementation
 
-{$ifdef cgidebug}
-uses dbugintf;
-{$endif}
+
+{$ifdef cgidebug} uses dbugintf; {$endif}
+
 
 Var
   GSM : TSessionFactory;
@@ -256,6 +268,21 @@ begin
   Result:=GSM;
 end;
 
+{ TModuleItem }
+
+procedure TModuleItem.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  if (Collection is TModuleFactory) then
+    (Collection as TModuleFactory).DoHandleRequest(Self,ARequest,AResponse);
+end;
+
+destructor TModuleItem.Destroy;
+begin
+  if (FRouteID>0) then
+    httprouter.DeleteRouteByID(FRouteID-1);
+  inherited Destroy;
+end;
+
 
 { TCustomHTTPModule }
 
@@ -335,6 +362,39 @@ begin
   Items[Index]:=AValue;
 end;
 
+procedure TModuleFactory.DoHandleRequest(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+begin
+  If Assigned(OnModuleRequest) then
+    OnModuleRequest(Sender,ARequest,AResponse)
+  else
+    Raise EFPHTTPError.Create('Cannot handle module request, OnModuleRequest not set');
+end;
+
+procedure TModuleFactory.RegisterHTTPModule(const ModuleName: String; ModuleClass: TCustomHTTPModuleClass; SkipStreaming: Boolean);
+
+Var
+  I : Integer;
+  MI : TModuleItem;
+
+begin
+  I:=IndexOfModule(ModuleName);
+  If (I=-1) then
+    begin
+    MI:=Add as TModuleItem;
+    MI.ModuleName:=ModuleName;
+    MI.FRouteID:=httprouter.RegisterRoute('/'+MI.FModuleName+'/*', MI as IRouteInterface,False).ID+1;
+    end
+  else
+    MI:=ModuleFactory[I];
+  MI.ModuleClass:=ModuleClass;
+  MI.SkipStreaming:=SkipStreaming;
+end;
+
+procedure TModuleFactory.RegisterHTTPModule(ModuleClass: TCustomHTTPModuleClass; SkipStreaming: Boolean);
+begin
+  RegisterHTTPModule(ModuleClass.DefaultModuleName,ModuleClass,SkipStreaming);
+end;
+
 function TModuleFactory.FindModule(const AModuleName: String): TModuleItem;
 
 Var
@@ -366,27 +426,14 @@ end;
 
 procedure RegisterHTTPModule(ModuleClass: TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
 begin
-  RegisterHTTPModule(ModuleClass.ClassName,ModuleClass,SkipStreaming);
+  ModuleFactory.RegisterHTTPModule(ModuleClass,SkipStreaming);
 end;
 
 procedure RegisterHTTPModule(const ModuleName: String;
   ModuleClass: TCustomHTTPModuleClass; SkipStreaming : Boolean = False);
   
-Var
-  I : Integer;
-  MI : TModuleItem;
-  
 begin
-  I:=ModuleFactory.IndexOfModule(ModuleName);
-  If (I=-1) then
-    begin
-    MI:=ModuleFactory.Add as TModuleItem;
-    MI.ModuleName:=ModuleName;
-    end
-  else
-    MI:=ModuleFactory[I];
-  MI.ModuleClass:=ModuleClass;
-  MI.SkipStreaming:=SkipStreaming;
+  ModuleFactory.RegisterHTTPModule(ModuleName,ModuleClass,SkipStreaming);
 end;
 
 { THTTPContentProducer }

+ 21 - 0
packages/fcl-web/src/base/httpdefs.pp

@@ -415,8 +415,11 @@ type
     FServerPort : String;
     FContentRead : Boolean;
     FContent : String;
+    FRouteParams : TStrings;
     function GetLocalPathPrefix: string;
     function GetFirstHeaderLine: String;
+    function GetRP(AParam : String): String;
+    procedure SetRP(AParam : String; AValue: String);
   Protected
     Function AllowReadContent : Boolean; virtual;
     Function CreateUploadedFiles : TUploadedFiles; virtual;
@@ -441,6 +444,7 @@ type
     constructor Create; override;
     destructor destroy; override;
     Function GetNextPathInfo : String;
+    Property RouteParams[AParam : String] : String Read GetRP Write SetRP;
     Property ReturnedPathInfo : String Read FReturnedPathInfo Write FReturnedPathInfo;
     Property LocalPathPrefix : string Read GetLocalPathPrefix;
     Property CommandLine : String Read FCommandLine;
@@ -1453,6 +1457,7 @@ end;
 
 destructor TRequest.destroy;
 begin
+  FreeAndNil(FRouteParams);
   FreeAndNil(FFiles);
   inherited destroy;
 end;
@@ -1534,6 +1539,22 @@ begin
     Result := Result + ' HTTP/' + HttpVersion;
 end;
 
+function TRequest.GetRP(AParam : String): String;
+begin
+  if Assigned(FRouteParams) then
+    Result:=FRouteParams.Values[AParam]
+  else
+    Result:='';
+end;
+
+procedure TRequest.SetRP(AParam : String; AValue: String);
+begin
+  if (AValue<>GetRP(AParam)) And ((AValue<>'')<>Assigned(FRouteParams)) then
+    FRouteParams:=TStringList.Create;
+  if (AValue<>'') and Assigned(FRouteParams) then
+    FRouteParams.Values[AParam]:=AValue;
+end;
+
 function TRequest.AllowReadContent: Boolean;
 begin
   Result:=True;

+ 778 - 0
packages/fcl-web/src/base/httproute.pp

@@ -0,0 +1,778 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2017 by the Free Pascal development team
+
+    HTTPRoute: HTTP request router
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+}
+
+{
+  Note:
+  The MatchPattern routine was taken from Brook Framework's router unit, by Silvio Clecio.
+}
+
+{$mode objfpc}
+{$H+}
+
+unit httproute;
+
+interface
+
+uses
+  Classes, SysUtils, httpdefs;
+
+Type
+  EHTTPRoute = Class(EHTTP);
+
+  // Forward definitions;
+
+  THTTPRouter = Class;
+  THTTPRouterClass = Class of THTTPRouter;
+  // Some common HTTP methods.
+
+  TRouteMethod = (rmUnknown,rmAll,rmGet,rmPost,rmPut,rmDelete,rmOptions,rmHead, rmTrace);
+
+  { THTTPRoute }
+
+  THTTPRoute = Class(TCollectionItem)
+  private
+    FDefault: Boolean;
+    FMethod: TRouteMethod;
+    FURLPattern: String;
+    procedure SetURLPattern(AValue: String);
+  Protected
+    Procedure DoHandleRequest(ARequest : TRequest; AResponse : TResponse); virtual;
+  Public
+    Destructor Destroy; override;
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+    Function Matches(Const APattern : String; AMethod : TRouteMethod) : Boolean;
+    Function MatchPattern(Const Path : String; L : TStrings) : Boolean;
+    Function MatchMethod(Const AMethod : TRouteMethod) : Boolean;
+  Published
+    Property Default : Boolean Read FDefault Write FDefault;
+    Property URLPattern : String Read FURLPattern Write SetURLPattern;
+    Property Method : TRouteMethod Read FMethod Write FMethod;
+  end;
+  THTTPRouteClass = Class of THTTPRoute;
+
+  { THTTPRouteList }
+
+  THTTPRouteList = Class (TCollection)
+  private
+    function GetR(AIndex : Integer): THTTPRoute;
+    procedure SetR(AIndex : Integer; AValue: THTTPRoute);
+  Public
+    Property Routes[AIndex : Integer] : THTTPRoute Read GetR Write SetR; default;
+  end;
+
+  TRouteCallBack = Procedure (ARequest: TRequest; AResponse: TResponse);
+
+  { THTTPRouteCallback }
+
+  THTTPRouteCallback = Class(THTTPRoute)
+  private
+    FCallBack: TRouteCallBack;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property CallBack : TRouteCallBack Read FCallBack Write FCallback;
+  end;
+
+  TRouteCallBackEx = Procedure (AData : Pointer; ARequest: TRequest; AResponse: TResponse);
+
+  { THTTPRouteCallbackex }
+
+  THTTPRouteCallbackEx = Class(THTTPRoute)
+  private
+    FCallBack: TRouteCallBackex;
+    FData: Pointer;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property CallBack : TRouteCallBackex Read FCallBack Write FCallback;
+    Property Data : Pointer Read FData Write FData;
+  end;
+
+  TRouteEvent = Procedure (ARequest: TRequest; AResponse: TResponse) of object;
+
+  { THTTPRouteEvent }
+
+  THTTPRouteEvent = Class(THTTPRoute)
+  private
+    FEvent: TRouteEvent;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property Event : TRouteEvent Read FEvent Write FEvent;
+  end;
+
+{$INTERFACES CORBA}
+  IRouteInterface = Interface ['{10115353-10BA-4B00-FDA5-80B69AC4CAD0}']
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse);
+  end;
+
+  { THTTPRouteInterface }
+
+  THTTPRouteInterface = Class(THTTPRoute)
+  private
+    FIntf: IRouteInterface;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property Intf : IRouteInterface Read FIntf Write FIntf;
+  end;
+
+  TRouteObject = Class(TObject,IRouteInterface)
+  Public
+    Procedure HandleRequest(ARequest : TRequest; AResponse : TResponse); virtual; abstract;
+  end;
+  TRouteObjectClass = Class of TRouteObject;
+
+  { THTTPRouteObject }
+
+  THTTPRouteObject = Class(THTTPRoute)
+  private
+    FClass: TRouteObjectClass;
+  Protected
+    Procedure DoHandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  Public
+    Property ObjectCLass : TRouteObjectClass Read FClass Write FClass;
+  end;
+
+  THTTPRouteRequestEvent = Procedure (Sender : TObject; ARequest : TRequest; AResponse : TResponse) of object;
+
+  { THTTPRouter }
+
+  THTTPRouter = Class(TComponent)
+  private
+    FAfterRequest: THTTPRouteRequestEvent;
+    FBeforeRequest: THTTPRouteRequestEvent;
+    FRoutes : THTTPRouteList;
+    function GetR(AIndex : Integer): THTTPRoute;
+    Class Procedure DoneService;
+    Class
+      Var FService : THTTPRouter;
+          FServiceClass : THTTPRouterClass;
+    function GetRouteCount: Integer;
+  Protected
+    // Return an instance of given class with Pattern, Method, IsDefault filled in.
+    function CreateHTTPRoute(AClass: THTTPRouteClass; const APattern: String; AMethod: TRouteMethod; IsDefault: Boolean ): THTTPRoute; virtual;
+    // Override this if you want to use another collection class.
+    Function CreateRouteList : THTTPRouteList; virtual;
+    Procedure CheckDuplicate(APattern : String; AMethod : TRouteMethod; isDefault : Boolean);
+    // Actually route request. Override this for customized behaviour.
+    Procedure DoRouteRequest(ARequest : TRequest; AResponse : TResponse); virtual;
+    // Extract route from request. This is PathInfo by default (sanitized);
+    Function GetRequestPath(ARequest : TRequest) : String; virtual;
+  Public
+    Constructor Create(AOwner: TComponent); override;
+    Destructor Destroy; override;
+    // Delete given route by index.
+    Procedure DeleteRoute(AIndex : Integer);
+    // Delete given route by index.
+    Procedure DeleteRouteByID(AID : Integer);
+    // Delete given route by index. The route object will be freed.
+    Procedure DeleteRoute(ARoute : THTTPRoute);
+    // Sanitize route path. Strips of query parameters and makes sure it ends in /
+    class function SanitizeRoute(const Path: String): String;
+    // Global instance.
+    Class Function Service : THTTPRouter;
+    // Class for global instance when it is created;
+    Class Function ServiceClass : THTTPRouterClass;
+    // This will destroy the service
+    Class Procedure SetServiceClass(AClass : THTTPRouterClass);
+    // Convert string to HTTP Route method
+    Class Function StringToRouteMethod(Const S : String) : TRouteMethod;
+    // Register event based route
+    Function RegisterRoute(Const APattern : String; AEvent: TRouteEvent; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; AEvent: TRouteEvent; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Register interface based route. Programmer is responsible for the lifetime of the interface.
+    Function RegisterRoute(Const APattern : String; const AIntf: IRouteInterface; IsDefault : Boolean = False) : THTTPRoute; overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; const AIntf: IRouteInterface; IsDefault : Boolean = False): THTTPRoute; overload;
+    // Object class based route. The router is responsible for the lifetime of the object instance
+    Function RegisterRoute(Const APattern : String; const AObjectClass: TRouteObjectClass; IsDefault : Boolean = False) : THTTPRoute; overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; const AobjectClass: TRouteObjectClass; IsDefault : Boolean = False): THTTPRoute; overload;
+    // Register callback based route
+    Function RegisterRoute(Const APattern : String; AData : Pointer; ACallBack: TRouteCallBackex; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AData : Pointer; AMethod : TRouteMethod; ACallBack: TRouteCallBackEx; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Register callbackEx based route
+    Function RegisterRoute(Const APattern : String; ACallBack: TRouteCallBack; IsDefault : Boolean = False) : THTTPRoute;overload;
+    Function RegisterRoute(Const APattern : String; AMethod : TRouteMethod; ACallBack: TRouteCallBack; IsDefault : Boolean = False): THTTPRoute;overload;
+    // Find route. Matches Path on the various patterns. If a pattern is found, then the method is tested.
+    // Returns the route that matches the pattern and method.
+    function FindHTTPRoute(const Path: String; AMethod: TRouteMethod; Params: TStrings; out MethodMismatch: Boolean): THTTPRoute;
+    function GetHTTPRoute(const Path: String; AMethod: TRouteMethod; Params: TStrings): THTTPRoute;
+    // Do actual routing. Exceptions raised will not be caught. Request must be initialized
+    Procedure RouteRequest(ARequest : TRequest; AResponse : TResponse);
+    // Indexed access to the registered routes.
+    Property Routes [AIndex : Integer]  : THTTPRoute Read GetR; Default;
+    // Number of registered routes.
+    Property RouteCount : Integer Read GetRouteCount;
+    // Called before the request is routed.
+    Property BeforeRequest : THTTPRouteRequestEvent Read FBeforeRequest Write FBeforeRequest;
+    // Called after the request is routed, if no exception was raised during or before the request.
+    Property AfterRequest : THTTPRouteRequestEvent Read FAfterRequest Write FAfterRequest;
+  end;
+
+Function RouteMethodToString (R : TRouteMethod)  : String;
+// Shortcut for THTTPRouter.Service;
+Function HTTPRouter : THTTPRouter;
+
+implementation
+
+uses strutils, typinfo;
+
+Resourcestring
+  EDuplicateRoute = 'Duplicate route pattern: %s and method: %s';
+  EDuplicateDefaultRoute = 'Duplicate default route registered with pattern: %s and method: %s';
+
+function RouteMethodToString(R: TRouteMethod): String;
+
+begin
+  if R=rmUnknown then
+    Result:=''
+  else if R=rmAll then
+    Result:='*'
+  else
+    Result:=GetEnumName(TypeInfo(TRouteMethod),Ord(R));
+end;
+
+function HTTPRouter: THTTPRouter;
+begin
+  Result:=THTTPRouter.Service;
+end;
+
+{ THTTPRouteCallback }
+
+procedure THTTPRouteCallback.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  CallBack(ARequest, AResponse);
+end;
+
+{ THTTPRouteObject }
+
+procedure THTTPRouteObject.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+Var
+  O : TRouteObject;
+
+begin
+  O:=ObjectClass.Create;
+  try
+    O.HandleRequest(ARequest,AResponse);
+  finally
+    O.Free;
+  end;
+end;
+
+{ THTTPRouter }
+
+function THTTPRouter.GetR(AIndex : Integer): THTTPRoute;
+begin
+  Result:=FRoutes[AIndex]
+end;
+
+class procedure THTTPRouter.DoneService;
+begin
+  FreeAndNil(FService);
+end;
+
+function THTTPRouter.GetRouteCount: Integer;
+begin
+  Result:=FRoutes.Count;
+end;
+
+function THTTPRouter.CreateRouteList: THTTPRouteList;
+begin
+  Result:=THTTPRouteList.Create(THTTPRoute);
+end;
+
+procedure THTTPRouter.CheckDuplicate(APattern: String; AMethod: TRouteMethod;
+  isDefault: Boolean);
+Var
+  I,DI : Integer;
+  R : THTTPRoute;
+
+begin
+  DI:=-1;
+  For I:=0 to FRoutes.Count-1 do
+    begin
+    R:=FRoutes[I];
+    if R.Default then
+      DI:=I;
+    if R.Matches(APattern,AMethod) then
+      Raise EHTTPRoute.CreateFmt(EDuplicateRoute,[APattern,RouteMethodToString(AMethod)]);
+    end;
+  if isDefault and (DI<>-1) then
+    Raise EHTTPRoute.CreateFmt(EDuplicateDefaultRoute,[APattern,RouteMethodToString(AMethod)]);
+end;
+
+procedure THTTPRouter.DoRouteRequest(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  APath : String;
+  AMethod : TRouteMethod;
+  R : THTTPRoute;
+  L : TStrings;
+  I : Integer;
+  N,V : string;
+
+begin
+  APath:=GetRequestPath(ARequest);
+  AMethod:=StringToRouteMethod(ARequest.Method);
+  L:=TStringList.Create;
+  try
+    R:=GetHTTPRoute(APath,AMethod,L);
+    For I:=0 to L.Count-1 do
+      begin
+      L.GetNameValue(I,N,V);
+      if (N<>'') then
+        ARequest.RouteParams[N]:=V;
+      end;
+    R.HandleRequest(ARequest,AResponse);
+  finally
+    L.Free;
+  end;
+end;
+
+function THTTPRouter.GetRequestPath(ARequest: TRequest): String;
+begin
+  Result:=SanitizeRoute(ARequest.PathInfo);
+end;
+
+constructor THTTPRouter.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+  froutes:=CreateRouteList;
+end;
+
+destructor THTTPRouter.Destroy;
+begin
+  FreeAndNil(FRoutes);
+  inherited Destroy;
+end;
+
+procedure THTTPRouter.DeleteRoute(AIndex: Integer);
+begin
+  FRoutes.Delete(Aindex)
+end;
+
+procedure THTTPRouter.DeleteRouteByID(AID: Integer);
+begin
+  FRoutes.FindItemID(AID).Free;
+end;
+
+procedure THTTPRouter.DeleteRoute(ARoute: THTTPRoute);
+begin
+  ARoute.Free;
+end;
+
+class function THTTPRouter.Service: THTTPRouter;
+begin
+  if FService=Nil then
+    FService:=ServiceClass.Create(Nil);
+  Result:=FService;
+end;
+
+class function THTTPRouter.ServiceClass: THTTPRouterClass;
+begin
+  If FServiceClass=nil then
+    FServiceClass:=THTTPRouter;
+  Result:=FServiceClass;
+end;
+
+class procedure THTTPRouter.SetServiceClass(AClass: THTTPRouterClass);
+begin
+  if Assigned(FService) then
+    FreeAndNil(FService);
+  FServiceClass:=AClass;
+end;
+
+class function THTTPRouter.StringToRouteMethod(const S: String): TRouteMethod;
+begin
+
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AData : Pointer;
+  ACallBack: TRouteCallBackEx; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,AData,rmAll,ACallBack,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AData : Pointer;
+  AMethod: TRouteMethod; ACallBack: TRouteCallBackEx; IsDefault: Boolean
+  ): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteCallbackex,APattern,AMethod,IsDefault);
+  THTTPRouteCallbackex(Result).CallBack:=ACallBack;
+  THTTPRouteCallbackex(Result).Data:=AData;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; ACallBack: TRouteCallBack; IsDefault: Boolean
+  ): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,rmAll,ACallBack,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AMethod: TRouteMethod; ACallBack: TRouteCallBack;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=CreateHTTPRoute(THTTPRouteCallback,APattern,AMethod,IsDefault);
+  THTTPRouteCallback(Result).CallBack:=ACallBack;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AEvent: TRouteEvent;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:= RegisterRoute(APattern,rmAll,AEvent,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;
+  AMethod: TRouteMethod; AEvent: TRouteEvent; IsDefault: Boolean): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteEvent,APattern,AMethod,IsDefault);
+  THTTPRouteEvent(Result).Event:=AEvent;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;
+  const AIntf: IRouteInterface; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=RegisterRoute(APattern,rmAll,AIntf,IsDefault);
+end;
+
+function THTTPRouter.CreateHTTPRoute(AClass : THTTPRouteClass; const APattern: String;AMethod: TRouteMethod; IsDefault: Boolean) : THTTPRoute;
+
+begin
+  CheckDuplicate(APattern,AMethod,isDefault);
+  Result:=AClass.Create(FRoutes);
+  With Result do
+    begin
+    URLPattern:=APattern;
+    Default:=IsDefault;
+    Method:=AMethod;
+    end;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String;AMethod: TRouteMethod; const AIntf: IRouteInterface; IsDefault: Boolean ): THTTPRoute;
+
+begin
+  Result:=CreateHTTPRoute(THTTPRouteInterface,APattern,AMethod,IsDefault);
+  THTTPRouteInterface(Result).Intf:=AIntf;
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; const AObjectClass: TRouteObjectClass; IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=RegisterRoute(APattern,rmAll,AObjectClass,IsDefault);
+end;
+
+function THTTPRouter.RegisterRoute(const APattern: String; AMethod: TRouteMethod; const AobjectClass: TRouteObjectClass;
+  IsDefault: Boolean): THTTPRoute;
+begin
+  Result:=CreateHTTPRoute(THTTPRouteObject,APattern,AMethod,IsDefault);
+  THTTPRouteObject(Result).ObjectCLass:=AObjectClass;
+end;
+
+Class function THTTPRouter.SanitizeRoute(const Path: String) : String;
+
+Var
+  APathInfo : String;
+
+begin
+  APathInfo:=Path;
+  Delete(APathInfo,Pos('?', APathInfo), MaxInt);
+  Result:=IncludeHTTPPathDelimiter(APathInfo);
+end;
+
+function THTTPRouter.FindHTTPRoute(const Path: String; AMethod: TRouteMethod; Params : TStrings; Out MethodMismatch : Boolean): THTTPRoute;
+
+Var
+  I : Integer;
+  APathInfo : String;
+
+begin
+  APathInfo:=SanitizeRoute(Path);
+  MethodMisMatch:=False;
+  Result:=Nil;
+  I:=0;
+  While (Result=Nil) and (I<FRoutes.Count) do
+    begin
+    Result:=FRoutes[i];
+    If Not Result.MatchPattern(APathInfo,Params) then
+      Result:=Nil
+    else if Not Result.MatchMethod(AMethod) then
+      begin
+      Result:=Nil;
+      Params.Clear;
+      MethodMisMatch:=True;
+      end;
+    Inc(I);
+    end;
+end;
+
+function THTTPRouter.GetHTTPRoute(const Path: String; AMethod: TRouteMethod; Params : TStrings): THTTPRoute;
+
+Const
+  Status : Array[Boolean] of Integer = (404,405);
+  StatusText :Array[Boolean] of String = ('Not found','Method not allowed');
+
+Var
+  MethodMisMatch : Boolean;
+  E:EHTTPRoute;
+
+begin
+  Result:=FindHTTPRoute(Path,AMethod,Params,MethodMisMatch);
+  if Not Assigned(Result) then
+    begin
+    E:=EHTTPRoute.Create(StatusText[MethodMisMatch]);
+    E.StatusText:=StatusText[MethodMisMatch];
+    E.StatusCode:=Status[MethodMisMatch];
+    Raise E;
+    end;
+end;
+
+procedure THTTPRouter.RouteRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  If Assigned(FBeforeRequest) then
+    FBeforeRequest(Self,ARequest,AResponse);
+  DoRouteRequest(ARequest,AResponse);
+  If Assigned(FAfterRequest) then
+    FAfterRequest(Self,ARequest,AResponse);
+end;
+
+{ THTTPRouteInterface }
+
+procedure THTTPRouteInterface.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Intf.HandleRequest(ARequest, AResponse);
+end;
+
+{ THTTPRouteEvent }
+
+procedure THTTPRouteEvent.DoHandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Event(ARequest, AResponse);
+end;
+
+{ THTTPRouteList }
+
+function THTTPRouteList.GetR(AIndex : Integer): THTTPRoute;
+begin
+  Result:=Items[AIndex] as THTTPRoute;
+end;
+
+procedure THTTPRouteList.SetR(AIndex : Integer; AValue: THTTPRoute);
+begin
+  Items[AIndex]:=AValue;
+end;
+
+{ THTTPRoute }
+
+procedure THTTPRoute.SetURLPattern(AValue: String);
+
+Var
+  V : String;
+
+begin
+  V:=IncludeHTTPPathDelimiter(AValue);
+  if (V<>'/') and (V[1]='/') then
+    Delete(V,1,1);
+  if FURLPattern=V then Exit;
+  FURLPattern:=V;
+end;
+
+procedure THTTPRoute.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  // Do nothing
+end;
+
+destructor THTTPRoute.Destroy;
+begin
+
+  inherited Destroy;
+end;
+
+procedure THTTPRoute.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  DoHandleRequest(ARequest,AResponse);
+end;
+
+function THTTPRoute.Matches(const APattern: String; AMethod: TRouteMethod
+  ): Boolean;
+begin
+  Result:=(CompareText(URLPattern,APattern)=0)
+          and ((Method=rmAll) or (AMethod=Method))
+end;
+
+Function THTTPRoute.MatchPattern(Const Path : String; L : TStrings) : Boolean;
+
+  Function StartsWith(C : Char; S : String): Boolean; 
+  
+  begin
+    Result:=(Length(S)>0) and (S[1]=C);
+  end;
+  
+  Function EndsWith(C : Char; S : String): Boolean; 
+  
+  Var
+  L : Integer;
+  
+  begin
+    L:=Length(S);
+    Result:=(L>0) and (S[L]=C);
+  end;
+  
+
+  procedure ExtractNextPathLevel(var ALeft: string;
+    var ALvl: string; var ARight: string; const ADelim: Char = '/');
+  var
+    P: Integer;
+  begin
+    if (ALvl<>ADelim) then
+      begin
+      ALeft:=ALeft+ALvl;
+      if StartsWith(ADelim,ARight) then
+        begin
+        ALeft:=ALeft+ADelim;
+        Delete(ARight,1,1);
+        end;
+      end;
+    P:=Pos(ADelim,ARight);
+    if P=0 then
+      P:=Length(ARight)+1;
+    ALvl:=Copy(ARight,1,P-1);
+    ARight:=Copy(ARight,P,MaxInt);
+  end;
+
+  procedure ExtractPrevPathLevel(var ALeft: string;
+    var ALvl: string; var ARight: string; const ADelim: Char = '/');
+  var
+    P,L: Integer;
+  begin
+    if (ALvl<>ADelim) then
+      begin
+      ARight:=ALvl+ARight;
+      L:=Length(ALeft);
+      if EndsWith(ADelim,ALeft) then
+        begin
+        ARight:=ADelim+ARight;
+        Delete(ALeft,L,1);
+        end;
+      end;
+    P:=RPos(ADelim,ALeft);
+    ALvl:=Copy(ALeft,P+1,MaxInt);
+    ALeft:=Copy(ALeft,1,P);
+  end;
+
+var
+  APathInfo : String;
+  APattern : String;
+  VLeftPat, VRightPat, VLeftVal, VRightVal, VVal, VPat, VName: string;
+
+begin
+  Result:= False;
+  if (URLPattern='') then
+     Exit; // Maybe empty pattern should match any path?
+  APathInfo:=Path;
+  APattern:=URLPattern;
+  Delete(APattern, Pos('?', APattern), MaxInt);
+  Delete(APathInfo, Pos('?', APathInfo), MaxInt);
+  if StartsWith('/',APattern) then
+    Delete(APattern,1,1);
+  if StartsWith('/',APathInfo) then
+    Delete(APathInfo,1,1);
+  VLeftPat := '';
+  VLeftVal := '';
+  VPat := '/'; // init value is '/', not ''
+  VVal := '/'; // init value is '/', not ''
+  VRightPat := APattern;
+  VRightVal := APathInfo;
+  repeat
+    // Extract next part
+    ExtractNextPathLevel(VLeftPat, VPat, VRightPat);
+    ExtractNextPathLevel(VLeftVal, VVal, VRightVal);
+    if StartsWith(':',VPat) then
+      begin
+      L.Values[Copy(VPat,2,Maxint)]:=VVal;
+      end
+    else
+      if StartsWith('*',VPat) then
+        begin
+        // *path
+        VName := Copy(VPat, 2, MaxInt);
+        VLeftPat := VRightPat;
+        VLeftVal := VVal + VRightVal;
+        VPat := '/'; // init value is '/', not ''
+        VVal := '/'; // init value is '/', not ''
+        VRightPat := '';
+        VRightVal := '';
+        // if AutoAddSlash ...
+        if EndsWith('/',VLeftPat) and not EndsWith('/',VLeftVal) then
+          Delete(VLeftPat, Length(VLeftPat), 1);
+        repeat
+          // Extract backwards
+          ExtractPrevPathLevel(VLeftPat, VPat, VRightPat);
+          ExtractPrevPathLevel(VLeftVal, VVal, VRightVal);
+          if StartsWith(':', VPat) then
+            begin
+            // *path/:field
+            L.Values[Copy(VPat,2,Maxint)]:=VVal;
+            end
+          else
+            // *path/const
+            if not ((VPat='') and (VLeftPat='')) and (VPat<>VVal) then
+              Exit;
+          // Check if we already done
+          if (VLeftPat='') or (VLeftVal='') then
+            begin
+            if VLeftPat='' then
+              begin
+              if (VName<>'') then
+                L.Values[VName]:=VLeftVal+VVal;
+              Result:=True;
+              end;
+            Exit;
+          end;
+        until False;
+        end
+      else
+        // const
+        if (VPat <> VVal) then
+          Exit;
+    // Check if we already done
+    if (VRightPat='') or (VRightVal='') then
+      begin
+      if (VRightPat='') and (VRightVal='') then
+        Result:=True
+      else if (VRightPat='/') then
+        Result := True;
+      Exit;
+      end;
+  until False;
+end;
+
+function THTTPRoute.MatchMethod(const AMethod: TRouteMethod): Boolean;
+begin
+  Result:=(Method=rmAll) or (Method=AMethod);
+end;
+
+{ THTTPRouteCallbackex }
+
+procedure THTTPRouteCallbackEx.DoHandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  CallBack(Data,ARequest, AResponse);
+end;
+
+finalization
+  THTTPRouter.DoneService;
+end.
+

+ 346 - 0
packages/fcl-web/src/base/tcwebmodule.pp

@@ -0,0 +1,346 @@
+{
+    This file is part of the Free Component Library (FCL)
+    Copyright (c) 2017 by the Free Pascal development team
+
+    Various helper classes to help in unit testing fpweb based code.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+}
+unit tcwebmodule;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, httpdefs, fphttp, fpcunit, custweb;
+
+Type
+
+  { TFakeRequest }
+
+  TFakeRequest = Class(TRequest)
+  Protected
+    Procedure InitRequest;
+  Public
+    Procedure SetAuthentication(Const AUserName,APassword : String);
+  end;
+
+  { TFakeResponse }
+
+  TFakeResponse = Class(TResponse)
+  private
+    FSCCC: Integer;
+    FSentContent: TStringStream;
+    FFields : TStrings;
+    FSentHeaders: TStrings;
+    FSHCC: Integer;
+    function GetSCS: Ansistring;
+  protected
+    Function GetFieldValue(Index : Integer) : String; override;
+    Procedure SetFieldValue(Index : Integer; Value : String); override;
+    Procedure DoSendHeaders(Headers : TStrings); override;
+    Procedure DoSendContent; override;
+  Public
+    Destructor Destroy; override;
+    Property SendHeaderCallCount: Integer Read FSHCC;
+    Property SendContentCallCount: Integer Read FSCCC;
+    Property SentHeaders : TStrings Read FSentHeaders;
+    Property SentContent : TStringStream Read FSentContent;
+    Property SentContentAsString : Ansistring Read GetSCS;
+  end;
+
+  { TFakeSession }
+
+  TFakeSession = Class(TCustomSession)
+  private
+    FValues : Tstrings;
+    procedure CheckValues;
+    function GetValues: TStrings;
+  Protected
+    Destructor Destroy; override;
+    Function GetSessionVariable(VarName : String) : String; override;
+    procedure SetSessionVariable(VarName : String; const AValue: String);override;
+    Property Values : TStrings Read GetValues;
+  end;
+
+  { TFakeSessionFactory }
+
+  TFakeSessionFactory = Class(TSessionFactory)
+  public
+    Class Var FSession: TCustomSession;
+  published
+    Function DoCreateSession(ARequest : TRequest) : TCustomSession; override;
+    Procedure DoDoneSession(Var ASession : TCustomSession); override;
+    Procedure DoCleanupSessions; override;
+  end;
+
+  { TFakeWebHandler }
+
+  TFakeWebHandler = Class(TWebhandler)
+  private
+    FFakeRequest: TRequest;
+    FFakeResponse: TResponse;
+  Protected
+    // Sets terminated to true after being called
+    function WaitForRequest(out ARequest : TRequest; out AResponse : TResponse) : boolean; override;
+    // Do not free request/response, as we're not the owner
+    procedure EndRequest(ARequest : TRequest;AResponse : TResponse); override;
+  Public
+    // Set these to make WaitForRequest return true. They will be cleared when EndRequest is called.
+    Property FakeRequest : TRequest Read FFakeRequest Write FFakeRequest;
+    Property FakeResponse : TResponse Read FFakeResponse Write FFakeResponse;
+  end;
+
+  { TTestWebModule }
+
+  TTestWebModule = Class(TTestCase)
+  private
+    FRequest: TFakeRequest;
+    FResponse: TFakeResponse;
+    FSession: TCustomSession;
+    FUseFakeSession: Boolean;
+    procedure SetSession(AValue: TCustomSession);
+  Protected
+    Procedure Setup; override;
+    Procedure TearDown; override;
+    function GetFakeSessionFactoryClass: TSessionFactoryClass; virtual;
+    Procedure TestWebModule(AModuleClass : TCustomHTTPModuleClass; Stream : Boolean);
+    Procedure AssertStatus(Const Msg : String; AStatus : Integer; Const AStatusText: String);
+    Property Request : TFakeRequest Read FRequest;
+    Property Response : TFakeResponse Read FResponse;
+    Property Session : TCustomSession Read FSession Write SetSession;
+    Property UseFakeSession : Boolean Read FUseFakeSession Write FUseFakeSession;
+  end;
+
+implementation
+
+uses base64;
+
+{ TFakeWebHandler }
+
+function TFakeWebHandler.WaitForRequest(out ARequest: TRequest; out AResponse: TResponse): boolean;
+begin
+  Result:=Assigned(FFakeRequest);
+  if Result then
+    begin
+    ARequest:=FFakeRequest;
+    AResponse:=FFakeResponse;
+    Terminate;
+    end;
+end;
+
+procedure TFakeWebHandler.EndRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  if ARequest=FFakeRequest then
+    begin
+    FFakeRequest:=Nil;
+    FFakeResponse:=Nil;
+    end;
+end;
+
+{ TFakeRequest }
+
+procedure TFakeRequest.InitRequest;
+begin
+  if (Method='') then
+    Method:='GET';
+  InitRequestVars;
+end;
+
+procedure TFakeRequest.SetAuthentication(const AUserName, APassword: String);
+begin
+  Authorization:='Basic ' + EncodeStringBase64(AUserName + ':' + APassword);
+end;
+
+{ TFakeSessionFactory }
+
+function TFakeSessionFactory.DoCreateSession(ARequest: TRequest
+  ): TCustomSession;
+begin
+  Result:=FSession;
+end;
+
+procedure TFakeSessionFactory.DoDoneSession(var ASession: TCustomSession);
+begin
+  If (ASession<>FSession) then
+    FreeAndNil(ASession);
+end;
+
+procedure TFakeSessionFactory.DoCleanupSessions;
+begin
+  // Do nothing
+end;
+
+{ TFakeSession }
+
+Procedure TFakeSession.CheckValues;
+
+begin
+  If not Assigned(FValues) then
+    FValues:=TStringList.Create;
+end;
+
+function TFakeSession.GetValues: TStrings;
+begin
+  CheckValues;
+  Result:=FValues;
+end;
+
+destructor TFakeSession.Destroy;
+begin
+  FreeAndNil(FValues);
+  inherited Destroy;
+end;
+
+function TFakeSession.GetSessionVariable(VarName: String): String;
+begin
+  If Assigned(FValues) then
+    Result:=FValues.Values[VarName]
+  else
+    Result:='';
+end;
+
+procedure TFakeSession.SetSessionVariable(VarName: String; const AValue: String);
+begin
+  CheckValues;
+  FValues.Values[VarName]:=AValue;
+end;
+
+{ TTestWebModule }
+
+procedure TTestWebModule.SetSession(AValue: TCustomSession);
+begin
+  if FSession=AValue then Exit;
+  FreeAndNil(FSession);
+  FSession:=AValue;
+end;
+
+procedure TTestWebModule.Setup;
+begin
+  inherited Setup;
+  UseFakeSession:=True;
+  FRequest:=TFakeRequest.Create;
+  FResponse:=TFakeResponse.Create(FRequest);
+  FSession:=TFakeSession.Create(Nil);
+end;
+
+procedure TTestWebModule.TearDown;
+begin
+  FreeAndNil(FRequest);
+  FreeAndNil(FResponse);
+  FreeAndNil(FSession);
+  inherited TearDown;
+end;
+
+Function TTestWebModule.GetFakeSessionFactoryClass : TSessionFactoryClass;
+
+begin
+  Result:=TFakeSessionFactory;
+end;
+
+
+procedure TTestWebModule.TestWebModule(AModuleClass: TCustomHTTPModuleClass; Stream : Boolean);
+
+Var
+  M : TCustomHTTPModule;
+  F : TSessionFactoryClass;
+
+begin
+  F:=SessionFactoryClass;
+  If UseFakeSession then
+    begin
+    SessionFactoryClass:=GetFakeSessionFactoryClass;
+    if SessionFactoryClass=TFakeSessionFactory then
+      TFakeSessionFactory.FSession:=Self.Session;
+    end;
+  try
+    Request.InitRequest;
+
+    if Stream then
+      M:=AModuleClass.Create(Nil)
+    else
+      M:=AModuleClass.CreateNew(Nil,0);
+    try
+      M.DoAfterInitModule(Request);
+      M.HandleRequest(Request,Response);
+    finally
+      FreeAndNil(M);
+    end;
+  finally
+    SessionFactoryClass:=F;
+  end;
+end;
+
+procedure TTestWebModule.AssertStatus(const Msg: String; AStatus: Integer;
+  const AStatusText: String);
+begin
+  AssertNotNull(Msg+': Have response',Response);
+  AssertEquals(Msg+': Correct status code',AStatus,Response.Code);
+  AssertEquals(Msg+': Correct status text',AStatusText,Response.CodeText);
+end;
+
+{ TFakeResponse }
+
+function TFakeResponse.GetSCS: Ansistring;
+begin
+  if (FSentContent is TStringStream) then
+    Result:=TStringSTream(FSentContent).DataString
+  else
+    Result:='';
+end;
+
+function TFakeResponse.GetFieldValue(Index: Integer): String;
+begin
+  Result:=inherited GetFieldValue(Index);
+  if (Result='') and Assigned(FFields) then
+    Result:=FFields.Values[IntToStr(Index)];
+end;
+
+procedure TFakeResponse.SetFieldValue(Index: Integer; Value: String);
+begin
+  inherited SetFieldValue(Index, Value);
+  If (Value<>'') and (GetFieldValue(Index)='') then
+    begin
+    if (FFields=Nil) then
+      FFields:=TStringList.Create;
+    FFields.Add(IntToStr(Index)+'='+Value);
+    end;
+end;
+
+destructor TFakeResponse.Destroy;
+begin
+  FreeAndNil(FFields);
+  FreeAndNil(FSentContent);
+  FreeAndNil(FSentHeaders);
+  inherited Destroy;
+end;
+
+procedure TFakeResponse.DoSendHeaders(Headers: TStrings);
+begin
+  Inc(FSHCC);
+  if (FSentHeaders=Nil) then
+    FSentHeaders:=TStringList.Create;
+  FSentHeaders.Assign(Headers)
+end;
+
+procedure TFakeResponse.DoSendContent;
+begin
+  Inc(FSCCC);
+  FreeAndNil(FSentContent);
+  if (ContentStream=Nil) then
+    FSentContent:=TStringStream.Create(Content)
+  else
+    begin
+    FSentContent:=TStringStream.Create('');
+    FSentContent.CopyFrom(ContentStream,0);
+    end;
+end;
+
+end.
+

+ 971 - 0
packages/fcl-web/tests/tchttproute.pp

@@ -0,0 +1,971 @@
+unit tchttproute;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, fpcunit, testutils, tcwebmodule, testregistry, httpdefs, httproute, fphttp, fpweb, custweb;
+
+Type
+
+  { TMyModule }
+
+  TMyModule = Class(TCustomHTTPModule)
+  Private
+    class Var
+      FCallCount : Integer;
+      FCallRequest : TRequest;
+      FCallResponse : TResponse;
+  Public
+    Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+
+  { TTestHTTPRoute }
+  TMyHTTPRouter = Class(THTTPRouter);
+
+  { TMyInterfacedHandler }
+
+  TMyInterfacedHandler = class(TObject,IRouteInterface)
+  private
+    FCallCount: Integer;
+  public
+    procedure HandleRequest(ARequest: TRequest; AResponse: TResponse);
+    Property CallCount : Integer Read FCallCount;
+  end;
+
+  { TMyObjectHandler }
+
+  TMyObjectHandler = Class(TRouteObject)
+    class Var
+      FCallCount : Integer;
+      FCallRequest : TRequest;
+      FCallResponse : TResponse;
+  Public
+    Procedure HandleRequest(ARequest: TRequest; AResponse: TResponse); override;
+  end;
+
+  TTestHTTPRoute = class(TTestCase)
+  private
+    FInterfacedHandler: TMyInterfacedHandler;
+    FEventCalled : Integer;
+    FRequest: TFakeRequest;
+    FResponse: TFakeResponse;
+    FRouteParams: TStrings;
+    FGetRouteMethod: TRouteMethod;
+    FGetRoutePath: string;
+    FBeforeCalledCount:integer;
+    FAfterCalledCount:integer;
+    FDoException : Boolean;
+    FModuleItem: TModuleItem;
+    FModuleCallCount : Integer;
+    FWebhandler : TWebhandler;
+    procedure DoGetRoute;
+    procedure DoRouteRequest;
+    function GetWebHandler: TWebhandler;
+  protected
+    Procedure MyRouteEvent(ARequest : TRequest; AResponse : TResponse);
+    Procedure MyRouteEvent2(ARequest : TRequest; AResponse : TResponse);
+    Procedure MyRouteEvent3(ARequest : TRequest; AResponse : TResponse);
+    procedure SetUp; override;
+    procedure TearDown; override;
+    Property InterfacedHandler : TMyInterfacedHandler Read FInterfacedHandler;
+    Property RouteParams : TStrings Read FRouteParams;
+    Property FakeRequest : TFakeRequest Read FRequest;
+    Property FakeResponse : TFakeResponse Read FResponse;
+    Property WebHandler : TWebhandler Read GetWebHandler;
+  public
+    procedure DoAfterRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+    procedure DoBeforeRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+    procedure DoModuleRoute(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+  published
+    procedure TestHookUp;
+    Procedure TestAddEvent;
+    Procedure TestAddEventMethod;
+    Procedure TestAddEventDefault;
+    Procedure TestAddInterface;
+    Procedure TestAddInterfaceMethod;
+    Procedure TestAddInterfaceDefault;
+    Procedure TestAddCallBackex;
+    Procedure TestAddCallBackMethodEx;
+    Procedure TestAddCallBackDefaultEx;
+    Procedure TestAddCallBack;
+    Procedure TestAddCallBackMethod;
+    Procedure TestAddCallBackDefault;
+    Procedure TestAddRouteObject;
+    Procedure TestAddRouteObjectMethod;
+    Procedure TestAddRouteObjectDefault;
+    Procedure TestFindRouteStatic;
+    Procedure TestFindRouteStaticNoMatch;
+    Procedure TestGetRouteStatic;
+    Procedure TestGetRouteStaticNoMatch;
+    Procedure TestGetRouteStaticNoMethodMatch;
+    Procedure TestFindRouteStatic2Paths;
+    Procedure TestFindRouteStatic2PathsNoMatch;
+    Procedure TestFindRouteStaticMethodMismatch;
+    Procedure TestFindRouteWildCard;
+    Procedure TestFindRouteNamedWildCard;
+    Procedure TestFindRouteNamedWildCard2;
+    Procedure TestFindRouteWildCard3;
+    Procedure TestFindRouteWildCard3Named;
+    Procedure TestFindRouteParam;
+    Procedure TestFindRouteParam2;
+    Procedure TestFindRouteWildcardParam;
+    Procedure TestFindRouteWildcardParamNoMatch;
+    Procedure TestSetServiceClass;
+    Procedure TestRouteRequestEvent;
+    Procedure TestRouteRequestCallback;
+    Procedure TestRouteRequestInterface;
+    Procedure TestRouteRequestObject;
+    Procedure TestRouteRequestException;
+    Procedure TestRouteModule;
+    procedure TestRouteModuleAfterRoute;
+    procedure TestRouteModuleAfterRoute2;
+    Procedure TestWebModuleHandlerLegacy;
+    Procedure TestWebModuleHandlerNew;
+  end;
+
+implementation
+
+
+Var
+  CallBackCalled : Integer;
+  CallBackData : Pointer;
+
+Procedure MyRouteCallBackEx(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=1;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack2Ex(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=2;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack3Ex(Data : Pointer;ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=3;
+  CallBackData:=Data;
+end;
+
+Procedure MyRouteCallBack(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=1;
+  CallBackData:=Nil;
+end;
+
+Procedure MyRouteCallBack2(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=2;
+  CallBackData:=Nil;
+end;
+
+Procedure MyRouteCallBack3(ARequest : TRequest; AResponse : TResponse);
+
+begin
+  CallBackCalled:=3;
+  CallBackData:=Nil;
+end;
+
+{ TMyObjectHandler }
+
+procedure TMyObjectHandler.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  Inc(FCallCount);
+  FCallRequest:=ARequest;
+  FCallResponse:=AResponse;
+end;
+
+{ TMyModule }
+
+procedure TMyModule.HandleRequest(ARequest: TRequest; AResponse: TResponse);
+begin
+  Inc(FCallCount);
+  FCallRequest:=ARequest;
+  FCallResponse:=AResponse;
+end;
+
+
+{ TMyInterfacedHandler }
+
+procedure TMyInterfacedHandler.HandleRequest(ARequest: TRequest;
+  AResponse: TResponse);
+begin
+  Inc(FCallCount);
+end;
+
+procedure TTestHTTPRoute.TestHookUp;
+begin
+  AssertEquals('No routes registered.',0,HTTPRouter.RouteCount);
+  AssertEquals('Routeclass.',THTTPRouter,THTTPRouter.ServiceClass);
+  AssertNotNull('Have interfaced handler',InterfacedHandler);
+  AssertEquals('interfaced handler not called',0,InterfacedHandler.CallCount);
+  AssertEquals('No callbacks',0,CallBackCalled);
+  AssertEquals('No events',0,FEventCalled);
+  AssertEquals('No module calls',0,TMyModule.FCallCount);
+  AssertNull('No module request',TMyModule.FCallRequest);
+  AssertNull('No module response',TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.TestAddEvent;
+
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddEventMethod;
+
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST, @MyRouteEvent);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPOST=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddEventDefault;
+Var
+  E : THTTPRouteEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST, @MyRouteEvent,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteEvent,HTTPRouter[0].ClassType);
+  E:=THTTPRouteEvent(HTTPRouter[0]);
+  AssertTrue('Route event correct',E.Event=@MyRouteEvent);
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPOST=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterface;
+
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',InterfacedHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterfaceMethod;
+
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,InterfacedHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddInterfaceDefault;
+Var
+  E : THTTPRouteInterface;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,InterfacedHandler,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteInterface,HTTPRouter[0].ClassType);
+  E:=THTTPRouteInterface(HTTPRouter[0]);
+  AssertTrue('Route interface correct',Pointer(E.Intf)=Pointer(InterfacedHandler as IRouteInterface));
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URLPattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackex;
+
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,@MyRouteCallBackex);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackMethodEx;
+
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,rmPOST,@MyRouteCallBackex);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackDefaultEx;
+Var
+  E : THTTPRouteCallBackex;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@E,rmPOST,@MyRouteCallBackex,true);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBackex,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBackex(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBackex));
+  AssertTrue('Data pointer correct',E.Data=@E);
+  AssertEquals('Route class not default',true,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBack;
+
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteCallBack);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackMethod;
+
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST,@MyRouteCallBack);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddCallBackDefault;
+Var
+  E : THTTPRouteCallBack;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPOST,@MyRouteCallBack,true);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteCallBack,HTTPRouter[0].ClassType);
+  E:=THTTPRouteCallBack(HTTPRouter[0]);
+  AssertTrue('Route event correct',Pointer(E.CallBack)=Pointer(@MyRouteCallBack));
+  AssertEquals('Route class not default',true,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObject;
+
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',TMyObjectHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmAll=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObjectMethod;
+
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,TMyObjectHandler);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',False,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestAddRouteObjectDefault;
+Var
+  E : THTTPRouteObject;
+
+begin
+  HTTPRouter.RegisterRoute('*path',rmPost,TMyObjectHandler,True);
+  AssertEquals('1 route registered.',1,HTTPRouter.RouteCount);
+  AssertEquals('Route class correct',THTTPRouteObject,HTTPRouter[0].ClassType);
+  E:=THTTPRouteObject(HTTPRouter[0]);
+  AssertEquals('Route event correct',TMyObjectHandler,E.ObjectCLass);
+  AssertEquals('Route class not default',True,E.Default);
+  AssertEquals('Route URL pattern','*path/',E.URLPattern);
+  AssertTrue('Correct method',rmPost=E.Method);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic;
+
+Var
+  R,F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStaticNoMatch;
+
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path4',rmPOST,RouteParams,MM);
+  AssertNull('Found no route',F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStatic;
+
+Var
+  R,F : THTTPRoute;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.GetHTTPRoute('/path2',rmPOST,RouteParams);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+end;
+
+procedure TTestHTTPRoute.DoGetRoute;
+
+begin
+  HTTPRouter.GetHTTPRoute(FGetRoutePath,FGetRouteMethod,RouteParams);
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStaticNoMatch;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmAll,@MyRouteCallBack,False);
+  FGetRoutePath:='/pathNNNN';
+  FGetRouteMethod:=rmPost;
+  AssertException('No route found raises exception',EHTTPRoute,@DoGetRoute,'Not found')
+end;
+
+procedure TTestHTTPRoute.TestGetRouteStaticNoMethodMatch;
+
+begin
+  HTTPRouter.RegisterRoute('/path1',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path3',rmGet,@MyRouteCallBack,False);
+  FGetRoutePath:='/path1';
+  FGetRouteMethod:=rmPost;
+  AssertException('No route found raises exception',EHTTPRoute,@DoGetRoute,'Method not allowed')
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic2Paths;
+
+Var
+  R,F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStatic2PathsNoMatch;
+
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNull('No route',F);
+  AssertEquals('No route mismatch',False,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteStaticMethodMismatch;
+Var
+  F : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2/b',rmGet,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNull('No route',F);
+  AssertEquals('No route mismatch',True,MM);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/*',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('No route params',0,RouteParams.Count);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteNamedWildCard;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/*thepath',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('Route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','path2/b',RouteParams.Values['thepath']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteNamedWildCard2;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('/path2/*thepath',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/b',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('Route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','b',RouteParams.Values['thepath']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard3;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('No route params',0,RouteParams.Count);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildCard3Named;
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',1,RouteParams.Count);
+  AssertEquals('Wildcard path correctly registered','path2',RouteParams.Values['start']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteParam;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute(':start/c',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',1,RouteParams.Count);
+  AssertEquals('Param path correctly registered','path2',RouteParams.Values['start']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteParam2;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute(':start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',2,RouteParams.Count);
+  AssertEquals('Param 1 correctly registered','path2',RouteParams.Values['start']);
+  AssertEquals('Param 2 correctly registered','c',RouteParams.Values['end']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildcardParam;
+
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path1/path2/c',rmPOST,RouteParams,MM);
+  AssertNotNull('Found route',F);
+  AssertSame('Correct route found',R,F);
+  AssertEquals('No route mismatch',False,MM);
+  AssertEquals('route params',2,RouteParams.Count);
+  AssertEquals('Param 1 correctly registered','path1/path2',RouteParams.Values['start']);
+  AssertEquals('Param 2 correctly registered','c',RouteParams.Values['end']);
+end;
+
+procedure TTestHTTPRoute.TestFindRouteWildcardParamNoMatch;
+Var
+  F,R : THTTPRoute;
+  MM : Boolean;
+
+begin
+  HTTPRouter.RegisterRoute('/path1/b',rmAll,@MyRouteCallBack,False);
+  HTTPRouter.RegisterRoute('/path2',rmAll,@MyRouteCallBack,False);
+  R:=HTTPRouter.RegisterRoute('*start/:end',rmAll,@MyRouteCallBack,False);
+  F:=HTTPRouter.FindHTTPRoute('/path1',rmPOST,RouteParams,MM);
+  AssertNull('Found route',F);
+end;
+
+procedure TTestHTTPRoute.TestSetServiceClass;
+begin
+  THTTPRouter.SetServiceClass(TMyHTTPRouter);
+  AssertEquals('Correct service class',TMyHTTPRouter,THTTPRouter.ServiceClass);
+  AssertEquals('Correct service class used for singleton',TMyHTTPRouter,HTTPRouter.ClassType);
+end;
+
+procedure TTestHTTPRoute.DoRouteRequest;
+
+begin
+  HTTPRouter.RouteRequest(FakeRequest,FakeResponse);
+end;
+
+function TTestHTTPRoute.GetWebHandler: TWebhandler;
+
+Var
+  F: TFakeWebhandler;
+begin
+  if FWebhandler=Nil then
+    begin
+    F:=TFakeWebhandler.Create(Nil);
+    F.FakeRequest:=Self.FakeRequest;
+    F.FakeResponse:=Self.FakeResponse;
+    FWebhandler:=F;
+    end;
+  Result:=FWebhandler;
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestEvent;
+
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  FakeRequest.PathInfo:='me';
+  RouteParams.Values['path']:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,FEventCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestCallback;
+begin
+  HTTPRouter.RegisterRoute('*path',@MyRouteCallBack);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,CallBackCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestInterface;
+begin
+  HTTPRouter.RegisterRoute('*path',InterfacedHandler);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent called',1,InterfacedHandler.CallCount);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestObject;
+begin
+  HTTPRouter.RegisterRoute('*path',TMyObjectHandler);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  DoRouteRequest;
+  AssertEquals('TMyObjectHandler.handleRequest called',1,TMyObjectHandler.FCallCount);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request called once',1,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteRequestException;
+begin
+  FDoException:=true;
+  HTTPRouter.RegisterRoute('*path',@MyRouteEvent);
+  FakeRequest.PathInfo:='me';
+  HTTPRouter.BeforeRequest:=@DoBeforeRequest;
+  HTTPRouter.AfterRequest:=@DoAfterRequest;
+  AssertException('Raise exception',EXception,@DoRouteRequest);
+  AssertEquals('MyRouteEvent called',1,FEventCalled);
+  AssertEquals('Before request called once',1,FBeforeCalledCount);
+  AssertEquals('After request not called',0,FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteModule;
+begin
+  RegisterHTTPModule('my',TMyModule,True);
+  // Should not be called, as the module registration takes precedence.
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module route event called',1,FModuleCallCount);
+  AssertSame('Module route event called with correct module',ModuleFactory.Modules[0],FModuleItem);
+end;
+
+procedure TTestHTTPRoute.TestRouteModuleAfterRoute;
+
+begin
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  // Should not be called, as the event registration takes precedence.
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',1,FEventCalled);
+  AssertEquals('Module route event called',0,FModuleCallCount);
+end;
+
+procedure TTestHTTPRoute.TestRouteModuleAfterRoute2;
+begin
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/ap/';
+  DoRouteRequest;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module route event called',1,FModuleCallCount);
+  AssertSame('Module route event called with correct module',ModuleFactory.Modules[0],FModuleItem);
+end;
+
+procedure TTestHTTPRoute.TestWebModuleHandlerLegacy;
+begin
+  WebHandler.LegacyRouting:=True;
+  // will not be called because of legacy routing
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  WebHandler.Run;
+  AssertEquals('MyRouteEvent not called',0,FEventCalled);
+  AssertEquals('Module handler called',1,TMyModule.FCallCount);
+  AssertSame('Module handler request correct',FakeRequest,TMyModule.FCallRequest);
+  AssertSame('Module handler response correct',FakeResponse,TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.TestWebModuleHandlerNew;
+
+begin
+  WebHandler.LegacyRouting:=False;
+  // will not be called because of legacy routing
+  HTTPRouter.RegisterRoute('/my/no',@MyRouteEvent);
+  RegisterHTTPModule('my',TMyModule,True);
+  ModuleFactory.OnModuleRequest:=@DoModuleRoute;
+  FakeRequest.PathInfo:='/my/no/';
+  WebHandler.Run;
+  AssertEquals('MyRouteEvent not called',1,FEventCalled);
+  AssertEquals('Module handler not called',0,TMyModule.FCallCount);
+  AssertSame('Module handler request correct',Nil,TMyModule.FCallRequest);
+  AssertSame('Module handler response correct',Nil,TMyModule.FCallResponse);
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent(ARequest: TRequest; AResponse: TResponse);
+
+Var
+  I : integer;
+  N,V : string;
+
+begin
+  FEventCalled:=1;
+  for I:=0 to RouteParams.Count-1 do
+    begin
+    RouteParams.GetNameValue(I,N,V);
+    AssertEquals('Have route parameter '+N,V,ARequest.RouteParams[N]);
+    end;
+  if FDoException then
+    Raise Exception.Create('An error');
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent2(ARequest: TRequest; AResponse: TResponse);
+begin
+  FEventCalled:=2;
+end;
+
+procedure TTestHTTPRoute.MyRouteEvent3(ARequest: TRequest; AResponse: TResponse);
+begin
+  FEventCalled:=3;
+end;
+
+procedure TTestHTTPRoute.SetUp;
+
+begin
+  // Resets all.
+  THTTPRouter.SetServiceClass(THTTPRouter);
+  FInterfacedHandler:=TMyInterfacedHandler.Create;
+  FRouteParams:=TStringList.Create;
+  FRequest:=TFakeRequest.Create;
+  FResponse:=TFakeResponse.Create(FRequest);
+  ModuleFactory.Clear;
+  CallBackCalled:=0;
+  FEventCalled:=0;
+  TMyModule.FCallCount:=0;
+  TMyModule.FCallRequest:=Nil;
+  TMyModule.FCallResponse:=Nil;
+end;
+
+procedure TTestHTTPRoute.TearDown;
+
+begin
+  CallBackCalled:=0;
+  FEventCalled:=0;
+  FreeAndNil(FRouteParams);
+  FreeAndNil(FInterfacedHandler);
+  FreeAndNil(FRequest);
+  FreeAndNil(FResponse);
+  THTTPRouter.SetServiceClass(Nil);
+end;
+
+procedure TTestHTTPRoute.DoAfterRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+begin
+  AssertSame('Sender is router',HTTPRouter,Sender);
+  AssertSame('Request is original request',FakeRequest,ARequest);
+  AssertSame('Response is original response',FakeResponse,AResponse);
+  Inc(FAfterCalledCount);
+end;
+
+procedure TTestHTTPRoute.DoBeforeRequest(Sender: TObject; ARequest: TRequest; AResponse: TResponse);
+begin
+  AssertSame('Sender is router',HTTPRouter,Sender);
+  AssertSame('Request is original request',FakeRequest,ARequest);
+  AssertSame('Response is original response',FakeResponse,AResponse);
+  Inc(FBeforeCalledCount);
+end;
+
+procedure TTestHTTPRoute.DoModuleRoute(Sender: TModuleItem; ARequest: TRequest; AResponse: TResponse);
+begin
+  FModuleItem:=Sender;
+  Inc(FModuleCallCount);
+end;
+
+initialization
+
+  RegisterTest(TTestHTTPRoute);
+end.
+

+ 71 - 0
packages/fcl-web/tests/testfpweb.lpi

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="9"/>
+    <General>
+      <SessionStorage Value="InProjectDir"/>
+      <MainUnit Value="0"/>
+      <Title Value="testfpweb"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <VersionInfo>
+      <StringTable ProductVersion=""/>
+    </VersionInfo>
+    <BuildModes Count="1">
+      <Item1 Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+    </PublishOptions>
+    <RunParams>
+      <local>
+        <FormatVersion Value="1"/>
+        <CommandLineParams Value="--suite=TTestHTTPRoute.TestWebModuleHandler"/>
+      </local>
+    </RunParams>
+    <RequiredPackages Count="1">
+      <Item1>
+        <PackageName Value="FCL"/>
+      </Item1>
+    </RequiredPackages>
+    <Units Count="3">
+      <Unit0>
+        <Filename Value="testfpweb.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit0>
+      <Unit1>
+        <Filename Value="tchttproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit1>
+      <Unit2>
+        <Filename Value="../src/base/httproute.pp"/>
+        <IsPartOfProject Value="True"/>
+      </Unit2>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="testfpweb"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <OtherUnitFiles Value="../src/base"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions Count="3">
+      <Item1>
+        <Name Value="EAbort"/>
+      </Item1>
+      <Item2>
+        <Name Value="ECodetoolError"/>
+      </Item2>
+      <Item3>
+        <Name Value="EFOpenError"/>
+      </Item3>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 28 - 0
packages/fcl-web/tests/testfpweb.lpr

@@ -0,0 +1,28 @@
+program testfpweb;
+
+{$mode objfpc}{$H+}
+
+uses
+  Classes, consoletestrunner, tchttproute;
+
+type
+
+  { TMyTestRunner }
+
+  TMyTestRunner = class(TTestRunner)
+  protected
+  // override the protected methods of TTestRunner to customize its behavior
+  end;
+
+var
+  Application: TMyTestRunner;
+
+begin
+  DefaultFormat:=fPlain;
+  DefaultRunAllTests:=True;
+  Application := TMyTestRunner.Create(nil);
+  Application.Initialize;
+  Application.Title := 'FPCUnit Console test runner';
+  Application.Run;
+  Application.Free;
+end.

File diff suppressed because it is too large
+ 562 - 205
packages/pastojs/src/fppas2js.pp


+ 3 - 3
packages/pastojs/tests/tcconverter.pp

@@ -389,7 +389,7 @@ begin
   //   for(i=1; i<=$loopend1; i++){ a:=b; }
 
   // "var $loopend1=100"
-  LoopEndVar:=DefaultLoopEndVarName+'1';
+  LoopEndVar:=DefaultVarNameLoopEnd+'1';
   VS:=TJSVariableStatement(AssertElement('First in list is var '+LoopEndVar,TJSVariableStatement,L.A));
   VD:=TJSVarDeclaration(AssertElement('var '+LoopEndVar,TJSVarDeclaration,VS.A));
   AssertEquals('Correct name for '+LoopEndVar,LoopEndVar,VD.Name);
@@ -443,7 +443,7 @@ begin
   //   for(i=100; i>=$loopend1; i--){ a:=b; }
 
   // "var $loopend1=1"
-  LoopEndVar:=DefaultLoopEndVarName+'1';
+  LoopEndVar:=DefaultVarNameLoopEnd+'1';
   VS:=TJSVariableStatement(AssertElement('var '+LoopEndVar,TJSVariableStatement,L.A));
   VD:=TJSVarDeclaration(AssertElement('var '+LoopEndVar,TJSVarDeclaration,VS.A));
   AssertEquals('Correct name for '+LoopEndVar,LoopEndVar,VD.Name);
@@ -1197,7 +1197,7 @@ begin
   B.AddParam(CreateIdent('b'));
   B.AddParam(CreateIdent('c'));
   AttemptConvert:=B;
-  AssertException('Cannot yet convert 2-dim arrays',EPasToJS,@TryConvert);
+  AssertException('Pascal element not supported: TParamsExpr:TParamsExpr: Cannot convert 2-dim arrays',EPas2JS,@TryConvert);
 end;
 
 Procedure TTestExpressionConverter.TestVariable;

+ 873 - 16
packages/pastojs/tests/tcmodules.pas

@@ -147,6 +147,12 @@ type
     Procedure TestUnitImplRecord;
     Procedure TestRenameJSNameConflict;
 
+    // strings
+    Procedure TestCharConst;
+    Procedure TestStringConst;
+    Procedure TestString_SetLength;
+    // ToDo: TestString: read, write []
+
     Procedure TestEmptyProc;
     Procedure TestAliasTypeRef;
 
@@ -168,16 +174,27 @@ type
     Procedure TestExit;
     Procedure TestBreak;
     Procedure TestContinue;
-    // ToDo: TestString; SetLength,Length,[],char
 
     // ToDo: pass by reference
 
-    // ToDo: enums
+    Procedure TestEnumName;
+    Procedure TestEnumNumber;
+    Procedure TestEnumFunctions;
+    Procedure TestSet;
+    Procedure TestSetOperators;
+    Procedure TestSetFunctions;
+    // ToDo:  str
+    // ToDo: pass set as non const parameter using cloneSet
 
     // statements
     Procedure TestIncDec;
     Procedure TestAssignments;
-    Procedure TestOperators1;
+    Procedure TestArithmeticOperators1;
+    // test integer := double
+    // test integer := integer + double
+    // test pass double to an integer parameter
+    Procedure TestLogicalOperators;
+    Procedure TestBitwiseOperators;
     Procedure TestFunctionInt;
     Procedure TestFunctionString;
     Procedure TestVarRecord;
@@ -196,7 +213,9 @@ type
     Procedure TestCaseOfRange;
 
     // arrays
-    Procedure TestArray;
+    Procedure TestArray_Dynamic;
+    Procedure TestArray_Dynamic_Nil;
+    // ToDo: TestArray_LowHigh
 
     // classes
     Procedure TestClass_TObjectDefaultConstructor;
@@ -207,9 +226,13 @@ type
     Procedure TestClass_AbstractMethod;
     Procedure TestClass_CallInherited_NoParams;
     Procedure TestClass_CallInherited_WithParams;
+    Procedure TestClasS_CallInheritedConstructor;
     Procedure TestClass_ClassVar;
     Procedure TestClass_CallClassMethod;
-    // ToDo: Procedure TestClass_CallInheritedConstructor;
+    Procedure TestClass_Property;
+    Procedure TestClass_Property_ClassMethod;
+    Procedure TestClass_Property_Index;
+    Procedure TestClass_PropertyOfTypeArray;
     // ToDo: overload
     // ToDo: second constructor
     // ToDo: call another constructor within a constructor
@@ -299,7 +322,7 @@ constructor TTestEnginePasResolver.Create;
 begin
   inherited Create;
   StoreSrcColumns:=true;
-  Options:=Options+[proFixCaseOfOverrides];
+  Options:=Options+DefaultPasResolverOptions;
 end;
 
 destructor TTestEnginePasResolver.Destroy;
@@ -1104,7 +1127,7 @@ begin
     ]));
 end;
 
-procedure TTestModule.TestOperators1;
+procedure TTestModule.TestArithmeticOperators1;
 begin
   StartProgram(false);
   Add('var');
@@ -1112,13 +1135,15 @@ begin
   Add('begin');
   Add('  va:=1;');
   Add('  vb:=va+va;');
+  Add('  vb:=va div vb;');
+  Add('  vb:=va mod vb;');
   Add('  vb:=va+va*vb+va div vb;');
   Add('  vc:=-va;');
   Add('  va:=va-vb;');
   Add('  vb:=va;');
   Add('  if va<vb then vc:=va else vc:=vb;');
   ConvertProgram;
-  CheckSource('TestOperators1',
+  CheckSource('TestArithmeticOperators1',
     LinesToStr([ // statements
     'this.vA = 0;',
     'this.vB = 0;',
@@ -1127,7 +1152,9 @@ begin
     LinesToStr([ // this.$main
     'this.vA = 1;',
     'this.vB = this.vA + this.vA;',
-    'this.vB = (this.vA + (this.vA * this.vB)) + (this.vA / this.vB);',
+    'this.vB = Math.floor(this.vA / this.vB);',
+    'this.vB = this.vA % this.vB;',
+    'this.vB = (this.vA + (this.vA * this.vB)) + Math.floor(this.vA / this.vB);',
     'this.vC = -this.vA;',
     'this.vA = this.vA - this.vB;',
     'this.vB = this.vA;',
@@ -1135,6 +1162,66 @@ begin
     ]));
 end;
 
+procedure TTestModule.TestLogicalOperators;
+begin
+  StartProgram(false);
+  Add('var');
+  Add('  vA,vB,vC:boolean;');
+  Add('begin');
+  Add('  va:=vb and vc;');
+  Add('  va:=vb or vc;');
+  Add('  va:=true and vc;');
+  Add('  va:=(vb and vc) or (va and vb);');
+  Add('  va:=not vb;');
+  ConvertProgram;
+  CheckSource('TestLogicalOperators',
+    LinesToStr([ // statements
+    'this.vA = false;',
+    'this.vB = false;',
+    'this.vC = false;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.vA = this.vB && this.vC;',
+    'this.vA = this.vB || this.vC;',
+    'this.vA = true && this.vC;',
+    'this.vA = (this.vB && this.vC) || (this.vA && this.vB);',
+    'this.vA = !this.vB;'
+    ]));
+end;
+
+procedure TTestModule.TestBitwiseOperators;
+begin
+  StartProgram(false);
+  Add('var');
+  Add('  vA,vB,vC:longint;');
+  Add('begin');
+  Add('  va:=vb and vc;');
+  Add('  va:=vb or vc;');
+  Add('  va:=vb xor vc;');
+  Add('  va:=vb shl vc;');
+  Add('  va:=vb shr vc;');
+  Add('  va:=3 and vc;');
+  Add('  va:=(vb and vc) or (va and vb);');
+  Add('  va:=not vb;');
+  ConvertProgram;
+  CheckSource('TestBitwiseOperators',
+    LinesToStr([ // statements
+    'this.vA = 0;',
+    'this.vB = 0;',
+    'this.vC = 0;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.vA = this.vB & this.vC;',
+    'this.vA = this.vB | this.vC;',
+    'this.vA = this.vB ^ this.vC;',
+    'this.vA = this.vB << this.vC;',
+    'this.vA = this.vB >>> this.vC;',
+    'this.vA = 3 & this.vC;',
+    'this.vA = (this.vB & this.vC) | (this.vA & this.vB);',
+    'this.vA = ~this.vB;'
+    ]));
+end;
+
 procedure TTestModule.TestPrgProcVar;
 begin
   StartProgram(false);
@@ -1179,10 +1266,10 @@ begin
     LinesToStr([ // statements
     'var $impl = {',
     '};',
+    'this.$impl = $impl;',
     'this.Proc1 = function () {',
     '  var v1 = 0;',
     '};',
-    'this.$impl = $impl;',
     '$impl.v2 = "";'
     ]),
     '' // this.$init
@@ -1476,6 +1563,300 @@ begin
     ]));
 end;
 
+procedure TTestModule.TestEnumName;
+begin
+  StartProgram(false);
+  Add('type TMyEnum = (Red, Green, Blue);');
+  Add('var e: TMyEnum;');
+  Add('var f: TMyEnum = Blue;');
+  Add('begin');
+  Add('  e:=green;');
+  ConvertProgram;
+  CheckSource('TestEnumName',
+    LinesToStr([ // statements
+    'this.TMyEnum = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1,',
+    '  "2":"Blue",',
+    '  Blue:2',
+    '  };',
+    'this.e = 0;',
+    'this.f = this.TMyEnum.Blue;'
+    ]),
+    LinesToStr([
+    'this.e=this.TMyEnum.Green;'
+    ]));
+end;
+
+procedure TTestModule.TestEnumNumber;
+begin
+  Converter.Options:=Converter.Options+[coEnumNumbers];
+  StartProgram(false);
+  Add('type TMyEnum = (Red, Green);');
+  Add('var');
+  Add('  e: TMyEnum;');
+  Add('  f: TMyEnum = Green;');
+  Add('begin');
+  Add('  e:=green;');
+  ConvertProgram;
+  CheckSource('TestEnumNumber',
+    LinesToStr([ // statements
+    'this.TMyEnum = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1',
+    '  };',
+    'this.e = 0;',
+    'this.f = 1;'
+    ]),
+    LinesToStr([
+    'this.e=1;'
+    ]));
+end;
+
+procedure TTestModule.TestEnumFunctions;
+begin
+  StartProgram(false);
+  Add('type TMyEnum = (Red, Green);');
+  Add('var');
+  Add('  e: TMyEnum;');
+  Add('  i: longint;');
+  Add('begin');
+  Add('  i:=ord(red);');
+  Add('  i:=ord(green);');
+  Add('  i:=ord(e);');
+  Add('  e:=low(tmyenum);');
+  Add('  e:=low(e);');
+  Add('  e:=high(tmyenum);');
+  Add('  e:=high(e);');
+  Add('  e:=pred(green);');
+  Add('  e:=pred(e);');
+  Add('  e:=succ(red);');
+  Add('  e:=succ(e);');
+  Add('  e:=tmyenum(1);');
+  Add('  e:=tmyenum(i);');
+  ConvertProgram;
+  CheckSource('TestEnumNumber',
+    LinesToStr([ // statements
+    'this.TMyEnum = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1',
+    '  };',
+    'this.e = 0;',
+    'this.i = 0;'
+    ]),
+    LinesToStr([
+    'this.i=this.TMyEnum.Red;',
+    'this.i=this.TMyEnum.Green;',
+    'this.i=this.e;',
+    'this.e=this.TMyEnum.Red;',
+    'this.e=this.TMyEnum.Red;',
+    'this.e=this.TMyEnum.Green;',
+    'this.e=this.TMyEnum.Green;',
+    'this.e=this.TMyEnum.Green-1;',
+    'this.e=this.e-1;',
+    'this.e=this.TMyEnum.Red+1;',
+    'this.e=this.e+1;',
+    'this.e=1;',
+    'this.e=this.i;',
+    '']));
+end;
+
+procedure TTestModule.TestSet;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TColor = (Red, Green, Blue);');
+  Add('  TColors = set of TColor;');
+  Add('var');
+  Add('  c: TColor;');
+  Add('  s: TColors;');
+  Add('  t: TColors = [];');
+  Add('  u: TColors = [Red];');
+  Add('begin');
+  Add('  s:=[];');
+  Add('  s:=[Green];');
+  Add('  s:=[Green,Blue];');
+  Add('  s:=[Red..Blue];');
+  Add('  s:=[Red,Green..Blue];');
+  Add('  s:=[Red,c];');
+  Add('  s:=t;');
+  ConvertProgram;
+  CheckSource('TestEnumName',
+    LinesToStr([ // statements
+    'this.TColor = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1,',
+    '  "2":"Blue",',
+    '  Blue:2',
+    '  };',
+    'this.c = 0;',
+    'this.s = {};',
+    'this.t = {};',
+    'this.u = rtl.createSet(this.TColor.Red);'
+    ]),
+    LinesToStr([
+    'this.s={};',
+    'this.s=rtl.createSet(this.TColor.Green);',
+    'this.s=rtl.createSet(this.TColor.Green,this.TColor.Blue);',
+    'this.s=rtl.createSet(null,this.TColor.Red,this.TColor.Blue);',
+    'this.s=rtl.createSet(this.TColor.Red,null,this.TColor.Green,this.TColor.Blue);',
+    'this.s=rtl.createSet(this.TColor.Red,this.c);',
+    'this.s=rtl.cloneSet(this.t);',
+    '']));
+end;
+
+procedure TTestModule.TestSetOperators;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TColor = (Red, Green, Blue);');
+  Add('  TColors = set of tcolor;');
+  Add('var');
+  Add('  vC: TColor;');
+  Add('  vS: TColors;');
+  Add('  vT: TColors;');
+  Add('  vU: TColors;');
+  Add('  B: boolean;');
+  Add('begin');
+  Add('  include(vs,green);');
+  Add('  exclude(vs,vc);');
+  Add('  vs:=vt+vu;');
+  Add('  vs:=vt+[red];');
+  Add('  vs:=[red]+vt;');
+  Add('  vs:=[red]+[green];');
+  Add('  vs:=vt-vu;');
+  Add('  vs:=vt-[red];');
+  Add('  vs:=[red]-vt;');
+  Add('  vs:=[red]-[green];');
+  Add('  vs:=vt*vu;');
+  Add('  vs:=vt*[red];');
+  Add('  vs:=[red]*vt;');
+  Add('  vs:=[red]*[green];');
+  Add('  vs:=vt><vu;');
+  Add('  vs:=vt><[red];');
+  Add('  vs:=[red]><vt;');
+  Add('  vs:=[red]><[green];');
+  Add('  b:=vt=vu;');
+  Add('  b:=vt=[red];');
+  Add('  b:=[red]=vt;');
+  Add('  b:=[red]=[green];');
+  Add('  b:=vt<>vu;');
+  Add('  b:=vt<>[red];');
+  Add('  b:=[red]<>vt;');
+  Add('  b:=[red]<>[green];');
+  Add('  b:=vt<=vu;');
+  Add('  b:=vt<=[red];');
+  Add('  b:=[red]<=vt;');
+  Add('  b:=[red]<=[green];');
+  Add('  b:=vt>=vu;');
+  Add('  b:=vt>=[red];');
+  Add('  b:=[red]>=vt;');
+  Add('  b:=[red]>=[green];');
+  Add('  b:=Red in vt;');
+  Add('  b:=vc in vt;');
+  Add('  b:=Green in [Red..Blue];');
+  Add('  b:=vc in [Red..Blue];');
+  ConvertProgram;
+  CheckSource('TestEnumName',
+    LinesToStr([ // statements
+    'this.TColor = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1,',
+    '  "2":"Blue",',
+    '  Blue:2',
+    '  };',
+    'this.vC = 0;',
+    'this.vS = {};',
+    'this.vT = {};',
+    'this.vU = {};',
+    'this.B = false;'
+    ]),
+    LinesToStr([
+    'this.vS[this.TColor.Green] = true;',
+    'delete this.vS[this.vC];',
+    'this.vS = rtl.unionSet(this.vT, this.vU);',
+    'this.vS = rtl.unionSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.vS = rtl.unionSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.vS = rtl.unionSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.vS = rtl.diffSet(this.vT, this.vU);',
+    'this.vS = rtl.diffSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.vS = rtl.diffSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.vS = rtl.diffSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.vS = rtl.intersectSet(this.vT, this.vU);',
+    'this.vS = rtl.intersectSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.vS = rtl.intersectSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.vS = rtl.intersectSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.vS = rtl.symDiffSet(this.vT, this.vU);',
+    'this.vS = rtl.symDiffSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.vS = rtl.symDiffSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.vS = rtl.symDiffSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.B = rtl.eqSet(this.vT, this.vU);',
+    'this.B = rtl.eqSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.B = rtl.eqSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.B = rtl.eqSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.B = rtl.neSet(this.vT, this.vU);',
+    'this.B = rtl.neSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.B = rtl.neSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.B = rtl.neSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.B = rtl.leSet(this.vT, this.vU);',
+    'this.B = rtl.leSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.B = rtl.leSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.B = rtl.leSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.B = rtl.geSet(this.vT, this.vU);',
+    'this.B = rtl.geSet(this.vT, rtl.createSet(this.TColor.Red));',
+    'this.B = rtl.geSet(rtl.createSet(this.TColor.Red), this.vT);',
+    'this.B = rtl.geSet(rtl.createSet(this.TColor.Red), rtl.createSet(this.TColor.Green));',
+    'this.B = this.vT[this.TColor.Red];',
+    'this.B = this.vT[this.vC];',
+    'this.B = rtl.createSet(null, this.TColor.Red, this.TColor.Blue)[this.TColor.Green];',
+    'this.B = rtl.createSet(null, this.TColor.Red, this.TColor.Blue)[this.vC];',
+    '']));
+end;
+
+procedure TTestModule.TestSetFunctions;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TMyEnum = (Red, Green);');
+  Add('  TMyEnums = set of TMyEnum;');
+  Add('var');
+  Add('  e: TMyEnum;');
+  Add('  s: TMyEnums;');
+  Add('begin');
+  Add('  e:=Low(TMyEnums);');
+  Add('  e:=Low(s);');
+  Add('  e:=High(TMyEnums);');
+  Add('  e:=High(s);');
+  ConvertProgram;
+  CheckSource('TestSetFunctions',
+    LinesToStr([ // statements
+    'this.TMyEnum = {',
+    '  "0":"Red",',
+    '  Red:0,',
+    '  "1":"Green",',
+    '  Green:1',
+    '  };',
+    'this.e = 0;',
+    'this.s = {};'
+    ]),
+    LinesToStr([
+    'this.e=this.TMyEnum.Red;',
+    'this.e=this.TMyEnum.Red;',
+    'this.e=this.TMyEnum.Green;',
+    'this.e=this.TMyEnum.Green;',
+    '']));
+end;
+
 procedure TTestModule.TestUnitImplVars;
 begin
   StartUnit(false);
@@ -1566,6 +1947,92 @@ begin
     ]));
 end;
 
+procedure TTestModule.TestCharConst;
+begin
+  StartProgram(false);
+  Add('const');
+  Add('  c: char = ''1'';');
+  Add('begin');
+  Add('  c:=#0;');
+  Add('  c:=#1;');
+  Add('  c:=#9;');
+  Add('  c:=#10;');
+  Add('  c:=#13;');
+  Add('  c:=#31;');
+  Add('  c:=#32;');
+  Add('  c:=#$A;');
+  Add('  c:=#$0A;');
+  Add('  c:=#$b;');
+  Add('  c:=#$0b;');
+  Add('  c:=^A;');
+  Add('  c:=''"'';');
+  ConvertProgram;
+  CheckSource('TestCharConst',
+    LinesToStr([
+    'this.c="1";'
+    ]),
+    LinesToStr([
+    'this.c="\x00";',
+    'this.c="\x01";',
+    'this.c="\t";',
+    'this.c="\n";',
+    'this.c="\r";',
+    'this.c="\x1F";',
+    'this.c=" ";',
+    'this.c="\n";',
+    'this.c="\n";',
+    'this.c="\x0B";',
+    'this.c="\x0B";',
+    'this.c="\x01";',
+    'this.c=''"'';'
+    ]));
+end;
+
+procedure TTestModule.TestStringConst;
+begin
+  StartProgram(false);
+  Add('var');
+  Add('  s: string = ''abc'';');
+  Add('begin');
+  Add('  s:='''';');
+  Add('  s:=#13#10;');
+  Add('  s:=#9''foo'';');
+  Add('  s:=#$A9;');
+  Add('  s:=''foo''#13''bar'';');
+  Add('  s:=''"'';');
+  Add('  s:=''"''''"'';');
+  ConvertProgram;
+  CheckSource('TestCharConst',
+    LinesToStr([
+    'this.s="abc";'
+    ]),
+    LinesToStr([
+    'this.s="";',
+    'this.s="\r\n";',
+    'this.s="\tfoo";',
+    'this.s="©";',
+    'this.s="foo\rbar";',
+    'this.s=''"'';',
+    'this.s=''"\''"'';'
+    ]));
+end;
+
+procedure TTestModule.TestString_SetLength;
+begin
+  StartProgram(false);
+  Add('var s: string;');
+  Add('begin');
+  Add('  SetLength(s,3);');
+  ConvertProgram;
+  CheckSource('TestString_SetLength',
+    LinesToStr([ // statements
+    'this.s = "";'
+    ]),
+    LinesToStr([ // this.$main
+    'rtl.setStringLength(this.s,3);'
+    ]));
+end;
+
 procedure TTestModule.TestProcTwoArgs;
 begin
   StartProgram(false);
@@ -1901,7 +2368,7 @@ begin
     LinesToStr([ // this.$main
     'try {',
     '  this.i = 0;',
-    '  this.i = 2 / this.i;',
+    '  this.i = Math.floor(2 / this.i);',
     '} finally {',
     '  this.i = 3;',
     '};'
@@ -2495,6 +2962,103 @@ begin
     ]));
 end;
 
+procedure TTestModule.TestClasS_CallInheritedConstructor;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TObject = class');
+  Add('    constructor Create; virtual;');
+  Add('    constructor CreateWithB(b: boolean);');
+  Add('  end;');
+  Add('  TA = class');
+  Add('    constructor Create; override;');
+  Add('    constructor CreateWithC(c: char);');
+  Add('    procedure DoIt;');
+  Add('    class function DoSome: TObject;');
+  Add('  end;');
+  Add('constructor tobject.create;');
+  Add('begin');
+  Add('  inherited; // call non existing ancestor -> ignore silently');
+  Add('end;');
+  Add('constructor tobject.createwithb(b: boolean);');
+  Add('begin');
+  Add('  inherited; // call non existing ancestor -> ignore silently');
+  Add('  create; // normal call');
+  Add('end;');
+  Add('constructor ta.create;');
+  Add('begin');
+  Add('  inherited; // normal call TObject.Create');
+  Add('  inherited create; // normal call TObject.Create');
+  Add('  inherited createwithb(false); // normal call TObject.CreateWithB');
+  Add('end;');
+  Add('constructor ta.createwithc(c: char);');
+  Add('begin');
+  Add('  inherited create; // call TObject.Create');
+  Add('  inherited createwithb(true); // call TObject.CreateWithB');
+  Add('  doit;');
+  Add('  doit();');
+  Add('  dosome;');
+  Add('end;');
+  Add('procedure ta.doit;');
+  Add('begin');
+  Add('  create; // normal call');
+  Add('  createwithb(false); // normal call');
+  Add('  createwithc(''c''); // normal call');
+  Add('end;');
+  Add('class function ta.dosome: TObject;');
+  Add('begin');
+  Add('  Result:=create; // constructor');
+  Add('  Result:=createwithb(true); // constructor');
+  Add('  Result:=createwithc(''c''); // constructor');
+  Add('end;');
+  Add('begin');
+  ConvertProgram;
+  CheckSource('TestClass_CallInheritedConstructor',
+    LinesToStr([ // statements
+    'rtl.createClass(this,"TObject",null,function(){',
+    '  this.$init = function () {',
+    '  };',
+    '  this.Create = function () {',
+    '  };',
+    '  this.CreateWithB = function (b) {',
+    '    this.Create();',
+    '  };',
+    '});',
+    'rtl.createClass(this, "TA", this.TObject, function () {',
+    '  this.$init = function () {',
+    '    pas.program.TObject.$init.call(this);',
+    '  };',
+    '  this.Create = function () {',
+    '    pas.program.TObject.Create.apply(this, arguments);',
+    '    pas.program.TObject.Create.call(this);',
+    '    pas.program.TObject.CreateWithB.call(this, false);',
+    '  };',
+    '  this.CreateWithC = function (c) {',
+    '    pas.program.TObject.Create.call(this);',
+    '    pas.program.TObject.CreateWithB.call(this, true);',
+    '    this.DoIt();',
+    '    this.DoIt();',
+    '    this.$class.DoSome();',
+    '  };',
+    '  this.DoIt = function () {',
+    '    this.Create();',
+    '    this.CreateWithB(false);',
+    '    this.CreateWithC("c");',
+    '  };',
+    '  this.DoSome = function () {',
+    '    var Result = null;',
+    '    Result = this.$create("Create");',
+    '    Result = this.$create("CreateWithB", [true]);',
+    '    Result = this.$create("CreateWithC", ["c"]);',
+    '    return Result;',
+    '  };',
+    '});'
+    ]),
+    LinesToStr([ // this.$main
+    ''
+    ]));
+end;
+
 procedure TTestModule.TestClass_ClassVar;
 begin
   StartProgram(false);
@@ -2643,7 +3207,277 @@ begin
     '']));
 end;
 
-procedure TTestModule.TestArray;
+procedure TTestModule.TestClass_Property;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TObject = class');
+  Add('    Fx: longint;');
+  Add('    Fy: longint;');
+  Add('    function GetInt: longint;');
+  Add('    procedure SetInt(Value: longint);');
+  Add('    procedure DoIt;');
+  Add('    property IntA: longint read Fx write Fy;');
+  Add('    property IntB: longint read GetInt write SetInt;');
+  Add('  end;');
+  Add('function tobject.getint: longint;');
+  Add('begin');
+  Add('  result:=fx;');
+  Add('end;');
+  Add('procedure tobject.setint(value: longint);');
+  Add('begin');
+  Add('  if value=fy then exit;');
+  Add('  fy:=value;');
+  Add('end;');
+  Add('procedure tobject.doit;');
+  Add('begin');
+  Add('  IntA:=IntA+1;');
+  Add('  Self.IntA:=Self.IntA+1;');
+  Add('  IntB:=IntB+1;');
+  Add('  Self.IntB:=Self.IntB+1;');
+  Add('end;');
+  Add('var Obj: tobject;');
+  Add('begin');
+  Add('  obj.inta:=obj.inta+1;');
+  Add('  if obj.intb=2 then;');
+  Add('  obj.intb:=obj.intb+2;');
+  Add('  obj.setint(obj.inta);');
+  ConvertProgram;
+  CheckSource('TestClass_Property',
+    LinesToStr([ // statements
+    'rtl.createClass(this, "TObject", null, function () {',
+    '  this.$init = function () {',
+    '    this.Fx = 0;',
+    '    this.Fy = 0;',
+    '  };',
+    '  this.GetInt = function () {',
+    '    var Result = 0;',
+    '    Result = this.Fx;',
+    '    return Result;',
+    '  };',
+    '  this.SetInt = function (Value) {',
+    '    if (Value == this.Fy) return;',
+    '    this.Fy = Value;',
+    '  };',
+    '  this.DoIt = function () {',
+    '    this.Fy = this.Fx + 1;',
+    '    this.Fy = this.Fx + 1;',
+    '    this.SetInt(this.GetInt() + 1);',
+    '    this.SetInt(this.GetInt() + 1);',
+    '  };',
+    '});',
+    'this.Obj = null;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.Obj.Fy = this.Obj.Fx + 1;',
+    'if (this.Obj.GetInt() == 2) {',
+    '};',
+    'this.Obj.SetInt(this.Obj.GetInt() + 2);',
+    'this.Obj.SetInt(this.Obj.Fx);'
+    ]));
+end;
+
+procedure TTestModule.TestClass_Property_ClassMethod;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TObject = class');
+  Add('    class var Fx: longint;');
+  Add('    class var Fy: longint;');
+  Add('    class function GetInt: longint;');
+  Add('    class procedure SetInt(Value: longint);');
+  Add('    class procedure DoIt;');
+  Add('    class property IntA: longint read Fx write Fy;');
+  Add('    class property IntB: longint read GetInt write SetInt;');
+  Add('  end;');
+  Add('class function tobject.getint: longint;');
+  Add('begin');
+  Add('  result:=fx;');
+  Add('end;');
+  Add('class procedure tobject.setint(value: longint);');
+  Add('begin');
+  Add('end;');
+  Add('class procedure tobject.doit;');
+  Add('begin');
+  Add('  IntA:=IntA+1;');
+  Add('  Self.IntA:=Self.IntA+1;');
+  Add('  IntB:=IntB+1;');
+  Add('  Self.IntB:=Self.IntB+1;');
+  Add('end;');
+  Add('var Obj: tobject;');
+  Add('begin');
+  Add('  tobject.inta:=tobject.inta+1;');
+  Add('  if tobject.intb=2 then;');
+  Add('  tobject.intb:=tobject.intb+2;');
+  Add('  tobject.setint(tobject.inta);');
+  Add('  obj.inta:=obj.inta+1;');
+  Add('  if obj.intb=2 then;');
+  Add('  obj.intb:=obj.intb+2;');
+  Add('  obj.setint(obj.inta);');
+  ConvertProgram;
+  CheckSource('TestClass_Property_ClassMethod',
+    LinesToStr([ // statements
+    'rtl.createClass(this, "TObject", null, function () {',
+    '  this.Fx = 0;',
+    '  this.Fy = 0;',
+    '  this.$init = function () {',
+    '  };',
+    '  this.GetInt = function () {',
+    '    var Result = 0;',
+    '    Result = this.Fx;',
+    '    return Result;',
+    '  };',
+    '  this.SetInt = function (Value) {',
+    '  };',
+    '  this.DoIt = function () {',
+    '    this.Fy = this.Fx + 1;',
+    '    this.Fy = this.Fx + 1;',
+    '    this.SetInt(this.GetInt() + 1);',
+    '    this.SetInt(this.GetInt() + 1);',
+    '  };',
+    '});',
+    'this.Obj = null;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.TObject.Fy = this.TObject.Fx + 1;',
+    'if (this.TObject.GetInt() == 2) {',
+    '};',
+    'this.TObject.SetInt(this.TObject.GetInt() + 2);',
+    'this.TObject.SetInt(this.TObject.Fx);',
+    'this.Obj.$class.Fy = this.Obj.Fx + 1;',
+    'if (this.Obj.$class.GetInt() == 2) {',
+    '};',
+    'this.Obj.$class.SetInt(this.Obj.$class.GetInt() + 2);',
+    'this.Obj.$class.SetInt(this.Obj.Fx);'
+    ]));
+end;
+
+procedure TTestModule.TestClass_Property_Index;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TObject = class');
+  Add('    FItems: array of longint;');
+  Add('    function GetItems(Index: longint): longint;');
+  Add('    procedure SetItems(Index: longint; Value: longint);');
+  Add('    procedure DoIt;');
+  Add('    property Items[Index: longint]: longint read getitems write setitems;');
+  Add('  end;');
+  Add('function tobject.getitems(index: longint): longint;');
+  Add('begin');
+  Add('  Result:=fitems[index];');
+  Add('end;');
+  Add('procedure tobject.setitems(index: longint; value: longint);');
+  Add('begin');
+  Add('  fitems[index]:=value;');
+  Add('end;');
+  Add('procedure tobject.doit;');
+  Add('begin');
+  Add('  items[1]:=2;');
+  Add('  items[3]:=items[4];');
+  Add('  self.items[5]:=self.items[6];');
+  Add('  items[items[7]]:=items[items[8]];');
+  Add('end;');
+  Add('var Obj: tobject;');
+  Add('begin');
+  Add('  obj.Items[11]:=obj.Items[12];');
+  ConvertProgram;
+  CheckSource('TestClass_Property_Index',
+    LinesToStr([ // statements
+    'rtl.createClass(this, "TObject", null, function () {',
+    '  this.$init = function () {',
+    '    this.FItems = [];',
+    '  };',
+    '  this.GetItems = function (Index) {',
+    '    var Result = 0;',
+    '    Result = this.FItems[Index];',
+    '    return Result;',
+    '  };',
+    '  this.SetItems = function (Index, Value) {',
+    '    this.FItems[Index] = Value;',
+    '  };',
+    '  this.DoIt = function () {',
+    '    this.SetItems(1, 2);',
+    '    this.SetItems(3,this.GetItems(4));',
+    '    this.SetItems(5,this.GetItems(6));',
+    '    this.SetItems(this.GetItems(7), this.GetItems(this.GetItems(8)));',
+    '  };',
+    '});',
+    'this.Obj = null;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.Obj.SetItems(11,this.Obj.GetItems(12));'
+    ]));
+end;
+
+procedure TTestModule.TestClass_PropertyOfTypeArray;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TArray = array of longint;');
+  Add('  TObject = class');
+  Add('    FItems: TArray;');
+  Add('    function GetItems: tarray;');
+  Add('    procedure SetItems(Value: tarray);');
+  Add('    property Items: tarray read getitems write setitems;');
+  Add('  end;');
+  Add('function tobject.getitems: tarray;');
+  Add('begin');
+  Add('  Result:=fitems;');
+  Add('end;');
+  Add('procedure tobject.setitems(value: tarray);');
+  Add('begin');
+  Add('  fitems:=value;');
+  Add('  fitems:=nil;');
+  Add('  Items:=nil;');
+  Add('  Items:=Items;');
+  Add('  Items[1]:=2;');
+  Add('  fitems[3]:=Items[4];');
+  Add('  Items[5]:=Items[6];');
+  Add('  Self.Items[7]:=8;');
+  Add('  Self.Items[9]:=Self.Items[10];');
+  Add('  Items[Items[11]]:=Items[Items[12]];');
+  Add('end;');
+  Add('var Obj: tobject;');
+  Add('begin');
+  Add('  obj.items:=nil;');
+  Add('  obj.items:=obj.items;');
+  Add('  obj.items[11]:=obj.items[12];');
+  ConvertProgram;
+  CheckSource('TestClass_PropertyOfTypeArray',
+    LinesToStr([ // statements
+    'rtl.createClass(this, "TObject", null, function () {',
+    '  this.$init = function () {',
+    '    this.FItems = [];',
+    '  };',
+    '  this.GetItems = function () {',
+    '    var Result = [];',
+    '    Result = this.FItems;',
+    '    return Result;',
+    '  };',
+    '  this.SetItems = function (Value) {',
+    '    this.FItems = Value;',
+    '    this.FItems = null;',
+    '    this.SetItems(null);',
+    '    this.SetItems(this.GetItems());',
+    '    this.GetItems()[1] = 2;',
+    '    this.FItems[3] = this.GetItems()[4];',
+    '    this.GetItems()[5] = this.GetItems()[6];',
+    '    this.GetItems()[7] = 8;',
+    '    this.GetItems()[9] = this.GetItems()[10];',
+    '    this.GetItems()[this.GetItems()[11]] = this.GetItems()[this.GetItems()[12]];',
+    '  };',
+    '});',
+    'this.Obj = null;'
+    ]),
+    LinesToStr([ // this.$main
+    'this.Obj.SetItems(null);',
+    'this.Obj.SetItems(this.Obj.GetItems());',
+    'this.Obj.GetItems()[11] = this.Obj.GetItems()[12];'
+    ]));
+end;
+
+procedure TTestModule.TestArray_Dynamic;
 begin
   StartProgram(false);
   Add('type');
@@ -2655,14 +3489,37 @@ begin
   Add('  arr[0]:=4;');
   Add('  arr[1]:=length(arr)+arr[0];');
   ConvertProgram;
-  CheckSource('TestArray',
+  CheckSource('TestArray_Dynamic',
+    LinesToStr([ // statements
+    'this.Arr = [];'
+    ]),
+    LinesToStr([ // this.$main
+    'this.Arr = rtl.setArrayLength(this.Arr,3,0);',
+    'this.Arr[0] = 4;',
+    'this.Arr[1] = rtl.length(this.Arr)+this.Arr[0];'
+    ]));
+end;
+
+procedure TTestModule.TestArray_Dynamic_Nil;
+begin
+  StartProgram(false);
+  Add('type');
+  Add('  TArrayInt = array of longint;');
+  Add('var');
+  Add('  Arr: TArrayInt;');
+  Add('begin');
+  Add('  arr:=nil;');
+  Add('  if arr=nil then;');
+  Add('  if nil=arr then;');
+  ConvertProgram;
+  CheckSource('TestArray_Dynamic',
     LinesToStr([ // statements
     'this.Arr = [];'
     ]),
     LinesToStr([ // this.$main
-    'rtl.setArrayLength(this.Arr,3,0);',
-    'this.Arr[0]=4;',
-    'this.Arr[1]=rtl.length(this.Arr)+this.Arr[0];'
+    'this.Arr = null;',
+    'if (this.Arr == null) {};',
+    'if (null == this.Arr) {};'
     ]));
 end;
 

+ 291 - 0
utils/pas2js/dist/rtl.js

@@ -0,0 +1,291 @@
+/*
+    This file is part of the Free Pascal pas2js tool.
+    Copyright (c) 2017 Mattias Gaertner
+
+    Basic RTL for pas2js programs.
+
+    See the file COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+*/
+
+var pas = {};
+
+var rtl = {
+
+  quiet: false,
+  debug_load_units: true,
+
+  m_loading: 0,
+  m_loading_intf: 1,
+  m_intf_loaded: 2,
+  m_loading_impl: 3, // loading all used unit
+  m_initializing: 4, // running initialization
+  m_initialized: 5,
+
+  debug: function(){
+    if (!window.console || rtl.quiet) return;
+    console.log(arguments);
+  },
+
+  error: function(s){
+    rtl.debug('Error: ',s);
+    throw s;
+  },
+
+  warn: function(s){
+    rtl.debug('Warn: ',s);
+  },
+
+  isArray: function isArray(a) {
+    return a instanceof Array;
+  },
+
+  isNumber: function isNumber(n){
+    return typeof(n)=="number";
+  },
+
+  isInteger: function isInteger(A){
+    return Math.floor(A)===A;
+  },
+
+  isBoolean: function isBoolean(b){
+    return typeof(b)=="boolean";
+  },
+
+  isString: function isString(s){
+    return typeof(s)=="string";
+  },
+
+  isObject: function isObject(o){
+    return typeof(o)=="object";
+  },
+
+  isFunction: function isFunction(f){
+    return typeof(f)=="function";
+  },
+
+  isNull: function isNull(o){
+    return (o==null && typeof(o)=='object') || o==undefined;
+  },
+
+  hasString: function(s){
+    return rtl.isString(s) && (s.length>0);
+  },
+
+  module: function(module_name, intfuseslist, code, impluseslist){
+    if (rtl.debug_load_units) rtl.debug('rtl.module name="'+module_name+'" intfuses='+intfuseslist+' impluses='+impluseslist);
+    if (!rtl.hasString(module_name)) rtl.error('invalid module name "'+module_name+'"');
+    if (!rtl.isArray(intfuseslist)) rtl.error('invalid interface useslist of "'+module_name+'"');
+    if (!rtl.isFunction(code)) rtl.error('invalid module code of "'+module_name+'"');
+    if ((impluseslist!=undefined) && !rtl.isArray(impluseslist)) rtl.error('invalid implementation useslist of "'+module_name+'"');
+
+    if (pas[module_name])
+      rtl.error('module "'+module_name+'" already registered');
+
+    var module = pas[module_name] = {
+      $name: module_name,
+      $intfuseslist: intfuseslist,
+      $impluseslist: impluseslist,
+      $state: rtl.m_loading,
+      $code: code
+    };
+  },
+
+  run: function(module_name){
+    if (module_name==undefined) module_name='program';
+    var module = pas[module_name];
+    rtl.loadintf(module);
+    rtl.loadimpl(module);
+    if (module_name=='program'){
+      rtl.debug('running $main');
+      pas.program.$main();
+    }
+    return pas.System.ExitCode;
+  },
+
+  loadintf: function(module){
+    if (module.state>rtl.m_loading_intf) return; // already finished
+    rtl.debug('loadintf: '+module.$name);
+    if (module.$state==rtl.m_loading_intf)
+      rtl.error('unit cycle detected "'+module.$name+'"');
+    module.$state=rtl.m_loading_intf;
+    // load interfaces of interface useslist
+    rtl.loaduseslist(module,module.$intfuseslist,rtl.loadintf);
+    // run interface
+    rtl.debug('loadintf: run intf of '+module.$name);
+    module.$code(module.$intfuseslist,module);
+    // success
+    module.$state=rtl.m_intf_loaded;
+    // Note: units only used in implementations are not yet loaded (not even their interfaces)
+  },
+
+  loaduseslist: function(module,useslist,f){
+    if (useslist==undefined) return;
+    for (var i in useslist){
+      var unitname=useslist[i];
+      //rtl.debug('loaduseslist of "'+module.name+'" uses="'+unitname+'"');
+      if (pas[unitname]==undefined)
+        rtl.error('module "'+module.$name+'" misses "'+unitname+'"');
+      f(pas[unitname]);
+    }
+  },
+
+  loadimpl: function(module){
+    if (module.$state>=rtl.m_loading_impl) return; // already processing
+    if (module.$state<rtl.m_loading_intf) rtl.loadintf(module);
+    rtl.debug('loadimpl: '+module.$name+' load uses');
+    module.$state=rtl.m_loading_impl;
+    // load implementation of interfaces useslist
+    rtl.loaduseslist(module,module.$intfuseslist,rtl.loadimpl);
+    // load implementation of implementation useslist
+    rtl.loaduseslist(module,module.$impluseslist,rtl.loadimpl);
+    // Note: At this point all interfaces used by this unit are loaded. If
+    // there are implementation uses cycles some used units might not yet be
+    // initialized. This is by design.
+
+    // run initialization
+    rtl.debug('loadimpl: '+module.$name+' run init');
+    module.$state=rtl.m_initializing;
+    if (rtl.isFunction(module.$init))
+      module.$init();
+    // unit initialized
+    module.$state=rtl.m_initialized;
+  },
+
+  createCallback: function(scope, fn){
+    var wrapper = function(){
+      return fn.apply(scope,arguments);
+    };
+    wrapper.fn = fn;
+    return wrapper;
+  },
+
+  createClass: function(owner,name,ancestor,initfn){
+    var c = null;
+    if (ancestor != null){
+      c = Object.create(ancestor);
+      c.$ancestor = ancestor; // c.$ancestor == Object.getPrototypeOf(c)
+    } else {
+      c = {};
+      c.$create = function(fnname,args){
+        var o = Object.create(this);
+        o.$class = this; // Note: o.$class == Object.getPrototypeOf(o)
+        if (args == undefined) args = [];
+        o[fnname].apply(o,args);
+        o.$init();
+        o.AfterConstruction();
+        return o;
+      };
+      c.$destroy = function(fnname){
+        this.BeforeDestruction();
+        this[fnname].apply(obj,[]);
+      };
+    };
+    c.$classname = name;
+    c.$name = owner.$name+'.'+name;
+    c.$unitname = rtl.isString(owner.$unitname) ? owner.$unitname : owner.$name;
+    owner[name] = c;
+    initfn.call(c);
+  },
+
+  as: function(instance,typ){
+    if(typ.isPrototypeOf(instance)) return instance;
+    throw pas.System.EInvalidCast.$create("create");
+  },
+
+  setArrayLength: function(arr,newlength,defaultvalue){
+    if (newlength == 0) return null;
+    if (arr == null) arr = [];
+    var oldlen = arr.length;
+    if (oldlen==newlength) return;
+    arr.length = newlength;
+    if (rtl.isArray(defaultvalue)){
+      for (var i=oldlen; i<newlength; i++) arr[i]=[]; // new array
+    } else {
+      for (var i=oldlen; i<newlength; i++) arr[i]=defaultvalue;
+    }
+    return arr;
+  },
+
+  setStringLength: function(s,newlength){
+    s.length = newlength;
+  },
+
+  length: function(a){
+    return (a!=null) ? a.length : 0;
+  },
+
+  setCharAt: function(s,index,c){
+    return s.substr(0,index)+c+s.substr(index+1);
+  },
+
+  createSet: function(){
+    var s = {};
+    for (var i=0; i<arguments.length; i++){
+      if (arguments[i]!=null){
+        s[arguments[i]]=true;
+      } else {
+        var first=arguments[i+=1];
+        var last=arguments[i+=1];
+        for(var j=first; j<=last; j++) s[j]=true;
+      }
+    }
+    return s;
+  },
+
+  cloneSet: function(s){
+    var r = {};
+    for (var key in s) if (s.hasOwnProperty(key)) r[key]=true;
+    return r;
+  },
+
+  diffSet: function(s,t){
+    var r = {};
+    for (var key in s) if (s.hasOwnProperty(key) && !t[key]) r[key]=true;
+    return r;
+  },
+
+  unionSet: function(s,t){
+    var r = {};
+    for (var key in s) if (s.hasOwnProperty(key)) r[key]=true;
+    for (var key in t) if (t.hasOwnProperty(key)) r[key]=true;
+    return r;
+  },
+
+  intersectSet: function(s,t){
+    var r = {};
+    for (var key in s) if (s.hasOwnProperty(key) && t[key]) r[key]=true;
+    return r;
+  },
+
+  symDiffSet: function(s,t){
+    var r = {};
+    for (var key in s) if (s.hasOwnProperty(key) && !t[key]) r[key]=true;
+    for (var key in t) if (t.hasOwnProperty(key) && !s[key]) r[key]=true;
+    return r;
+  },
+
+  eqSet: function(s,t){
+    for (var key in s) if (s.hasOwnProperty(key) && !t[key]) return false;
+    for (var key in t) if (t.hasOwnProperty(key) && !s[key]) return false;
+    return true;
+  },
+
+  neSet: function(s,t){
+    return !rtl.eqSet(s,t);
+  },
+
+  leSet: function(s,t){
+    for (var key in s) if (s.hasOwnProperty(key) && !t[key]) return false;
+    return true;
+  },
+
+  geSet: function(s,t){
+    for (var key in t) if (t.hasOwnProperty(key) && !s[key]) return false;
+    return true;
+  },
+}

Some files were not shown because too many files changed in this diff