Skip to content

Commit

Permalink
Merge pull request #56 from zoncoen/protobuf
Browse files Browse the repository at this point in the history
feat(extractor/protobuf): add extractor for protobuf
  • Loading branch information
zoncoen authored Jan 25, 2024
2 parents 8941364 + a0d0024 commit a762b74
Show file tree
Hide file tree
Showing 11 changed files with 682 additions and 0 deletions.
1 change: 1 addition & 0 deletions extractor/protobuf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/bin
49 changes: 49 additions & 0 deletions extractor/protobuf/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export

.DEFAULT_GOAL := generate

UNAME_OS := $(shell uname -s)
UNAME_ARCH := $(shell uname -m)

BIN_DIR := $(CURDIR)/bin
PROTO_DIR := $(CURDIR)/testdata/proto
GEN_PB_DIR := $(CURDIR)/testdata/gen
GOBIN := $(BIN_DIR)
PATH := $(GOBIN):$(PATH)

$(BIN_DIR):
@mkdir -p $(BIN_DIR)

PROTOC := $(BIN_DIR)/protoc
PROTOC_VERSION := 25.1
PROTOC_OS := $(UNAME_OS)
ifeq "$(UNAME_OS)" "Darwin"
PROTOC_OS = osx
endif
PROTOC_ARCH := $(UNAME_ARCH)
ifeq "$(UNAME_ARCH)" "arm64"
PROTOC_ARCH = aarch_64
endif
PROTOC_ZIP := protoc-$(PROTOC_VERSION)-$(PROTOC_OS)-$(PROTOC_ARCH).zip
$(PROTOC): | $(BIN_DIR)
@curl -sSOL \
"https://github.com/protocolbuffers/protobuf/releases/download/v$(PROTOC_VERSION)/$(PROTOC_ZIP)"
@unzip -j -o $(PROTOC_ZIP) -d $(BIN_DIR) bin/protoc
@unzip -o $(PROTOC_ZIP) -d $(BIN_DIR) "include/*"
@rm -f $(PROTOC_ZIP)

PROTOC_GEN_GO := $(BIN_DIR)/protoc-gen-go
$(PROTOC_GEN_GO): | $(BIN_DIR)
@go install google.golang.org/protobuf/cmd/[email protected]

PROTOC_GEN_GO_GRPC := $(BIN_DIR)/protoc-gen-go-grpc
$(PROTOC_GEN_GO_GRPC): | $(BIN_DIR)
@go install google.golang.org/grpc/cmd/[email protected]

PROTOC_OPTION := -I$(PROTO_DIR)
PROTOC_GO_OPTION := --plugin=${PROTOC_GEN_GO} --go_out=$(GEN_PB_DIR) --go_opt=paths=source_relative
PROTOC_GO_GRPC_OPTION := --go-grpc_out=require_unimplemented_servers=false:$(GEN_PB_DIR) --go-grpc_opt=paths=source_relative
.PHONY: generate
generate: $(PROTOC) $(PROTOC_GEN_GO) $(PROTOC_GEN_GO_GRPC)
@mkdir -p $(GEN_PB_DIR)
@find $(PROTO_DIR) -name '*.proto' | xargs -P8 protoc $(PROTOC_OPTION) $(PROTOC_GO_OPTION) $(PROTOC_GO_GRPC_OPTION)
32 changes: 32 additions & 0 deletions extractor/protobuf/example_protobuf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package protobuf_test

import (
"fmt"
"log"

"github.com/zoncoen/query-go"

protobufextractor "github.com/zoncoen/query-go/extractor/protobuf"
testpb "github.com/zoncoen/query-go/extractor/protobuf/testdata/gen/testpb"
)

func ExampleExtractFunc() {
v := testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
}
q := query.New(
query.CustomExtractFunc(protobufextractor.ExtractFunc()),
query.CustomIsInlineStructFieldFunc(protobufextractor.OneofIsInlineStructFieldFunc()),
).Key("B").Key("bar_value")
got, err := q.Extract(v)
if err != nil {
log.Fatal(err)
}
fmt.Println(got)
// Output:
// yyy
}
15 changes: 15 additions & 0 deletions extractor/protobuf/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/zoncoen/query-go/extractor/protobuf

go 1.21.6

require (
github.com/zoncoen/query-go v1.3.0
github.com/zoncoen/query-go/extractor/protobuf/testdata/gen v0.0.0-00010101000000-000000000000
)

replace github.com/zoncoen/query-go/extractor/protobuf/testdata/gen => ./testdata/gen/

require (
github.com/pkg/errors v0.9.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)
8 changes: 8 additions & 0 deletions extractor/protobuf/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/zoncoen/query-go v1.3.0 h1:wL5e3jxP1l3mMueyrsnj7KOtAvSe1JNzW1FaLhD5rsQ=
github.com/zoncoen/query-go v1.3.0/go.mod h1:rbLi2EwarQtEJ5cNg9LFmAnleihBLsQSc1ow7vA8nms=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
84 changes: 84 additions & 0 deletions extractor/protobuf/protobuf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package protobuf

