Skip to content

Commit

Permalink
Generate XXX_WellKnownType method for recognised well-known types.
Browse files Browse the repository at this point in the history
Just Duration and Timestamp for now.

Make jsonpb recognise them and format/parse according to the spec.
  • Loading branch information
dsymonds committed Feb 25, 2016
1 parent 3c84672 commit 553c764
Show file tree
Hide file tree
Showing 14 changed files with 293 additions and 153 deletions.
77 changes: 76 additions & 1 deletion jsonpb/jsonpb.go
Expand Up @@ -47,6 +47,7 @@ import (
"sort"
"strconv"
"strings"
"time"

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

// marshalObject writes a struct to the Writer.
func (m *Marshaler) marshalObject(out *errWriter, v proto.Message, indent string) error {
s := reflect.ValueOf(v).Elem()

// Handle well-known types.
type wkt interface {
XXX_WellKnownType() string
}
if wkt, ok := v.(wkt); ok {
switch wkt.XXX_WellKnownType() {
case "Duration":
// "Generated output always contains 3, 6, or 9 fractional digits,
// depending on required precision."
s, ns := s.Field(0).Int(), s.Field(1).Int()
d := time.Duration(s)*time.Second + time.Duration(ns)*time.Nanosecond
x := fmt.Sprintf("%.9f", d.Seconds())
x = strings.TrimSuffix(x, "000")
x = strings.TrimSuffix(x, "000")
out.write(`"`)
out.write(x)
out.write(`s"`)
return out.err
case "Timestamp":
// "RFC 3339, where generated output will always be Z-normalized
// and uses 3, 6 or 9 fractional digits."
s, ns := s.Field(0).Int(), s.Field(1).Int()
t := time.Unix(s, ns).UTC()
// time.RFC3339Nano isn't exactly right (we need to get 3/6/9 fractional digits).
x := t.Format("2006-01-02T15:04:05.000000000")
x = strings.TrimSuffix(x, "000")
x = strings.TrimSuffix(x, "000")
out.write(`"`)
out.write(x)
out.write(`Z"`)
return out.err
}
}

out.write("{")
if m.Indent != "" {
out.write("\n")
}

s := reflect.ValueOf(v).Elem()
firstField := true
for i := 0; i < s.NumField(); i++ {
value := s.Field(i)
Expand Down Expand Up @@ -385,6 +421,45 @@ func unmarshalValue(target reflect.Value, inputValue json.RawMessage) error {
return unmarshalValue(target.Elem(), inputValue)
}

// Handle well-known types.
type wkt interface {
XXX_WellKnownType() string
}
if wkt, ok := target.Addr().Interface().(wkt); ok {
switch wkt.XXX_WellKnownType() {
case "Duration":
unq, err := strconv.Unquote(string(inputValue))
if err != nil {
return err
}
d, err := time.ParseDuration(unq)
if err != nil {
return fmt.Errorf("bad Duration: %v", err)
}
ns := d.Nanoseconds()
s := ns / 1e9
ns %= 1e9
target.Field(0).SetInt(s)
target.Field(1).SetInt(ns)
return nil
case "Timestamp":
unq, err := strconv.Unquote(string(inputValue))
if err != nil {
return err
}
t, err := time.Parse(time.RFC3339Nano, unq)
if err != nil {
return fmt.Errorf("bad Timestamp: %v", err)
}
ns := t.UnixNano()
s := ns / 1e9
ns %= 1e9
target.Field(0).SetInt(s)
target.Field(1).SetInt(ns)
return nil
}
}

// Handle nested messages.
if targetType.Kind() == reflect.Struct {
var jsonFields map[string]json.RawMessage
Expand Down
13 changes: 11 additions & 2 deletions jsonpb/jsonpb_test.go
Expand Up @@ -35,9 +35,12 @@ import (
"reflect"
"testing"

pb "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
"github.com/golang/protobuf/proto"

pb "github.com/golang/protobuf/jsonpb/jsonpb_test_proto"
proto3pb "github.com/golang/protobuf/proto/proto3_proto"
durpb "github.com/golang/protobuf/ptypes/duration"
tspb "github.com/golang/protobuf/ptypes/timestamp"
)

var (
Expand Down Expand Up @@ -315,6 +318,9 @@ var marshalingTests = []struct {
{"force orig_name", Marshaler{OrigName: true}, &pb.Simple{OInt32: proto.Int32(4)},
`{"o_int32":4}`},
{"proto2 extension", marshaler, realNumber, realNumberJSON},

{"Duration", marshaler, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}, `{"dur":"3.000s"}`},
{"Timestamp", marshaler, &pb.KnownTypes{Ts: &tspb.Timestamp{Seconds: 14e8, Nanos: 21e6}}, `{"ts":"2014-05-13T16:53:20.021Z"}`},
}

func TestMarshaling(t *testing.T) {
Expand Down Expand Up @@ -354,6 +360,9 @@ var unmarshalingTests = []struct {
{"oneof", `{"salary":31000}`, &pb.MsgWithOneof{Union: &pb.MsgWithOneof_Salary{31000}}},
{"orig_name input", `{"o_bool":true}`, &pb.Simple{OBool: proto.Bool(true)}},
{"camelName input", `{"oBool":true}`, &pb.Simple{OBool: proto.Bool(true)}},

{"Duration", `{"dur":"3.000s"}`, &pb.KnownTypes{Dur: &durpb.Duration{Seconds: 3}}},
{"Timestamp", `{"ts":"2014-05-13T16:53:20.021Z"}`, &pb.KnownTypes{Ts: &tspb.Timestamp{Seconds: 14e8, Nanos: 21e6}}},
}

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

err := UnmarshalString(tt.json, p)
if err != nil {
t.Error(err)
t.Errorf("%s: %v", tt.desc, err)
continue
}

Expand Down
2 changes: 1 addition & 1 deletion jsonpb/jsonpb_test_proto/Makefile
Expand Up @@ -30,4 +30,4 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

regenerate:
protoc --go_out=. *.proto
protoc --go_out=Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration,Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp:. *.proto
1 change: 1 addition & 0 deletions jsonpb/jsonpb_test_proto/more_test_objects.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

127 changes: 80 additions & 47 deletions jsonpb/jsonpb_test_proto/test_objects.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions jsonpb/jsonpb_test_proto/test_objects.proto
Expand Up @@ -31,6 +31,9 @@

syntax = "proto2";

import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";

package jsonpb;

// Test message for holding primitive types.
Expand Down Expand Up @@ -108,3 +111,8 @@ message Complex {
optional double imaginary = 1;
extensions 100 to max;
}

message KnownTypes {
optional google.protobuf.Duration dur = 1;
optional google.protobuf.Timestamp ts = 2;
}
12 changes: 12 additions & 0 deletions protoc-gen-go/generator/generator.go
Expand Up @@ -1652,6 +1652,13 @@ var methodNames = [...]string{
"Descriptor",
}

// Names of messages in the `google.protobuf` package for which
// we will generate XXX_WellKnownType methods.
var wellKnownTypes = map[string]bool{
"Duration": true,
"Timestamp": true,
}

// Generate the type and default constant definitions for this Descriptor.
func (g *Generator) generateMessage(message *Descriptor) {
// The full type name
Expand Down Expand Up @@ -1840,6 +1847,11 @@ func (g *Generator) generateMessage(message *Descriptor) {
}
g.P("func (*", ccTypeName, ") Descriptor() ([]byte, []int) { return fileDescriptor", g.file.index, ", []int{", strings.Join(indexes, ", "), "} }")
}
// TODO: Revisit the decision to use a XXX_WellKnownType method
// if we change proto.MessageName to work with multiple equivalents.
if message.file.GetPackage() == "google.protobuf" && wellKnownTypes[message.GetName()] {
g.P("func (*", ccTypeName, `) XXX_WellKnownType() string { return "`, message.GetName(), `" }`)
}

// Extension support methods
var hasExtensions, isMessageSet bool
Expand Down

0 comments on commit 553c764

Please sign in to comment.