diff --git a/README b/README deleted file mode 100644 index b0f2226..0000000 --- a/README +++ /dev/null @@ -1,27 +0,0 @@ -# Gadgeto! - -Author: Thomas Schaffer -Language: Golang -License: MIT - -Gadgeto! is a collection of tools that aim to facilitate the development of -REST APIs in Go. -These tools are based on and enrich popular open-source solutions, to provide -higher-level functionalities. - -Components: - - tonic: Based on the REST framework Gin (https://github.com/gin-gonic/gin), - tonic lets you write simpler handler functions, and handles - repetitive tasks for you (parameter binding, error handling). - - - zesty: Based on Gorp (https://github.com/go-gorp/gorp), zesty abstracts - DB specifics for easy (nested) transaction management. - - - iffy: An HTTP testing library for functional/unit tests, - iffy does all the hard work (http, marshaling, templating...) and - lets you describe http calls as one-liners, chaining them in complex scenarios. - - - amock: An HTTP mocking library. amock lets you easily write mock objects to inject - into http clients. - It does not require any go-generate tricks, and lets you inject mocks - into existing libraries that may not be properly abstracted through interfaces. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1711683 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Gadgeto! + +Author: Thomas Schaffer +Language: Golang +License: MIT + +Gadgeto! is a collection of tools that aim to facilitate the development of +REST APIs in Go. +These tools are based on and enrich popular open-source solutions, to provide +higher-level functionalities. + +## Components + +- [tonic](./tonic/README.md): Based on the REST framework Gin (), + tonic lets you write simpler handler functions, and handles + repetitive tasks for you (parameter binding, error handling). + +- [zesty](./zesty/README.md): Based on Gorp (), zesty abstracts + DB specifics for easy (nested) transaction management. + +- [iffy](./iffy/README.md): An HTTP testing library for functional/unit tests, + iffy does all the hard work (http, marshaling, templating...) and + lets you describe http calls as one-liners, chaining them in complex scenarios. + +- [amock](./amock/README.md): An HTTP mocking library. amock lets you easily write mock objects to inject + into http clients. + It does not require any go-generate tricks, and lets you inject mocks + into existing libraries that may not be properly abstracted through interfaces. diff --git a/amock/README b/amock/README deleted file mode 100644 index 2feab32..0000000 --- a/amock/README +++ /dev/null @@ -1,24 +0,0 @@ -amock lets you easily mock any HTTP dependency you may have. -It respects the http.RoundTripper interface to replace an http client's transport. - -Responses are stacked, and indexed by code path: you specify responses for a certain Go function, -and when it's invoked the mock object will go up the stack until it reaches max depth, -or finds a function for which you specified a response. - -The response will be pop'ed, so the next identical call will get the next expected response. - -You can specify conditional filters on responses: - - - OnFunc(foo.GetFoo): - Filter on calls that went through a given go function - - - - OnIdentifier("foo"): - Shortcut to filter on requests following a path pattern of /.../foo(/...) - It is a reasonable assumption that REST implementations follow that pattern, - which makes writing conditions for these simple cases very easy. - - - On(func(c *amock.Context) bool { return c.Request.Method == "GET" } ): - More verbose but possible to express anything. - -For a working example, see amock_test.go diff --git a/amock/README.md b/amock/README.md new file mode 100644 index 0000000..f70f2f6 --- /dev/null +++ b/amock/README.md @@ -0,0 +1,21 @@ +# amock + +amock lets you easily mock any HTTP dependency you may have. It respects the http.RoundTripper interface to replace an http client's transport. + +Responses are stacked, and indexed by code path: you specify responses for a certain Go function, and when it's invoked the mock object will go up the stack until it reaches max depth, or finds a function for which you specified a response. + +The response will be pop'ed, so the next identical call will get the next expected response. + +You can specify conditional filters on responses: + +- `OnFunc(foo.GetFoo)`: Filter on calls that went through a given go function +- `OnIdentifier("foo")`: Shortcut to filter on requests following a path pattern of /.../foo(/...). It is a reasonable assumption that REST implementations follow that pattern, which makes writing conditions for these simple cases very easy. +- `On(func(c *amock.Context) bool)`: More verbose but possible to express anything. Example that would filter all GET requests: + +```go +On(func(c *amock.Context) bool { + return c.Request.Method == "GET" +}) +``` + +For a working example, see amock_test.go diff --git a/iffy/README b/iffy/README.md similarity index 93% rename from iffy/README rename to iffy/README.md index 61f8fe0..73ba57b 100644 --- a/iffy/README +++ b/iffy/README.md @@ -1,11 +1,13 @@ +# iffy + iffy helps you test http handlers. We assume JSON for any marshaling/unmarshaling. If you use something else, that's fine, but the advanced features (responseObject + templating) will not work. -Example: +## Example +```go func TestFoo(t *testing.T) { - // Instantiate & configure anything that implements http.Handler r := gin.Default() r.GET("/hello", tonic.Handler(helloHandler, 200)) @@ -32,5 +34,6 @@ func TestFoo(t *testing.T) { tester.Run() } +``` -For a real-life example, see https://github.com/loopfz/gadgeto/blob/master/tonic/tonic_test.go +For a real-life example, see diff --git a/tonic/README b/tonic/README deleted file mode 100644 index 860f82f..0000000 --- a/tonic/README +++ /dev/null @@ -1,117 +0,0 @@ -tonic lets you write simpler gin handlers. -The way it works is that it generates wrapping gin-compatible handlers, -that do all the repetitive work and wrap the call to your simple tonic handler. - -Package tonic handles path/query/body parameter binding in a single consolidated input object -which allows you to remove all the boilerplate code that retrieves and tests the presence -of various parameters. - -Here is an example input object. - - type MyInput struct { - Foo int `path:"foo"` - Bar string `query:"bar" default:"foobar"` - Baz string `json:"baz"` - } - -Output objects can be of any type, and will be marshaled to JSON. - -Input validation is performed after binding into the object using the validator library -(https://github.com/go-playground/validator). You can use the tag 'validate' on your object -definition to perform validation inside the tonic handler phase. - - type MyInput struct { - Foo int `path:"foo" validate:"required,gt=10"` - Bar string `query:"bar" default:"foobar" validate:"nefield=Baz"` - Baz string `json:"baz" validate:"required,email"` - } - -enum input validation is also implemented natively by tonic, and can check that the provided input -value corresponds to one of the expected enum values. - - type MyInput struct { - Bar string `query:"bar" enum:"foo,buz,biz"` - } - - -The handler can return an error, which will be returned to the caller. - -Here is a basic application that greets a user on http://localhost:8080/hello/me - - import ( - "errors" - "fmt" - - "github.com/gin-gonic/gin" - "github.com/loopfz/gadgeto/tonic" - ) - - type GreetUserInput struct { - Name string `path:"name" validate:"required,gt=3" description:"User name"` - } - - type GreetUserOutput struct { - Message string `json:"message"` - } - - func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { - if in.Name == "satan" { - return nil, errors.New("go to hell") - } - return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil - } - - func main() { - r := gin.Default() - r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) - r.Run(":8080") - } - - -If needed, you can also override different parts of the logic via certain available hooks in tonic: - - binding - - error handling - - render - -We provide defaults for these (bind from JSON, render into JSON, error = http status 400). -You will probably want to customize the error hook, to produce finer grained error status codes. - -The role of this error hook is to inspect the returned error object and deduce the http specifics from it. -We provide a ready-to-use error hook that depends on the juju/errors package (richer errors): - https://github.com/loopfz/gadgeto/tree/master/tonic/utils/jujerr - -Example of the same application as before, using juju errors: - - import ( - "fmt" - - "github.com/gin-gonic/gin" - "github.com/juju/errors" - "github.com/loopfz/gadgeto/tonic" - "github.com/loopfz/gadgeto/tonic/utils/jujerr" - ) - - type GreetUserInput struct { - Name string `path:"name" description:"User name" validate:"required"` - } - - type GreetUserOutput struct { - Message string `json:"message"` - } - - func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { - if in.Name == "satan" { - return nil, errors.NewForbidden(nil, "go to hell") - } - return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil - } - - func main() { - tonic.SetErrorHook(jujerr.ErrHook) - r := gin.Default() - r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) - r.Run(":8080") - } - - -You can also easily serve auto-generated swagger documentation (using tonic data) with https://github.com/wi2l/fizz diff --git a/tonic/README.md b/tonic/README.md new file mode 100644 index 0000000..3761d92 --- /dev/null +++ b/tonic/README.md @@ -0,0 +1,152 @@ +# tonic + +tonic lets you write simpler gin handlers. The way it works is that it generates wrapping gin-compatible handlers, that do all the repetitive work and wrap the call to your simple tonic handler. + +Package tonic handles path/query/body parameter binding in a single consolidated input object which allows you to remove all the boilerplate code that retrieves and tests the presence of various parameters. + +## Table of contents + +1. [Usage](#usage) + - [Inputs](#inputs) + - [Outputs](#outputs) + - [Validation](#validation) + - [Enum Validation](#enum-validation) +2. [Basic Example](#basic-example) +3. [Customization](#customization) + - [Custom Errors Example](#custom-errors-example) +4. [OpenAPI / Swagger Documentation](#openapi--swagger-documentation) + +## Usage + +### Inputs + +Here is an example input object. + +```go +type MyInput struct { + Foo int `path:"foo"` + Bar string `query:"bar" default:"foobar"` + Baz string `json:"baz"` +} +``` + +### Outputs + +Output objects can be of any type, and will be marshaled to JSON. + +### Validation + +Input validation is performed after binding into the object using the validator library +(). You can use the tag 'validate' on your object +definition to perform validation inside the tonic handler phase. + +```go +type MyInput struct { + Foo int `path:"foo" validate:"required,gt=10"` + Bar string `query:"bar" default:"foobar" validate:"nefield=Baz"` + Baz string `json:"baz" validate:"required,email"` +} +``` + +#### Enum validation + +In addition to the validation provided by the validator library, enum input validation is also implemented natively by tonic, and can check that the provided input value corresponds to one of the expected enum values. + +```go +type MyInput struct { + Bar string `query:"bar" enum:"foo,buz,biz"` +} +``` + +The handler can return an error, which will be returned to the caller. + +## Basic Example + +Here is a basic application that greets a user on + +```go +import ( + "errors" + "fmt" + + "github.com/gin-gonic/gin" + "github.com/loopfz/gadgeto/tonic" +) + +type GreetUserInput struct { + Name string `path:"name" validate:"required,gt=3" description:"User name"` +} + +type GreetUserOutput struct { + Message string `json:"message"` +} + +func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { + if in.Name == "satan" { + return nil, errors.New("go to hell") + } + return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil +} + +func main() { + r := gin.Default() + r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) + r.Run(":8080") +} +``` + +## Customization + +If needed, you can also override different parts of the logic via certain available hooks in tonic: + +- binding + - default: bind from JSON +- error handling + - default: http status 400 +- render + - default: render into JSON + +You will probably want to customize the error hook to produce finer grained error status codes. + +The role of this error hook is to inspect the returned error object and deduce the http specifics from it. We provide a ready-to-use error hook that depends on the juju/errors package (richer errors): + +## Custom Errors Example + +Example of the same application as before, using juju errors: + +```go +import ( + "fmt" + + "github.com/gin-gonic/gin" + "github.com/juju/errors" + "github.com/loopfz/gadgeto/tonic" + "github.com/loopfz/gadgeto/tonic/utils/jujerr" +) + +type GreetUserInput struct { + Name string `path:"name" description:"User name" validate:"required"` +} + +type GreetUserOutput struct { + Message string `json:"message"` +} + +func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { + if in.Name == "satan" { + return nil, errors.NewForbidden(nil, "go to hell") + } + return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil +} + +func main() { + tonic.SetErrorHook(jujerr.ErrHook) + r := gin.Default() + r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) + r.Run(":8080") +} +``` + +## OpenAPI / Swagger Documentation + +You can also easily serve auto-generated swagger documentation (using tonic data) with diff --git a/zesty/README b/zesty/README deleted file mode 100644 index b8e3deb..0000000 --- a/zesty/README +++ /dev/null @@ -1,23 +0,0 @@ -zesty is based on gorp, and abstracts DB transaction specifics. - -It abstracts DB and Tx objects through a unified interface: DBProvider, which in turn lets your function -remain ignorant of the current transaction state they get passed. - -It also manages nested transactions, with mid-Tx savepoints making partial rollbacks very easy. - -You can create a zesty.DB by calling NewDB(). -You can then register this DB by calling RegisterDB(). - -This lets you instantiate DBProviders for this DB with NewDBProvider(), which is the main -object that you manipulate. - -A DBProvider contains a DB instance, and provides Tx functionalities. - -You access the DB by calling provider.DB() - -By calling provider.Tx(), you create a new transaction. -Future calls to provider.DB() will provide the Tx instead of the main DB object, -allowing caller code to be completely ignorant of transaction context. - -Transactions can be nested infinitely, and each nesting level can be rolled back independantly. -Only the final commit will end the transaction and commit the changes to the DB. diff --git a/zesty/README.md b/zesty/README.md new file mode 100644 index 0000000..90cee1a --- /dev/null +++ b/zesty/README.md @@ -0,0 +1,19 @@ +# zesty + +Zesty is based on [gorp](https://github.com/go-gorp/gorp), and abstracts DB transaction specifics. + +It abstracts DB and Tx objects through a unified interface: DBProvider, which in turn lets your function remain ignorant of the current transaction state they get passed. + +It also manages nested transactions, with mid-Tx savepoints making partial rollbacks very easy. + +You can create a zesty.DB by calling NewDB(). You can then register this DB by calling RegisterDB(). + +This lets you instantiate DBProviders for this DB with NewDBProvider(), which is the main object that you manipulate. + +A DBProvider contains a DB instance, and provides Tx functionalities. + +You access the DB by calling provider.DB() + +By calling provider.Tx(), you create a new transaction. Future calls to provider.DB() will provide the Tx instead of the main DB object, allowing caller code to be completely ignorant of transaction context. + +Transactions can be nested infinitely, and each nesting level can be rolled back independantly. Only the final commit will end the transaction and commit the changes to the DB.