diff --git a/dispatch.go b/dispatch.go new file mode 100644 index 0000000..9973eed --- /dev/null +++ b/dispatch.go @@ -0,0 +1,17 @@ +package resolvers + +import "fmt" + +type dispatch struct { + repository *Repository +} + +func (d dispatch) Serve(in Invocation) (interface{}, error) { + handler, found := d.repository.resolvers[in.Resolve] + + if found { + return handler.call(in.payload()) + } + + return nil, fmt.Errorf("No resolver found: %s", in.Resolve) +} diff --git a/handle.go b/handle.go new file mode 100644 index 0000000..193b17b --- /dev/null +++ b/handle.go @@ -0,0 +1,15 @@ +package resolvers + +// Handler responds to requests in resolvers +type Handler interface { + Serve(Invocation) (interface{}, error) +} + +// The HandlerFunc type is an adapter to allow the use of +// ordinary functions +type HandlerFunc func(Invocation) (interface{}, error) + +// Serve calls from resolver +func (f HandlerFunc) Serve(in Invocation) (interface{}, error) { + return f(in) +} diff --git a/invocation.go b/invocation.go index 78b4515..6574cfb 100644 --- a/invocation.go +++ b/invocation.go @@ -2,21 +2,23 @@ package resolvers import "encoding/json" -type context struct { +// ContextData data received from AppSync +type ContextData struct { Arguments json.RawMessage `json:"arguments"` Source json.RawMessage `json:"source"` } -type invocation struct { - Resolve string `json:"resolve"` - Context context `json:"context"` +// Invocation data received from AppSync +type Invocation struct { + Resolve string `json:"resolve"` + Context ContextData `json:"context"` } -func (in invocation) isRoot() bool { +func (in Invocation) isRoot() bool { return in.Context.Source == nil || string(in.Context.Source) == "null" } -func (in invocation) payload() json.RawMessage { +func (in Invocation) payload() json.RawMessage { if in.isRoot() { return in.Context.Arguments } diff --git a/invocation_test.go b/invocation_test.go index c7ceccc..5e66ebd 100644 --- a/invocation_test.go +++ b/invocation_test.go @@ -9,9 +9,9 @@ import ( var _ = Describe("Invocation", func() { Context("With Arguments", func() { - data := invocation{ + data := Invocation{ Resolve: "exaple.resolver", - Context: context{ + Context: ContextData{ Arguments: json.RawMessage(`{ "foo": "bar" }`), }, } @@ -26,9 +26,9 @@ var _ = Describe("Invocation", func() { }) Context("With Source", func() { - data := invocation{ + data := Invocation{ Resolve: "exaple.resolver", - Context: context{ + Context: ContextData{ Source: json.RawMessage(`{ "bar": "foo" }`), }, } diff --git a/main.go b/main.go index 7e06c9d..fc872b1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package resolvers // New returns a new Repository with a list of resolver -func New() Repository { - return Repository{} +func New() *Repository { + r := &Repository{} + r.buildChain() + return r } diff --git a/middleware.go b/middleware.go new file mode 100644 index 0000000..9024fec --- /dev/null +++ b/middleware.go @@ -0,0 +1,14 @@ +package resolvers + +// Use appends middleware to repository +func (r *Repository) Use(middleware func(Handler) Handler) { + r.middleware = append(r.middleware, middleware) + r.buildChain() +} + +func (r *Repository) buildChain() { + r.handler = dispatch{repository: r} + for i := len(r.middleware) - 1; i >= 0; i-- { + r.handler = r.middleware[i](r.handler) + } +} diff --git a/middleware_test.go b/middleware_test.go new file mode 100644 index 0000000..720bf1a --- /dev/null +++ b/middleware_test.go @@ -0,0 +1,151 @@ +package resolvers + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +type graphQLError struct { + Type string `json:"error_type"` + Message string `json:"error_message"` + Data interface{} `json:"error_data"` +} + +func (e *graphQLError) Error() string { + return e.Message +} + +func newGraphQLError(t string, m string, d interface{}) *graphQLError { + return &graphQLError{ + Type: t, + Message: m, + Data: d, + } +} + +func sequence(ch chan string, seq ...string) bool { + for _, str := range seq { + if msg := <-ch; msg != str { + return false + } + } + return true +} + +var _ = Describe("Middleware", func() { + type arguments struct { + Bar string `json:"bar"` + } + type response struct { + Foo string + } + + Context("With no hijacking", func() { + ch := make(chan string, 10) + r := New() + r.Add("example.resolver", func(arg arguments) (response, error) { + ch <- "handler" + return response{"bar"}, nil + }) + r.Use(func(h Handler) Handler { + m := func(in Invocation) (interface{}, error) { + ch <- "before 1" + out, err := h.Serve(in) + ch <- "after 1" + return out, err + } + return HandlerFunc(m) + }) + r.Use(func(h Handler) Handler { + m := func(in Invocation) (interface{}, error) { + ch <- "before 2" + out, err := h.Serve(in) + ch <- "after 2" + return out, err + } + return HandlerFunc(m) + }) + res, err := r.Handle(Invocation{ + Resolve: "example.resolver", + Context: ContextData{ + Arguments: json.RawMessage(`{"bar":"foo"}`), + }, + }) + + It("Should not error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should have data", func() { + Expect(res.(response).Foo).To(Equal("bar")) + }) + + It("Should be in sequence", func() { + Expect( + sequence(ch, + "before 1", + "before 2", + "handler", + "after 2", + "after 1", + )).To(BeTrue()) + }) + }) + + Context("With custom error middleware", func() { + ch := make(chan string, 10) + r := New() + r.Add("example.resolver", func(arg arguments) (*response, error) { + ch <- "handler" + return nil, newGraphQLError("BAD_REQUEST", "Invalid type", response{"bar"}) + }) + r.Use(func(h Handler) Handler { + m := func(in Invocation) (interface{}, error) { + ch <- "before 1" + out, err := h.Serve(in) + ch <- "after 1" + return out, err + } + return HandlerFunc(m) + }) + r.Use(func(h Handler) Handler { + m := func(in Invocation) (interface{}, error) { + out, err := h.Serve(in) + if err != nil { + if errData, ok := err.(*graphQLError); ok { + return errData, nil + } + } + return out, err + } + return HandlerFunc(m) + }) + res, err := r.Handle(Invocation{ + Resolve: "example.resolver", + Context: ContextData{ + Arguments: json.RawMessage(`{"bar":"foo"}`), + }, + }) + + It("Should not error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + + It("Should have error data", func() { + Expect(res.(*graphQLError).Message).To(Equal("Invalid type")) + Expect(res.(*graphQLError).Type).To(Equal("BAD_REQUEST")) + Expect(res.(*graphQLError).Data.(response).Foo).To(Equal("bar")) + }) + + It("Should be in sequence", func() { + Expect( + sequence(ch, + "before 1", + "handler", + "after 1", + )).To(BeTrue()) + }) + }) +}) diff --git a/repository.go b/repository.go index 397b862..51f511e 100644 --- a/repository.go +++ b/repository.go @@ -1,31 +1,32 @@ package resolvers import ( - "fmt" "reflect" ) // Repository stores all resolvers -type Repository map[string]resolver +type Repository struct { + handler Handler + middleware []func(Handler) Handler + resolvers map[string]resolver +} // Add stores a new resolver -func (r Repository) Add(resolve string, handler interface{}) error { +func (r *Repository) Add(resolve string, handler interface{}) error { + if r.resolvers == nil { + r.resolvers = map[string]resolver{} + } + err := validators.run(reflect.TypeOf(handler)) if err == nil { - r[resolve] = resolver{handler} + r.resolvers[resolve] = resolver{handler} } return err } // Handle responds to the AppSync request -func (r Repository) Handle(in invocation) (interface{}, error) { - handler, found := r[in.Resolve] - - if found { - return handler.call(in.payload()) - } - - return nil, fmt.Errorf("No resolver found: %s", in.Resolve) +func (r *Repository) Handle(in Invocation) (interface{}, error) { + return r.handler.Serve(in) } diff --git a/repository_test.go b/repository_test.go index 0e20fa7..3ca3ac6 100644 --- a/repository_test.go +++ b/repository_test.go @@ -20,9 +20,9 @@ var _ = Describe("Repository", func() { r.Add("example.resolver.with.error", func(arg arguments) (response, error) { return response{"bar"}, errors.New("Has Error") }) Context("Matching invocation", func() { - res, err := r.Handle(invocation{ + res, err := r.Handle(Invocation{ Resolve: "example.resolver", - Context: context{ + Context: ContextData{ Arguments: json.RawMessage(`{"bar":"foo"}`), }, }) @@ -37,9 +37,9 @@ var _ = Describe("Repository", func() { }) Context("Matching invocation with error", func() { - _, err := r.Handle(invocation{ + _, err := r.Handle(Invocation{ Resolve: "example.resolver.with.error", - Context: context{ + Context: ContextData{ Arguments: json.RawMessage(`{"bar":"foo"}`), }, }) @@ -50,9 +50,9 @@ var _ = Describe("Repository", func() { }) Context("Matching invocation with invalid payload", func() { - _, err := r.Handle(invocation{ + _, err := r.Handle(Invocation{ Resolve: "example.resolver.with.error", - Context: context{ + Context: ContextData{ Arguments: json.RawMessage(`{"bar:foo"}`), }, }) @@ -63,9 +63,9 @@ var _ = Describe("Repository", func() { }) Context("Not matching invocation", func() { - res, err := r.Handle(invocation{ + res, err := r.Handle(Invocation{ Resolve: "example.resolver.not.found", - Context: context{ + Context: ContextData{ Arguments: json.RawMessage(`{"bar":"foo"}`), }, })