import (
"context"
"reflect"
"strings"

"github.com/zoncoen/query-go"
)

// ExtractFunc is a function for query.CustomExtractFunc option to extract values by protobuf struct tag.
func ExtractFunc() func(query.ExtractFunc) query.ExtractFunc {
return func(f query.ExtractFunc) query.ExtractFunc {
return func(in reflect.Value) (reflect.Value, bool) {
v := in
for {
if v.IsValid() {
if k := v.Kind(); k == reflect.Interface || k == reflect.Pointer {
v = v.Elem()
continue
}
}
break
}
switch v.Kind() {
case reflect.Struct:
for i := 0; i < v.Type().NumField(); i++ {
field := v.Type().FieldByIndex([]int{i})
if s := field.Tag.Get("protobuf"); s != "" {
if v, found := f(reflect.ValueOf(&keyExtractor{v})); found {
return v, true
}
}
}
}
return f(in)
}
}
}

type keyExtractor struct {
v reflect.Value
}

// ExtractByKey implements KeyExtractorContext interface.
func (e *keyExtractor) ExtractByKey(ctx context.Context, key string) (any, bool) {
ci := query.IsCaseInsensitive(ctx)
if ci {
key = strings.ToLower(key)
}
switch e.v.Kind() {
case reflect.Struct:
for i := 0; i < e.v.Type().NumField(); i++ {
if s := e.v.Type().FieldByIndex([]int{i}).Tag.Get("protobuf"); s != "" {
for _, opt := range strings.Split(s, ",") {
kv := strings.Split(opt, "=")
if len(kv) == 2 {
k, v := kv[0], kv[1]
if k == "name" {
if ci {
v = strings.ToLower(v)
}
if v == key {
var resp any
if field := e.v.Field(i); field.CanInterface() {
resp = field.Interface()
}
return resp, true
}
}
}
}
}
}
}
return nil, false
}

// OneofIsInlineStructFieldFunc is a function for query.CustomIsInlineStructFieldFunc option to enable extracting values even if the oneof field name is omitted.
func OneofIsInlineStructFieldFunc() func(reflect.StructField) bool {
return func(f reflect.StructField) bool {
return f.Tag.Get("protobuf_oneof") != ""
}
}
141 changes: 141 additions & 0 deletions extractor/protobuf/protobuf_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package protobuf

import (
"strings"
"testing"

"github.com/zoncoen/query-go"
testpb "github.com/zoncoen/query-go/extractor/protobuf/testdata/gen/testpb"
)

func TestExtractFunc(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := map[string]struct {
query *query.Query
v any
expect any
}{
"by field name": {
query: query.New(
query.CustomExtractFunc(ExtractFunc()),
).Key("Value").Key("B").Key("BarValue"),
v: testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
},
expect: "yyy",
},
"by struct tag": {
query: query.New(
query.CustomExtractFunc(ExtractFunc()),
).Key("Value").Key("B").Key("bar_value"),
v: testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
},
expect: "yyy",
},
"by struct tag (case insensitive)": {
query: query.New(
query.CaseInsensitive(),
query.CustomExtractFunc(ExtractFunc()),
).Key("Value").Key("B").Key("BAR_VALUE"),
v: testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
},
expect: "yyy",
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
got, err := test.query.Extract(test.v)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if got != test.expect {
t.Errorf("expect %v but got %v", test.expect, got)
}
})
}
})
t.Run("failure", func(t *testing.T) {
tests := map[string]struct {
query *query.Query
v any
expect string
}{
"not found": {
query: query.New(
query.CustomExtractFunc(ExtractFunc()),
).Key("Value").Key("B").Key("BAR_VALUE"),
v: testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
},
expect: `".Value.B.BAR_VALUE" not found`,
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
_, err := test.query.Extract(test.v)
if err == nil {
t.Fatal("no error")
}
if got := err.Error(); !strings.Contains(got, test.expect) {
t.Errorf("expect %v but got %v", test.expect, got)
}
})
}
})
}

func TestOneofIsInlineStructFieldFunc(t *testing.T) {
t.Run("success", func(t *testing.T) {
tests := map[string]struct {
query *query.Query
v any
expect any
}{
"omit .Value": {
query: query.New(
query.CustomIsInlineStructFieldFunc(OneofIsInlineStructFieldFunc()),
).Key("B").Key("BarValue"),
v: testpb.OneofMessage{
Value: &testpb.OneofMessage_B_{
B: &testpb.OneofMessage_B{
BarValue: "yyy",
},
},
},
expect: "yyy",
},
}
for name, test := range tests {
test := test
t.Run(name, func(t *testing.T) {
got, err := test.query.Extract(test.v)
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if got != test.expect {
t.Errorf("expect %v but got %v", test.expect, got)
}
})
}
})
}
5 changes: 5 additions & 0 deletions extractor/protobuf/testdata/gen/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/zoncoen/query-go/extractor/protobuf/testdata/gen

go 1.21.6

require google.golang.org/protobuf v1.32.0
6 changes: 6 additions & 0 deletions extractor/protobuf/testdata/gen/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
Loading

0 comments on commit a762b74

Please sign in to comment.