Skip to content

Commit

Permalink
different cases support
Browse files Browse the repository at this point in the history
  • Loading branch information
Ximik authored and aniketawati committed May 28, 2019
1 parent ef6c0dd commit baa965d
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 31 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

77 changes: 59 additions & 18 deletions easytags.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,47 @@ import (
)

const defaultTag = "json"
const defaultCase = "snake"
const cmdUsage = `
Usage : easytags [options] <file_name> [<tag names>]
Usage : easytags [options] <file_name> [<tag:case>]
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)
return
} 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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
}
}
Expand All @@ -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)
}
83 changes: 79 additions & 4 deletions easytags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}

0 comments on commit baa965d

Please sign in to comment.