Skip to content

Commit acec622

Browse files
authored
Add a protomongo package which exports a Go MongoDB codec for encoding/decoding protobuf objects (with protobuf field numbers as keys). (dataform-co#542)
* Add a protomongo package which exports a Go MongoDB codec for encoding/decoding protobuf objects (with protobuf field numbers as keys). * cleanup and comments * lewis comments * add missing file
1 parent 4d1feee commit acec622

File tree

7 files changed

+403
-1
lines changed

7 files changed

+403
-1
lines changed

WORKSPACE

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ go_rules_dependencies()
8181

8282
go_register_toolchains()
8383

84-
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
84+
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository")
8585

8686
gazelle_dependencies()
8787

@@ -119,3 +119,24 @@ load(
119119
)
120120

121121
_nodejs_image_repos()
122+
123+
go_repository(
124+
name = "com_github_go_stack_stack",
125+
importpath = "github.com/go-stack/stack",
126+
sum = "h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=",
127+
version = "v1.8.0",
128+
)
129+
130+
go_repository(
131+
name = "com_github_golang_protobuf",
132+
importpath = "github.com/golang/protobuf",
133+
sum = "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=",
134+
version = "v1.3.2",
135+
)
136+
137+
go_repository(
138+
name = "org_mongodb_go_mongo_driver",
139+
importpath = "go.mongodb.org/mongo-driver",
140+
sum = "h1:6fhXjXSzzXRQdqtFKOI1CDw6Gw5x6VflovRpfbrlVi0=",
141+
version = "v1.2.0",
142+
)

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/dataform-co/dataform
2+
3+
go 1.13
4+
5+
require (
6+
github.com/go-stack/stack v1.8.0 // indirect
7+
github.com/golang/protobuf v1.3.2
8+
go.mongodb.org/mongo-driver v1.2.0
9+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
2+
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
3+
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
4+
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
5+
go.mongodb.org/mongo-driver v1.2.0 h1:6fhXjXSzzXRQdqtFKOI1CDw6Gw5x6VflovRpfbrlVi0=
6+
go.mongodb.org/mongo-driver v1.2.0/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=

protomongo/BUILD

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
2+
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
3+
4+
go_test(
5+
name = "go_default_test",
6+
srcs = ["protomongo_test.go"],
7+
embed = [":go_default_library"],
8+
deps = [
9+
":example_go_proto",
10+
"@com_github_golang_protobuf//proto:go_default_library",
11+
"@org_mongodb_go_mongo_driver//bson:go_default_library",
12+
],
13+
)
14+
15+
go_library(
16+
name = "go_default_library",
17+
srcs = ["protomongo.go"],
18+
importpath = "github.com/dataform-co/dataform/protomongo",
19+
visibility = ["//visibility:public"],
20+
deps = [
21+
"@com_github_golang_protobuf//descriptor:go_default_library_gen",
22+
"@com_github_golang_protobuf//proto:go_default_library",
23+
"@org_mongodb_go_mongo_driver//bson/bsoncodec:go_default_library",
24+
"@org_mongodb_go_mongo_driver//bson/bsonrw:go_default_library",
25+
],
26+
)
27+
28+
proto_library(
29+
name = "example_proto",
30+
testonly = 1,
31+
srcs = ["example.proto"],
32+
visibility = ["//visibility:public"],
33+
)
34+
35+
go_proto_library(
36+
name = "example_go_proto",
37+
testonly = 1,
38+
importpath = "github.com/dataform-co/dataform/protomongo/example",
39+
proto = ":example_proto",
40+
visibility = ["//visibility:public"],
41+
)

protomongo/example.proto

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
syntax="proto3";
2+
3+
package protomongo.example;
4+
5+
option go_package = "github.com/dataform-co/dataform/protomongo/example";
6+
7+
enum Enum {
8+
VAL_0 = 0;
9+
VAL_1 = 17;
10+
VAL_2 = 364;
11+
}
12+
13+
message SimpleMessage {
14+
string string_field = 1;
15+
int32 int32_field = 2;
16+
int64 int64_field = 3;
17+
float float_field = 4;
18+
double double_field = 5;
19+
bool bool_field = 6;
20+
Enum enum_field = 7;
21+
}
22+
23+
message RepeatedFieldMessage {
24+
repeated string string_field = 1;
25+
repeated int32 int32_field = 2;
26+
repeated int64 int64_field = 3;
27+
repeated float float_field = 4;
28+
repeated double double_field = 5;
29+
repeated bool bool_field = 6;
30+
repeated Enum enum_field = 7;
31+
}
32+
33+
message MessageWithSubMessage {
34+
string string_field = 1;
35+
SimpleMessage simple_message = 2;
36+
}
37+
38+
message MessageWithRepeatedSubMessage {
39+
string string_field = 1;
40+
repeated SimpleMessage simple_message = 2;
41+
}
42+
43+
message MessageWithOneof {
44+
string string_field = 1;
45+
oneof oneof_field {
46+
int32 int32_oneof_field = 2;
47+
int64 int64_oneof_field = 3;
48+
}
49+
}

protomongo/protomongo.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package protomongo
2+
3+
import (
4+
"reflect"
5+
"strconv"
6+
7+
"github.com/golang/protobuf/descriptor"
8+
"github.com/golang/protobuf/proto"
9+
"go.mongodb.org/mongo-driver/bson/bsoncodec"
10+
"go.mongodb.org/mongo-driver/bson/bsonrw"
11+
)
12+
13+
// ProtobufCodec is a MongoDB codec. It encodes protobuf objects using the protobuf field numbers as document
14+
// keys. This means that stored protobufs can survive normal protobuf definition changes, e.g. renaming a field.
15+
type ProtobufCodec struct {
16+
}
17+
18+
func (pc *ProtobufCodec) EncodeValue(ectx bsoncodec.EncodeContext, vw bsonrw.ValueWriter, val reflect.Value) error {
19+
oneofs := oneofNames(val.Interface().(descriptor.Message))
20+
for val.Kind() != reflect.Struct {
21+
val = val.Elem()
22+
}
23+
props := proto.GetProperties(val.Type())
24+
25+
dw, err := vw.WriteDocument()
26+
if err != nil {
27+
return err
28+
}
29+
30+
for _, prop := range props.Prop {
31+
fVal := val.FieldByName(prop.Name)
32+
if !fVal.IsZero() {
33+
// Figure out what tag and what value we are encoding.
34+
tag := prop.Tag
35+
if oneofs[prop.OrigName] {
36+
// If this field is a oneof, we need to get the single Go value stored inside its oneof struct,
37+
// instead of simply using the value as-is.
38+
oneof := fVal.Elem().Elem()
39+
singleProp := proto.GetProperties(oneof.Type()).Prop[0]
40+
tag = singleProp.Tag
41+
fVal = oneof.Field(0)
42+
}
43+
44+
// Actually encode the tag/value.
45+
fvw, err := dw.WriteDocumentElement(strconv.Itoa(tag))
46+
if err != nil {
47+
return err
48+
}
49+
enc, err := ectx.LookupEncoder(fVal.Type())
50+
if err != nil {
51+
return err
52+
}
53+
if err = enc.EncodeValue(ectx, fvw, fVal); err != nil {
54+
return err
55+
}
56+
}
57+
}
58+
59+
return dw.WriteDocumentEnd()
60+
}
61+
62+
func (pc *ProtobufCodec) DecodeValue(ectx bsoncodec.DecodeContext, vr bsonrw.ValueReader, val reflect.Value) error {
63+
oneofs := oneofNames(val.Interface().(descriptor.Message))
64+
if val.IsNil() {
65+
val.Set(reflect.New(val.Type().Elem()))
66+
}
67+
for val.Kind() != reflect.Struct {
68+
val = val.Elem()
69+
}
70+
props := proto.GetProperties(val.Type())
71+
72+
dr, err := vr.ReadDocument()
73+
if err != nil {
74+
return err
75+
}
76+
77+
indexedProps := make(map[string]*proto.Properties)
78+
indexedOneofProps := make(map[string]*proto.OneofProperties)
79+
for _, prop := range props.Prop {
80+
if !oneofs[prop.OrigName] {
81+
indexedProps[strconv.Itoa(prop.Tag)] = prop
82+
}
83+
}
84+
for _, oneof := range props.OneofTypes {
85+
indexedOneofProps[strconv.Itoa(oneof.Prop.Tag)] = oneof
86+
}
87+
88+
for f, fvr, err := dr.ReadElement(); err != bsonrw.ErrEOD; f, fvr, err = dr.ReadElement() {
89+
if err != nil {
90+
return err
91+
}
92+
93+
prop, isProp := indexedProps[f]
94+
oneof, isOneof := indexedOneofProps[f]
95+
96+
// Skip any field that we don't recognize.
97+
if !isProp && !isOneof {
98+
if err = vr.Skip(); err != nil {
99+
return err
100+
}
101+
continue
102+
}
103+
104+
// Figure out what field we need to decode into.
105+
var fVal reflect.Value
106+
if isProp {
107+
fVal = val.FieldByName(prop.Name)
108+
} else if isOneof {
109+
oneofVal := reflect.New(oneof.Type.Elem())
110+
val.Field(oneof.Field).Set(oneofVal)
111+
fVal = oneofVal.Elem().Field(0)
112+
}
113+
114+
// Actually decode the value.
115+
enc, err := ectx.LookupDecoder(fVal.Type())
116+
if err != nil {
117+
return err
118+
}
119+
if err = enc.DecodeValue(ectx, fvr, fVal); err != nil {
120+
return err
121+
}
122+
}
123+
124+
return nil
125+
}
126+
127+
func oneofNames(pb descriptor.Message) map[string]bool {
128+
_, msgDescriptor := descriptor.ForMessage(pb)
129+
names := make(map[string]bool)
130+
for _, oneof := range msgDescriptor.OneofDecl {
131+
names[*oneof.Name] = true
132+
}
133+
return names
134+
}

0 commit comments

Comments
 (0)