diff --git a/README.md b/README.md index 703ba7b..f93af92 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ json2go generate --url="https://gorest.co.in/public/v2/users" | Root Object Name | `--root=RootObject` | `string` | Name for top-level object in JSON payload | `Root` | | Package Name | `--package=api` | `string` | Name of package to generate types into. A nested package path is valid | `main` | | Output File Name | `--output` | `string` | The name of the file that is generated. If a file is provided as input, will use matching name unless explicitly provided. The ".go" extension is not required and will be automatically appended. | `types.go` | +| Time Format | `--time=2006-01-02` | `string` | Time format to use while parsing strings for potential time.Time variables. View time.Time constants for possible defaults: https://pkg.go.dev/time#pkg-constants | `RFC3339` | | Debug logging | `--debug` | `bool` | Will output debugging console logs. | `false` | | Quiet | `--quiet` | `bool` | Will quiet fatal errors. | `false` | | STDOUT | `--out` | `bool` | Instead of generating a Go file, will instead print the contents to STDOUT | `false` | diff --git a/cmd/generate.go b/cmd/generate.go index 81f1353..4648c2d 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "log" + "time" "github.com/alehechka/json2go/gen" "github.com/urfave/cli/v2" @@ -17,6 +18,7 @@ const ( debugFlag = "debug" quietFlag = "quiet" stdoutFlag = "out" + timeFormatFlag = "time" ) var generateFlags = []cli.Flag{ @@ -49,6 +51,12 @@ var generateFlags = []cli.Flag{ The ".go" extension is not required and will be automatically appended.`, Value: gen.DefaultOutputFile, }, + &cli.StringFlag{ + Name: timeFormatFlag, + Aliases: []string{"t"}, + Usage: "Time format to use while parsing strings for potential time.Time variables. View time.Time constants for possible defaults: https://pkg.go.dev/time#pkg-constants", + Value: time.RFC3339, + }, &cli.BoolFlag{ Name: debugFlag, Usage: "Log debug messages.", @@ -78,6 +86,7 @@ func generateTypes(ctx *cli.Context) (err error) { RootName: ctx.String(rootFlag), PackageName: ctx.String(packageFlag), OutputFileName: ctx.String(outputFileFlag), + TimeFormat: ctx.String(timeFormatFlag), } if ctx.Bool(stdoutFlag) { diff --git a/gen/config.go b/gen/config.go index 0844e40..d7143f4 100644 --- a/gen/config.go +++ b/gen/config.go @@ -5,6 +5,7 @@ import ( "log" "path/filepath" "strings" + "time" "github.com/alehechka/json2go/jenshared" ) @@ -17,6 +18,7 @@ type Config struct { RootName string PackageName string OutputFileName string + TimeFormat string } func (c *Config) toJensharedConfig() *jenshared.Config { @@ -34,6 +36,7 @@ func (c *Config) toJensharedConfig() *jenshared.Config { PackageName: c.PackageName, OutputFileName: c.OutputFileName, OutputDirectory: dir, + TimeFormat: c.getTimeFormat(), Debugger: c.Debugger, } } @@ -60,3 +63,47 @@ func (c *Config) prepareOutputFileName() { c.OutputFileName += ".go" } } + +func (c *Config) getTimeFormat() string { + + if len(c.TimeFormat) == 0 { + return time.RFC3339 + } + + switch c.TimeFormat { + case "Layout": + return time.Layout + case "ANSIC": + return time.ANSIC + case "UnixDate": + return time.UnixDate + case "RubyDate": + return time.RubyDate + case "RFC822": + return time.RFC822 + case "RFC822Z": + return time.RFC822Z + case "RFC850": + return time.RFC850 + case "RFC1123": + return time.RFC1123 + case "RFC1123Z": + return time.RFC1123Z + case "RFC3339": + return time.RFC3339 + case "RFC3339Nano": + return time.RFC3339Nano + case "Kitchen": + return time.Kitchen + case "Stamp": + return time.Stamp + case "StampMilli": + return time.StampMilli + case "StampMicro": + return time.StampMicro + case "StampNano": + return time.StampNano + default: + return c.TimeFormat + } +} diff --git a/go.mod b/go.mod index 04d311a..956a0e0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/dave/jennifer v1.5.0 + github.com/google/uuid v1.3.0 github.com/stretchr/testify v1.7.4 github.com/urfave/cli/v2 v2.10.2 ) diff --git a/go.sum b/go.sum index 25884f7..2c3c946 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWE github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/jenshared/addStructs.go b/jenshared/addStructs.go index 8592523..53075c0 100644 --- a/jenshared/addStructs.go +++ b/jenshared/addStructs.go @@ -37,7 +37,16 @@ func createStructItems(items TypeItems) []jen.Code { } func createStructItem(item TypeItem) jen.Code { - s := jen.Id(item.Title()).Id(item.Type) + s := jen.Id(item.Title()) + + switch item.Type { + case "time": + s.Qual("time", "Time") + case "uuid": + s.Qual("github.com/google/uuid", "UUID") + default: + s.Id(item.Type) + } if item.Name != "" { s.Tag(map[string]string{"json": item.Name}) diff --git a/jenshared/addStructsFromJSON.go b/jenshared/addStructsFromJSON.go index 37ecffa..142338b 100644 --- a/jenshared/addStructsFromJSON.go +++ b/jenshared/addStructsFromJSON.go @@ -3,8 +3,10 @@ package jenshared import ( "fmt" "strings" + "time" "github.com/dave/jennifer/jen" + "github.com/google/uuid" ) func addStructsFromJSON(f *jen.File, data interface{}, config *Config) { @@ -21,9 +23,9 @@ func createTypeItemsMapFromJSON(data interface{}, config *Config) TypeItemsMap { func parseInterface(items TypeItemsMap, data interface{}, config *Config) TypeItemsMap { switch concreteVal := data.(type) { case bool, float64, string: - items[config.RootName] = TypeItems{{Name: config.RootName, Type: inferDataType(concreteVal)}} + items[config.RootName] = TypeItems{{Name: config.RootName, Type: inferDataType(concreteVal, config)}} case map[string]interface{}: - parseMap(items, concreteVal, config.RootName) + parseMap(items, concreteVal, config.RootName, config) case []interface{}: diveTopLevelArray(items, concreteVal, config, "[]") } @@ -35,11 +37,11 @@ func diveTopLevelArray(items TypeItemsMap, data []interface{}, config *Config, a if len(data) > 0 { switch firstVal := data[0].(type) { case bool, float64, string: - items[config.RootName] = TypeItems{{Name: config.RootName, Type: fmt.Sprintf("%s%s", acc, inferDataType(firstVal))}} + items[config.RootName] = TypeItems{{Name: config.RootName, Type: fmt.Sprintf("%s%s", acc, inferDataType(firstVal, config))}} case map[string]interface{}: arrTitle := fmt.Sprintf("%sArray", config.RootName) items[arrTitle] = TypeItems{{Name: arrTitle, Type: fmt.Sprintf("%s%s", acc, config.RootName)}} - parseMap(items, firstVal, config.RootName) + parseMap(items, firstVal, config.RootName, config) case []interface{}: diveTopLevelArray(items, firstVal, config, fmt.Sprintf("%s[]", acc)) @@ -49,36 +51,36 @@ func diveTopLevelArray(items TypeItemsMap, data []interface{}, config *Config, a return items } -func parseMap(items TypeItemsMap, data map[string]interface{}, parent string) TypeItemsMap { +func parseMap(items TypeItemsMap, data map[string]interface{}, parent string, config *Config) TypeItemsMap { for key, val := range data { title := strings.Title(key) switch concreteVal := val.(type) { case map[string]interface{}: items[title] = make(TypeItems, 0) items[parent] = append(items[parent], TypeItem{Name: key, Type: title}) - parseMap(items, concreteVal, title) + parseMap(items, concreteVal, title, config) case []interface{}: items[parent] = append(items[parent], TypeItem{Name: key, Type: fmt.Sprintf("[]%s", title)}) - parseFirstIndexArray(items, concreteVal, title) + parseFirstIndexArray(items, concreteVal, title, config) default: - items[parent] = append(items[parent], TypeItem{Name: key, Type: inferDataType(concreteVal)}) + items[parent] = append(items[parent], TypeItem{Name: key, Type: inferDataType(concreteVal, config)}) } } return items } -func parseFirstIndexArray(items TypeItemsMap, array []interface{}, parent string) TypeItemsMap { +func parseFirstIndexArray(items TypeItemsMap, array []interface{}, parent string, config *Config) TypeItemsMap { if len(array) > 0 { switch concreteVal := array[0].(type) { case map[string]interface{}: - parseMap(items, concreteVal, parent) + parseMap(items, concreteVal, parent, config) case []interface{}: InterfaceArrayOuter: for key, itemArray := range items { for index, item := range itemArray { if item.Title() == parent { items[key][index].Type = fmt.Sprintf("[]%s", item.Type) - parseFirstIndexArray(items, concreteVal, parent) + parseFirstIndexArray(items, concreteVal, parent, config) break InterfaceArrayOuter } } @@ -88,7 +90,7 @@ func parseFirstIndexArray(items TypeItemsMap, array []interface{}, parent string for key, itemArray := range items { for index, item := range itemArray { if item.Title() == parent && strings.HasSuffix(item.Type, parent) { - items[key][index].Type = fmt.Sprintf("%s%s", strings.TrimSuffix(item.Type, parent), inferDataType(concreteVal)) + items[key][index].Type = fmt.Sprintf("%s%s", strings.TrimSuffix(item.Type, parent), inferDataType(concreteVal, config)) break DefaultOuter } } @@ -98,6 +100,15 @@ func parseFirstIndexArray(items TypeItemsMap, array []interface{}, parent string return items } -func inferDataType(value interface{}) string { - return fmt.Sprintf("%T", value) +func inferDataType(value interface{}, config *Config) string { + valType := fmt.Sprintf("%T", value) + if valType == "string" { + if _, err := time.Parse(config.TimeFormat, value.(string)); err == nil { + return "time" + } + if _, err := uuid.Parse(value.(string)); err == nil { + return "uuid" + } + } + return valType } diff --git a/jenshared/types.go b/jenshared/types.go index de43cef..6f61a5d 100644 --- a/jenshared/types.go +++ b/jenshared/types.go @@ -1,7 +1,9 @@ package jenshared import ( + "fmt" "log" + "regexp" "strings" ) @@ -11,6 +13,7 @@ type Config struct { PackageName string OutputFileName string OutputDirectory string + TimeFormat string Debugger *log.Logger } @@ -22,7 +25,14 @@ type TypeItem struct { // Title converts the JSON name to TitleCase func (t TypeItem) Title() string { - return strings.Title(t.Name) + str := regexp.MustCompile(`[^a-zA-Z0-9]`).ReplaceAllString(t.Name, "_") + + numbers := regexp.MustCompile(`\d`) + if len(str) > 0 && numbers.MatchString(str[0:1]) { + str = fmt.Sprintf("_%s", str[1:]) + } + + return strings.Title(str) } // TypeItems is an array of TypeItem objects diff --git a/testdata/object.json b/testdata/object.json index c415ec8..3d46b6e 100644 --- a/testdata/object.json +++ b/testdata/object.json @@ -18,5 +18,8 @@ "nestedBooleanArray": [true, false], "nestedFloat64Array": [12.34, 43.21], "deeplyNestedObjectArray": [[{ "thats": "deep" }]], - "deeplyNestedStringArray": [[["hello", "world"]]] + "deeplyNestedStringArray": [[["hello", "world"]]], + "timeString": "2006-01-02", + "uuidString": "5051ec14-ce89-4fcf-985e-99628a373497", + "9%badName%": "this should break stuff" }