From a84a71babb48959d6c4e357bd1382baf79d1eb77 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Mon, 16 Jul 2018 17:32:50 -0700 Subject: [PATCH 01/18] Added body to default APIGRequest fields --- router.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/router.go b/router.go index d1514d0..b7f871c 100644 --- a/router.go +++ b/router.go @@ -21,6 +21,7 @@ const ( // The Claims, Path, and QryStr will be populated by the the APIGatewayProxyRequest. // The Request itself is also passed through if you need further access. type APIGRequest struct { + Body string Claims map[string]interface{} Path map[string]string QryStr map[string]string @@ -124,6 +125,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { for _, handler := range handlers { req := &APIGRequest{ + Body: r.request.Body, Path: r.request.PathParameters, QryStr: r.request.QueryStringParameters, Request: r.request, From e4134e6fd9fa5c23f569e9b87da7c18f55e04de4 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Tue, 17 Jul 2018 14:01:18 -0700 Subject: [PATCH 02/18] Modified APIGHanlder function to only take one parameter that servces as both request and response: APIGContext --- router.go | 36 +++++++++++++++--------------------- router_test.go | 48 ++++++++++++++++++++++++------------------------ 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/router.go b/router.go index b7f871c..4bed5f3 100644 --- a/router.go +++ b/router.go @@ -17,28 +17,23 @@ const ( delete = http.MethodDelete ) -// APIGRequest is used as the input of handler functions. -// The Claims, Path, and QryStr will be populated by the the APIGatewayProxyRequest. +// APIGContext is used as the input and output of handler functions. +// The Body, Claims, Path, and QryStr will be populated by the the APIGatewayProxyRequest. // The Request itself is also passed through if you need further access. -type APIGRequest struct { - Body string +// Fill the Status and Body, or Status and Error to respond. +type APIGContext struct { Claims map[string]interface{} Path map[string]string QryStr map[string]string Request *events.APIGatewayProxyRequest -} - -// APIGResponse is used as the output of handler functions. -// Populate Status and Body with your http response or populate Err with your error. -type APIGResponse struct { - Status int - Body []byte - Err error + Status int + Body []byte + Err error } // APIGHandler is the interface a handler function must implement to be used // with Get, Post, Put, Patch, and Delete. -type APIGHandler func(req *APIGRequest, res *APIGResponse) +type APIGHandler func(ctx *APIGContext) // APIGRouter is the object that handlers build upon and is used in the end to respond. type APIGRouter struct { @@ -124,19 +119,18 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { handlers := handlersInterface.([]APIGHandler) for _, handler := range handlers { - req := &APIGRequest{ - Body: r.request.Body, + ctx := &APIGContext{ + Body: []byte(r.request.Body), Path: r.request.PathParameters, QryStr: r.request.QueryStringParameters, Request: r.request, } if r.request.RequestContext.Authorizer["claims"] != nil { - req.Claims = r.request.RequestContext.Authorizer["claims"].(map[string]interface{}) + ctx.Claims = r.request.RequestContext.Authorizer["claims"].(map[string]interface{}) } - res := &APIGResponse{} - handler(req, res) - status, respbody, err = res.deconstruct() + handler(ctx) + status, respbody, err = ctx.respDeconstruct() if err != nil { respbody, _ := json.Marshal(map[string]string{"error": err.Error()}) @@ -164,8 +158,8 @@ func stripSlashesAndSplit(s string) []string { return strings.Split(s, "/") } -func (res *APIGResponse) deconstruct() (int, []byte, error) { - return res.Status, res.Body, res.Err +func (ctx *APIGContext) respDeconstruct() (int, []byte, error) { + return ctx.Status, ctx.Body, ctx.Err } func (r *APIGRouter) addEndpoint(method string, route string, handlers []APIGHandler) { diff --git a/router_test.go b/router_test.go index 723a7c5..d08e94c 100644 --- a/router_test.go +++ b/router_test.go @@ -16,18 +16,18 @@ func TestRouterSpec(t *testing.T) { rtr := NewAPIGRouter(&request, "shipping") Convey("When the handler func does NOT return an error", func() { - hdlrfunc := func(req *APIGRequest, res *APIGResponse) { - res.Status = http.StatusOK - res.Body = []byte("hello") - res.Err = nil + hdlrfunc := func(ctx *APIGContext) { + ctx.Status = http.StatusOK + ctx.Body = []byte("hello") + ctx.Err = nil } Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) - rtr.Post("/orders", func(req *APIGRequest, res *APIGResponse) {}) - rtr.Put("/orders", func(req *APIGRequest, res *APIGResponse) {}) - rtr.Patch("/orders", func(req *APIGRequest, res *APIGResponse) {}) - rtr.Delete("/orders/{id}", func(req *APIGRequest, res *APIGResponse) {}) + rtr.Post("/orders", func(ctx *APIGContext) {}) + rtr.Put("/orders", func(ctx *APIGContext) {}) + rtr.Patch("/orders", func(ctx *APIGContext) {}) + rtr.Delete("/orders/{id}", func(ctx *APIGContext) {}) Convey("And the request matches the pattern and the path params are filled", func() { request.HTTPMethod = http.MethodGet @@ -99,10 +99,10 @@ func TestRouterSpec(t *testing.T) { }) Convey("When the handler func does return a record not found", func() { - hdlrfunc := func(req *APIGRequest, res *APIGResponse) { - res.Status = http.StatusBadRequest - res.Body = []byte("hello") - res.Err = errors.New("record not found") + hdlrfunc := func(ctx *APIGContext) { + ctx.Status = http.StatusBadRequest + ctx.Body = []byte("hello") + ctx.Err = errors.New("record not found") } @@ -127,20 +127,20 @@ func TestRouterSpec(t *testing.T) { }) Convey("When the handler func does return a status < 400", func() { - middlefunc1 := func(req *APIGRequest, res *APIGResponse) { - res.Status = http.StatusOK - res.Body = []byte("hello") - res.Err = nil + middlefunc1 := func(ctx *APIGContext) { + ctx.Status = http.StatusOK + ctx.Body = []byte("hello") + ctx.Err = nil } - middlefunc2 := func(req *APIGRequest, res *APIGResponse) { - res.Status = http.StatusOK - res.Body = []byte("hello") - res.Err = errors.New("bad request") + middlefunc2 := func(ctx *APIGContext) { + ctx.Status = http.StatusOK + ctx.Body = []byte("hello") + ctx.Err = errors.New("bad request") } - hdlrfunc := func(req *APIGRequest, res *APIGResponse) { - res.Status = http.StatusOK - res.Body = []byte("hello") - res.Err = nil + hdlrfunc := func(ctx *APIGContext) { + ctx.Status = http.StatusOK + ctx.Body = []byte("hello") + ctx.Err = nil } Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { From 820e7f77930e2efd958f4f3266679500a587a3e5 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 18 Jul 2018 14:04:18 -0700 Subject: [PATCH 03/18] Added log.Printf to error if, format: 404 error: not found --- router.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/router.go b/router.go index 4bed5f3..9644924 100644 --- a/router.go +++ b/router.go @@ -2,6 +2,7 @@ package lambdarouter import ( "encoding/json" + "log" "net/http" "strings" @@ -140,6 +141,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { status = 400 } + log.Printf("%v error: %v", status, err.Error()) response.StatusCode = status response.Body = string(respbody) return response From fa2a855e975c5674433457be5360292cb7328eb5 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 18 Jul 2018 16:15:07 -0700 Subject: [PATCH 04/18] Changed test cases to use pattern known to cause issue; updated path param replacement algorithm to fix said issue --- router.go | 16 ++++++++++++---- router_test.go | 31 +++++++++++++++++-------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/router.go b/router.go index 9644924..5432322 100644 --- a/router.go +++ b/router.go @@ -99,15 +99,23 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { endpointTree = r.endpoints[r.request.HTTPMethod] path = strings.TrimPrefix(r.request.Path, "/"+r.svcprefix) response = events.APIGatewayProxyResponse{} + splitPath = stripSlashesAndSplit(path) ) for k := range r.params { - p := strings.TrimPrefix(k, "{") - p = strings.TrimSuffix(p, "}") - if r.request.PathParameters[p] != "" { - path = strings.Replace(path, r.request.PathParameters[p], k, -1) + pname := strings.TrimPrefix(k, "{") + pname = strings.TrimSuffix(pname, "}") + if r.request.PathParameters[pname] != "" { + pval := r.request.PathParameters[pname] + for i, v := range splitPath { + if v == pval { + splitPath[i] = k + } + } + } } + path = "/" + strings.Join(splitPath, "/") if handlersInterface, ok = endpointTree.Get(path); !ok { respbody, _ := json.Marshal(map[string]string{"error": "no route matching path found"}) diff --git a/router_test.go b/router_test.go index d08e94c..bd8102d 100644 --- a/router_test.go +++ b/router_test.go @@ -22,8 +22,8 @@ func TestRouterSpec(t *testing.T) { ctx.Err = nil } - Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { - rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { + rtr.Get("/listings/{id}/state/{event}", hdlrfunc) rtr.Post("/orders", func(ctx *APIGContext) {}) rtr.Put("/orders", func(ctx *APIGContext) {}) rtr.Patch("/orders", func(ctx *APIGContext) {}) @@ -31,9 +31,10 @@ func TestRouterSpec(t *testing.T) { Convey("And the request matches the pattern and the path params are filled", func() { request.HTTPMethod = http.MethodGet - request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.Path = "/shipping/listings/57/state/list" request.PathParameters = map[string]string{ - "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + "id": "57", + "event": "list", } request.RequestContext.Authorizer = map[string]interface{}{ "claims": map[string]interface{}{ @@ -61,9 +62,9 @@ func TestRouterSpec(t *testing.T) { }) }) - Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined AGAIN", func() { + Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined AGAIN", func() { So(func() { - rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + rtr.Get("/listings/{id}/state/{event}", hdlrfunc) }, ShouldPanicWith, "endpoint already existent") }) @@ -106,14 +107,15 @@ func TestRouterSpec(t *testing.T) { } - Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { - rtr.Get("/orders/filter/by_user/{id}", hdlrfunc) + Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { + rtr.Get("/listings/{id}/state/{event}", hdlrfunc) Convey("And the request matches the pattern and the path params are filled", func() { request.HTTPMethod = http.MethodGet - request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.Path = "/shipping/listings/57/state/list" request.PathParameters = map[string]string{ - "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + "id": "57", + "event": "list", } Convey("The router will return the expected status and body", func() { @@ -143,14 +145,15 @@ func TestRouterSpec(t *testing.T) { ctx.Err = nil } - Convey("And a Get handler expecting the pattern /orders/filter/by_user/{id} is defined", func() { - rtr.Get("/orders/filter/by_user/{id}", middlefunc1, middlefunc2, hdlrfunc) + Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { + rtr.Get("/listings/{id}/state/{event}", middlefunc1, middlefunc2, hdlrfunc) Convey("And the request matches the pattern and the path params are filled", func() { request.HTTPMethod = http.MethodGet - request.Path = "/shipping/orders/filter/by_user/4d50ff90-66e3-4047-bf37-0ca25837e41d" + request.Path = "/shipping/listings/57/state/list" request.PathParameters = map[string]string{ - "id": "4d50ff90-66e3-4047-bf37-0ca25837e41d", + "id": "57", + "event": "list", } Convey("The router will return the expected status and body", func() { From 5a9f5e2bf3f2cece0456a93fea2e1d0612206db5 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 18 Jul 2018 19:52:14 -0700 Subject: [PATCH 05/18] Changed the NewAPIGRouter input to a struct; Added response Headers to config struct --- router.go | 26 +++++++++++++++++++------- router_test.go | 15 +++++++++++++-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/router.go b/router.go index 5432322..326f840 100644 --- a/router.go +++ b/router.go @@ -41,15 +41,25 @@ type APIGRouter struct { request *events.APIGatewayProxyRequest endpoints map[string]*radix.Tree params map[string]string - svcprefix string + prefix string + headers map[string]string +} + +// APIGRouterConfig is used as the input to NewAPIGRouter, request is your incoming +// apig request and prefix will be stripped of all incoming request paths. Headers +// will be sent with all responses. +type APIGRouterConfig struct { + Request *events.APIGatewayProxyRequest + Prefix string + Headers map[string]string } // NOTE: Begin router methods. -// NewAPIGRouter creates a new router using the request and a prefix to strip from your incoming requests. -func NewAPIGRouter(r *events.APIGatewayProxyRequest, svcprefix string) *APIGRouter { +// NewAPIGRouter creates a new router using the given router config. +func NewAPIGRouter(cfg *APIGRouterConfig) *APIGRouter { return &APIGRouter{ - request: r, + request: cfg.Request, endpoints: map[string]*radix.Tree{ post: radix.New(), get: radix.New(), @@ -57,8 +67,9 @@ func NewAPIGRouter(r *events.APIGatewayProxyRequest, svcprefix string) *APIGRout patch: radix.New(), delete: radix.New(), }, - params: map[string]string{}, - svcprefix: svcprefix, + params: map[string]string{}, + prefix: cfg.Prefix, + headers: cfg.Headers, } } @@ -97,7 +108,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { err error endpointTree = r.endpoints[r.request.HTTPMethod] - path = strings.TrimPrefix(r.request.Path, "/"+r.svcprefix) + path = strings.TrimPrefix(r.request.Path, r.prefix) response = events.APIGatewayProxyResponse{} splitPath = stripSlashesAndSplit(path) ) @@ -158,6 +169,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { response.StatusCode = status response.Body = string(respbody) + response.Headers = r.headers return response } diff --git a/router_test.go b/router_test.go index bd8102d..f9cc168 100644 --- a/router_test.go +++ b/router_test.go @@ -13,7 +13,14 @@ func TestRouterSpec(t *testing.T) { Convey("Given an instantiated router", t, func() { request := events.APIGatewayProxyRequest{} - rtr := NewAPIGRouter(&request, "shipping") + rtr := NewAPIGRouter(&APIGRouterConfig{ + Request: &request, + Prefix: "/shipping", + Headers: map[string]string{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": "true", + }, + }) Convey("When the handler func does NOT return an error", func() { hdlrfunc := func(ctx *APIGContext) { @@ -42,11 +49,15 @@ func TestRouterSpec(t *testing.T) { }, } - Convey("The router will return the expected status and body", func() { + Convey("The router will return the expected status, body, and headers", func() { response := rtr.Respond() So(response.StatusCode, ShouldEqual, http.StatusOK) So(response.Body, ShouldEqual, "hello") + So(response.Headers, ShouldResemble, map[string]string{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": "true", + }) }) }) From 3f71da28d8b81cb5040cfa804f2d5776e8670080 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 18 Jul 2018 19:56:04 -0700 Subject: [PATCH 06/18] Minor changes to travis build script --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 18fad04..59be1dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ language: go go: - 1.x -- '1.8' -- 1.10.x -- master install: - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure @@ -11,8 +8,8 @@ install: - go get github.com/mattn/goveralls script: - go test -v -covermode=count -coverprofile=coverage.out -- "$HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken - $COVERALLS_TOKEN" +- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken + $COVERALLS_TOKEN env: global: secure: ZQwEmXhn/whrpwJAeSBi2FduujLN/fmoyHzExOaeIK19jf0zd7HDGU/aIaBLzxCmPnBtvayeHwPqtJncqkpKYD1VKiVd2uEIj/2y2GwNTQrea8rDMKNPv38rBoFwBJv7xAygki79JrjXZ9s0EdoSZC8U4osYJksMpEJpbHTnOXxR5Ci0nAjEOpNfUmetLTK6Er2Vy+XKMMZdnwbrVbp9WNluVLC4ISiuV3SfpEmZjtktIgqoHbrEKyejzXWE8G6Ax6QptHagB3ZgltDAu2pV4w55JoI3O1dGaHtIerVjlM85BWHpQZXA/nON9KglPlDRx0wNIhWj30WdsjKUUUmjldhCwXCpMueFC3ttvPQmKAT6HjnJUcYA5Q/hVMp0NV/AFSdpKz9/jJ9nZCO73OZZVENZOo6kYq7qyz3gNwSjGuuov+hjq8tlzYmGlblvZMMT2Nw0aIXMLNEcAvaK3vl6PsBTc9JCj3xtxSVULhOZoN8vceltC1+0upi4KwoMzBfZBAMtW86LUpl2iYokVj6jzGwKCGNAI9H5c+3G/ItAwTM+E2uyCEzXGFUl8dB2Iwz2qU98XWX2aO2KLB2EexFxXuWXwJQL/kxdzRx+VfHPG00I+mDkJNOkA5Vy43hAWULnDM/Ov8c/Rx+1j5KQGvXxxKvsdy7q6dV2GOYr4/oq+3Y= From 695a7bea2e9fc6d36b13986de85c8d414dc3eee9 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 25 Jul 2018 15:28:26 -0700 Subject: [PATCH 07/18] Changed code coverage provider to code climate; added badges --- .travis.yml | 28 +++++++++++++++++----------- README.md | 8 +++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59be1dc..7e5537a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,21 @@ language: go + go: -- 1.x + - 1.x + +before_install: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + install: -- curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh -- dep ensure -- go get golang.org/x/tools/cmd/cover -- go get github.com/mattn/goveralls + - chmod +x ./cc-test-reporter + - dep ensure + +before_script: + - ./cc-test-reporter before-build + script: -- go test -v -covermode=count -coverprofile=coverage.out -- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken - $COVERALLS_TOKEN -env: - global: - secure: ZQwEmXhn/whrpwJAeSBi2FduujLN/fmoyHzExOaeIK19jf0zd7HDGU/aIaBLzxCmPnBtvayeHwPqtJncqkpKYD1VKiVd2uEIj/2y2GwNTQrea8rDMKNPv38rBoFwBJv7xAygki79JrjXZ9s0EdoSZC8U4osYJksMpEJpbHTnOXxR5Ci0nAjEOpNfUmetLTK6Er2Vy+XKMMZdnwbrVbp9WNluVLC4ISiuV3SfpEmZjtktIgqoHbrEKyejzXWE8G6Ax6QptHagB3ZgltDAu2pV4w55JoI3O1dGaHtIerVjlM85BWHpQZXA/nON9KglPlDRx0wNIhWj30WdsjKUUUmjldhCwXCpMueFC3ttvPQmKAT6HjnJUcYA5Q/hVMp0NV/AFSdpKz9/jJ9nZCO73OZZVENZOo6kYq7qyz3gNwSjGuuov+hjq8tlzYmGlblvZMMT2Nw0aIXMLNEcAvaK3vl6PsBTc9JCj3xtxSVULhOZoN8vceltC1+0upi4KwoMzBfZBAMtW86LUpl2iYokVj6jzGwKCGNAI9H5c+3G/ItAwTM+E2uyCEzXGFUl8dB2Iwz2qU98XWX2aO2KLB2EexFxXuWXwJQL/kxdzRx+VfHPG00I+mDkJNOkA5Vy43hAWULnDM/Ov8c/Rx+1j5KQGvXxxKvsdy7q6dV2GOYr4/oq+3Y= + - go test -v -coverprofile=c.out ./... + +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/README.md b/README.md index 5e0469c..96c4a87 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# lambdarouter [![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) [![Build Status](https://travis-ci.org/mitchelljfs/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchelljfs/lambdarouter) [![Coverage Status](https://coveralls.io/repos/github/mitchelljfs/lambdarouter/badge.svg?branch=master)](https://coveralls.io/github/mitchelljfs/lambdarouter?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) +# lambdarouter +[![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) +[![Build Status](https://travis-ci.org/mitchelljfs/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchelljfs/lambdarouter) +[![Test Coverage](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/test_coverage)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) +[![Maintainability](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/maintainability)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) +[![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) + This package will become a fully featured AWS Lambda function router, able to respond to HTTP, Schedule, Cognito, and SNS events. It will also support middleware interfacing. So far it includes functionality for API Gateway. Check out the GoDoc Reference to see how to instantiate a router and From c8fbc0ee347c4eb5d6af89e4d404e1110006b6b9 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 25 Jul 2018 16:19:35 -0700 Subject: [PATCH 08/18] Change README badges [ci skip] --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 96c4a87..dd88bc6 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # lambdarouter [![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) [![Build Status](https://travis-ci.org/mitchelljfs/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchelljfs/lambdarouter) -[![Test Coverage](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/test_coverage)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) -[![Maintainability](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/maintainability)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) +[![Test Coverage](https://img.shields.io/codeclimate/coverage/mitchelljfs/lambdarouter.svg)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) +[![Maintainability](https://img.shields.io/codeclimate/maintainability/mitchelljfs/lambdarouter.svg)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) [![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) This package will become a fully featured AWS Lambda function router, able to respond to HTTP, Schedule, Cognito, and SNS events. It will also support middleware interfacing. From da00efd4a39da862498149a43671dd6717bd9e22 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Wed, 25 Jul 2018 18:53:26 -0700 Subject: [PATCH 09/18] Revert "Change README badges [ci skip]" This reverts commit c8fbc0ee347c4eb5d6af89e4d404e1110006b6b9. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dd88bc6..96c4a87 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # lambdarouter [![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) [![Build Status](https://travis-ci.org/mitchelljfs/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchelljfs/lambdarouter) -[![Test Coverage](https://img.shields.io/codeclimate/coverage/mitchelljfs/lambdarouter.svg)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) -[![Maintainability](https://img.shields.io/codeclimate/maintainability/mitchelljfs/lambdarouter.svg)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/test_coverage)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) +[![Maintainability](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/maintainability)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) [![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) This package will become a fully featured AWS Lambda function router, able to respond to HTTP, Schedule, Cognito, and SNS events. It will also support middleware interfacing. From 639d28c78e89aa07a0f024e938b3444b49fe92a8 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Fri, 3 Aug 2018 00:01:13 -0700 Subject: [PATCH 10/18] Added context to apig router config and apig context --- Gopkg.lock | 2 +- Gopkg.toml | 4 ++++ router.go | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Gopkg.lock b/Gopkg.lock index 95b28b8..85c4c77 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -48,6 +48,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "751d4fc4b7be23b18796aed082f5e83d9ba309c184255d4c63d8225cc0048152" + inputs-digest = "5da761903d61cf9e47d309477db924d6e310f97fbd7c553ff46af570b65d4356" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 5a78b31..4c8db6c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -33,6 +33,10 @@ name = "github.com/aws/aws-lambda-go" version = "1.2.0" +[[constraint]] + name = "github.com/smartystreets/goconvey" + version = "1.6.3" + [prune] go-tests = true unused-packages = true diff --git a/router.go b/router.go index 326f840..03a72e3 100644 --- a/router.go +++ b/router.go @@ -1,6 +1,7 @@ package lambdarouter import ( + "context" "encoding/json" "log" "net/http" @@ -24,6 +25,7 @@ const ( // Fill the Status and Body, or Status and Error to respond. type APIGContext struct { Claims map[string]interface{} + Context context.Context Path map[string]string QryStr map[string]string Request *events.APIGatewayProxyRequest @@ -43,12 +45,14 @@ type APIGRouter struct { params map[string]string prefix string headers map[string]string + context context.Context } // APIGRouterConfig is used as the input to NewAPIGRouter, request is your incoming // apig request and prefix will be stripped of all incoming request paths. Headers // will be sent with all responses. type APIGRouterConfig struct { + Context context.Context Request *events.APIGatewayProxyRequest Prefix string Headers map[string]string @@ -144,6 +148,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { Path: r.request.PathParameters, QryStr: r.request.QueryStringParameters, Request: r.request, + Context: r.context, } if r.request.RequestContext.Authorizer["claims"] != nil { ctx.Claims = r.request.RequestContext.Authorizer["claims"].(map[string]interface{}) From 4399e41cd2a38c13d55060c368bc955da888f7eb Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Fri, 3 Aug 2018 11:07:37 -0700 Subject: [PATCH 11/18] Added context to NewAPIGRouter --- router.go | 1 + 1 file changed, 1 insertion(+) diff --git a/router.go b/router.go index 03a72e3..b6b0a25 100644 --- a/router.go +++ b/router.go @@ -74,6 +74,7 @@ func NewAPIGRouter(cfg *APIGRouterConfig) *APIGRouter { params: map[string]string{}, prefix: cfg.Prefix, headers: cfg.Headers, + context: cfg.Context, } } From ade833df398813493359af699990062d4d77c3d3 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Thu, 30 Aug 2018 13:07:44 -0700 Subject: [PATCH 12/18] Added headers to error response --- router.go | 4 +++- router_test.go | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index b6b0a25..9523403 100644 --- a/router.go +++ b/router.go @@ -138,6 +138,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { response.StatusCode = http.StatusNotFound response.Body = string(respbody) + response.Headers = r.headers return response } @@ -162,13 +163,14 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { respbody, _ := json.Marshal(map[string]string{"error": err.Error()}) if strings.Contains(err.Error(), "record not found") { status = 204 - } else if status < 400 { + } else if status != 204 && status < 400 { status = 400 } log.Printf("%v error: %v", status, err.Error()) response.StatusCode = status response.Body = string(respbody) + response.Headers = r.headers return response } } diff --git a/router_test.go b/router_test.go index f9cc168..fe4e567 100644 --- a/router_test.go +++ b/router_test.go @@ -70,6 +70,10 @@ func TestRouterSpec(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusNotFound) So(response.Body, ShouldEqual, "{\"error\":\"no route matching path found\"}") + So(response.Headers, ShouldResemble, map[string]string{ + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Credentials": "true", + }) }) }) @@ -112,7 +116,7 @@ func TestRouterSpec(t *testing.T) { Convey("When the handler func does return a record not found", func() { hdlrfunc := func(ctx *APIGContext) { - ctx.Status = http.StatusBadRequest + ctx.Status = http.StatusNoContent ctx.Body = []byte("hello") ctx.Err = errors.New("record not found") From ada0c0871a2585b49a847e4bf2202aba338f6533 Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Mon, 24 Sep 2018 16:59:18 -0700 Subject: [PATCH 13/18] Changed internal params attribute to map[string]interface{}; fixed issue with having the same value for two path parameters --- router.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/router.go b/router.go index 9523403..6f1ce94 100644 --- a/router.go +++ b/router.go @@ -42,7 +42,7 @@ type APIGHandler func(ctx *APIGContext) type APIGRouter struct { request *events.APIGatewayProxyRequest endpoints map[string]*radix.Tree - params map[string]string + params map[string]interface{} prefix string headers map[string]string context context.Context @@ -71,7 +71,7 @@ func NewAPIGRouter(cfg *APIGRouterConfig) *APIGRouter { patch: radix.New(), delete: radix.New(), }, - params: map[string]string{}, + params: map[string]interface{}{}, prefix: cfg.Prefix, headers: cfg.Headers, context: cfg.Context, @@ -118,23 +118,21 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { splitPath = stripSlashesAndSplit(path) ) - for k := range r.params { - pname := strings.TrimPrefix(k, "{") - pname = strings.TrimSuffix(pname, "}") - if r.request.PathParameters[pname] != "" { - pval := r.request.PathParameters[pname] + for p := range r.params { + if r.request.PathParameters[p] != "" { + pval := r.request.PathParameters[p] for i, v := range splitPath { if v == pval { - splitPath[i] = k + splitPath[i] = "{" + p + "}" + break } } - } } path = "/" + strings.Join(splitPath, "/") if handlersInterface, ok = endpointTree.Get(path); !ok { - respbody, _ := json.Marshal(map[string]string{"error": "no route matching path found"}) + respbody, _ = json.Marshal(map[string]string{"error": "no route matching path found"}) response.StatusCode = http.StatusNotFound response.Body = string(respbody) @@ -160,7 +158,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { status, respbody, err = ctx.respDeconstruct() if err != nil { - respbody, _ := json.Marshal(map[string]string{"error": err.Error()}) + respbody, _ = json.Marshal(map[string]string{"error": err.Error()}) if strings.Contains(err.Error(), "record not found") { status = 204 } else if status != 204 && status < 400 { @@ -200,7 +198,9 @@ func (r *APIGRouter) addEndpoint(method string, route string, handlers []APIGHan rtearr := stripSlashesAndSplit(route) for _, v := range rtearr { if strings.HasPrefix(v, "{") { - r.params[v] = "" // adding params as keys with {brackets} + v = strings.TrimPrefix(v, "{") + v = strings.TrimSuffix(v, "}") + r.params[v] = nil // adding params as *unique* keys } } From ecdb0bed8414c5e4959f465f081627d2d6ba89cd Mon Sep 17 00:00:00 2001 From: mitchelljfs Date: Sat, 13 Oct 2018 14:08:14 -0700 Subject: [PATCH 14/18] Added to error logging details --- Gopkg.lock | 22 +++++++++++++++++++--- router.go | 4 +++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 85c4c77..fc394f8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -3,51 +3,67 @@ [[projects]] branch = "master" + digest = "1:9fd3a6ab34bb103ba228eefd044d3f9aa476237ea95a46d12e8cccd3abf3fea2" name = "github.com/armon/go-radix" packages = ["."] + pruneopts = "UT" revision = "1fca145dffbcaa8fe914309b1ec0cfc67500fe61" [[projects]] + digest = "1:5580dfd33541011ae489dde24be4db55f1de8eaff11165d469342d95a7c0f790" name = "github.com/aws/aws-lambda-go" packages = ["events"] + pruneopts = "UT" revision = "4d30d0ff60440c2d0480a15747c96ee71c3c53d4" version = "v1.2.0" [[projects]] branch = "master" + digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795" name = "github.com/gopherjs/gopherjs" packages = ["js"] + pruneopts = "UT" revision = "0892b62f0d9fb5857760c3cfca837207185117ee" [[projects]] + digest = "1:1e15f4e455f94aeaedfcf9c75b3e1c449b5acba1551c58446b4b45be507c707b" name = "github.com/jtolds/gls" packages = ["."] + pruneopts = "UT" revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" version = "v4.2.1" [[projects]] + digest = "1:cc1c574c9cb5e99b123888c12b828e2d19224ab6c2244bda34647f230bf33243" name = "github.com/smartystreets/assertions" packages = [ ".", "internal/go-render/render", - "internal/oglematchers" + "internal/oglematchers", ] + pruneopts = "UT" revision = "7678a5452ebea5b7090a6b163f844c133f523da2" version = "1.8.3" [[projects]] + digest = "1:a3e081e593ee8e3b0a9af6a5dcac964c67a40c4f2034b5345b2ad78d05920728" name = "github.com/smartystreets/goconvey" packages = [ "convey", "convey/gotest", - "convey/reporting" + "convey/reporting", ] + pruneopts = "UT" revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" version = "1.6.3" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "5da761903d61cf9e47d309477db924d6e310f97fbd7c553ff46af570b65d4356" + input-imports = [ + "github.com/armon/go-radix", + "github.com/aws/aws-lambda-go/events", + "github.com/smartystreets/goconvey/convey", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/router.go b/router.go index 6f1ce94..7ef6a31 100644 --- a/router.go +++ b/router.go @@ -114,6 +114,7 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { endpointTree = r.endpoints[r.request.HTTPMethod] path = strings.TrimPrefix(r.request.Path, r.prefix) + inPath = path response = events.APIGatewayProxyResponse{} splitPath = stripSlashesAndSplit(path) ) @@ -165,7 +166,8 @@ func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { status = 400 } - log.Printf("%v error: %v", status, err.Error()) + log.Printf("%v %v %v error: %v \n", r.request.HTTPMethod, inPath, status, err.Error()) + log.Println("error causing body: " + r.request.Body) response.StatusCode = status response.Body = string(respbody) response.Headers = r.headers From 83bb9a2aa1ab43cf1807876bf5d6969107ec2733 Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 6 Feb 2019 19:39:58 -0800 Subject: [PATCH 15/18] Updated README.md --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 96c4a87..51a97fd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,8 @@ # lambdarouter [![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) -[![Build Status](https://travis-ci.org/mitchelljfs/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchelljfs/lambdarouter) +[![Build Status](https://travis-ci.org/mitchell/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchell/lambdarouter) [![Test Coverage](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/test_coverage)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/maintainability)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) [![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) -This package will become a fully featured AWS Lambda function router, able to respond to HTTP, Schedule, Cognito, and SNS events. It will also support middleware interfacing. - -So far it includes functionality for API Gateway. Check out the GoDoc Reference to see how to instantiate a router and -create endpoints. +So far this package can create a router and routes whose execution utilizes a middleware context pattern, all in one Golang Lambda function. It has full test coverage, and is currently deployed in numerous services where I am employed. Check out the GoDoc Reference to see how to instantiate a router and create endpoints. From f65f17158c3ce9bf0d5e5840ca93b10e0b8b9700 Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 7 Apr 2019 15:22:26 -0700 Subject: [PATCH 16/18] First iteration of v1 lambdarouter, full API change --- .travis.yml | 4 +- Gopkg.lock | 81 +++++---- Gopkg.toml | 11 +- README.md | 26 ++- examples/hello-world/.gitignore | 5 + examples/hello-world/Makefile | 10 ++ examples/hello-world/main.go | 47 +++++ examples/hello-world/serverless.yml | 42 +++++ go.mod | 9 + go.sum | 17 ++ router.go | 267 ++++++++++------------------ router_test.go | 249 +++++++++----------------- 12 files changed, 376 insertions(+), 392 deletions(-) create mode 100644 examples/hello-world/.gitignore create mode 100644 examples/hello-world/Makefile create mode 100644 examples/hello-world/main.go create mode 100644 examples/hello-world/serverless.yml create mode 100644 go.mod create mode 100644 go.sum diff --git a/.travis.yml b/.travis.yml index 7e5537a..aa6ae99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,12 +4,12 @@ go: - 1.x before_install: - - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh install: + - dep ensure -v + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - - dep ensure before_script: - ./cc-test-reporter before-build diff --git a/Gopkg.lock b/Gopkg.lock index fc394f8..9d3fce4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,68 +2,67 @@ [[projects]] - branch = "master" - digest = "1:9fd3a6ab34bb103ba228eefd044d3f9aa476237ea95a46d12e8cccd3abf3fea2" - name = "github.com/armon/go-radix" - packages = ["."] - pruneopts = "UT" - revision = "1fca145dffbcaa8fe914309b1ec0cfc67500fe61" - -[[projects]] - digest = "1:5580dfd33541011ae489dde24be4db55f1de8eaff11165d469342d95a7c0f790" + digest = "1:95fd14230b48a74765db94f7064f0b5617f52739a164f6d41e5b57bec5b3ff9b" name = "github.com/aws/aws-lambda-go" - packages = ["events"] + packages = [ + "events", + "lambda", + "lambda/handlertrace", + "lambda/messages", + "lambdacontext", + ] pruneopts = "UT" - revision = "4d30d0ff60440c2d0480a15747c96ee71c3c53d4" - version = "v1.2.0" + revision = "08d251dc58f204411d79012fd6f1beefb7107fea" + version = "v1.10.0" [[projects]] - branch = "master" - digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795" - name = "github.com/gopherjs/gopherjs" - packages = ["js"] + digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" + name = "github.com/davecgh/go-spew" + packages = ["spew"] pruneopts = "UT" - revision = "0892b62f0d9fb5857760c3cfca837207185117ee" + revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" + version = "v1.1.1" [[projects]] - digest = "1:1e15f4e455f94aeaedfcf9c75b3e1c449b5acba1551c58446b4b45be507c707b" - name = "github.com/jtolds/gls" + digest = "1:2be5a35f0c5b35162c41bb24971e5dcf6ce825403296ee435429cdcc4e1e847e" + name = "github.com/hashicorp/go-immutable-radix" packages = ["."] pruneopts = "UT" - revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" - version = "v4.2.1" + revision = "27df80928bb34bb1b0d6d0e01b9e679902e7a6b5" + version = "v1.0.0" [[projects]] - digest = "1:cc1c574c9cb5e99b123888c12b828e2d19224ab6c2244bda34647f230bf33243" - name = "github.com/smartystreets/assertions" - packages = [ - ".", - "internal/go-render/render", - "internal/oglematchers", - ] + digest = "1:67474f760e9ac3799f740db2c489e6423a4cde45520673ec123ac831ad849cb8" + name = "github.com/hashicorp/golang-lru" + packages = ["simplelru"] pruneopts = "UT" - revision = "7678a5452ebea5b7090a6b163f844c133f523da2" - version = "1.8.3" + revision = "7087cb70de9f7a8bc0a10c375cb0d2280a8edf9c" + version = "v0.5.1" [[projects]] - digest = "1:a3e081e593ee8e3b0a9af6a5dcac964c67a40c4f2034b5345b2ad78d05920728" - name = "github.com/smartystreets/goconvey" - packages = [ - "convey", - "convey/gotest", - "convey/reporting", - ] + digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] pruneopts = "UT" - revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" - version = "1.6.3" + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + digest = "1:972c2427413d41a1e06ca4897e8528e5a1622894050e2f527b38ddf0f343f759" + name = "github.com/stretchr/testify" + packages = ["assert"] + pruneopts = "UT" + revision = "ffdc059bfe9ce6a4e144ba849dbedead332c6053" + version = "v1.3.0" [solve-meta] analyzer-name = "dep" analyzer-version = 1 input-imports = [ - "github.com/armon/go-radix", "github.com/aws/aws-lambda-go/events", - "github.com/smartystreets/goconvey/convey", + "github.com/aws/aws-lambda-go/lambda", + "github.com/hashicorp/go-immutable-radix", + "github.com/stretchr/testify/assert", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 4c8db6c..59d17d5 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -24,18 +24,9 @@ # go-tests = true # unused-packages = true - -[[constraint]] - branch = "master" - name = "github.com/armon/go-radix" - [[constraint]] name = "github.com/aws/aws-lambda-go" - version = "1.2.0" - -[[constraint]] - name = "github.com/smartystreets/goconvey" - version = "1.6.3" + version = "1.x" [prune] go-tests = true diff --git a/README.md b/README.md index 51a97fd..f3447e2 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,28 @@ # lambdarouter -[![GoDoc Reference](https://godoc.org/github.com/mitchelljfs/lambdarouter?status.svg)](https://godoc.org/github.com/mitchelljfs/lambdarouter) +[![GoDoc Reference](https://godoc.org/github.com/mitchell/lambdarouter?status.svg)](https://godoc.org/github.com/mitchell/lambdarouter) [![Build Status](https://travis-ci.org/mitchell/lambdarouter.svg?branch=master)](https://travis-ci.org/mitchell/lambdarouter) [![Test Coverage](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/test_coverage)](https://codeclimate.com/github/mitchelljfs/lambdarouter/test_coverage) [![Maintainability](https://api.codeclimate.com/v1/badges/7270c6c4017b36d07360/maintainability)](https://codeclimate.com/github/mitchelljfs/lambdarouter/maintainability) -[![Go Report Card](https://goreportcard.com/badge/github.com/mitchelljfs/lambdarouter)](https://goreportcard.com/report/github.com/mitchelljfs/lambdarouter) +[![Go Report Card](https://goreportcard.com/badge/github.com/mitchell/lambdarouter)](https://goreportcard.com/report/github.com/mitchell/lambdarouter) -So far this package can create a router and routes whose execution utilizes a middleware context pattern, all in one Golang Lambda function. It has full test coverage, and is currently deployed in numerous services where I am employed. Check out the GoDoc Reference to see how to instantiate a router and create endpoints. +This package contains a router capable of routing many AWS Lambda API gateway requests to anything +that implements the aws-lambda-go/lambda.Handler interface, all in one Lambda function. It plays +especially well with go-kit's awslambda transport package. Get started by reading below and visiting +the [GoDoc reference](https://godoc.org/github.com/mitchell/lambdarouter). + +## Initializing a Router +``` +r := lambdarouter.New("prefix/") + +r.Get("hello/{name}", helloHandler) +r.Post("hello/server", helloHandler) +r.Delete("hello", lambda.NewHandler(func() (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + Body: "nothing to delete", + }, nil +})) + +lambda.StartHandler(r) +``` + +Check out the `examples/` folder for more fleshed out examples in the proper context. diff --git a/examples/hello-world/.gitignore b/examples/hello-world/.gitignore new file mode 100644 index 0000000..f5b4c36 --- /dev/null +++ b/examples/hello-world/.gitignore @@ -0,0 +1,5 @@ +# Serverless directories +.serverless + +# golang output binary directory +bin \ No newline at end of file diff --git a/examples/hello-world/Makefile b/examples/hello-world/Makefile new file mode 100644 index 0000000..d50679e --- /dev/null +++ b/examples/hello-world/Makefile @@ -0,0 +1,10 @@ +.PHONY: all build clean deploy test + +build: + env GOOS=linux go build -ldflags="-s -w" -o ./bin/hello ./main.go + +clean: + rm -rf ./bin + +deploy: clean build + sls deploy --verbose diff --git a/examples/hello-world/main.go b/examples/hello-world/main.go new file mode 100644 index 0000000..1ddc7fb --- /dev/null +++ b/examples/hello-world/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "net/http" + + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + "github.com/mitchell/lambdarouter" +) + +var r = lambdarouter.New("hellosrv") + +func init() { + r.Post("hello", lambda.NewHandler(func() (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusCreated, + Body: "hello world", + }, nil + })) + + r.Group("hello", func(r *lambdarouter.Router) { + r.Get("{name}", lambda.NewHandler(func(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + Body: "hello " + req.PathParameters["name"], + }, nil + })) + + r.Put("french", lambda.NewHandler(func() (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + Body: "bonjour le monde", + }, nil + })) + + r.Get("french/{prenom}", lambda.NewHandler(func(req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + StatusCode: http.StatusOK, + Body: "bonjour " + req.PathParameters["prenom"], + }, nil + })) + }) +} + +func main() { + lambda.StartHandler(r) +} diff --git a/examples/hello-world/serverless.yml b/examples/hello-world/serverless.yml new file mode 100644 index 0000000..9c4fb5a --- /dev/null +++ b/examples/hello-world/serverless.yml @@ -0,0 +1,42 @@ +service: lambdarouter-hello-world +frameworkVersion: ">=1.28.0 <2.0.0" + +provider: + name: aws + runtime: go1.x + stage: ${opt:stage, 'dev'} + region: us-west-2 +# iamRoleStatements: +# environment: + +package: + exclude: + - ./** + include: + - ./bin/** + +functions: + hello: + handler: bin/hello + events: + - http: + path: hellosrv/hello + method: post + - http: + path: hellosrv/hello/{name} + method: get + request: + parameters: + path: + name: true + - http: + path: hellosrv/hello/french + method: put + - http: + path: hellosrv/hello/french/{prenom} + method: get + request: + parameters: + path: + prenom: true +#resources: diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..76920db --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/mitchell/lambdarouter + +require ( + github.com/aws/aws-lambda-go v1.10.0 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/go-immutable-radix v1.0.0 + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/stretchr/testify v1.3.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..75abf98 --- /dev/null +++ b/go.sum @@ -0,0 +1,17 @@ +github.com/aws/aws-lambda-go v1.10.0 h1:uafgdfYGQD0UeT7d2uKdyWW8j/ZYRifRPIdmeqLzLCk= +github.com/aws/aws-lambda-go v1.10.0/go.mod h1:zUsUQhAUjYzR8AuduJPCfhBuKWUaDbQiPOG+ouzmE1A= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/router.go b/router.go index 7ef6a31..b88a434 100644 --- a/router.go +++ b/router.go @@ -3,207 +3,134 @@ package lambdarouter import ( "context" "encoding/json" - "log" + "fmt" "net/http" "strings" - radix "github.com/armon/go-radix" "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" + iradix "github.com/hashicorp/go-immutable-radix" ) -const ( - post = http.MethodPost - get = http.MethodGet - put = http.MethodPut - patch = http.MethodPatch - delete = http.MethodDelete -) - -// APIGContext is used as the input and output of handler functions. -// The Body, Claims, Path, and QryStr will be populated by the the APIGatewayProxyRequest. -// The Request itself is also passed through if you need further access. -// Fill the Status and Body, or Status and Error to respond. -type APIGContext struct { - Claims map[string]interface{} - Context context.Context - Path map[string]string - QryStr map[string]string - Request *events.APIGatewayProxyRequest - Status int - Body []byte - Err error +// Router holds the defined routes for use upon invocation. +type Router struct { + events *iradix.Tree + prefix string } -// APIGHandler is the interface a handler function must implement to be used -// with Get, Post, Put, Patch, and Delete. -type APIGHandler func(ctx *APIGContext) +// New initializes an empty router. +func New(prefix string) Router { + if prefix[0] != '/' { + prefix = "/" + prefix + } + if prefix[len(prefix)-1] != '/' { + prefix += "/" + } -// APIGRouter is the object that handlers build upon and is used in the end to respond. -type APIGRouter struct { - request *events.APIGatewayProxyRequest - endpoints map[string]*radix.Tree - params map[string]interface{} - prefix string - headers map[string]string - context context.Context -} - -// APIGRouterConfig is used as the input to NewAPIGRouter, request is your incoming -// apig request and prefix will be stripped of all incoming request paths. Headers -// will be sent with all responses. -type APIGRouterConfig struct { - Context context.Context - Request *events.APIGatewayProxyRequest - Prefix string - Headers map[string]string -} - -// NOTE: Begin router methods. - -// NewAPIGRouter creates a new router using the given router config. -func NewAPIGRouter(cfg *APIGRouterConfig) *APIGRouter { - return &APIGRouter{ - request: cfg.Request, - endpoints: map[string]*radix.Tree{ - post: radix.New(), - get: radix.New(), - put: radix.New(), - patch: radix.New(), - delete: radix.New(), - }, - params: map[string]interface{}{}, - prefix: cfg.Prefix, - headers: cfg.Headers, - context: cfg.Context, + return Router{ + events: iradix.New(), + prefix: prefix, } } -// Get creates a new get endpoint. -func (r *APIGRouter) Get(route string, handlers ...APIGHandler) { - r.addEndpoint(get, route, handlers) +// Get adds a new GET method route to the router. The path parameter is the route path you wish to +// define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the +// route. +func (r *Router) Get(path string, handler lambda.Handler) { + path = r.prefix + path + r.addEvent(http.MethodGet+path, event{h: handler}) } -// Post creates a new post endpoint. -func (r *APIGRouter) Post(route string, handlers ...APIGHandler) { - r.addEndpoint(post, route, handlers) +// Post adds a new POST method route to the router. The path parameter is the route path you wish to +// define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the +// route. +func (r *Router) Post(path string, handler lambda.Handler) { + path = r.prefix + path + r.addEvent(http.MethodPost+path, event{h: handler}) } -// Put creates a new put endpoint. -func (r *APIGRouter) Put(route string, handlers ...APIGHandler) { - r.addEndpoint(put, route, handlers) +// Put adds a new PUT method route to the router. The path parameter is the route path you wish to +// define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the +// route. +func (r *Router) Put(path string, handler lambda.Handler) { + path = r.prefix + path + r.addEvent(http.MethodPut+path, event{h: handler}) } -// Patch creates a new patch endpoint -func (r *APIGRouter) Patch(route string, handlers ...APIGHandler) { - r.addEndpoint(patch, route, handlers) +// Patch adds a new PATCH method route to the router. The path parameter is the route path you wish +// to define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the +// route. +func (r *Router) Patch(path string, handler lambda.Handler) { + path = r.prefix + path + r.addEvent(http.MethodPatch+path, event{h: handler}) } -// Delete creates a new delete endpoint. -func (r *APIGRouter) Delete(route string, handlers ...APIGHandler) { - r.addEndpoint(delete, route, handlers) +// Delete adds a new DELETE method route to the router. The path parameter is the route path you +// wish to define. The handler parameter is a lambda.Handler to invoke if an incoming path matches +// the route. +func (r *Router) Delete(path string, handler lambda.Handler) { + path = r.prefix + path + r.addEvent(http.MethodDelete+path, event{h: handler}) } -// Respond returns an APIGatewayProxyResponse to respond to the lambda request. -func (r *APIGRouter) Respond() events.APIGatewayProxyResponse { - var ( - handlersInterface interface{} - ok bool - status int - respbody []byte - err error +// Invoke implements the lambda.Handler interface for the Router type. +func (r Router) Invoke(ctx context.Context, payload []byte) ([]byte, error) { + var req events.APIGatewayProxyRequest - endpointTree = r.endpoints[r.request.HTTPMethod] - path = strings.TrimPrefix(r.request.Path, r.prefix) - inPath = path - response = events.APIGatewayProxyResponse{} - splitPath = stripSlashesAndSplit(path) - ) - - for p := range r.params { - if r.request.PathParameters[p] != "" { - pval := r.request.PathParameters[p] - for i, v := range splitPath { - if v == pval { - splitPath[i] = "{" + p + "}" - break - } - } - } - } - path = "/" + strings.Join(splitPath, "/") - - if handlersInterface, ok = endpointTree.Get(path); !ok { - respbody, _ = json.Marshal(map[string]string{"error": "no route matching path found"}) - - response.StatusCode = http.StatusNotFound - response.Body = string(respbody) - response.Headers = r.headers - return response + if err := json.Unmarshal(payload, &req); err != nil { + return nil, err } - handlers := handlersInterface.([]APIGHandler) + path := req.Path - for _, handler := range handlers { - ctx := &APIGContext{ - Body: []byte(r.request.Body), - Path: r.request.PathParameters, - QryStr: r.request.QueryStringParameters, - Request: r.request, - Context: r.context, - } - if r.request.RequestContext.Authorizer["claims"] != nil { - ctx.Claims = r.request.RequestContext.Authorizer["claims"].(map[string]interface{}) - } - - handler(ctx) - status, respbody, err = ctx.respDeconstruct() - - if err != nil { - respbody, _ = json.Marshal(map[string]string{"error": err.Error()}) - if strings.Contains(err.Error(), "record not found") { - status = 204 - } else if status != 204 && status < 400 { - status = 400 - } - - log.Printf("%v %v %v error: %v \n", r.request.HTTPMethod, inPath, status, err.Error()) - log.Println("error causing body: " + r.request.Body) - response.StatusCode = status - response.Body = string(respbody) - response.Headers = r.headers - return response - } + for param, value := range req.PathParameters { + path = strings.Replace(path, value, "{"+param+"}", -1) } - response.StatusCode = status - response.Body = string(respbody) - response.Headers = r.headers - return response -} + i, found := r.events.Get([]byte(req.HTTPMethod + path)) -// NOTE: Begin helper functions. -func stripSlashesAndSplit(s string) []string { - s = strings.TrimPrefix(s, "/") - s = strings.TrimSuffix(s, "/") - return strings.Split(s, "/") -} - -func (ctx *APIGContext) respDeconstruct() (int, []byte, error) { - return ctx.Status, ctx.Body, ctx.Err -} - -func (r *APIGRouter) addEndpoint(method string, route string, handlers []APIGHandler) { - if _, overwrite := r.endpoints[method].Insert(route, handlers); overwrite { - panic("endpoint already existent") + if !found { + return json.Marshal(events.APIGatewayProxyResponse{ + StatusCode: http.StatusNotFound, + Body: "not found", + }) } - rtearr := stripSlashesAndSplit(route) - for _, v := range rtearr { - if strings.HasPrefix(v, "{") { - v = strings.TrimPrefix(v, "{") - v = strings.TrimSuffix(v, "}") - r.params[v] = nil // adding params as *unique* keys - } + e := i.(event) + return e.h.Invoke(ctx, payload) +} + +// Group allows you to define many routes with the same prefix. The prefix parameter will apply the +// prefix to all routes defined in the function. The fn parmater is a function in which the grouped +// routes should be defined. +func (r *Router) Group(prefix string, fn func(r *Router)) { + if prefix[0] == '/' { + prefix = prefix[1:] + } + if prefix[len(prefix)-1] != '/' { + prefix += "/" } + original := r.prefix + r.prefix += prefix + fn(r) + r.prefix = original +} + +type event struct { + h lambda.Handler +} + +func (r *Router) addEvent(key string, e event) { + if r.events == nil { + panic("router not initialized") + } + + routes, _, overwrite := r.events.Insert([]byte(key), e) + + if overwrite { + panic(fmt.Sprintf("event '%s' already exists", key)) + } + + r.events = routes } diff --git a/router_test.go b/router_test.go index fe4e567..e417acc 100644 --- a/router_test.go +++ b/router_test.go @@ -1,184 +1,101 @@ package lambdarouter import ( - "errors" + "context" + "encoding/json" + "fmt" "net/http" "testing" "github.com/aws/aws-lambda-go/events" - . "github.com/smartystreets/goconvey/convey" + "github.com/aws/aws-lambda-go/lambda" + "github.com/stretchr/testify/assert" ) -func TestRouterSpec(t *testing.T) { +func TestRouter(t *testing.T) { + a := assert.New(t) - Convey("Given an instantiated router", t, func() { - request := events.APIGatewayProxyRequest{} - rtr := NewAPIGRouter(&APIGRouterConfig{ - Request: &request, - Prefix: "/shipping", - Headers: map[string]string{ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": "true", - }, + desc(t, 0, "Intialize Router and") + r := New("prefix") + handler := lambda.NewHandler(handler) + ctx := context.Background() + + desc(t, 2, "Get|Post|Put|Patch|Delete method should") + { + desc(t, 4, "insert a new route succesfully") + a.NotPanics(func() { + r.Get("thing/{id}", handler) + r.Delete("thing/{id}", handler) + r.Put("thing", handler) }) - Convey("When the handler func does NOT return an error", func() { - hdlrfunc := func(ctx *APIGContext) { - ctx.Status = http.StatusOK - ctx.Body = []byte("hello") - ctx.Err = nil - } - - Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { - rtr.Get("/listings/{id}/state/{event}", hdlrfunc) - rtr.Post("/orders", func(ctx *APIGContext) {}) - rtr.Put("/orders", func(ctx *APIGContext) {}) - rtr.Patch("/orders", func(ctx *APIGContext) {}) - rtr.Delete("/orders/{id}", func(ctx *APIGContext) {}) - - Convey("And the request matches the pattern and the path params are filled", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/shipping/listings/57/state/list" - request.PathParameters = map[string]string{ - "id": "57", - "event": "list", - } - request.RequestContext.Authorizer = map[string]interface{}{ - "claims": map[string]interface{}{ - "cognito:username": "mitchell", - }, - } - - Convey("The router will return the expected status, body, and headers", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusOK) - So(response.Body, ShouldEqual, "hello") - So(response.Headers, ShouldResemble, map[string]string{ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": "true", - }) - }) - }) - - Convey("And the request does NOT match the pattern", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/orders/filter" - - Convey("The router will return an error body and a status not found", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusNotFound) - So(response.Body, ShouldEqual, "{\"error\":\"no route matching path found\"}") - So(response.Headers, ShouldResemble, map[string]string{ - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Credentials": "true", - }) - }) - }) - - Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined AGAIN", func() { - So(func() { - rtr.Get("/listings/{id}/state/{event}", hdlrfunc) - }, ShouldPanicWith, "endpoint already existent") - }) - - Convey("And a Get handler expecting the pattern /orders/filter", func() { - rtr.Get("/orders/filter", hdlrfunc) - - Convey("And the request matches the pattern and the path params are filled", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/shipping/orders/filter" - - Convey("The router will return the expected status and body", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusOK) - So(response.Body, ShouldEqual, "hello") - }) - }) - - Convey("And the request does NOT match either of the patterns", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/shipping/orders/filter/by_user" - - Convey("The router will return an error body and a status not found", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusNotFound) - So(response.Body, ShouldEqual, "{\"error\":\"no route matching path found\"}") - }) - }) - }) - }) - + desc(t, 4, "panic when inserting the same route") + a.Panics(func() { + r.Put("thing", handler) }) - Convey("When the handler func does return a record not found", func() { - hdlrfunc := func(ctx *APIGContext) { - ctx.Status = http.StatusNoContent - ctx.Body = []byte("hello") - ctx.Err = errors.New("record not found") - - } - - Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { - rtr.Get("/listings/{id}/state/{event}", hdlrfunc) - - Convey("And the request matches the pattern and the path params are filled", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/shipping/listings/57/state/list" - request.PathParameters = map[string]string{ - "id": "57", - "event": "list", - } - - Convey("The router will return the expected status and body", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusNoContent) - So(response.Body, ShouldEqual, "{\"error\":\"record not found\"}") - }) - }) - }) + desc(t, 4, "panic when router is uninitalized") + var r2 Router + a.Panics(func() { + r2.Patch("panic", handler) }) + } - Convey("When the handler func does return a status < 400", func() { - middlefunc1 := func(ctx *APIGContext) { - ctx.Status = http.StatusOK - ctx.Body = []byte("hello") - ctx.Err = nil - } - middlefunc2 := func(ctx *APIGContext) { - ctx.Status = http.StatusOK - ctx.Body = []byte("hello") - ctx.Err = errors.New("bad request") - } - hdlrfunc := func(ctx *APIGContext) { - ctx.Status = http.StatusOK - ctx.Body = []byte("hello") - ctx.Err = nil - } - - Convey("And a Get handler expecting the pattern /listings/{id}/state/{event} is defined", func() { - rtr.Get("/listings/{id}/state/{event}", middlefunc1, middlefunc2, hdlrfunc) - - Convey("And the request matches the pattern and the path params are filled", func() { - request.HTTPMethod = http.MethodGet - request.Path = "/shipping/listings/57/state/list" - request.PathParameters = map[string]string{ - "id": "57", - "event": "list", - } - - Convey("The router will return the expected status and body", func() { - response := rtr.Respond() - - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) - So(response.Body, ShouldEqual, "{\"error\":\"bad request\"}") - }) - }) - }) + desc(t, 2, "PrefixGroup method should") + { + desc(t, 4, "insert routes with the specified prefix succesfully") + r.Group("/ding", func(r *Router) { + r.Post("dong/{door}", handler) }) - }) + } + + desc(t, 2, "Invoke method should") + { + e := events.APIGatewayProxyRequest{ + Path: "/prefix/ding/dong/mitchell", + HTTPMethod: http.MethodPost, + PathParameters: map[string]string{"door": "mitchell"}, + } + + desc(t, 4, "should succesfully route and invoke a defined route") + ejson, _ := json.Marshal(e) + + res, err := r.Invoke(ctx, ejson) + + a.NoError(err) + a.Exactly("null", string(res)) + + desc(t, 4, "return the expected response when a route is not found") + e.Path = "thing" + e.PathParameters = nil + ejson2, _ := json.Marshal(e) + eres := events.APIGatewayProxyResponse{ + StatusCode: http.StatusNotFound, + Body: "not found", + } + eresjson, _ := json.Marshal(eres) + + res, err = r.Invoke(ctx, ejson2) + + a.NoError(err) + a.ElementsMatch(eresjson, res) + + desc(t, 4, "return an error when the there is an issue with the incoming event") + _, err = r.Invoke(ctx, nil) + + a.Error(err) + } + +} + +func handler() error { + return nil +} + +func desc(t *testing.T, depth int, str string, args ...interface{}) { + for i := 0; i < depth; i++ { + str = " " + str + } + + t.Log(fmt.Sprintf(str, args...)) } From 6cbf67263514a9b6d4a37ed0b013b6e943a4670b Mon Sep 17 00:00:00 2001 From: mitchell Date: Sun, 21 Apr 2019 17:37:39 -0700 Subject: [PATCH 17/18] Update LICENSE --- LICENSE | 216 ++++++-------------------------------------------------- 1 file changed, 20 insertions(+), 196 deletions(-) diff --git a/LICENSE b/LICENSE index 307a4a0..7f09bc2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,25 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +BSD 2-Clause License - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Copyright (c) 2019, The Lambdarouter Author(s) +All rights reserved. - 1. Definitions. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Mitchell Simon - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 3c33e95efd962dfee517a0961599f8d368ef9cce Mon Sep 17 00:00:00 2001 From: Mitchell Date: Wed, 12 Jun 2019 00:38:03 -0700 Subject: [PATCH 18/18] Refactored path preparation for http event adding --- router.go | 54 +++++++++++++++++++++++++++++++++----------------- router_test.go | 7 ++++++- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/router.go b/router.go index b88a434..8e1fcf6 100644 --- a/router.go +++ b/router.go @@ -18,13 +18,15 @@ type Router struct { prefix string } -// New initializes an empty router. +// New initializes an empty router. The prefix parameter may be of any length. func New(prefix string) Router { - if prefix[0] != '/' { - prefix = "/" + prefix - } - if prefix[len(prefix)-1] != '/' { - prefix += "/" + if len(prefix) > 0 { + if prefix[0] != '/' { + prefix = "/" + prefix + } + if prefix[len(prefix)-1] != '/' { + prefix += "/" + } } return Router{ @@ -37,40 +39,35 @@ func New(prefix string) Router { // define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the // route. func (r *Router) Get(path string, handler lambda.Handler) { - path = r.prefix + path - r.addEvent(http.MethodGet+path, event{h: handler}) + r.addEvent(prepPath(http.MethodGet, r.prefix, path), event{h: handler}) } // Post adds a new POST method route to the router. The path parameter is the route path you wish to // define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the // route. func (r *Router) Post(path string, handler lambda.Handler) { - path = r.prefix + path - r.addEvent(http.MethodPost+path, event{h: handler}) + r.addEvent(prepPath(http.MethodPost, r.prefix, path), event{h: handler}) } // Put adds a new PUT method route to the router. The path parameter is the route path you wish to // define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the // route. func (r *Router) Put(path string, handler lambda.Handler) { - path = r.prefix + path - r.addEvent(http.MethodPut+path, event{h: handler}) + r.addEvent(prepPath(http.MethodPut, r.prefix, path), event{h: handler}) } // Patch adds a new PATCH method route to the router. The path parameter is the route path you wish // to define. The handler parameter is a lambda.Handler to invoke if an incoming path matches the // route. func (r *Router) Patch(path string, handler lambda.Handler) { - path = r.prefix + path - r.addEvent(http.MethodPatch+path, event{h: handler}) + r.addEvent(prepPath(http.MethodPatch, r.prefix, path), event{h: handler}) } // Delete adds a new DELETE method route to the router. The path parameter is the route path you // wish to define. The handler parameter is a lambda.Handler to invoke if an incoming path matches // the route. func (r *Router) Delete(path string, handler lambda.Handler) { - path = r.prefix + path - r.addEvent(http.MethodDelete+path, event{h: handler}) + r.addEvent(prepPath(http.MethodDelete, r.prefix, path), event{h: handler}) } // Invoke implements the lambda.Handler interface for the Router type. @@ -100,10 +97,12 @@ func (r Router) Invoke(ctx context.Context, payload []byte) ([]byte, error) { return e.h.Invoke(ctx, payload) } -// Group allows you to define many routes with the same prefix. The prefix parameter will apply the -// prefix to all routes defined in the function. The fn parmater is a function in which the grouped +// Group allows you to define many routes with the same prefix. The prefix parameter will be applied +// to all routes defined in the function. The fn parameter is a function in which the grouped // routes should be defined. func (r *Router) Group(prefix string, fn func(r *Router)) { + validatePathPart(prefix) + if prefix[0] == '/' { prefix = prefix[1:] } @@ -134,3 +133,22 @@ func (r *Router) addEvent(key string, e event) { r.events = routes } + +func prepPath(method, prefix, path string) string { + validatePathPart(path) + + if path[0] == '/' { + path = path[1:] + } + if path[len(path)-1] == '/' { + path = path[:len(path)-1] + } + + return method + prefix + path +} + +func validatePathPart(part string) { + if len(part) == 0 { + panic("path was empty") + } +} diff --git a/router_test.go b/router_test.go index e417acc..7896967 100644 --- a/router_test.go +++ b/router_test.go @@ -25,7 +25,7 @@ func TestRouter(t *testing.T) { desc(t, 4, "insert a new route succesfully") a.NotPanics(func() { r.Get("thing/{id}", handler) - r.Delete("thing/{id}", handler) + r.Delete("/thing/{id}/", handler) r.Put("thing", handler) }) @@ -39,6 +39,11 @@ func TestRouter(t *testing.T) { a.Panics(func() { r2.Patch("panic", handler) }) + + desc(t, 4, "panic when when given an empty path") + a.Panics(func() { + r.Post("", handler) + }) } desc(t, 2, "PrefixGroup method should")