Skip to content

Commit

Permalink
Add native json.Number support
Browse files Browse the repository at this point in the history
Allow encoding and decoding of `json.Number` values, either as struct members or as interface members.

Numbers will be encoded as integer, if possible, otherwise float64 is used. To cover the zero values, write an empty string, but invalid values will return errors when encoded

It is possible to encode as string with `//msgp:replace json.Number with:string`.

Fixes #292
  • Loading branch information
klauspost committed Sep 28, 2024
1 parent 3dc88ae commit 01ec106
Show file tree
Hide file tree
Showing 11 changed files with 323 additions and 10 deletions.
7 changes: 7 additions & 0 deletions _generated/def.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package _generated

import (
"encoding/json"
"os"
"time"

Expand Down Expand Up @@ -299,3 +300,9 @@ type StructByteSlice struct {
AComplex128 []complex128 `msg:",allownil"`
AStruct []Fixed `msg:",allownil"`
}

type NumberJSONSample struct {
Single json.Number
Array []json.Number
Map map[string]json.Number
}
105 changes: 105 additions & 0 deletions _generated/def_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package _generated

import (
"bytes"
"encoding/json"
"reflect"
"testing"

Expand Down Expand Up @@ -74,3 +75,107 @@ func TestRuneMarshalUnmarshal(t *testing.T) {
t.Errorf("rune slice mismatch")
}
}

func TestJSONNumber(t *testing.T) {
test := NumberJSONSample{
Single: "-42",
Array: []json.Number{"0", "-0", "1", "-1", "0.1", "-0.1", "1234", "-1234", "12.34", "-12.34", "12E0", "12E1", "12e34", "12E-0", "12e+1", "12e-34", "-12E0", "-12E1", "-12e34", "-12E-0", "-12e+1", "-12e-34", "1.2E0", "1.2E1", "1.2e34", "1.2E-0", "1.2e+1", "1.2e-34", "-1.2E0", "-1.2E1", "-1.2e34", "-1.2E-0", "-1.2e+1", "-1.2e-34", "0E0", "0E1", "0e34", "0E-0", "0e+1", "0e-34", "-0E0", "-0E1", "-0e34", "-0E-0", "-0e+1", "-0e-34"},
Map: map[string]json.Number{
"a": json.Number("50.2"),
},
}

// This is not guaranteed to be symmetric
encoded, err := test.MarshalMsg(nil)
if err != nil {
t.Errorf("%v", err)
}
var v NumberJSONSample
_, err = v.UnmarshalMsg(encoded)
if err != nil {
t.Errorf("%v", err)
}
// Test two values
if v.Single != "-42" {
t.Errorf("want %v, got %v", "-42", v.Single)
}
if v.Map["a"] != "50.2" {
t.Errorf("want %v, got %v", "50.2", v.Map["a"])
}

var jsBuf bytes.Buffer
remain, err := msgp.UnmarshalAsJSON(&jsBuf, encoded)
if err != nil {
t.Errorf("%v", err)
}
if len(remain) != 0 {
t.Errorf("remain should be empty")
}
wantjs := `{"Single":-42,"Array":[0,0,1,-1,0.1,-0.1,1234,-1234,12.34,-12.34,12,120,120000000000000000000000000000000000,12,120,0.0000000000000000000000000000000012,-12,-120,-120000000000000000000000000000000000,-12,-120,-0.0000000000000000000000000000000012,1.2,12,12000000000000000000000000000000000,1.2,12,0.00000000000000000000000000000000012,-1.2,-12,-12000000000000000000000000000000000,-1.2,-12,-0.00000000000000000000000000000000012,0,0,0,0,0,0,-0,-0,-0,-0,-0,-0],"Map":{"a":50.2}}`
if jsBuf.String() != wantjs {
t.Errorf("jsBuf.String() = \n%s, want \n%s", jsBuf.String(), wantjs)
}
// Test encoding
var buf bytes.Buffer
en := msgp.NewWriter(&buf)
err = test.EncodeMsg(en)
if err != nil {
t.Errorf("%v", err)
}
en.Flush()
encoded = buf.Bytes()

dc := msgp.NewReader(&buf)
err = v.DecodeMsg(dc)
if err != nil {
t.Errorf("%v", err)
}
// Test two values
if v.Single != "-42" {
t.Errorf("want %s, got %s", "-42", v.Single)
}
if v.Map["a"] != "50.2" {
t.Errorf("want %s, got %s", "50.2", v.Map["a"])
}

jsBuf.Reset()
remain, err = msgp.UnmarshalAsJSON(&jsBuf, encoded)
if err != nil {
t.Errorf("%v", err)
}
if len(remain) != 0 {
t.Errorf("remain should be empty")
}
if jsBuf.String() != wantjs {
t.Errorf("jsBuf.String() = \n%s, want \n%s", jsBuf.String(), wantjs)
}

// Try interface encoder
jd := json.NewDecoder(&jsBuf)
jd.UseNumber()
var jsIntf map[string]any
err = jd.Decode(&jsIntf)
if err != nil {
t.Errorf("%v", err)
}
// Ensure we encode correctly
_ = (jsIntf["Single"]).(json.Number)

fromInt, err := msgp.AppendIntf(nil, jsIntf)
if err != nil {
t.Errorf("%v", err)
}

// Take the value from the JSON interface encoder and unmarshal back into our struct.
v = NumberJSONSample{}
_, err = v.UnmarshalMsg(fromInt)
if err != nil {
t.Errorf("%v", err)
}
if v.Single != "-42" {
t.Errorf("want %s, got %s", "-42", v.Single)
}
if v.Map["a"] != "50.2" {
t.Errorf("want %s, got %s", "50.2", v.Map["a"])
}
}
10 changes: 10 additions & 0 deletions _generated/replace.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package _generated

import "encoding/json"

//go:generate msgp
//msgp:replace Any with:any
//msgp:replace MapString with:CompatibleMapString
Expand Down Expand Up @@ -74,3 +76,11 @@ type (
String String
}
)

