Skip to content

Commit

Permalink
Merge pull request #7 from alehechka/feature/parse-uuid-time
Browse files Browse the repository at this point in the history
Parse time and uuid variables
  • Loading branch information
alehechka authored Jun 28, 2022
2 parents 268b409 + b379a2e commit 52f4aa7
Show file tree
Hide file tree
Showing 9 changed files with 110 additions and 17 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
9 changes: 9 additions & 0 deletions cmd/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cmd
import (
"fmt"
"log"
"time"

"github.com/alehechka/json2go/gen"
"github.com/urfave/cli/v2"
Expand All @@ -17,6 +18,7 @@ const (
debugFlag = "debug"
quietFlag = "quiet"
stdoutFlag = "out"
timeFormatFlag = "time"
)

var generateFlags = []cli.Flag{
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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) {
Expand Down
47 changes: 47 additions & 0 deletions gen/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"log"
"path/filepath"
"strings"
"time"

"github.com/alehechka/json2go/jenshared"
)
Expand All @@ -17,6 +18,7 @@ type Config struct {
RootName string
PackageName string
OutputFileName string
TimeFormat string
}

func (c *Config) toJensharedConfig() *jenshared.Config {
Expand All @@ -34,6 +36,7 @@ func (c *Config) toJensharedConfig() *jenshared.Config {
PackageName: c.PackageName,
OutputFileName: c.OutputFileName,
OutputDirectory: dir,
TimeFormat: c.getTimeFormat(),
Debugger: c.Debugger,
}
}
Expand All @@ -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
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 10 additions & 1 deletion jenshared/addStructs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
39 changes: 25 additions & 14 deletions jenshared/addStructsFromJSON.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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, "[]")
}
Expand All @@ -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))
Expand All @@ -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
}
}
Expand All @@ -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
}
}
Expand All @@ -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
}
12 changes: 11 additions & 1 deletion jenshared/types.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package jenshared

import (
"fmt"
"log"
"regexp"
"strings"
)

Expand All @@ -11,6 +13,7 @@ type Config struct {
PackageName string
OutputFileName string
OutputDirectory string
TimeFormat string
Debugger *log.Logger
}

Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion testdata/object.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 52f4aa7

Please sign in to comment.