Mercurial > hg > orthanc-stone
changeset 491:8e7e151ef472 bgo-commands-codegen
Unit tests pass for enum generation
author | bgo-osimis |
---|---|
date | Wed, 20 Feb 2019 20:51:30 +0100 |
parents | 6470248790db |
children | 6fbf2eae7c88 |
files | Resources/CodeGeneration/playground.ts Resources/CodeGeneration/playground3.ts Resources/CodeGeneration/stonegentool.py Resources/CodeGeneration/stonegentool_test.py Resources/CodeGeneration/template.in.ts Resources/CodeGeneration/test_data/test1.yaml |
diffstat | 6 files changed, 663 insertions(+), 405 deletions(-) [+] |
line wrap: on
line diff
--- a/Resources/CodeGeneration/playground.ts Mon Feb 18 15:38:05 2019 +0100 +++ b/Resources/CodeGeneration/playground.ts Wed Feb 20 20:51:30 2019 +0100 @@ -34,7 +34,6 @@ printf(`About to serialize value. Type is ${valueType}`) printf(`About to serialize value. Type is ${typeof value}`) return "Choucroute"; - } function main():number
--- a/Resources/CodeGeneration/playground3.ts Mon Feb 18 15:38:05 2019 +0100 +++ b/Resources/CodeGeneration/playground3.ts Wed Feb 20 20:51:30 2019 +0100 @@ -3,7 +3,7 @@ 12345678901234567890123456789012345678901234567890123456789012345678901234567890 */ -namespace VsolStuff { +namespace VsolStuff222 { export enum EnumMonth0 { January, February, @@ -17,7 +17,7 @@ d: boolean; public StoneSerialize(): string { let container: object = {}; - container['type'] = 'Message1'; + container['type'] = 'VsolStuff.Message1'; container['value'] = this; return JSON.stringify(container); } @@ -32,7 +32,7 @@ public StoneSerialize(): string { let container: object = {}; - container['type'] = 'Message1'; + container['type'] = 'VsolStuff.Message2'; container['value'] = this; return JSON.stringify(container); } @@ -69,7 +69,7 @@ msg2_0.tata.push(msg1_0); msg2_0.tata.push(msg1_1); msg2_0.tutu.push("Mercadet"); - msg2_0.tutu.push("Poisson"); + msg2_0.tutu.push("Poisson");ing msg2_0.titi["44"] = "key 44"; msg2_0.titi["45"] = "key 45"; msg2_0.lulu["54"] = msg1_1;
--- a/Resources/CodeGeneration/stonegentool.py Mon Feb 18 15:38:05 2019 +0100 +++ b/Resources/CodeGeneration/stonegentool.py Wed Feb 20 20:51:30 2019 +0100 @@ -1,6 +1,8 @@ import json +import yaml import re import sys +from jinja2 import Template from typing import ( Any, Dict, @@ -22,7 +24,34 @@ 12345678901234567890123456789012345678901234567890123456789012345678901234567890 """ -class GeneratedCode: +# see https://stackoverflow.com/a/2504457/2927708 +def trim(docstring): + if not docstring: + return '' + # Convert tabs to spaces (following the normal Python rules) + # and split into a list of lines: + lines = docstring.expandtabs().splitlines() + # Determine minimum indentation (first line doesn't count): + indent = sys.maxsize + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < sys.maxsize: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + # Return a single string: + return '\n'.join(trimmed) + + +class GenCode: def __init__(self): # file-wide preamble (#include directives, comment...) @@ -45,10 +74,6 @@ raise NotImplementedError() -raise Exception(""" -$$$$TODO check field names are unique -""") - class JsonHelpers: """A set of utilities to perform JSON operations""" @@ -83,13 +108,13 @@ def LoadSchemaFromJson(filePath: str): return JsonHelpers.loadJsonWithComments(filePath) -def GetCppTypeNameFromCanonical(canonicalTypeName: str) -> str: +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: str = canonicalTypename retVal = retVal.replace("map", "std::map") retVal = retVal.replace("vector", "std::vector") retVal = retVal.replace("int32", "int32_t") @@ -97,13 +122,13 @@ retVal = retVal.replace("float64", "double") return retVal -def GetTypeScriptTypeNameFromCanonical(canonicalTypeName: str) -> str: +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: str = canonicalTypename retVal = retVal.replace("map", "Map") retVal = retVal.replace("vector", "Array") retVal = retVal.replace("int32", "number") @@ -112,54 +137,6 @@ retVal = retVal.replace("bool", "boolean") return retVal -# class Schema: -# def __init__(self, root_prefix : str, defined_types : List[Type]): -# self.rootName : str = root_prefix -# self.definedTypes : str = defined_types - -def CheckTypeSchema(definedType: Dict) -> None: - allowedDefinedTypeKinds = ["enum", "struct"] - if not "name" in definedType: - raise Exception("type lacks the 'name' key") - name = definedType["name"] - if not "kind" in definedType: - raise Exception(f"type {name} lacks the 'kind' key") - kind = definedType["kind"] - if not (kind in allowedDefinedTypeKinds): - raise Exception( - f"type {name} : kind {kind} is not allowed. " - + f"It must be one of {allowedDefinedTypeKinds}" - ) - - if not "fields" in definedType: - raise Exception("type {name} lacks the 'fields' key") - - # generic check on all kinds of types - fields = definedType["fields"] - for field in fields: - fieldName = field["name"] - if not "name" in field: - raise Exception("field in type {name} lacks the 'name' key") - - # fields in struct must have types - if kind == "struct": - for field in fields: - fieldName = field["name"] - if not "type" in field: - raise Exception( - f"field {fieldName} in type {name} " + "has no 'type' key" - ) - - -def CheckSchemaSchema(schema: Dict) -> None: - if not "root_name" in schema: - raise Exception("schema lacks the 'root_name' key") - if not "types" in schema: - raise Exception("schema lacks the 'types' key") - for definedType in schema["types"]: - CheckTypeSchema(definedType) - - def EatToken(sentence: str) -> Tuple[str, str]: """splits "A,B,C" into "A" and "B,C" where A, B and C are type names (including templates) like "int32", "TotoTutu", or @@ -183,7 +160,7 @@ return (sentence, "") -def SplitListOfTypes(typeName: str) -> List[str]: +def SplitListOfTypes(typename: str) -> List[str]: """Splits something like vector<string>,int32,map<string,map<string,int32>> in: @@ -195,7 +172,7 @@ """ stillStuffToEat: bool = True tokenList = [] - restOfString = typeName + restOfString = typename while stillStuffToEat: firstToken, restOfString = EatToken(restOfString) tokenList.append(firstToken) @@ -208,16 +185,16 @@ re.compile(r"([a-zA-Z0-9_]*[a-zA-Z0-9_]*)<([a-zA-Z0-9_,:<>]+)>") -def ParseTemplateType(typeName) -> Tuple[bool, str, List[str]]: - """ If the type is a template like "SOMETHING<SOME<THING,EL<SE>>>", then - it returns (true,"SOMETHING","SOME<THING,EL<SE>>") +def ParseTemplateType(typename) -> Tuple[bool, str, List[str]]: + """ If the type is a template like "SOMETHING<SOME<THING,EL<SE>>>", + then it returns (true,"SOMETHING","SOME<THING,EL<SE>>") otherwise it returns (false,"","")""" # let's remove all whitespace from the type # split without argument uses any whitespace string as separator # (space, tab, newline, return or formfeed) - typeName = "".join(typeName.split()) - matches = templateRegex.match(typeName) + typename = "".join(typename.split()) + matches = templateRegex.match(typename) if matches == None: return (False, "", []) else: @@ -228,234 +205,220 @@ listOfDependentTypes = SplitListOfTypes(m.group(2)) return (True, m.group(1), listOfDependentTypes) -def ProcessTypeTree( - ancestors: List[str], - genOrderQueue: List[str], - structTypes: Dict[str, Dict], - typeName: str) -> None: - if typeName in ancestors: + +def ComputeOrderFromTypeTree( + ancestors: List[str], + genOrder: List[str], + shortTypename: str, schema: Dict[str, Dict]) -> None: + + if shortTypename in ancestors: raise Exception( f"Cyclic dependency chain found: the last of {ancestors} " - + f"depends on {typeName} that is already in the list." + + f"depends on {shortTypename} that is already in the list." ) - if not (typeName in genOrderQueue): - # if we reach this point, it means the type is NOT a struct or an enum. - # it is another (non directly user-defined) type that we must parse and - # create. Let's do it! - (isTemplate, _, dependentTypeNames) = ParseTemplateType(typeName) + if not (shortTypename in genOrder): + (isTemplate, _, dependentTypenames) = ParseTemplateType(shortTypename) if isTemplate: - for dependentTypeName in dependentTypeNames: + # if it is a template, it HAS dependent types... They can be + # anything (primitive, collection, enum, structs..). + # Let's process them! + for dependentTypename in dependentTypenames: # childAncestors = ancestors.copy() NO TEMPLATE ANCESTOR!!! - # childAncestors.append(typeName) - ProcessTypeTree( - ancestors, genOrderQueue, structTypes, dependentTypeName + # childAncestors.append(typename) + ComputeOrderFromTypeTree( + ancestors, genOrder, dependentTypename, schema ) else: - if typeName in structTypes: - ProcessStructType_DepthFirstRecursive( - genOrderQueue, structTypes, structTypes[typeName] - ) + # If it is not template, we are only interested if it is a + # dependency that we must take into account in the dep graph, + # i.e., a struct. + if IsShortStructType(shortTypename, schema): + struct:Dict = schema[GetLongTypename(shortTypename, schema)] + # The keys in the struct dict are the member names + # The values in the struct dict are the member types + for field in struct.keys(): + # we fill the chain of dependent types (starting here) + ancestors.append(shortTypename) + ComputeOrderFromTypeTree( + ancestors, genOrder, struct[field], schema) + # don't forget to restore it! + ancestors.pop() + + # now we're pretty sure our dependencies have been processed, + # we can start marking our code for generation (it might + # already have been done if someone referenced us earlier) + if not shortTypename in genOrder: + genOrder.append(shortTypename) -def ProcessStructType_DepthFirstRecursive(genOrderQueue: List[str], \ - structTypes: Dict[str, Dict], typeDict: Dict) -> None: - # let's generate the code according to the - typeName: str = typeDict["name"] - if typeDict["kind"] != "struct": - raise Exception( - f"Unexpected kind '{typeDict['kind']}' for " + "type '{typeName}'" - ) - typeFields: List[Dict] = typeDict["fields"] - for typeField in typeFields: - ancestors = [typeName] - ProcessTypeTree(ancestors, genOrderQueue, structTypes, typeField["type"]) - # now we're pretty sure our dependencies have been processed, - # we can start marking our code for generation (it might already have - # been done if someone referenced us earlier) - if not typeName in genOrderQueue: - genOrderQueue.append(typeName) +# +-----------------------+ +# | Utility functions | +# +-----------------------+ + +def IsShortStructType(typename: str, schema: Dict[str, Dict]) -> bool: + fullStructName = "struct " + typename + return (fullStructName in schema) -def ProcessEnumerationType(outputStreams: GeneratedCode, typeDict: Dict) -> None: +def GetLongTypename(shortTypename: str, schema: Dict): + if shortTypename.startswith("enum "): + raise RuntimeError('shortTypename.startswith("enum "):') + enumName: str = "enum " + shortTypename + isEnum = enumName in schema - # enumeration declarations - tsDeclText: StringIO = StringIO() - tsDeclText.write("enum %s\n" % typeDict["name"]) - tsDeclText.write("{\n") + if shortTypename.startswith("struct "): + raise RuntimeError('shortTypename.startswith("struct "):') + structName: str = "struct " + shortTypename + isStruct = ("struct " + shortTypename) in schema - cppDeclText: StringIO = StringIO() - cppDeclText.write("enum %s\n" % typeDict["name"]) - cppDeclText.write("{\n") + if isEnum and isStruct: + raise RuntimeError('Enums and structs cannot have the same name') - cppToStringText: StringIO = StringIO() - cppToStringText.write("enum %s\n" % typeDict["name"]) - cppToStringText.write("{\n") + if isEnum: + return enumName + if isStruct: + return structName + +def IsTypename(fullName: str) -> bool: + return (fullName.startswith("enum ") or fullName.startswith("struct ")) + +def IsEnumType(fullName: str) -> bool: + return fullName.startswith("enum ") - cppFromStringText: StringIO = StringIO() - cppFromStringText.write("enum %s\n" % typeDict["name"]) - cppFromStringText.write("{\n") +def IsStructType(fullName: str) -> bool: + return fullName.startswith("struct ") - for i in range(len(typeDict["fields"])): - field = typeDict["fields"][i] - name = field["name"] +def GetShortTypename(fullTypename: str) -> str: + if fullTypename.startswith("struct "): + return fullTypename[7:] + elif fullTypename.startswith("enum"): + return fullTypename[5:] + else: + raise RuntimeError \ + ('fullTypename should start with either "struct " or "enum "') - tsText.write(" %s" % name) - if i < len(typeDict["fields"]) - 1: - tsText.write(",") - tsText.write("\n") +def CheckSchemaSchema(schema: Dict) -> None: + if not "rootName" in schema: + raise Exception("schema lacks the 'rootName' key") + for name in schema.keys(): + if (not IsEnumType(name)) and (not IsStructType(name)) and \ + (name != 'rootName'): + raise RuntimeError \ + (f'Type "{name}" should start with "enum " or "struct "') + + # TODO: check enum fields are unique (in whole namespace) + # TODO: check struct fields are unique (in each struct) + +# +-----------------------+ +# | Main processing logic | +# +-----------------------+ + +def ComputeRequiredDeclarationOrder(schema: dict) -> List[str]: + # sanity check + CheckSchemaSchema(schema) - cppText.write(" %s" % name) - if i < len(typeDict["fields"]) - 1: - cppText.write(",") - cppText.write("\n") + # we traverse the type dependency graph and we fill a queue with + # the required struct types, in a bottom-up fashion, to compute + # the declaration order + # The genOrder list contains the struct full names in the order + # where they must be defined. + # We do not care about the enums here... They do not depend upon + # anything and we'll handle them, in their original declaration + # order, at the start + genOrder: List = [] + for fullName in schema.keys(): + if IsStructType(fullName): + realName: str = GetShortTypename(fullName) + ancestors: List[str] = [] + ComputeOrderFromTypeTree(ancestors, genOrder, realName, schema) + return genOrder - tsText.write("};\n\n") - cppText.write("};\n\n") - - outputStreams.tsEnums.write(tsText.getvalue()) - outputStreams.cppEnums.write(cppText.getvalue()) +def ProcessSchema(schema: dict, genOrder: List[str]) -> Dict: + # sanity check + CheckSchemaSchema(schema) -def GetSerializationCode(typeName: str,valueName: str, tempName: str) - if IsPrimitiveType(typeName): - """ - json::Value val(objectTypeInt...) - val.setValue(valueName) <--- val - """ - elif IsArray(typeName) - """ - { - json::Value val(objectTypeArray...) - for(size_t i = 0; i < {fieldName}.size(); ++i) - { - json::Value val(objectTypeArray...) - } - val.setValue(valueName) - // <--- the calling code will insert collection/field writing here, - // like "parent.set("{fieldName}",val) or parent.append(val) - $collectValue - } - """ + # let's doctor the schema to clean it up a bit + # order DOES NOT matter for enums, even though it's a list + enums: List[Dict] = [] + for fullName in schema.keys(): + if IsEnumType(fullName): + # convert "enum Toto" to "Toto" + typename:str = GetShortTypename(fullName) + enum = {} + enum['name'] = typename + assert(type(schema[fullName]) == list) + enum['fields'] = schema[fullName] # must be a list + enums.append(enum) - - -def ProcessStructType(outputStreams: GeneratedCode, typeDict) -> None: - tsText: StringIO = StringIO() - cppText: StringIO = StringIO() + # now that the order has been established, we actually store\ + # the structs in the correct order + # the structs are like: + # example = [ + # { + # "name": "Message1", + # "fields": { + # "someMember":"int32", + # "someOtherMember":"vector<string>" + # } + # }, + # { + # "name": "Message2", + # "fields": { + # "someMember":"int32", + # "someOtherMember22":"vector<Message1>" + # } + # } + # ] - tsText.write("class %s\n" % typeDict["name"]) - tsText.write("{\n") - - cppText.write("struct %s\n" % typeDict["name"]) - cppText.write("{\n") - - """ + structs: List[Dict] = [] + for i in range(len(genOrder)): + # this is already the short name + typename = genOrder[i] + fieldDict = schema["struct " + typename] + struct = {} + struct['name'] = typename + struct['fields'] = fieldDict + structs.append(struct) - GenerateSerializationCode(typeName,valueName) + templatingDict = {} + templatingDict['enums'] = enums + templatingDict['structs'] = structs + templatingDict['rootName'] = schema['rootName'] - primitives: - ----------- - int - jsonValue val(objectInt); - val.setValue("$name") - parent.add(("$name",$name) - double - ... - string - ... - - collections: - ----------- - dict { } - - - - - - serializeValue() - """ + return templatingDict - for i in range(len(typeDict["fields"])): - field = typeDict["fields"][i] - name = field["name"] - tsType = GetTypeScriptTypeNameFromCanonical(field["type"]) - tsText.write(" public %s %s;\n" % (tsType, name)) - cppType = GetCppTypeNameFromCanonical(field["type"]) - cppText.write(" %s %s;\n" % (cppType, name)) - - tsText.write("};\n\n") - cppText.write("};\n\n") +# +-----------------------+ +# | Write to files | +# +-----------------------+ - outputStreams.tsStructs.write(tsText.getvalue()) - outputStreams.cppStructs.write(cppText.getvalue()) - - -def WritePreambles(rootName: str, outputStreams: GeneratedCode) -> None: - outputStreams.cppPreamble.write( -"""// autogenerated by stonegentool on %s for module %s -#include <cstdint> -#include <string> -#include <vector> -#include <map> - -""" % (time.ctime(), rootName)) +# def WriteStreamsToFiles(rootName: str, genc: Dict[str, StringIO]) \ +# -> None: +# pass - outputStreams.tsPreamble.write( - """// autogenerated by stonegentool on %s for module %s -""" % (time.ctime(), rootName)) - - -def ProcessSchema(schema: dict) -> Tuple[str, GeneratedCode, List[str]]: - CheckSchemaSchema(schema) - rootName: str = schema["root_name"] - definedTypes: list = schema["types"] - - # this will be filled with the generation queue. That is, the type - # names in the order where they must be defined. - genOrderQueue: List = [] - - # the struct names are mapped to their JSON dictionary - structTypes: Dict[str, Dict] = {} - - outputStreams: GeneratedCode = GeneratedCode() - - WritePreambles(rootName, outputStreams) +def LoadSchema(fn): + with open(fn, 'rb') as f: + schema = yaml.load(f) + return schema - # the order here is the generation order - for definedType in definedTypes: - if definedType["kind"] == "enum": - ProcessEnumerationType(outputStreams, definedType) - - for definedType in definedTypes: - if definedType["kind"] == "struct": - structTypes[definedType["name"]] = definedType +def GetTemplatingDictFromSchemaFilename(fn): + obj = LoadSchema(fn) + genOrder: str = ComputeRequiredDeclarationOrder(obj) + templatingDict = ProcessSchema(obj, genOrder) + return templatingDict - # the order here is NOT the generation order: the types - # will be processed according to their dependency graph - for definedType in definedTypes: - if definedType["kind"] == "struct": - ProcessStructType_DepthFirstRecursive( - genOrderQueue, structTypes, definedType - ) - - for i in range(len(genOrderQueue)): - typeName = genOrderQueue[i] - typeDict = structTypes[typeName] - ProcessStructType(outputStreams, typeDict) - - return (rootName, outputStreams, genOrderQueue) - - -def WriteStreamsToFiles(rootName: str, outputStreams: Dict[str, StringIO]) -> None: - pass +# +-----------------------+ +# | ENTRY POINT | +# +-----------------------+ if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( - usage="""stonegentool.py [-h] [-o OUT_DIR] [-v] input_schemas - EXAMPLE: python command_gen.py -o "generated_files/" """ - + """ "mainSchema.json,App Specific Commands.json" """ + usage="""stonegentool.py [-h] [-o OUT_DIR] [-v] input_schema + EXAMPLE: python stonegentool.py -o "generated_files/" """ + + """ "mainSchema.yaml,App Specific Commands.json" """ ) - parser.add_argument("input_schema", type=str, help="path to the schema file") + parser.add_argument("input_schema", type=str, \ + help="path to the schema file") parser.add_argument( "-o", "--out_dir", @@ -479,6 +442,219 @@ inputSchemaFilename = args.input_schema outDir = args.out_dir - (rootName, outputStreams, _) = ProcessSchema(LoadSchema(inputSchemaFilename)) - WriteStreamsToFiles(rootName, outputStreams) + schema: Dict = LoadSchema(inputSchemaFilename) + genOrder: List[str] = ComputeRequiredDeclarationOrder(schema) + processedSchema: Dict = ProcessSchema(schema,genOrder) + + +# def GenEnumDecl(genc: GenCode, fullName: str, schema: Dict) -> None: +# """Writes the enumerations in genc""" +# enumDict:Dict=schema[fullName] +# # jinja2 template +# j2cppEnum = Template(trim( +# """ {{fullName}} +# { +# {% for key in enumDict.keys()%} +# {{key}}, +# {%endfor%} +# }; +# """)) +# j2cppEnumR = j2cppEnum.render(locals()) +# genc.cppEnums.write(j2cppEnumR) + +# j2tsEnum = Template(trim( +# """ export {{fullName}} +# { +# {% for key in enumDict.keys()%} +# {{key}}, +# {%endfor%} +# }; +# """)) +# j2cppEnumR = j2cppEnum.render(locals()) +# genc.tsEnums.write(j2cppEnumR) + + + +# def GetSerializationCode(typename: str,valueName: str, tempName: str) +# if IsPrimitiveType(typename) or IsTemplateCollection(typename): +# # no need to write code for the primitive types or collections. +# # It is handled in C++ by the template functions and in TS by +# # the JSON.stringify code. +# elif IsStructType(typename): +# pass + +# def GenStructTypeDeclAndSerialize(genc: GenCode, type, schema) -> None: +# ###### +# # CPP +# ###### +# sampleCpp = """ struct Message1 +# { +# int32_t a; +# std::string b; +# EnumMonth0 c; +# bool d; +# }; + +# Json::Value StoneSerialize(const Message1& value) +# { +# Json::Value result(Json::objectValue); +# result["a"] = StoneSerialize(value.a); +# result["b"] = StoneSerialize(value.b); +# result["c"] = StoneSerialize(value.c); +# result["d"] = StoneSerialize(value.d); +# return result; +# } +# """ + + +# ###### +# # TS +# ###### +# sampleTs = """ +# { +# export class Message1 { +# a: number; +# b: string; +# c: EnumMonth0; +# d: boolean; +# public StoneSerialize(): string { +# let container: object = {}; +# container['type'] = 'Message1'; +# container['value'] = this; +# return JSON.stringify(container); +# } +# }; +# } +# """ + + + + +# tsText: StringIO = StringIO() +# cppText: StringIO = StringIO() + +# tsText.write("class %s\n" % typeDict["name"]) +# tsText.write("{\n") + +# cppText.write("struct %s\n" % typeDict["name"]) +# cppText.write("{\n") + +# """ + +# GenerateSerializationCode(typename,valueName) +# primitives: +# ----------- +# int +# jsonValue val(objectInt); +# val.setValue("$name") +# parent.add(("$name",$name) +# double +# ... +# string +# ... + +# collections: +# ----------- +# dict { } + +# serializeValue() +# """ + +# for i in range(len(typeDict["fields"])): +# field = typeDict["fields"][i] +# name = field["name"] +# tsType = GetTypeScriptTypenameFromCanonical(field["type"]) +# tsText.write(" public %s %s;\n" % (tsType, name)) +# cppType = GetCppTypenameFromCanonical(field["type"]) +# cppText.write(" %s %s;\n" % (cppType, name)) + +# tsText.write("};\n\n") +# cppText.write("};\n\n") + +# genc.tsStructs.write(tsText.getvalue()) +# genc.cppStructs.write(cppText.getvalue()) + + +# def GenerateCodeFromTsTemplate(genc) + + +# +-----------------------+ +# | CODE GENERATION | +# +-----------------------+ + +# def GenPreambles(rootName: str, genc: GenCode) -> None: +# cppPreambleT = Template(trim( +# """// autogenerated by stonegentool on {{time.ctime()}} +# // for module {{rootName}} +# #include <cstdint> +# #include <string> +# #include <vector> +# #include <map> +# namespace {{rootName}} +# { +# Json::Value StoneSerialize(int32_t value) +# { +# Json::Value result(value); +# return result; +# } +# Json::Value StoneSerialize(double value) +# { +# Json::Value result(value); +# return result; +# } +# Json::Value StoneSerialize(bool value) +# { +# Json::Value result(value); +# return result; +# } +# Json::Value StoneSerialize(const std::string& value) +# { +# // the following is better than +# Json::Value result(value.data(),value.data()+value.size()); +# return result; +# } +# template<typename T> +# Json::Value StoneSerialize(const std::map<std::string,T>& value) +# { +# Json::Value result(Json::objectValue); + +# for (std::map<std::string, T>::const_iterator it = value.cbegin(); +# it != value.cend(); ++it) +# { +# // it->first it->second +# result[it->first] = StoneSerialize(it->second); +# } +# return result; +# } +# template<typename T> +# Json::Value StoneSerialize(const std::vector<T>& value) +# { +# Json::Value result(Json::arrayValue); +# for (size_t i = 0; i < value.size(); ++i) +# { +# result.append(StoneSerialize(value[i])); +# } +# return result; +# } +# """ +# cppPreambleR = cppPreambleT.render(locals()) +# genc.cppPreamble.write(cppPreambleR) + +# tsPreambleT = Template(trim( +# """// autogenerated by stonegentool on {{time.ctime()}} +# // for module {{rootName}} + +# namespace {{rootName}} +# { +# """ +# tsPreambleR = tsPreambleT.render(locals()) +# genc.tsPreamble.write(tsPreambleR) + +# def ComputeOrder_ProcessStruct( \ +# genOrder: List[str], name:str, schema: Dict[str, str]) -> None: +# # let's generate the code according to the +# struct = schema[name] + +# if not IsStructType(name): +# raise Exception(f'{typename} should start with "struct "')
--- a/Resources/CodeGeneration/stonegentool_test.py Mon Feb 18 15:38:05 2019 +0100 +++ b/Resources/CodeGeneration/stonegentool_test.py Wed Feb 20 20:51:30 2019 +0100 @@ -1,8 +1,46 @@ +# +# 1 2 3 4 5 6 7 8 +# 345678901234567890123456789012345678901234567890123456789012345678901234567890 +# + from stonegentool import \ -EatToken,SplitListOfTypes,ParseTemplateType,LoadSchema,CheckSchemaSchema,ProcessSchema +EatToken,SplitListOfTypes,ParseTemplateType,ProcessSchema, \ +CheckSchemaSchema,LoadSchema,trim,ComputeRequiredDeclarationOrder, \ +GetTemplatingDictFromSchemaFilename 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 @@ -78,129 +116,87 @@ self.assertEqual(b4,["int","vector<string>"]) def test_ParseSchema(self): - fn = os.path.join(os.path.dirname(__file__), 'test', 'test1.jsonc') + fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml') obj = LoadSchema(fn) - # we're happy if it does not crash + # 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_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_ParseSchema_bogus_schema(self): - fn = os.path.join(os.path.dirname(__file__), 'test', 'test1_bogus_schema.jsonc') + def test_ComputeRequiredDeclarationOrder(self): + fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml') obj = LoadSchema(fn) - self.assertRaises(Exception,CheckSchemaSchema,obj) + genOrder: str = ComputeRequiredDeclarationOrder(obj) + self.assertEqual(3,len(genOrder)) + self.assertEqual("A",genOrder[0]) + self.assertEqual("B",genOrder[1]) + self.assertEqual("C",genOrder[2]) - def test_GenOrderQueue(self): - fn = os.path.join(os.path.dirname(__file__), 'test', 'test1.jsonc') + # def test_GeneratePreambleEnumerationAndStructs(self): + # fn = os.path.join(os.path.dirname(__file__), 'test', 'test1.jsonc') + # obj = LoadSchema(fn) + # (_,genc,_) = ProcessSchema(obj) + + def test_genEnums(self): + fn = os.path.join(os.path.dirname(__file__), 'test_data', 'test1.yaml') obj = LoadSchema(fn) - genOrderQueue:str - _, _, genOrderQueue = ProcessSchema(obj) - self.assertEqual(3,len(genOrderQueue)) - self.assertEqual("A",genOrderQueue[0]) - self.assertEqual("B",genOrderQueue[1]) - self.assertEqual("C",genOrderQueue[2]) + 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'}]}""" - def test_GeneratePreambleEnumerationAndStructs(self): - fn = os.path.join(os.path.dirname(__file__), 'test', 'test1.jsonc') - obj = LoadSchema(fn) - (_,outputStreams,_) = ProcessSchema(obj) + self.assertEqual(processedSchemaStrRef,processedSchemaStr) - tsPreambleRef: str = "// autogenerated by stonegentool on Fri Feb 15 07:36:51 2019\n" - tsEnumsRef: str = """enum MovieType -{ - Romcom, + def test_GenerateTypeScriptEnums(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%} export enum {{enum['name']}} { +{% for key in enum['fields']%} {{key}}, +{%endfor%} }; + +{%endfor%}""") + renderedCode = template.render(**tdico) + renderedCodeRef = """ // end of generic methods + export enum MovieType { + RomCom, Horror, ScienceFiction, - Vegetables -}; + Vegetables, + }; -enum CrispType -{ + export enum CrispType { SaltAndPepper, CreamAndChives, Paprika, - Barbecue -}; - -""" - - tsStructsRef: str = """class A -{ - public Array<string> someStrings; - public Array<number> someInts2; -}; - -class B -{ - public Array<A> someAs; - public Array<number> someInts; -}; - -class C -{ - public Array<B> someBs; - public Array<D> ddd; -}; - -""" - - tsPreambleRefCastrated: str = RemoveDateTimeLine(tsPreambleRef) - tsPreambleCastrated: str = RemoveDateTimeLine(outputStreams.tsPreamble.getvalue()) - self.assertEqual(tsPreambleRefCastrated,tsPreambleCastrated) - self.assertEqual(tsEnumsRef,outputStreams.tsEnums.getvalue()) - self.assertEqual(tsStructsRef,outputStreams.tsStructs.getvalue()) - - cppPreambleRef: str = """// autogenerated by stonegentool on Fri Feb 15 07:36:51 2019 -#include <cstdint> -#include <string> -#include <vector> -#include <map> + Barbecue, + }; """ - - cppEnumsRef: str = """enum MovieType -{ - Romcom, - Horror, - ScienceFiction, - Vegetables -}; - -enum CrispType -{ - SaltAndPepper, - CreamAndChives, - Paprika, - Barbecue -}; + self.assertEqual(renderedCodeRef,renderedCode) -""" - cppStructsRef: str = """struct A -{ - std::vector<string> someStrings; - std::vector<int32_t> someInts2; -}; - -struct B -{ - std::vector<A> someAs; - std::vector<int32_t> someInts; -}; - -struct C -{ - std::vector<B> someBs; - std::vector<D> ddd; -}; - -""" - cppPreambleRefCastrated: str = RemoveDateTimeLine(cppPreambleRef) - cppPreambleCastrated: str = RemoveDateTimeLine(outputStreams.cppPreamble.getvalue()) - self.assertEqual(cppPreambleRefCastrated,cppPreambleCastrated) - self.assertEqual(cppEnumsRef,outputStreams.cppEnums.getvalue()) - self.assertEqual(cppStructsRef,outputStreams.cppStructs.getvalue()) def test_GenerateTypeScriptHandlerInterface(self): pass
--- a/Resources/CodeGeneration/template.in.ts Mon Feb 18 15:38:05 2019 +0100 +++ b/Resources/CodeGeneration/template.in.ts Wed Feb 20 20:51:30 2019 +0100 @@ -1,38 +1,125 @@ -class Greeter { - greeting: string; - constructor(message: string) { - this.greeting = message; +/* + 1 2 3 4 5 6 7 +12345678901234567890123456789012345678901234567890123456789012345678901234567890 +*/ + +namespace {{rootName}} +{ + function StoneCheckSerializedValueType(value: any, typeStr: string) + { + StoneCheckSerializedValueTypeGeneric(value); + + if (value['type'] != typeStr) + { + throw new Error( + `Cannot deserialize type ${value['type']} into ${typeStr}`); } - greet() { - return "Hello, " + this.greeting; + } + + function isString(val: any) :boolean + { + return ((typeof val === 'string') || (val instanceof String)); + } + + function StoneCheckSerializedValueTypeGeneric(value: any) + { + if ( (!('type' in value)) || (!isString(value)) ) + { + throw new Error( + "Cannot deserialize value ('type' key invalid)"); } -} -enum Color { Red, Green, Blue }; + } + +// end of generic methods +{% for enum in enums%} + export enum {{enum['name']}} { + {% for key in enumDict.keys()%} + {{key}}, + {%endfor%} + }; +{%endfor%} + + 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); + } + }; -class TestMessage { - s1: string; - s2: Array<string>; - s3: Array<Array<string>>; - s4: Map<string, number>; - s5: Map<number, Array<string>>; - s6: Color; - s7: boolean; -} + 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 + { + let value: any = JSON.parse(valueStr); + StoneCheckSerializedValueType(value, "VsolStuff.Message2"); + let result: Message2 = value['value'] as Message2; + return result; + } + }; + + export interface IDispatcher + { + HandleMessage1(value: Message1): boolean; + HandleMessage2(value: Message2): boolean; + }; -let tm = new TestMessage(); -tm.s2 = new Array<string>() -tm.s2.push("toto"); -tm.s2.push("toto2"); -tm.s2.push("toto3"); -tm.s4 = new Map<string, number>(); -tm.s4["toto"] = 42; -tm.s4["toto"] = 1999; -tm.s4["tatata"] = 1999; -tm.s6 = Color.Red; -tm.s7 = true + /** Service function for StoneDispatchToHandler */ + export function StoneDispatchJsonToHandler( + jsonValueStr: string, dispatcher: IDispatcher): boolean + { + let jsonValue: any = JSON.parse(jsonValueStr); + StoneCheckSerializedValueTypeGeneric(jsonValue); + let type: string = jsonValue["type"]; + if (type == "") + { + // this should never ever happen + throw new Error("Caught empty type while dispatching"); + } + else if (type == "VsolStuff.Message1") + { + 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); + } + else + { + return false; + } + } -let txt = JSON.stringify(tm) -console.log(txt); - -let greeter = new Greeter("world"); - + /** Takes a serialized type and passes this to the dispatcher */ + export function StoneDispatchToHandler( + strValue: string, dispatcher: IDispatcher): boolean + { + let jsonValue: any = JSON.parse(strValue) + return StoneDispatchJsonToHandler(jsonValue, dispatcher); + } +}