//msgp:replace json.Number with:string

type NumberJSONSampleReplace struct {
Single json.Number
Array []json.Number
Map map[string]json.Number
}
73 changes: 73 additions & 0 deletions _generated/replace_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package _generated

import (
"bytes"
"encoding/json"
"reflect"
"testing"
"time"

"github.com/tinylib/msgp/msgp"
)

func compareStructD(t *testing.T, a, b *CompatibleStructD) {
Expand Down Expand Up @@ -288,3 +293,71 @@ func TestReplace_Dummy(t *testing.T) {
t.Fatal("not same string")
}
}

func TestJSONNumberReplace(t *testing.T) {
test := NumberJSONSampleReplace{
Single: "-42",
Array: []json.Number{"0", "-0", "1", "-1", "0.1", "-0.1", "1234", "-1234", "12.34", "-12.34", "12E0", "12E1", "12e34", "12E-0", "12e+1", "12e-34", "-12E0", "-12E1", "-12e34", "-12E-0", "-12e+1", "-12e-34", "1.2E0", "1.2E1", "1.2e34", "1.2E-0", "1.2e+1", "1.2e-34", "-1.2E0", "-1.2E1", "-1.2e34", "-1.2E-0", "-1.2e+1", "-1.2e-34", "0E0", "0E1", "0e34", "0E-0", "0e+1", "0e-34", "-0E0", "-0E1", "-0e34", "-0E-0", "-0e+1", "-0e-34"},
Map: map[string]json.Number{
"a": json.Number("50"),
},
}

encoded, err := test.MarshalMsg(nil)
if err != nil {
t.Errorf("%v", err)
}
var v NumberJSONSampleReplace
_, err = v.UnmarshalMsg(encoded)
if err != nil {
t.Errorf("%v", err)
}
// Symmetric since we store strings.
if !reflect.DeepEqual(v, test) {
t.Fatalf("want %v, got %v", test, v)
}

var jsBuf bytes.Buffer
remain, err := msgp.UnmarshalAsJSON(&jsBuf, encoded)
if err != nil {
t.Errorf("%v", err)
}
if len(remain) != 0 {
t.Errorf("remain should be empty")
}
// Retains number formatting. Map order is random, though.
wantjs := `{"Single":"-42","Array":["0","-0","1","-1","0.1","-0.1","1234","-1234","12.34","-12.34","12E0","12E1","12e34","12E-0","12e+1","12e-34","-12E0","-12E1","-12e34","-12E-0","-12e+1","-12e-34","1.2E0","1.2E1","1.2e34","1.2E-0","1.2e+1","1.2e-34","-1.2E0","-1.2E1","-1.2e34","-1.2E-0","-1.2e+1","-1.2e-34","0E0","0E1","0e34","0E-0","0e+1","0e-34","-0E0","-0E1","-0e34","-0E-0","-0e+1","-0e-34"],"Map":{"a":"50"}}`
if jsBuf.String() != wantjs {
t.Errorf("jsBuf.String() = \n%s, want \n%s", jsBuf.String(), wantjs)
}
// Test encoding
var buf bytes.Buffer
en := msgp.NewWriter(&buf)
err = test.EncodeMsg(en)
if err != nil {
t.Errorf("%v", err)
}
en.Flush()
encoded = buf.Bytes()

dc := msgp.NewReader(&buf)
err = v.DecodeMsg(dc)
if err != nil {
t.Errorf("%v", err)
}
if !reflect.DeepEqual(v, test) {
t.Fatalf("want %v, got %v", test, v)
}

jsBuf.Reset()
remain, err = msgp.UnmarshalAsJSON(&jsBuf, encoded)
if err != nil {
t.Errorf("%v", err)
}
if len(remain) != 0 {
t.Errorf("remain should be empty")
}
if jsBuf.String() != wantjs {
t.Errorf("jsBuf.String() = \n%s, want \n%s", jsBuf.String(), wantjs)
}
}
20 changes: 15 additions & 5 deletions gen/elem.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,11 @@ const (
Int32
Int64
Bool
Intf // interface{}
Time // time.Time
Duration // time.Duration
Ext // extension
Intf // interface{}
Time // time.Time
Duration // time.Duration
Ext // extension
JsonNumber // json.Number

IDENT // IDENT means an unrecognized identifier
)
Expand Down Expand Up @@ -123,6 +124,7 @@ var primitives = map[string]Primitive{
"time.Time": Time,
"time.Duration": Duration,
"msgp.Extension": Ext,
"json.Number": JsonNumber,
}

