diff --git a/.gitignore b/.gitignore index db8c66e..26156fb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *_easyjson.go *.iml .idea +*.swp diff --git a/Makefile b/Makefile index f877ab2..7cfec87 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ all: test .root/src/$(PKG): mkdir -p $@ - for i in $$PWD/* ; do ln -s $$i $@/`basename $$i` ; done + for i in $$PWD/* ; do ln -s $$i $@/`basename $$i` ; done root: .root/src/$(PKG) @@ -23,9 +23,11 @@ generate: root build .root/src/$(PKG)/tests/data.go \ .root/src/$(PKG)/tests/omitempty.go \ .root/src/$(PKG)/tests/nothing.go \ - .root/src/$(PKG)/tests/named_type.go + .root/src/$(PKG)/tests/named_type.go \ + .root/src/$(PKG)/tests/custom_map_key_type.go \ + .root/src/$(PKG)/tests/embedded_type.go - .root/bin/easyjson -all .root/src/$(PKG)/tests/data.go + .root/bin/easyjson -all .root/src/$(PKG)/tests/data.go .root/bin/easyjson -all .root/src/$(PKG)/tests/nothing.go .root/bin/easyjson -all .root/src/$(PKG)/tests/errors.go .root/bin/easyjson -snake_case .root/src/$(PKG)/tests/snake.go @@ -33,6 +35,9 @@ generate: root build .root/bin/easyjson -build_tags=use_easyjson .root/src/$(PKG)/benchmark/data.go .root/bin/easyjson .root/src/$(PKG)/tests/nested_easy.go .root/bin/easyjson .root/src/$(PKG)/tests/named_type.go + .root/bin/easyjson .root/src/$(PKG)/tests/custom_map_key_type.go + .root/bin/easyjson .root/src/$(PKG)/tests/embedded_type.go + .root/bin/easyjson -disallow_unknown_fields .root/src/$(PKG)/tests/disallow_unknown.go test: generate root go test \ diff --git a/README.md b/README.md index 9366e3f..7fd7686 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Usage of easyjson: use lowerCamelCase instead of CamelCase by default -stubs only generate stubs for marshaler/unmarshaler funcs + -disallow_unknown_fields + return error if some unknown field in json appeared ``` Using `-all` will generate marshalers/unmarshalers for all Go structs in the diff --git a/bootstrap/bootstrap.go b/bootstrap/bootstrap.go index 3c20e09..95e5d1e 100644 --- a/bootstrap/bootstrap.go +++ b/bootstrap/bootstrap.go @@ -22,10 +22,11 @@ type Generator struct { PkgPath, PkgName string Types []string - NoStdMarshalers bool - SnakeCase bool - LowerCamelCase bool - OmitEmpty bool + NoStdMarshalers bool + SnakeCase bool + LowerCamelCase bool + OmitEmpty bool + DisallowUnknownFields bool OutName string BuildTags string @@ -120,6 +121,9 @@ func (g *Generator) writeMain() (path string, err error) { if g.NoStdMarshalers { fmt.Fprintln(f, " g.NoStdMarshalers()") } + if g.DisallowUnknownFields { + fmt.Fprintln(f, " g.DisallowUnknownFields()") + } sort.Strings(g.Types) for _, v := range g.Types { diff --git a/easyjson/main.go b/easyjson/main.go index 1cd30bb..d4035f7 100644 --- a/easyjson/main.go +++ b/easyjson/main.go @@ -27,6 +27,7 @@ var stubs = flag.Bool("stubs", false, "only generate stubs for marshaler/unmarsh var noformat = flag.Bool("noformat", false, "do not run 'gofmt -w' on output file") var specifiedName = flag.String("output_filename", "", "specify the filename of the output") var processPkg = flag.Bool("pkg", false, "process the whole package instead of just the given file") +var disallowUnknownFields = flag.Bool("disallow_unknown_fields", false, "return error if any unknown field in json appeared") func generate(fname string) (err error) { fInfo, err := os.Stat(fname) @@ -60,18 +61,19 @@ func generate(fname string) (err error) { } g := bootstrap.Generator{ - BuildTags: trimmedBuildTags, - PkgPath: p.PkgPath, - PkgName: p.PkgName, - Types: p.StructNames, - SnakeCase: *snakeCase, - LowerCamelCase: *lowerCamelCase, - NoStdMarshalers: *noStdMarshalers, - OmitEmpty: *omitEmpty, - LeaveTemps: *leaveTemps, - OutName: outName, - StubsOnly: *stubs, - NoFormat: *noformat, + BuildTags: trimmedBuildTags, + PkgPath: p.PkgPath, + PkgName: p.PkgName, + Types: p.StructNames, + SnakeCase: *snakeCase, + LowerCamelCase: *lowerCamelCase, + NoStdMarshalers: *noStdMarshalers, + DisallowUnknownFields: *disallowUnknownFields, + OmitEmpty: *omitEmpty, + LeaveTemps: *leaveTemps, + OutName: outName, + StubsOnly: *stubs, + NoFormat: *noformat, } if err := g.Run(); err != nil { diff --git a/gen/decoder.go b/gen/decoder.go index 021933a..606602f 100644 --- a/gen/decoder.go +++ b/gen/decoder.go @@ -48,10 +48,12 @@ var primitiveStringDecoders = map[reflect.Kind]string{ reflect.Uint32: "in.Uint32Str()", reflect.Uint64: "in.Uint64Str()", reflect.Uintptr: "in.UintptrStr()", + reflect.Float32: "in.Float32Str()", + reflect.Float64: "in.Float64Str()", } var customDecoders = map[string]string{ - "json.Number": "in.JsonNumber()", + "json.Number": "in.JsonNumber()", } // genTypeDecoder generates decoding code for the type t, but uses unmarshaler interface if implemented by t. @@ -84,11 +86,19 @@ func (g *Generator) genTypeDecoder(t reflect.Type, out string, tags fieldTags, i return err } +// returns true of the type t implements one of the custom unmarshaler interfaces +func hasCustomUnmarshaler(t reflect.Type) bool { + t = reflect.PtrTo(t) + return t.Implements(reflect.TypeOf((*easyjson.Unmarshaler)(nil)).Elem()) || + t.Implements(reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()) || + t.Implements(reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()) +} + // genTypeDecoderNoCheck generates decoding code for the type t. func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags fieldTags, indent int) error { ws := strings.Repeat(" ", indent) // Check whether type is primitive, needs to be done after interface check. - if dec := customDecoders[t.String()]; dec != "" { + if dec := customDecoders[t.String()]; dec != "" { fmt.Fprintln(g.out, ws+out+" = "+dec) return nil } else if dec := primitiveStringDecoders[t.Kind()]; dec != "" && tags.asString { @@ -104,7 +114,7 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field tmpVar := g.uniqueVarName() elem := t.Elem() - if elem.Kind() == reflect.Uint8 { + if elem.Kind() == reflect.Uint8 && elem.Name() == "uint8" { fmt.Fprintln(g.out, ws+"if in.IsNull() {") fmt.Fprintln(g.out, ws+" in.Skip()") fmt.Fprintln(g.out, ws+" "+out+" = nil") @@ -151,7 +161,7 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field iterVar := g.uniqueVarName() elem := t.Elem() - if elem.Kind() == reflect.Uint8 { + if elem.Kind() == reflect.Uint8 && elem.Name() == "uint8" { fmt.Fprintln(g.out, ws+"if in.IsNull() {") fmt.Fprintln(g.out, ws+" in.Skip()") fmt.Fprintln(g.out, ws+"} else {") @@ -170,7 +180,7 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field fmt.Fprintln(g.out, ws+" for !in.IsDelim(']') {") fmt.Fprintln(g.out, ws+" if "+iterVar+" < "+fmt.Sprint(length)+" {") - if err := g.genTypeDecoder(elem, out+"["+iterVar+"]", tags, indent+3); err != nil { + if err := g.genTypeDecoder(elem, "("+out+")["+iterVar+"]", tags, indent+3); err != nil { return err } @@ -208,9 +218,9 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field case reflect.Map: key := t.Key() keyDec, ok := primitiveStringDecoders[key.Kind()] - if !ok { - return fmt.Errorf("map type %v not supported: only string and integer keys are allowed", key) - } + if !ok && !hasCustomUnmarshaler(key) { + return fmt.Errorf("map type %v not supported: only string and integer keys and types implementing json.Unmarshaler are allowed", key) + } // else assume the caller knows what they are doing and that the custom unmarshaler performs the translation from string or integer keys to the key type elem := t.Elem() tmpVar := g.uniqueVarName() @@ -225,7 +235,15 @@ func (g *Generator) genTypeDecoderNoCheck(t reflect.Type, out string, tags field fmt.Fprintln(g.out, ws+" }") fmt.Fprintln(g.out, ws+" for !in.IsDelim('}') {") - fmt.Fprintln(g.out, ws+" key := "+g.getType(key)+"("+keyDec+")") + if keyDec != "" { + fmt.Fprintln(g.out, ws+" key := "+g.getType(key)+"("+keyDec+")") + } else { + fmt.Fprintln(g.out, ws+" var key "+g.getType(key)) + if err := g.genTypeDecoder(key, "key", tags, indent+2); err != nil { + return err + } + } + fmt.Fprintln(g.out, ws+" in.WantColon()") fmt.Fprintln(g.out, ws+" var "+tmpVar+" "+g.getType(elem)) @@ -443,7 +461,15 @@ func (g *Generator) genStructDecoder(t reflect.Type) error { } fmt.Fprintln(g.out, " default:") - fmt.Fprintln(g.out, " in.SkipRecursive()") + if g.disallowUnknownFields { + fmt.Fprintln(g.out, ` in.AddError(&jlexer.LexerError{ + Offset: in.GetPos(), + Reason: "unknown field", + Data: key, + })`) + } else { + fmt.Fprintln(g.out, " in.SkipRecursive()") + } fmt.Fprintln(g.out, " }") fmt.Fprintln(g.out, " in.WantComma()") fmt.Fprintln(g.out, " }") diff --git a/gen/encoder.go b/gen/encoder.go index 408ae55..b2be743 100644 --- a/gen/encoder.go +++ b/gen/encoder.go @@ -45,6 +45,8 @@ var primitiveStringEncoders = map[reflect.Kind]string{ reflect.Uint32: "out.Uint32Str(uint32(%v))", reflect.Uint64: "out.Uint64Str(uint64(%v))", reflect.Uintptr: "out.UintptrStr(uintptr(%v))", + reflect.Float32: "out.Float32Str(float32(%v))", + reflect.Float64: "out.Float64Str(float64(%v))", } // fieldTags contains parsed version of json struct field tags. @@ -83,7 +85,7 @@ func parseFieldTags(f reflect.StructField) fieldTags { } // genTypeEncoder generates code that encodes in of type t into the writer, but uses marshaler interface if implemented by t. -func (g *Generator) genTypeEncoder(t reflect.Type, in string, tags fieldTags, indent int) error { +func (g *Generator) genTypeEncoder(t reflect.Type, in string, tags fieldTags, indent int, assumeNonEmpty bool) error { ws := strings.Repeat(" ", indent) marshalerIface := reflect.TypeOf((*easyjson.Marshaler)(nil)).Elem() @@ -104,12 +106,20 @@ func (g *Generator) genTypeEncoder(t reflect.Type, in string, tags fieldTags, in return nil } - err := g.genTypeEncoderNoCheck(t, in, tags, indent) + err := g.genTypeEncoderNoCheck(t, in, tags, indent, assumeNonEmpty) return err } +// returns true of the type t implements one of the custom marshaler interfaces +func hasCustomMarshaler(t reflect.Type) bool { + t = reflect.PtrTo(t) + return t.Implements(reflect.TypeOf((*easyjson.Marshaler)(nil)).Elem()) || + t.Implements(reflect.TypeOf((*json.Marshaler)(nil)).Elem()) || + t.Implements(reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem()) +} + // genTypeEncoderNoCheck generates code that encodes in of type t into the writer. -func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldTags, indent int) error { +func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldTags, indent int, assumeNonEmpty bool) error { ws := strings.Repeat(" ", indent) // Check whether type is primitive, needs to be done after interface check. @@ -127,19 +137,23 @@ func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldT iVar := g.uniqueVarName() vVar := g.uniqueVarName() - if t.Elem().Kind() == reflect.Uint8 { + if t.Elem().Kind() == reflect.Uint8 && elem.Name() == "uint8" { fmt.Fprintln(g.out, ws+"out.Base64Bytes("+in+")") } else { - fmt.Fprintln(g.out, ws+"if "+in+" == nil && (out.Flags & jwriter.NilSliceAsEmpty) == 0 {") - fmt.Fprintln(g.out, ws+` out.RawString("null")`) - fmt.Fprintln(g.out, ws+"} else {") + if !assumeNonEmpty { + fmt.Fprintln(g.out, ws+"if "+in+" == nil && (out.Flags & jwriter.NilSliceAsEmpty) == 0 {") + fmt.Fprintln(g.out, ws+` out.RawString("null")`) + fmt.Fprintln(g.out, ws+"} else {") + } else { + fmt.Fprintln(g.out, ws+"{") + } fmt.Fprintln(g.out, ws+" out.RawByte('[')") fmt.Fprintln(g.out, ws+" for "+iVar+", "+vVar+" := range "+in+" {") fmt.Fprintln(g.out, ws+" if "+iVar+" > 0 {") fmt.Fprintln(g.out, ws+" out.RawByte(',')") fmt.Fprintln(g.out, ws+" }") - if err := g.genTypeEncoder(elem, vVar, tags, indent+2); err != nil { + if err := g.genTypeEncoder(elem, vVar, tags, indent+2, false); err != nil { return err } @@ -152,7 +166,7 @@ func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldT elem := t.Elem() iVar := g.uniqueVarName() - if t.Elem().Kind() == reflect.Uint8 { + if t.Elem().Kind() == reflect.Uint8 && elem.Name() == "uint8" { fmt.Fprintln(g.out, ws+"out.Base64Bytes("+in+"[:])") } else { fmt.Fprintln(g.out, ws+"out.RawByte('[')") @@ -161,7 +175,7 @@ func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldT fmt.Fprintln(g.out, ws+" out.RawByte(',')") fmt.Fprintln(g.out, ws+" }") - if err := g.genTypeEncoder(elem, in+"["+iVar+"]", tags, indent+1); err != nil { + if err := g.genTypeEncoder(elem, "("+in+")["+iVar+"]", tags, indent+1, false); err != nil { return err } @@ -176,36 +190,50 @@ func (g *Generator) genTypeEncoderNoCheck(t reflect.Type, in string, tags fieldT fmt.Fprintln(g.out, ws+enc+"(out, "+in+")") case reflect.Ptr: - fmt.Fprintln(g.out, ws+"if "+in+" == nil {") - fmt.Fprintln(g.out, ws+` out.RawString("null")`) - fmt.Fprintln(g.out, ws+"} else {") + if !assumeNonEmpty { + fmt.Fprintln(g.out, ws+"if "+in+" == nil {") + fmt.Fprintln(g.out, ws+` out.RawString("null")`) + fmt.Fprintln(g.out, ws+"} else {") + } - if err := g.genTypeEncoder(t.Elem(), "*"+in, tags, indent+1); err != nil { + if err := g.genTypeEncoder(t.Elem(), "*"+in, tags, indent+1, false); err != nil { return err } - fmt.Fprintln(g.out, ws+"}") + if !assumeNonEmpty { + fmt.Fprintln(g.out, ws+"}") + } case reflect.Map: key := t.Key() keyEnc, ok := primitiveStringEncoders[key.Kind()] - if !ok { - return fmt.Errorf("map key type %v not supported: only string and integer keys are allowed", key) - } + if !ok && !hasCustomMarshaler(key) { + return fmt.Errorf("map key type %v not supported: only string and integer keys and types implementing Marshaler interfaces are allowed", key) + } // else assume the caller knows what they are doing and that the custom marshaler performs the translation from the key type to a string or integer tmpVar := g.uniqueVarName() - fmt.Fprintln(g.out, ws+"if "+in+" == nil && (out.Flags & jwriter.NilMapAsEmpty) == 0 {") - fmt.Fprintln(g.out, ws+" out.RawString(`null`)") - fmt.Fprintln(g.out, ws+"} else {") + if !assumeNonEmpty { + fmt.Fprintln(g.out, ws+"if "+in+" == nil && (out.Flags & jwriter.NilMapAsEmpty) == 0 {") + fmt.Fprintln(g.out, ws+" out.RawString(`null`)") + fmt.Fprintln(g.out, ws+"} else {") + } else { + fmt.Fprintln(g.out, ws+"{") + } fmt.Fprintln(g.out, ws+" out.RawByte('{')") fmt.Fprintln(g.out, ws+" "+tmpVar+"First := true") fmt.Fprintln(g.out, ws+" for "+tmpVar+"Name, "+tmpVar+"Value := range "+in+" {") - fmt.Fprintln(g.out, ws+" if !"+tmpVar+"First { out.RawByte(',') }") - fmt.Fprintln(g.out, ws+" "+tmpVar+"First = false") - fmt.Fprintln(g.out, ws+" "+fmt.Sprintf(keyEnc, tmpVar+"Name")) + fmt.Fprintln(g.out, ws+" if "+tmpVar+"First { "+tmpVar+"First = false } else { out.RawByte(',') }") + if keyEnc != "" { + fmt.Fprintln(g.out, ws+" "+fmt.Sprintf(keyEnc, tmpVar+"Name")) + } else { + if err := g.genTypeEncoder(key, tmpVar+"Name", tags, indent+2, false); err != nil { + return err + } + } + fmt.Fprintln(g.out, ws+" out.RawByte(':')") - if err := g.genTypeEncoder(t.Elem(), tmpVar+"Value", tags, indent+2); err != nil { + if err := g.genTypeEncoder(t.Elem(), tmpVar+"Value", tags, indent+2, false); err != nil { return err } @@ -265,19 +293,21 @@ func (g *Generator) genStructFieldEncoder(t reflect.Type, f reflect.StructField) if tags.omit { return nil } - if !tags.omitEmpty && !g.omitEmpty || tags.noOmitEmpty { - fmt.Fprintln(g.out, " if !first { out.RawByte(',') }") - fmt.Fprintln(g.out, " first = false") - fmt.Fprintf(g.out, " out.RawString(%q)\n", strconv.Quote(jsonName)+":") - return g.genTypeEncoder(f.Type, "in."+f.Name, tags, 1) + noOmitEmpty := (!tags.omitEmpty && !g.omitEmpty) || tags.noOmitEmpty + if noOmitEmpty { + fmt.Fprintln(g.out, " {") + } else { + fmt.Fprintln(g.out, " if", g.notEmptyCheck(f.Type, "in."+f.Name), "{") } + fmt.Fprintf(g.out, " const prefix string = %q\n", ","+strconv.Quote(jsonName)+":") + fmt.Fprintln(g.out, " if first {") + fmt.Fprintln(g.out, " first = false") + fmt.Fprintln(g.out, " out.RawString(prefix[1:])") + fmt.Fprintln(g.out, " } else {") + fmt.Fprintln(g.out, " out.RawString(prefix)") + fmt.Fprintln(g.out, " }") - fmt.Fprintln(g.out, " if", g.notEmptyCheck(f.Type, "in."+f.Name), "{") - fmt.Fprintln(g.out, " if !first { out.RawByte(',') }") - fmt.Fprintln(g.out, " first = false") - - fmt.Fprintf(g.out, " out.RawString(%q)\n", strconv.Quote(jsonName)+":") - if err := g.genTypeEncoder(f.Type, "in."+f.Name, tags, 2); err != nil { + if err := g.genTypeEncoder(f.Type, "in."+f.Name, tags, 2, !noOmitEmpty); err != nil { return err } fmt.Fprintln(g.out, " }") @@ -304,7 +334,7 @@ func (g *Generator) genSliceArrayMapEncoder(t reflect.Type) error { typ := g.getType(t) fmt.Fprintln(g.out, "func "+fname+"(out *jwriter.Writer, in "+typ+") {") - err := g.genTypeEncoderNoCheck(t, "in", fieldTags{}, 1) + err := g.genTypeEncoderNoCheck(t, "in", fieldTags{}, 1, false) if err != nil { return err } diff --git a/gen/generator.go b/gen/generator.go index eb0d70b..a34a852 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -33,9 +33,10 @@ type Generator struct { varCounter int - noStdMarshalers bool - omitEmpty bool - fieldNamer FieldNamer + noStdMarshalers bool + omitEmpty bool + disallowUnknownFields bool + fieldNamer FieldNamer // package path to local alias map for tracking imports imports map[string]string @@ -110,6 +111,11 @@ func (g *Generator) NoStdMarshalers() { g.noStdMarshalers = true } +// DisallowUnknownFields instructs not to skip unknown fields in json and return error. +func (g *Generator) DisallowUnknownFields() { + g.disallowUnknownFields = true +} + // OmitEmpty triggers `json=",omitempty"` behaviour by default. func (g *Generator) OmitEmpty() { g.omitEmpty = true @@ -284,7 +290,11 @@ func (g *Generator) getType(t reflect.Type) string { lines := make([]string, 0, nf) for i := 0; i < nf; i++ { f := t.Field(i) - line := f.Name + " " + g.getType(f.Type) + var line string + if !f.Anonymous { + line = f.Name + " " + } // else the field is anonymous (an embedded type) + line += g.getType(f.Type) t := f.Tag if t != "" { line += " " + escapeTag(t) diff --git a/jlexer/lexer.go b/jlexer/lexer.go index e5558ae..90525e6 100644 --- a/jlexer/lexer.go +++ b/jlexer/lexer.go @@ -649,7 +649,7 @@ func (r *Lexer) Bytes() []byte { return nil } ret := make([]byte, base64.StdEncoding.DecodedLen(len(r.token.byteValue))) - len, err := base64.StdEncoding.Decode(ret, r.token.byteValue) + n, err := base64.StdEncoding.Decode(ret, r.token.byteValue) if err != nil { r.fatalError = &LexerError{ Reason: err.Error(), @@ -658,7 +658,7 @@ func (r *Lexer) Bytes() []byte { } r.consume() - return ret[:len] + return ret[:n] } // Bool reads a true or false boolean keyword. @@ -997,6 +997,22 @@ func (r *Lexer) Float32() float32 { return float32(n) } +func (r *Lexer) Float32Str() float32 { + s, b := r.unsafeString() + if !r.Ok() { + return 0 + } + n, err := strconv.ParseFloat(s, 32) + if err != nil { + r.addNonfatalError(&LexerError{ + Offset: r.start, + Reason: err.Error(), + Data: string(b), + }) + } + return float32(n) +} + func (r *Lexer) Float64() float64 { s := r.number() if !r.Ok() { @@ -1014,6 +1030,22 @@ func (r *Lexer) Float64() float64 { return n } +func (r *Lexer) Float64Str() float64 { + s, b := r.unsafeString() + if !r.Ok() { + return 0 + } + n, err := strconv.ParseFloat(s, 64) + if err != nil { + r.addNonfatalError(&LexerError{ + Offset: r.start, + Reason: err.Error(), + Data: string(b), + }) + } + return n +} + func (r *Lexer) Error() error { return r.fatalError } @@ -1056,7 +1088,7 @@ func (r *Lexer) JsonNumber() json.Number { } if !r.Ok() { r.errInvalidToken("json.Number") - return json.Number("0") + return json.Number("") } switch r.token.kind { @@ -1064,9 +1096,12 @@ func (r *Lexer) JsonNumber() json.Number { return json.Number(r.String()) case tokenNumber: return json.Number(r.Raw()) + case tokenNull: + r.Null() + return json.Number("") default: r.errSyntax() - return json.Number("0") + return json.Number("") } } diff --git a/jlexer/lexer_test.go b/jlexer/lexer_test.go index 4ce4abe..529a270 100644 --- a/jlexer/lexer_test.go +++ b/jlexer/lexer_test.go @@ -25,9 +25,9 @@ func TestString(t *testing.T) { {toParse: `"test"junk`, want: "test"}, - {toParse: `5`, wantError: true}, // not a string - {toParse: `"\x"`, wantError: true}, // invalid escape - {toParse: `"\ud800"`, want: "�"}, // invalid utf-8 char; return replacement char + {toParse: `5`, wantError: true}, // not a string + {toParse: `"\x"`, wantError: true}, // invalid escape + {toParse: `"\ud800"`, want: "�"}, // invalid utf-8 char; return replacement char } { l := Lexer{Data: []byte(test.toParse)} @@ -269,16 +269,19 @@ func TestJsonNumber(t *testing.T) { {toParse: `"0.12"`, want: json.Number("0.12"), wantValue: 0.12}, {toParse: `"25E-4"`, want: json.Number("25E-4"), wantValue: 25E-4}, - {toParse: `"a""`, wantValueError: true}, + {toParse: `"foo"`, want: json.Number("foo"), wantValueError: true}, + {toParse: `null`, want: json.Number(""), wantValueError: true}, - {toParse: `[1]`, wantLexerError: true}, - {toParse: `{}`, wantLexerError: true}, - {toParse: `a`, wantLexerError: true}, + {toParse: `"a""`, want: json.Number("a"), wantValueError: true}, + + {toParse: `[1]`, want: json.Number(""), wantLexerError: true, wantValueError: true}, + {toParse: `{}`, want: json.Number(""), wantLexerError: true, wantValueError: true}, + {toParse: `a`, want: json.Number(""), wantLexerError: true, wantValueError: true}, } { l := Lexer{Data: []byte(test.toParse)} got := l.JsonNumber() - if got != test.want && !test.wantLexerError && !test.wantValueError { + if got != test.want { t.Errorf("[%d, %q] JsonNumber() = %v; want %v", i, test.toParse, got, test.want) } @@ -303,7 +306,7 @@ func TestJsonNumber(t *testing.T) { } if valueErr != nil && !test.wantValueError { - t.Errorf("[%d, %q] JsonNumber() value error: %v", i, test.toParse, err) + t.Errorf("[%d, %q] JsonNumber() value error: %v", i, test.toParse, valueErr) } else if valueErr == nil && test.wantValueError { t.Errorf("[%d, %q] JsonNumber() ok; want value error", i, test.toParse) } diff --git a/jwriter/writer.go b/jwriter/writer.go index e5a5ddf..b9ed7cc 100644 --- a/jwriter/writer.go +++ b/jwriter/writer.go @@ -240,11 +240,25 @@ func (w *Writer) Float32(n float32) { w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32) } +func (w *Writer) Float32Str(n float32) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 32) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + func (w *Writer) Float64(n float64) { w.Buffer.EnsureSpace(20) w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, n, 'g', -1, 64) } +func (w *Writer) Float64Str(n float64) { + w.Buffer.EnsureSpace(20) + w.Buffer.Buf = append(w.Buffer.Buf, '"') + w.Buffer.Buf = strconv.AppendFloat(w.Buffer.Buf, float64(n), 'g', -1, 64) + w.Buffer.Buf = append(w.Buffer.Buf, '"') +} + func (w *Writer) Bool(v bool) { w.Buffer.EnsureSpace(5) if v { @@ -340,12 +354,11 @@ func (w *Writer) base64(in []byte) { return } - w.Buffer.EnsureSpace(((len(in) - 1) / 3 + 1) * 4) + w.Buffer.EnsureSpace(((len(in)-1)/3 + 1) * 4) si := 0 n := (len(in) / 3) * 3 - for si < n { // Convert 3x 8bit source bytes into 4 bytes val := uint(in[si+0])<<16 | uint(in[si+1])<<8 | uint(in[si+2]) diff --git a/parser/parser.go b/parser/parser.go index 5bd06e9..babb84c 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -93,5 +93,5 @@ func (p *Parser) Parse(fname string, isDir bool) error { func getDefaultGoPath() (string, error) { output, err := exec.Command("go", "env", "GOPATH").Output() - return string(output), err + return strings.TrimSpace(string(output)), err } diff --git a/parser/parser_unix.go b/parser/parser_unix.go index 09b20a2..cc0686e 100644 --- a/parser/parser_unix.go +++ b/parser/parser_unix.go @@ -27,7 +27,7 @@ func getPkgPath(fname string, isDir bool) (string, error) { } } - for _, p := range strings.Split(os.Getenv("GOPATH"), ":") { + for _, p := range strings.Split(gopath, ":") { prefix := path.Join(p, "src") + "/" if rel := strings.TrimPrefix(fname, prefix); rel != fname { if !isDir { diff --git a/parser/parser_windows.go b/parser/parser_windows.go index c777f43..73da3ee 100644 --- a/parser/parser_windows.go +++ b/parser/parser_windows.go @@ -33,7 +33,7 @@ func getPkgPath(fname string, isDir bool) (string, error) { } } - for _, p := range strings.Split(os.Getenv("GOPATH"), ";") { + for _, p := range strings.Split(gopath, ";") { prefix := path.Join(normalizePath(p), "src") + "/" if rel := strings.TrimPrefix(fname, prefix); rel != fname { if !isDir { diff --git a/tests/basic_test.go b/tests/basic_test.go index 0186784..3b1cc65 100644 --- a/tests/basic_test.go +++ b/tests/basic_test.go @@ -38,6 +38,8 @@ var testCases = []struct { {&IntsValue, IntsString}, {&mapStringStringValue, mapStringStringString}, {&namedTypeValue, namedTypeValueString}, + {&customMapKeyTypeValue, customMapKeyTypeValueString}, + {&embeddedTypeValue, embeddedTypeValueString}, {&mapMyIntStringValue, mapMyIntStringValueString}, {&mapIntStringValue, mapIntStringValueString}, {&mapInt32StringValue, mapInt32StringValueString}, @@ -47,6 +49,9 @@ var testCases = []struct { {&mapUint64StringValue, mapUint64StringValueString}, {&mapUintptrStringValue, mapUintptrStringValueString}, {&intKeyedMapStructValue, intKeyedMapStructValueString}, + {&intArrayStructValue, intArrayStructValueString}, + {&myUInt8SliceValue, myUInt8SliceString}, + {&myUInt8ArrayValue, myUInt8ArrayString}, } func TestMarshal(t *testing.T) { @@ -229,3 +234,11 @@ func TestUnmarshalStructWithEmbeddedPtrStruct(t *testing.T) { t.Errorf("easyjson.Unmarshal() = %#v; want %#v", s, structWithInterfaceValueFilled) } } + +func TestDisallowUnknown(t *testing.T) { + var d DisallowUnknown + err := easyjson.Unmarshal([]byte(disallowUnknownString), &d) + if err == nil { + t.Error("want error, got nil") + } +} diff --git a/tests/custom_map_key_type.go b/tests/custom_map_key_type.go new file mode 100644 index 0000000..099bd06 --- /dev/null +++ b/tests/custom_map_key_type.go @@ -0,0 +1,29 @@ +package tests + +import fmt "fmt" + +//easyjson:json +type CustomMapKeyType struct { + Map map[customKeyType]int +} + +type customKeyType [2]byte + +func (k customKeyType) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%02x"`, k)), nil +} + +func (k *customKeyType) UnmarshalJSON(b []byte) error { + _, err := fmt.Sscanf(string(b), `"%02x%02x"`, &k[0], &k[1]) + return err +} + +var customMapKeyTypeValue CustomMapKeyType + +func init() { + customMapKeyTypeValue.Map = map[customKeyType]int{ + customKeyType{0x01, 0x02}: 3, + } +} + +var customMapKeyTypeValueString = `{"Map":{"0102":3}}` diff --git a/tests/data.go b/tests/data.go index 145f093..6ae90a0 100644 --- a/tests/data.go +++ b/tests/data.go @@ -41,6 +41,9 @@ type PrimitiveTypes struct { Float32 float32 Float64 float64 + Float32String float32 `json:",string"` + Float64String float64 `json:",string"` + Ptr *string PtrNil *string } @@ -77,6 +80,9 @@ var primitiveTypesValue = PrimitiveTypes{ Float32: 1.5, Float64: math.MaxFloat64, + Float32String: 1.5, + Float64String: math.MaxFloat64, + Ptr: &str, } @@ -110,6 +116,9 @@ var primitiveTypesString = "{" + `"Float32":` + fmt.Sprint(1.5) + `,` + `"Float64":` + fmt.Sprint(math.MaxFloat64) + `,` + + `"Float32String":"` + fmt.Sprint(1.5) + `",` + + `"Float64String":"` + fmt.Sprint(math.MaxFloat64) + `",` + + `"Ptr":"bla",` + `"PtrNil":null` + @@ -757,3 +766,37 @@ var intKeyedMapStructValueString = `{` + `"foo":{"42":"life"},` + `"bar":{"32":{"354634382":"life"}}` + `}` + +type IntArray [2]int + +//easyjson:json +type IntArrayStruct struct { + Pointer *IntArray `json:"pointer"` + Value IntArray `json:"value"` +} + +var intArrayStructValue = IntArrayStruct{ + Pointer: &IntArray{1, 2}, + Value: IntArray{1, 2}, +} + +var intArrayStructValueString = `{` + + `"pointer":[1,2],` + + `"value":[1,2]` + + `}` + +type MyUInt8 uint8 + +//easyjson:json +type MyUInt8Slice []MyUInt8 + +var myUInt8SliceValue = MyUInt8Slice{1, 2, 3, 4, 5} + +var myUInt8SliceString = `[1,2,3,4,5]` + +//easyjson:json +type MyUInt8Array [2]MyUInt8 + +var myUInt8ArrayValue = MyUInt8Array{1, 2} + +var myUInt8ArrayString = `[1,2]` diff --git a/tests/disallow_unknown.go b/tests/disallow_unknown.go new file mode 100644 index 0000000..5b884c6 --- /dev/null +++ b/tests/disallow_unknown.go @@ -0,0 +1,8 @@ +package tests + +//easyjson:json +type DisallowUnknown struct { + FieldOne string `json:"field_one"` +} + +var disallowUnknownString = `{"field_one": "one", "field_two": "two"}` diff --git a/tests/embedded_type.go b/tests/embedded_type.go new file mode 100644 index 0000000..66470b6 --- /dev/null +++ b/tests/embedded_type.go @@ -0,0 +1,24 @@ +package tests + +//easyjson:json +type EmbeddedType struct { + EmbeddedInnerType + Inner struct { + EmbeddedInnerType + } + Field2 int +} + +type EmbeddedInnerType struct { + Field1 int +} + +var embeddedTypeValue EmbeddedType + +func init() { + embeddedTypeValue.Field1 = 1 + embeddedTypeValue.Field2 = 2 + embeddedTypeValue.Inner.Field1 = 3 +} + +var embeddedTypeValueString = `{"Inner":{"Field1":3},"Field2":2,"Field1":1}`