-
Notifications
You must be signed in to change notification settings - Fork 15
/
easytags.go
206 lines (182 loc) · 4.65 KB
/
easytags.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
package main
import (
"bufio"
"flag"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"strings"
"unicode"
)
const defaultTag = "json"
const defaultCase = "snake"
const cmdUsage = `
Usage : easytags [options] <file_name> [<tag:case>]
Examples:
- 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 tags []*TagOpt
if len(args) < 1 {
fmt.Println(cmdUsage)
return
} else if len(args) == 2 {
provided := strings.Split(args[1], ",")
for _, e := range provided {
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(tags) == 0 && *remove == false {
tags = append(tags, &TagOpt{defaultTag, defaultCase})
}
for _, arg := range args {
files, err := filepath.Glob(arg)
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
for _, f := range files {
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, 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)
if err != nil {
fmt.Printf("Error parsing file %v", err)
return
}
// range over the objects in the scope of this generated AST and check for StructType. Then range over fields
// contained in that struct.
ast.Inspect(f, func(n ast.Node) bool {
switch t := n.(type) {
case *ast.StructType:
processTags(t, tags, remove)
return false
}
return true
})
// overwrite the file with modified version of ast.
write, err := os.Create(fileName)
if err != nil {
fmt.Printf("Error opening file %v", err)
return
}
defer write.Close()
w := bufio.NewWriter(write)
err = format.Node(w, fset, f)
if err != nil {
fmt.Printf("Error formating file %s", err)
return
}
w.Flush()
}
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.Tag))
existingTag := existingTagReg.FindString(field.Tag.Value)
if existingTag == "" {
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, "`"))
if len(tagValues) > 0 {
updatedTags = append(updatedTags, tagValues...)
}
newValue := "`" + strings.Join(updatedTags, " ") + "`"
return newValue
}
func processTags(x *ast.StructType, tags []*TagOpt, remove bool) {
for _, field := range x.Fields.List {
if len(field.Names) == 0 {
continue
}
if !unicode.IsUpper(rune(field.Names[0].String()[0])) {
// not exported
continue
}
if remove {
field.Tag = nil
continue
}
if field.Tag == nil {
field.Tag = &ast.BasicLit{}
field.Tag.ValuePos = field.Type.Pos() + 1
field.Tag.Kind = token.STRING
}
newTags := parseTags(field, tags)
field.Tag.Value = newTags
}
}
// ToSnake convert the given string to snake case following the Golang format:
// acronyms are converted to lower-case and preceded by an underscore.
// Original source : https://gist.github.com/elwinar/14e1e897fdbe4d3432e1
func ToSnake(in string) string {
runes := []rune(in)
length := len(runes)
var out []rune
for i := 0; i < length; i++ {
if i > 0 && unicode.IsUpper(runes[i]) && ((i+1 < length && unicode.IsLower(runes[i+1])) || unicode.IsLower(runes[i-1])) {
out = append(out, '_')
}
out = append(out, unicode.ToLower(runes[i]))
}
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)
}