protoc-gen-go-micro/helpers/helpers.go

870 lines
22 KiB
Go

package pgghelpers
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"strings"
"text/template"
"github.com/Masterminds/sprig"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/protoc-gen-go/descriptor"
ggdescriptor "github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway/descriptor"
"github.com/huandu/xstrings"
options "google.golang.org/genproto/googleapis/api/annotations"
)
var jsReservedRe = regexp.MustCompile(`(^|[^A-Za-z])(do|if|in|for|let|new|try|var|case|else|enum|eval|false|null|this|true|void|with|break|catch|class|const|super|throw|while|yield|delete|export|import|public|return|static|switch|typeof|default|extends|finally|package|private|continue|debugger|function|arguments|interface|protected|implements|instanceof)($|[^A-Za-z])`)
var (
registry *ggdescriptor.Registry // some helpers need access to registry
)
func SetRegistry(reg *ggdescriptor.Registry) {
registry = reg
}
var ProtoHelpersFuncMap = template.FuncMap{
"string": func(i interface {
String() string
}) string {
return i.String()
},
"json": func(v interface{}) string {
a, err := json.Marshal(v)
if err != nil {
return err.Error()
}
return string(a)
},
"prettyjson": func(v interface{}) string {
a, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err.Error()
}
return string(a)
},
"splitArray": func(sep string, s string) []interface{} {
var r []interface{}
t := strings.Split(s, sep)
for i := range t {
if t[i] != "" {
r = append(r, t[i])
}
}
return r
},
"first": func(a []string) string {
return a[0]
},
"last": func(a []string) string {
return a[len(a)-1]
},
"upperFirst": func(s string) string {
return strings.ToUpper(s[:1]) + s[1:]
},
"lowerFirst": func(s string) string {
return strings.ToLower(s[:1]) + s[1:]
},
"camelCase": func(s string) string {
if len(s) > 1 {
return xstrings.ToCamelCase(s)
}
return strings.ToUpper(s[:1])
},
"lowerCamelCase": func(s string) string {
if len(s) > 1 {
s = xstrings.ToCamelCase(s)
}
return strings.ToLower(s[:1]) + s[1:]
},
"kebabCase": func(s string) string {
return strings.Replace(xstrings.ToSnakeCase(s), "_", "-", -1)
},
"contains": func(sub, s string) bool {
return strings.Contains(s, sub)
},
"trimstr": func(cutset, s string) string {
return strings.Trim(s, cutset)
},
"index": func(array interface{}, i int32) interface{} {
slice := reflect.ValueOf(array)
if slice.Kind() != reflect.Slice {
panic("Error in index(): given a non-slice type")
}
if i < 0 || int(i) >= slice.Len() {
panic("Error in index(): index out of bounds")
}
return slice.Index(int(i)).Interface()
},
"snakeCase": xstrings.ToSnakeCase,
"getProtoFile": getProtoFile,
"getMessageType": getMessageType,
"getEnumValue": getEnumValue,
"isFieldMessage": isFieldMessage,
"isFieldMessageTimeStamp": isFieldMessageTimeStamp,
"isFieldRepeated": isFieldRepeated,
"haskellType": haskellType,
"goType": goType,
"goZeroValue": goZeroValue,
"goTypeWithPackage": goTypeWithPackage,
"jsType": jsType,
"jsSuffixReserved": jsSuffixReservedKeyword,
"namespacedFlowType": namespacedFlowType,
"httpVerb": httpVerb,
"httpPath": httpPath,
"httpPathsAdditionalBindings": httpPathsAdditionalBindings,
"httpBody": httpBody,
"shortType": shortType,
"urlHasVarsFromMessage": urlHasVarsFromMessage,
"lowerGoNormalize": lowerGoNormalize,
"goNormalize": goNormalize,
"leadingComment": leadingComment,
"trailingComment": trailingComment,
"leadingDetachedComments": leadingDetachedComments,
"stringFieldExtension": stringFieldExtension,
"boolFieldExtension": boolFieldExtension,
"isFieldMap": isFieldMap,
"fieldMapKeyType": fieldMapKeyType,
"fieldMapValueType": fieldMapValueType,
"replaceDict": replaceDict,
}
var pathMap map[interface{}]*descriptor.SourceCodeInfo_Location
func InitPathMap(file *descriptor.FileDescriptorProto) {
pathMap = make(map[interface{}]*descriptor.SourceCodeInfo_Location)
addToPathMap(file.GetSourceCodeInfo(), file, []int32{})
}
func InitPathMaps(files []*descriptor.FileDescriptorProto) {
pathMap = make(map[interface{}]*descriptor.SourceCodeInfo_Location)
for _, file := range files {
addToPathMap(file.GetSourceCodeInfo(), file, []int32{})
}
}
// addToPathMap traverses through the AST adding SourceCodeInfo_Location entries to the pathMap.
// Since the AST is a tree, the recursion finishes once it has gone through all the nodes.
func addToPathMap(info *descriptor.SourceCodeInfo, i interface{}, path []int32) {
loc := findLoc(info, path)
if loc != nil {
pathMap[i] = loc
}
switch d := i.(type) {
case *descriptor.FileDescriptorProto:
for index, descriptor := range d.MessageType {
addToPathMap(info, descriptor, newPath(path, 4, index))
}
for index, descriptor := range d.EnumType {
addToPathMap(info, descriptor, newPath(path, 5, index))
}
for index, descriptor := range d.Service {
addToPathMap(info, descriptor, newPath(path, 6, index))
}
case *descriptor.DescriptorProto:
for index, descriptor := range d.Field {
addToPathMap(info, descriptor, newPath(path, 2, index))
}
for index, descriptor := range d.NestedType {
addToPathMap(info, descriptor, newPath(path, 3, index))
}
for index, descriptor := range d.EnumType {
addToPathMap(info, descriptor, newPath(path, 4, index))
}
case *descriptor.EnumDescriptorProto:
for index, descriptor := range d.Value {
addToPathMap(info, descriptor, newPath(path, 2, index))
}
case *descriptor.ServiceDescriptorProto:
for index, descriptor := range d.Method {
addToPathMap(info, descriptor, newPath(path, 2, index))
}
}
}
func newPath(base []int32, field int32, index int) []int32 {
p := append([]int32{}, base...)
p = append(p, field, int32(index))
return p
}
func findLoc(info *descriptor.SourceCodeInfo, path []int32) *descriptor.SourceCodeInfo_Location {
for _, loc := range info.GetLocation() {
if samePath(loc.Path, path) {
return loc
}
}
return nil
}
func samePath(a, b []int32) bool {
if len(a) != len(b) {
return false
}
for i, p := range a {
if p != b[i] {
return false
}
}
return true
}
func findSourceInfoLocation(i interface{}) *descriptor.SourceCodeInfo_Location {
if pathMap == nil {
return nil
}
return pathMap[i]
}
func leadingComment(i interface{}) string {
loc := pathMap[i]
return loc.GetLeadingComments()
}
func trailingComment(i interface{}) string {
loc := pathMap[i]
return loc.GetTrailingComments()
}
func leadingDetachedComments(i interface{}) []string {
loc := pathMap[i]
return loc.GetLeadingDetachedComments()
}
func stringFieldExtension(fieldID int32, f *descriptor.FieldDescriptorProto) string {
if f == nil {
return ""
}
if f.Options == nil {
return ""
}
var extendedType *descriptor.FieldOptions
var extensionType *string
eds := proto.RegisteredExtensions(f.Options)
if eds[fieldID] == nil {
ed := &proto.ExtensionDesc{
ExtendedType: extendedType,
ExtensionType: extensionType,
Field: fieldID,
Tag: fmt.Sprintf("bytes,%d", fieldID),
}
proto.RegisterExtension(ed)
eds = proto.RegisteredExtensions(f.Options)
}
ext, err := proto.GetExtension(f.Options, eds[fieldID])
if err != nil {
return ""
}
str, ok := ext.(*string)
if !ok {
return ""
}
return *str
}
func boolFieldExtension(fieldID int32, f *descriptor.FieldDescriptorProto) bool {
if f == nil {
return false
}
if f.Options == nil {
return false
}
var extendedType *descriptor.FieldOptions
var extensionType *bool
eds := proto.RegisteredExtensions(f.Options)
if eds[fieldID] == nil {
ed := &proto.ExtensionDesc{
ExtendedType: extendedType,
ExtensionType: extensionType,
Field: fieldID,
Tag: fmt.Sprintf("varint,%d", fieldID),
}
proto.RegisterExtension(ed)
eds = proto.RegisteredExtensions(f.Options)
}
ext, err := proto.GetExtension(f.Options, eds[fieldID])
if err != nil {
return false
}
str, ok := ext.(*bool)
if !ok {
return false
}
return *str
}
func init() {
for k, v := range sprig.TxtFuncMap() {
ProtoHelpersFuncMap[k] = v
}
}
func getProtoFile(name string) *ggdescriptor.File {
if registry == nil {
return nil
}
file, err := registry.LookupFile(name)
if err != nil {
panic(err)
}
return file
}
func getMessageType(f *descriptor.FileDescriptorProto, name string) *ggdescriptor.Message {
if registry != nil {
msg, err := registry.LookupMsg(".", name)
if err != nil {
panic(err)
}
return msg
}
// name is in the form .packageName.MessageTypeName.InnerMessageTypeName...
// e.g. .article.ProductTag
splits := strings.Split(name, ".")
target := splits[len(splits)-1]
for _, m := range f.MessageType {
if target == *m.Name {
return &ggdescriptor.Message{
DescriptorProto: m,
}
}
}
return nil
}
func getEnumValue(f []*descriptor.EnumDescriptorProto, name string) []*descriptor.EnumValueDescriptorProto {
for _, item := range f {
if strings.EqualFold(*item.Name, name) {
return item.GetValue()
}
}
return nil
}
func isFieldMessageTimeStamp(f *descriptor.FieldDescriptorProto) bool {
if f.Type != nil && *f.Type == descriptor.FieldDescriptorProto_TYPE_MESSAGE {
if strings.Compare(*f.TypeName, ".google.protobuf.Timestamp") == 0 {
return true
}
}
return false
}
func isFieldMessage(f *descriptor.FieldDescriptorProto) bool {
if f.Type != nil && *f.Type == descriptor.FieldDescriptorProto_TYPE_MESSAGE {
return true
}
return false
}
func isFieldRepeated(f *descriptor.FieldDescriptorProto) bool {
if f == nil {
return false
}
if f.Type != nil && f.Label != nil && *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return true
}
return false
}
func isFieldMap(f *descriptor.FieldDescriptorProto, m *descriptor.DescriptorProto) bool {
if f.TypeName == nil {
return false
}
shortName := shortType(*f.TypeName)
var nt *descriptor.DescriptorProto
for _, t := range m.NestedType {
if *t.Name == shortName {
nt = t
break
}
}
if nt == nil {
return false
}
for _, f := range nt.Field {
switch *f.Name {
case "key":
if *f.Number != 1 {
return false
}
case "value":
if *f.Number != 2 {
return false
}
default:
return false
}
}
return true
}
func fieldMapKeyType(f *descriptor.FieldDescriptorProto, m *descriptor.DescriptorProto) *descriptor.FieldDescriptorProto {
if f.TypeName == nil {
return nil
}
shortName := shortType(*f.TypeName)
var nt *descriptor.DescriptorProto
for _, t := range m.NestedType {
if *t.Name == shortName {
nt = t
break
}
}
if nt == nil {
return nil
}
for _, f := range nt.Field {
if *f.Name == "key" {
return f
}
}
return nil
}
func fieldMapValueType(f *descriptor.FieldDescriptorProto, m *descriptor.DescriptorProto) *descriptor.FieldDescriptorProto {
if f.TypeName == nil {
return nil
}
shortName := shortType(*f.TypeName)
var nt *descriptor.DescriptorProto
for _, t := range m.NestedType {
if *t.Name == shortName {
nt = t
break
}
}
if nt == nil {
return nil
}
for _, f := range nt.Field {
if *f.Name == "value" {
return f
}
}
return nil
}
func goTypeWithPackage(f *descriptor.FieldDescriptorProto) string {
pkg := ""
if *f.Type == descriptor.FieldDescriptorProto_TYPE_MESSAGE || *f.Type == descriptor.FieldDescriptorProto_TYPE_ENUM {
pkg = getPackageTypeName(*f.TypeName)
}
return goType(pkg, f)
}
func haskellType(pkg string, f *descriptor.FieldDescriptorProto) string {
switch *f.Type {
case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Float]"
}
return "Float"
case descriptor.FieldDescriptorProto_TYPE_FLOAT:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Float]"
}
return "Float"
case descriptor.FieldDescriptorProto_TYPE_INT64:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Int64]"
}
return "Int64"
case descriptor.FieldDescriptorProto_TYPE_UINT64:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Word]"
}
return "Word"
case descriptor.FieldDescriptorProto_TYPE_INT32:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Int]"
}
return "Int"
case descriptor.FieldDescriptorProto_TYPE_UINT32:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Word]"
}
return "Word"
case descriptor.FieldDescriptorProto_TYPE_BOOL:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Bool]"
}
return "Bool"
case descriptor.FieldDescriptorProto_TYPE_STRING:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Text]"
}
return "Text"
case descriptor.FieldDescriptorProto_TYPE_MESSAGE:
if pkg != "" {
pkg = pkg + "."
}
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return fmt.Sprintf("[%s%s]", pkg, shortType(*f.TypeName))
}
return fmt.Sprintf("%s%s", pkg, shortType(*f.TypeName))
case descriptor.FieldDescriptorProto_TYPE_BYTES:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[Word8]"
}
return "Word8"
case descriptor.FieldDescriptorProto_TYPE_ENUM:
return fmt.Sprintf("%s%s", pkg, shortType(*f.TypeName))
default:
return "Generic"
}
}
func goType(pkg string, f *descriptor.FieldDescriptorProto) string {
if pkg != "" {
pkg = pkg + "."
}
switch *f.Type {
case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]float64"
}
return "float64"
case descriptor.FieldDescriptorProto_TYPE_FLOAT:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]float32"
}
return "float32"
case descriptor.FieldDescriptorProto_TYPE_INT64:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]int64"
}
return "int64"
case descriptor.FieldDescriptorProto_TYPE_UINT64:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]uint64"
}
return "uint64"
case descriptor.FieldDescriptorProto_TYPE_INT32:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]int32"
}
return "int32"
case descriptor.FieldDescriptorProto_TYPE_UINT32:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]uint32"
}
return "uint32"
case descriptor.FieldDescriptorProto_TYPE_BOOL:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]bool"
}
return "bool"
case descriptor.FieldDescriptorProto_TYPE_STRING:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]string"
}
return "string"
case descriptor.FieldDescriptorProto_TYPE_MESSAGE:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return fmt.Sprintf("[]*%s%s", pkg, shortType(*f.TypeName))
}
return fmt.Sprintf("*%s%s", pkg, shortType(*f.TypeName))
case descriptor.FieldDescriptorProto_TYPE_BYTES:
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return "[]byte"
}
return "byte"
case descriptor.FieldDescriptorProto_TYPE_ENUM:
return fmt.Sprintf("*%s%s", pkg, shortType(*f.TypeName))
default:
return "interface{}"
}
}
func goZeroValue(f *descriptor.FieldDescriptorProto) string {
const nilString = "nil"
if *f.Label == descriptor.FieldDescriptorProto_LABEL_REPEATED {
return nilString
}
switch *f.Type {
case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
return "0.0"
case descriptor.FieldDescriptorProto_TYPE_FLOAT:
return "0.0"
case descriptor.FieldDescriptorProto_TYPE_INT64:
return "0"
case descriptor.FieldDescriptorProto_TYPE_UINT64:
return "0"
case descriptor.FieldDescriptorProto_TYPE_INT32:
return "0"
case descriptor.FieldDescriptorProto_TYPE_UINT32:
return "0"
case descriptor.FieldDescriptorProto_TYPE_BOOL:
return "false"
case descriptor.FieldDescriptorProto_TYPE_STRING:
return "\"\""
case descriptor.FieldDescriptorProto_TYPE_MESSAGE:
return nilString
case descriptor.FieldDescriptorProto_TYPE_BYTES:
return "0"
case descriptor.FieldDescriptorProto_TYPE_ENUM:
return nilString
default:
return nilString
}
}
func jsType(f *descriptor.FieldDescriptorProto) string {
template := "%s"
if isFieldRepeated(f) {
template = "Array<%s>"
}
switch *f.Type {
case descriptor.FieldDescriptorProto_TYPE_MESSAGE,
descriptor.FieldDescriptorProto_TYPE_ENUM:
return fmt.Sprintf(template, namespacedFlowType(*f.TypeName))
case descriptor.FieldDescriptorProto_TYPE_DOUBLE,
descriptor.FieldDescriptorProto_TYPE_FLOAT,
descriptor.FieldDescriptorProto_TYPE_INT64,
descriptor.FieldDescriptorProto_TYPE_UINT64,
descriptor.FieldDescriptorProto_TYPE_INT32,
descriptor.FieldDescriptorProto_TYPE_FIXED64,
descriptor.FieldDescriptorProto_TYPE_FIXED32,
descriptor.FieldDescriptorProto_TYPE_UINT32,
descriptor.FieldDescriptorProto_TYPE_SFIXED32,
descriptor.FieldDescriptorProto_TYPE_SFIXED64,
descriptor.FieldDescriptorProto_TYPE_SINT32,
descriptor.FieldDescriptorProto_TYPE_SINT64:
return fmt.Sprintf(template, "number")
case descriptor.FieldDescriptorProto_TYPE_BOOL:
return fmt.Sprintf(template, "boolean")
case descriptor.FieldDescriptorProto_TYPE_BYTES:
return fmt.Sprintf(template, "Uint8Array")
case descriptor.FieldDescriptorProto_TYPE_STRING:
return fmt.Sprintf(template, "string")
default:
return fmt.Sprintf(template, "any")
}
}
func jsSuffixReservedKeyword(s string) string {
return jsReservedRe.ReplaceAllString(s, "${1}${2}_${3}")
}
func getPackageTypeName(s string) string {
if strings.Compare(s, ".google.protobuf.Timestamp") == 0 {
return "timestamp"
}
if strings.Contains(s, ".") {
return strings.Split(s, ".")[1]
}
return ""
}
func shortType(s string) string {
t := strings.Split(s, ".")
return t[len(t)-1]
}
func namespacedFlowType(s string) string {
trimmed := strings.TrimLeft(s, ".")
splitted := strings.Split(trimmed, ".")
return strings.Join(splitted, "$")
}
func httpPath(m *descriptor.MethodDescriptorProto) string {
ext, err := proto.GetExtension(m.Options, options.E_Http)
if err != nil {
return err.Error()
}
opts, ok := ext.(*options.HttpRule)
if !ok {
return fmt.Sprintf("extension is %T; want an HttpRule", ext)
}
switch t := opts.Pattern.(type) {
default:
return ""
case *options.HttpRule_Get:
return t.Get
case *options.HttpRule_Post:
return t.Post
case *options.HttpRule_Put:
return t.Put
case *options.HttpRule_Delete:
return t.Delete
case *options.HttpRule_Patch:
return t.Patch
case *options.HttpRule_Custom:
return t.Custom.Path
}
}
func httpPathsAdditionalBindings(m *descriptor.MethodDescriptorProto) []string {
ext, err := proto.GetExtension(m.Options, options.E_Http)
if err != nil {
panic(err.Error())
}
opts, ok := ext.(*options.HttpRule)
if !ok {
panic(fmt.Sprintf("extension is %T; want an HttpRule", ext))
}
var httpPaths []string
var optsAdditionalBindings = opts.GetAdditionalBindings()
for _, optAdditionalBindings := range optsAdditionalBindings {
switch t := optAdditionalBindings.Pattern.(type) {
case *options.HttpRule_Get:
httpPaths = append(httpPaths, t.Get)
case *options.HttpRule_Post:
httpPaths = append(httpPaths, t.Post)
case *options.HttpRule_Put:
httpPaths = append(httpPaths, t.Put)
case *options.HttpRule_Delete:
httpPaths = append(httpPaths, t.Delete)
case *options.HttpRule_Patch:
httpPaths = append(httpPaths, t.Patch)
case *options.HttpRule_Custom:
httpPaths = append(httpPaths, t.Custom.Path)
default:
// nothing
}
}
return httpPaths
}
func httpVerb(m *descriptor.MethodDescriptorProto) string {
ext, err := proto.GetExtension(m.Options, options.E_Http)
if err != nil {
return err.Error()
}
opts, ok := ext.(*options.HttpRule)
if !ok {
return fmt.Sprintf("extension is %T; want an HttpRule", ext)
}
switch t := opts.Pattern.(type) {
default:
return ""
case *options.HttpRule_Get:
return "GET"
case *options.HttpRule_Post:
return "POST"
case *options.HttpRule_Put:
return "PUT"
case *options.HttpRule_Delete:
return "DELETE"
case *options.HttpRule_Patch:
return "PATCH"
case *options.HttpRule_Custom:
return t.Custom.Kind
}
}
func httpBody(m *descriptor.MethodDescriptorProto) string {
ext, err := proto.GetExtension(m.Options, options.E_Http)
if err != nil {
return err.Error()
}
opts, ok := ext.(*options.HttpRule)
if !ok {
return fmt.Sprintf("extension is %T; want an HttpRule", ext)
}
return opts.Body
}
func urlHasVarsFromMessage(path string, d *ggdescriptor.Message) bool {
for _, field := range d.Field {
if !isFieldMessage(field) {
if strings.Contains(path, fmt.Sprintf("{%s}", *field.Name)) {
return true
}
}
}
return false
}
// lowerGoNormalize takes a string and applies formatting
// rules to conform to Golang convention. It applies a camel
// case filter, lowers the first character and formats fields
// with `id` to `ID`.
func lowerGoNormalize(s string) string {
fmtd := xstrings.ToCamelCase(s)
fmtd = xstrings.FirstRuneToLower(fmtd)
return formatID(s, fmtd)
}
// goNormalize takes a string and applies formatting rules
// to conform to Golang convention. It applies a camel case
// filter and formats fields with `id` to `ID`.
func goNormalize(s string) string {
fmtd := xstrings.ToCamelCase(s)
return formatID(s, fmtd)
}
// formatID takes a base string alonsgide a formatted string.
// It acts as a transformation filter for fields containing
// `id` in order to conform to Golang convention.
func formatID(base string, formatted string) string {
if formatted == "" {
return formatted
}
switch {
case base == "id":
// id -> ID
return "ID"
case strings.HasPrefix(base, "id_"):
// id_some -> IDSome
return "ID" + formatted[2:]
case strings.HasSuffix(base, "_id"):
// some_id -> SomeID
return formatted[:len(formatted)-2] + "ID"
case strings.HasSuffix(base, "_ids"):
// some_ids -> SomeIDs
return formatted[:len(formatted)-3] + "IDs"
}
return formatted
}
func replaceDict(src string, dict map[string]interface{}) string {
for old, v := range dict {
new, ok := v.(string)
if !ok {
continue
}
src = strings.Replace(src, old, new, -1)
}
return src
}