Skip to content

Commit

Permalink
handling []struct
Browse files Browse the repository at this point in the history
  • Loading branch information
cneill committed Aug 18, 2023
1 parent 3debb98 commit 548c3e8
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 39 deletions.
143 changes: 121 additions & 22 deletions fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,48 @@ import (

// Field represents a single struct field.
type Field struct {
GoName string
OriginalName string
RawValue any
goName string
originalName string
rawValue any
optional bool
}

func (f *Field) SetName(originalName string) *Field {
f.goName = GetGoName(originalName)
f.originalName = originalName

return f
}

func (f *Field) SetValue(value any) *Field {
f.rawValue = value

return f
}

func (f *Field) SetOptional() *Field {
f.optional = true

return f
}

// Name returns the name of this field as it will be rendered in the final struct.
func (f Field) Name() string {
return f.GoName
return f.goName
}

// Tag returns the JSON tag as it will be rendered in the final struct.
func (f Field) Tag() string {
return fmt.Sprintf("`json: \"%s\"`", f.OriginalName)
if f.optional {
return fmt.Sprintf("`json: \"%s,omitempty\"", f.Name())
}

return fmt.Sprintf("`json: \"%s\"`", f.Name())
}

// Type returns the type of the field as it will be rendered in the final struct.
func (f Field) Type() string {
switch f.RawValue.(type) {
switch f.rawValue.(type) {
case int64:
return "int64"
case float64:
Expand All @@ -36,7 +60,7 @@ func (f Field) Type() string {
return "bool"
}

if f.RawValue == nil {
if f.rawValue == nil {
return "*json.RawMessage"
}

Expand All @@ -45,25 +69,35 @@ func (f Field) Type() string {
}

if f.IsStruct() {
return fmt.Sprintf("*%s", f.GoName)
return fmt.Sprintf("*%s", f.goName)
}

return "DUNNO BOSS"
return "any"
}

func (f Field) SliceType() string {
rawVal := reflect.ValueOf(f.RawValue)
rawType := reflect.TypeOf(f.RawValue)
rawVal := reflect.ValueOf(f.rawValue)
rawType := reflect.TypeOf(f.rawValue)

// we got a non-slice here
if rawType.Kind() != reflect.Slice {
return ""
}

if f.IsStructSlice() {
return fmt.Sprintf("[]*%s", f.Name())
}

var sliceType string

for i := 0; i < rawVal.Len(); i++ {
idxVal := rawVal.Index(i).Elem()
idxVal := rawVal.Index(i)
kind := idxVal.Type().Kind()

if kind == reflect.Pointer || kind == reflect.Interface {
idxVal = idxVal.Elem()
}

idxType := idxVal.Type()

if sliceType != "" && idxType.String() != sliceType {
Expand All @@ -80,7 +114,7 @@ func (f Field) SliceType() string {

// Value returns the string version of RawValue.
func (f Field) Value() string {
switch val := f.RawValue.(type) {
switch val := f.rawValue.(type) {
case bool:
return fmt.Sprintf("%t", val)
case float64:
Expand All @@ -102,44 +136,109 @@ func (f Field) Value() string {

// Comment returns the string used for example value comments.
func (f Field) Comment() string {
return fmt.Sprintf("// Example: %s", f.Value())
val := f.Value()
if val != "" {
return fmt.Sprintf("// Example: %s", f.Value())
}

return ""
}

// IsStruct returns true if RawValue is of kind struct.
func (f Field) IsStruct() bool {
kind := reflect.TypeOf(f.RawValue).Kind()
kind := reflect.TypeOf(f.rawValue).Kind()

return kind == reflect.Struct
}

// GetStruct gets a the JSONStruct in RawValue if f is a struct, otherwise returns an empty JSONStruct.
func (f Field) GetStruct() JSONStruct {
if !f.IsStruct() {
switch {
case f.IsStruct():
js, ok := f.rawValue.(JSONStruct)
if !ok {
return JSONStruct{}
}

return js.SetName(f.Name())
case f.IsStructSlice():
return f.GetSliceStruct()
default:
return JSONStruct{}
}
}

js, ok := f.RawValue.(JSONStruct)
func (f Field) GetSliceStruct() JSONStruct {
result := JSONStruct{}.SetName(f.Name())

anySlice, ok := f.rawValue.([]any)
if !ok {
return JSONStruct{}
}

js.Name = f.Name()
jss, err := anySliceToJSONStructs(anySlice)
if err != nil {
return JSONStruct{}
}

foundFields := map[string][]*Field{}

// we have a slice of structs, each of which may or may not contain the full set of fields
for _, js := range jss {
for _, field := range js.Fields {
foundFields[field.Name()] = append(foundFields[field.Name()], field)
}
}

for _, fields := range foundFields {
if len(fields) != len(jss) {
fields[0].SetOptional()
}

result.AddFields(fields[0])
}

return js
return result
}

// IsSlice returns true if RawValue is of kind slice.
func (f Field) IsSlice() bool {
kind := reflect.TypeOf(f.RawValue).Kind()
kind := reflect.TypeOf(f.rawValue).Kind()

return kind == reflect.Slice
}

func (f Field) IsStructSlice() bool {
anySlice, ok := f.rawValue.([]any)
if !ok {
return false
}

if _, err := anySliceToJSONStructs(anySlice); err != nil {
return false
}

return true
}

func (f Field) Equals(input Field) bool {
switch {
case f.Name() != input.Name():
return false
case f.Type() != input.Type():
return false
case f.Tag() != input.Tag():
return false
}

return true
}

// Fields is a convenience type for a slice of Field structs.
type Fields []Field
type Fields []*Field

func (f Fields) SortAlphabetically() {
sort.Slice(f, func(i, j int) bool {
return f[i].GoName < f[j].GoName
return f[i].goName < f[j].goName
})
}
43 changes: 43 additions & 0 deletions fields_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package jsonstruct_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/cneill/jsonstruct"
)

func TestFieldType(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input any
output string
}{
{"bool", true, "bool"},
{"int", int64(123), "int64"},
{"float", float64(1.23), "float64"},
{"string", "test", "string"},
{"bool_slice", []bool{true, false, true}, "[]bool"},
{"int_slice", []int{1, 2, 3}, "[]int"},
{"float_slice", []float64{1.1, 1.2, 1.3}, "[]float64"},
{"string_slice", []string{"1", "2", "3"}, "[]string"},
{"garbage_slice", []any{1, "1", 1.0}, "[]*json.RawMessage"},
{"any_bool_slice", []any{true, false, true}, "[]bool"},
{"any_int_slice", []any{int64(1), int64(2), int64(3)}, "[]int64"},
{"any_float_slice", []any{1.0, 2.0, 3.0}, "[]float64"},
{"any_string_slice", []any{"1", "2", "3"}, "[]string"},
}

for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
t.Parallel()
f := (&jsonstruct.Field{}).SetName(test.name).SetValue(test.input)

assert.Equal(t, test.output, f.Type())
})
}
}
24 changes: 13 additions & 11 deletions formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@ func (f *Formatter) FormatString(input ...JSONStruct) (string, error) {
js.Fields.SortAlphabetically()
}

result += fmt.Sprintf("type %s struct {\n\t%s\n}", js.Name, strings.Join(f.fieldStrings(js.Fields...), "\n\t"))
fieldStrings := f.fieldStrings(js.Fields...)
result += fmt.Sprintf("type %s struct {\n\t%s\n}", js.Name, strings.Join(fieldStrings, "\n\t"))

// if we're not inlining structs, find all the fields of type struct and print their type definitions out too
for _, field := range js.Fields {
if !f.InlineStructs && field.IsStruct() {
if !f.InlineStructs && field.IsStruct() || field.IsStructSlice() {
formatted, err := f.FormatString(field.GetStruct())
if err != nil {
return "", fmt.Errorf("failed to format child struct %q: %w", field.Name(), err)
Expand All @@ -66,7 +68,7 @@ func (f *Formatter) FormatString(input ...JSONStruct) (string, error) {
return result, nil
}

func (f *Formatter) fieldStrings(fields ...Field) []string {
func (f *Formatter) fieldStrings(fields ...*Field) []string {
var (
results = []string{}
buckets = f.fieldBuckets(fields...)
Expand All @@ -77,15 +79,15 @@ func (f *Formatter) fieldStrings(fields ...Field) []string {
var longestName, longestType, longestTag int

for _, field := range bucket {
if name := field.Name(); len(name) > longestName {
if name := field.Name(); len(name) > longestName-1 {
longestName = len(name) + 1
}

if typ := field.Type(); len(typ) > longestType {
if typ := field.Type(); len(typ) > longestType-1 {
longestType = len(typ) + 1
}

if tag := field.Tag(); len(tag) > longestTag {
if tag := field.Tag(); len(tag) > longestTag-1 {
longestTag = len(tag) + 1
}
}
Expand All @@ -106,20 +108,20 @@ func (f *Formatter) fieldStrings(fields ...Field) []string {
return results
}

func (f *Formatter) fieldBuckets(fields ...Field) [][]Field {
func (f *Formatter) fieldBuckets(fields ...*Field) [][]*Field {
// TODO: handle the case of comments on previous lines when that's a possibility
if !f.InlineStructs {
return [][]Field{fields}
return [][]*Field{fields}
}

buckets := [][]Field{}
bucket := []Field{}
buckets := [][]*Field{}
bucket := []*Field{}

for _, field := range fields {
if field.IsStruct() {
bucket = append(bucket, field)
buckets = append(buckets, bucket)
bucket = []Field{}
bucket = []*Field{}
} else {
bucket = append(bucket, field)
}
Expand Down
2 changes: 1 addition & 1 deletion jsonstruct.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (j JSONStruct) SetName(name string) JSONStruct {
}

// AddFields appends Field objects to the JSONStruct.
func (j *JSONStruct) AddFields(fields ...Field) JSONStruct {
func (j *JSONStruct) AddFields(fields ...*Field) JSONStruct {
j.Fields = append(j.Fields, fields...)

return *j
Expand Down
6 changes: 1 addition & 5 deletions parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,7 @@ func (p *Parser) parseObject() (JSONStruct, error) {
return result, fmt.Errorf("failed to parse value: %w", err)
}

field := Field{
GoName: GetGoName(key),
OriginalName: key,
RawValue: val,
}
field := (&Field{}).SetName(key).SetValue(val)

result.AddFields(field)
}
Expand Down
16 changes: 16 additions & 0 deletions utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package jsonstruct

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -36,3 +37,18 @@ func GetGoName(input string) string {

return result
}

func anySliceToJSONStructs(input []any) (JSONStructs, error) {
result := JSONStructs{}

for i, item := range input {
js, ok := item.(JSONStruct)
if !ok {
return nil, fmt.Errorf("item %d was not a JSONStruct", i)
}

result = append(result, js)
}

return result, nil
}
Loading

0 comments on commit 548c3e8

Please sign in to comment.