changeset 493:6fbf2eae7c88 bgo-commands-codegen

All unit tests pass for generation, including handler and dispatcher
author bgo-osimis
date Fri, 22 Feb 2019 10:48:43 +0100
parents 8e7e151ef472
children fc17251477d6
files Resources/CodeGeneration/stonegentool.py Resources/CodeGeneration/stonegentool_test.py Resources/CodeGeneration/template.in.ts Resources/CodeGeneration/test_data/test1.yaml Resources/CodeGeneration/test_data/test2.yaml
diffstat 5 files changed, 363 insertions(+), 125 deletions(-) [+]
line wrap: on
line diff
--- a/Resources/CodeGeneration/stonegentool.py	Wed Feb 20 20:51:30 2019 +0100
+++ b/Resources/CodeGeneration/stonegentool.py	Fri Feb 22 10:48:43 2019 +0100
@@ -108,34 +108,55 @@
 def LoadSchemaFromJson(filePath: str):
     return JsonHelpers.loadJsonWithComments(filePath)
 
-def GetCppTypenameFromCanonical(canonicalTypename: str) -> str:
-    # C++: prefix map vector and string with std::map, std::vector and
-    # std::string
-    # replace int32 by int32_t
-    # replace float32 by float
-    # replace float64 by double
-    retVal: str = canonicalTypename
-    retVal = retVal.replace("map", "std::map")
-    retVal = retVal.replace("vector", "std::vector")
-    retVal = retVal.replace("int32", "int32_t")
-    retVal = retVal.replace("float32", "float")
-    retVal = retVal.replace("float64", "double")
-    return retVal
+def CanonToCpp(canonicalTypename: str) -> str:
+  # C++: prefix map vector and string with std::map, std::vector and
+  # std::string
+  # replace int32 by int32_t
+  # replace float32 by float
+  # replace float64 by double
+  retVal: str = canonicalTypename
+  retVal = retVal.replace("map", "std::map")
+  retVal = retVal.replace("vector", "std::vector")
+  retVal = retVal.replace("int32", "int32_t")
+  retVal = retVal.replace("float32", "float")
+  retVal = retVal.replace("float64", "double")
+  return retVal
 
