Skip to content

Commit

Permalink
Store api as nested maps rather than method list
Browse files Browse the repository at this point in the history
  • Loading branch information
LLKennedy committed Nov 16, 2019
1 parent 5afaf72 commit 1f918b2
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 59 deletions.
25 changes: 15 additions & 10 deletions proxy/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,28 @@ import (
"strings"
)

func validateMethod(apiMethod reflect.Method, serverType reflect.Type) error {
func validateMethod(apiMethod reflect.Method, serverType reflect.Type) (methodType string, procedureName string, err error) {
name := apiMethod.Name
trueName, valid := matchAndStrip(name)
httpType, trueName, valid := matchAndStrip(name)
if !valid {
return fmt.Errorf("%s does not begin with a valid HTTP method", name)
err = fmt.Errorf("%s does not begin with a valid HTTP method", name)
return
}
serverMethod, found := serverType.MethodByName(trueName)
if !found {
return fmt.Errorf("server is missing method %s", trueName)
err = fmt.Errorf("server is missing method %s", trueName)
return
}
expectedType := apiMethod.Type
foundType := serverMethod.Type
err := validateArgs(expectedType, foundType)
err = validateArgs(expectedType, foundType)
if err != nil {
return fmt.Errorf("validation of %s to %s mapping: %v", name, trueName, err)
err = fmt.Errorf("validation of %s to %s mapping: %v", name, trueName, err)
return
}
return nil
methodType = httpType
procedureName = trueName
return
}

var httpStrings = []string{
Expand All @@ -38,13 +43,13 @@ var httpStrings = []string{
}

// matchAndStrip returns the method name stripped of its HTTP method and a success flag for that operation
func matchAndStrip(methodName string) (string, bool) {
func matchAndStrip(methodName string) (string, string, bool) {
for _, httpType := range httpStrings {
if matchInsensitive(methodName, httpType) {
return stripInsensitive(methodName, httpType), true
return httpType, stripInsensitive(methodName, httpType), true
}
}
return "", false
return "", "", false
}

// matchInsensitive returns true if methodName starts with httpType (case insensitive), is at least one character longer, and follows httpType with an uppercase letter (exported method)
Expand Down
22 changes: 14 additions & 8 deletions proxy/methods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,19 @@ func (u *unrelatedThing) Thing(x, y int) bool {

func Test_validateMethod(t *testing.T) {
tests := []struct {
name string
apiMethod reflect.Method
serverType reflect.Type
expectedErr string
name string
apiMethod reflect.Method
serverType reflect.Type
expectedErr string
expectedType string
expectedName string
}{
{
name: "matching",
apiMethod: reflect.TypeOf(&exposedThingA{}).Method(0),
serverType: reflect.TypeOf(&thingA{}),
name: "matching",
apiMethod: reflect.TypeOf(&exposedThingA{}).Method(0),
serverType: reflect.TypeOf(&thingA{}),
expectedType: "POST",
expectedName: "DoThing",
},
{
name: "no method at the start",
Expand All @@ -52,9 +56,11 @@ func Test_validateMethod(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateMethod(tt.apiMethod, tt.serverType)
gotType, gotName, err := validateMethod(tt.apiMethod, tt.serverType)
if tt.expectedErr == "" {
assert.NoError(t, err)
assert.Equal(t, tt.expectedType, gotType)
assert.Equal(t, tt.expectedName, gotName)
} else {
assert.EqualError(t, err, tt.expectedErr)
}
Expand Down
55 changes: 31 additions & 24 deletions proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,39 +3,46 @@ package proxy
import (
"context"
"fmt"
"reflect"

"github.com/LLKennedy/httpgrpc"
)

// Proxy proxies connections through the server
func (s *Server) Proxy(ctx context.Context, req *httpgrpc.Request) (*httpgrpc.Response, error) {
innerMethod, err := s.fetchInnerMethod(req.GetMethod(), req.GetProcedure())
wrapErr := func(err error) error {
return fmt.Errorf("httpgrpc: %v", err)
}
methodString, err := methodToString(req.GetMethod())
if err != nil {
return nil, err
return nil, wrapErr(err)
}
_ = innerMethod // use reflection to call this
_ = methodString
return (&httpgrpc.UnimplementedExposedServiceServer{}).Proxy(ctx, req)
}

func (s *Server) fetchInnerMethod(methodType httpgrpc.Method, name string) (reflect.Method, error) {
// innerName, valid := matchAndStrip(name)
// if !valid {
// return reflect.Method{}, fmt.Errorf("no HTTP method type prepending procedure name")
// }
// switch methodType {
// case httpgrpc.Method_UNKNOWN:
// return reflect.Method{}, fmt.Errorf("httpgrpc: unknown HTTP method")
// case httpgrpc.Method_GET:
// case httpgrpc.Method_HEAD:
// case httpgrpc.Method_POST:
// case httpgrpc.Method_PUT:
// case httpgrpc.Method_DELETE:
// case httpgrpc.Method_CONNECT:
// case httpgrpc.Method_OPTIONS:
// case httpgrpc.Method_TRACE:
// case httpgrpc.Method_PATCH:
// default:
return reflect.Method{}, fmt.Errorf("httpgrpc: unknown HTTP method")
// }
func methodToString(in httpgrpc.Method) (out string, err error) {
switch in {
case httpgrpc.Method_GET:
out = "GET"
case httpgrpc.Method_HEAD:
out = "HEAD"
case httpgrpc.Method_POST:
out = "POST"
case httpgrpc.Method_PUT:
out = "PUT"
case httpgrpc.Method_DELETE:
out = "DELETE"
case httpgrpc.Method_CONNECT:
out = "CONNECT"
case httpgrpc.Method_OPTIONS:
out = "OPTIONS"
case httpgrpc.Method_TRACE:
out = "TRACE"
case httpgrpc.Method_PATCH:
out = "PATCH"
}
if out == "" {
err = fmt.Errorf("unknown HTTP method")
}
return
}
8 changes: 4 additions & 4 deletions proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ var defaultServer = &Server{}
// Server is an HTTP to GRPC proxy server
type Server struct {
grpcServer *grpc.Server
api []reflect.Method // the api of innerServer
innerServer interface{} // the actual protobuf endpoints we want to use
api map[string]map[string]reflect.Method // the api of innerServer
innerServer interface{} // the actual protobuf endpoints we want to use
}

func (s *Server) getGrpcServer() *grpc.Server {
Expand All @@ -30,14 +30,14 @@ func (s *Server) setGrpcServer(in *grpc.Server) {
s.grpcServer = in
}

func (s *Server) getAPI() []reflect.Method {
func (s *Server) getAPI() map[string]map[string]reflect.Method {
if s == nil {
return defaultServer.api
}
return s.api
}

func (s *Server) setAPI(in []reflect.Method) {
func (s *Server) setAPI(in map[string]map[string]reflect.Method) {
if s == nil {
defaultServer.api = in
}
Expand Down
11 changes: 7 additions & 4 deletions proxy/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,20 @@ func (s *Server) setAPIConfig(api, server interface{}) (err error) {
}()
apiType := reflect.TypeOf(api)
serverType := reflect.TypeOf(server)
apiMethods := make([]reflect.Method, apiType.NumMethod())
apiMethods := map[string]map[string]reflect.Method{}
// Check every function defined on api
for i := range apiMethods {
for i := 0; i < apiType.NumMethod(); i++ {
// Each function in api must map exactly to an equivalent on server with the HTTP method stripped off
apiMethod := apiType.Method(i)
err := validateMethod(apiMethod, serverType)
methodString, procedureName, err := validateMethod(apiMethod, serverType)
if err != nil {
// one of the functions didn't match
return wrapErr(err)
}
apiMethods[i] = apiMethod
if _, exists := apiMethods[methodString]; !exists {
apiMethods[methodString] = map[string]reflect.Method{}
}
apiMethods[methodString][procedureName] = apiMethod
}
// We know all api functions map to server functions, now hold onto the method list and server pointer for later
s.setAPI(apiMethods)
Expand Down
22 changes: 13 additions & 9 deletions proxy/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestServer_setAPIConfig(t *testing.T) {
api: &emptyThing{},
server: &emptyThing{},
result: &Server{
api: []reflect.Method{},
api: map[string]map[string]reflect.Method{},
innerServer: &emptyThing{},
},
expectedErr: "",
Expand All @@ -36,8 +36,10 @@ func TestServer_setAPIConfig(t *testing.T) {
api: specificExposedThing,
server: &thingA{},
result: &Server{
api: []reflect.Method{
reflect.TypeOf(specificExposedThing).Method(0),
api: map[string]map[string]reflect.Method{
"POST": {
"DoThing": reflect.TypeOf(specificExposedThing).Method(0),
},
},
innerServer: &thingA{},
},
Expand All @@ -60,12 +62,14 @@ func TestServer_setAPIConfig(t *testing.T) {
assert.Equal(t, tt.result.grpcServer, tt.s.grpcServer)
assert.Equal(t, tt.result.innerServer, tt.s.innerServer)
if assert.Len(t, tt.s.api, len(tt.result.api)) {
for i, method := range tt.s.api {
expectedMethod := tt.result.api[i]
assert.Equal(t, method.Name, expectedMethod.Name)
assert.Equal(t, method.PkgPath, expectedMethod.PkgPath)
assert.Equal(t, method.Type, expectedMethod.Type)
assert.Equal(t, method.Index, expectedMethod.Index)
for key, inMap := range tt.s.api {
for procName, method := range inMap {
expectedMethod := tt.result.api[key][procName]
assert.Equal(t, method.Name, expectedMethod.Name)
assert.Equal(t, method.PkgPath, expectedMethod.PkgPath)
assert.Equal(t, method.Type, expectedMethod.Type)
assert.Equal(t, method.Index, expectedMethod.Index)
}
}
}
} else {
Expand Down

0 comments on commit 1f918b2

Please sign in to comment.