Skip to content

Commit 553c764

Browse files
committed
Generate XXX_WellKnownType method for recognised well-known types.
Just Duration and Timestamp for now. Make jsonpb recognise them and format/parse according to the spec.
1 parent 3c84672 commit 553c764

File tree

14 files changed

+293
-153
lines changed

14 files changed

+293
-153
lines changed

jsonpb/jsonpb.go

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
"sort"
4848
"strconv"
4949
"strings"
50+
"time"
5051

5152
"github.com/golang/protobuf/proto"
5253
)
@@ -98,12 +99,47 @@ func (s int32Slice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
9899

99100
// marshalObject writes a struct to the Writer.
100101
func (m *Marshaler) marshalObject(out *errWriter, v proto.Message, indent string) error {
102+
s := reflect.ValueOf(v).Elem()
103+
104+
// Handle well-known types.
105+
type wkt interface {
106+
XXX_WellKnownType() string
107+
}
108+
if wkt, ok := v.(wkt); ok {
109+
switch wkt.XXX_WellKnownType() {
110+
case "Duration":
111+
// "Generated output always contains 3, 6, or 9 fractional digits,
112+
// depending on required precision."
113+
s, ns := s.Field(0).Int(), s.Field(1).Int()
114+
d := time.Duration(s)*time.Second + time.Duration(ns)*time.Nanosecond
115+
x := fmt.Sprintf("%.9f", d.Seconds())
116+
x = strings.TrimSuffix(x, "000")
117+
x = strings.TrimSuffix(x, "000")
118+
out.write(`"`)
119+
out.write(x)
120+
out.write(`s"`)
121+
return out.err
122+
case "Timestamp":
123+
// "RFC 3339, where generated output will always be Z-normalized
124+
// and uses 3, 6 or 9 fractional digits."
125+
s, ns := s.Field(0).Int(), s.Field(1).Int()
126+
t := time.Unix(s, ns).UTC()
127+
// time.RFC3339Nano isn't exactly right (we need to get 3/6/9 fractional digits).
128+
x := t.Format("2006-01-02T15:04:05.000000000")
129+
x = strings.TrimSuffix(x, "000")
130+
x = strings.TrimSuffix(x, "000")
131+
out.write(`"`)
132+
out.write(x)
133+
out.write(`Z"`)
134+
return out.err
135+
}
136+
}
137+
101138
out.write("{")
102139
if m.Indent != "" {
103140
out.write("\n")
104141
}
105142

106-
s := reflect.ValueOf(v).Elem()
107143
firstField := true
108144
for i := 0; i < s.NumField(); i++ {
109145
value := s.Field(i)
@@ -385,6 +421,45 @@ func unmarshalValue(target reflect.Value, inputValue json.RawMessage) error {
385421
return unmarshalValue(target.Elem(), inputValue)
386422
}
387423

424+
// Handle well-known types.
425+
type wkt interface {
426+
XXX_WellKnownType() string
427+
}
428+
if wkt, ok := target.Addr().Interface().(wkt); ok {
429+
switch wkt.XXX_WellKnownType() {
430+
case "Duration":
431+
unq, err := strconv.Unquote(string(inputValue))
432+
if err != nil {
433+
return err
434+
}
435+
d, err := time.ParseDuration(unq)
436+
if err != nil {
437+
return fmt.Errorf("bad Duration: %v", err)
438+
}
439+
ns := d.Nanoseconds()
440+
s := ns / 1e9
441+
ns %= 1e9
442+
target.Field(0).SetInt(s)
443+
target.Field(1).SetInt(ns)
444+
return nil
445+
case "Timestamp":
446+
unq, err := strconv.Unquote(string(inputValue))
447+
if err != nil {
448+
return err
449+
}
450+
t, err := time.Parse(time.RFC3339Nano, unq)
451+
if err != nil {
452+
return fmt.Errorf("bad Timestamp: %v", err)
453+
}
454+
ns := t.UnixNano()
455+
s := ns / 1e9
456+
ns %= 1e9
457+
target.Field(0).SetInt(s)
458+
target.Field(1).SetInt(ns)
459+
return nil
460+
}
461+
}
462+
388463
// Handle nested messages.
389464
if targetType.Kind() == reflect.Struct {
390465
var jsonFields map[string]json.RawMessage

jsonpb/jsonpb_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ import (
3535
"reflect"
3636
"testing"
3737

38-
pb "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
3938
"github.com/golang/protobuf/proto"
39+
40+
pb "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
4041
proto3pb "github.com/golang/protobuf/proto/proto3_proto"
42+
durpb "github.com/golang/protobuf/ptypes/duration"
43+
tspb "github.com/golang/protobuf/ptypes/timestamp"
4144
)
4245

4346
var (
@@ -315,6 +318,9 @@ var marshalingTests = []struct {
315318
{"force orig_name", Marshaler{OrigName: true}, &pb.Simple{OInt32: proto.Int32(4)},
316319
`{"o_int32":4}`},
317320
{"proto2 extension", marshaler, realNumber, realNumberJSON},
321+
322+
{"Duration", marshaler, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}, `{"dur":"3.000s"}`},
323+
{"Timestamp", marshaler, &pb.KnownTypes{Ts: &tspb.Timestamp{Seconds: 14e8, Nanos: 21e6}}, `{"ts":"2014-05-13T16:53:20.021Z"}`},
318324
}
319325

320326
func TestMarshaling(t *testing.T) {
@@ -354,6 +360,9 @@ var unmarshalingTests = []struct {
354360
{"oneof", `{"salary":31000}`, &pb.MsgWithOneof{Union: &pb.MsgWithOneof_Salary{31000}}},
355361
{"orig_name input", `{"o_bool":true}`, &pb.Simple{OBool: proto.Bool(true)}},
356362
{"camelName input", `{"oBool":true}`, &pb.Simple{OBool: proto.Bool(true)}},
363+
364+
{"Duration", `{"dur":"3.000s"}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}},
365+
{"Timestamp", `{"ts":"2014-05-13T16:53:20.021Z"}`, &pb.KnownTypes{Ts: &tspb.Timestamp{Seconds: 14e8, Nanos: 21e6}}},
357366
}
358367

359368
func TestUnmarshaling(t *testing.T) {
@@ -363,7 +372,7 @@ func TestUnmarshaling(t *testing.T) {
363372

364373
err := UnmarshalString(tt.json, p)
365374
if err != nil {
366-
t.Error(err)
375+
t.Errorf("%s: %v", tt.desc, err)
367376
continue
368377
}
369378

jsonpb/jsonpb_test_proto/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@
3030
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3131

3232
regenerate:
33-
protoc --go_out=. *.proto
33+
protoc --go_out=Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration,Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp:. *.proto

jsonpb/jsonpb_test_proto/more_test_objects.pb.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jsonpb/jsonpb_test_proto/test_objects.pb.go

Lines changed: 80 additions & 47 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jsonpb/jsonpb_test_proto/test_objects.proto

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
syntax = "proto2";
3333

34+
import "google/protobuf/duration.proto";
35+
import "google/protobuf/timestamp.proto";
36+
3437
package jsonpb;
3538

3639
// Test message for holding primitive types.
@@ -108,3 +111,8 @@ message Complex {
108111
optional double imaginary = 1;
109112
extensions 100 to max;
110113
}
114+
115+
message KnownTypes {
116+
optional google.protobuf.Duration dur = 1;
117+
optional google.protobuf.Timestamp ts = 2;
118+
}

protoc-gen-go/generator/generator.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,13 @@ var methodNames = [...]string{
16521652
"Descriptor",
16531653
}
16541654

1655+
// Names of messages in the `google.protobuf` package for which
1656+
// we will generate XXX_WellKnownType methods.
1657+
var wellKnownTypes = map[string]bool{
1658+
"Duration": true,
1659+
"Timestamp": true,
1660+
}
1661+
16551662
// Generate the type and default constant definitions for this Descriptor.
16561663
func (g *Generator) generateMessage(message *Descriptor) {
16571664
// The full type name
@@ -1840,6 +1847,11 @@ func (g *Generator) generateMessage(message *Descriptor) {
18401847
}
18411848
g.P("func (*", ccTypeName, ") Descriptor() ([]byte, []int) { return fileDescriptor", g.file.index, ", []int{", strings.Join(indexes, ", "), "} }")
18421849
}
1850+
// TODO: Revisit the decision to use a XXX_WellKnownType method
1851+
// if we change proto.MessageName to work with multiple equivalents.
1852+
if message.file.GetPackage() == "google.protobuf" && wellKnownTypes[message.GetName()] {
1853+
g.P("func (*", ccTypeName, `) XXX_WellKnownType() string { return "`, message.GetName(), `" }`)
1854+
}
18431855

18441856
// Extension support methods
18451857
var hasExtensions, isMessageSet bool

0 commit comments

Comments
 (0)