-def GetTypeScriptTypenameFromCanonical(canonicalTypename: str) -> str:
-    # TS: replace vector with Array and map with Map
-    # string remains string
-    # replace int32 by number
-    # replace float32 by number
-    # replace float64 by number
-    retVal: str = canonicalTypename
-    retVal = retVal.replace("map", "Map")
-    retVal = retVal.replace("vector", "Array")
-    retVal = retVal.replace("int32", "number")
-    retVal = retVal.replace("float32", "number")
-    retVal = retVal.replace("float64", "number")
-    retVal = retVal.replace("bool", "boolean")
-    return retVal
+def CanonToTs(canonicalTypename: str) -> str:
+  # TS: replace vector with Array and map with Map
+  # string remains string
+  # replace int32 by number
+  # replace float32 by number
+  # replace float64 by number
+  retVal: str = canonicalTypename
+  retVal = retVal.replace("map", "Map")
+  retVal = retVal.replace("vector", "Array")
+  retVal = retVal.replace("int32", "number")
+  retVal = retVal.replace("float32", "number")
+  retVal = retVal.replace("float64", "number")
+  retVal = retVal.replace("bool", "boolean")
+  return retVal
+
+def NeedsConstruction(canonTypename):
+  return True
+
+def RegisterTemplateFunction(template,func):
+  """Makes a function callable by a jinja2 template"""
+  template.globals[func.__name__] = func
+  return func
+
+def MakeTemplate(templateStr):
+  template = Template(templateStr)
+  RegisterTemplateFunction(template,CanonToCpp)
+  RegisterTemplateFunction(template,CanonToTs)
+  RegisterTemplateFunction(template,NeedsConstruction)
+  return template
+
+def MakeTemplateFromFile(templateFileName):
+  templateFile = open(templateFileName, "r")
+  templateFileContents = templateFile.read()
+  return MakeTemplate(templateFileContents)
+  templateFile.close()
 
 def EatToken(sentence: str) -> Tuple[str, str]:
     """splits "A,B,C" into "A" and "B,C" where A, B and C are type names
@@ -307,6 +328,7 @@
 
   # TODO: check enum fields are unique (in whole namespace)
   # TODO: check struct fields are unique (in each struct)
+  # TODO: check that in the source schema, there are spaces after each colon
 
 # +-----------------------+
 # | Main processing logic |
@@ -395,8 +417,23 @@
 #   pass
 
 def LoadSchema(fn):
-  with open(fn, 'rb') as f:
-    schema = yaml.load(f)
+  # latin-1 is a trick, when we do NOT care about NON-ascii chars but
+  # we wish to avoid using a decoding error handler
+  # (see http://python-notes.curiousefficiency.org/en/latest/python3/text_file_processing.html#files-in-an-ascii-compatible-encoding-best-effort-is-acceptable)
+  # TL;DR: all 256 values are mapped to characters in latin-1 so the file 
+  # contents never cause an error.
+  with open(fn, 'r', encoding='latin-1') as f:
+    schemaText = f.read()
+    assert(type(schemaText) == str)
+    # ensure there is a space after each colon. Otherwise, dicts could be
+    # erroneously recognized as an array of strings containing ':'
+    for i in range(len(schemaText)-1):
+      ch = schemaText[i]
+      nextCh = schemaText[i+1]
+      if ch == ':':
+        if not (nextCh == ' ' or nextCh == '\n'):
+          assert(False)
+    schema = yaml.load(schemaText)
   return schema
 
 def GetTemplatingDictFromSchemaFilename(fn):
@@ -658,3 +695,5 @@
 
 #   if not IsStructType(name):
 #     raise Exception(f'{typename} should start with "struct "')
+
+
--- a/Resources/CodeGeneration/stonegentool_test.py	Wed Feb 20 20:51:30 2019 +0100
+++ b/Resources/CodeGeneration/stonegentool_test.py	Fri Feb 22 10:48:43 2019 +0100
@@ -6,42 +6,13 @@
 from stonegentool import \
 EatToken,SplitListOfTypes,ParseTemplateType,ProcessSchema, \
 CheckSchemaSchema,LoadSchema,trim,ComputeRequiredDeclarationOrder, \
-GetTemplatingDictFromSchemaFilename
+GetTemplatingDictFromSchemaFilename,MakeTemplate
 import unittest
 import os
 import re
 import pprint
 from jinja2 import Template
 
-ymlSchema = trim("""rootName: VsolMessages
-
-struct B:
-  someAs: vector<A>
-  someInts: vector<int32>
-
-struct C:
-  someBs: vector<B>
-  ddd:    vector<string>
-
-struct A:
-  someStrings: vector<string>
-  someInts2: vector<int32>
-  movies: vector<MovieType>
-
-enum MovieType:
-  - RomCom
-  - Horror
-  - ScienceFiction
-  - Vegetables
-
-enum CrispType:
- - SaltAndPepper
- - CreamAndChives
- - Paprika
- - Barbecue
-)
-""")
-
 def RemoveDateTimeLine(s : str):
   # regex are non-multiline by default, and $ does NOT match the end of the line
   s2 = re.sub(r"^// autogenerated by stonegentool on .*\n","",s)
@@ -121,23 +92,16 @@
     # we're happy if it does not crash :)
     CheckSchemaSchema(obj)
 
-  # def test_ParseSchema_bogus_json(self):
-  #   fn = os.path.join(os.path.dirname(__file__), 'test', 'test1_bogus_json.jsonc')
-  #   self.assertRaises(Exception,LoadSchema,fn)
-
-  # def test_ParseSchema_bogus_schema(self):
-  #   fn = os.path.join(os.path.dirname(__file__), 'test', 'test1_bogus_schema.jsonc')
-  #   obj = LoadSchema(fn)
-  #   self.assertRaises(Exception,CheckSchemaSchema,obj) 
-
   def test_ComputeRequiredDeclarationOrder(self):
     fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
     obj = LoadSchema(fn)
     genOrder: str = ComputeRequiredDeclarationOrder(obj)
-    self.assertEqual(3,len(genOrder))
+    self.assertEqual(5,len(genOrder))
     self.assertEqual("A",genOrder[0])
     self.assertEqual("B",genOrder[1])
     self.assertEqual("C",genOrder[2])
+    self.assertEqual("Message1",genOrder[3])
+    self.assertEqual("Message2",genOrder[4])
 
   # def test_GeneratePreambleEnumerationAndStructs(self):
   #   fn = os.path.join(os.path.dirname(__file__), 'test', 'test1.jsonc')
@@ -145,29 +109,35 @@
   #   (_,genc,_) = ProcessSchema(obj)
 
   def test_genEnums(self):
+    self.maxDiff = None
     fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
     obj = LoadSchema(fn)
     genOrder: str = ComputeRequiredDeclarationOrder(obj)
     processedSchema = ProcessSchema(obj, genOrder)
-    processedSchemaStr = pprint.pformat(processedSchema,indent=2)
-    processedSchemaStrRef = """{ 'enums': [ { 'fields': ['RomCom', 'Horror', 'ScienceFiction', 'Vegetables'],
-               'name': 'MovieType'},
-             { 'fields': [ 'SaltAndPepper',
-                           'CreamAndChives',
-                           'Paprika',
-                           'Barbecue'],
-               'name': 'CrispType'}],
-  'rootName': 'VsolMessages',
-  'structs': [ { 'fields': { 'movies': 'vector<MovieType>',
-                             'someInts2': 'vector<int32>',
-                             'someStrings': 'vector<string>'},
-                 'name': 'A'},
-               { 'fields': {'someAs': 'vector<A>', 'someInts': 'vector<int32>'},
-                 'name': 'B'},
-               { 'fields': {'ddd': 'vector<string>', 'someBs': 'vector<B>'},
-                 'name': 'C'}]}"""
+    self.assertTrue('rootName' in processedSchema)
+
+    structs = {}
+    for v in processedSchema['structs']:
+      structs[v['name']] = v
+    enums = {}
+    for v in processedSchema['enums']:
+      enums[v['name']] = v
 
-    self.assertEqual(processedSchemaStrRef,processedSchemaStr)
+    self.assertTrue('C' in structs)
+    self.assertTrue('someBs' in structs['C']['fields'])
+    self.assertTrue('CrispType' in enums)
+    self.assertTrue('Message1' in structs)
+    message1Struct = structs['Message1']
+    self.assertDictEqual(message1Struct,
+    {
+      'name':'Message1',
+      'fields': {
+        'a': 'int32',
+        'b': 'string',
+        'c': 'EnumMonth0',
+        'd': 'bool'
+      }
+    })
 
   def test_GenerateTypeScriptEnums(self):
     fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
@@ -197,6 +167,221 @@
 """
     self.assertEqual(renderedCodeRef,renderedCode)
 
