From e4b8f277c7540c059ae933e394496b1a6c6d4b6e Mon Sep 17 00:00:00 2001 From: Eiko Thomas Date: Tue, 12 Nov 2024 22:36:14 +0100 Subject: [PATCH] update golang template --- CHANGELOG.md | 3 + resources/templates/examples/golang.mako | 174 ++++++++++--- resources/templates/examples/golang_alt.mako | 133 ++++++++++ tests/generators/test_golangTemplate.py | 28 +++ version.txt | 2 +- yacg/generators/templates/golang.mako | 247 +++++++++++++++++++ 6 files changed, 556 insertions(+), 31 deletions(-) create mode 100644 resources/templates/examples/golang_alt.mako create mode 100644 yacg/generators/templates/golang.mako diff --git a/CHANGELOG.md b/CHANGELOG.md index e01c91ae..be73722e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 6.9.0 +* update Golang template, so that the builder pattern is supported + # 6.8.1 * update Dockerfile to add 'validateSchemas' script diff --git a/resources/templates/examples/golang.mako b/resources/templates/examples/golang.mako index 561367a3..363a8171 100644 --- a/resources/templates/examples/golang.mako +++ b/resources/templates/examples/golang.mako @@ -5,13 +5,17 @@ import yacg.model.modelFuncs as modelFuncs import yacg.util.stringUtils as stringUtils - templateFile = 'golang.mako' + templateFile = 'golang_types.mako' templateVersion = '1.1.0' packageName = templateParameters.get('modelPackage','<>') + jsonTypesPackage = templateParameters.get('jsonTypesPackage','<>') + jsonSerialization = templateParameters.get('jsonSerialization',False) + def printStarForJson(isJson): + return "*" if isJson else "" - def printGolangType(typeObj, isArray, isRequired, arrayDimensions = 1): + def printGolangType(typeObj, isArray, isRequired, arrayDimensions, forJson): ret = '' if typeObj is None: return '???' @@ -28,46 +32,80 @@ else: ret = 'float32' elif isinstance(typeObj, model.BooleanType): - ret = 'bool' + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'bool' + else: + ret = 'bool' elif isinstance(typeObj, model.StringType): ret = 'string' elif isinstance(typeObj, model.BytesType): ret = 'byte' elif isinstance(typeObj, model.UuidType): - ret = 'uuid.UUID' + ret = printStarForJson(forJson and (not isRequired)) + 'uuid.UUID' elif isinstance(typeObj, model.EnumType): ret = typeObj.name elif isinstance(typeObj, model.DateType): - ret = 'time.Date' + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonDate' + else: + ret = 'time.Time' elif isinstance(typeObj, model.TimeType): - ret = 'time.Time' + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonTime' + else: + ret = 'time.Time' elif isinstance(typeObj, model.DateTimeType): - ret = 'time.Date' + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonTimestamp' + else: + ret = 'time.Time' elif isinstance(typeObj, model.DictionaryType): - ret = 'map[string]{}'.format(printGolangType(typeObj.valueType, False, True)) + ret = '{}map[string]{}'.format(printStarForJson(forJson),printGolangType(typeObj.valueType, False, True, 0, False)) elif isinstance(typeObj, model.ComplexType): - ret = typeObj.name + if (not isArray) and (not isRequired): + ret = printStarForJson(forJson) + typeObj.name + else: + ret = typeObj.name else: ret = '???' + + if (not isRequired) and (not isArray) and (not isinstance(typeObj, model.DictionaryType)): + if isinstance(typeObj, model.EnumType) or hasattr(typeObj, "properties"): + ret = "*{}".format(ret) + else: + ret = "*{}".format(ret) if isArray: - ret = ("[]" * arrayDimensions) + ret - if not isRequired: - return "*" + ret + ret = printStarForJson(forJson) + ("[]" * arrayDimensions) + ret + + return ret + + def printOmitemptyIfNeeded(property): + if not property.required or property.isArray or isinstance(property.type, model.DictionaryType): + return ",omitempty" else: - return ret + return "" def getEnumDefaultValue(type): if type.default is not None: - return secureEnumValues(type.default) + return secureEnumValues(type.default, type.name) else: - return secureEnumValues(type.values[0]) + return secureEnumValues(type.values[0], type.name) - def secureEnumValues(value): - pattern = re.compile("^[0-9]") - return '_' + value if pattern.match(value) else value + def secureEnumValues(value, typeName): + valueName = stringUtils.toName(value) + typeName = stringUtils.toName(typeName) + return typeName + "_" + valueName + #pattern = re.compile("^[0-9]") + #return '_' + value if pattern.match(value) else value def isEnumDefaultValue(value, type): - return getEnumDefaultValue(type) == secureEnumValues(value) + return getEnumDefaultValue(type) == secureEnumValues(value, type.name) + + def sanitizePropertyName(property): + name = property.name + if name == "type": + return "type_" + return name %>// Attention, this file is generated. Manual changes get lost with the next @@ -75,13 +113,18 @@ // created by yacg (template: ${templateFile} v${templateVersion}) package ${packageName} -% if modelFuncs.isUuidContained(modelTypes): + import ( - // go get github.com/google/uuid - // https://pkg.go.dev/github.com/google/uuid#section-readme - uuid "github.com/google/uuid" -) +% if modelFuncs.isUuidContained(modelTypes): + uuid "github.com/google/uuid" +% endif +% if modelFuncs.isAtLeastOneDateRelatedTypeContained(modelTypes): + "time" % endif + "encoding/json" + "errors" + "fmt" +) % for type in modelTypes: % if modelFuncs.isEnumType(type): @@ -95,7 +138,7 @@ const ( ${getEnumDefaultValue(type)} ${type.name} = iota % for value in type.values: % if not isEnumDefaultValue(value, type): - ${value} + ${secureEnumValues(value, type.name)} % endif % endfor ) @@ -103,17 +146,37 @@ const ( func (s ${type.name}) String() string { switch s { % for value in type.values: - case ${secureEnumValues(value)}: + case ${secureEnumValues(value, type.name)}: return "${value}" % endfor } return "???" } +func (s ${type.name}) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *${type.name}) UnmarshalJSON(data []byte) error { + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + switch value { + % for value in type.values: + case "${value}": + *s = ${secureEnumValues(value, type.name)} + % endfor + default: + msg := fmt.Sprintf("invalid value for DDDDomainType: %s", value) + return errors.New(msg) + } + return nil +} + % endif -% endfor -% for type in modelTypes: % if hasattr(type, "properties"): % if type.description != None: /* ${templateHelper.addLineBreakToDescription(type.description,4)} @@ -125,9 +188,60 @@ type ${type.name} struct { % if property.description != None: // ${property.description} % endif - ${stringUtils.toUpperCamelCase(property.name)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions)} `json:"${property.name}"` + ${stringUtils.toUpperCamelCase(property.name)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions, False)} `json:"${property.name}${printOmitemptyIfNeeded(property)}"` + % endfor +} + + +func Make${type.name}Builder() *${type.name}Builder { + var b ${type.name}Builder + b.Init() + return &b +} + +type ${type.name}Builder struct { + % for property in type.properties: + ${sanitizePropertyName(property)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions, False)} + % endfor +} + +func (b *${type.name}Builder) Init() { + % for property in type.properties: + % if property.isArray: + b.${sanitizePropertyName(property)} = make(${"[]" * property.arrayDimensions}${printGolangType(property.type, True, True, 0, False)}, 0) + % endif + % if isinstance(property.type, model.DictionaryType): + b.${sanitizePropertyName(property)} = make(${printGolangType(property.type, False, True, 0, False)}) + % endif % endfor } + + + % for property in type.properties: + % if property.required or property.isArray or isinstance(property.type, model.DictionaryType): +func (b *${type.name}Builder) ${stringUtils.toUpperCamelCase(property.name)}(v ${printGolangType(property.type, property.isArray, True, property.arrayDimensions, False)}) *${type.name}Builder { + b.${sanitizePropertyName(property)} = v + return b +} + % else: +func (b *${type.name}Builder) ${stringUtils.toUpperCamelCase(property.name)}(v ${printGolangType(property.type, False, False, property.arrayDimensions, False)}) *${type.name}Builder { + b.${sanitizePropertyName(property)} = v + return b +} + % endif + % endfor +func (b *${type.name}Builder) Build() ${type.name} { + var r ${type.name} + % for property in type.properties: + r.${stringUtils.toUpperCamelCase(property.name)} = b.${sanitizePropertyName(property)} + % endfor + return r +} + + + % endif -% endfor \ No newline at end of file + + +% endfor diff --git a/resources/templates/examples/golang_alt.mako b/resources/templates/examples/golang_alt.mako new file mode 100644 index 00000000..561367a3 --- /dev/null +++ b/resources/templates/examples/golang_alt.mako @@ -0,0 +1,133 @@ +<% + import re + import yacg.model.model as model + import yacg.templateHelper as templateHelper + import yacg.model.modelFuncs as modelFuncs + import yacg.util.stringUtils as stringUtils + + templateFile = 'golang.mako' + templateVersion = '1.1.0' + + packageName = templateParameters.get('modelPackage','<>') + + + def printGolangType(typeObj, isArray, isRequired, arrayDimensions = 1): + ret = '' + if typeObj is None: + return '???' + elif isinstance(typeObj, model.IntegerType): + if typeObj.format is None or typeObj.format == model.IntegerTypeFormatEnum.INT32: + ret = 'int32' + else: + ret = 'int' + elif isinstance(typeObj, model.ObjectType): + ret = 'interface{}' + elif isinstance(typeObj, model.NumberType): + if typeObj.format is None or typeObj.format == model.NumberTypeFormatEnum.DOUBLE: + ret = 'float64' + else: + ret = 'float32' + elif isinstance(typeObj, model.BooleanType): + ret = 'bool' + elif isinstance(typeObj, model.StringType): + ret = 'string' + elif isinstance(typeObj, model.BytesType): + ret = 'byte' + elif isinstance(typeObj, model.UuidType): + ret = 'uuid.UUID' + elif isinstance(typeObj, model.EnumType): + ret = typeObj.name + elif isinstance(typeObj, model.DateType): + ret = 'time.Date' + elif isinstance(typeObj, model.TimeType): + ret = 'time.Time' + elif isinstance(typeObj, model.DateTimeType): + ret = 'time.Date' + elif isinstance(typeObj, model.DictionaryType): + ret = 'map[string]{}'.format(printGolangType(typeObj.valueType, False, True)) + elif isinstance(typeObj, model.ComplexType): + ret = typeObj.name + else: + ret = '???' + if isArray: + ret = ("[]" * arrayDimensions) + ret + if not isRequired: + return "*" + ret + else: + return ret + + def getEnumDefaultValue(type): + if type.default is not None: + return secureEnumValues(type.default) + else: + return secureEnumValues(type.values[0]) + + def secureEnumValues(value): + pattern = re.compile("^[0-9]") + return '_' + value if pattern.match(value) else value + + def isEnumDefaultValue(value, type): + return getEnumDefaultValue(type) == secureEnumValues(value) + + +%>// Attention, this file is generated. Manual changes get lost with the next +// run of the code generation. +// created by yacg (template: ${templateFile} v${templateVersion}) +package ${packageName} + +% if modelFuncs.isUuidContained(modelTypes): +import ( + // go get github.com/google/uuid + // https://pkg.go.dev/github.com/google/uuid#section-readme + uuid "github.com/google/uuid" +) +% endif + +% for type in modelTypes: + % if modelFuncs.isEnumType(type): + % if type.description != None: +/* ${templateHelper.addLineBreakToDescription(type.description,4)} +*/ + % endif +type ${type.name} int64 + +const ( + ${getEnumDefaultValue(type)} ${type.name} = iota + % for value in type.values: + % if not isEnumDefaultValue(value, type): + ${value} + % endif + % endfor +) + +func (s ${type.name}) String() string { + switch s { + % for value in type.values: + case ${secureEnumValues(value)}: + return "${value}" + % endfor + } + return "???" +} + + % endif +% endfor + +% for type in modelTypes: + % if hasattr(type, "properties"): + % if type.description != None: +/* ${templateHelper.addLineBreakToDescription(type.description,4)} +*/ + % endif +type ${type.name} struct { + % for property in type.properties: + + % if property.description != None: + // ${property.description} + % endif + ${stringUtils.toUpperCamelCase(property.name)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions)} `json:"${property.name}"` + % endfor +} + + % endif +% endfor \ No newline at end of file diff --git a/tests/generators/test_golangTemplate.py b/tests/generators/test_golangTemplate.py index ebef48fb..e509a436 100644 --- a/tests/generators/test_golangTemplate.py +++ b/tests/generators/test_golangTemplate.py @@ -45,6 +45,34 @@ def testGolangTemplate(self): (), singleFileTask) + def testGolangTemplate(self): + dirpath = Path('tmp', 'golang3') + if dirpath.exists() and dirpath.is_dir(): + shutil.rmtree(dirpath) + modelFile = 'resources/models/yaml/layer.yaml' + modelFileExists = os.path.isfile(modelFile) + self.assertTrue('model file exists: ' + modelFile, modelFileExists) + model = config.Model() + model.schema = modelFile + modelTypes = getModelFromYaml(model, []) + templateFile = 'yacg/generators/templates/golang.mako' + templateParameters = [] + templateParam = config.TemplateParam() + templateParam.name = 'modelPackage' + templateParam.value = 'golang_test' + templateParameters.append(templateParam) + singleFileTask = SingleFileTask() + singleFileTask.template = templateFile + singleFileTask.destFile = 'tmp/golang3/layer.go' + singleFileTask.templateParams = templateParameters + + renderSingleFileTemplate( + modelTypes, + (), + (), + singleFileTask) + + def testEvilEnum(self): dirpath = Path('tmp', 'golang2') if dirpath.exists() and dirpath.is_dir(): diff --git a/version.txt b/version.txt index 5f6c0863..97f57815 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -6.8.1 +6.9.0 diff --git a/yacg/generators/templates/golang.mako b/yacg/generators/templates/golang.mako new file mode 100644 index 00000000..363a8171 --- /dev/null +++ b/yacg/generators/templates/golang.mako @@ -0,0 +1,247 @@ +<% + import re + import yacg.model.model as model + import yacg.templateHelper as templateHelper + import yacg.model.modelFuncs as modelFuncs + import yacg.util.stringUtils as stringUtils + + templateFile = 'golang_types.mako' + templateVersion = '1.1.0' + + packageName = templateParameters.get('modelPackage','<>') + jsonTypesPackage = templateParameters.get('jsonTypesPackage','<>') + jsonSerialization = templateParameters.get('jsonSerialization',False) + + def printStarForJson(isJson): + return "*" if isJson else "" + + def printGolangType(typeObj, isArray, isRequired, arrayDimensions, forJson): + ret = '' + if typeObj is None: + return '???' + elif isinstance(typeObj, model.IntegerType): + if typeObj.format is None or typeObj.format == model.IntegerTypeFormatEnum.INT32: + ret = 'int32' + else: + ret = 'int' + elif isinstance(typeObj, model.ObjectType): + ret = 'interface{}' + elif isinstance(typeObj, model.NumberType): + if typeObj.format is None or typeObj.format == model.NumberTypeFormatEnum.DOUBLE: + ret = 'float64' + else: + ret = 'float32' + elif isinstance(typeObj, model.BooleanType): + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'bool' + else: + ret = 'bool' + elif isinstance(typeObj, model.StringType): + ret = 'string' + elif isinstance(typeObj, model.BytesType): + ret = 'byte' + elif isinstance(typeObj, model.UuidType): + ret = printStarForJson(forJson and (not isRequired)) + 'uuid.UUID' + elif isinstance(typeObj, model.EnumType): + ret = typeObj.name + elif isinstance(typeObj, model.DateType): + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonDate' + else: + ret = 'time.Time' + elif isinstance(typeObj, model.TimeType): + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonTime' + else: + ret = 'time.Time' + elif isinstance(typeObj, model.DateTimeType): + if jsonSerialization: + ret = printStarForJson(forJson and (not isRequired)) + 'json_types.JsonTimestamp' + else: + ret = 'time.Time' + elif isinstance(typeObj, model.DictionaryType): + ret = '{}map[string]{}'.format(printStarForJson(forJson),printGolangType(typeObj.valueType, False, True, 0, False)) + elif isinstance(typeObj, model.ComplexType): + if (not isArray) and (not isRequired): + ret = printStarForJson(forJson) + typeObj.name + else: + ret = typeObj.name + else: + ret = '???' + + if (not isRequired) and (not isArray) and (not isinstance(typeObj, model.DictionaryType)): + if isinstance(typeObj, model.EnumType) or hasattr(typeObj, "properties"): + ret = "*{}".format(ret) + else: + ret = "*{}".format(ret) + if isArray: + ret = printStarForJson(forJson) + ("[]" * arrayDimensions) + ret + + return ret + + def printOmitemptyIfNeeded(property): + if not property.required or property.isArray or isinstance(property.type, model.DictionaryType): + return ",omitempty" + else: + return "" + + def getEnumDefaultValue(type): + if type.default is not None: + return secureEnumValues(type.default, type.name) + else: + return secureEnumValues(type.values[0], type.name) + + def secureEnumValues(value, typeName): + valueName = stringUtils.toName(value) + typeName = stringUtils.toName(typeName) + return typeName + "_" + valueName + #pattern = re.compile("^[0-9]") + #return '_' + value if pattern.match(value) else value + + def isEnumDefaultValue(value, type): + return getEnumDefaultValue(type) == secureEnumValues(value, type.name) + + def sanitizePropertyName(property): + name = property.name + if name == "type": + return "type_" + return name + + +%>// Attention, this file is generated. Manual changes get lost with the next +// run of the code generation. +// created by yacg (template: ${templateFile} v${templateVersion}) +package ${packageName} + + +import ( +% if modelFuncs.isUuidContained(modelTypes): + uuid "github.com/google/uuid" +% endif +% if modelFuncs.isAtLeastOneDateRelatedTypeContained(modelTypes): + "time" +% endif + "encoding/json" + "errors" + "fmt" +) + +% for type in modelTypes: + % if modelFuncs.isEnumType(type): + % if type.description != None: +/* ${templateHelper.addLineBreakToDescription(type.description,4)} +*/ + % endif +type ${type.name} int64 + +const ( + ${getEnumDefaultValue(type)} ${type.name} = iota + % for value in type.values: + % if not isEnumDefaultValue(value, type): + ${secureEnumValues(value, type.name)} + % endif + % endfor +) + +func (s ${type.name}) String() string { + switch s { + % for value in type.values: + case ${secureEnumValues(value, type.name)}: + return "${value}" + % endfor + } + return "???" +} + +func (s ${type.name}) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *${type.name}) UnmarshalJSON(data []byte) error { + var value string + if err := json.Unmarshal(data, &value); err != nil { + return err + } + + switch value { + % for value in type.values: + case "${value}": + *s = ${secureEnumValues(value, type.name)} + % endfor + default: + msg := fmt.Sprintf("invalid value for DDDDomainType: %s", value) + return errors.New(msg) + } + return nil +} + + % endif + + % if hasattr(type, "properties"): + % if type.description != None: +/* ${templateHelper.addLineBreakToDescription(type.description,4)} +*/ + % endif +type ${type.name} struct { + % for property in type.properties: + + % if property.description != None: + // ${property.description} + % endif + ${stringUtils.toUpperCamelCase(property.name)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions, False)} `json:"${property.name}${printOmitemptyIfNeeded(property)}"` + % endfor +} + + +func Make${type.name}Builder() *${type.name}Builder { + var b ${type.name}Builder + b.Init() + return &b +} + +type ${type.name}Builder struct { + % for property in type.properties: + ${sanitizePropertyName(property)} ${printGolangType(property.type, property.isArray, property.required, property.arrayDimensions, False)} + % endfor +} + +func (b *${type.name}Builder) Init() { + % for property in type.properties: + % if property.isArray: + b.${sanitizePropertyName(property)} = make(${"[]" * property.arrayDimensions}${printGolangType(property.type, True, True, 0, False)}, 0) + % endif + % if isinstance(property.type, model.DictionaryType): + b.${sanitizePropertyName(property)} = make(${printGolangType(property.type, False, True, 0, False)}) + % endif + % endfor +} + + + + % for property in type.properties: + % if property.required or property.isArray or isinstance(property.type, model.DictionaryType): +func (b *${type.name}Builder) ${stringUtils.toUpperCamelCase(property.name)}(v ${printGolangType(property.type, property.isArray, True, property.arrayDimensions, False)}) *${type.name}Builder { + b.${sanitizePropertyName(property)} = v + return b +} + % else: +func (b *${type.name}Builder) ${stringUtils.toUpperCamelCase(property.name)}(v ${printGolangType(property.type, False, False, property.arrayDimensions, False)}) *${type.name}Builder { + b.${sanitizePropertyName(property)} = v + return b +} + % endif + % endfor +func (b *${type.name}Builder) Build() ${type.name} { + var r ${type.name} + % for property in type.properties: + r.${stringUtils.toUpperCamelCase(property.name)} = b.${sanitizePropertyName(property)} + % endfor + return r +} + + + + % endif + + +% endfor