Skip to content

Commit

Permalink
Add native json.Number support (#364)
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/float32 is used. The zero value json.Number will be encoded as 0.

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

Fixes #292
  • Loading branch information
klauspost authored Oct 1, 2024
1 parent 10368af commit 128abdb
Show file tree
Hide file tree
Showing 11 changed files with 315 additions and 10 deletions.
8 changes: 8 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,10 @@ 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
OE json.Number `msg:",omitempty"`
}
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 `""`

}

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 @@ -306,7 +306,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
34 changes: 34 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,35 @@ 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
}
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 128abdb

Please sign in to comment.