diff --git a/Makefile b/Makefile index 70f368f..501c800 100644 --- a/Makefile +++ b/Makefile @@ -26,11 +26,11 @@ test: ffize test-core go test -v github.com/pquerna/ffjson/tests/... ffize: install - ffjson tests/ff.go - ffjson tests/goser/ff/goser.go - ffjson tests/go.stripe/ff/customer.go - ffjson tests/types/ff/everything.go - ffjson tests/number/ff/number.go + ffjson -force-regenerate tests/ff.go + ffjson -force-regenerate tests/goser/ff/goser.go + ffjson -force-regenerate tests/go.stripe/ff/customer.go + ffjson -force-regenerate -reset-fields tests/types/ff/everything.go + ffjson -force-regenerate tests/number/ff/number.go bench: ffize all go test -v -benchmem -bench MarshalJSON github.com/pquerna/ffjson/tests @@ -39,6 +39,6 @@ bench: ffize all clean: go clean -i github.com/pquerna/ffjson/... - rm -f tests/*/ff/*_ffjson.go tests/*_ffjson.go + rm -rf tests/ff/*_ffjson.go tests/*_ffjson.go tests/ffjson-inception* .PHONY: deps clean test fmt install all diff --git a/ffjson.go b/ffjson.go index 54994a9..656a9c3 100644 --- a/ffjson.go +++ b/ffjson.go @@ -29,10 +29,11 @@ import ( "regexp" ) -var outputPathFlag = flag.String("w", "", "Write generate code to this path instead of ${input}_ffjson.go.") -var goCmdFlag = flag.String("go-cmd", "", "Path to go command; Useful for `goapp` support.") -var importNameFlag = flag.String("import-name", "", "Override import name in case it cannot be detected.") +var outputPathFlag = flag.String("w", "", "Write generate code to this path instead of ${input}_ffjson.go.") +var goCmdFlag = flag.String("go-cmd", "", "Path to go command; Useful for `goapp` support.") +var importNameFlag = flag.String("import-name", "", "Override import name in case it cannot be detected.") var forceRegenerateFlag = flag.Bool("force-regenerate", false, "Regenerate every input file, without checking modification date.") +var resetFields = flag.Bool("reset-fields", false, "When unmarshalling reset all fields missing in the JSON") func usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n\n", os.Args[0]) @@ -73,7 +74,7 @@ func main() { importName = *importNameFlag } - err := generator.GenerateFiles(goCmd, inputPath, outputPath, importName, *forceRegenerateFlag) + err := generator.GenerateFiles(goCmd, inputPath, outputPath, importName, *forceRegenerateFlag, *resetFields) if err != nil { fmt.Fprintf(os.Stderr, "Error: %s:\n\n", err) diff --git a/generator/generator.go b/generator/generator.go index c8d4e1f..1f50380 100644 --- a/generator/generator.go +++ b/generator/generator.go @@ -23,10 +23,10 @@ import ( "os" ) -func GenerateFiles(goCmd string, inputPath string, outputPath string, importName string, forceRegenerate bool) error { +func GenerateFiles(goCmd string, inputPath string, outputPath string, importName string, forceRegenerate bool, resetFields bool) error { if _, StatErr := os.Stat(outputPath); !os.IsNotExist(StatErr) { - inputFileInfo, inputFileErr := os.Stat(inputPath) + inputFileInfo, inputFileErr := os.Stat(inputPath) outputFileInfo, outputFileErr := os.Stat(outputPath) if nil == outputFileErr && nil == inputFileErr { @@ -43,7 +43,7 @@ func GenerateFiles(goCmd string, inputPath string, outputPath string, importName return err } - im := NewInceptionMain(goCmd, inputPath, outputPath) + im := NewInceptionMain(goCmd, inputPath, outputPath, resetFields) err = im.Generate(packageName, structs, importName) if err != nil { diff --git a/generator/inceptionmain.go b/generator/inceptionmain.go index 3700c5c..69f6553 100644 --- a/generator/inceptionmain.go +++ b/generator/inceptionmain.go @@ -45,7 +45,7 @@ import ( ) func main() { - i := ffjsoninception.NewInception("{{.InputPath}}", "{{.PackageName}}", "{{.OutputPath}}") + i := ffjsoninception.NewInception("{{.InputPath}}", "{{.PackageName}}", "{{.OutputPath}}", {{.ResetFields}}) i.AddMany(importedinceptionpackage.FFJSONExpose()) i.Execute() } @@ -83,6 +83,7 @@ type templateCtx struct { PackageName string InputPath string OutputPath string + ResetFields bool } type InceptionMain struct { @@ -94,15 +95,17 @@ type InceptionMain struct { tempDir string tempMain *os.File tempExpose *os.File + resetFields bool } -func NewInceptionMain(goCmd string, inputPath string, outputPath string) *InceptionMain { +func NewInceptionMain(goCmd string, inputPath string, outputPath string, resetFields bool) *InceptionMain { exposePath := getExposePath(inputPath) return &InceptionMain{ - goCmd: goCmd, - inputPath: inputPath, - outputPath: outputPath, - exposePath: exposePath, + goCmd: goCmd, + inputPath: inputPath, + outputPath: outputPath, + exposePath: exposePath, + resetFields: resetFields, } } @@ -192,6 +195,7 @@ func (im *InceptionMain) Generate(packageName string, si []*StructInfo, importNa StructNames: sn, InputPath: im.inputPath, OutputPath: im.outputPath, + ResetFields: im.resetFields, } t := template.Must(template.New("inception.go").Parse(inceptionMainTemplate)) diff --git a/inception/decoder.go b/inception/decoder.go index 2ca2261..b65279b 100644 --- a/inception/decoder.go +++ b/inception/decoder.go @@ -52,6 +52,7 @@ func CreateUnmarshalJSON(ic *Inception, si *StructInfo) error { SI: si, IC: ic, ValidValues: validValues, + ResetFields: ic.ResetFields, }) ic.OutputFuncs = append(ic.OutputFuncs, out) @@ -227,20 +228,20 @@ sliceOrArray: if typ.Kind() == reflect.Array { return tplStr(decodeTpl["handleArray"], handleArray{ - IC: ic, - Name: name, - Typ: typ, + IC: ic, + Name: name, + Typ: typ, IsPtr: ptr, - Ptr: reflect.Ptr, + Ptr: reflect.Ptr, }) } return tplStr(decodeTpl["handleSlice"], handleArray{ - IC: ic, - Name: name, - Typ: typ, + IC: ic, + Name: name, + Typ: typ, IsPtr: ptr, - Ptr: reflect.Ptr, + Ptr: reflect.Ptr, }) } diff --git a/inception/decoder_tpl.go b/inception/decoder_tpl.go index 7c8db69..9eba505 100644 --- a/inception/decoder_tpl.go +++ b/inception/decoder_tpl.go @@ -19,6 +19,7 @@ package ffjsoninception import ( "reflect" + "strconv" "text/template" ) @@ -278,7 +279,7 @@ type handleArray struct { Typ reflect.Type Ptr reflect.Kind UseReflectToSet bool - IsPtr bool + IsPtr bool } var handleArrayTxt = ` @@ -521,6 +522,7 @@ type ujFunc struct { IC *Inception SI *StructInfo ValidValues []string + ResetFields bool } var ujFuncTxt = ` @@ -539,6 +541,12 @@ func (uj *{{.SI.Name}}) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFPa tok := fflib.FFTok_init wantedTok := fflib.FFTok_init + {{if eq .ResetFields true}} + {{range $index, $field := $si.Fields}} + var ffj_set_{{$si.Name}}_{{$field.Name}} = false + {{end}} + {{end}} + mainparse: for { tok = fs.Scan() @@ -635,11 +643,13 @@ mainparse: } } } - {{range $index, $field := $si.Fields}} handle_{{$field.Name}}: {{with $fieldName := $field.Name | printf "uj.%s"}} {{handleField $ic $fieldName $field.Typ $field.Pointer $field.ForceString}} + {{if eq $.ResetFields true}} + ffj_set_{{$si.Name}}_{{$field.Name}} = true + {{end}} state = fflib.FFParse_after_value goto mainparse {{end}} @@ -659,9 +669,35 @@ tokerror: } panic("ffjson-generated: unreachable, please report bug.") done: +{{if eq .ResetFields true}} +{{range $index, $field := $si.Fields}} + if !ffj_set_{{$si.Name}}_{{$field.Name}} { + {{with $fieldName := $field.Name | printf "uj.%s"}} + {{if eq $field.Pointer true}} + {{$fieldName}} = nil + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Interface), 10) + `}} + {{$fieldName}} = nil + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Slice), 10) + `}} + {{$fieldName}} = nil + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Array), 10) + `}} + {{$fieldName}} = [{{$field.Typ.Len}}]{{getType $ic $fieldName $field.Typ.Elem}}{} + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Map), 10) + `}} + {{$fieldName}} = nil + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Bool), 10) + `}} + {{$fieldName}} = false + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.String), 10) + `}} + {{$fieldName}} = "" + {{else if eq $field.Typ.Kind ` + strconv.FormatUint(uint64(reflect.Struct), 10) + `}} + {{$fieldName}} = {{getType $ic $fieldName $field.Typ}}{} + {{else}} + {{$fieldName}} = {{getType $ic $fieldName $field.Typ}}(0) + {{end}} + {{end}} + } +{{end}} +{{end}} return nil } - ` type handleUnmarshaler struct { diff --git a/inception/inception.go b/inception/inception.go index 53f5b58..10cb271 100644 --- a/inception/inception.go +++ b/inception/inception.go @@ -36,9 +36,10 @@ type Inception struct { OutputImports map[string]bool OutputFuncs []string q ConditionalWrite + ResetFields bool } -func NewInception(inputPath string, packageName string, outputPath string) *Inception { +func NewInception(inputPath string, packageName string, outputPath string, resetFields bool) *Inception { return &Inception{ objs: make([]*StructInfo, 0), InputPath: inputPath, @@ -46,6 +47,7 @@ func NewInception(inputPath string, packageName string, outputPath string) *Ince PackageName: packageName, OutputFuncs: make([]string, 0), OutputImports: make(map[string]bool), + ResetFields: resetFields, } } diff --git a/tests/types/types_test.go b/tests/types/types_test.go index 2d73c06..f6db0a0 100644 --- a/tests/types/types_test.go +++ b/tests/types/types_test.go @@ -21,6 +21,7 @@ import ( "encoding/json" ff "github.com/pquerna/ffjson/tests/types/ff" "reflect" + "strings" "testing" ) @@ -135,3 +136,52 @@ func TestUnmarshalNullPointer(t *testing.T) { t.Fatalf("record.Something decoding problem, expected: nil got: %v", record.FooStruct) } } + +func TestUnmarshalToReusedObject(t *testing.T) { + JSONParts := []string{ + `"Bool":true`, + `"Int":1`, + `"Int8": 2`, + `"Int16": 3`, + `"Int32": -4`, + `"Int64": 57`, + `"Uint": 100`, + `"Uint8": 101`, + `"Uint16": 102`, + `"Uint32": 50`, + `"Uint64": 103`, + `"Uintptr": 104`, + `"Float32": 3.14`, + `"Float64": 3.15`, + `"Array": [1,2,3]`, + `"Map": {"bar": 2,"foo": 1}`, + `"String": "snowman☃\uD801\uDC37"`, + `"StringPointer": "pointed snowman☃\uD801\uDC37"`, + `"Int64Pointer": 44`, + `"FooStruct": {"Bar": 1}`, + `"MapMap": {"a0": {"b0":"foo"}, "a1":{"a2":"bar"}}`, + `"MapArraySlice": {"foo":[[1,2,3],[4,5,6],[7]], "bar": [[1,2,3,4],[5,6,7]]}`, + `"Something": 99`, + } + + JSONWhole := "{" + strings.Join(JSONParts, ",") + "}" + var record ff.Everything + if err := record.UnmarshalJSON([]byte(JSONWhole)); err != nil { + t.Fatalf("UnmarshalJSON: %v", err) + } + + for _, part := range JSONParts { + reuseRecord := record + if err := reuseRecord.UnmarshalJSON([]byte("{" + part + "}")); err != nil { + t.Fatalf("UnmarshalJSON: %v", err) + } + var emptyRecord ff.Everything + if err := emptyRecord.UnmarshalJSON([]byte("{" + part + "}")); err != nil { + t.Fatalf("UnmarshalJSON: %v", err) + } + + if !reflect.DeepEqual(reuseRecord, emptyRecord) { + t.Errorf("%#v should be equal to %#v", reuseRecord, emptyRecord) + } + } +}