+  def test_GenerateCplusplusEnums(self):
+    fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
+    tdico = GetTemplatingDictFromSchemaFilename(fn)
+    template = Template("""  // end of generic methods
+{% for enum in enums%}  enum {{enum['name']}} {
+{% for key in enum['fields']%}    {{key}},
+{%endfor%}  };
+
+{%endfor%}""")
+    renderedCode = template.render(**tdico)
+    renderedCodeRef = """  // end of generic methods
+  enum MovieType {
+    RomCom,
+    Horror,
+    ScienceFiction,
+    Vegetables,
+  };
+
+  enum CrispType {
+    SaltAndPepper,
+    CreamAndChives,
+    Paprika,
+    Barbecue,
+  };
+
+"""
+    self.assertEqual(renderedCodeRef,renderedCode)
+
+  def test_generateTsStructType(self):
+    fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
+    tdico = GetTemplatingDictFromSchemaFilename(fn)
+    ref = """  export class Message1 {
+    a: number;
+    b: string;
+    c: EnumMonth0;
+    d: boolean;
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolStuff.Message1';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+  export class Message2 {
+    toto: string;
+    tata: Message1[];
+    tutu: string[];
+    titi: Map<string, string>;
+    lulu: Map<string, Message1>;
+
+    constructor()
+    {
+      this.tata = new Array<Message1>();
+      this.tutu = new Array<string>();
+      this.titi = new Map<string, string>();
+      this.lulu = new Map<string, Message1>();  
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolStuff.Message2';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+"""
+#     template = MakeTemplate("""  // end of generic methods
+# {% for struct in struct%}  export class {{struct['name']}} {
+#   {% for key in struct['fields']%}    {{key}}:{{struct['fields'][key]}},
+#   {% endfor %}  
+#     constructor() {
+#   {% for key in struct['fields']%}
+#     {% if NeedsConstruction(struct['fields']['key'])}
+#       {{key}} = new {{CanonToTs(struct['fields']['key'])}};
+#     {% end if %}
+#   {% endfor %}
+#     }
+# {% endfor %}
+#     public StoneSerialize(): string {
+#       let container: object = {};
+#       container['type'] = '{{rootName}}.{{struct['name']}}';
+#       container['value'] = this;
+#       return JSON.stringify(container);
+#     }  };""")
+    template = MakeTemplate("""  // end of generic methods
+{% for struct in structs%}  export class {{struct['name']}} {
+{% for key in struct['fields']%}    {{key}}:{{CanonToTs(struct['fields'][key])}};
+{% endfor %}
+    constructor() {
+{% for key in struct['fields']%}      {{key}} = new {{CanonToTs(struct['fields'][key])}}();
+{% endfor %}    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = '{{rootName}}.{{struct['name']}}';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+{% endfor %}""")
+    renderedCode = template.render(**tdico)
+    renderedCodeRef = """  // end of generic methods
+  export class A {
+    someStrings:Array<string>;
+    someInts2:Array<number>;
+    movies:Array<MovieType>;
+
+    constructor() {
+      someStrings = new Array<string>();
+      someInts2 = new Array<number>();
+      movies = new Array<MovieType>();
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolMessages.A';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+  export class B {
+    someAs:Array<A>;
+    someInts:Array<number>;
+
+    constructor() {
+      someAs = new Array<A>();
+      someInts = new Array<number>();
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolMessages.B';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+  export class C {
+    someBs:Array<B>;
+    ddd:Array<string>;
+
+    constructor() {
+      someBs = new Array<B>();
+      ddd = new Array<string>();
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolMessages.C';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+  export class Message1 {
+    a:number;
+    b:string;
+    c:EnumMonth0;
+    d:boolean;
+
+    constructor() {
+      a = new number();
+      b = new string();
+      c = new EnumMonth0();
+      d = new boolean();
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolMessages.Message1';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+  export class Message2 {
+    toto:string;
+    tata:Array<Message1>;
+    tutu:Array<string>;
+    titi:Map<string, string>;
+    lulu:Map<string, Message1>;
+
+    constructor() {
+      toto = new string();
+      tata = new Array<Message1>();
+      tutu = new Array<string>();
+      titi = new Map<string, string>();
+      lulu = new Map<string, Message1>();
+    }
+
+    public StoneSerialize(): string {
+      let container: object = {};
+      container['type'] = 'VsolMessages.Message2';
+      container['value'] = this;
+      return JSON.stringify(container);
+    }
+  };
+
+"""
+    # print(renderedCode)
+    self.maxDiff = None
+    self.assertEqual(renderedCodeRef, renderedCode)
+
+  def test_generateWholeTsFile(self):
+    schemaFile = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
+    tdico = GetTemplatingDictFromSchemaFilename(schemaFile)
+    tsTemplateFile = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml')
+    template = MakeTemplateFromFile(tsTemplateFile)
+    renderedCode = template.render(**tdico)
+
+    print(renderedCode)
 
   def test_GenerateTypeScriptHandlerInterface(self):
     pass
