From 6f74d650ab47bccee662927fad67bc424915ce7d Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sat, 12 Mar 2022 14:19:33 -0800 Subject: [PATCH] break nject into five repos (#24) --- nject/INTERNALS.md => INTERNALS.md | 0 README.md | 246 ++-- nject/api.go => api.go | 0 nject/bind.go => bind.go | 0 nject/bind_test.go => bind_test.go | 0 nject/cache.go => cache.go | 0 nject/cache_test.go => cache_test.go | 0 nject/characterize.go => characterize.go | 0 ...aracterize_test.go => characterize_test.go | 0 nject/debug.go => debug.go | 0 nject/debug_test.go => debug_test.go | 0 nject/doc.go => doc.go | 0 nject/dummy_test.go => dummy_test.go | 0 nject/error.go => error.go | 0 ...ample_bind_test.go => example_bind_test.go | 2 +- ...cluster_test.go => example_cluster_test.go | 2 +- ...tion_test.go => example_collection_test.go | 2 +- .../example_db_test.go => example_db_test.go | 2 +- ...rated_test.go => example_generated_test.go | 2 +- ...memoize_test.go => example_memoize_test.go | 2 +- ...call_test.go => example_methodcall_test.go | 2 +- ...me_test.go => example_must_consume_test.go | 2 +- ...on2_test.go => example_postaction2_test.go | 2 +- ...tion_test.go => example_postaction_test.go | 2 +- ...ovider_test.go => example_provider_test.go | 2 +- ...example_run_test.go => example_run_test.go | 2 +- ...ack_test.go => example_setcallback_test.go | 2 +- ...leton_test.go => example_singleton_test.go | 2 +- nject/example_test.go => example_test.go | 2 +- nject/filler.go => filler.go | 0 nject/filler_api.go => filler_api.go | 0 nject/filler_test.go => filler_test.go | 0 nject/generate.go => generate.go | 0 go.mod | 9 +- go.sum | 56 - nject/include.go => include.go | 0 nject/match.go => match.go | 0 nject/match_test.go => match_test.go | 0 nject/nject.go => nject.go | 0 nject/README.md | 75 -- nject/nject_test.go => nject_test.go | 0 npoint/README.md | 86 -- npoint/doc.go | 228 ---- npoint/endpoint_test.go | 541 --------- npoint/example_create_endpoint_test.go | 48 - npoint/example_preregister_test.go | 134 --- npoint/gorilla.go | 146 --- npoint/mux.go | 169 --- npoint/npoint.go | 203 ---- npoint/regression_test.go | 76 -- npoint/service_test.go | 478 -------- nserve/README.md | 71 -- nserve/example_test.go | 75 -- nserve/hook.go | 114 -- nserve/nserve.go | 97 -- nvelope/README.md | 100 -- nvelope/debug.go | 14 - nvelope/decode.go | 1040 ----------------- nvelope/decode_test.go | 190 --- nvelope/deferred.go | 146 --- nvelope/deferred_test.go | 142 --- nvelope/doc.go | 30 - nvelope/encode.go | 261 ----- nvelope/errors.go | 71 -- nvelope/errors_test.go | 19 - nvelope/example_middleware_test.go | 126 -- nvelope/example_mwhandler_test.go | 126 -- nvelope/example_panic_test.go | 29 - nvelope/example_test.go | 111 -- nvelope/helpers_test.go | 105 -- nvelope/logger.go | 68 -- nvelope/middleware.go | 151 --- nvelope/panic.go | 88 -- ...regressions_test.go => regressions_test.go | 0 nject/run_test.go => run_test.go | 0 ...setcallback_test.go => setcallback_test.go | 0 nject/type_codes.go => type_codes.go | 0 nject/types.go => types.go | 0 78 files changed, 99 insertions(+), 5600 deletions(-) rename nject/INTERNALS.md => INTERNALS.md (100%) rename nject/api.go => api.go (100%) rename nject/bind.go => bind.go (100%) rename nject/bind_test.go => bind_test.go (100%) rename nject/cache.go => cache.go (100%) rename nject/cache_test.go => cache_test.go (100%) rename nject/characterize.go => characterize.go (100%) rename nject/characterize_test.go => characterize_test.go (100%) rename nject/debug.go => debug.go (100%) rename nject/debug_test.go => debug_test.go (100%) rename nject/doc.go => doc.go (100%) rename nject/dummy_test.go => dummy_test.go (100%) rename nject/error.go => error.go (100%) rename nject/example_bind_test.go => example_bind_test.go (98%) rename nject/example_cluster_test.go => example_cluster_test.go (97%) rename nject/example_collection_test.go => example_collection_test.go (96%) rename nject/example_db_test.go => example_db_test.go (99%) rename nject/example_generated_test.go => example_generated_test.go (98%) rename nject/example_memoize_test.go => example_memoize_test.go (97%) rename nject/example_methodcall_test.go => example_methodcall_test.go (92%) rename nject/example_must_consume_test.go => example_must_consume_test.go (98%) rename nject/example_postaction2_test.go => example_postaction2_test.go (96%) rename nject/example_postaction_test.go => example_postaction_test.go (98%) rename nject/example_provider_test.go => example_provider_test.go (98%) rename nject/example_run_test.go => example_run_test.go (93%) rename nject/example_setcallback_test.go => example_setcallback_test.go (97%) rename nject/example_singleton_test.go => example_singleton_test.go (96%) rename nject/example_test.go => example_test.go (98%) rename nject/filler.go => filler.go (100%) rename nject/filler_api.go => filler_api.go (100%) rename nject/filler_test.go => filler_test.go (100%) rename nject/generate.go => generate.go (100%) rename nject/include.go => include.go (100%) rename nject/match.go => match.go (100%) rename nject/match_test.go => match_test.go (100%) rename nject/nject.go => nject.go (100%) delete mode 100644 nject/README.md rename nject/nject_test.go => nject_test.go (100%) delete mode 100644 npoint/README.md delete mode 100644 npoint/doc.go delete mode 100644 npoint/endpoint_test.go delete mode 100644 npoint/example_create_endpoint_test.go delete mode 100644 npoint/example_preregister_test.go delete mode 100644 npoint/gorilla.go delete mode 100644 npoint/mux.go delete mode 100644 npoint/npoint.go delete mode 100644 npoint/regression_test.go delete mode 100644 npoint/service_test.go delete mode 100644 nserve/README.md delete mode 100644 nserve/example_test.go delete mode 100644 nserve/hook.go delete mode 100644 nserve/nserve.go delete mode 100644 nvelope/README.md delete mode 100644 nvelope/debug.go delete mode 100644 nvelope/decode.go delete mode 100644 nvelope/decode_test.go delete mode 100644 nvelope/deferred.go delete mode 100644 nvelope/deferred_test.go delete mode 100644 nvelope/doc.go delete mode 100644 nvelope/encode.go delete mode 100644 nvelope/errors.go delete mode 100644 nvelope/errors_test.go delete mode 100644 nvelope/example_middleware_test.go delete mode 100644 nvelope/example_mwhandler_test.go delete mode 100644 nvelope/example_panic_test.go delete mode 100644 nvelope/example_test.go delete mode 100644 nvelope/helpers_test.go delete mode 100644 nvelope/logger.go delete mode 100644 nvelope/middleware.go delete mode 100644 nvelope/panic.go rename nject/regressions_test.go => regressions_test.go (100%) rename nject/run_test.go => run_test.go (100%) rename nject/setcallback_test.go => setcallback_test.go (100%) rename nject/type_codes.go => type_codes.go (100%) rename nject/types.go => types.go (100%) diff --git a/nject/INTERNALS.md b/INTERNALS.md similarity index 100% rename from nject/INTERNALS.md rename to INTERNALS.md diff --git a/README.md b/README.md index 82444ef..78ac1d8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# nject, npoint, nserve, & nvelope - dependency injection +# nject - dependency injection [![GoDoc](https://godoc.org/github.com/muir/nject?status.png)](https://pkg.go.dev/github.com/muir/nject) ![unit tests](https://github.com/muir/nject/actions/workflows/go.yml/badge.svg) @@ -6,25 +6,24 @@ [![codecov](https://codecov.io/gh/muir/nject/branch/main/graph/badge.svg)](https://codecov.io/gh/muir/nject) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fmuir%2Fnject.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fmuir%2Fnject?ref=badge_shield) - Install: go get github.com/muir/nject --- -This is a quartet of packages that together make up a most of a -golang API server framework: - -nject: type safe dependency injection w/o requiring type assertions. - -npoint: dependency injection wrappers for binding http endpoint handlers - -nvelope: injection chains for building endpoints +Prior to release 0.20, nject was bundled with other packages. Those +other packages are now in their own repos: +[npoint](https://github.com/muir/npoint) +[nserve](https://github.com/muir/nserve) +[nvelope](https://github.com/muir/nvelope). Additionally, npoint +was split apart so that the gorilla dependency is separate and is in +[nape](https://github.com/muir/nape). -nserve: injection chains for for starting and stopping servers +--- -### Basic idea +This package provides type-safe dependency injection without requiring +users to do type assertions. Dependencies are injected via a call chain: list functions to be called that take and return various parameters. The functions will be called @@ -43,7 +42,7 @@ remaing list zero or more times. Chains may be pre-compiled into closures so that they have very little runtime penealty. -### nject example +### example func example() { // Sequences can be reused. @@ -64,158 +63,75 @@ runtime penealty. }) } -### npoint example - -CreateEndpoint is the simplest way to start using the npoint framework. It -generates an http.HandlerFunc from a list of handlers. The handlers will be called -in order. In the example below, first WriteErrorResponse() will be called. It -has an inner() func that it uses to invoke the rest of the chain. When -WriteErrorResponse() calls its inner() function, the db injector returned by -InjectDB is called. If that does not return error, then the inline function below -to handle the endpint is called. - - mux := http.NewServeMux() - mux.HandleFunc("/my/endpoint", npoint.CreateEndpoint( - WriteErrorResponse, - InjectDB("postgres", "postgres://..."), - func(r *http.Request, db *sql.DB, w http.ResponseWriter) error { - // Write response to w or return error... - return nil - })) - -WriteErrorResponse invokes the remainder of the handler chain by calling inner(). - - func WriteErrorResponse(inner func() nject.TerminalError, w http.ResponseWriter) { - err := inner() - if err != nil { - w.Write([]byte(err.Error())) - w.WriteHeader(500) - } - } +### Main APIs -InjectDB returns a handler function that opens a database connection. If the open -fails, executation of the handler chain is terminated. InjectDB returns an injector -so that it can be called with arguments -- injectors are functions, not invocations -and so we need to return a function. InjectDB also closes the database connection. - - func InjectDB(driver, uri string) func(func(*sql.DB) error) error { - return func(inner func(*sql.DB) error) (finalError error) { - db, err := sql.Open(driver, uri) - if err != nil { - return err - } - defer func() { - err := db.Close() - if err != nil && finalError == nil { - finalError = err - } - }() - return inner(db) - } - } +Nject provides two main APIs: Bind() and Run(). + +Bind() is used when performance matters: given a chain of providers, +it will write two functions: one to initialize the chain and another to +invoke it. As much as possible, all dependency injection work is done +at the time of binding and initialization so that the invoke function +operates with very little overhead. The chain is initialized when the +initialize function is called. The chain is run when the invoke function +is called. Bind() does not run the chain. + +Run() is used when ad-hoc injection is desired and performance is not +critical. Run is appropriate when starting servers and running tests. +It is not reccomended for http endpoint handlers. Run exectes the +chain immediately. + +### Identified by type + +Rather than naming values, inputs and outputs are identified by their types. +Since Go makes it easy to create new types, this turns out to be quite easy to use. + +### Types of providers + +Multiple types of providers are supported. + +#### Literal values + +You can provide a constant if you have one. + +#### Injectors + +Regular functions can provide values. Injectors will be called at +initialization time when they're marked as cacheable or at invocation +time if they're not. + +Injectors can be memoized. + +Injectors can return a special error type that stops the chain. + +Injectors can use data produced by earlier injectors simply by having +a function parameter that matches the type of a return value of an +earlier injector. + +#### Wrappers + +Wrappers are special functions that are responsible for invoking +the part of the injection chain that comes after themselves. They +do this by calling an `inner()` function that the nject framework +defines for them. + +Any arguments to the inner() function are injected as values available +further down the chain. Any return values from inner() must be returned +by the final function in the chain or from another wrapper futher down +the chain. + +### Composition + +Collections of injectors may be composed by including them in +other collections. + +# Related packages + +The following use nject to provide nicer APIs: -### nvelope example - -Nvelope provides pre-defined handlers for basic endpoint tasks. When used -in combination with npoint, all that's left is the business logic. - -```go -type ExampleRequestBundle struct { - Request PostBodyModel `nvelope:"model"` - With string `nvelope:"path,name=with"` - Parameters int64 `nvelope:"path,name=parameters"` - Friends []int `nvelope:"query,name=friends"` - ContentType string `nvelope:"header,name=Content-Type"` -} - -func Service(router *mux.Router) { - service := npoint.RegisterServiceWithMux("example", router) - service.RegisterEndpoint("/some/path", - nvelope.LoggerFromStd(log.Default()), - nvelope.InjectWriter, - nvelope.EncodeJSON, - nvelope.CatchPanic, - nvelope.Nil204, - nvelope.ReadBody, - nvelope.DecodeJSON, - func (req ExampleRequestBundle) (nvelope.Response, error) { - .... - }, - ).Methods("POST") -} -``` - -### nserve example - -On thing you might want to do with nserve is to use a `Hook` to trigger -per-library database migrations using [libschema](https://github.com/muir/libschema). - -First create the hook: - -```go -package myhooks - -import "github.com/nject/nserve" - -var MigrateMyDB = nserve.NewHook("migrate, nserve.Ascending) -``` - -In each library, have a create function: - -```go -package users - -import( - "github.com/muir/libschema/lspostgres" - "github.com/muir/nject/nserve" -) - -func NewUsersStore(app *nserve.App) *Store { - ... - app.On(myhooks.MigrateMyDB, func(database *libschema.Database) { - database.Migrations("MyLibrary", - lspostgres.Script("create users", ` - CREATE TABLE users ( - id bigint PRIMARY KEY, - name text - ) - `), - ) - }) - ... - return &Store{} -} -``` - -Then as part of server startup, invoke the migration hook: - -```go -package main - -import( - "github.com/muir/libschema" - "github.com/muir/libschema/lspostgres" - "github.com/muir/nject/nject" -) - -func main() { - app, err := nserve.CreateApp("myApp", users.NewUserStore, ...) - schema := libschema.NewSchema(ctx, libschema.Options{}) - sqlDB, err := sql.Open("postgres", "....") - database, err := lspostgres.New(logger, "main-db", schema, sqlDB) - myhooks.MigrateMyDB.Using(database) - err = app.Do(myhooks.MigrateMyDB) -``` - -### Development status - -This repo represents continued development of Blue Owl's -[nject](https://github.com/BlueOwlOpenSource/nject) base. Blue Owl's code -has been in production use for years and has been unchanged for years. -The core of nject is mostly unchanged. Nvelope and nserve are new. - -### Go version - -Due to the use of strconv.ParseComplex in nvelope, the minimum supported -version of Go is 1.15 +- [npoint](https://github.com/muir/npoint): dependency injection wrappers for binding http endpoint handlers +- [nape](https://github.com/muir/nape): dependency injection wrappers for binding http endpoint handlers using gorillia/mux +- [nvelope](https://github.com/muir/nvelope): injection chains for building endpoints +- [nserve](https://github.com/muir/nserve): injection chains for for starting and stopping servers +- [nvalid](https://github.com/muir/nvalid): enforce that http endpoints conform to Swagger definitions +- [nfigure](https://github.com/muir/nfigure): configuration and flag processings diff --git a/nject/api.go b/api.go similarity index 100% rename from nject/api.go rename to api.go diff --git a/nject/bind.go b/bind.go similarity index 100% rename from nject/bind.go rename to bind.go diff --git a/nject/bind_test.go b/bind_test.go similarity index 100% rename from nject/bind_test.go rename to bind_test.go diff --git a/nject/cache.go b/cache.go similarity index 100% rename from nject/cache.go rename to cache.go diff --git a/nject/cache_test.go b/cache_test.go similarity index 100% rename from nject/cache_test.go rename to cache_test.go diff --git a/nject/characterize.go b/characterize.go similarity index 100% rename from nject/characterize.go rename to characterize.go diff --git a/nject/characterize_test.go b/characterize_test.go similarity index 100% rename from nject/characterize_test.go rename to characterize_test.go diff --git a/nject/debug.go b/debug.go similarity index 100% rename from nject/debug.go rename to debug.go diff --git a/nject/debug_test.go b/debug_test.go similarity index 100% rename from nject/debug_test.go rename to debug_test.go diff --git a/nject/doc.go b/doc.go similarity index 100% rename from nject/doc.go rename to doc.go diff --git a/nject/dummy_test.go b/dummy_test.go similarity index 100% rename from nject/dummy_test.go rename to dummy_test.go diff --git a/nject/error.go b/error.go similarity index 100% rename from nject/error.go rename to error.go diff --git a/nject/example_bind_test.go b/example_bind_test.go similarity index 98% rename from nject/example_bind_test.go rename to example_bind_test.go index d05b6f5..6c44f18 100644 --- a/nject/example_bind_test.go +++ b/example_bind_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Bind does as much work before invoke as possible. diff --git a/nject/example_cluster_test.go b/example_cluster_test.go similarity index 97% rename from nject/example_cluster_test.go rename to example_cluster_test.go index 5af9a13..1274d8b 100644 --- a/nject/example_cluster_test.go +++ b/example_cluster_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) func ExampleCluster() { diff --git a/nject/example_collection_test.go b/example_collection_test.go similarity index 96% rename from nject/example_collection_test.go rename to example_collection_test.go index 97514d2..e01d29f 100644 --- a/nject/example_collection_test.go +++ b/example_collection_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) func ExampleSequence() { diff --git a/nject/example_db_test.go b/example_db_test.go similarity index 99% rename from nject/example_db_test.go rename to example_db_test.go index 0920d49..09dff29 100644 --- a/nject/example_db_test.go +++ b/example_db_test.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // InjectDB injects both an *sql.DB and an *sql.Tx if they're needed. diff --git a/nject/example_generated_test.go b/example_generated_test.go similarity index 98% rename from nject/example_generated_test.go rename to example_generated_test.go index 80993bf..7ded496 100644 --- a/nject/example_generated_test.go +++ b/example_generated_test.go @@ -4,7 +4,7 @@ import ( "fmt" "reflect" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // ExampleGeneratedFromInjectionChain demonstrates how a special diff --git a/nject/example_memoize_test.go b/example_memoize_test.go similarity index 97% rename from nject/example_memoize_test.go rename to example_memoize_test.go index a637a94..f913263 100644 --- a/nject/example_memoize_test.go +++ b/example_memoize_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Memoize implies Chacheable. To make sure that Memoize can actually function diff --git a/nject/example_methodcall_test.go b/example_methodcall_test.go similarity index 92% rename from nject/example_methodcall_test.go rename to example_methodcall_test.go index fbda877..92f8af6 100644 --- a/nject/example_methodcall_test.go +++ b/example_methodcall_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) type S struct { diff --git a/nject/example_must_consume_test.go b/example_must_consume_test.go similarity index 98% rename from nject/example_must_consume_test.go rename to example_must_consume_test.go index 38035c8..303f65e 100644 --- a/nject/example_must_consume_test.go +++ b/example_must_consume_test.go @@ -3,7 +3,7 @@ package nject_test import ( "database/sql" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) type ( diff --git a/nject/example_postaction2_test.go b/example_postaction2_test.go similarity index 96% rename from nject/example_postaction2_test.go rename to example_postaction2_test.go index 7b67d3f..e69ead0 100644 --- a/nject/example_postaction2_test.go +++ b/example_postaction2_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) type Causer interface { diff --git a/nject/example_postaction_test.go b/example_postaction_test.go similarity index 98% rename from nject/example_postaction_test.go rename to example_postaction_test.go index aa96352..af219de 100644 --- a/nject/example_postaction_test.go +++ b/example_postaction_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) func ExamplePostActionByTag() { diff --git a/nject/example_provider_test.go b/example_provider_test.go similarity index 98% rename from nject/example_provider_test.go rename to example_provider_test.go index 9857f02..aeef182 100644 --- a/nject/example_provider_test.go +++ b/example_provider_test.go @@ -4,7 +4,7 @@ import ( "fmt" "strconv" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Provide does one job: it names an otherwise anonymous diff --git a/nject/example_run_test.go b/example_run_test.go similarity index 93% rename from nject/example_run_test.go rename to example_run_test.go index df3ac65..6b00efb 100644 --- a/nject/example_run_test.go +++ b/example_run_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Run is the simplest way to use the nject framework. diff --git a/nject/example_setcallback_test.go b/example_setcallback_test.go similarity index 97% rename from nject/example_setcallback_test.go rename to example_setcallback_test.go index f13fa5a..c2fba38 100644 --- a/nject/example_setcallback_test.go +++ b/example_setcallback_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // SetCallback invokes a function passing a function that diff --git a/nject/example_singleton_test.go b/example_singleton_test.go similarity index 96% rename from nject/example_singleton_test.go rename to example_singleton_test.go index 7dc02e3..d187e6b 100644 --- a/nject/example_singleton_test.go +++ b/example_singleton_test.go @@ -3,7 +3,7 @@ package nject_test import ( "fmt" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Singleton providers get run only once even if their arguments are different. diff --git a/nject/example_test.go b/example_test.go similarity index 98% rename from nject/example_test.go rename to example_test.go index 2f8d96b..0e60c14 100644 --- a/nject/example_test.go +++ b/example_test.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/muir/nject/nject" + "github.com/muir/nject" ) // Example shows what gets included and what does not for several injection chains. diff --git a/nject/filler.go b/filler.go similarity index 100% rename from nject/filler.go rename to filler.go diff --git a/nject/filler_api.go b/filler_api.go similarity index 100% rename from nject/filler_api.go rename to filler_api.go diff --git a/nject/filler_test.go b/filler_test.go similarity index 100% rename from nject/filler_test.go rename to filler_test.go diff --git a/nject/generate.go b/generate.go similarity index 100% rename from nject/generate.go rename to generate.go diff --git a/go.mod b/go.mod index af773ab..f25f1f4 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,8 @@ module github.com/muir/nject go 1.15 require ( - github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f - github.com/gorilla/mux v1.8.0 - github.com/muir/reflectutils v0.4.0 - github.com/pkg/errors v0.9.1 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.2.0 // indirect github.com/stretchr/testify v1.7.0 - golang.org/x/net v0.0.0-20210913180222-943fd674d43e - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect ) diff --git a/go.sum b/go.sum index bbec404..3b471db 100644 --- a/go.sum +++ b/go.sum @@ -1,74 +1,18 @@ -cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= 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/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg= -github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4= -github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285 h1:voz4XQjiyYyhlp7CjBDaTejOZGKv3R9+5PM5QrDgegQ= -github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= -github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/muir/reflectutils v0.4.0 h1:1VXbXaav7tR5BTYTHcGTdOAyxu3idCzM5FCtCpgm2u4= -github.com/muir/reflectutils v0.4.0/go.mod h1:hyMWDtoeNsc1FTq9qlXsbwDzbwp3A3M8zbRNKDzvSmc= -github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= -github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= -golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/nject/include.go b/include.go similarity index 100% rename from nject/include.go rename to include.go diff --git a/nject/match.go b/match.go similarity index 100% rename from nject/match.go rename to match.go diff --git a/nject/match_test.go b/match_test.go similarity index 100% rename from nject/match_test.go rename to match_test.go diff --git a/nject/nject.go b/nject.go similarity index 100% rename from nject/nject.go rename to nject.go diff --git a/nject/README.md b/nject/README.md deleted file mode 100644 index bd7329f..0000000 --- a/nject/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# nject - dependency injection - -[![GoDoc](https://godoc.org/github.com/muir/nject/nject?status.png)](http://godoc.org/github.com/muir/nject/nject) -[![Coverage](http://gocover.io/_badge/github.com/muir/nject/nject)](https://gocover.io/github.com/muir/nject/nject) - -Install: - - go get github.com/muir/nject - ---- - -This package provides type-safe dependency injection without requiring -users to do type assertions. - -### Main APIs - -It provides two main APIs: Bind() and Run(). - -Bind() is used when performance matters: given a chain of providers, -it will write two functions: one to initialize the chain and another to -invoke it. As much as possible, all dependency injection work is done -at the time of binding and initialization so that the invoke function -operates with very little overhead. The chain is initialized when the -initialize function is called. The chain is run when the invoke function -is called. Bind() does not run the chain. - -Run() is used when ad-hoc injection is desired and performance is not -critical. Run is appropriate when starting servers and running tests. -It is not reccomended for http endpoint handlers. Run exectes the -chain immediately. - -### Identified by type - -Rather than naming values, inputs and outputs are identified by their types. -Since Go makes it easy to create new types, this turns out to be quite easy to use. - -### Types of providers - -Multiple types of providers are supported. - -#### Literal values - -You can provide a constant if you have one. - -#### Injectors - -Regular functions can provide values. Injectors will be called at -initialization time when they're marked as cacheable or at invocation -time if they're not. - -Injectors can be memoized. - -Injectors can return a special error type that stops the chain. - -Injectors can use data produced by earlier injectors simply by having -a function parameter that matches the type of a return value of an -earlier injector. - -#### Wrappers - -Wrappers are special functions that are responsible for invoking -the part of the injection chain that comes after themselves. They -do this by calling an `inner()` function that the nject framework -defines for them. - -Any arguments to the inner() function are injected as values available -further down the chain. Any return values from inner() must be returned -by the final function in the chain or from another wrapper futher down -the chain. - -### Composition - -Collections of injectors may be composed by including them in -other collections. - diff --git a/nject/nject_test.go b/nject_test.go similarity index 100% rename from nject/nject_test.go rename to nject_test.go diff --git a/npoint/README.md b/npoint/README.md deleted file mode 100644 index c62bc79..0000000 --- a/npoint/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# npoint - dependency injection and wrapping for http handlers - -[![GoDoc](https://godoc.org/github.com/muir/nject/npoint?status.png)](http://godoc.org/github.com/muir/nject/npoint) -[![Coverage](http://gocover.io/_badge/github.com/muir/nject/npoint)](https://gocover.io/github.com/muir/nject/npoint) - -Install: - - go get github.com/muir/npoint - ---- - -This package attempts to solve several issues with http endpoint handlers: - - * Chaining actions to create composite handlers - * Wrapping endpoint handlers with before/after actions - * Dependency injection for endpoint handlers - * Binding handlers to their endpoints next to the code that defines the endpoints - * Delaying initialization code execution until services are started allowing services that are not used to remain uninitialized - * Reducing the cost of refactoring endpoint handlers passing data directly from producers to consumers without needing intermediaries to care about what is being passed - * Avoid the overhead of verifying that requested items are present when passed indirectly (a common problem when using context.Context to pass data indirectly) - -It does this by defining endpoints as a sequence of handler functions. Handler functions -come in different flavors. Data is passed from one handler to the next using reflection to -match the output types with input types requested later in the handler chain. To the extent -possible, the handling is precomputed so there is very little reflection happening when the -endpoint is actually invoked. - -Endpoints are registered to services before or after the service is started. - -When services are pre-registered and started later, it is possible to bind endpoints -to them in init() functions and thus colocate the endpoint binding with the endpoint -definition and spread the endpoint definition across multiple files and/or packages. - -Services come in two flavors: one flavor binds endpoints with http.HandlerFunc and the -other binds using mux.Router. - -Example uses include: - - * Turning output structs into json - * Common error handling - * Common logging - * Common initialization - * Injection of resources and dependencies - -## Small Example - -CreateEndpoint is the simplest way to start using the npoint framework. It -generates an http.HandlerFunc from a list of handlers. The handlers will be called -in order. In the example below, first WriteErrorResponse() will be called. It -has an inner() func that it uses to invoke the rest of the chain. When -WriteErrorResponse() calls its inner() function, the db injector returned by -InjectDB is called. If that does not return error, then the inline function below -to handle the endpint is called. - - mux := http.NewServeMux() - mux.HandleFunc("/my/endpoint", npoint.CreateEndpoint( - WriteErrorResponse, - InjectDB("postgres", "postgres://..."), - func(r *http.Request, db *sql.DB, w http.ResponseWriter) error { - // Write response to w or return error... - return nil - })) - -WriteErrorResponse invokes the remainder of the handler chain by calling inner(). - - func WriteErrorResponse(inner func() nject.TerminalError, w http.ResponseWriter) { - err := inner() - if err != nil { - w.Write([]byte(err.Error())) - w.WriteHeader(500) - } - } - -InjectDB returns a handler function that opens a database connection. If the open -fails, executation of the handler chain is terminated. - - func InjectDB(driver, uri string) func() (nject.TerminalError, *sql.DB) { - return func() (nject.TerminalError, *sql.DB) { - db, err := sql.Open(driver, uri) - if err != nil { - return err, nil - } - return nil, db - } - } - diff --git a/npoint/doc.go b/npoint/doc.go deleted file mode 100644 index 027d8bf..0000000 --- a/npoint/doc.go +++ /dev/null @@ -1,228 +0,0 @@ -// Stuff - -/* - -Package npoint is a general purpose lightweight non-opinionated web -server framework that provides a concise way to handle errors and inject -dependencies into http endpoint handlers. - -Why - -Composite endpoints: endpoints are assembled from a collection of handlers. - -Before/after actions: the middleware handler type wraps the rest of the -handler chain so that it can both inject items that are used downstream -and process return values. - -Dependency injection: injectors, static injectors, and fallible injectors -can all be used to provide data and code to downstream handlers. Downstream -handlers request what they need by including appropriate types in their -argument lists. Injectors are invoked only if their outputs are consumed. - -Code juxtaposition: when using pre-registered services, endpoint binding can -be registered next to the code that implements the endpoint even if the endpoints -are implemented in multiple files and/or packages. - -Delayed initialization: initializers for pre-registered services are only executed -when the service is started and bound to an http server. This allow code to define -such endpoints to depend on resources that may not be present unless the service -is started. - -Reduced refactoring cost: handlers and endpoints declare their inputs and outputs -in the argument lists and return lists. Handlers only need to know about their own -inputs and outputs. The endpoint framework carries the data to where it is needed. -It does so with a minimum of copies and without recursive searches (see context.Context). -Type checking is done at service start time (or endpoint binding time when binding to -services that are already running). - -Lower overhead indirect passing: when using context.Context to pass values indirectly, -the Go type system cannot be used to verify types at compile time or startup time. Endpoint -verifies types at startup time allowing code that receives indirectly-passed data simpler. -As much as possible, work is done at initialization time rather than endpoint invocation time. - -Basics - -To use the npoint package, create services first. After that the -endpoints can be registered to the service and the service can be started. - -A simpler way to use endpoint is to use the CreateEndpoint function. It -converts a list of handlers into an http.HandlerFunc. This bypasses service -creation and endpoint registration. -See https://github.com/muir/npoint/blob/master/README.md -for an example. - -Terminology - -Service is a collection of endpoints that can be started together and may share -a handler collection. - -Handler is a function that is used to help define an endpoint. - -Handler collection is a group of handlers. - -Downstream handlers are handlers that are to the right of the current handler -in the list of handlers. They will be invoked after the current handler. - -Upstream handlers are handlers that are to the left of the current handler -in the list of handlers. They will have already been invoked by the time the -current handler is invoked. - -Services - -A service allows a group of related endpoints to be started together. -Each service may have a set of common handlers that are shared among -all the endpoints registered with that service. - -Services come in four flavors: started or pre-registered; with Mux or -with without. - -Pre-registered services are not initialized until they are Start()ed. This -allows them to depend upon resources that may not may not be available without -causing a startup panic unless they're started without their required resources. -It also allows endpoints to be registered in init() functions next to the -definition of the endpoint. - -Handlers - -The handlers are defined using the nject framework: -See https://github.com/muir/nject/blob/master/README.md - -A list of handlers will be invoked from left-to-right. The first -handler in the list is invoked first and the last one (the endpoint) -is invoked last. The handlers do not directly call each other -- -rather the framework manages the invocations. Data provided by one -handler can be used by any handler to its right and then as the -handlers return, the data returned can be used by any handler to its -left. The data provided and required is identified by its type. -Since Go makes it easy to make aliases of types, it is easy to make -types distinct. When there is not an exact match of types, the framework -will use the closest (in distance) value that can convert to the -required type. - -Each handler function is distinguished by its position in the -handler list and by its primary signature: its arguments -and return values. In Go, types may be named or unnamed. Unnamed function -types are part of primary signature. Named function types are not part -of the primary signature. - -These are the types that are recognized as valid handlers: -Static Injectors, Injectors, Endpoints, and Middleware. - -Injectors are only invoked if their output is consumed or they have -no output. Middleware handlers are (currently) always invoked. - -Injectors - -There are three kinds of injectors: static injectors, injectors, and -fallible injectors. - -Injectors and static injectors have the following type signature: - - func(input value(s)) output values(s) - -None of the input or output parameters may be un-named functions. -That describes nearly every function in Go. Handlers that match a more -specific type signature are that type, rather than being an injector or -static injector. - -Injectors whose output values are not used by a downstream handler -are dropped from the handler chain. They are not invoked. Injectors -that have no output values are a special case and they are always retained -in the handler chain. - -Static injectors are called exactly once per endpoint. They are called -when the endpoint is started or when the endpoint is registered -- whichever -comes last. - -Values returned by static injectors will be shared by all invocations of -the endpoint. - -Injectors are called once per endpoint invocation (or more if they are -downstream from a middleware handler that calls inner() more than once). - -Injectors a distingued from static injectors by either their position in -the handler list or by the parameters that they take. If they take -http.ResponseWriter or *http.Request, then they're not static. Anything -that is downstream of a non-static injector or middleware handler is also -not static. - -Fallible injectors are injectors whose first return values is of type -nject.TerminalError: - - func(input value(s)) (nject.TerminalError, output values(s)) - -If a non-nil value is returned as the nject.TerminalError from a fallible -injector, none of the downstream handlers will be called. The handler -chain returns from that point with the nject.TerminalError as a return -value. Since all return values must be consumed by a middleware handler, -fallible injectors must come downstream from a middleware handler that -takes nject.TerminalError as a returned value. If a fallible injector returns -nil for the nject.TerminalError, the other output values are made available -for downstream handlers to consume. The other output values are not -considered return values and are not available to be consumed by upstream -middleware handlers. - -Some examples: - - func staticInjector(i int, s string) int { return i+7 } - - func injector(r *http.Request) string { return r.FormValue("x") } - - func fallibleInjector(i int) nject.TerminalError { - if i > 10 { - return fmt.Errorf("limit exceeded") - } - return nil - } - -Middleware handlers - -Middleware handlers wrap the handlers downstream in a inner() function that they -may call. The type signature of a middleware handler is a function that -receives an function as its first parameter. That function must be of an -anonymous type: - - // middleware handler - func(innerfunc, input value(s)) return value(s) - - // innerfunc - func(output value(s)) returned value(s) - -For example: - - func middleware(inner func(string) int, i int) int { - j := inner(fmt.Sprintf("%d", i) - return j * 2 - } - -When this middleware function runs, it is responsible for invoking -the rest of the handler chain. It does this by calling inner(). -The parameters to inner are available as inputs to downstream -handlers. The value(s) returned by inner come from the return -values of downstream middleware handlers and the endpoint handler. - -Middleware handlers can call inner() zero or more times. - -The values returned by middleware handlers must be consumed by another -upstream middlware handler. - -Endpoint Handlers - -Endpoint handlers are simply the last handler in the handler chain. -They look like regular Go functions. Their input parameters come -from other handlers. Their return values (if any) must be consumed by -an upstream middleware handler. - - func(input value(s)) return values(s) - -Panics - -Endpoint will panic during endpoint registration if the provided handlers -do not constitute a valid chain. For example, if a some handler requires -a FooType but there is no upstream handler that provides a FooType then -the handler list is invalid and endpoint will panic. - -Endpoint should not panic after initialization. - -*/ -package npoint diff --git a/npoint/endpoint_test.go b/npoint/endpoint_test.go deleted file mode 100644 index efe5340..0000000 --- a/npoint/endpoint_test.go +++ /dev/null @@ -1,541 +0,0 @@ -package npoint_test - -import ( - "bytes" - "database/sql" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/muir/nject/nject" - "github.com/muir/nject/npoint" - "github.com/stretchr/testify/assert" - "golang.org/x/net/context" -) - -type ( - intType3 int - intType5 int - intType7 int -) - -type ( - stringA string - stringB string - stringC string - stringD string - stringE string - stringF string -) - -func NewBinder() *ManualBinder { - return &ManualBinder{ - Bound: make(map[string]http.HandlerFunc), - } -} - -type ManualBinder struct { - Bound map[string]http.HandlerFunc -} - -func (b *ManualBinder) Bind(path string, h http.HandlerFunc) { - b.Bound[path] = h -} - -func (b *ManualBinder) Call(path string, method string, buf string, h http.Header) *http.Response { - handler, found := b.Bound[path] - if !found { - panic(fmt.Sprintf("no handler for %s", path)) - } - url := "http://localhost" + path - // nolint:noctx - req, err := http.NewRequest(method, url, bytes.NewReader([]byte(buf))) - if err != nil { - panic(err) - } - req.Header = h - w := NewWriter() - handler(w, req) - resp := &http.Response{ - Status: fmt.Sprintf("%d Something", w.code), - StatusCode: w.code, - Proto: "HTTP/1.0", - ProtoMajor: 1, - ProtoMinor: 0, - Header: w.h, - Body: ioutil.NopCloser(bytes.NewBuffer([]byte(w.buf))), - ContentLength: int64(len(w.buf)), - Request: req, - } - return resp -} - -type Writer struct { - h http.Header - buf string - code int -} - -func NewWriter() *Writer { return &Writer{h: make(http.Header)} } -func (w *Writer) Write(b []byte) (int, error) { w.buf += string(b); return len(b), nil } -func (w *Writer) Header() http.Header { return w.h } -func (w *Writer) WriteHeader(i int) { w.code = i } - -func TestTestFramework(t *testing.T) { - t.Parallel() - b := NewBinder() - var bodyReceived string - var headerReceived string - b.Bind("/y", func(w http.ResponseWriter, r *http.Request) { - buf, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - bodyReceived = string(buf) - headerReceived = r.Header.Get("X-Test-Request") - w.Header().Set("X-Test-Respond", "H1") - w.Write([]byte("some data written")) - w.WriteHeader(203) - }) - h := make(http.Header) - h.Set("X-Test-Request", "H2") - // nolint:bodyclose - resp := b.Call("/y", "POST", "some data sent", h) - buf, err := ioutil.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, "some data written", string(buf)) - assert.Equal(t, "H1", resp.Header.Get("X-Test-Respond")) - assert.Equal(t, "", resp.Header.Get("X-Test-Request")) - assert.Equal(t, "some data sent", bodyReceived) - assert.Equal(t, "H2", headerReceived) -} - -type interfaceI interface { - I() int -} - -type interfaceJ interface { - I() int -} - -type interfaceK interface { - I() int -} - -type doesI struct { - i int -} - -func (di *doesI) I() int { return di.i * 2 } - -type doesJ struct { - j int -} - -func (dj *doesJ) I() int { return dj.j * 3 } - -func TestVariablePassing(t *testing.T) { - t.Skip("Requires fuzzy matching") - t.Parallel() - s := npoint.PreregisterService("TestVariablePassing") - s.RegisterEndpoint("/x", - // ------------- static injectors ------------ - // [0] Send simple things - func() (stringA, stringB) { - return "static1", "static2" - }, - func(b stringB) { - assert.Equal(t, stringB("static2"), b) - }, - func(b stringB) { - assert.Equal(t, stringB("static2"), b) - }, - - // [3] Send interfaces - func() interfaceI { - return &doesI{7} - }, - func() interfaceJ { - return &doesI{8} - }, - func() *doesI { - return &doesI{9} - }, - func() *doesJ { - return &doesJ{10} - }, - - // [7] check receiving by priority - func(i interfaceI) { - // exact match on interface - assert.Equal(t, 14, i.I()) - }, - func(j interfaceJ) { - // exact match on interface - assert.Equal(t, 16, j.I()) - }, - func(di *doesI) { - // exact match on interface - assert.Equal(t, 18, di.I()) - }, - func(k interfaceK) { - // nearest one that satisfies interface - assert.Equal(t, 30, k.I()) - }, - - // ------------- middleware ------------ - // [11] - func(inner func(intType3, stringC) (intType5, stringE), a stringA) { - assert.Equal(t, stringA("static1"), a) - i5, e := inner(93, "c-v") - assert.Equal(t, stringE("fooE"), e) - assert.Equal(t, intType5(55), i5) - }, - - // ------------- injector ------------ - // [12] - func(c stringC, i3 intType3, j *doesJ) stringA { - assert.Equal(t, stringC("c-v"), c) - assert.Equal(t, intType3(93), i3) - assert.Equal(t, 30, j.I()) - return "newAv" - }, - - // ------------- endpoint ------------ - func(i3 intType3, b stringB, a stringA, c stringC) (stringE, intType5) { - assert.Equal(t, intType3(93), i3) - assert.Equal(t, stringB("static2"), b) - assert.Equal(t, stringA("newAv"), a) - assert.Equal(t, stringC("c-v"), c) - return "fooE", 55 - }) - b := NewBinder() - s.Start(b.Bind) - // nolint:bodyclose - _ = b.Call("/x", "GET", "", make(http.Header)) -} - -var inclusionTests = []struct { - name string - endpoint interface{} - called string -}{ - { - "just e", - func(e stringE) {}, - "e cd", - }, -} - -func TestInclusion(t *testing.T) { - t.Parallel() - - for _, tc := range inclusionTests { - s := npoint.PreregisterService(fmt.Sprintf("TestDemandInclusion-%s", tc.name)) - called := make(map[string]bool) - s.RegisterEndpoint("/x", - func() (stringA, stringB) { - called["ab"] = true - return "static1", "static2" - }, - func() (stringC, stringD) { - called["cd"] = true - return "static3", "static4" - }, - func(d stringD) stringE { - called["e"] = true - return "static5" - }, - func(b stringB) stringF { - called["f"] = true - return "static6" - }, - tc.endpoint) - b := NewBinder() - s.Start(b.Bind) - // nolint:bodyclose - _ = b.Call("/x", "GET", "", make(http.Header)) - expected := make(map[string]bool) - for _, c := range strings.Split(tc.called, " ") { - expected[c] = true - } - assert.Equal(t, expected, called, tc.name) - } -} - -type error2 struct { - e error -} - -func (e2 error2) Error() string { - return e2.e.Error() -} - -type paymentProvider interface { - Stuff() int -} -type examplePaymentProvider int - -func (epp examplePaymentProvider) Stuff() int { - return int(epp * 2) -} - -type ( - tripsURI string - csettings map[string]string - logger interface { - Logf(string, ...interface{}) - } -) - -type enhancedWriter interface { - http.ResponseWriter - S(int) -} - -type enhancedWriterImp struct { - http.ResponseWriter -} - -func (w enhancedWriterImp) S(i int) { - w.WriteHeader(i) -} - -type ( - dbname string - rbody []byte - jresult interface{} -) - -func TestChains(t *testing.T) { - t.Parallel() - chainTests := []struct { - Name string - Panics bool - Chain []interface{} - }{ - { - "interface games", - true, // requires fuzzy matching - []interface{}{ - func() logger { - return t - }, - func(w http.ResponseWriter, l logger) enhancedWriter { - return &enhancedWriterImp{w} - }, - func(inner func() jresult) { - j := inner() - e, is := j.(error) - assert.True(t, is) - assert.Equal(t, "example error", e.Error()) - }, - func(inner func() (jresult, error2), l logger, w enhancedWriter) jresult { - _, err := inner() - return err - }, - func(inner func() error, l logger, w enhancedWriter) error2 { - return error2{inner()} - }, - func() error { - return fmt.Errorf("example error") - }, - }, - }, - { - "unused return", - true, - []interface{}{ - func(inner func() error) error { - return inner() - }, - func(r *http.Request, w http.ResponseWriter) error { - return nil - }, - }, - }, - { - "obscured return", - true, - []interface{}{ - func(inner func() error2) { - _ = inner() - }, - func(inner func() error) error { - return inner() - }, - func(inner func() error) *error2 { - return &error2{inner()} - }, - func(r *http.Request, w http.ResponseWriter) error { - return nil - }, - }, - }, - { - "regression", - false, - []interface{}{ - nject.Sequence("service", - func() paymentProvider { - return examplePaymentProvider(7) - }, - func() tripsURI { - return "tu" - }, - func() context.Context { - return context.Background() - }, - nject.Sequence("common-handlers", - func() logger { - return t - }, - nject.Sequence("base-collection", - func() csettings { - return make(map[string]string) - }, - func(r *http.Request, l logger) logger { - return l - }, - func(w http.ResponseWriter, l logger) enhancedWriter { - return &enhancedWriterImp{w} - }, - func(w enhancedWriter, ac csettings) {}, - func(inner func() jresult, w enhancedWriter, l logger) { - _ = inner() - }, - func(inner func() (jresult, error2), l logger, w enhancedWriter) jresult { - res, _ := inner() - return res - }, - func(inner func() error, l logger, w enhancedWriter) error2 { - return error2{inner()} - }, - func(inner func(rbody) error, r *http.Request) error { - return inner([]byte("foo")) - }, - ), - nject.Sequence("open-database", - func() dbname { - return "foo" - }, - func(inner func(*sql.DB) error, name dbname) error { - db, err := sql.Open("postgres", string(name)) - if err != nil { - return err - } - err = inner(db) - db.Close() - return err - }, - ), - func(inner func(*sql.Tx) error, db *sql.DB, l logger, w enhancedWriter) error { - tx, _ := db.Begin() - return inner(tx) - }, - ), - ), - func(inner func() error, w enhancedWriter) jresult { - return nil - }, - func(l logger, b rbody, tx *sql.Tx) error { - return nil - }, - }, - }, - { - "static regression", - false, - []interface{}{ - nject.Sequence("service", - func() paymentProvider { - return examplePaymentProvider(7) - }, - func() tripsURI { - return "tu" - }, - func() context.Context { - return context.Background() - }, - nject.Sequence("common-handlers", - func() logger { - return t - }, - nject.Sequence("base-collection", - func() csettings { - return make(map[string]string) - }, - func(r *http.Request, l logger) logger { - return l - }, - func(w http.ResponseWriter, l logger) enhancedWriter { - return &enhancedWriterImp{w} - }, - func(w enhancedWriter, ac csettings) {}, - func(inner func() jresult, w enhancedWriter, l logger) { - _ = inner() - }, - func(inner func() (jresult, error2), l logger, w enhancedWriter) jresult { - res, _ := inner() - return res - }, - func(inner func() error, l logger, w enhancedWriter) error2 { - return error2{inner()} - }, - func(inner func(rbody) error, r *http.Request) error { - return inner([]byte("foo")) - }, - ), - nject.Sequence("open-database", - func() dbname { - return "foo" - }, - func(inner func(*sql.DB) error, name dbname) error { - db, err := sql.Open("postgres", string(name)) - if err != nil { - return err - } - err = inner(db) - db.Close() - return err - }, - ), - func(inner func(*sql.Tx) error, db *sql.DB, l logger, w enhancedWriter) error { - tx, _ := db.Begin() - return inner(tx) - }, - func() intType7 { - return 3 - }, - ), - ), - nject.Sequence("endpoint-list", - func(i intType7) intType5 { - return 3 - }, - func(l logger, b rbody, tx *sql.Tx, i intType5) (jresult, error) { - return nil, nil - }, - ), - }, - }, - } - - for _, test := range chainTests { - test := test - t.Log("TEST:", test.Name) - f := func() { - e := npoint.CreateEndpoint(test.Chain...) - b := NewBinder() - b.Bind("/foo", e) - // nolint:bodyclose - b.Call("/foo", "GET", "", nil) - } - if test.Panics { - assert.Panics(t, f, test.Name) - } else { - f() - assert.NotPanics(t, f, test.Name) - } - } -} diff --git a/npoint/example_create_endpoint_test.go b/npoint/example_create_endpoint_test.go deleted file mode 100644 index 017c7b7..0000000 --- a/npoint/example_create_endpoint_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package npoint_test - -import ( - "database/sql" - "net/http" - - "github.com/muir/nject/nject" - "github.com/muir/nject/npoint" -) - -// CreateEndpoint is the simplest way to start using the npoint framework. It -// generates an http.HandlerFunc from a list of handlers. The handlers will be called -// in order. In the example below, first WriteErrorResponse() will be called. It -// has an inner() func that it uses to invoke the rest of the chain. When -// WriteErrorResponse() calls its inner() function, the db injector returned by -// InjectDB is called. If that does not return error, then the inline function below -// to handle the endpint is called. -func ExampleCreateEndpoint() { - mux := http.NewServeMux() - mux.HandleFunc("/my/endpoint", npoint.CreateEndpoint( - WriteErrorResponse, - InjectDB("postgres", "postgres://..."), - func(r *http.Request, db *sql.DB, w http.ResponseWriter) error { - // Write response to w or return error... - return nil - })) -} - -// WriteErrorResponse invokes the remainder of the handler chain by calling inner(). -func WriteErrorResponse(inner func() nject.TerminalError, w http.ResponseWriter) { - err := inner() - if err != nil { - w.Write([]byte(err.Error())) - w.WriteHeader(500) - } -} - -// InjectDB returns a handler function that opens a database connection. If the open -// fails, executation of the handler chain is terminated. -func InjectDB(driver, uri string) func() (nject.TerminalError, *sql.DB) { - return func() (nject.TerminalError, *sql.DB) { - db, err := sql.Open(driver, uri) - if err != nil { - return err, nil - } - return nil, db - } -} diff --git a/npoint/example_preregister_test.go b/npoint/example_preregister_test.go deleted file mode 100644 index aa49e25..0000000 --- a/npoint/example_preregister_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package npoint_test - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - - "github.com/gorilla/mux" - "github.com/muir/nject/npoint" -) - -// The npoint framework distinguishes parameters based on their types. -// All parameters of type "string" look the same, but a type that is -// defined as another type (like exampleType) is a different type. -type ( - exampleType string - fooParam string - fromMiddleware string -) - -// exampleStaticInjector will not be called until the service.Start() -// call in Example_PreregisterServiceWithMux. It will be called only -// once per endpoint registered. Since it has a return value, it will -// only run if a downstream handler consumes the value it returns. -// -// The values returned by injectors and available as input parameters -// to any downstream handler. -func exampleStaticInjector() exampleType { - return "example static value" -} - -// exampleInjector will be called for each request. We know that -// exampleInjector is a regular injector because it takes a parameter -// that is specific to the request (*http.Request). -func exampleInjector(r *http.Request) fooParam { - return fooParam(r.FormValue("foo")) -} - -type returnValue interface{} - -// jsonifyResult wraps all handlers downstream of it in the call chain. -// We know that jsonifyResult is a middleware handler because its first -// argument is an function with an anonymous type (inner). Calling inner -// invokes all handlers downstream from jsonifyResult. The value returned -// by inner can come from the return values of the final endpoint handler -// or from values returned by any downstream middleware. The parameters -// to inner are available as inputs to any downstream handler. -// -// Parameters are matched by their types. Since inner returns a returnValue, -// it can come from any downstream middleware or endpoint that returns something -// of type returnValue. -func jsonifyResult(inner func(fromMiddleware) returnValue, w http.ResponseWriter) { - v := inner("jsonify!") - w.Header().Set("Content-Type", "application/json") - encoded, _ := json.Marshal(v) - w.WriteHeader(200) - w.Write(encoded) -} - -// Endpoints are grouped and started by services. Handlers that are -// common to all endpoints are attached to the service. -var service = npoint.PreregisterServiceWithMux("example-service", - exampleStaticInjector, - jsonifyResult) - -func init() { - // The /example endpoint is bound to a handler chain - // that combines the functions included at the service - // level and the functions included here. The final chain is: - // exampleStaticInjector, jsonifyResult, exampleInjector, exampleEndpoint. - // ExampleStaticInjector and jsonifyResult come from the service - // definition. ExampleInjector and exampleEndpoint are attached when - // the endpoint is registered. - // - // Handlers will execute in the order of the chain: exampleStaticInjector - // then jsonifyResult. When jsonifyResult calls inner(), exampleInjector - // runs, then exampleEndpoint. When exampleEndpoint returns, inner() returns - // so jsonifyResult continues its work. When jsonifyResult returns, the - // handler chain is complete and the http server can form a reply from the - // ResponseWriter. - // - // Since service is WithMux, we can use gorilla mux modifiers when - // we register endpoints. This allows us to trivially indicate that our - // example endpoint supports the GET method only. - service.RegisterEndpoint( - "/example", exampleInjector, exampleEndpoint).Methods("GET") -} - -// This is the final endpoint handler. The parameters it takes can -// be provided by any handler upstream from it. It can also take the two -// values that are included by the http handler signature: http.ResponseWriter -// and *http.Request. -// -// Any values that the final endpoint handler returns must be consumed by an -// upstream middleware handler. In this example, a "returnValue" is returned -// here and consumed by jsonifyResult. -func exampleEndpoint(sv exampleType, foo fooParam, mid fromMiddleware) returnValue { - return map[string]string{ - "value": fmt.Sprintf("%s-%s-%s", sv, foo, mid), - } -} - -// The code below puts up a test http server, hits the /example -// endpoint, decodes the response, prints it, and exits. This -// is just to exercise the endpoint defined above. The interesting -// stuff happens above. -func Example() { - muxRouter := mux.NewRouter() - service.Start(muxRouter) - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - // nolint:noctx - r, err := http.Get(localServer.URL + "/example?foo=bar") - if err != nil { - fmt.Println("get error", err) - return - } - buf, err := ioutil.ReadAll(r.Body) - if err != nil { - fmt.Println("read error", err) - return - } - r.Body.Close() - var res map[string]string - err = json.Unmarshal(buf, &res) - if err != nil { - fmt.Println("unmarshal error", err) - return - } - fmt.Println("Value:", res["value"]) - // Output: Value: example static value-bar-jsonify! -} diff --git a/npoint/gorilla.go b/npoint/gorilla.go deleted file mode 100644 index e8a59a4..0000000 --- a/npoint/gorilla.go +++ /dev/null @@ -1,146 +0,0 @@ -package npoint - -import ( - "fmt" - "net/http" - "net/url" - - "github.com/gorilla/mux" -) - -func (r *EndpointRegistrationWithMux) add(f func(m *mux.Route) *mux.Route) { - r.muxroutes = append(r.muxroutes, f) -} - -// Route returns the *mux.Route that has been registered to this endpoint, if possible. -func (r *EndpointRegistrationWithMux) Route() (*mux.Route, error) { - if !r.bound { - return nil, fmt.Errorf("Registration is not complete for %s", r.path) - } - if r.route == nil { - return nil, fmt.Errorf("No *mux.Route was used to start %s", r.path) - } - return r.route, nil -} - -// BuildOnly applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) BuildOnly() *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.BuildOnly() }) - return r -} - -// BuildVarsFunc applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) BuildVarsFunc(f mux.BuildVarsFunc) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.BuildVarsFunc(f) }) - return r -} - -// Headers applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Headers(pairs ...string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Headers(pairs...) }) - return r -} - -// HeadersRegexp applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) HeadersRegexp(pairs ...string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.HeadersRegexp(pairs...) }) - return r -} - -// Host applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Host(tpl string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Host(tpl) }) - return r -} - -// MatcherFunc applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) MatcherFunc(f mux.MatcherFunc) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.MatcherFunc(f) }) - return r -} - -// Methods applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Methods(methods ...string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Methods(methods...) }) - return r -} - -// Name applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Name(name string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Name(name) }) - return r -} - -// Path applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Path(tpl string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Path(tpl) }) - return r -} - -// PathPrefix applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) PathPrefix(tpl string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.PathPrefix(tpl) }) - return r -} - -// Queries applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Queries(pairs ...string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Queries(pairs...) }) - return r -} - -// Schemes applies the mux.Route method of the same name to this endpoint when the endpoint is initialized. -func (r *EndpointRegistrationWithMux) Schemes(schemes ...string) *EndpointRegistrationWithMux { - r.add(func(m *mux.Route) *mux.Route { return m.Schemes(schemes...) }) - return r -} - -// GetError calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) GetError() error { - return r.err -} - -// GetHandler calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) GetHandler() http.Handler { - return r.route.GetHandler() -} - -// GetHostTemplate calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) GetHostTemplate() (string, error) { - return r.route.GetHostTemplate() -} - -// GetName calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) GetName() string { - return r.route.GetName() -} - -// GetPathTemplate calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) GetPathTemplate() (string, error) { - return r.route.GetPathTemplate() -} - -// Match calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) Match(req *http.Request, match *mux.RouteMatch) bool { - return r.route.Match(req, match) -} - -// SkipClean calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) SkipClean() bool { - return r.route.SkipClean() -} - -// URL calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) URL(pairs ...string) (*url.URL, error) { - return r.route.URL(pairs...) -} - -// URLHost calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) URLHost(pairs ...string) (*url.URL, error) { - return r.route.URLHost(pairs...) -} - -// URLPath calls the mux.Route method of the same name on the route created for this endpoint. -func (r *EndpointRegistrationWithMux) URLPath(pairs ...string) (*url.URL, error) { - return r.route.URLPath(pairs...) -} diff --git a/npoint/mux.go b/npoint/mux.go deleted file mode 100644 index 0e4fb92..0000000 --- a/npoint/mux.go +++ /dev/null @@ -1,169 +0,0 @@ -package npoint - -import ( - "fmt" - "net/http" - "sync" - - "github.com/gorilla/mux" - "github.com/muir/nject/nject" -) - -// ServiceWithMux allows a group of related endpoints to be started -// together. This form of service represents an already-started -// service that binds its endpoints using gorilla -// mux.Router.HandleFunc. -type ServiceWithMux struct { - Name string - endpoints map[string][]*EndpointRegistrationWithMux - Collection *nject.Collection - binder endpointBinderWithMux - lock sync.Mutex -} - -// ServiceRegistrationWithMux allows a group of related endpoints to be started -// together. This form of service represents pre-registered service -// service that binds its endpoints using gorilla -// mux.Router.HandleFunc. None of the endpoints associated -// with this service will initialize themselves or start listening -// until Start() is called. -type ServiceRegistrationWithMux struct { - Name string - started *ServiceWithMux - endpoints map[string][]*EndpointRegistrationWithMux - Collection *nject.Collection - lock sync.Mutex -} - -// EndpointRegistrationWithMux holds endpoint definitions for -// services that will be Start()ed with gorilla mux. Most of -// the gorilla mux methods can be used with these endpoint -// definitions. -type EndpointRegistrationWithMux struct { - EndpointRegistration - muxroutes []func(*mux.Route) *mux.Route - route *mux.Route - err error -} - -type endpointBinderWithMux func(string, func(http.ResponseWriter, *http.Request)) *mux.Route - -// PreregisterServiceWithMux creates a service that must be Start()ed later. -// -// The passed in funcs follow the same rules as for the funcs in a -// nject.Collection. -// -// The injectors and middlware functions will precede any injectors -// and middleware specified on each endpoint that registers with this -// service. -// -// PreregsteredServices do not initialize or bind to handlers until -// they are Start()ed. -// -// The name of the service is just used for error messages and is otherwise ignored. -func PreregisterServiceWithMux(name string, funcs ...interface{}) *ServiceRegistrationWithMux { - return registerServiceWithMux(name, funcs...) -} - -// RegisterServiceWithMux creates a service and starts it immediately. -func RegisterServiceWithMux(name string, router *mux.Router, funcs ...interface{}) *ServiceWithMux { - sr := PreregisterServiceWithMux(name, funcs...) - return sr.Start(router) -} - -// Start calls endpoints initializers for this Service and then registers all the -// endpoint handlers to the router. Start() should be called at most once. -func (s *ServiceRegistrationWithMux) Start(router *mux.Router) *ServiceWithMux { - s.lock.Lock() - defer s.lock.Unlock() - if s.started != nil { - panic("duplicate call to Start()") - } - for path, el := range s.endpoints { - for _, endpoint := range el { - endpoint.start(path, router.HandleFunc) - } - } - svc := &ServiceWithMux{ - Name: s.Name, - endpoints: s.endpoints, - Collection: s.Collection, - binder: router.HandleFunc, - } - s.started = svc - return svc -} - -// Start an endpoint: invokes the endpoint and binds it to the path. -// If called more than once, subsequent calls to -// EndpointRegistrationWithMux methods that act on the route will -// only act on the last route bound. -func (r *EndpointRegistrationWithMux) start( - path string, - binder endpointBinderWithMux, -) *mux.Route { - if !r.bound { - r.initialize() - r.bound = true - } - r.path = path - r.route = binder(path, r.finalFunc) - for _, mod := range r.muxroutes { - r.route = mod(r.route) - } - r.err = r.route.GetError() - return r.route -} - -// RegisterEndpoint pre-registers an endpoint. The provided funcs must all match one of the -// handler types. -// The functions provided are invoked in-order. -// Static injectors first and the endpoint last. -// -// The return value does not need to be retained -- it is also remembered -// in the Service. The return value can be used to add mux.Route-like -// modifiers. They will not take effect until the service is started. -// -// The endpoint initialization will not run until the service is started. If the -// service has already been started, the endpoint will be started immediately. -func (s *ServiceRegistrationWithMux) RegisterEndpoint(path string, funcs ...interface{}) *EndpointRegistrationWithMux { - s.lock.Lock() - defer s.lock.Unlock() - wmux := &EndpointRegistrationWithMux{ - EndpointRegistration: EndpointRegistration{ - path: path, - }, - muxroutes: make([]func(*mux.Route) *mux.Route, 0), - } - err := s.Collection.Append(path, funcs...).Bind(&wmux.EndpointRegistration.finalFunc, &wmux.EndpointRegistration.initialize) - if err != nil { - panic(fmt.Sprintf("Cannot bind %s %s: %s", s.Name, path, nject.DetailedError(err))) - } - s.endpoints[path] = append(s.endpoints[path], wmux) - - if s.started != nil { - wmux.start(path, s.started.binder) - } - return wmux -} - -// RegisterEndpoint registers and immediately starts an endpoint. -// The provided funcs must all match one of the handler types. -// The functions provided are invoked in-order. -// Static injectors first and the endpoint last. -func (s *ServiceWithMux) RegisterEndpoint(path string, funcs ...interface{}) *mux.Route { - s.lock.Lock() - defer s.lock.Unlock() - wmux := &EndpointRegistrationWithMux{ - EndpointRegistration: EndpointRegistration{ - path: path, - }, - muxroutes: make([]func(*mux.Route) *mux.Route, 0), - } - err := s.Collection.Append(path, funcs...).Bind(&wmux.EndpointRegistration.finalFunc, &wmux.EndpointRegistration.initialize) - if err != nil { - panic(fmt.Sprintf("Cannot bind %s %s: %s", s.Name, path, nject.DetailedError(err))) - } - s.endpoints[path] = append(s.endpoints[path], wmux) - return wmux.start(path, s.binder) -} diff --git a/npoint/npoint.go b/npoint/npoint.go deleted file mode 100644 index d30473b..0000000 --- a/npoint/npoint.go +++ /dev/null @@ -1,203 +0,0 @@ -package npoint - -// TODO: tests for CallsInner annotation -// TODO: inject path as a Path type. -// TODO: inject route as a mux.Route type. -// TODO: Duplicate service -// TODO: Duplicate endpoint -// TODO: When making copies of valueCollections, do deep copies when they implement a DeepCopy method. -// TODO: Re-use slots in the value collection when values do not overlap in time -// TODO: order the value collection so that middleware can make only partial copies -// TODO: new annotator: skip copying the value collection - -import ( - "fmt" - "net/http" - "sync" - - "github.com/muir/nject/nject" -) - -// Service allows a group of related endpoints to be started -// together. This form of service represents an already-started -// service that binds its enpoints using a simple binder like -// http.ServeMux.HandleFunc(). -type Service struct { - Name string - endpoints map[string]*EndpointRegistration - Collection *nject.Collection - binder EndpointBinder - lock sync.Mutex -} - -// ServiceRegistration allows a group of related endpoints to be started -// together. This form of service represents pre-registered service -// service that binds its enpoints using a simple binder like -// http.ServeMux.HandleFunc(). None of the endpoints associated -// with this service will initialize themselves or start listening -// until Start() is called. -type ServiceRegistration struct { - Name string - started *Service - endpoints map[string]*EndpointRegistration - Collection *nject.Collection - lock sync.Mutex -} - -// EndpointRegistration holds endpoint defintions for services -// that will be started w/o gorilla mux. -type EndpointRegistration struct { - finalFunc http.HandlerFunc - initialize func() - path string - bound bool -} - -// PreregisterService creates a service that must be Start()ed later. -// -// The passed in funcs follow the same rules as for the funcs in a -// nject.Collection. -// -// The injectors and middlware functions will precede any injectors -// and middleware specified on each endpoint that registers with this -// service. -// -// PreregsteredServices do not initialize or bind to handlers until -// they are Start()ed. -// -// The name of the service is just used for error messages and is otherwise ignored. -func PreregisterService(name string, funcs ...interface{}) *ServiceRegistration { - return registerService(name, funcs...) -} - -// RegisterService creates a service and starts it immediately. -func RegisterService(name string, binder EndpointBinder, funcs ...interface{}) *Service { - sr := PreregisterService(name, funcs...) - return sr.Start(binder) -} - -func registerService(name string, funcs ...interface{}) *ServiceRegistration { - return &ServiceRegistration{ - Name: name, - endpoints: make(map[string]*EndpointRegistration), - Collection: nject.Sequence(name, funcs...), - } -} - -func registerServiceWithMux(name string, funcs ...interface{}) *ServiceRegistrationWithMux { - return &ServiceRegistrationWithMux{ - Name: name, - endpoints: make(map[string][]*EndpointRegistrationWithMux), - Collection: nject.Sequence(name, funcs...), - } -} - -// EndpointBinder is the signature of the binding function -// used to start a ServiceRegistration. -type EndpointBinder func(path string, fn http.HandlerFunc) - -// Start runs all staticInjectors for all endpoints pre-registered with this -// service. Bind all endpoints and starts listening. Start() may -// only be called once. -func (s *ServiceRegistration) Start(binder EndpointBinder) *Service { - s.lock.Lock() - defer s.lock.Unlock() - if s.started != nil { - panic("duplicate call to Start()") - } - for path, endpoint := range s.endpoints { - endpoint.start(path, binder) - } - svc := &Service{ - Name: s.Name, - endpoints: s.endpoints, - Collection: s.Collection, - binder: binder, - } - s.started = svc - return svc -} - -// Start and endpoint: invokes the endpoint and binds it to the -// path. -func (r *EndpointRegistration) start(path string, binder EndpointBinder) { - r.path = path - if !r.bound { - r.initialize() - r.bound = true - } - binder(path, r.finalFunc) -} - -// CreateEndpoint generates a http.HandlerFunc from a list of handlers. This bypasses Service, -// ServiceRegistration, ServiceWithMux, and ServiceRegistrationWithMux. The -// static initializers are invoked immedately. -func CreateEndpoint(funcs ...interface{}) http.HandlerFunc { - c := nject.Sequence("createEndpoint", funcs...) - if len(funcs) == 0 { - panic("at least one handler must be provided") - } - var httpHandler http.HandlerFunc - var initFunc func() - err := c.Bind(&httpHandler, &initFunc) - if err != nil { - panic(fmt.Sprintf("Cannot create HandlerFunc binding %s", nject.DetailedError(err))) - } - initFunc() - return httpHandler -} - -// RegisterEndpoint pre-registers an endpoint. The provided funcs must all match one of the -// handler types. -// The functions provided are invoked in-order. -// Static injectors first and the endpoint last. -// -// The return value does not need to be retained -- it is also remembered -// in the ServiceRegistration. -// -// The endpoint initialization will not run until the service is started. If the -// service has already been started, the endpoint will be started immediately. -func (s *ServiceRegistration) RegisterEndpoint(path string, funcs ...interface{}) *EndpointRegistration { - s.lock.Lock() - defer s.lock.Unlock() - if s.endpoints[path] != nil { - panic("endpoint path already registered") - } - r := &EndpointRegistration{ - path: path, - } - err := s.Collection.Append(path, funcs...).Bind(&r.finalFunc, &r.initialize) - if err != nil { - panic(fmt.Sprintf("Cannot bind %s %s: %s", s.Name, path, nject.DetailedError(err))) - } - s.endpoints[path] = r - if s.started != nil { - r.start(path, s.started.binder) - } - return r -} - -// RegisterEndpoint registers and immedately starts an endpoint. -// The provided funcs must all match one of handler types. -// The functions provided are invoked in-order. -// Static injectors first and the endpoint last. -// -// The return value does not need to be retained -- it is also remembered -// in the Service. -func (s *Service) RegisterEndpoint(path string, funcs ...interface{}) *EndpointRegistration { - s.lock.Lock() - defer s.lock.Unlock() - if s.endpoints[path] != nil { - panic("endpoint path already registered") - } - r := &EndpointRegistration{ - path: path, - } - err := s.Collection.Append(path, funcs...).Bind(&r.finalFunc, &r.initialize) - if err != nil { - panic(fmt.Sprintf("Cannot bind %s %s: %s", s.Name, path, nject.DetailedError(err))) - } - s.endpoints[path] = r - r.start(path, s.binder) - return r -} diff --git a/npoint/regression_test.go b/npoint/regression_test.go deleted file mode 100644 index 7777838..0000000 --- a/npoint/regression_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package npoint_test - -import ( - "bytes" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gorilla/mux" - "github.com/muir/nject/npoint" - "github.com/stretchr/testify/assert" -) - -type RequestBody []byte - -func LogError(inner func() error) { - // actually, ignore error - inner() -} - -func SaveRequest(inner func(RequestBody) error, r *http.Request) error { - buf, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - r.Body = ioutil.NopCloser(bytes.NewBuffer(buf)) - return inner(buf) -} - -func TestSaveRequest(t *testing.T) { - t.Parallel() - calledPost := false - calledGet := false - s := npoint.PreregisterServiceWithMux("TestSaveRequest", LogError, SaveRequest) - - s.RegisterEndpoint("/ept", func(body RequestBody, w http.ResponseWriter) error { - w.WriteHeader(204) - assert.Equal(t, "some stuff", string(body)) - calledPost = true - return nil - }).Methods("POST") - - s.RegisterEndpoint("/ept", func(w http.ResponseWriter) error { - w.WriteHeader(204) - calledGet = true - return nil - }).Methods("GET") - - muxRouter := mux.NewRouter() - assert.False(t, calledPost) - assert.False(t, calledGet) - s.Start(muxRouter) - assert.False(t, calledPost) - assert.False(t, calledGet) - - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - - // nolint:noctx - resp, err := http.Post(localServer.URL+"/ept", "text/plain", ioutil.NopCloser(bytes.NewBuffer([]byte("some stuff")))) - assert.NoError(t, err) - assert.True(t, calledPost) - assert.False(t, calledGet) - if resp != nil { - resp.Body.Close() - } - - // nolint:noctx - resp, err = http.Get(localServer.URL + "/ept") - assert.NoError(t, err) - assert.True(t, calledGet) - if resp != nil { - resp.Body.Close() - } -} diff --git a/npoint/service_test.go b/npoint/service_test.go deleted file mode 100644 index 0036295..0000000 --- a/npoint/service_test.go +++ /dev/null @@ -1,478 +0,0 @@ -package npoint_test - -import ( - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gorilla/mux" - "github.com/muir/nject/nject" - "github.com/muir/nject/npoint" - "github.com/stretchr/testify/assert" -) - -var ( - // Use a custom Transport because httptest "helpfully" kills idle - // connections on the default transport when a httptest server shuts - // down. - tr = &http.Transport{ - // Disable keepalives to avoid the hassle of closing idle - // connections after each test. - DisableKeepAlives: true, - } - client = &http.Client{Transport: tr} -) - -func TestStaticInitializerWaitsForStart(t *testing.T) { - t.Parallel() - var debugOutput string - var svcInitCount int - var svcInvokeCount int - serviceSequenceInitFunc := func(db *nject.Debugging) string { - t.Logf("service init chain") - debugOutput = strings.Join(db.Included, "\n") - svcInitCount++ - return "foo" - } - serviceSequenceNonCacheable := func(s string, r *http.Request, db *nject.Debugging) { - t.Logf("service invoke chain") - debugOutput = strings.Join(db.Included, "\n") - svcInvokeCount++ - } - var initCount int - var invokeCount int - endpointSequenceInitFunc := func(db *nject.Debugging) int { - t.Logf("endpoint init chain") - initCount++ - debugOutput = strings.Join(db.Included, "\n") - return initCount - } - endpointSequenceFinalFunc := func(w http.ResponseWriter, i int, db *nject.Debugging) { - t.Logf("endpoint invoke chain") - w.WriteHeader(204) - debugOutput = strings.Join(db.Included, "\n") - invokeCount++ - } - multiStartups( - t, - "test", - nject.Sequence("SERVICE", nject.Cacheable(serviceSequenceInitFunc), nject.Cacheable(serviceSequenceNonCacheable)), - nject.Sequence("ENDPOINT", nject.Cacheable(endpointSequenceInitFunc), nject.Cacheable(endpointSequenceFinalFunc)), - func(s string) { - // reset - t.Logf("reset for %s", s) - debugOutput = "" - initCount = 0 - invokeCount = 0 - svcInitCount = 10 - svcInvokeCount = 10 - }, - func(s string) { - // after register - assert.Equal(t, 0, initCount, s+" after register endpoint init count\n"+debugOutput) - assert.Equal(t, 0, invokeCount, s+" after register endpoint invoke count\n"+debugOutput) - assert.Equal(t, 10, svcInitCount, s+" after register service init count\n"+debugOutput) - assert.Equal(t, 10, svcInvokeCount, s+" after register service invoke count\n"+debugOutput) - }, - func(s string) { - // after start - assert.Equal(t, 1, initCount, s+" after start endpoint init count\n"+debugOutput) - assert.Equal(t, 0, invokeCount, s+" after start endpoint invoke count\n"+debugOutput) - assert.Equal(t, 11, svcInitCount, s+" after start service init count\n"+debugOutput) - assert.Equal(t, 10, svcInvokeCount, s+" after start service invoke count\n"+debugOutput) - }, - func(s string) { - // after 1st call - assert.Equal(t, 1, initCount, s+" 1st call start init count\n"+debugOutput) - assert.Equal(t, 1, invokeCount, s+" 1st call start invoke count\n"+debugOutput) - assert.Equal(t, 11, svcInitCount, s+" 1st call service init count\n"+debugOutput) - assert.Equal(t, 11, svcInvokeCount, s+" 1st call service invoke count\n"+debugOutput) - }, - func(s string) { - // after 2nd call - assert.Equal(t, 1, initCount, s+" 2nd call endpoint init count\n"+debugOutput) - assert.Equal(t, 2, invokeCount, s+" 2nd call endpoint invoke count\n"+debugOutput) - assert.Equal(t, 11, svcInitCount, s+" 2nd call service init count\n"+debugOutput) - assert.Equal(t, 12, svcInvokeCount, s+" 2nd call service invoke count\n"+debugOutput) - }, - ) -} - -func TestFallibleInjectorFailing(t *testing.T) { - t.Parallel() - var initCount int - var invokeCount int - var errorsCount int - multiStartups( - t, - "test", - nil, - nject.Sequence("hc", - func(inner func() error, w http.ResponseWriter) { - t.Logf("wraper (before)") - err := inner() - t.Logf("wraper (after, err=%v)", err) - if err != nil { - assert.Equal(t, "bailing out", err.Error()) - errorsCount++ - w.WriteHeader(204) - } - }, - func() (nject.TerminalError, int) { - t.Logf("endpoint init") - initCount++ - return fmt.Errorf("bailing out"), initCount - }, - func(w http.ResponseWriter, i int) error { - t.Logf("endpoint invoke") - w.WriteHeader(204) - invokeCount++ - return nil - }, - ), - func(s string) { - // reset - t.Logf("reset for %s", s) - initCount = 0 - invokeCount = 0 - errorsCount = 0 - }, - func(s string) { - // after register - assert.Equal(t, 0, initCount, s+" after register endpoint init count") - assert.Equal(t, 0, invokeCount, s+" after register endpoint invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after start - assert.Equal(t, 0, initCount, s+" after start endpoint init count") - assert.Equal(t, 0, invokeCount, s+" after start endpoint invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after 1st call - assert.Equal(t, 1, initCount, s+" 1st call start init count") - assert.Equal(t, 0, invokeCount, s+" 1st call start invoke count") - assert.Equal(t, 1, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after 2nd call - assert.Equal(t, 2, initCount, s+" 2nd call endpoint init count") - assert.Equal(t, 0, invokeCount, s+" 2nd call endpoint invoke count") - assert.Equal(t, 2, errorsCount, s+" after register endpoint invoke count") - }, - ) -} - -func TestFallibleInjectorNotFailing(t *testing.T) { - t.Parallel() - var initCount int - var invokeCount int - var errorsCount int - multiStartups( - t, - "testFallibleInjectorNotFailing", - nil, - nject.Sequence("hc", - func(inner func() error) { - t.Logf("wraper (before)") - err := inner() - t.Logf("wraper (after, err=%v)", err) - if err != nil { - errorsCount++ - } - }, - func() (nject.TerminalError, int) { - t.Logf("endpoint init") - initCount++ - return nil, 17 - }, - func(w http.ResponseWriter, i int) error { - assert.Equal(t, 17, i) - t.Logf("endpoint invoke") - w.WriteHeader(204) - invokeCount++ - return nil - }, - ), - func(s string) { - // reset - t.Logf("reset for %s", s) - initCount = 0 - invokeCount = 0 - errorsCount = 0 - }, - func(s string) { - // after register - assert.Equal(t, 0, initCount, s+" after register endpoint init count") - assert.Equal(t, 0, invokeCount, s+" after register endpoint invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after start - assert.Equal(t, 0, initCount, s+" after start endpoint init count") - assert.Equal(t, 0, invokeCount, s+" after start endpoint invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after 1st call - assert.Equal(t, 1, initCount, s+" 1st call start init count") - assert.Equal(t, 1, invokeCount, s+" 1st call start invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - func(s string) { - // after 2nd call - assert.Equal(t, 2, initCount, s+" 2nd call endpoint init count") - assert.Equal(t, 2, invokeCount, s+" 2nd call endpoint invoke count") - assert.Equal(t, 0, errorsCount, s+" after register endpoint invoke count") - }, - ) -} - -func multiStartups( - t *testing.T, - name string, - shc *nject.Collection, - hc *nject.Collection, - reset func(string), - afterRegister func(string), // not called for CreateEnpoint - afterStart func(string), - afterCall1 func(string), - afterCall2 func(string), -) { - { - n := name + "-1PreregisterNoMuxRegisterEndpointBeforeStart" - reset(n) - ept := "/" + n - s := npoint.PreregisterService(n, shc) - s.RegisterEndpoint(ept, hc) - afterRegister(n) - b := NewBinder() - s.Start(b.Bind) - afterStart(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall1(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall2(n) - } - { - n := name + "-2PreregisterNoMuxRegisterEndpointAfterStartWithOriginalService" - reset(n) - ept := "/" + n - s := npoint.PreregisterService(n, shc) - b := NewBinder() - s.Start(b.Bind) - s.RegisterEndpoint(ept, hc) - // afterRegister(n) - afterStart(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall1(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall2(n) - } - { - n := name + "-3PreregisterNoMuxRegisterEndpointAfterStartWithStartedService" - reset(n) - ept := "/" + n - s := npoint.PreregisterService(n, shc) - b := NewBinder() - sr := s.Start(b.Bind) - sr.RegisterEndpoint(ept, hc) - // afterRegister(n) - afterStart(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall1(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall2(n) - } - { - n := name + "-4PreregisterServiceWithMuxRegisterEndpointBeforeStart" - reset(n) - ept := "/" + n - s := npoint.PreregisterServiceWithMux(n, shc) - s.RegisterEndpoint(ept, hc) - afterRegister(n) - muxRouter := mux.NewRouter() - s.Start(muxRouter) - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - afterStart(n) - t.Logf("GET %s%s\n", localServer.URL, ept) - // nolint:noctx - resp, err := client.Get(localServer.URL + ept) - assert.NoError(t, err) - if resp != nil { - resp.Body.Close() - } - afterCall1(n) - // nolint:noctx - resp, err = client.Get(localServer.URL + ept) - assert.NoError(t, err, name) - if resp != nil { - assert.Equal(t, 204, resp.StatusCode, name) - resp.Body.Close() - } - afterCall2(n) - } - { - n := name + "-5PreregisterWithMuxRegisterEndpointAfterStartUsingOriginalService" - reset(n) - ept := "/" + n - s := npoint.PreregisterServiceWithMux(n, shc) - muxRouter := mux.NewRouter() - s.Start(muxRouter) - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - s.RegisterEndpoint(ept, hc) - // afterRegister(n) - afterStart(n) - t.Logf("GET %s%s\n", localServer.URL, ept) - // nolint:noctx - resp, err := client.Get(localServer.URL + ept) - assert.NoError(t, err) - if resp != nil { - resp.Body.Close() - } - afterCall1(n) - // nolint:noctx - resp, err = client.Get(localServer.URL + ept) - assert.NoError(t, err, name) - if resp != nil { - assert.Equal(t, 204, resp.StatusCode, name) - resp.Body.Close() - } - afterCall2(n) - } - { - n := name + "-6PreregisterWithMuxRegisterEndpointAfterStartUsingStartedService" - reset(n) - ept := "/" + n - s := npoint.PreregisterServiceWithMux(n, shc) - muxRouter := mux.NewRouter() - sr := s.Start(muxRouter) - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - sr.RegisterEndpoint(ept, hc) - // afterRegister(n) - afterStart(n) - t.Logf("GET %s%s\n", localServer.URL, ept) - // nolint:noctx - resp, err := client.Get(localServer.URL + ept) - assert.NoError(t, err) - if resp != nil { - resp.Body.Close() - } - afterCall1(n) - // nolint:noctx - resp, err = client.Get(localServer.URL + ept) - assert.NoError(t, err, name) - if resp != nil { - assert.Equal(t, 204, resp.StatusCode, name) - resp.Body.Close() - } - afterCall2(n) - } - { - n := name + "-7CreateEndpoint" - reset(n) - ept := "/" + n - b := NewBinder() - e := npoint.CreateEndpoint(shc, hc) - b.Bind(ept, e) - // afterRegister(n) - afterStart(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall1(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall2(n) - } - { - n := name + "-8RegisterServiceNoMux" - reset(n) - ept := "/" + n - b := NewBinder() - s := npoint.RegisterService(n, b.Bind, shc) - s.RegisterEndpoint(ept, hc) - // afterRegister(n) - afterStart(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall1(n) - // nolint:bodyclose - b.Call(ept, "GET", "", nil) - afterCall2(n) - } - { - n := name + "-9RegisterServiceWithMux" - reset(n) - ept := "/" + n - muxRouter := mux.NewRouter() - s := npoint.RegisterServiceWithMux(n, muxRouter, shc) - s.RegisterEndpoint(ept, hc) - // afterRegister(n) - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - afterStart(n) - t.Logf("GET %s%s\n", localServer.URL, ept) - // nolint:noctx - resp, err := client.Get(localServer.URL + ept) - assert.NoError(t, err) - if resp != nil { - resp.Body.Close() - } - afterCall1(n) - // nolint:noctx - resp, err = client.Get(localServer.URL + ept) - assert.NoError(t, err, name) - if resp != nil { - assert.Equal(t, 204, resp.StatusCode, name) - resp.Body.Close() - } - afterCall2(n) - } -} - -func TestMuxModifiers(t *testing.T) { - t.Parallel() - s := npoint.PreregisterServiceWithMux("TestCharacterize") - - s.RegisterEndpoint("/x", func(w http.ResponseWriter) { - w.WriteHeader(204) - }).Methods("GET") - - s.RegisterEndpoint("/x", func(w http.ResponseWriter) { - w.WriteHeader(205) - }).Methods("POST") - - muxRouter := mux.NewRouter() - s.Start(muxRouter) - - localServer := httptest.NewServer(muxRouter) - defer localServer.Close() - - // nolint:noctx - resp, err := client.Get(localServer.URL + "/x") - if !assert.NoError(t, err) { - return - } - resp.Body.Close() - assert.Equal(t, 204, resp.StatusCode) - - // nolint:noctx - resp, err = client.Post(localServer.URL+"/x", "application/json", nil) - if !assert.NoError(t, err) { - return - } - resp.Body.Close() - assert.Equal(t, 205, resp.StatusCode) -} diff --git a/nserve/README.md b/nserve/README.md deleted file mode 100644 index f647be0..0000000 --- a/nserve/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# nserve - server startup/shutdown in an nject world - -[![GoDoc](https://godoc.org/github.com/muir/nject/nserver?status.png)](http://godoc.org/github.com/muir/nject/nserve) -[![Coverage](http://gocover.io/_badge/github.com/muir/nject/nserve)](https://gocover.io/github.com/muir/nject/nserve) - -Install: - - go get github.com/muir/nject - ---- - -This package provides server startup and shutdown wrappers that can be used -with libraries and servers that are stated with nject. - -### How to structure your application - -Libraries become injected dependencies. They can in turn have other libraries -as dependencies of them. Since only the depenencies that are required are -actaully injected, the easiest thing is to have a master list of all your libraries -and provide that to all your apps. - -Let's call that master list `allLibraries`. - -```go -app, err := nserve.CreateApp("myApp", allLibrariesSequence, createAppFunction) -err = app.Do(nserve.Start) -err = app.Do(nserve.Stop) -``` - -### Hooks - -Libaries and appliations can register callbacks on a per-hook basis. Two hooks -are pre-provided by other hooks can be created. - -Hook invocation can be limited by a timeout. If the hook does not complete in -that amount of time, the hook will return error and continue processing in the -background. - -The Start, Stop, and Shutdown hooks are pre-defined: - -```go -var Shutdown = NewHook("shutdown", ReverseOrder) -var Stop = NewHook("stop", ReverseOrder).OnError(Shutdown).ContinuePastError(true) -var Start = NewHook("start", ForwardOrder).OnError(Stop) -``` - -Hooks can be invoked in registration order (`ForwardOrder`) or in -reverse registration order `ReverseOrder`. - -If there is an `OnError` modifier for the hook registration, then that -hook gets invoked if there is an error returned when the hook is running. - -Libraries and applications can register callbacks for hooks by taking an -`*nserve.App` as as an input parameter and then using that to register callbacks: - -```go -func CreateMyLibrary(app *nserve.App) *MyLibrary { - lib := &MyLibrary{ ... } - app.On(Start, lib.Start) - return lib -} -func (lib *MyLibrary) Start(app *nserve.App) { - app.On(Stop, lib.Stop) -} -``` - -The callback function can be any nject injection chain. If it ends with a -function that can return -error, then any such error will be become the error return from `app.Do` and if -there is an `OnError` handler for that hook, that handler will be invoked. - diff --git a/nserve/example_test.go b/nserve/example_test.go deleted file mode 100644 index 95b9bf1..0000000 --- a/nserve/example_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package nserve_test - -import ( - "fmt" - - "github.com/muir/nject/nserve" - "github.com/pkg/errors" -) - -type ( - L1 struct{} - L2 struct{} - L3 struct{} -) - -func NewL1(app *nserve.App) *L1 { - fmt.Println("L1 created") - app.On(nserve.Start, func(app *nserve.App) { - app.On(nserve.Stop, func() error { - fmt.Println("L1 stopped") - return fmt.Errorf("L1 stop error") - }) - fmt.Println("L1 started") - }) - return &L1{} -} - -func NewL2(app *nserve.App, _ *L1) *L2 { - fmt.Println("L2 created") - app.On(nserve.Start, func(app *nserve.App) error { - app.On(nserve.Stop, func() error { - fmt.Println("L2 stopped") - return fmt.Errorf("L2 stop error") - }) - fmt.Println("L2 started") - // Note: Library2 start will return error - return fmt.Errorf("L2 start error") - }) - return &L2{} -} - -func NewL3(_ *L2, app *nserve.App) *L3 { - fmt.Println("L3 created") - app.On(nserve.Start, func(app *nserve.App) { - fmt.Println("L3 started") - }) - return &L3{} -} - -func ErrorCombiner(e1, e2 error) error { - return errors.New(e1.Error() + "; " + e2.Error()) -} - -// Example shows the injection, startup, and shutdown of an app with two libraries -func Example() { - nserve.Start.SetErrorCombiner(ErrorCombiner) - nserve.Stop.SetErrorCombiner(ErrorCombiner) - nserve.Shutdown.SetErrorCombiner(ErrorCombiner) - app, err := nserve.CreateApp("myApp", NewL1, NewL2, NewL3, func(_ *L1, _ *L2, _ *L3, app *nserve.App) { - fmt.Println("App created") - }) - fmt.Println("create error:", err) - err = app.Do(nserve.Start) - fmt.Println("do start error:", err) - // Output: L1 created - // L2 created - // L3 created - // App created - // create error: - // L1 started - // L2 started - // L1 stopped - // L2 stopped - // do start error: L2 start error; L1 stop error; L2 stop error -} diff --git a/nserve/hook.go b/nserve/hook.go deleted file mode 100644 index ef1774a..0000000 --- a/nserve/hook.go +++ /dev/null @@ -1,114 +0,0 @@ -package nserve - -import ( - "sync" - "sync/atomic" -) - -var hookCounter int32 - -type hookOrder string - -const ( - // ForwardOrder is used to indicate that the items - // registered for a hook will be invoked in the order - // that they were registered. - ForwardOrder hookOrder = "forward" - // ReverseOrder is used to indicate that the items - // registered for a hook will be invoked opposite to the order - // that they were registered. - ReverseOrder = "forward" -) - -type hookId int32 - -// Hook is the handle/name for a list of callbacks to invoke. -type Hook struct { - Id hookId - lock *sync.Mutex - Name string - Order hookOrder - InvokeOnError []*Hook - ContinuePast bool - ErrorCombiner func(first, second error) error - Providers []interface{} -} - -// Copy makes a deep copy of a hook and the new hook gets a new Id. -// Copy is thread-safe. -func (h *Hook) Copy() *Hook { - h.lock.Lock() - defer h.lock.Unlock() - oe := make([]*Hook, len(h.InvokeOnError)) - copy(oe, h.InvokeOnError) - op := make([]interface{}, len(h.Providers)) - copy(op, h.Providers) - hc := *h - hc.InvokeOnError = oe - hc.Id = hookId(atomic.AddInt32(&hookCounter, 1)) - hc.Providers = op - hc.lock = new(sync.Mutex) - return &hc -} - -// NewHook creates a new category of callbacks. -func NewHook(name string, order hookOrder) *Hook { - return &Hook{ - Id: hookId(atomic.AddInt32(&hookCounter, 1)), - Name: name, - Order: order, - lock: new(sync.Mutex), - } -} - -// OnError adds to the set of hooks to invoke when this hook is -// thows an error. Call with nil to clear the set of hooks to invoke. -// OnError is thread-safe. -func (h *Hook) OnError(e *Hook) *Hook { - h.lock.Lock() - defer h.lock.Unlock() - if e == nil { - h.InvokeOnError = nil - } else { - h.InvokeOnError = append(h.InvokeOnError, e) - } - return h -} - -// SetErrorCombiner sets a function to combine two errors into one when there -// is more than one error to return from a invoking all the callbacks -// SetErrorCombiner is thread-safe. -func (h *Hook) SetErrorCombiner(f func(first, second error) error) *Hook { - h.lock.Lock() - defer h.lock.Unlock() - h.ErrorCombiner = f - return h -} - -// ContinuePastError sets if callbacks should continue to be invoked -// if there has already been an error. -// ContinuePastError is thread-safe. -func (h *Hook) ContinuePastError(b bool) *Hook { - h.lock.Lock() - defer h.lock.Unlock() - h.ContinuePast = b - return h -} - -// String is not thread-safe with respect to reaching into a hook and -// changing it's Name. Don't do that. -func (h *Hook) String() string { - return "hook " + h.Name -} - -// Shutdown is a reverse-order hook meant to be used for forced shutdowns. -// If Stop encounters an error, then Shutdown will also be called. -var Shutdown = NewHook("shutdown", ReverseOrder) - -// Stop is a reverse-order hook meant to be used when stopping. If an error -// is encountered, Shutdown will also be used. -var Stop = NewHook("stop", ReverseOrder).OnError(Shutdown).ContinuePastError(true) - -// Start is a forward-order hook for starting services. If it encounters -// an error, it will invoke Stop on whatever was started. -var Start = NewHook("start", ForwardOrder).OnError(Stop) diff --git a/nserve/nserve.go b/nserve/nserve.go deleted file mode 100644 index 564e1cb..0000000 --- a/nserve/nserve.go +++ /dev/null @@ -1,97 +0,0 @@ -// Package nserve helps with server startup and shutdown by by allowing libraries -// too register themselves with hooks that run at startup and shutdown. -package nserve - -import ( - "context" - "sync" - - "github.com/muir/nject/nject" -) - -// App provides hooks to start and stop libraries that are used by an app. It -// expected that an App corresponds to a service and that libraries that the -// service uses need to be started & stopped. -type App struct { - lock sync.Mutex // held when adding hooks - runLock sync.Mutex // held when running hooks - Hooks map[hookId][]nject.Provider -} - -// CreateApp will use nject.Run() to invoke the providers that make up the service -// represented by the app. -func CreateApp(name string, providers ...interface{}) (*App, error) { - app := &App{ - Hooks: make(map[hookId][]nject.Provider), - } - ctx, cancel := context.WithCancel(context.Background()) - app.Hooks[Shutdown.Id] = append(app.Hooks[Shutdown.Id], nject.Provide("app-cancel-ctx", cancel)) - err := nject.Run(name, ctx, app, nject.Sequence("app-providers", providers...)) - return app, err -} - -// On registers a callback to be invoked on hook invocation. This can be used during -// callbacks, for example a start callback, can register a stop callback. Each call -// to On() adds one nject provider chain. By default, only the last function will -// be called and nject annotations can be used to control the behavior. -func (app *App) On(h *Hook, providers ...interface{}) { - app.lock.Lock() - defer app.lock.Unlock() - app.Hooks[h.Id] = append(app.Hooks[h.Id], nject.Sequence("on-"+h.Name, providers...)) -} - -// Do invokes the callbacks for a hook. It returns only the first error reported -// unless the hook provides an error combiner that preserves the other errors. -func (app *App) Do(h *Hook) error { - app.runLock.Lock() - defer app.runLock.Unlock() - return app.do(h) -} - -func (app *App) do(h *Hook) error { - ec := h.ErrorCombiner - if ec == nil { - ec = func(err, _ error) error { return err } - } - ecw := func(e1, e2 error) error { - if e1 == nil { - return e2 - } - if e2 == nil { - return e1 - } - return ec(e1, e2) - } - app.lock.Lock() - chains := make([]nject.Provider, len(app.Hooks[h.Id])) - copy(chains, app.Hooks[h.Id]) - app.lock.Unlock() - var err error - runChain := func(chain nject.Provider) { - e := nject.Run("hook-"+h.Name, - app, - nject.Sequence("hook-"+h.Name+"-providers", h.Providers...), - chain) - err = ecw(err, e) - } - if h.Order == ForwardOrder { - for _, chain := range chains { - runChain(chain) - if err != nil && !h.ContinuePast { - break - } - } - } else { - for i := len(chains) - 1; i >= 0; i-- { - chain := chains[i] - runChain(chain) - if err != nil && !h.ContinuePast { - break - } - } - } - for _, oe := range h.InvokeOnError { - err = ecw(err, app.do(oe)) - } - return err -} diff --git a/nvelope/README.md b/nvelope/README.md deleted file mode 100644 index 2339a07..0000000 --- a/nvelope/README.md +++ /dev/null @@ -1,100 +0,0 @@ -# nvelope - http endpoint helpers in an nject world - -[![GoDoc](https://godoc.org/github.com/muir/nject/nserver?status.png)](http://godoc.org/github.com/muir/nject/nvelope) -[![Coverage](http://gocover.io/_badge/github.com/muir/nject/nvelope)](https://gocover.io/github.com/muir/nject/nvelope) - -Install: - - go get github.com/muir/nject - ---- - -This package provides helpers for wrapping endpoints. - -## Typical chain - -A typical endpoint wrapping chan contains some or all of the following. - -### Create a logger - -This is an option step that is recommended if you're using request-specific -loggers. The encoding provider can uses a logger that implements the -`nvelope.BasicLogger` interface. `nvelope.LoggerFromStd` can create -an `nvelope.BasicLogger` from the "log" logger. `nvelope.NoLogger` provides -a `nvelope.BasicLogger` that does nothing. - -### Deferred Writer - -Use `nvelope.InjectWriter` to create a `*DeferredWriter`. A `*DeferredWriter` is a -useful enchancement to `http.ResponseWriter` that allows the output to be reset and -allows headers to be set at any time. The cost of a `*DeferredWriter` is that -the output is buffered and copied. - -### Marshal response - -We need the request encoder this early in the framework -so that it can marshal error responses. - -A JSON marshaller is provided: `nvelope.EncodeJSON`. Other -response encoders can be created with `nvelope.MakeResponseEncoder`. - -### Catch panics - -Have the endpoint return a 500 when there is a panic. -`nvelope.SetErrorOnPanic()` is a function that can be deferred to -notice a panic and create a useful error. In an injection -chain, use `nvelpe.CatchPanic`. - -### Return 204 for nil responses - -Use an extra injector to trigger a 204 response for nil content instead -of having the encoder handle nil specially. `nvelope.Nil204` does this. - -### Grab the request body - -The request body is more convieniently handled as a []byte . This is also -one place where API enforcement can be done. The type `nvelope.Body` is provided by -`nvelope.ReadBody` via injection to any provider that wants it. - -### Validate response - -This is a user-provided optional step that can be used to double-check -that what is being sent matches the API defintion. - -The [nvalid](https://github.com/muir/nvalid) package provides a function -to generate a response validator from Swagger. - -### Decode the request body - -The request body needs to be unpacked with an unmarshaller of some kind. -`nvelope.GenerateDecoder` creates decoders that examine the injection chain -looking for models that are consumed but not provided. If it finds any, -it examines those models for struct tags that indicate that nvelope should -create and fill the model. - -If so, it generates a provider that fills the model from the request. -This includes filling fields for the main decoded request body and also -includes filling fields from URL path elements, URL query parameters, and -HTTP headers. - -`nvelope.DecodeJSON` and `nvelope.DecodeXML` are pre-defined for -convience. - -### Validate the request - -This is an optional step, provided by the user of `nvelope`, that -should return `nject.TerminalError` if the request is not valid. Other -validation can happen later, but this is good place to enforce API compliance. -The [nvalid](https://github.com/muir/nvalid) package provides a function -to generate an input validator from Swagger. - -### Actually handle the request - -At this point the request model has been decoded. The other input parameters -(from headers, path, and query parameters) have been decoded. The input model -may have been validated. - -The response will automatically be encoded. The endpoint handler returns the -response and and error. If there is an error, it will trigger an appropriate -return. Use `nvelope.ReturnCode` to set the return code if returning an error. - diff --git a/nvelope/debug.go b/nvelope/debug.go deleted file mode 100644 index b6117e7..0000000 --- a/nvelope/debug.go +++ /dev/null @@ -1,14 +0,0 @@ -package nvelope - -import ( - "strings" - - "github.com/muir/nject/nject" -) - -// DebugIncludeExclude is a tiny wrapper around nject.Debugging. -// It logs the IncludeExclude strings. -var DebugIncludeExclude = nject.Required(nject.Provide("debug-include/exclude", - func(log BasicLogger, d *nject.Debugging) { - log.Debug(strings.Join(d.IncludeExclude, "\n")) - })) diff --git a/nvelope/decode.go b/nvelope/decode.go deleted file mode 100644 index 967e3d8..0000000 --- a/nvelope/decode.go +++ /dev/null @@ -1,1040 +0,0 @@ -package nvelope - -import ( - "bytes" - "encoding" - "encoding/json" - "encoding/xml" - "io/ioutil" - "net/http" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/muir/nject/nject" - "github.com/muir/reflectutils" - - "github.com/gorilla/mux" - "github.com/pkg/errors" - "gopkg.in/yaml.v2" -) - -// Body is a type provideded by ReadBody: it is a []byte -// with the request body pre-read. -type Body []byte - -// ReadBody is a provider that reads the input body from -// an http.Request and provides it in the Body type. -var ReadBody = nject.Provide("read-body", readBody) - -func readBody(r *http.Request) (Body, nject.TerminalError) { - // nolint:errcheck - defer r.Body.Close() - body, err := ioutil.ReadAll(r.Body) - r.Body = ioutil.NopCloser(bytes.NewReader(body)) - return Body(body), err -} - -// DecodeJSON is is a pre-defined special nject.Provider -// created with GenerateDecoder for decoding JSON requests. -var DecodeJSON = GenerateDecoder( - WithDecoder("application/json", json.Unmarshal), - WithDefaultContentType("application/json"), -) - -// DecodeXML is is a pre-defined special nject.Provider -// created with GenerateDecoder for decoding XML requests. -var DecodeXML = GenerateDecoder( - WithDecoder("application/xml", xml.Unmarshal), - WithDefaultContentType("application/xml"), -) - -// Decoder is the signature for decoders: take bytes and -// a pointer to something and deserialize it. -type Decoder func([]byte, interface{}) error - -type eigo struct { - tag string - decoders map[string]Decoder - defaultContentType string - rejectUnknownQueryParameters bool -} - -// DecodeInputsGeneratorOpt are functional arguments for -// GenerateDecoder -type DecodeInputsGeneratorOpt func(*eigo) - -// WithDecoder maps conent types (eg "application/json") to -// decode functions (eg json.Unmarshal). If a Content-Type header -// is used in the requet, then the value of that header will be -// used to pick a decoder. -func WithDecoder(contentType string, decoder Decoder) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.decoders[contentType] = decoder - } -} - -// WithDefaultContentType specifies which model decoder to use when -// no "Content-Type" header was sent. -func WithDefaultContentType(contentType string) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.defaultContentType = contentType - } -} - -// RejectUnknownQueryParameters true indicates that if there are any -// query parameters supplied that were not expected, the request should -// be rejected with a 400 response code. This parameter also controls -// what happens if there an embedded object is filled and there is no -// object key corresponding to the request parameter. -// -// This does not apply to query parameters with content=application/json -// decodings. If you want to disallow unknown tags for content= decodings, -// define a custom decoder. -func RejectUnknownQueryParameters(b bool) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.rejectUnknownQueryParameters = b - } -} - -/* TODO -func WithModelValidator(f func(interface{}) error) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.modelValidators = append(o.modelValidators, f) - } -} -*/ - -/* TODO -func CallModelMethodIfPresent(method string) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.methodIfPresent = append(o.methodIfPresent, method) - } -} -*/ - -// WithTag overrides the tag for specifying fields to be filled -// from the http request. The default is "nvelope" -func WithTag(tag string) DecodeInputsGeneratorOpt { - return func(o *eigo) { - o.tag = tag - } -} - -// TODO: Does this work? -// This model can be defined right in the function though: -// -// func HandleEndpoint( -// inputs struct { -// EndpointRequestModel `nvelope:model` -// }) (nvelope.Any, error) { -// ... -// } - -var deepObjectRE = regexp.MustCompile(`^([^\[]+)\[([^\]]+)\]$`) // id[name] - -// TODO: handle multipart form uploads - -// GenerateDecoder injects a special provider that uses -// nject.GenerateFromInjectionChain to examine the injection -// chain to see if there are any models that are used but -// never provided. If so, it looks at the struct tags in -// the models to see if they are tagged for filling with -// the decoder. If so, a provider is created that injects -// the missing model into the dependency chain. The intended -// use for this is to have an endpoint handler receive the -// deocded request body. -// -// Major warning: the endpoint handler must receive the request -// model as a field inside a model, not as a standalone model. -// -// The following tags are recognized: -// -// `nvelope:"model"` causes the POST or PUT body to be decoded -// using a decoder like json.Unmarshal. -// -// `nvelope:"path,name=xxx"` causes part of the URL path to -// be extracted and written to the tagged field. -// -// `nvelope:"query,name=xxx"` causes the named URL query -// parameters to be extracted and written to the tagged field. -// -// `nvelope:"header,name=xxx"` causes the named HTTP header -// to be extracted and written to the tagged field. -// -// `nvelope:"cookie,name=xxx"` cause the named HTTP cookie to be -// extracted and writted to the tagged field. -// -// Path, query, header, and cookie support options described -// in https://swagger.io/docs/specification/serialization/ for -// controlling how to serialize. The following are supported -// as appropriate. -// -// explode=true # default for query, header -// explode=false # default for path -// delimiter=comma # default -// delimiter=space # query parameters only -// delimiter=pipe # query parameters only -// allowReserved=false # default -// allowReserved=true # query parameters only -// form=false # default -// form=true # cookies only -// content=application/json # specifies that the value should be decoded with JSON -// content=application/xml # specifies that the value should be decoded with XML -// deepObject=false # default -// deepObject=true # required for query object -// -// "style=label" and "style=matrix" are NOT yet supported for path parameters. -// -// For query parameters filling maps and structs, the only the following -// combinations are supported: -// -// deepObject=true -// deepObject=false,explode=false -// -// When filling embedded structs from query, or header, parameters, -// using explode=false or deepObject=true, tagging struct members is -// optional. Tag them with their name or with "-" if you do not want -// them filled. -// -// type Fillme struct { -// Embedded struct { -// IntValue int // will get filled by key "IntValue" -// FloatValue float64 `nvelope:"-"` // will not get filled -// StringValue string `nvelope:"bob"` // will get filled by key "bob" -// } `nvelope:"query,name=embedded,explode=false"` -// } -// -// "deepObject=true" is only supported for maps and structs and only for query parameters. -// -// Generally setting "content" to something should be paired with "explode=false" -// -// GenerateDecoder depends upon and uses Gorilla mux. -// -// GenerateDecoder uses https://pkg.go.dev/github.com/muir/reflectutils#MakeStringSetter to -// unpack strings into struct fields. That provides support for time.Duration and anything -// that implements encoding.TextUnmarshaler or flag.Value. Additional custom decoders can -// be registered with https://pkg.go.dev/github.com/muir/reflectutils#RegisterStringSetter . -func GenerateDecoder( - genOpts ...DecodeInputsGeneratorOpt, -) interface{} { - options := eigo{ - tag: "nvelope", - decoders: make(map[string]Decoder), - } - for _, opt := range genOpts { - opt(&options) - } - return nject.GenerateFromInjectionChain(func(before nject.Collection, after nject.Collection) (nject.Provider, error) { - full := before.Append("after", after) - missingInputs, _ := full.DownFlows() - var providers []interface{} - for _, missingType := range missingInputs { - returnType := missingType - var nonPointer reflect.Type - var returnAddress bool - // nolint:exhaustive - switch missingType.Kind() { - case reflect.Struct: - nonPointer = returnType - case reflect.Ptr: - returnAddress = true - e := returnType.Elem() - if e.Kind() != reflect.Struct { - continue - } - nonPointer = e - default: - continue - } - var varsFillers []func(model reflect.Value, vars map[string]string) error - var headerFillers []func(model reflect.Value, header http.Header) error - var cookieFillers []func(model reflect.Value, r *http.Request) error - var bodyFillers []func(model reflect.Value, body []byte, r *http.Request) error - queryFillers := make(map[string]func(reflect.Value, []string) error) - deepObjectFillers := make(map[string]func(reflect.Value, map[string][]string) error) - var returnError error - reflectutils.WalkStructElements(nonPointer, func(field reflect.StructField) bool { - tag, ok := field.Tag.Lookup(options.tag) - if !ok { - return true - } - base, tags, err := parseTag(tag, true) - if err != nil { - returnError = err - return false - } - if base == "model" { - bodyFillers = append(bodyFillers, - func(model reflect.Value, body []byte, r *http.Request) error { - f := model.FieldByIndex(field.Index) - ct := r.Header.Get("Content-Type") - if ct == "" { - ct = options.defaultContentType - } - exactDecoder, ok := options.decoders[ct] - if !ok { - return errors.Errorf("No body decoder for content type %s", ct) - } - // nolint:govet - err := exactDecoder(body, f.Addr().Interface()) - return errors.Wrapf(err, "Could not decode %s into %s", ct, field.Type) - }) - return false - } - - name := field.Name // not used by model, but used by the rest - if tags.name != "" { - name = tags.name - } - unpacker, err := getUnpacker(field.Type, field.Name, name, base, tags, options) - if err != nil { - returnError = err - return false - } - switch base { - case "path": - varsFillers = append(varsFillers, func(model reflect.Value, vars map[string]string) error { - f := model.FieldByIndex(field.Index) - return errors.Wrapf( - unpacker.single("path", f, vars[name]), - "path element %s into field %s", - name, field.Name) - }) - case "header": - if unpacker.multi != nil { - headerFillers = append(headerFillers, func(model reflect.Value, header http.Header) error { - f := model.FieldByIndex(field.Index) - values, ok := header[name] - if !ok { - return nil - } - return errors.Wrapf( - unpacker.multi("header", f, values), - "header %s into field %s", - name, field.Name) - }) - } else { - headerFillers = append(headerFillers, func(model reflect.Value, header http.Header) error { - f := model.FieldByIndex(field.Index) - values, ok := header[name] - if !ok || len(values) == 0 { - return nil - } - return errors.Wrapf( - unpacker.single("header", f, values[0]), - "header %s into field %s", - name, field.Name) - }) - } - case "query": - switch { - case unpacker.deepObject != nil: - deepObjectFillers[name] = func(model reflect.Value, mapValues map[string][]string) error { - f := model.FieldByIndex(field.Index) - return unpacker.deepObject(f, mapValues) - } - case unpacker.multi != nil: - queryFillers[name] = func(model reflect.Value, values []string) error { - f := model.FieldByIndex(field.Index) - return errors.Wrapf( - unpacker.multi("query", f, values), - "query parameter %s into field %s", - name, field.Name) - } - default: - queryFillers[name] = func(model reflect.Value, values []string) error { - if len(values) == 0 { - return nil - } - f := model.FieldByIndex(field.Index) - return errors.Wrapf( - unpacker.single("query", f, values[0]), - "query parameter %s into field %s", - name, field.Name) - } - } - case "cookie": - cookieFillers = append(cookieFillers, func(model reflect.Value, r *http.Request) error { - f := model.FieldByIndex(field.Index) - cookie, err := r.Cookie(name) - if err != nil { - if errors.Is(err, http.ErrNoCookie) { - return nil - } - return errors.Wrapf(err, "cookie parameter %s into field %s", name, field.Name) - } - return errors.Wrapf( - unpacker.single("cookie", f, cookie.Value), - "cookie parameter %s into field %s", - name, field.Name) - }) - } - return true - }) - if returnError != nil { - return nil, returnError - } - - if len(varsFillers) == 0 && - len(headerFillers) == 0 && - len(cookieFillers) == 0 && - len(queryFillers) == 0 && - len(bodyFillers) == 0 && - len(deepObjectFillers) == 0 { - continue - } - - inputs := []reflect.Type{httpRequestType} - if len(bodyFillers) != 0 { - inputs = append(inputs, bodyType) - } - outputs := []reflect.Type{returnType, terminalErrorType} - - reflective := nject.MakeReflective(inputs, outputs, func(in []reflect.Value) []reflect.Value { - // nolint:errcheck - r := in[0].Interface().(*http.Request) - mp := reflect.New(nonPointer) - model := mp.Elem() - var err error - setError := func(e error) { - if err == nil && e != nil { - err = e - } - } - if len(bodyFillers) != 0 { - body := []byte(in[1].Interface().(Body)) - for _, bf := range bodyFillers { - setError(bf(model, body, r)) - } - } - if len(varsFillers) != 0 { - vars := mux.Vars(r) - for _, vf := range varsFillers { - setError(vf(model, vars)) - } - } - for _, hf := range headerFillers { - setError(hf(model, r.Header)) - } - var deepObjects map[string]map[string][]string - for key, vals := range r.URL.Query() { - if qf, ok := queryFillers[key]; ok { - setError(qf(model, vals)) - continue - } - if len(deepObjectFillers) != 0 { - if m := deepObjectRE.FindStringSubmatch(key); len(m) == 3 { - if _, ok := deepObjectFillers[m[1]]; ok { - if deepObjects == nil { - deepObjects = make(map[string]map[string][]string) - } - if deepObjects[m[1]] == nil { - deepObjects[m[1]] = make(map[string][]string) - } - deepObjects[m[1]][m[2]] = vals - continue - } - } - } - if options.rejectUnknownQueryParameters { - setError(errors.Errorf("query parameter '%s' not supported", key)) - } - } - for dofKey, values := range deepObjects { - setError(deepObjectFillers[dofKey](model, values)) - } - for _, cf := range cookieFillers { - setError(cf(model, r)) - } - var ev reflect.Value - if err == nil { - ev = reflect.Zero(errorType) - } else { - ev = reflect.ValueOf(errors.Wrapf(ReturnCode(err, 400), "%s model", returnType)) - } - if returnAddress { - return []reflect.Value{mp, ev} - } - return []reflect.Value{model, ev} - }) - providers = append(providers, nject.Provide("create "+nonPointer.String(), reflective)) - } - return nject.Sequence("fill functions from request", providers...), nil - }) -} - -// generateStructUnpacker generates a function to deal with filling a struct from -// an array of key, value pairs. -func generateStructUnpacker( - base string, - fieldType reflect.Type, - tagName string, - outerTags tags, - options eigo, -) (unpack, error) { - type fillTarget struct { - field reflect.StructField - unpack - } - targets := make(map[string]fillTarget) - var anyErr error - reflectutils.WalkStructElements(fieldType, func(field reflect.StructField) bool { - tag, _ := field.Tag.Lookup(tagName) - // nolint:govet - name, tags, err := parseTag(tag, false) - if err != nil { - anyErr = errors.Wrap(err, field.Name) - return false - } - switch name { - case "-": - return true - case "": - name = field.Name - } - if _, ok := targets[name]; ok { - anyErr = errors.Errorf("Only one field can be filled with the same name. '%s' is duplicated. One example is %s", - name, field.Name) - return false - } - if !outerTags.deepObject { - tags.explode = false - } - if tags.deepObject { - anyErr = errors.Errorf("deepObject=true is not allowed on fields inside a struct. Used on %s", name) - return false - } - unpacker, err := getUnpacker(field.Type, field.Name, name, base, tags, options) - if err != nil { - anyErr = errors.Wrap(err, field.Name) - return false - } - targets[name] = fillTarget{ - field: field, - unpack: unpacker, - } - return true - }) - if anyErr != nil { - return unpack{}, anyErr - } - return unpack{ - multi: func(from string, model reflect.Value, values []string) error { - for i := 0; i < len(values); i += 2 { - keyString := values[i] - var valueString string - if i+1 < len(values) { - valueString = values[i+1] - } - target, ok := targets[keyString] - if !ok { - if options.rejectUnknownQueryParameters { - return errors.Errorf("No struct member to receive key '%s'", keyString) - } - continue - } - f := model.FieldByIndex(target.field.Index) - err := target.single(from, f, valueString) - if err != nil { - return errors.Wrap(err, target.field.Name) - } - } - return nil - }, - deepObject: func(model reflect.Value, mapValues map[string][]string) error { - for keyString, values := range mapValues { - target, ok := targets[keyString] - if !ok { - if options.rejectUnknownQueryParameters { - return errors.Errorf("No struct member to receive key '%s'", keyString) - } - continue - } - f := model.FieldByIndex(target.field.Index) - var err error - if target.single != nil { - if len(values) > 0 { - err = target.single("query", f, values[0]) - } - } else { - err = target.multi("query", f, values) - } - if err != nil { - return errors.Wrap(err, target.field.Name) - } - } - return nil - }, - }, nil -} - -func mapUnpack( - from string, f reflect.Value, - keyUnpack func(from string, target reflect.Value, value string) error, - valueUnpack func(from string, target reflect.Value, value string) error, - values []string, -) error { - m := reflect.MakeMap(f.Type()) - for i := 0; i < len(values); i += 2 { - keyString := values[i] - var valueString string - if i+1 < len(values) { - valueString = values[i+1] - } - keyPointer := reflect.New(f.Type().Key()) - err := keyUnpack(from, keyPointer.Elem(), keyString) - if err != nil { - return err - } - valuePointer := reflect.New(f.Type().Elem()) - err = valueUnpack(from, valuePointer.Elem(), valueString) - if err != nil { - return err - } - m.SetMapIndex(reflect.Indirect(keyPointer), reflect.Indirect(valuePointer)) - } - f.Set(m) - return nil -} - -func sliceUnpack( - from string, f reflect.Value, - singleUnpack func(from string, target reflect.Value, value string) error, - values []string, -) error { - a := reflect.MakeSlice(f.Type(), len(values), len(values)) - for i, value := range values { - err := singleUnpack(from, a.Index(i), value) - if err != nil { - return err - } - } - f.Set(a) - return nil -} - -func arrayUnpack( - from string, f reflect.Value, - singleUnpack func(from string, target reflect.Value, value string) error, - values []string, -) error { - arrayLen := f.Len() - if len(values) > arrayLen { - return errors.New("too many values for fixed length array") - } - for i, value := range values { - err := singleUnpack(from, f.Index(i), value) - if err != nil { - return err - } - } - for k := len(values); k < arrayLen; k++ { - f.Index(k).Set(reflect.Zero(f.Index(0).Type())) - } - return nil -} - -type unpack struct { - createMe bool - single func(from string, target reflect.Value, value string) error - multi func(from string, target reflect.Value, values []string) error - deepObject func(target reflect.Value, mapValues map[string][]string) error -} - -// getUnpacker is used for unpacking headers, query parameters, and path elements -func getUnpacker( - fieldType reflect.Type, - fieldName string, - name string, - base string, // "path", "query", etc. - tags tags, - options eigo, -) (unpack, error) { - if tags.content != "" { - return contentUnpacker(fieldType, fieldName, name, base, tags, options) - } - if fieldType.AssignableTo(textUnmarshallerType) { - return unpack{ - createMe: true, - single: func(from string, target reflect.Value, value string) error { - p := reflect.New(fieldType.Elem()) - target.Set(p) - return errors.Wrapf( - target.Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)), - "decode %s %s", from, name) - }, - }, nil - } - if reflect.PtrTo(fieldType).AssignableTo(textUnmarshallerType) { - return unpack{ - createMe: true, - single: func(from string, target reflect.Value, value string) error { - return errors.Wrapf( - target.Addr().Interface().(encoding.TextUnmarshaler).UnmarshalText([]byte(value)), - "decode %s %s", from, name) - }, - }, nil - } - - switch fieldType.Kind() { - case reflect.Ptr: - unpacker, err := getUnpacker(fieldType.Elem(), fieldName, name, base, tags, options) - if err != nil { - return unpack{}, err - } - switch { - case unpacker.deepObject != nil: - return unpack{deepObject: func(target reflect.Value, mapValues map[string][]string) error { - p := reflect.New(fieldType.Elem()) - target.Set(p) - return unpacker.deepObject(target.Elem(), mapValues) - }}, nil - case unpacker.multi != nil: - return unpack{multi: func(from string, target reflect.Value, values []string) error { - p := reflect.New(fieldType.Elem()) - target.Set(p) - return unpacker.multi(from, target.Elem(), values) - }}, nil - default: - return unpack{single: func(from string, target reflect.Value, value string) error { - p := reflect.New(fieldType.Elem()) - target.Set(p) - return unpacker.single(from, target.Elem(), value) - }}, nil - } - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uintptr, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, - reflect.Float32, reflect.Float64, - reflect.String, - reflect.Complex64, reflect.Complex128, - reflect.Bool: - f, err := reflectutils.MakeStringSetter(fieldType) - if err != nil { - return unpack{}, errors.Wrapf(err, "Cannot decode into %s, %s", fieldName, fieldType) - } - return unpack{single: func(from string, target reflect.Value, value string) error { - return errors.Wrapf(f(target, value), "decode %s %s", from, name) - }}, nil - - case reflect.Slice, reflect.Array: - switch base { - case "cookie", "path": - if tags.delimiter != "," { - return unpack{}, errors.New("delimiter setting is only allowed for 'query' parameters") - } - if tags.explode { - return unpack{}, errors.New("explode=true not supported for cookies & path parameters") - } - } - if tags.deepObject { - return unpack{}, errors.New("deepObject=true not supported for slices") - } - - singleUnpack, err := getUnpacker(fieldType.Elem(), fieldName, name, base, tags.WithoutExplode(), options) - if err != nil { - return unpack{}, err - } - unslicer := sliceUnpack - if fieldType.Kind() == reflect.Array { - unslicer = arrayUnpack - } - switch base { - case "query", "header": - if tags.explode { - return unpack{ - multi: func(from string, target reflect.Value, values []string) error { - return unslicer(from, target, singleUnpack.single, values) - }, - }, nil - } - } - return unpack{single: func(from string, target reflect.Value, value string) error { - values := strings.Split(value, tags.delimiter) - return unslicer(from, target, singleUnpack.single, values) - }}, nil - - case reflect.Struct: - structUnpacker, err := generateStructUnpacker(base, fieldType, options.tag, tags, options) - if err != nil { - return unpack{}, err - } - if tags.deepObject { - if base != "query" { - return unpack{}, errors.Errorf("deepObject=true not supported for %s", base) - } - return unpack{deepObject: structUnpacker.deepObject}, nil - } - switch base { - case "query", "header": - if tags.explode { - return unpack{ - multi: func(from string, target reflect.Value, values []string) error { - return structUnpacker.multi(from, target, resplitOnEquals(values)) - }, - }, nil - } - } - return unpack{single: func(from string, target reflect.Value, value string) error { - values := strings.Split(value, tags.delimiter) - return structUnpacker.multi(from, target, values) - }}, nil - - case reflect.Map: - switch base { - case "cookie", "path": - if tags.delimiter != "," { - return unpack{}, errors.New("delimiter setting is only allowed for 'query' parameters") - } - } - keyUnpack, err := getUnpacker(fieldType.Key(), fieldName, name, base, tags.WithoutExplode().WithoutDeepObject(), options) - if err != nil { - return unpack{}, err - } - etags := tags - if tags.deepObject { - etags = etags.WithoutDeepObject() - } else { - etags = etags.WithoutExplode() - } - elementUnpack, err := getUnpacker(fieldType.Elem(), fieldName, name, base, etags, options) - if err != nil { - return unpack{}, err - } - if tags.deepObject { - if base != "query" { - return unpack{}, errors.Errorf("deepObject=true not supported for %s", base) - } - return unpack{deepObject: func(target reflect.Value, mapValues map[string][]string) error { - m := reflect.MakeMap(fieldType) - for keyString, values := range mapValues { - keyPointer := reflect.New(fieldType.Key()) - err := keyUnpack.single("query", keyPointer.Elem(), keyString) - if err != nil { - return err - } - valuePointer := reflect.New(fieldType.Elem()) - if elementUnpack.multi != nil { - err = elementUnpack.multi("query", valuePointer.Elem(), values) - } else { - var valueString string - if len(values) > 0 { - valueString = values[0] - } - err = elementUnpack.single("query", valuePointer.Elem(), valueString) - } - if err != nil { - return err - } - m.SetMapIndex(reflect.Indirect(keyPointer), reflect.Indirect(valuePointer)) - } - target.Set(m) - return nil - }}, nil - } - switch base { - case "query", "header": - if tags.explode { - return unpack{ - multi: func(from string, target reflect.Value, values []string) error { - return mapUnpack(from, target, keyUnpack.single, elementUnpack.single, resplitOnEquals(values)) - }, - }, nil - } - } - return unpack{single: func(from string, target reflect.Value, value string) error { - values := strings.Split(value, tags.delimiter) - return mapUnpack(from, target, keyUnpack.single, elementUnpack.single, values) - }}, nil - - case reflect.Chan, reflect.Interface, reflect.UnsafePointer, reflect.Func, reflect.Invalid: - fallthrough - default: - return unpack{}, errors.Errorf( - "Cannot decode into %s, %s does not implement UnmarshalText", - fieldName, fieldType) - } -} - -// contentUnpacker generates an unpacker to use when something has -// been tagged "content=application/json" or such. We bypass our -// regular unpackers and instead use a regular decoder. The interesting -// case is where this is combined with "explode=true" because then -// we have to decode many times -func contentUnpacker( - fieldType reflect.Type, - fieldName string, - name string, - base string, // "path", "query", etc. - tags tags, - options eigo, -) (unpack, error) { - decoder, ok := options.decoders[tags.content] - if !ok { - // tags.content can provide access to decoders beyond what - // is specified for GenerateDecoder - switch tags.content { - case "application/json": - decoder = json.Unmarshal - case "application/xml": - decoder = xml.Unmarshal - case "application/yaml": - decoder = yaml.Unmarshal - default: - return unpack{}, errors.Errorf("No decoder provided for content type '%s'", tags.content) - } - } - kind := fieldType.Kind() - if tags.explode && - (base == "query" || base == "header") && - (kind == reflect.Map || kind == reflect.Slice) { - valueUnpack, err := getUnpacker(fieldType.Elem(), fieldName, name, base, tags.WithoutExplode(), options) - if err != nil { - return unpack{}, err - } - if kind == reflect.Slice { - return unpack{multi: func(from string, target reflect.Value, values []string) error { - a := reflect.MakeSlice(target.Type(), len(values), len(values)) - for i, valueString := range values { - // nolint:govet - err := valueUnpack.single(from, a.Index(i), valueString) - if err != nil { - return err - } - } - target.Set(a) - return nil - }}, nil - } - keyUnpack, err := getUnpacker(fieldType.Key(), fieldName, name, base, tags.WithoutExplode().WithoutContent().WithoutDeepObject(), options) - if err != nil { - return unpack{}, err - } - return unpack{multi: func(from string, target reflect.Value, values []string) error { - m := reflect.MakeMap(target.Type()) - for _, pair := range values { - kv := strings.SplitN(pair, "=", 2) - keyString := kv[0] - var valueString string - if len(kv) == 2 { - valueString = kv[1] - } - keyPointer := reflect.New(fieldType.Key()) - err := keyUnpack.single(from, keyPointer, keyString) - if err != nil { - return err - } - valuePointer := reflect.New(fieldType.Elem()) - err = valueUnpack.single(from, valuePointer, valueString) - if err != nil { - return err - } - m.SetMapIndex(reflect.Indirect(keyPointer), reflect.Indirect(valuePointer)) - } - target.Set(m) - return nil - }}, nil - } - - return unpack{single: func(from string, target reflect.Value, value string) error { - i := target.Addr().Interface() - err := decoder([]byte(value), i) - return errors.Wrap(err, fieldName) - }}, nil -} - -var ( - httpRequestType = reflect.TypeOf(&http.Request{}) - bodyType = reflect.TypeOf(Body{}) - textUnmarshallerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() - terminalErrorType = reflect.TypeOf((*nject.TerminalError)(nil)).Elem() - errorType = reflect.TypeOf((*error)(nil)).Elem() -) - -var delimiters = map[string]string{ - "comma": ",", - "pipe": "|", - "space": " ", -} - -type tags struct { - name string - explode bool - delimiter string - allowReserved bool - content string - deepObject bool -} - -func (tags tags) WithoutExplode() tags { tags.explode = false; return tags } -func (tags tags) WithoutContent() tags { tags.content = ""; return tags } -func (tags tags) WithoutDeepObject() tags { tags.deepObject = false; return tags } - -func parseTag(s string, validate bool) (string, tags, error) { - a := strings.Split(s, ",") - // nolint:govet - var tags tags - if len(a) == 0 { - return "", tags, errors.New("must specify the source of the data ('path', 'query', etc)") - } - tags.delimiter = "," - if validate { - switch a[0] { - case "path": - case "query": - tags.explode = true - case "header": - tags.explode = true - case "cookie": - case "model": - default: - return "", tags, errors.Errorf("'%s' is not a valid source of the data use ('model', 'path', 'query', etc)", a[0]) - } - } - for _, v := range a[1:] { - kvs := strings.SplitN(v, "=", 2) - k := kvs[0] - var val string - if len(kvs) == 2 { - val = kvs[1] - } - var err error - switch k { - case "name": - tags.name = val - case "explode": - tags.explode, err = strconv.ParseBool(val) - case "delimiter": - var ok bool - tags.delimiter, ok = delimiters[val] - if !ok { - err = errors.Errorf("Invalid delimiter value (must be 'comma', 'space', or 'pipe')") - } - case "allowReserved": - tags.allowReserved, err = strconv.ParseBool(val) - case "content": - tags.content = val - case "deepObject": - tags.deepObject = true - default: - return "", tags, errors.Errorf("tag %s is not supported", k) - } - if err != nil { - return "", tags, errors.Wrap(err, k) - } - } - return a[0], tags, nil -} - -func resplitOnEquals(values []string) []string { - nv := make([]string, len(values)*2) - for i, v := range values { - a := strings.SplitN(v, "=", 2) - nv[i*2] = a[0] - if len(a) == 2 { - nv[i*2+1] = a[1] - } - } - return nv -} diff --git a/nvelope/decode_test.go b/nvelope/decode_test.go deleted file mode 100644 index 6b64c3d..0000000 --- a/nvelope/decode_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package nvelope_test - -import ( - "fmt" - "net/url" - "testing" - - "github.com/muir/nject/nvelope" - - "github.com/stretchr/testify/assert" -) - -type Complex128 complex128 - -func (c Complex128) MarshalText() ([]byte, error) { - return []byte(fmt.Sprint(c)), nil -} - -type Complex64 complex64 - -func (c Complex64) MarshalText() ([]byte, error) { - return []byte(fmt.Sprint(c)), nil -} - -func TestDecodeQuerySimpleParameters(t *testing.T) { - do := captureOutput("/x", func(s struct { - Int int `json:",omitempty" nvelope:"query,name=int"` - Int8 int8 `json:",omitempty" nvelope:"query,name=int8"` - Int16 int16 `json:",omitempty" nvelope:"query,name=int16"` - Int32 int32 `json:",omitempty" nvelope:"query,name=int32"` - Int64 int64 `json:",omitempty" nvelope:"query,name=int64"` - Uint uint `json:",omitempty" nvelope:"query,name=uint"` - Uint8 uint8 `json:",omitempty" nvelope:"query,name=uint8"` - Uint16 uint16 `json:",omitempty" nvelope:"query,name=uint16"` - Uint32 uint32 `json:",omitempty" nvelope:"query,name=uint32"` - Uint64 uint64 `json:",omitempty" nvelope:"query,name=uint64"` - Float32 float32 `json:",omitempty" nvelope:"query,name=float32"` - Float64 float64 `json:",omitempty" nvelope:"query,name=float64"` - String string `json:",omitempty" nvelope:"query,name=string"` - IntP *int `json:",omitempty" nvelope:"query,name=intp"` - Int8P *int8 `json:",omitempty" nvelope:"query,name=int8p"` - Int16P *int16 `json:",omitempty" nvelope:"query,name=int16p"` - Int32P *int32 `json:",omitempty" nvelope:"query,name=int32p"` - Int64P *int64 `json:",omitempty" nvelope:"query,name=int64p"` - UintP *uint `json:",omitempty" nvelope:"query,name=uintp"` - Uint8P *uint8 `json:",omitempty" nvelope:"query,name=uint8p"` - Uint16P *uint16 `json:",omitempty" nvelope:"query,name=uint16p"` - Uint32P *uint32 `json:",omitempty" nvelope:"query,name=uint32p"` - Uint64P *uint64 `json:",omitempty" nvelope:"query,name=uint64p"` - Float32P *float32 `json:",omitempty" nvelope:"query,name=float32p"` - Float64P *float64 `json:",omitempty" nvelope:"query,name=float64p"` - StringP *string `json:",omitempty" nvelope:"query,name=stringp"` - Complex64 *Complex64 `json:",omitempty" nvelope:"query,name=complex64"` - Complex128 *Complex128 `json:",omitempty" nvelope:"query,name=complex128"` - BoolP *bool `json:",omitempty" nvelope:"query,name=boolp"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"Int":135}`, do("/x?int=135")) - assert.Equal(t, `200->{"Int8":-5}`, do("/x?int8=-5")) - assert.Equal(t, `200->{"Int16":127}`, do("/x?int16=127")) - assert.Equal(t, `200->{"Int32":11}`, do("/x?int32=11")) - assert.Equal(t, `200->{"Int64":-38}`, do("/x?int64=-38")) - assert.Equal(t, `200->{"Uint":135}`, do("/x?uint=135")) - assert.Equal(t, `200->{"Uint8":5}`, do("/x?uint8=5")) - assert.Equal(t, `200->{"Uint16":127}`, do("/x?uint16=127")) - assert.Equal(t, `200->{"Uint32":11}`, do("/x?uint32=11")) - assert.Equal(t, `200->{"Uint64":38}`, do("/x?uint64=38")) - assert.Equal(t, `200->{"Float64":38.7}`, do("/x?float64=38.7")) - assert.Equal(t, `200->{"Float32":11.1}`, do("/x?float32=11.1")) - assert.Equal(t, `200->{"String":"fred"}`, do("/x?string=fred")) - assert.Equal(t, `200->{"IntP":135}`, do("/x?intp=135")) - assert.Equal(t, `200->{"Int8P":-5}`, do("/x?int8p=-5")) - assert.Equal(t, `200->{"Int16P":127}`, do("/x?int16p=127")) - assert.Equal(t, `200->{"Int32P":11}`, do("/x?int32p=11")) - assert.Equal(t, `200->{"Int64P":-38}`, do("/x?int64p=-38")) - assert.Equal(t, `200->{"UintP":135}`, do("/x?uintp=135")) - assert.Equal(t, `200->{"Uint8P":5}`, do("/x?uint8p=5")) - assert.Equal(t, `200->{"Uint16P":127}`, do("/x?uint16p=127")) - assert.Equal(t, `200->{"Uint32P":11}`, do("/x?uint32p=11")) - assert.Equal(t, `200->{"Uint64P":38}`, do("/x?uint64p=38")) - assert.Equal(t, `200->{"Float64P":38.7}`, do("/x?float64p=38.7")) - assert.Equal(t, `200->{"Float32P":11.1}`, do("/x?float32p=11.1")) - assert.Equal(t, `200->{"StringP":"fred"}`, do("/x?stringp=fred")) - assert.Equal(t, `200->{"Complex64":"(38.7-9.3i)"}`, do("/x?complex64="+url.QueryEscape("38.7-9.3i"))) - assert.Equal(t, `200->{"Complex128":"(11.1+22.1i)"}`, do("/x?complex128="+url.QueryEscape("11.1+22.1i"))) - assert.Equal(t, `200->{"BoolP":false}`, do("/x?boolp=false")) -} - -func TestDecodeQueryComplexParameters(t *testing.T) { - do := captureOutput("/x", func(s struct { - IntSlice []int `json:",omitempty" nvelope:"query,name=intslice,explode=false"` - Int8Slice []*int8 `json:",omitempty" nvelope:"query,name=int8slice,explode=true"` - Int16Slice []*int8 `json:",omitempty" nvelope:"query,name=int16slice,explode=false,delimiter=space"` - Int32Slice *[]*int8 `json:",omitempty" nvelope:"query,name=int32slice,explode=false,delimiter=pipe"` - MapIntBool map[int]bool `json:",omitempty" nvelope:"query,name=mapintbool,explode=false"` - MapIntString map[int]string `json:",omitempty" nvelope:"query,name=mapintstring,deepObject=true"` - IntArrayP *[3]int `json:",omitempty" nvelope:"query,name=intarrayp,explode=false"` - Emb1 *struct { - Int int `json:",omitempty" nvelope:"eint"` - Int8 int8 `json:",omitempty" nvelope:"eint8"` - Int16 int16 `json:",omitempty" nvelope:"eint16"` - String string `json:",omitempty"` - } `json:",omitempty" nvelope:"query,name=emb1,explode=false"` - Emb2 *struct { - Int int `json:",omitempty" nvelope:"eint"` - Int8 int8 `json:",omitempty" nvelope:"eint8"` - Int16 int16 `json:",omitempty" nvelope:"eint16"` - String string `json:",omitempty"` - } `json:",omitempty" nvelope:"query,name=emb2,deepObject=true"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"IntSlice":[1,7]}`, do("/x?intslice=1,7")) - assert.Equal(t, `200->{"Int8Slice":[10,11,12]}`, do("/x?int8slice=10&int8slice=11&int8slice=12")) - assert.Equal(t, `200->{"Int16Slice":[8,22,-3]}`, do("/x?int16slice=8%2022%20-3")) - assert.Equal(t, `200->{"Int32Slice":[7,11,13]}`, do("/x?int32slice=7|11|13")) - assert.Equal(t, `200->{"MapIntBool":{"-9":false,"7":true}}`, do("/x?mapintbool=7,true,-9,false")) - assert.Equal(t, `200->{"MapIntString":{"-9":"hi","7":"bye"}}`, do("/x?mapintstring[7]=bye&mapintstring[-9]=hi")) - assert.Equal(t, `200->{"Emb1":{"Int":192,"Int8":-3,"String":"foo"}}`, do("/x?emb1=eint,192,eint8,-3,String,foo")) - assert.Equal(t, `200->{"Emb2":{"Int":193,"Int8":-4,"String":"bar"}}`, do("/x?emb2[eint]=193&emb2[eint8]=-4&emb2[String]=bar")) - assert.Equal(t, `200->{"IntArrayP":[7,22,0]}`, do("/x?intarrayp=7,22")) -} - -type Foo string - -func (fp *Foo) UnmarshalText(b []byte) error { - *fp = Foo("~" + string(b) + "~") - return nil -} - -func TestDecodeQueryJSONParameters(t *testing.T) { - do := captureOutput("/x", func(s struct { - Foo Foo `json:",omitempty" nvelope:"query,name=foo,explode=false"` - FooP *Foo `json:",omitempty" nvelope:"query,name=foop,explode=false"` - FooA []Foo `json:",omitempty" nvelope:"query,name=fooa,explode=true"` - FooB *[]*Foo `json:",omitempty" nvelope:"query,name=foob,explode=false"` - S1 string `json:",omitempty" nvelope:"query,name=s1,content=application/json"` - S2 *string `json:",omitempty" nvelope:"query,name=s2,content=application/json"` - S3 **string `json:",omitempty" nvelope:"query,name=s3,content=application/json"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"Foo":"~bar~"}`, do("/x?foo=bar")) - assert.Equal(t, `200->{"FooP":"~baz~"}`, do("/x?foop=baz")) - assert.Equal(t, `200->{"FooA":["~bar~","~baz~"]}`, do("/x?fooa=bar&fooa=baz")) - assert.Equal(t, `200->{"FooB":["~bing~","~baz~"]}`, do("/x?foob=bing,baz")) - assert.Equal(t, `200->{"S1":"doof"}`, do(`/x?s1="doof"`)) - assert.Equal(t, `200->{"S2":"boor"}`, do(`/x?s2="boor"`)) - assert.Equal(t, `200->{"S3":"ppp"}`, do(`/x?s3="ppp"`)) -} - -func TestDecodeQueryHeaderParameters(t *testing.T) { - do := captureOutput("/x", func(s struct { - S string `json:",omitempty" nvelope:"header,name=S"` - A1 []string `json:",omitempty" nvelope:"header,name=A1"` - A2 []string `json:",omitempty" nvelope:"header,name=A2"` - A3 []string `json:",omitempty" nvelope:"header,explode=false,name=A3"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"S":"yip"}`, do("/x", header("S", "yip"))) - assert.Equal(t, `200->{"A1":["eee"]}`, do("/x", header("A1", "eee"))) - assert.Equal(t, `200->{"A2":["yia","yo"]}`, do("/x", header("A2", "yia"), header("A2", "yo"))) - assert.Equal(t, `200->{"A3":["cow","boy"]}`, do("/x", header("A3", "cow,boy"))) -} - -func TestDecodeQueryCookieParameters(t *testing.T) { - do := captureOutput("/x", func(s struct { - S string `json:",omitempty" nvelope:"cookie,name=S"` - A1 []string `json:",omitempty" nvelope:"cookie,name=A1"` - A3 []string `json:",omitempty" nvelope:"cookie,explode=false,name=A3"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"S":"yip"}`, do("/x", cookie("S", "yip"))) - assert.Equal(t, `200->{"A1":["eee"]}`, do("/x", cookie("A1", "eee"))) - assert.Equal(t, `200->{"A3":["cow","boy"]}`, do("/x", cookie("A3", "cow,boy"))) -} - -func TestDecodeQueryPathParameters(t *testing.T) { - do := captureOutput("/x/{a}/{b}/{c}", func(s struct { - A string `json:",omitempty" nvelope:"path,name=a"` - B *int `json:",omitempty" nvelope:"path,name=b"` - C Foo `json:",omitempty" nvelope:"path,name=c"` - }) (nvelope.Response, error) { - return s, nil - }) - assert.Equal(t, `200->{"A":"foobar","B":38,"C":"~john~"}`, do("/x/foobar/38/john")) -} diff --git a/nvelope/deferred.go b/nvelope/deferred.go deleted file mode 100644 index 1d17dae..0000000 --- a/nvelope/deferred.go +++ /dev/null @@ -1,146 +0,0 @@ -package nvelope - -import ( - "io" - "net/http" - - "github.com/pkg/errors" -) - -// DeferredWriter that wraps an underlying http.ResponseWriter. -// DeferredWriter buffers writes and headers. The buffer can be -// reset. When it's time to actually write, use Flush(). -type DeferredWriter struct { - base http.ResponseWriter - passthrough bool - header http.Header - buffer []byte - status int - resetHeader http.Header -} - -// NewDeferredWriter returns a DeferredWriter based on a -// base ResponseWriter -func NewDeferredWriter(w http.ResponseWriter) *DeferredWriter { - return &DeferredWriter{ - base: w, - header: w.Header().Clone(), - resetHeader: w.Header().Clone(), - buffer: make([]byte, 0, 4*1024), - } -} - -// Header is the same as http.ResponseWriter.Header -func (w *DeferredWriter) Header() http.Header { - if w.passthrough { - return w.base.Header() - } - return w.header -} - -// Write is the same as http.ResponseWriter.Write -// except that the action is delayed until Flush() is called. -func (w *DeferredWriter) Write(b []byte) (int, error) { - if w.passthrough { - return w.base.Write(b) - } - w.buffer = append(w.buffer, b...) - return len(b), nil -} - -// WriteHeader is the same as http.ResponseWriter.WriteHeader -// except that the action is delayed until Flush() is called. -func (w *DeferredWriter) WriteHeader(statusCode int) { - if w.passthrough { - w.base.WriteHeader(statusCode) - } else { - w.status = statusCode - } -} - -// Reset empties the DeferredWriter's buffers and resets its Header -// back to its original state. Reset returns error if UnderlyingWriter() -// or Flush() have been called. -func (w *DeferredWriter) Reset() error { - if w.passthrough { - return errors.New("Attempt to reset a DeferredWriter after it is in passthrough mode") - } - w.buffer = nil - w.status = 0 - w.header = w.resetHeader.Clone() - return nil -} - -// PreserveHeader saves the current Header so that a Reset will revert -// back to the header just saved. -func (w *DeferredWriter) PreserveHeader() { - w.resetHeader = w.header.Clone() -} - -// UnderlyingWriter returns the underlying writer. Any header -// modifications made with the DeferredWriter are copied to the -// base writer. After a call to UnderlyingWriter, the DeferredWriter -// switches to passthrough mode: all future calls to Write(), -// Header(), etc are passed through to the http.ResponseWriter that -// was used to initialize the DeferredWrited. -func (w *DeferredWriter) UnderlyingWriter() http.ResponseWriter { - w.passthrough = true - h := w.base.Header() - for k := range h { - if v, ok := w.header[k]; ok { - h[k] = v - } else { - delete(h, k) - } - } - for k, v := range w.header { - if _, ok := h[k]; ok { - continue - } - h[k] = v - } - return w.base -} - -// Flush pushes the buffered write content through to the base writer. -// You can only flush once. After a flush, all further calls are passed -// through to be base writer. WriteHeader() will be called on the base -// writer even if there is no buffered data. -func (w *DeferredWriter) Flush() error { - if w.passthrough { - return errors.New("Attempt flush deferred writer that is not deferred") - } - base := w.UnderlyingWriter() - if w.status != 0 { - base.WriteHeader(w.status) - } - for i := 0; i < len(w.buffer)-1; { - amt, err := base.Write(w.buffer[i:]) - if err != nil { - // Is this handling of short writes necessary? Perhaps - // so since a follow-up write will probably give a - // more accurate error. - if errors.Is(err, io.ErrShortWrite) { - i += amt - continue - } - return errors.Wrap(err, "flush buffered writer") - } - break - } - return nil -} - -// FlushIfNotFlushed calls Flush if the DeferredWriter is not in -// passthrough mode. -func (w *DeferredWriter) FlushIfNotFlushed() error { - if !w.passthrough { - return w.Flush() - } - return nil -} - -// Done returns true if the DeferredWriter is in passthrough mode. -func (w *DeferredWriter) Done() bool { - return w.passthrough -} diff --git a/nvelope/deferred_test.go b/nvelope/deferred_test.go deleted file mode 100644 index edf6727..0000000 --- a/nvelope/deferred_test.go +++ /dev/null @@ -1,142 +0,0 @@ -package nvelope_test - -import ( - "fmt" - "io" - "net/http" - "testing" - - "github.com/muir/nject/nvelope" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type testResponseWriter struct { - header http.Header - simulateWriteError error - buffer []byte - code int -} - -var _ http.ResponseWriter = &testResponseWriter{} - -func (w *testResponseWriter) Header() http.Header { return w.header } -func (w *testResponseWriter) WriteHeader(code int) { w.code = code } -func (w *testResponseWriter) Write(b []byte) (int, error) { - if w.simulateWriteError != nil { - // nolint:errorlint - if w.simulateWriteError == io.ErrShortWrite { - if len(b) == 0 { - return 0, nil - } - w.buffer = append(w.buffer, b[0]) - w.simulateWriteError = nil - return 1, io.ErrShortWrite - } - return 0, w.simulateWriteError - } - w.buffer = append(w.buffer, b...) - return len(b), nil -} - -func TestUnderlyingWriter(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - w := nvelope.NewDeferredWriter(tw) - assert.Equal(t, tw, w.UnderlyingWriter()) -} - -func TestFlush(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - tw.Header().Set("a", "b") - w := nvelope.NewDeferredWriter(tw) - w.Write([]byte("howdy")) - assert.Empty(t, tw.buffer, "no write before flush") - assert.Equal(t, "b", w.Header().Get("a"), "original header still there") - w.Header().Set("c", "d") - assert.Equal(t, "", tw.Header().Get("c"), "original header untouched with new key") - w.Header().Set("a", "d") - assert.Equal(t, "", tw.Header().Get("c"), "original header untouched with existing key") - assert.Equal(t, "d", w.Header().Get("c"), "new header override works though") - w.WriteHeader(303) - assert.Equal(t, 0, tw.code, "code not written before flush") - assert.False(t, w.Done(), "done before flush") - require.NoError(t, w.Flush(), "flush") - assert.True(t, w.Done(), "done after flush") - assert.Equal(t, "howdy", string(tw.buffer), "write after flush") - assert.Equal(t, 303, tw.code, "code written after flush") - assert.Equal(t, "d", tw.Header().Get("c"), "new header written - c") - assert.Equal(t, "d", tw.Header().Get("a"), "new header written - a") -} - -func TestReset(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - tw.Header().Set("a", "b") - w := nvelope.NewDeferredWriter(tw) - - w.Write([]byte("doody")) - w.Header().Set("c", "e") - w.Header().Set("a", "e") - w.Header().Set("d", "g") - w.WriteHeader(109) - - w.Reset() - - w.Write([]byte("howdy")) - w.Header().Set("c", "d") - w.Header().Set("a", "d") - w.WriteHeader(303) - - require.NoError(t, w.Flush(), "flush") - - assert.Equal(t, "howdy", string(tw.buffer), "write after flush") - assert.Equal(t, 303, tw.code, "code written after flush") - assert.Equal(t, "d", tw.Header().Get("c"), "new header written - c") - assert.Equal(t, "d", tw.Header().Get("a"), "new header written - a") - assert.Equal(t, "", tw.Header().Get("d"), "new header not written - d") -} - -func TestFlushErrShortWrite(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - w := nvelope.NewDeferredWriter(tw) - - tw.simulateWriteError = io.ErrShortWrite - w.Write([]byte("howdy")) - - require.NoError(t, w.Flush(), "flush") - assert.Equal(t, "howdy", string(tw.buffer), "write after flush") -} - -func TestFlushError(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - w := nvelope.NewDeferredWriter(tw) - - tw.simulateWriteError = fmt.Errorf("an error") - w.Write([]byte("howdy")) - - assert.Error(t, w.Flush(), "flush error") -} - -func TestPreserveHeader(t *testing.T) { - tw := &testResponseWriter{header: make(http.Header)} - tw.Header().Set("a", "b") - tw.Header().Set("b", "c") - w := nvelope.NewDeferredWriter(tw) - - w.Header().Set("a", "B") - w.Header().Set("c", "d") - - w.PreserveHeader() - - w.Reset() - w.Header().Set("a", "x") - w.Header().Set("d", "x") - - w.Reset() - w.Flush() - - assert.Equal(t, "B", tw.Header().Get("a"), "new header written - a") - assert.Equal(t, "c", tw.Header().Get("b"), "new header written - b") - assert.Equal(t, "d", tw.Header().Get("c"), "new header written - c") - assert.Equal(t, "", tw.Header().Get("d"), "new header written - d") -} diff --git a/nvelope/doc.go b/nvelope/doc.go deleted file mode 100644 index 80a7824..0000000 --- a/nvelope/doc.go +++ /dev/null @@ -1,30 +0,0 @@ -// Stuff - -/* - -Package nvelope provides injection handlers that make building -HTTP endpoints simple. In combination with npoint and nject it -provides a API endpoint framework. - -The main things it provides are a request decoder and a response -encoder. - -The request decoder will fill in a struct to capture all the -parts of the request: path parameters, query parameters, headers, -and the body. The decoding is driven by struct tags that are -interpreted at program startup. - -The response encoder is comparatively simpler: given a model and an -error, it encodes the error or the model appropriately. - -Deferred writer allows output to be buffered and then abandoned. - -NotFound, Forbidden, and BadRequest provide easy ways to annotate -an error return to cause a specific HTTP error code to be sent. - -CatchPanic makes it easy to turn panics into error returns. - -The provided example puts it all together. - -*/ -package nvelope diff --git a/nvelope/encode.go b/nvelope/encode.go deleted file mode 100644 index bd7e2e4..0000000 --- a/nvelope/encode.go +++ /dev/null @@ -1,261 +0,0 @@ -package nvelope - -import ( - "encoding/json" - "encoding/xml" - "net/http" - - "github.com/muir/nject/nject" - - "github.com/golang/gddo/httputil" - "github.com/pkg/errors" -) - -// InjectWriter injects a DeferredWriter -var InjectWriter = nject.Provide("writer", NewDeferredWriter) - -// AutoFlushWriter calls Flush on the deferred writer if it hasn't -// already been done -var AutoFlushWriter = nject.Provide("autoflush-writer", func(inner func(), w *DeferredWriter) { - inner() - if !w.Done() { - _ = w.Flush() - } -}) - -// Response is an empty interface that is the expected return value -// from endpoints. -type Response interface{} - -// EncodeJSON is a JSON encoder manufactured by MakeResponseEncoder with default options. -var EncodeJSON = MakeResponseEncoder("JSON", - WithEncoder("application/json", json.Marshal, - WithEncoderErrorTransform(func(err error) (interface{}, bool) { - var jm json.Marshaler - if errors.As(err, &jm) { - return jm, true - } - return nil, false - }), - )) - -// EncodeXML is a XML encoder manufactured by MakeResponseEncoder with default options. -var EncodeXML = MakeResponseEncoder("XML", - WithEncoder("application/xml", xml.Marshal, - WithEncoderErrorTransform(func(err error) (interface{}, bool) { - var me xml.Marshaler - if errors.As(err, &me) { - return me, true - } - return nil, false - }), - )) - -type encoderOptions struct { - encoders map[string]specificEncoder - contentOffers []string - defaultEncoder string - errorTransformer ErrorTranformer -} - -type specificEncoder struct { - apiEnforcer func(httpCode int, enc []byte, header http.Header, r *http.Request) error - errorTransformer ErrorTranformer - encode func(interface{}) ([]byte, error) -} - -// ResponseEncoderFuncArg is a function argument for MakeResponseEncoder -type ResponseEncoderFuncArg func(*encoderOptions) - -// EncoderSpecificFuncArg is a functional arguemnt for WithEncoder -type EncoderSpecificFuncArg func(*specificEncoder) - -// ErrorTranformer transforms an error into a model that can be logged. -type ErrorTranformer func(error) (replacementModel interface{}, useReplacement bool) - -// WithEncoder adds an model encoder to what MakeResponseEncoder will support. -// The first encoder added becomes the default encoder that is used if there -// is no match between the client's Accept header and the encoders that -// MakeResponseEncoder knows about. -func WithEncoder(contentType string, encode func(interface{}) ([]byte, error), encoderOpts ...EncoderSpecificFuncArg) ResponseEncoderFuncArg { - return func(o *encoderOptions) { - if o.defaultEncoder == "" { - o.defaultEncoder = contentType - } - se := specificEncoder{ - encode: encode, - apiEnforcer: func(_ int, _ []byte, _ http.Header, _ *http.Request) error { return nil }, - } - for _, eo := range encoderOpts { - eo(&se) - } - if _, ok := o.encoders[contentType]; !ok { - o.contentOffers = append(o.contentOffers, contentType) - } - o.encoders[contentType] = se - } -} - -// WithErrorModel provides a function to transform errors before -// encoding them using the normal encoder. The return values are the model -// to use instead of the error and a boolean to indicate that the replacement -// should be used. If the boolean is false, then a plain text error -// message will be generated using err.Error(). -func WithErrorModel(errorTransformer ErrorTranformer) ResponseEncoderFuncArg { - return func(o *encoderOptions) { - o.errorTransformer = errorTransformer - } -} - -// WithEncoderErrorTransform provides an encoder-specific function to -// transform errors before -// encoding them using the normal encoder. The return values are the model -// to use instead of the error and a boolean to indicate that the replacement -// should be used. If the boolean is false, then a plain text error -// message will be generated using err.Error(). -func WithEncoderErrorTransform(errorTransformer ErrorTranformer) EncoderSpecificFuncArg { - return func(o *specificEncoder) { - o.errorTransformer = errorTransformer - } -} - -type APIEnforcerFunc func(httpCode int, enc []byte, header http.Header, r *http.Request) error - -// WithAPIEnforcer specifies -// a function that can check if the encoded API response is valid -// for the endpoint that is generating the response. This is where -// swagger enforcement could be added. The default is not not verify -// API conformance. -// -// https://github.com/muir/nvalid provides a function to generate an -// APIEnforcerFunc from swagger. -func WithAPIEnforcer(apiEnforcer APIEnforcerFunc) EncoderSpecificFuncArg { - return func(o *specificEncoder) { - o.apiEnforcer = apiEnforcer - } -} - -// MakeResponseEncoder generates an nject Provider to encode API responses. -// -// The generated provider is a wrapper that invokes the rest of the -// handler injection chain and expect to receive as return values -// an Response and and error. If the error is not nil, then the response -// becomes the error. -// -// If more than one encoder is configurured, then MakeResponseEncoder will default to -// the first one specified in its functional arguments. -func MakeResponseEncoder( - name string, - encoderFuncArgs ...ResponseEncoderFuncArg, -) nject.Provider { - o := encoderOptions{ - errorTransformer: func(_ error) (interface{}, bool) { return nil, false }, - encoders: make(map[string]specificEncoder), - } - for _, fa := range encoderFuncArgs { - fa(&o) - } - if o.defaultEncoder == "" { - // oops, the user should have done something! - WithEncoder("application/json", json.Marshal)(&o) - } - return nject.Provide("marshal-"+name, - func( - inner func() (Response, error), - w *DeferredWriter, - log BasicLogger, - r *http.Request, - ) { - model, err := inner() - if w.Done() { - return - } - contentType := httputil.NegotiateContentType(r, o.contentOffers, o.defaultEncoder) - encoder := o.encoders[contentType] - w.Header().Set("Content-Type", contentType) - var code int - var enc []byte - - // handleError will always set enc - var handleError func(recurseOkay bool) - handleError = func(recurseOkay bool) { - code = GetReturnCode(err) - et := encoder.errorTransformer - if et == nil { - et = o.errorTransformer - } - logDetails := map[string]interface{}{ - "httpCode": code, - "error": err.Error(), - "method": r.Method, - "uri": r.URL.String(), - } - if code < 500 { - log.Warn("returning user error", logDetails) - } else { - log.Error("returning server error", logDetails) - } - if rm, ok := et(err); ok { - enc, err = encoder.encode(rm) - if err != nil { - err = errors.Wrapf(err, "encode %s response", contentType) - if recurseOkay { - handleError(false) - } else { - enc = []byte(err.Error()) - } - } - } else { - enc = []byte(err.Error()) - } - } - if err != nil { - handleError(true) - } - - if len(enc) == 0 { - enc, err = encoder.encode(model) - if err != nil { - handleError(true) - } - } - - if code == 0 { - code = 200 - } - err = encoder.apiEnforcer(code, enc, w.Header(), r) - if err != nil { - handleError(true) - } - w.WriteHeader(code) - _, err = w.Write(enc) - e2 := w.Flush() - if err == nil { - err = e2 - } - if err != nil { - log.Warn("Cannot write response", - map[string]interface{}{ - "error": err.Error(), - "method": r.Method, - "uri": r.URL.String(), - }) - } - }) -} - -// Nil204 is a wrapper that causes looks for return values of Response and error -// and if both are nil, writes a 204 header and no data. It is mean to be used -// downstream from a response encocder. -var Nil204 = nject.Desired(nject.Provide("nil-204", nil204)) - -func nil204(inner func() (Response, error), w *DeferredWriter) { - model, err := inner() - if w.Done() { - return - } - if err == nil && model == nil { - w.WriteHeader(204) - _ = w.Flush() - } -} diff --git a/nvelope/errors.go b/nvelope/errors.go deleted file mode 100644 index 5afdf76..0000000 --- a/nvelope/errors.go +++ /dev/null @@ -1,71 +0,0 @@ -package nvelope - -import ( - "encoding" - "errors" -) - -// ReturnCode associates an HTTP return code with a error. -// if err is nil, then nil is returned. -func ReturnCode(err error, code int) error { - if err == nil { - return nil - } - return returnCode{ - cause: err, - code: code, - } -} - -type returnCode struct { - cause error - code int -} - -func (err returnCode) Unwrap() error { - return err.cause -} - -func (err returnCode) Cause() error { - return err.cause -} - -func (err returnCode) Error() string { - return err.cause.Error() -} - -// NotFound annotates an error has giving 404 HTTP return code -func NotFound(err error) error { - return ReturnCode(err, 404) -} - -// BadRequest annotates an error has giving 400 HTTP return code -func BadRequest(err error) error { - return ReturnCode(err, 400) -} - -// Unauthorized annotates an error has giving 401 HTTP return code -func Unauthorized(err error) error { - return ReturnCode(err, 401) -} - -// Forbidden annotates an error has giving 403 HTTP return code -func Forbidden(err error) error { - return ReturnCode(err, 403) -} - -// GetReturnCode turns an error into an HTTP response code. -func GetReturnCode(err error) int { - var rc returnCode - if errors.As(err, &rc) { - return rc.code - } - return 500 -} - -// CanModel represents errors that can transform themselves into a model -// for logging. -type CanModel interface { - error - Model() encoding.TextUnmarshaler -} diff --git a/nvelope/errors_test.go b/nvelope/errors_test.go deleted file mode 100644 index fb12093..0000000 --- a/nvelope/errors_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package nvelope_test - -import ( - "fmt" - "testing" - - "github.com/muir/nject/nvelope" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" -) - -func TestErrors(t *testing.T) { - assert.Equal(t, 304, nvelope.GetReturnCode(nvelope.ReturnCode(fmt.Errorf("x"), 304)), "unwrapped") - assert.Equal(t, 303, nvelope.GetReturnCode(errors.Wrap(nvelope.ReturnCode(fmt.Errorf("x"), 303), "o")), "wrapped") - assert.Equal(t, 400, nvelope.GetReturnCode(nvelope.BadRequest(fmt.Errorf("x"))), "bad") - assert.Equal(t, 401, nvelope.GetReturnCode(nvelope.Unauthorized(fmt.Errorf("x"))), "unauth") - assert.Equal(t, 403, nvelope.GetReturnCode(nvelope.Forbidden(fmt.Errorf("x"))), "forbid") -} diff --git a/nvelope/example_middleware_test.go b/nvelope/example_middleware_test.go deleted file mode 100644 index e318a09..0000000 --- a/nvelope/example_middleware_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package nvelope_test - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "time" - - "github.com/muir/nject/npoint" - "github.com/muir/nject/nvelope" - - "github.com/gorilla/mux" -) - -func RequestTimingMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - fmt.Println("timing start") - before := time.Now() - next(w, r) - after := time.Now() - duration := after.Sub(before) - fmt.Println("timing end, Request took", duration.Round(time.Hour)) - } -} - -func AuthenticationMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - fmt.Println("authentication start") - a := r.Header.Get("Authentication") - if a != "good" { - w.WriteHeader(401) - w.Write([]byte("Invalid authentication")) - fmt.Println("authentication end (failed)") - return - } - next(w, r) - fmt.Println("authentication end") - } -} - -func AuthorizationMiddleware(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - fmt.Println("authorization start") - vars := mux.Vars(r) - if vars["with"] != "john" { - w.WriteHeader(403) - w.Write([]byte("Invalid authorization")) - fmt.Println("authorization end (failed)") - return - } - next(w, r) - fmt.Println("authorization end") - } -} - -func ServiceWithMiddleware(router *mux.Router) { - service := npoint.RegisterServiceWithMux("example", router) - service.RegisterEndpoint("/a/path/{with}/{parameters}", - // order matters and this is a correct order - nvelope.MiddlewareBaseWriter(RequestTimingMiddleware), - nvelope.NoLogger, - nvelope.InjectWriter, - nvelope.AutoFlushWriter, // because middlware won't Flush() - nvelope.MiddlewareDeferredWriter(AuthenticationMiddleware, AuthorizationMiddleware), - nvelope.EncodeJSON, - nvelope.CatchPanic, - func() (nvelope.Response, error) { - fmt.Println("thing") - return "did a thing", nil - }, - ).Methods("GET") -} - -// Example shows an injection chain handling a single endpoint using nject, -// npoint, and nvelope. -func ExampleServiceWithMiddleware() { - r := mux.NewRouter() - ServiceWithMiddleware(r) - ts := httptest.NewServer(r) - client := ts.Client() - doGet := func(url string, authHeader string) { - req, err := http.NewRequestWithContext(context.Background(), "GET", ts.URL+url, nil) - if err != nil { - fmt.Println("request error:", err) - return - } - req.Header.Set("Authentication", authHeader) - res, err := client.Do(req) - if err != nil { - fmt.Println("response error:", err) - return - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - fmt.Println("read error:", err) - return - } - res.Body.Close() - fmt.Println(res.StatusCode, "->"+string(b)) - } - doGet("/a/path/john/37", "good") - doGet("/a/path/john/37", "bad") - doGet("/a/path/fred/37", "good") - // Output: timing start - // authentication start - // authorization start - // thing - // authorization end - // authentication end - // timing end, Request took 0s - // 200 ->"did a thing" - // timing start - // authentication start - // authentication end (failed) - // timing end, Request took 0s - // 401 ->Invalid authentication - // timing start - // authentication start - // authorization start - // authorization end (failed) - // authentication end - // timing end, Request took 0s - // 403 ->Invalid authorization -} diff --git a/nvelope/example_mwhandler_test.go b/nvelope/example_mwhandler_test.go deleted file mode 100644 index 699a865..0000000 --- a/nvelope/example_mwhandler_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package nvelope_test - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "time" - - "github.com/muir/nject/npoint" - "github.com/muir/nject/nvelope" - - "github.com/gorilla/mux" -) - -func RequestTimingMiddlewareHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("timing start") - before := time.Now() - next.ServeHTTP(w, r) - after := time.Now() - duration := after.Sub(before) - fmt.Println("timing end, Request took", duration.Round(time.Hour)) - }) -} - -func AuthenticationMiddlewareHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("authentication start") - a := r.Header.Get("Authentication") - if a != "good" { - w.WriteHeader(401) - w.Write([]byte("Invalid authentication")) - fmt.Println("authentication end (failed)") - return - } - next.ServeHTTP(w, r) - fmt.Println("authentication end") - }) -} - -func AuthorizationMiddlewareHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("authorization start") - vars := mux.Vars(r) - if vars["with"] != "john" { - w.WriteHeader(403) - w.Write([]byte("Invalid authorization")) - fmt.Println("authorization end (failed)") - return - } - next.ServeHTTP(w, r) - fmt.Println("authorization end") - }) -} - -func ServiceWithMiddlewareHandler(router *mux.Router) { - service := npoint.RegisterServiceWithMux("example", router) - service.RegisterEndpoint("/a/path/{with}/{parameters}", - // order matters and this is a correct order - nvelope.MiddlewareHandlerBaseWriter(RequestTimingMiddlewareHandler), - nvelope.NoLogger, - nvelope.InjectWriter, - nvelope.AutoFlushWriter, // because middlware won't Flush() - nvelope.MiddlewareHandlerDeferredWriter(AuthenticationMiddlewareHandler, AuthorizationMiddlewareHandler), - nvelope.EncodeJSON, - nvelope.CatchPanic, - func() (nvelope.Response, error) { - fmt.Println("thing") - return "did a thing", nil - }, - ).Methods("GET") -} - -// Example shows an injection chain handling a single endpoint using nject, -// npoint, and nvelope. -func ExampleServiceWithMiddlewareHandler() { - r := mux.NewRouter() - ServiceWithMiddleware(r) - ts := httptest.NewServer(r) - client := ts.Client() - doGet := func(url string, authHeader string) { - req, err := http.NewRequestWithContext(context.Background(), "GET", ts.URL+url, nil) - if err != nil { - fmt.Println("request error:", err) - return - } - req.Header.Set("Authentication", authHeader) - res, err := client.Do(req) - if err != nil { - fmt.Println("response error:", err) - return - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - fmt.Println("read error:", err) - return - } - res.Body.Close() - fmt.Println(res.StatusCode, "->"+string(b)) - } - doGet("/a/path/john/37", "good") - doGet("/a/path/john/37", "bad") - doGet("/a/path/fred/37", "good") - // Output: timing start - // authentication start - // authorization start - // thing - // authorization end - // authentication end - // timing end, Request took 0s - // 200 ->"did a thing" - // timing start - // authentication start - // authentication end (failed) - // timing end, Request took 0s - // 401 ->Invalid authentication - // timing start - // authentication start - // authorization start - // authorization end (failed) - // authentication end - // timing end, Request took 0s - // 403 ->Invalid authorization -} diff --git a/nvelope/example_panic_test.go b/nvelope/example_panic_test.go deleted file mode 100644 index c2f7540..0000000 --- a/nvelope/example_panic_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package nvelope_test - -import ( - "fmt" - - "github.com/muir/nject/nvelope" -) - -func ExampleRecoverStack() { - f := func(i int) (err error) { - defer nvelope.SetErrorOnPanic(&err, nvelope.NoLogger()) - return func() error { - switch i { - case 0: - panic("zero") - case 1: - return fmt.Errorf("a one") - default: - return nil - } - }() - } - err := f(0) - fmt.Println(err) - stack := nvelope.RecoverStack(err) - fmt.Println(len(stack) > 1000) - // Output: panic: zero - // true -} diff --git a/nvelope/example_test.go b/nvelope/example_test.go deleted file mode 100644 index f392337..0000000 --- a/nvelope/example_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package nvelope_test - -import ( - "errors" - "fmt" - "io/ioutil" - "log" - "net/http" - "net/http/httptest" - "strings" - - "github.com/muir/nject/npoint" - "github.com/muir/nject/nvelope" - - "github.com/gorilla/mux" -) - -// nolint:deadcode,unused -func Main() { - r := mux.NewRouter() - srv := &http.Server{ - Addr: "0.0.0.0:8080", - Handler: r, - } - Service(r) - log.Fatal(srv.ListenAndServe()) -} - -type PostBodyModel struct { - Use string `json:"use"` - Exported string `json:"exported"` - Names string `json:"names"` -} - -type ExampleRequestBundle struct { - Request PostBodyModel `nvelope:"model"` - With *string `nvelope:"path,name=with"` - Parameters int64 `nvelope:"path,name=parameters"` - Friends []int `nvelope:"query,name=friends"` - ContentType string `nvelope:"header,name=Content-Type"` -} - -type ExampleResponse struct { - Stuff string `json:"stuff,omitempty"` - Here string `json:"here,omitempty"` -} - -func HandleExampleEndpoint(req ExampleRequestBundle) (nvelope.Response, error) { - if req.ContentType != "application/json" { - return nil, errors.New("content type must be application/json") - } - switch req.Parameters { - case 666: - panic("something is not right") - case 100: - return nil, nil - default: - return ExampleResponse{ - Stuff: *req.With, - }, nil - } -} - -func Service(router *mux.Router) { - service := npoint.RegisterServiceWithMux("example", router) - service.RegisterEndpoint("/a/path/{with}/{parameters}", - // order matters and this is a correct order - nvelope.NoLogger, - nvelope.InjectWriter, - nvelope.EncodeJSON, - nvelope.CatchPanic, - nvelope.Nil204, - nvelope.ReadBody, - nvelope.DecodeJSON, - HandleExampleEndpoint, - ).Methods("POST") -} - -// Example shows an injection chain handling a single endpoint using nject, -// npoint, and nvelope. -func Example() { - r := mux.NewRouter() - Service(r) - ts := httptest.NewServer(r) - client := ts.Client() - doPost := func(url string, body string) { - // nolint:noctx - res, err := client.Post(ts.URL+url, "application/json", - strings.NewReader(body)) - if err != nil { - fmt.Println("response error:", err) - return - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - fmt.Println("read error:", err) - return - } - res.Body.Close() - fmt.Println(res.StatusCode, "->"+string(b)) - } - doPost("/a/path/joe/37", `{"Use":"yeah","Exported":"uh hu"}`) - doPost("/a/path/joe/100", `{"Use":"yeah","Exported":"uh hu"}`) - doPost("/a/path/joe/38", `invalid json`) - doPost("/a/path/joe/666", `{"Use":"yeah","Exported":"uh hu"}`) - - // Output: 200 ->{"stuff":"joe"} - // 204 -> - // 400 ->nvelope_test.ExampleRequestBundle model: Could not decode application/json into nvelope_test.PostBodyModel: invalid character 'i' looking for beginning of value - // 500 ->panic: something is not right -} diff --git a/nvelope/helpers_test.go b/nvelope/helpers_test.go deleted file mode 100644 index 48810e6..0000000 --- a/nvelope/helpers_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package nvelope_test - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/cookiejar" - "net/http/httptest" - "strings" - - "github.com/muir/nject/npoint" - "github.com/muir/nject/nvelope" - - "github.com/gorilla/mux" -) - -// nolint:deadcode,unused -func setupTestService(path string, f interface{}) func(string, ...mod) { - return captureOutputFunc(func(i ...interface{}) { - fmt.Println(i...) - }, path, f) -} - -func captureOutput(path string, f interface{}) func(string, ...mod) string { - var o string - do := captureOutputFunc(func(i ...interface{}) { - o += fmt.Sprint(i...) - }, path, f) - return func(url string, mods ...mod) string { - o = "" - do(url, mods...) - return o - } -} - -type mod func(*http.Request, *http.Client, *httptest.Server) - -// nolint:unused,deadcode -func body(s string) mod { - return func(r *http.Request, cl *http.Client, ts *httptest.Server) { - r.Body = ioutil.NopCloser(strings.NewReader(s)) - } -} - -func cookie(k, v string) mod { - return func(r *http.Request, cl *http.Client, ts *httptest.Server) { - cl.Jar.SetCookies(r.URL, []*http.Cookie{ - {Name: k, Value: v}, - }) - } -} - -func header(k, v string) mod { - return func(r *http.Request, cl *http.Client, ts *httptest.Server) { - r.Header[k] = append(r.Header[k], v) - } -} - -func captureOutputFunc(out func(...interface{}), path string, f interface{}) func(string, ...mod) { - router := mux.NewRouter() - service := npoint.RegisterServiceWithMux("example", router) - service.RegisterEndpoint(path, - // order matters and this is a correct order - nvelope.NoLogger, - nvelope.InjectWriter, - nvelope.EncodeJSON, - nvelope.CatchPanic, - nvelope.Nil204, - nvelope.ReadBody, - nvelope.DecodeJSON, - f, - ).Methods("POST") - ts := httptest.NewServer(router) - - return func(url string, mods ...mod) { - client := ts.Client() - var err error - client.Jar, err = cookiejar.New(&cookiejar.Options{}) - if err != nil { - panic("jar") - } - // nolint:noctx - req, err := http.NewRequest("POST", ts.URL+url, ioutil.NopCloser(strings.NewReader(""))) - if err != nil { - panic("request") - } - for _, m := range mods { - m(req, client, ts) - } - - // nolint:noctx - res, err := client.Do(req) - if err != nil { - out("response error:", err) - return - } - b, err := ioutil.ReadAll(res.Body) - if err != nil { - out("read error:", err) - return - } - res.Body.Close() - out(res.StatusCode, "->"+string(b)) - } -} diff --git a/nvelope/logger.go b/nvelope/logger.go deleted file mode 100644 index 0f8c3b6..0000000 --- a/nvelope/logger.go +++ /dev/null @@ -1,68 +0,0 @@ -package nvelope - -import ( - "fmt" -) - -// BasicLogger is just the start of what a logger might -// support. It exists mostly as a placeholder. Future -// versions of nvelope will prefer more capabile loggers -// but will use type assertions so that the BasicLogger -// will remain acceptable to the APIs. -type BasicLogger interface { - Debug(msg string, fields ...map[string]interface{}) - Error(msg string, fields ...map[string]interface{}) - Warn(msg string, fields ...map[string]interface{}) -} - -// StdLogger is implmented by the base library log.Logger -type StdLogger interface { - Print(v ...interface{}) -} - -type wrappedStdLogger struct { - log StdLogger -} - -// LoggerFromStd creates a -func LoggerFromStd(log StdLogger) func() BasicLogger { - return func() BasicLogger { - return wrappedStdLogger{log: log} - } -} - -func (std wrappedStdLogger) Error(msg string, fields ...map[string]interface{}) { - if len(fields) == 0 { - std.log.Print(msg) - return - } - vals := make([]interface{}, 1, len(fields)*4+1) - vals[0] = msg - for _, m := range fields { - for k, v := range m { - vals = append(vals, k+"="+fmt.Sprint(v)) - } - } - std.log.Print(vals...) -} - -func (std wrappedStdLogger) Warn(msg string, fields ...map[string]interface{}) { - std.Error(msg, fields...) -} - -func (std wrappedStdLogger) Debug(msg string, fields ...map[string]interface{}) { - std.Error(msg, fields...) -} - -// NoLogger injects a BasicLogger that discards all inputs -func NoLogger() BasicLogger { - return nilLogger{} -} - -type nilLogger struct{} - -var _ BasicLogger = nilLogger{} - -func (nilLogger) Error(msg string, fields ...map[string]interface{}) {} -func (nilLogger) Warn(msg string, fields ...map[string]interface{}) {} -func (nilLogger) Debug(msg string, fields ...map[string]interface{}) {} diff --git a/nvelope/middleware.go b/nvelope/middleware.go deleted file mode 100644 index 4fdc66f..0000000 --- a/nvelope/middleware.go +++ /dev/null @@ -1,151 +0,0 @@ -package nvelope - -import ( - "net/http" - - "github.com/muir/nject/nject" -) - -// MiddlewareBaseWriter acts as a translator. In the Go world, there -// are a bunch of packages that expect to use the wrapping -// func(http.HandlerFunc) http.HandlerFunc -// pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to -// use and not as expressive as the patterns supported by -// npoint and nvelope, but there may be code written -// with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with -// npoint and nvelope. -// -// MiddlewareBaseWriter converts existing func(http.HandlerFunc) http.HandlerFunc functions so that -// they're compatible with nject. Because Middleware may wrap -// http.ResponseWriter, it should be used earlier in the injection -// chain than InjectWriter so that InjectWriter gets the already-wrapped -// http.ResponseWriter. Use MiddlewareBaseWriter if you suspect that the -// middleware you're wrapping replaces the writer. -func MiddlewareBaseWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider { - combined := combineMiddleware(m) - - return nject.Required(nject.Provide("wrapped-func(http.HandlerFunc) http.HandlerFunc-base", - func(inner func(w http.ResponseWriter, r *http.Request), w http.ResponseWriter, r *http.Request) { - combined(inner)(w, r) - })) -} - -// MiddlewareDeferredWriter acts as a translator. In the Go world, there -// are a bunch of packages that expect to use the wrapping -// func(http.HandlerFunc) http.HandlerFunc -// pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to -// use and not as expressive as the patterns supported by -// npoint and nvelope, but there may be code written -// with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with -// npoint and nvelope. -// -// MiddlewareDeferredWriter converts existing func(http.HandlerFunc) http.HandlerFunc functions so that -// they're compatible with nject. MiddlewareDeferredWriter injects a -// DeferredWriter into the the func(http.HandlerFunc) http.HandlerFunc handler chain. If the chain -// replaces the writer, there will be two writers in play at once and -// results may be inconsistent. MiddlewareDeferredWriter must be used -// after InjectWriter. Use MiddlewareDeferredWriter if you know that the middleware -// you're wrapping does not replace the writer. -func MiddlewareDeferredWriter(m ...func(http.HandlerFunc) http.HandlerFunc) nject.Provider { - combined := combineMiddleware(m) - - return nject.Required(nject.Provide("wrapped-func(http.HandlerFunc) http.HandlerFunc-deferred", - func(inner func(w http.ResponseWriter, r *http.Request), w *DeferredWriter, r *http.Request) { - combined(inner)(http.ResponseWriter(w), r) - })) -} - -func combineMiddleware(m []func(http.HandlerFunc) http.HandlerFunc) func(http.HandlerFunc) http.HandlerFunc { - switch len(m) { - case 0: - return func(h http.HandlerFunc) http.HandlerFunc { - return h - } - case 1: - return m[0] - default: - combined := m[len(m)-1] - for i := len(m) - 2; i >= 0; i-- { - f := m[i] - c := combined - combined = func(h http.HandlerFunc) http.HandlerFunc { - return f(c(h)) - } - } - return combined - } -} - -// MiddlewareHandlerBaseWriter acts as a translator. In the Go world, there -// are a bunch of packages that expect to use the wrapping -// func(http.Handler) http.Handler -// pattern. The func(http.HandlerFunc) http.HandlerFunc pattern is harder to -// use and not as expressive as the patterns supported by -// npoint and nvelope, but there may be code written -// with the func(http.HandlerFunc) http.HandlerFunc pattern that you want to use with -// npoint and nvelope. -// -// MiddlewareHandlerBaseWriter converts existing func(http.Handler) http.Handler functions so that -// they're compatible with nject. Because Middleware may wrap -// http.ResponseWriter, it should be used earlier in the injection -// chain than InjectWriter so that InjectWriter gets the already-wrapped -// http.ResponseWriter. Use MiddlewareBaseWriter if you suspect that the -// middleware you're wrapping replaces the writer. -func MiddlewareHandlerBaseWriter(m ...func(http.Handler) http.Handler) nject.Provider { - combined := combineHandlerMiddleware(m) - - return nject.Required(nject.Provide("wrapped-func(http.Handler) http.Handler-base", - func(inner func(w http.ResponseWriter, r *http.Request), w http.ResponseWriter, r *http.Request) { - combined(inner)(w, r) - })) -} - -// MiddlewareHandlerDeferredWriter acts as a translator. In the Go world, there -// are a bunch of packages that expect to use the wrapping -// func(http.Handler) http.Handler -// pattern. The func(http.Handler) http.Handler pattern is harder to -// use and not as expressive as the patterns supported by -// npoint and nvelope, but there may be code written -// with the func(http.Handler) http.Handler pattern that you want to use with -// npoint and nvelope. -// -// MiddlewareHandlerDeferredWriter converts existing func(http.Handler) http.Handler functions so that -// they're compatible with nject. MiddlewareHandlerDeferredWriter injects a -// DeferredWriter into the the func(http.Handler) http.Handler handler chain. If the chain -// replaces the writer, there will be two writers in play at once and -// results may be inconsistent. MiddlewareHandlerDeferredWriter must be used -// after InjectWriter. Use MiddlewareHandlerDeferredWriter if you know that the middleware -// you're wrapping does not replace the writer. -func MiddlewareHandlerDeferredWriter(m ...func(http.Handler) http.Handler) nject.Provider { - combined := combineHandlerMiddleware(m) - - return nject.Required(nject.Provide("wrapped-func(http.Handler) http.Handler-deferred", - func(inner func(w http.ResponseWriter, r *http.Request), w *DeferredWriter, r *http.Request) { - combined(inner)(http.ResponseWriter(w), r) - })) -} - -func combineHandlerMiddleware(m []func(http.Handler) http.Handler) func(http.HandlerFunc) http.HandlerFunc { - switch len(m) { - case 0: - return func(h http.HandlerFunc) http.HandlerFunc { - return h - } - case 1: - return func(h http.HandlerFunc) http.HandlerFunc { - return m[0](h).ServeHTTP - } - default: - combined := func(h http.HandlerFunc) http.HandlerFunc { - return m[len(m)-1](h).ServeHTTP - } - for i := len(m) - 2; i >= 0; i-- { - f := m[i] - c := combined - combined = func(h http.HandlerFunc) http.HandlerFunc { - return f(c(h)).ServeHTTP - } - } - return combined - } -} diff --git a/nvelope/panic.go b/nvelope/panic.go deleted file mode 100644 index 81a8fe4..0000000 --- a/nvelope/panic.go +++ /dev/null @@ -1,88 +0,0 @@ -package nvelope - -import ( - "fmt" - "runtime/debug" - - "github.com/muir/nject/nject" - - "github.com/pkg/errors" -) - -// LogFlusher is used to check if a logger implements -// Flush(). This is useful as part of a panic handler. -type LogFlusher interface { - Flush() -} - -type panicError struct { - msg string - r interface{} - stack string -} - -func (err panicError) Error() string { - return "panic: " + err.msg -} - -// SetErrorOnPanic should be called as a defer. It -// sets an error value if there is a panic. -func SetErrorOnPanic(ep *error, log BasicLogger) { - r := recover() - if r == nil { - return - } - pe := panicError{ - msg: fmt.Sprint(r), - r: r, - stack: string(debug.Stack()), - } - *ep = errors.WithStack(pe) - log.Error("panic!", map[string]interface{}{ - "msg": pe.msg, - "stack": pe.stack, - }) - if flusher, ok := log.(LogFlusher); ok { - flusher.Flush() - } -} - -// CatchPanic is a wrapp that catches downstream panics and returns -// an error a downsteam provider panic's. -var CatchPanic = nject.Provide("catch-panic", catchPanicInjector) - -func catchPanicInjector(inner func() error, log BasicLogger) (err error) { - defer SetErrorOnPanic(&err, log) - err = inner() - return -} - -// RecoverInterface returns the interface{} that recover() -// originally provided. Or it returns nil if the -// error isn't a from a panic recovery. This works only -// in conjunction with SetErrorOnPanic() and CatchPanic. -func RecoverInterface(err error) interface{} { - if pe, ok := isPanicError(err); ok { - return pe.r - } - return nil -} - -// RecoverStack returns the stack from when recover() -// originally caught the panic. Or it returns "" if the -// error isn't a from a panic recovery. This works only -// in conjunction with SetErrorOnPanic() and CatchPanic. -func RecoverStack(err error) string { - if pe, ok := isPanicError(err); ok { - return pe.stack - } - return "" -} - -func isPanicError(err error) (panicError, bool) { - var pe panicError - if errors.As(err, &pe) { - return pe, true - } - return panicError{}, false -} diff --git a/nject/regressions_test.go b/regressions_test.go similarity index 100% rename from nject/regressions_test.go rename to regressions_test.go diff --git a/nject/run_test.go b/run_test.go similarity index 100% rename from nject/run_test.go rename to run_test.go diff --git a/nject/setcallback_test.go b/setcallback_test.go similarity index 100% rename from nject/setcallback_test.go rename to setcallback_test.go diff --git a/nject/type_codes.go b/type_codes.go similarity index 100% rename from nject/type_codes.go rename to type_codes.go diff --git a/nject/types.go b/types.go similarity index 100% rename from nject/types.go rename to types.go