mirror of
https://github.com/protocolbuffers/protobuf-go.git
synced 2025-01-30 12:32:36 +00:00
encoding/jsonpb: add support for unmarshaling Duration and Timestamp
Change-Id: Ia8319ed82d1d031e344ad7b095df2018286dcd43 Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/169698 Reviewed-by: Joe Tsai <thebrokentoaster@gmail.com>
This commit is contained in:
parent
61e93c70a2
commit
c4450377bd
@ -1531,6 +1531,126 @@ func TestUnmarshal(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Duration empty string",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `""`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Duration with secs",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"3s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: 3},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with escaped unicode",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"\u0033s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: 3},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with -secs",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"-3s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: -3},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with nanos",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"0.001s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Nanos: 1e6},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with -nanos",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"-0.001s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Nanos: -1e6},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with -secs -nanos",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"-123.000000450s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: -123, Nanos: -450},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with large secs",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"10000000000.000000001s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: 1e10, Nanos: 1},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with decimal without fractional",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"3.s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Seconds: 3},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with decimal without integer",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"0.5s"`,
|
||||||
|
wantMessage: &knownpb.Duration{Nanos: 5e8},
|
||||||
|
}, {
|
||||||
|
desc: "Duration with +secs out of range",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"315576000001s"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Duration with -secs out of range",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"-315576000001s"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Duration with nanos beyond 9 digits",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"0.9999999990s"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Duration without suffix s",
|
||||||
|
inputMessage: &knownpb.Duration{},
|
||||||
|
inputText: `"123"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp zero",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"1970-01-01T00:00:00Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp with tz adjustment",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"1970-01-01T00:00:00+01:00"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: -3600},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp UTC",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"2019-03-19T23:03:21Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: 1553036601},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp with escaped unicode",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"2019-0\u0033-19T23:03:21Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: 1553036601},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp with nanos",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"2019-03-19T23:03:21.000000001Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: 1553036601, Nanos: 1},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp upper limit",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"9999-12-31T23:59:59.999999999Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: 253402300799, Nanos: 999999999},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp above upper limit",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"9999-12-31T23:59:59-01:00"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp lower limit",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"0001-01-01T00:00:00Z"`,
|
||||||
|
wantMessage: &knownpb.Timestamp{Seconds: -62135596800},
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp below lower limit",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"0001-01-01T00:00:00+01:00"`,
|
||||||
|
wantErr: true,
|
||||||
|
}, {
|
||||||
|
desc: "Timestamp with nanos beyond 9 digits",
|
||||||
|
inputMessage: &knownpb.Timestamp{},
|
||||||
|
inputText: `"1970-01-01T00:00:00.0000000001Z"`,
|
||||||
|
wantErr: true,
|
||||||
}, {
|
}, {
|
||||||
desc: "FieldMask empty",
|
desc: "FieldMask empty",
|
||||||
inputMessage: &knownpb.FieldMask{},
|
inputMessage: &knownpb.FieldMask{},
|
||||||
|
@ -5,7 +5,10 @@
|
|||||||
package jsonpb
|
package jsonpb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -90,10 +93,8 @@ func (e encoder) marshalCustomType(m pref.Message) error {
|
|||||||
func (d decoder) unmarshalCustomType(m pref.Message) error {
|
func (d decoder) unmarshalCustomType(m pref.Message) error {
|
||||||
name := m.Type().FullName()
|
name := m.Type().FullName()
|
||||||
switch name {
|
switch name {
|
||||||
case "google.protobuf.Any",
|
case "google.protobuf.Any":
|
||||||
"google.protobuf.Duration",
|
panic("unmarshaling of google.protobuf.Any is not implemented yet")
|
||||||
"google.protobuf.Timestamp":
|
|
||||||
panic(fmt.Sprintf("unmarshaling of %v is not implemented yet", name))
|
|
||||||
|
|
||||||
case "google.protobuf.BoolValue",
|
case "google.protobuf.BoolValue",
|
||||||
"google.protobuf.DoubleValue",
|
"google.protobuf.DoubleValue",
|
||||||
@ -115,6 +116,12 @@ func (d decoder) unmarshalCustomType(m pref.Message) error {
|
|||||||
case "google.protobuf.Value":
|
case "google.protobuf.Value":
|
||||||
return d.unmarshalKnownValue(m)
|
return d.unmarshalKnownValue(m)
|
||||||
|
|
||||||
|
case "google.protobuf.Duration":
|
||||||
|
return d.unmarshalDuration(m)
|
||||||
|
|
||||||
|
case "google.protobuf.Timestamp":
|
||||||
|
return d.unmarshalTimestamp(m)
|
||||||
|
|
||||||
case "google.protobuf.FieldMask":
|
case "google.protobuf.FieldMask":
|
||||||
return d.unmarshalFieldMask(m)
|
return d.unmarshalFieldMask(m)
|
||||||
}
|
}
|
||||||
@ -417,6 +424,91 @@ func (e encoder) marshalDuration(m pref.Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d decoder) unmarshalDuration(m pref.Message) error {
|
||||||
|
var nerr errors.NonFatal
|
||||||
|
jval, err := d.Read()
|
||||||
|
if !nerr.Merge(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if jval.Type() != json.String {
|
||||||
|
return unexpectedJSONError{jval}
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType := m.Type()
|
||||||
|
input := jval.String()
|
||||||
|
secs, nanos, ok := parseDuration(input)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("%s: invalid duration value %q", msgType.FullName(), input)
|
||||||
|
}
|
||||||
|
// Validate seconds. No need to validate nanos because parseDuration would
|
||||||
|
// have covered that already.
|
||||||
|
if secs < -maxSecondsInDuration || secs > maxSecondsInDuration {
|
||||||
|
return errors.New("%s: out of range %q", msgType.FullName(), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
knownFields := m.KnownFields()
|
||||||
|
knownFields.Set(fieldnum.Duration_Seconds, pref.ValueOf(secs))
|
||||||
|
knownFields.Set(fieldnum.Duration_Nanos, pref.ValueOf(nanos))
|
||||||
|
return nerr.E
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular expression for Duration type in JSON format. This allows for values
|
||||||
|
// like 1s, 0.1s, 1.s, .1s. It limits fractional part to 9 digits only for
|
||||||
|
// nanoseconds precision, regardless of whether there are trailing zero digits.
|
||||||
|
var durationRE = regexp.MustCompile(`^-?([0-9]|[1-9][0-9]+)?(\.[0-9]{0,9})?s$`)
|
||||||
|
|
||||||
|
func parseDuration(input string) (int64, int32, bool) {
|
||||||
|
b := []byte(input)
|
||||||
|
// TODO: Parse input directly instead of using a regular expression.
|
||||||
|
matched := durationRE.FindSubmatch(b)
|
||||||
|
if len(matched) != 3 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var neg bool
|
||||||
|
if b[0] == '-' {
|
||||||
|
neg = true
|
||||||
|
}
|
||||||
|
var secb []byte
|
||||||
|
if len(matched[1]) == 0 {
|
||||||
|
secb = []byte{'0'}
|
||||||
|
} else {
|
||||||
|
secb = matched[1]
|
||||||
|
}
|
||||||
|
var nanob []byte
|
||||||
|
if len(matched[2]) <= 1 {
|
||||||
|
nanob = []byte{'0'}
|
||||||
|
} else {
|
||||||
|
nanob = matched[2][1:]
|
||||||
|
// Right-pad with 0s for nanosecond-precision.
|
||||||
|
for i := len(nanob); i < 9; i++ {
|
||||||
|
nanob = append(nanob, '0')
|
||||||
|
}
|
||||||
|
// Remove unnecessary 0s in the left.
|
||||||
|
nanob = bytes.TrimLeft(nanob, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
secs, err := strconv.ParseInt(string(secb), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
nanos, err := strconv.ParseInt(string(nanob), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if neg {
|
||||||
|
if secs > 0 {
|
||||||
|
secs = -secs
|
||||||
|
}
|
||||||
|
if nanos > 0 {
|
||||||
|
nanos = -nanos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secs, int32(nanos), true
|
||||||
|
}
|
||||||
|
|
||||||
// The JSON representation for a Timestamp is a JSON string in the RFC 3339
|
// The JSON representation for a Timestamp is a JSON string in the RFC 3339
|
||||||
// format, i.e. "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where
|
// format, i.e. "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z" where
|
||||||
// {year} is always expressed using four digits while {month}, {day}, {hour},
|
// {year} is always expressed using four digits while {month}, {day}, {hour},
|
||||||
@ -460,6 +552,35 @@ func (e encoder) marshalTimestamp(m pref.Message) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d decoder) unmarshalTimestamp(m pref.Message) error {
|
||||||
|
var nerr errors.NonFatal
|
||||||
|
jval, err := d.Read()
|
||||||
|
if !nerr.Merge(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if jval.Type() != json.String {
|
||||||
|
return unexpectedJSONError{jval}
|
||||||
|
}
|
||||||
|
|
||||||
|
msgType := m.Type()
|
||||||
|
input := jval.String()
|
||||||
|
t, err := time.Parse(time.RFC3339Nano, input)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("%s: invalid timestamp value %q", msgType.FullName(), input)
|
||||||
|
}
|
||||||
|
// Validate seconds. No need to validate nanos because time.Parse would have
|
||||||
|
// covered that already.
|
||||||
|
secs := t.Unix()
|
||||||
|
if secs < minTimestampSeconds || secs > maxTimestampSeconds {
|
||||||
|
return errors.New("%s: out of range %q", msgType.FullName(), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
knownFields := m.KnownFields()
|
||||||
|
knownFields.Set(fieldnum.Timestamp_Seconds, pref.ValueOf(secs))
|
||||||
|
knownFields.Set(fieldnum.Timestamp_Nanos, pref.ValueOf(int32(t.Nanosecond())))
|
||||||
|
return nerr.E
|
||||||
|
}
|
||||||
|
|
||||||
// The JSON representation for a FieldMask is a JSON string where paths are
|
// The JSON representation for a FieldMask is a JSON string where paths are
|
||||||
// separated by a comma. Fields name in each path are converted to/from
|
// separated by a comma. Fields name in each path are converted to/from
|
||||||
// lower-camel naming conventions. Encoding should fail if the path name would
|
// lower-camel naming conventions. Encoding should fail if the path name would
|
||||||
|
Loading…
x
Reference in New Issue
Block a user