diff --git a/README.md b/README.md index 55fa318..eb06bd3 100644 --- a/README.md +++ b/README.md @@ -3,24 +3,24 @@ Easy json/xml Tag generation tool for golang [![Build Status](https://travis-ci.org/betacraft/easytags.svg?branch=master)](https://travis-ci.org/rainingclouds/easytags) -We generally write Field names in CamelCase and we generally want them to be in snake case when marshalled to json/xml/sql etc. We use tags for this purpose. But it is a repeatative process which should be automated. +We generally write Field names in CamelCase (aka pascal case) and we generally want them to be in snake case (camel and pascal case are supported as well) when marshalled to json/xml/sql etc. We use tags for this purpose. But it is a repeatative process which should be automated. usage : -> easytags {file_name} {tag_name_1, tag_name_2} ->example: easytags file.go +> easytags {file_name} {tag_name_1:case_1, tag_name_2:case_2} -You can also use this with go generate -For example - In your source file, write following line +> example: easytags file.go ->go:generate easytags $GOFILE json,xml,sql +You can also use this with go generate +For example - In your source file, write following line + +> go:generate easytags $GOFILE json,xml,sql And run ->go generate +> go generate -This will go through all the struct declarations in your source files, and add corresponding json/xml/sql tags with field name changed to snake case. If you have already written tag with "-" value , this tool will not change that tag. +This will go through all the struct declarations in your source files, and add corresponding json/xml/sql tags with field name changed to snake case. If you have already written tag with "-" value, this tool will not change that tag. Now supports Go modules. ![Screencast with Go Generate](https://media.giphy.com/media/26n6G34sQ4hV8HMgo/giphy.gif) - diff --git a/easytags.go b/easytags.go index 51b1a1e..26b3f8f 100644 --- a/easytags.go +++ b/easytags.go @@ -16,23 +16,29 @@ import ( ) const defaultTag = "json" +const defaultCase = "snake" const cmdUsage = ` -Usage : easytags [options] [] +Usage : easytags [options] [] Examples: -- Will add json and xml tags to struct fields - easytags file.go json,xml +- Will add json in camel case and xml in default case (snake) tags to struct fields + easytags file.go json:camel,xml - Will remove all tags when -r flag used when no flags provided easytag -r file.go Options: -r removes all tags if none was provided` +type TagOpt struct { + Tag string + Case string +} + func main() { remove := flag.Bool("r", false, "removes all tags if none was provided") flag.Parse() args := flag.Args() - var tagNames []string + var tags []*TagOpt if len(args) < 1 { fmt.Println(cmdUsage) @@ -40,12 +46,17 @@ func main() { } else if len(args) == 2 { provided := strings.Split(args[1], ",") for _, e := range provided { - tagNames = append(tagNames, strings.TrimSpace(e)) + t := strings.SplitN(strings.TrimSpace(e), ":", 2) + tag := &TagOpt{t[0], defaultCase} + if len(t) == 2 { + tag.Case = t[1] + } + tags = append(tags, tag) } } - if len(tagNames) == 0 && *remove == false { - tagNames = append(tagNames, defaultTag) + if len(tags) == 0 && *remove == false { + tags = append(tags, &TagOpt{defaultTag, defaultCase}) } for _, arg := range args { files, err := filepath.Glob(arg) @@ -55,13 +66,13 @@ func main() { return } for _, f := range files { - GenerateTags(f, tagNames,*remove) + GenerateTags(f, tags, *remove) } } } // GenerateTags generates snake case json tags so that you won't need to write them. Can be also extended to xml or sql tags -func GenerateTags(fileName string, tagNames []string, remove bool) { +func GenerateTags(fileName string, tags []*TagOpt, remove bool) { fset := token.NewFileSet() // positions are relative to fset // Parse the file given in arguments f, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments) @@ -76,7 +87,7 @@ func GenerateTags(fileName string, tagNames []string, remove bool) { ast.Inspect(f, func(n ast.Node) bool { switch t := n.(type) { case *ast.StructType: - processTags(t, tagNames, remove) + processTags(t, tags, remove) return false } return true @@ -98,30 +109,41 @@ func GenerateTags(fileName string, tagNames []string, remove bool) { w.Flush() } -func parseTags(field *ast.Field, tags []string) string { +func parseTags(field *ast.Field, tags []*TagOpt) string { var tagValues []string fieldName := field.Names[0].String() for _, tag := range tags { var value string - existingTagReg := regexp.MustCompile(fmt.Sprintf("%s:\"[^\"]+\"", tag)) + existingTagReg := regexp.MustCompile(fmt.Sprintf("%s:\"[^\"]+\"", tag.Tag)) existingTag := existingTagReg.FindString(field.Tag.Value) if existingTag == "" { - value = fmt.Sprintf("%s:\"%s\"", tag, ToSnake(fieldName)) + var name string + switch tag.Case { + case "snake": + name = ToSnake(fieldName) + case "camel": + name = ToCamel(fieldName) + case "pascal": + name = fieldName + default: + fmt.Printf("Unknown case option %s", tag.Case) + } + value = fmt.Sprintf("%s:\"%s\"", tag.Tag, name) tagValues = append(tagValues, value) } } - updatedTags := strings.Fields(strings.Trim(field.Tag.Value,"`")) + updatedTags := strings.Fields(strings.Trim(field.Tag.Value, "`")) if len(tagValues) > 0 { - updatedTags = append(updatedTags,tagValues...) + updatedTags = append(updatedTags, tagValues...) } - newValue := "`" + strings.Join(updatedTags," ") + "`" + newValue := "`" + strings.Join(updatedTags, " ") + "`" return newValue } -func processTags(x *ast.StructType, tagNames []string, remove bool) { +func processTags(x *ast.StructType, tags []*TagOpt, remove bool) { for _, field := range x.Fields.List { if len(field.Names) == 0 { continue @@ -142,7 +164,7 @@ func processTags(x *ast.StructType, tagNames []string, remove bool) { field.Tag.Kind = token.STRING } - newTags := parseTags(field, tagNames) + newTags := parseTags(field, tags) field.Tag.Value = newTags } } @@ -163,3 +185,22 @@ func ToSnake(in string) string { } return string(out) } + +// ToLowerCamel convert the given string to camelCase +func ToCamel(in string) string { + runes := []rune(in) + length := len(runes) + + var i int + for i = 0; i < length; i++ { + if unicode.IsLower(runes[i]) { + break + } + runes[i] = unicode.ToLower(runes[i]) + } + if i != 1 && i != length { + i-- + runes[i] = unicode.ToUpper(runes[i]) + } + return string(runes) +} diff --git a/easytags_test.go b/easytags_test.go index fe09159..5ba21a0 100644 --- a/easytags_test.go +++ b/easytags_test.go @@ -14,13 +14,13 @@ func TestGenerateTags(t *testing.T) { t.Errorf("Error reading file %v", err) } defer ioutil.WriteFile("testfile.go", testCode, 0644) - GenerateTags("testfile.go", []string{"json"}, false) + GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "snake"}}, false) fset := token.NewFileSet() f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) if err != nil { t.Errorf("Error parsing generated file %v", err) genFile, _ := ioutil.ReadFile("testfile.go") - t.Errorf("\n%s",genFile) + t.Errorf("\n%s", genFile) return } @@ -76,7 +76,7 @@ func TestGenerateTags_Multiple(t *testing.T) { t.Errorf("Error reading file %v", err) } defer ioutil.WriteFile("testfile.go", testCode, 0644) - GenerateTags("testfile.go", []string{"json", "xml"}, false) + GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "snake"}, &TagOpt{"xml", "snake"}}, false) fset := token.NewFileSet() f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) if err != nil { @@ -130,13 +130,62 @@ func TestGenerateTags_Multiple(t *testing.T) { } } +func TestGenerateTags_PascalCase(t *testing.T) { + testCode, err := ioutil.ReadFile("testfile.go") + if err != nil { + t.Errorf("Error reading file %v", err) + } + defer ioutil.WriteFile("testfile.go", testCode, 0644) + GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "camel"}}, false) + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) + if err != nil { + t.Errorf("Error parsing generated file %v", err) + genFile, _ := ioutil.ReadFile("testfile.go") + t.Errorf("\n%s", genFile) + return + } + + for _, d := range f.Scope.Objects { + if d.Kind != ast.Typ { + continue + } + ts, ok := d.Decl.(*ast.TypeSpec) + if !ok { + t.Errorf("Unknown type without TypeSec: %v", d) + return + } + + x, ok := ts.Type.(*ast.StructType) + if !ok { + continue + } + for _, field := range x.Fields.List { + if len(field.Names) == 0 { + if field.Tag != nil { + t.Errorf("Embedded struct shouldn't be added a tag - %s", field.Tag.Value) + } + continue + } + name := field.Names[0].String() + if name == "TestField2" { + if field.Tag == nil { + t.Error("Tag should be generated for TestFiled2") + } else if field.Tag.Value != "`json:\"testField2\"`" { + t.Error("Camel tag should be generated for TestField2") + } + } + } + } +} + func TestGenerateTags_RemoveAll(t *testing.T) { testCode, err := ioutil.ReadFile("testfile.go") if err != nil { t.Errorf("Error reading file %v", err) } defer ioutil.WriteFile("testfile.go", testCode, 0644) - GenerateTags("testfile.go", []string{}, true) + GenerateTags("testfile.go", []*TagOpt{}, true) fset := token.NewFileSet() f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) if err != nil { @@ -178,3 +227,29 @@ func TestGenerateTags_RemoveAll(t *testing.T) { } } } + +func TestToSnake(t *testing.T) { + test := func(in, out string) { + r := ToSnake(in) + if r != out { + t.Errorf("%s in snake_case should be %s, instead found %s", in, out, r) + } + } + test("A", "a") + test("ID", "id") + test("UserID", "user_id") + test("CSRFToken", "csrf_token") +} + +func TestToCamel(t *testing.T) { + test := func(in, out string) { + r := ToCamel(in) + if r != out { + t.Errorf("%s in lowerCamelCase should be %s, instead found %s", in, out, r) + } + } + test("A", "a") + test("ID", "id") + test("UserID", "userID") + test("CSRFToken", "csrfToken") +}