--- a/Resources/CodeGeneration/template.in.ts	Wed Feb 20 20:51:30 2019 +0100
+++ b/Resources/CodeGeneration/template.in.ts	Fri Feb 22 10:48:43 2019 +0100
@@ -39,52 +39,40 @@
   };
 {%endfor%}
 
-  export class Message1 {
-    a: number;
-    b: string;
-    c: EnumMonth0;
-    d: boolean;
+
+"""  // end of generic methods
+{% for struct in structs%}  export class {{struct['name']}} {
+{% for key in struct['fields']%}    {{key}}:{{CanonToTs(struct['fields'][key])}};
+{% endfor %}
+    constructor() {
+{% for key in struct['fields']%}      {{key}} = new {{CanonToTs(struct['fields'][key])}}();
+{% endfor %}    }
+
     public StoneSerialize(): string {
       let container: object = {};
-      container['type'] = 'VsolStuff.Message1';
+      container['type'] = '{{rWholootName}}.{{struct['name']}}';
       container['value'] = this;
       return JSON.stringify(container);
     }
-  };
 
-  export class Message2 {
-    constructor()
-    {
-      this.tata = new Array<Message1>();
-      this.tutu = new Array<string>();
-      this.titi = new Map<string, string>();
-      this.lulu = new Map<string, Message1>();  
-    }
-    toto: string;
-    tata: Message1[];
-    tutu: string[];
-    titi: Map<string, string>;
-    lulu: Map<string, Message1>;
-
-    public StoneSerialize(): string {
-      let container: object = {};
-      container['type'] = 'VsolStuff.Message2';
-      container['value'] = this;
-      return JSON.stringify(container);
-    }
-    public static StoneDeserialize(valueStr: string) : Message2
+    public static StoneDeserialize(valueStr: string) : {{struct['name']}}
     {
       let value: any = JSON.parse(valueStr);
-      StoneCheckSerializedValueType(value, "VsolStuff.Message2");
-      let result: Message2 = value['value'] as Message2;
+      StoneCheckSerializedValueType(value, '{{rootName}}.{{struct['name']}}');
+      let result: {{struct['name']}} = value['value'] as {{struct['name']}};
       return result;
     }
+
+  }
+
+{% endfor %}
+
     };
 
   export interface IDispatcher
   {
-    HandleMessage1(value: Message1): boolean;
-    HandleMessage2(value: Message2): boolean;
+    {% for struct in structs%}    HandleMessage1(value:  {{struct['name']}}): boolean;
+    {% endfor %}
   };
 
   /** Service function for StoneDispatchToHandler */
@@ -99,16 +87,12 @@
       // this should never ever happen
       throw new Error("Caught empty type while dispatching");
     }
-    else if (type == "VsolStuff.Message1")
+{% for struct in structs%}    else if (type == "VsolStuff.{{struct['name']}}")
     {
       let value = jsonValue["value"] as Message1;
       return dispatcher.HandleMessage1(value);
     }
-    else if (type == "VsolStuff.Message2")
-    {
-      let value = jsonValue["value"] as Message2;
-      return dispatcher.HandleMessage2(value);
-    }
+{% enfor %}
     else
     {
       return false;
--- a/Resources/CodeGeneration/test_data/test1.yaml	Wed Feb 20 20:51:30 2019 +0100
+++ b/Resources/CodeGeneration/test_data/test1.yaml	Fri Feb 22 10:48:43 2019 +0100
@@ -17,6 +17,19 @@
   someInts2: vector<int32>
   movies: vector<MovieType>
 
+struct Message1:
+  a: int32
+  b: string
+  c: EnumMonth0
+  d: bool
+
+struct Message2:
+  toto: string
+  tata: vector<Message1>
+  tutu: vector<string>
+  titi: map<string, string>
+  lulu: map<string, Message1>
+
 enum MovieType:
   - RomCom
   - Horror
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/CodeGeneration/test_data/test2.yaml	Fri Feb 22 10:48:43 2019 +0100
@@ -0,0 +1,17 @@
+enum EnumMonth0:
+  - January
+  - February
+  - Month
+
+struct Message1:
+  a: int32
+  b: string
+  c: EnumMonth0
+  d: bool
+
+struct Message2:
+  toto: string
+  tata: vector<Message1>
+  tutu: vector<string>
+  titi: map<string, string>
+  lulu: map<string, Message1>