// types built into the library
Expand Down Expand Up @@ -634,6 +636,9 @@ func (s *BaseElem) BaseName() string {
if s.Value == Duration {
return "Duration"
}
if s.Value == JsonNumber {
return "JSONNumber"
}
return s.Value.String()
}

Expand All @@ -652,6 +657,8 @@ func (s *BaseElem) BaseType() string {
return "time.Time"
case Duration:
return "time.Duration"
case JsonNumber:
return "json.Number"
case Ext:
return "msgp.Extension"

Expand Down Expand Up @@ -719,9 +726,10 @@ func (s *BaseElem) ZeroExpr() string {
return "0"
case Bool:
return "false"

case Time:
return "(time.Time{})"
case JsonNumber:
return "(json.Number{})"

}

Expand Down Expand Up @@ -783,6 +791,8 @@ func (k Primitive) String() string {
return "time.Duration"
case Ext:
return "Extension"
case JsonNumber:
return "json.Number"
case IDENT:
return "Ident"
default:
Expand Down
2 changes: 1 addition & 1 deletion gen/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func (m *marshalGen) gBase(b *BaseElem) {
case IDENT:
echeck = true
m.p.printf("\no, err = %s.MarshalMsg(o)", vname)
case Intf, Ext:
case Intf, Ext, JsonNumber:
echeck = true
m.p.printf("\no, err = msgp.Append%s(o, %s)", b.BaseName(), vname)
default:
Expand Down
39 changes: 39 additions & 0 deletions msgp/read.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package msgp

import (
"encoding/json"
"io"
"math"
"strconv"
"sync"
"time"

Expand Down Expand Up @@ -45,6 +47,7 @@ const (
Complex64Type
Complex128Type
TimeType
NumberType

_maxtype
)
Expand Down Expand Up @@ -74,6 +77,8 @@ func (t Type) String() string {
return "ext"
case NilType:
return "nil"
case NumberType:
return "number"
default:
return "<invalid>"
}
Expand Down Expand Up @@ -1276,6 +1281,40 @@ func (m *Reader) ReadTime() (t time.Time, err error) {
return
}

// ReadJSONNumber reads an integer or a float value and return as json.Number
func (m *Reader) ReadJSONNumber() (n json.Number, err error) {
t, err := m.NextType()
if err != nil {
return
}
switch t {
case IntType:
v, err := m.ReadInt64()
if err == nil {
return json.Number(strconv.FormatInt(v, 10)), nil
}
return "", err
case UintType:
v, err := m.ReadUint64()
if err == nil {
return json.Number(strconv.FormatUint(v, 10)), nil
}
return "", err
case Float32Type, Float64Type:
v, err := m.ReadFloat64()
if err == nil {
return json.Number(strconv.FormatFloat(v, 'f', -1, 64)), nil
}
return "", err
case StrType:
v, err := m.ReadString()
if err == nil {
return json.Number(v), nil
}
}
return "", TypeError{Method: NumberType, Encoded: t}
}

// ReadIntf reads out the next object as a raw interface{}/any.
// Arrays are decoded as []interface{}, and maps are decoded
// as map[string]interface{}. Integers are decoded as int64
Expand Down
Loading

0 comments on commit 01ec106

Please sign in to comment.