Mercurial > hg > orthanc-stone
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>