├── go.mod ├── .travis.yml ├── testfile.go ├── .gitignore ├── LICENSE ├── README.md ├── easytags.go └── easytags_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/betacraft/easytags 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | 6 | script: go test -------------------------------------------------------------------------------- /testfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type TestStruct struct { 4 | Field1 int `json:"-"` 5 | TestField2 string 6 | ExistingTag string `custom:"" json:"etag"` 7 | Embed 8 | } 9 | 10 | type Embed struct { 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.DS_Store 26 | *.sublime-project 27 | easytags 28 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 RainingClouds 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easytags 2 | Easy json/xml Tag generation tool for golang 3 | 4 | [![Build Status](https://travis-ci.org/betacraft/easytags.svg?branch=master)](https://travis-ci.org/rainingclouds/easytags) 5 | 6 | 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. 7 | 8 | usage : 9 | 10 | > easytags {file_name} {tag_name_1:case_1, tag_name_2:case_2} 11 | 12 | > example: easytags file.go 13 | 14 | You can also use this with go generate 15 | For example - In your source file, write following line 16 | 17 | > go:generate easytags $GOFILE json,xml,sql 18 | 19 | And run 20 | > go generate 21 | 22 | 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. 23 | 24 | Now supports Go modules. 25 | 26 | ![Screencast with Go Generate](https://media.giphy.com/media/26n6G34sQ4hV8HMgo/giphy.gif) 27 | -------------------------------------------------------------------------------- /easytags.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "flag" 6 | "fmt" 7 | "go/ast" 8 | "go/format" 9 | "go/parser" 10 | "go/token" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | "strings" 15 | "unicode" 16 | ) 17 | 18 | const defaultTag = "json" 19 | const defaultCase = "snake" 20 | const cmdUsage = ` 21 | Usage : easytags [options] [] 22 | Examples: 23 | - Will add json in camel case and xml in default case (snake) tags to struct fields 24 | easytags file.go json:camel,xml 25 | - Will remove all tags when -r flag used when no flags provided 26 | easytag -r file.go 27 | Options: 28 | 29 | -r removes all tags if none was provided` 30 | 31 | type TagOpt struct { 32 | Tag string 33 | Case string 34 | } 35 | 36 | func main() { 37 | remove := flag.Bool("r", false, "removes all tags if none was provided") 38 | flag.Parse() 39 | 40 | args := flag.Args() 41 | var tags []*TagOpt 42 | 43 | if len(args) < 1 { 44 | fmt.Println(cmdUsage) 45 | return 46 | } else if len(args) == 2 { 47 | provided := strings.Split(args[1], ",") 48 | for _, e := range provided { 49 | t := strings.SplitN(strings.TrimSpace(e), ":", 2) 50 | tag := &TagOpt{t[0], defaultCase} 51 | if len(t) == 2 { 52 | tag.Case = t[1] 53 | } 54 | tags = append(tags, tag) 55 | } 56 | } 57 | 58 | if len(tags) == 0 && *remove == false { 59 | tags = append(tags, &TagOpt{defaultTag, defaultCase}) 60 | } 61 | for _, arg := range args { 62 | files, err := filepath.Glob(arg) 63 | if err != nil { 64 | fmt.Println(err) 65 | os.Exit(1) 66 | return 67 | } 68 | for _, f := range files { 69 | GenerateTags(f, tags, *remove) 70 | } 71 | } 72 | } 73 | 74 | // GenerateTags generates snake case json tags so that you won't need to write them. Can be also extended to xml or sql tags 75 | func GenerateTags(fileName string, tags []*TagOpt, remove bool) { 76 | fset := token.NewFileSet() // positions are relative to fset 77 | // Parse the file given in arguments 78 | f, err := parser.ParseFile(fset, fileName, nil, parser.ParseComments) 79 | if err != nil { 80 | fmt.Printf("Error parsing file %v", err) 81 | return 82 | } 83 | 84 | // range over the objects in the scope of this generated AST and check for StructType. Then range over fields 85 | // contained in that struct. 86 | 87 | ast.Inspect(f, func(n ast.Node) bool { 88 | switch t := n.(type) { 89 | case *ast.StructType: 90 | processTags(t, tags, remove) 91 | return false 92 | } 93 | return true 94 | }) 95 | 96 | // overwrite the file with modified version of ast. 97 | write, err := os.Create(fileName) 98 | if err != nil { 99 | fmt.Printf("Error opening file %v", err) 100 | return 101 | } 102 | defer write.Close() 103 | w := bufio.NewWriter(write) 104 | err = format.Node(w, fset, f) 105 | if err != nil { 106 | fmt.Printf("Error formating file %s", err) 107 | return 108 | } 109 | w.Flush() 110 | } 111 | 112 | func parseTags(field *ast.Field, tags []*TagOpt) string { 113 | var tagValues []string 114 | fieldName := field.Names[0].String() 115 | for _, tag := range tags { 116 | var value string 117 | existingTagReg := regexp.MustCompile(fmt.Sprintf("%s:\"[^\"]+\"", tag.Tag)) 118 | existingTag := existingTagReg.FindString(field.Tag.Value) 119 | if existingTag == "" { 120 | var name string 121 | switch tag.Case { 122 | case "snake": 123 | name = ToSnake(fieldName) 124 | case "camel": 125 | name = ToCamel(fieldName) 126 | case "pascal": 127 | name = fieldName 128 | default: 129 | fmt.Printf("Unknown case option %s", tag.Case) 130 | } 131 | value = fmt.Sprintf("%s:\"%s\"", tag.Tag, name) 132 | tagValues = append(tagValues, value) 133 | } 134 | 135 | } 136 | updatedTags := strings.Fields(strings.Trim(field.Tag.Value, "`")) 137 | 138 | if len(tagValues) > 0 { 139 | updatedTags = append(updatedTags, tagValues...) 140 | } 141 | newValue := "`" + strings.Join(updatedTags, " ") + "`" 142 | 143 | return newValue 144 | } 145 | 146 | func processTags(x *ast.StructType, tags []*TagOpt, remove bool) { 147 | for _, field := range x.Fields.List { 148 | if len(field.Names) == 0 { 149 | continue 150 | } 151 | if !unicode.IsUpper(rune(field.Names[0].String()[0])) { 152 | // not exported 153 | continue 154 | } 155 | 156 | if remove { 157 | field.Tag = nil 158 | continue 159 | } 160 | 161 | if field.Tag == nil { 162 | field.Tag = &ast.BasicLit{} 163 | field.Tag.ValuePos = field.Type.Pos() + 1 164 | field.Tag.Kind = token.STRING 165 | } 166 | 167 | newTags := parseTags(field, tags) 168 | field.Tag.Value = newTags 169 | } 170 | } 171 | 172 | // ToSnake convert the given string to snake case following the Golang format: 173 | // acronyms are converted to lower-case and preceded by an underscore. 174 | // Original source : https://gist.github.com/elwinar/14e1e897fdbe4d3432e1 175 | func ToSnake(in string) string { 176 | runes := []rune(in) 177 | length := len(runes) 178 | 179 | var out []rune 180 | for i := 0; i < length; i++ { 181 | if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) { 182 | out = append(out, '_') 183 | } 184 | out = append(out, unicode.ToLower(runes[i])) 185 | } 186 | return string(out) 187 | } 188 | 189 | // ToLowerCamel convert the given string to camelCase 190 | func ToCamel(in string) string { 191 | runes := []rune(in) 192 | length := len(runes) 193 | 194 | var i int 195 | for i = 0; i < length; i++ { 196 | if unicode.IsLower(runes[i]) { 197 | break 198 | } 199 | runes[i] = unicode.ToLower(runes[i]) 200 | } 201 | if i != 1 && i != length { 202 | i-- 203 | runes[i] = unicode.ToUpper(runes[i]) 204 | } 205 | return string(runes) 206 | } 207 | -------------------------------------------------------------------------------- /easytags_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go/ast" 5 | "go/parser" 6 | "go/token" 7 | "io/ioutil" 8 | "testing" 9 | ) 10 | 11 | func TestGenerateTags(t *testing.T) { 12 | testCode, err := ioutil.ReadFile("testfile.go") 13 | if err != nil { 14 | t.Errorf("Error reading file %v", err) 15 | } 16 | defer ioutil.WriteFile("testfile.go", testCode, 0644) 17 | GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "snake"}}, false) 18 | fset := token.NewFileSet() 19 | f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) 20 | if err != nil { 21 | t.Errorf("Error parsing generated file %v", err) 22 | genFile, _ := ioutil.ReadFile("testfile.go") 23 | t.Errorf("\n%s", genFile) 24 | return 25 | } 26 | 27 | for _, d := range f.Scope.Objects { 28 | if d.Kind != ast.Typ { 29 | continue 30 | } 31 | ts, ok := d.Decl.(*ast.TypeSpec) 32 | if !ok { 33 | t.Errorf("Unknown type without TypeSec: %v", d) 34 | return 35 | } 36 | 37 | x, ok := ts.Type.(*ast.StructType) 38 | if !ok { 39 | continue 40 | } 41 | for _, field := range x.Fields.List { 42 | if len(field.Names) == 0 { 43 | if field.Tag != nil { 44 | t.Errorf("Embedded struct shouldn't be added a tag - %s", field.Tag.Value) 45 | } 46 | continue 47 | } 48 | name := field.Names[0].String() 49 | if name == "Field1" { 50 | if field.Tag == nil { 51 | t.Error("Tag should exist for Field1") 52 | } else if field.Tag.Value != "`json:\"-\"`" { 53 | t.Error("Shouldn't overwrite existing tags") 54 | } 55 | } else if name == "TestField2" { 56 | if field.Tag == nil { 57 | t.Error("Tag should be generated for TestFiled2") 58 | } else if field.Tag.Value != "`json:\"test_field2\"`" { 59 | t.Error("Snake case tag should be generated for TestField2") 60 | } 61 | } else if name == "ExistingTag" { 62 | if field.Tag == nil { 63 | t.Error("Tag should be generated for TestFiled2") 64 | } else if field.Tag.Value != "`custom:\"\" json:\"etag\"`" { 65 | t.Error("existing tag should not be modified, instead found ", field.Tag.Value) 66 | } 67 | } 68 | 69 | } 70 | } 71 | } 72 | 73 | func TestGenerateTags_Multiple(t *testing.T) { 74 | testCode, err := ioutil.ReadFile("testfile.go") 75 | if err != nil { 76 | t.Errorf("Error reading file %v", err) 77 | } 78 | defer ioutil.WriteFile("testfile.go", testCode, 0644) 79 | GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "snake"}, &TagOpt{"xml", "snake"}}, false) 80 | fset := token.NewFileSet() 81 | f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) 82 | if err != nil { 83 | t.Errorf("Error parsing generated file %v", err) 84 | return 85 | } 86 | 87 | for _, d := range f.Scope.Objects { 88 | if d.Kind != ast.Typ { 89 | continue 90 | } 91 | ts, ok := d.Decl.(*ast.TypeSpec) 92 | if !ok { 93 | t.Errorf("Unknown type without TypeSec: %v", d) 94 | return 95 | } 96 | 97 | x, ok := ts.Type.(*ast.StructType) 98 | if !ok { 99 | continue 100 | } 101 | for _, field := range x.Fields.List { 102 | if len(field.Names) == 0 { 103 | if field.Tag != nil { 104 | t.Errorf("Embedded struct shouldn't be added a tag - %s", field.Tag.Value) 105 | } 106 | continue 107 | } 108 | name := field.Names[0].String() 109 | if name == "Field1" { 110 | if field.Tag == nil { 111 | t.Error("Tag should exist for Field1") 112 | } else if field.Tag.Value != "`json:\"-\" xml:\"field1\"`" { 113 | t.Error("Shouldn't overwrite existing json tag, and should add xml tag") 114 | } 115 | } else if name == "TestField2" { 116 | if field.Tag == nil { 117 | t.Error("Tag should be generated for TestFiled2") 118 | } else if field.Tag.Value != "`json:\"test_field2\" xml:\"test_field2\"`" { 119 | t.Error("Snake case tag should be generated for TestField2") 120 | } 121 | } else if name == "ExistingTag" { 122 | if field.Tag == nil { 123 | t.Error("Tag should be generated for TestFiled2") 124 | } else if field.Tag.Value != "`custom:\"\" json:\"etag\" xml:\"existing_tag\"`" { 125 | t.Error("new tag should be appended to existing tag, instead found ", field.Tag.Value) 126 | } 127 | } 128 | 129 | } 130 | } 131 | } 132 | 133 | func TestGenerateTags_PascalCase(t *testing.T) { 134 | testCode, err := ioutil.ReadFile("testfile.go") 135 | if err != nil { 136 | t.Errorf("Error reading file %v", err) 137 | } 138 | defer ioutil.WriteFile("testfile.go", testCode, 0644) 139 | GenerateTags("testfile.go", []*TagOpt{&TagOpt{"json", "camel"}}, false) 140 | fset := token.NewFileSet() 141 | f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) 142 | if err != nil { 143 | t.Errorf("Error parsing generated file %v", err) 144 | genFile, _ := ioutil.ReadFile("testfile.go") 145 | t.Errorf("\n%s", genFile) 146 | return 147 | } 148 | 149 | for _, d := range f.Scope.Objects { 150 | if d.Kind != ast.Typ { 151 | continue 152 | } 153 | ts, ok := d.Decl.(*ast.TypeSpec) 154 | if !ok { 155 | t.Errorf("Unknown type without TypeSec: %v", d) 156 | return 157 | } 158 | 159 | x, ok := ts.Type.(*ast.StructType) 160 | if !ok { 161 | continue 162 | } 163 | for _, field := range x.Fields.List { 164 | if len(field.Names) == 0 { 165 | if field.Tag != nil { 166 | t.Errorf("Embedded struct shouldn't be added a tag - %s", field.Tag.Value) 167 | } 168 | continue 169 | } 170 | name := field.Names[0].String() 171 | if name == "TestField2" { 172 | if field.Tag == nil { 173 | t.Error("Tag should be generated for TestFiled2") 174 | } else if field.Tag.Value != "`json:\"testField2\"`" { 175 | t.Error("Camel tag should be generated for TestField2") 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | func TestGenerateTags_RemoveAll(t *testing.T) { 183 | testCode, err := ioutil.ReadFile("testfile.go") 184 | if err != nil { 185 | t.Errorf("Error reading file %v", err) 186 | } 187 | defer ioutil.WriteFile("testfile.go", testCode, 0644) 188 | GenerateTags("testfile.go", []*TagOpt{}, true) 189 | fset := token.NewFileSet() 190 | f, err := parser.ParseFile(fset, "testfile.go", nil, parser.ParseComments) 191 | if err != nil { 192 | t.Errorf("Error parsing generated file %v", err) 193 | return 194 | } 195 | 196 | for _, d := range f.Scope.Objects { 197 | if d.Kind != ast.Typ { 198 | continue 199 | } 200 | ts, ok := d.Decl.(*ast.TypeSpec) 201 | if !ok { 202 | t.Errorf("Unknown type without TypeSec: %v", d) 203 | return 204 | } 205 | 206 | x, ok := ts.Type.(*ast.StructType) 207 | if !ok { 208 | continue 209 | } 210 | for _, field := range x.Fields.List { 211 | if len(field.Names) == 0 { 212 | if field.Tag != nil { 213 | t.Errorf("Embedded struct shouldn't be added a tag - %s", field.Tag.Value) 214 | } 215 | continue 216 | } 217 | name := field.Names[0].String() 218 | if name == "Field1" { 219 | if field.Tag != nil { 220 | t.Error("Field1 should not have any tag") 221 | } 222 | } else if name == "TestField2" { 223 | if field.Tag != nil { 224 | t.Error("TestField2 should not have any tag") 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | func TestToSnake(t *testing.T) { 232 | test := func(in, out string) { 233 | r := ToSnake(in) 234 | if r != out { 235 | t.Errorf("%s in snake_case should be %s, instead found %s", in, out, r) 236 | } 237 | } 238 | test("A", "a") 239 | test("ID", "id") 240 | test("UserID", "user_id") 241 | test("CSRFToken", "csrf_token") 242 | } 243 | 244 | func TestToCamel(t *testing.T) { 245 | test := func(in, out string) { 246 | r := ToCamel(in) 247 | if r != out { 248 | t.Errorf("%s in lowerCamelCase should be %s, instead found %s", in, out, r) 249 | } 250 | } 251 | test("A", "a") 252 | test("ID", "id") 253 | test("UserID", "userID") 254 | test("CSRFToken", "csrfToken") 255 | } 256 | --------------------------------------------------------------------------------