Skip to content

Commit

Permalink
feat(mock): implement grpc protocol
Browse files Browse the repository at this point in the history
  • Loading branch information
zoncoen committed Oct 27, 2024
1 parent 903b1ce commit eaa5bf4
Show file tree
Hide file tree
Showing 25 changed files with 1,485 additions and 293 deletions.
8 changes: 8 additions & 0 deletions assert/nop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package assert

// Nop returns an assertion that does not assert anything.
func Nop() Assertion {
return AssertionFunc(func(v interface{}) error {
return nil
})
}
65 changes: 65 additions & 0 deletions internal/queryutil/query.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package queryutil

import (
"context"
"reflect"
"strings"
"sync"

query "github.com/zoncoen/query-go"
yamlextractor "github.com/zoncoen/query-go/extractor/yaml"
"google.golang.org/protobuf/types/dynamicpb"
)

var (
Expand All @@ -23,11 +27,72 @@ func Options() []query.Option {
[]query.Option{
query.ExtractByStructTag("yaml", "json"),
query.CustomExtractFunc(yamlextractor.MapSliceExtractFunc()),
query.CustomExtractFunc(dynamicpbExtractFunc()),
},
opts...,
)
}

func dynamicpbExtractFunc() func(query.ExtractFunc) query.ExtractFunc {
return func(f query.ExtractFunc) query.ExtractFunc {
return func(in reflect.Value) (reflect.Value, bool) {
v := in
if v.IsValid() && v.CanInterface() {
if msg, ok := v.Interface().(*dynamicpb.Message); ok {
return f(reflect.ValueOf(&keyExtractor{
v: msg,
}))
}
}
return f(in)
}
}
}

type keyExtractor struct {
v *dynamicpb.Message
}

// ExtractByKey implements the query.KeyExtractorContext interface.
func (e *keyExtractor) ExtractByKey(ctx context.Context, key string) (interface{}, bool) {
ci := query.IsCaseInsensitive(ctx)
if ci {
key = strings.ToLower(key)
}
fields := e.v.Descriptor().Fields()
for i := range fields.Len() {
f := fields.Get(i)
{
name := string(f.Name())
if ci {
name = strings.ToLower(name)
}
if name == key {
return e.v.Get(f).Interface(), true
}
}
{
name := f.TextName()
if ci {
name = strings.ToLower(name)
}
if name == key {
return e.v.Get(f).Interface(), true
}
}
if f.HasJSONName() {
name := f.JSONName()
if ci {
name = strings.ToLower(name)
}
if name == key {
return e.v.Get(f).Interface(), true
}
}
}
return nil, false
}

func AppendOptions(customOpts ...query.Option) {
m.Lock()
defer m.Unlock()
Expand Down
92 changes: 92 additions & 0 deletions internal/queryutil/query_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package queryutil

import (
"context"
"strings"
"testing"

"github.com/zoncoen/query-go"
"github.com/zoncoen/scenarigo/protocol/grpc/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/dynamicpb"
)

func TestKeyExtractor_ExtractByKey(t *testing.T) {
comp := proto.NewCompiler([]string{})
fds, err := comp.Compile(context.Background(), []string{"testdata/test.proto"})
if err != nil {
t.Fatal(err)
}
svc, err := fds.ResolveService("scenarigo.testdata.test.Test")
if err != nil {
t.Fatal(err)
}
method := svc.Methods().ByName("Echo")
if method == nil {
t.Fatal("failed to get method")
}
msg := dynamicpb.NewMessage(method.Input())
msg.Set(method.Input().Fields().ByName("message_id"), protoreflect.ValueOf("1"))

t.Run("success", func(t *testing.T) {
tests := map[string]struct {
key string
opts []query.Option
expect any
}{
"name": {
key: "message_id",
expect: "1",
},
"name (case insensitive)": {
key: "MESSAGE_ID",
opts: []query.Option{query.CaseInsensitive()},
expect: "1",
},
"json": {
key: "messageId",
expect: "1",
},
"json (case insensitive)": {
key: "messageid",
opts: []query.Option{query.CaseInsensitive()},
expect: "1",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
v, err := New(test.opts...).Key(test.key).Extract(msg)
if err != nil {
t.Fatal(err)
}
if got, expect := v, test.expect; got != expect {
t.Errorf("expect %v but got %v", expect, got)
}
})
}
})

t.Run("failure", func(t *testing.T) {
tests := map[string]struct {
key string
opts []query.Option
expect string
}{
"not found": {
key: "messageid",
expect: `".messageid" not found`,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
_, err := New(test.opts...).Key(test.key).Extract(msg)
if err == nil {
t.Fatal("no error")
}
if got, expect := err.Error(), test.expect; !strings.Contains(got, test.expect) {
t.Errorf("expect %q but got %q", expect, got)
}
})
}
})
}
17 changes: 17 additions & 0 deletions internal/queryutil/testdata/test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
syntax = "proto3";

package scenarigo.testdata.test;

service Test {
rpc Echo(EchoRequest) returns (EchoResponse) {};
}

message EchoRequest {
string message_id = 1;
string message_body = 2;
}

message EchoResponse {
string message_id = 1;
string message_body = 2;
}
34 changes: 34 additions & 0 deletions internal/yamlutil/grpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package yamlutil

import (
"encoding/hex"
"strings"
"unicode/utf8"

"google.golang.org/grpc/metadata"

"github.com/goccy/go-yaml"
)

func NewMDMarshaler(md metadata.MD) *MDMarshaler { return (*MDMarshaler)(&md) }

type MDMarshaler metadata.MD

func (m *MDMarshaler) MarshalYAML() ([]byte, error) {
mp := make(metadata.MD, len(*m))
for k, vs := range *m {
if !strings.HasSuffix(k, "-bin") {
mp[k] = vs
continue
}
s := make([]string, len(vs))
for i, v := range vs {
if !utf8.ValidString(v) {
v = hex.EncodeToString([]byte(v))
}
s[i] = v
}
mp[k] = s
}
return yaml.Marshal(mp)
}
Loading

0 comments on commit eaa5bf4

Please sign in to comment.