From 1756af32869b2a7065d66ab6138b6f444368a74b Mon Sep 17 00:00:00 2001 From: Diffuse <48339639+diffuse@users.noreply.github.com> Date: Sun, 5 Jul 2020 12:39:58 -0400 Subject: [PATCH 1/3] Abstract routing with an interface --- chi/handlers.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/gloss/main.go | 22 ++++--------- domain.go | 10 ++++++ handlers.go | 51 ---------------------------- pgsql/db.go | 2 +- routes.go | 11 ------- 6 files changed, 102 insertions(+), 78 deletions(-) create mode 100644 chi/handlers.go delete mode 100644 handlers.go delete mode 100644 routes.go diff --git a/chi/handlers.go b/chi/handlers.go new file mode 100644 index 0000000..ad5b7e7 --- /dev/null +++ b/chi/handlers.go @@ -0,0 +1,84 @@ +package chi + +import ( + "github.com/diffuse/gloss" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "net/http" + "strconv" +) + +type Router struct { + *chi.Mux + db gloss.Database +} + +// NewRouter creates a router, associates a database +// with it, and mounts API routes +func NewRouter(db gloss.Database) *Router { + r := &Router{} + r.setupRoutes() + r.SetDb(db) + + return r +} + +// setupRoutes +func (rt *Router) setupRoutes() { + // set routes + routes := chi.NewRouter() + routes.Post("/counter/{counterId}", rt.IncrementCounterById) + routes.Get("/counter/{counterId}", rt.GetCounterById) + + // setup router + rt.Mux = chi.NewRouter() + + // set middleware + rt.Mux.Use( + middleware.Logger, + middleware.Recoverer) + + // mount routes on versioned path + rt.Mux.Mount("/v1", routes) +} + +// SetDb associates a gloss.Database with this router +func (rt *Router) SetDb(db gloss.Database) { + rt.db = db +} + +// Increment the value of the counter with ID counterId +func (rt *Router) IncrementCounterById(w http.ResponseWriter, r *http.Request) { + // get the counter ID + counterId, err := strconv.ParseUint(chi.URLParam(r, "counterId"), 10, 32) + if err != nil { + http.Error(w, "invalid counter ID", http.StatusBadRequest) + return + } + + // increment in db + if err := rt.db.IncrementCounter(int(counterId)); err != nil { + http.Error(w, "failed to increment counter value", http.StatusInternalServerError) + } +} + +// Get the value of the counter with ID counterId +func (rt *Router) GetCounterById(w http.ResponseWriter, r *http.Request) { + // get the counter ID + counterId, err := strconv.ParseInt(chi.URLParam(r, "counterId"), 10, 32) + if err != nil { + http.Error(w, "invalid counter ID", http.StatusBadRequest) + return + } + + // get value and return to requester + val, err := rt.db.GetCounterVal(int(counterId)) + if err != nil { + http.Error(w, "failed to get counter value", http.StatusNotFound) + return + } + + if _, err := w.Write([]byte(strconv.Itoa(val))); err != nil { + panic(err) + } +} diff --git a/cmd/gloss/main.go b/cmd/gloss/main.go index 65de2b0..0e425e5 100644 --- a/cmd/gloss/main.go +++ b/cmd/gloss/main.go @@ -1,28 +1,20 @@ package main import ( - "github.com/diffuse/gloss" - "github.com/go-chi/chi" - "github.com/go-chi/chi/middleware" + "github.com/diffuse/gloss/chi" + "github.com/diffuse/gloss/pgsql" "log" "net/http" "time" ) func main() { - // close the db connections on exit - defer gloss.Db.Close() + // create a thread-safe database instance for use with the router + db := pgsql.NewDatabase() + defer db.Close() - // setup router - r := chi.NewRouter() - - // set middleware - r.Use( - middleware.Logger, - middleware.Recoverer) - - // mount routes on versioned path - r.Mount("/v1", gloss.GetRoutes()) + // create a router and associate the database with it + r := chi.NewRouter(db) // create server with some reasonable defaults s := &http.Server{ diff --git a/domain.go b/domain.go index 84a645d..5d21391 100644 --- a/domain.go +++ b/domain.go @@ -1,5 +1,7 @@ package gloss +import "net/http" + // Database represents a wrapper around a database connection + driver // // additional methods can be added here, then implemented in custom packages @@ -15,3 +17,11 @@ type Database interface { IncrementCounter(counterId int) error GetCounterVal(counterId int) (int, error) } + +// Router represents a router that implements +// both the net/http Handler interface, as well as +// a method to associate a database with its handlers +type Router interface { + http.Handler + SetDb(db Database) error +} diff --git a/handlers.go b/handlers.go deleted file mode 100644 index b082d13..0000000 --- a/handlers.go +++ /dev/null @@ -1,51 +0,0 @@ -package gloss - -import ( - "github.com/diffuse/gloss/pgsql" - "github.com/go-chi/chi" - "net/http" - "strconv" -) - -// Db is the thread-safe database that will be used by the handlers -var Db Database - -func init() { - Db = pgsql.NewDatabase() -} - -// Increment the value of the counter with ID counterId -func IncrementCounterById(w http.ResponseWriter, r *http.Request) { - // get the counter ID - counterId, err := strconv.ParseUint(chi.URLParam(r, "counterId"), 10, 32) - if err != nil { - http.Error(w, "invalid counter ID", http.StatusBadRequest) - return - } - - // increment in db - if err := Db.IncrementCounter(int(counterId)); err != nil { - http.Error(w, "failed to increment counter value", http.StatusInternalServerError) - } -} - -// Get the value of the counter with ID counterId -func GetCounterById(w http.ResponseWriter, r *http.Request) { - // get the counter ID - counterId, err := strconv.ParseInt(chi.URLParam(r, "counterId"), 10, 32) - if err != nil { - http.Error(w, "invalid counter ID", http.StatusBadRequest) - return - } - - // get value and return to requester - val, err := Db.GetCounterVal(int(counterId)) - if err != nil { - http.Error(w, "failed to get counter value", http.StatusNotFound) - return - } - - if _, err := w.Write([]byte(strconv.Itoa(val))); err != nil { - panic(err) - } -} diff --git a/pgsql/db.go b/pgsql/db.go index 2069946..fefe257 100644 --- a/pgsql/db.go +++ b/pgsql/db.go @@ -6,7 +6,7 @@ import ( ) type Database struct { - db *pgx.Conn + db *pgx.Conn ctx context.Context } diff --git a/routes.go b/routes.go deleted file mode 100644 index 8a991bb..0000000 --- a/routes.go +++ /dev/null @@ -1,11 +0,0 @@ -package gloss - -import "github.com/go-chi/chi" - -func GetRoutes() *chi.Mux { - r := chi.NewRouter() - r.Post("/counter/{counterId}", IncrementCounterById) - r.Get("/counter/{counterId}", GetCounterById) - - return r -} From fa840b45ab09bb4ac8dc2a83d35c330a1769e68e Mon Sep 17 00:00:00 2001 From: Diffuse <48339639+diffuse@users.noreply.github.com> Date: Sun, 5 Jul 2020 13:23:21 -0400 Subject: [PATCH 2/3] Update README and remove unused Router interface --- README.md | 32 +++++++++++++++++++++++--------- chi/handlers.go | 8 +------- domain.go | 10 ---------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index e949e70..9acef44 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ gloss (Golang open simple service) provides boilerplate routing, database setup, microservice up and running. It includes example code to increment and retrieve counter values from a PostgreSQL database. Ideally, one would fork, or clone + mirror push this repository, then edit the handlers + routes, database queries, and configurations for their own purposes. It uses [chi](https://github.com/go-chi/chi) for routing, and -[pgx](https://github.com/jackc/pgx) for its PostgreSQL database driver. +[pgx](https://github.com/jackc/pgx) for its PostgreSQL database driver, but other databases or routers can be used +by implementing the `Database` interface from `domain.go`, and/or the `Handler` interface from `net/http`. ### Prerequisites - [Docker](https://www.docker.com/) @@ -73,14 +74,27 @@ psql -h localhost -p 5432 -U test -d test ``` ### Adapting to your own implementation -- Add your business logic methods to the `gloss.Database` interface (found in `domain.go`, then implement it in a -custom package (e.g. the example counter `IncrementCounter` and `GetCounterVal` methods, implemented in the pgsql -package) - - You can also just edit the pgsql package and change the domain interface methods if you want to use PostgreSQL -- Use your `gloss.Database` implementation by assigning an instance of your package's Database to the `Db` var in -`handlers.go` -- Replace/change the example handler bodies in `handlers.go` to perform your business logic -- Update the routes in `routes.go` to use your handlers +#### Database +- Add your business logic methods to the `Database` interface (found in `domain.go`), then implement them in a +custom package + - See the `IncrementCounter` and `GetCounterVal` methods for an example of this, implemented in the `pgsql` +package + - You can also just edit the `pgsql` package and change the domain interface methods if you want to continue using + PostgreSQL + +#### Routing +- Implement the `net/http` `Handler` interface, or edit the existing implementation in the `chi` package + - Replace/change the example handler bodies in `handlers.go` to perform your business logic + - Update the routes in `handlers.go`:`setupRoutes` to use your handlers + +#### Putting it all together +- Pass an instance of your `Database` implementation to your custom `Handler`, and use the instance to perform +business logic in handler functions + - An example of this is shown in `cmd/gloss/main.go`, where `pgsql`'s `Database` implementation is used with `chi`'s + `Handler` implementation + - A `pgsql.Database` instance is created, then passed as a parameter to `chi.NewRouter`, which creates a router, + associates handler functions in the `chi` package with routes, then stores the `pgsql.Database` instance so it can + be used in the custom handlers ### Disclaimer and considerations for deployment The deployment scripts, configurations, and any defaults included in this repository are not, under any circumstances, diff --git a/chi/handlers.go b/chi/handlers.go index ad5b7e7..68ca452 100644 --- a/chi/handlers.go +++ b/chi/handlers.go @@ -16,9 +16,8 @@ type Router struct { // NewRouter creates a router, associates a database // with it, and mounts API routes func NewRouter(db gloss.Database) *Router { - r := &Router{} + r := &Router{db: db} r.setupRoutes() - r.SetDb(db) return r } @@ -42,11 +41,6 @@ func (rt *Router) setupRoutes() { rt.Mux.Mount("/v1", routes) } -// SetDb associates a gloss.Database with this router -func (rt *Router) SetDb(db gloss.Database) { - rt.db = db -} - // Increment the value of the counter with ID counterId func (rt *Router) IncrementCounterById(w http.ResponseWriter, r *http.Request) { // get the counter ID diff --git a/domain.go b/domain.go index 5d21391..84a645d 100644 --- a/domain.go +++ b/domain.go @@ -1,7 +1,5 @@ package gloss -import "net/http" - // Database represents a wrapper around a database connection + driver // // additional methods can be added here, then implemented in custom packages @@ -17,11 +15,3 @@ type Database interface { IncrementCounter(counterId int) error GetCounterVal(counterId int) (int, error) } - -// Router represents a router that implements -// both the net/http Handler interface, as well as -// a method to associate a database with its handlers -type Router interface { - http.Handler - SetDb(db Database) error -} From f57158efedbc84522571e5c445801ae4160d150d Mon Sep 17 00:00:00 2001 From: Diffuse <48339639+diffuse@users.noreply.github.com> Date: Sun, 5 Jul 2020 13:32:12 -0400 Subject: [PATCH 3/3] Add some status messages on startup --- cmd/gloss/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/gloss/main.go b/cmd/gloss/main.go index 0e425e5..73c0cd0 100644 --- a/cmd/gloss/main.go +++ b/cmd/gloss/main.go @@ -10,10 +10,12 @@ import ( func main() { // create a thread-safe database instance for use with the router + log.Println("connecting to database") db := pgsql.NewDatabase() defer db.Close() // create a router and associate the database with it + log.Println("setting up routes") r := chi.NewRouter(db) // create server with some reasonable defaults @@ -27,5 +29,6 @@ func main() { } // serve + log.Println("serving") log.Fatal(s.ListenAndServe()) }