commit 0b30efee8960165b3be1f4003e430c145fbbfca0 Author: mitchelljfs Date: Fri Jul 13 14:12:31 2018 -0700 Initial commit of lambdarouter package including first iteration created; no docs; no tests as of yet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..22d0d82 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..260979e --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,21 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/armon/go-radix" + packages = ["."] + revision = "1fca145dffbcaa8fe914309b1ec0cfc67500fe61" + +[[projects]] + name = "github.com/aws/aws-lambda-go" + packages = ["events"] + revision = "4d30d0ff60440c2d0480a15747c96ee71c3c53d4" + version = "v1.2.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "35d3fdde995b8cf314934259f07ffb089e30092e11fb8e404ea398ce1c76a431" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..5a78b31 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# 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" + +[prune] + go-tests = true + unused-packages = true diff --git a/README.md b/README.md new file mode 100644 index 0000000..a116e47 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# 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. diff --git a/router.go b/router.go new file mode 100644 index 0000000..a4b9669 --- /dev/null +++ b/router.go @@ -0,0 +1,168 @@ +package lambdarouter + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + radix "github.com/armon/go-radix" + "github.com/aws/aws-lambda-go/events" +) + +// Method constants. +const ( + POST = http.MethodPost + GET = http.MethodGet + PUT = http.MethodPut + DELETE = http.MethodDelete +) + +// HandlerRequest ... +type HandlerRequest struct { + Claims map[string]string + Path map[string]string + QryStr map[string]string + Request *events.APIGatewayProxyRequest +} + +// HandlerResponse ... +type HandlerResponse struct { + Status int + Body []byte + Err error +} + +// Handler ... +type Handler func(req *HandlerRequest, res *HandlerResponse) + +// Router ... +type Router struct { + request *events.APIGatewayProxyRequest + endpoints map[string]*radix.Tree + params map[string]string + svcprefix string +} + +// NOTE: Begin router methods. + +// New ... +func New(r *events.APIGatewayProxyRequest, svcprefix string) *Router { + return &Router{ + request: r, + endpoints: map[string]*radix.Tree{ + POST: radix.New(), + GET: radix.New(), + PUT: radix.New(), + DELETE: radix.New(), + }, + params: map[string]string{}, + svcprefix: svcprefix, + } +} + +// Get ... +func (r *Router) Get(route string, handler Handler) { + r.addEndpoint(GET, route, handler) +} + +// Post ... +func (r *Router) Post(route string, handler Handler) { + r.addEndpoint(POST, route, handler) +} + +// Put ... +func (r *Router) Put(route string, handler Handler) { + r.addEndpoint(PUT, route, handler) +} + +// Delete ... +func (r *Router) Delete(route string, handler Handler) { + r.addEndpoint(DELETE, route, handler) +} + +// Respond ... +func (r *Router) Respond() events.APIGatewayProxyResponse { + var ( + handlerInterface interface{} + ok bool + + endpointTree = r.endpoints[r.request.HTTPMethod] + path = strings.TrimPrefix(r.request.Path, "/"+r.svcprefix) + response = events.APIGatewayProxyResponse{} + ) + log.Printf("path: %+v", 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) + } + } + log.Printf("path: %+v", path) + + if handlerInterface, 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) + return response + } + + handler := handlerInterface.(Handler) + + req := &HandlerRequest{ + Claims: r.request.RequestContext.Authorizer["claims"].(map[string]string), + Path: r.request.PathParameters, + QryStr: r.request.QueryStringParameters, + Request: r.request, + } + res := &HandlerResponse{} + + handler(req, res) + status, respbody, err := res.deconstruct() + + 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 < 400 { + status = 400 + } + + response.StatusCode = status + response.Body = string(respbody) + return response + } + + response.StatusCode = status + response.Body = string(respbody) + return response +} + +// NOTE: Begin helper functions. +func stripSlashesAndSplit(s string) []string { + s = strings.TrimPrefix(s, "/") + s = strings.TrimSuffix(s, "/") + return strings.Split(s, "/") +} + +func (res *HandlerResponse) deconstruct() (int, []byte, error) { + return res.Status, res.Body, res.Err +} + +func (r *Router) addEndpoint(method string, route string, handler Handler) { + if _, overwrite := r.endpoints[method].Insert(route, handler); overwrite { + panic("endpoint already existent") + } + + rtearr := stripSlashesAndSplit(route) + for _, v := range rtearr { + if strings.HasPrefix(v, "{") { + r.params[v] = "" // adding params as keys with {brackets} + } + } + + log.Printf("router: %+v", *r) +}