diff --git a/README.md b/README.md
index 47d31894..c1e01828 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-[](https://huma.rocks/) [](https://github.com/danielgtaylor/huma/actions?query=workflow%3ACI+branch%3Amaster++) [](https://codecov.io/gh/danielgtaylor/huma) [](https://pkg.go.dev/github.com/danielgtaylor/huma?tab=doc) [](https://goreportcard.com/report/github.com/danielgtaylor/huma)
+[](https://huma.rocks/) [](https://github.com/istreamlabs/huma/actions?query=workflow%3ACI+branch%3Amain++) [](https://codecov.io/gh/istreamlabs/huma) [](https://pkg.go.dev/github.com/istreamlabs/huma?tab=doc) [](https://goreportcard.com/report/github.com/istreamlabs/huma)
A modern, simple, fast & opinionated REST API framework for Go with batteries included. Pronounced IPA: [/'hjuːmɑ/](https://en.wiktionary.org/wiki/Wiktionary:International_Phonetic_Alphabet). The goals of this project are to provide:
@@ -14,8 +14,7 @@ A modern, simple, fast & opinionated REST API framework for Go with batteries in
Features include:
- HTTP, HTTPS (TLS), and [HTTP/2](https://http2.github.io/) built-in
- - Let's Encrypt auto-updating certificates via `--autotls`
-- Declarative interface on top of [Gin](https://github.com/gin-gonic/gin)
+- Declarative interface on top of [Chi](https://github.com/go-chi/chi)
- Operation & model documentation
- Request params (path, query, or header)
- Request body
@@ -25,17 +24,16 @@ Features include:
- Default (optional) middleware
- [RFC8631](https://tools.ietf.org/html/rfc8631) service description & docs links
- Automatic recovery from panics with traceback & request logging
- - Automatically handle CORS headers
- Structured logging middleware using [Zap](https://github.com/uber-go/zap)
- Automatic handling of `Prefer: return=minimal` from [RFC 7240](https://tools.ietf.org/html/rfc7240#section-4.2)
+ - [OpenTracing](https://opentracing.io/) for requests and errors
- Per-operation request size limits & timeouts with sane defaults
- [Content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation) between server and client
- - Support for GZip ([RFC 1952](https://tools.ietf.org/html/rfc1952)) & Brotli ([RFC 7932](https://tools.ietf.org/html/rfc7932)) content encoding via the `Accept-Encoding` header.
+ - Support for gzip ([RFC 1952](https://tools.ietf.org/html/rfc1952)) & Brotli ([RFC 7932](https://tools.ietf.org/html/rfc7932)) content encoding via the `Accept-Encoding` header.
- Support for JSON ([RFC 8259](https://tools.ietf.org/html/rfc8259)), YAML, and CBOR ([RFC 7049](https://tools.ietf.org/html/rfc7049)) content types via the `Accept` header.
- Annotated Go types for input and output models
- Generates JSON Schema from Go types
- Automatic input model validation & error handling
-- Dependency injection for loggers, datastores, etc
- Documentation generation using [RapiDoc](https://mrin9.github.io/RapiDoc/), [ReDoc](https://github.com/Redocly/redoc), or [SwaggerUI](https://swagger.io/tools/swagger-ui/)
- CLI built-in, configured via arguments or environment variables
- Set via e.g. `-p 8000`, `--port=8000`, or `SERVICE_PORT=8000`
@@ -43,269 +41,117 @@ Features include:
- Generates OpenAPI JSON for access to a rich ecosystem of tools
- Mocks with [API Sprout](https://github.com/danielgtaylor/apisprout)
- SDKs with [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator)
- - CLIs with [OpenAPI CLI Generator](https://github.com/danielgtaylor/openapi-cli-generator)
+ - CLI with [Restish](https://rest.sh/)
- And [plenty](https://openapi.tools/) [more](https://apis.guru/awesome-openapi3/category.html)
-This project was inspired by [FastAPI](https://fastapi.tiangolo.com/), [Gin](https://github.com/gin-gonic/gin), and countless others. Look at the [benchmarks](https://github.com/danielgtaylor/huma/tree/master/benchmark) to see how Huma compares.
+This project was inspired by [FastAPI](https://fastapi.tiangolo.com/). Look at the [benchmarks](https://github.com/istreamlabs/huma/tree/main/benchmark) to see how Huma compares.
Logo & branding designed by [Kari Taylor](https://www.kari.photography/).
-# Concepts & Example
+# Example
-REST APIs are composed of operations against resources and can include descriptions of various inputs and possible outputs. For each operation you will typically provide info like:
-
-- HTTP method & path (e.g. `GET /items/{id}`)
-- User-friendly description text
-- Input path, query, or header parameters
-- Input request body model, if appropriate
-- Response header names and descriptions
-- Response status code, content type, and output model
-
-Huma uses standard Go types and a declarative API to capture those descriptions in order to provide a combination of a simple interface and idiomatic code leveraging Go's speed and strong typing.
-
-Let's start by taking a quick look at a note-taking REST API. You can list notes, get a note's contents, create or update notes, and delete notes from an in-memory store. Each of the operations is registered with the router and descibes its inputs and outputs. You can view the full working example below:
+Here is a complete basic hello world example in Huma, that shows how to initialize a Huma app complete with CLI & default middleware, declare a resource with an operation, and define its handler function.
```go
package main
import (
"net/http"
- "sync"
- "time"
- "github.com/danielgtaylor/huma"
- "github.com/danielgtaylor/huma/schema"
+ "github.com/istreamlabs/huma"
+ "github.com/istreamlabs/huma/cli"
+ "github.com/istreamlabs/huma/responses"
)
-// NoteSummary is used to list notes. It does not include the (potentially)
-// large note content.
-type NoteSummary struct {
- ID string `json:"id" doc:"Note ID"`
- Created time.Time `json:"created" doc:"Created date/time as ISO8601"`
-}
-
-// Note records some content text for later reference.
-type Note struct {
- Created time.Time `json:"created" readOnly:"true" doc:"Created date/time as ISO8601"`
- Content string `json:"content" doc:"Note content"`
-}
-
-// We'll use an in-memory DB (a goroutine-safe map). Don't do this in
-// production code!
-var memoryDB = sync.Map{}
-
func main() {
- // Create a new router and give our API a title and version.
- r := huma.NewRouter("Notes API", "1.0.0",
- huma.DevServer("http://localhost:8888"),
- )
-
- notes := r.Resource("/v1/notes")
- notes.List("Returns a list of all notes", func() []*NoteSummary {
- // Create a list of summaries from all the notes.
- summaries := make([]*NoteSummary, 0)
-
- memoryDB.Range(func(k, v interface{}) bool {
- summaries = append(summaries, &NoteSummary{
- ID: k.(string),
- Created: v.(*Note).Created,
- })
- return true
- })
-
- return summaries
+ // Create a new router & CLI with default middleware.
+ app := cli.NewRouter("Minimal Example", "1.0.0")
+
+ // Declare the root resource and a GET operation on it.
+ app.Resource("/").Get("get-root", "Get a short text message",
+ // The only response is HTTP 200 with text/plain
+ responses.OK().ContentType("text/plain"),
+ ).Run(func(ctx huma.Context) {
+ // This is he handler function for the operation. Write the response.
+ ctx.Header().Set("Content-Type", "text/plain")
+ ctx.Write([]byte("Hello, world"))
})
- // Set up a custom schema to limit identifier values.
- idSchema := schema.Schema{Pattern: "^[a-zA-Z0-9._-]{1,32}$"}
-
- // Add an `id` path parameter to create a note resource.
- note := notes.With(huma.PathParam("id", "Note ID", huma.Schema(idSchema)))
-
- notFound := huma.ResponseError(http.StatusNotFound, "Note not found")
-
- note.Put("Create or update a note", func(id string, n *Note) bool {
- // Set the created time to now and then save the note in the DB.
- n.Created = time.Now()
- memoryDB.Store(id, n)
-
- // Empty responses don't have a body, so you can just return `true`.
- return true
- })
-
- note.With(notFound).Get("Get a note by its ID",
- func(id string) (*huma.ErrorModel, *Note) {
- if n, ok := memoryDB.Load(id); ok {
- // Note with that ID exists!
- return nil, n.(*Note)
- }
-
- return &huma.ErrorModel{
- Message: "Note " + id + " not found",
- }, nil
- },
- )
-
- note.With(notFound).Delete("Delete a note by its ID",
- func(id string) (*huma.ErrorModel, bool) {
- if _, ok := memoryDB.Load(id); ok {
- // Note with that ID exists!
- memoryDB.Delete(id)
- return nil, true
- }
-
- return &huma.ErrorModel{
- Message: "Note " + id + " not found",
- }, false
- },
- )
-
- // Run the app!
- r.Run()
+ // Run the CLI. When passed no arguments, it starts the server.
+ app.Run()
}
```
-Save this file as `notes/main.go`. Run it and then try to access the API with [HTTPie-Go](https://github.com/nojima/httpie-go) (or curl):
+You can test it with `go run hello.go` and make a sample request using [Restish](https://rest.sh/) (or `curl`). By default, Huma runs on port `8888`:
```sh
-# Grab reflex to enable reloading the server on code changes:
-$ go get github.com/cespare/reflex
-
-# Grab HTTPie-go for making requests
-$ go get -u github.com/nojima/httpie-go/cmd/ht
-
-# Run the server (default host/port is 0.0.0.0:8888, see --help for options)
-$ reflex -s go run notes/main.go
-
-# Make some requests (in another tab)
-$ ht put :8888/v1/notes/test1 content="Some content for note 1"
-HTTP/1.1 204 No Content
-Date: Sat, 07 Mar 2020 22:22:06 GMT
-
-$ ht put :8888/v1/notes/test2 content="Some content for note 2"
-HTTP/1.1 204 No Content
-Date: Sat, 07 Mar 2020 22:22:06 GMT
-
-# Parameter validation works too!
-$ ht put :8888/v1/notes/@bad content="Some content for an invalid note"
-HTTP/1.1 400 Bad Request
-Content-Length: 97
-Content-Type: application/json; charset=utf-8
-Date: Sat, 07 Mar 2020 22:22:06 GMT
-
-{
- "errors": [
- "(root): Does not match pattern '^[a-zA-Z0-9._-]{1,32}$'"
- ],
- "message": "Invalid input"
-}
-
-# List all the notes
-$ ht :8888/v1/notes
-HTTP/1.1 200 OK
-Content-Length: 122
-Content-Type: application/json; charset=utf-8
-Date: Sat, 07 Mar 2020 22:22:06 GMT
-
-[
- {
- "created": "2020-03-07T22:22:06-07:00",
- "id": "test1"
- },
- {
- "created": "2020-03-07T22:22:06-07:00",
- "id": "test2"
- }
-]
+# Get the message from the server
+$ restish :8888
+Hello, world
```
-The server works and responds as expected. There are also some neat extras built-in. If you go to http://localhost:8888/docs in a browser, you will see auto-generated interactive documentation:
-
-
-
-The documentation is generated from the OpenAPI 3 spec file that is available at http://localhost:8888/openapi.json. You can also access this spec without running the server:
+Even though the example is tiny you can also see some generated documentation at http://localhost:8888/docs.
-```sh
-# Save the OpenAPI 3 spec to a file.
-$ go run notes/main.go openapi notes.json
-```
-
-Combine the above with [openapi-cli-generator](https://github.com/danielgtaylor/openapi-cli-generator) and [huma-build](https://github.com/danielgtaylor/huma-build) and you get the following out of the box:
+See the examples directory for more complete examples.
-- Small, efficient deployment Docker image with your service
-- Auto-generated service documentation
-- Auto-generated SDKs for any language
-- Auto-generated cross-platform zero-dependency CLI
+- [Minimal](./examples/minimal/minimal.go) (a minimal "hello world")
+- [Echo](./examples/echo/echo.go) (echo input back to the user with validation)
+- [Notes](./examples/notes/notes.go) (note-taking API)
+- [Timeout](./examples/timeout/timeout.go) (show third-party request timing out)
+- [Test](./examples/test/service.go) (how to write a test)
# Documentation
-Official Go package documentation can always be found at https://pkg.go.dev/github.com/danielgtaylor/huma. Below is an introduction to the various features available in Huma.
+Official Go package documentation can always be found at https://pkg.go.dev/github.com/istreamlabs/huma. Below is an introduction to the various features available in Huma.
> :whale: Hi there! I'm the happy Huma whale here to provide help. You'll see me leave helpful tips down below.
-## Constructors & Options
+## The Router
-Huma uses the [functional options](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis) paradigm when creating a router, resource, operation, parameter, etc. Functional options were chosen due to an exponential explosion of constructor functions and the complexity of the problem space. They come with several advantages:
-
-- Friendly APIs with sane defaults
-- Extensible without breaking clients or polluting the global namespace with too many constructors
-- Options are immutable, reusable, and composable
-
-They are easy to use and look like this:
+The Huma router is the entrypoint to your service or application. There are a couple of ways to create it, depending on what level of customization you need.
```go
-// Add a parameter with an example
-huma.PathParam("id", "Resource identifier", huma.Example("abc123"))
-```
+// Simplest way to get started, which creats a router and a CLI with default
+// middleware attached. Note that the CLI is a router.
+app := cli.NewRouter("API Name", "1.0.0")
-Most text editors will auto-complete and show only the available options, which is an improvement over e.g. accepting `interface{}`.
+// Doing the same as above by hand:
+router := huma.New("API Name", "1.0.0")
+app := cli.New(router)
+middleware.Defaults(app)
-### Extending & Composition
-
-Functional options can be wrapped to extend the set of available options. For example:
-
-```go
-// IDParam creates a new path parameter limited to characters and a length that
-// is allowed for resource identifiers.
-func IDParam(name, description string) huma.DependencyOption {
- s := schema.Schema{Pattern: "^[a-zA-Z0-9_-]{3,20}"}
-
- return huma.PathParam(name, description, huma.Schema(s))
-}
+// Start the CLI after adding routes:
+app.Run()
```
-You can also compose multiple options into one, e.g by using `huma.ResourceOptions(..)` or one of the other related functions:
+You can also skip using the built-in `cli` package:
```go
-// CommonOptions sets up common options for every operation.
-func CommonOptions() huma.ResourceOption {
- return huma.ResourceOptions(
- huma.Tags("some-tag"),
- huma.HeaderParam("customer", "Customer name", "", huma.Internal()),
- huma.ResponseError(http.StatusInternalServerError, "Server error"),
- )
-}
+// Create and start a new router by hand:
+router := huma.New("API Name", "1.0.0")
+router.Middleware(middleware.DefaultChain)
+router.Listen("127.0.0.1:8888")
```
## Resources
-Huma APIs are composed of resources and sub-resources attached to a router. A resource refers to a unique URI on which operations can be performed. Huma resources can have dependencies, security requirements, parameters, response headers, and responses attached to them which are all applied to every operation and sub-resource.
+Huma APIs are composed of resources and sub-resources attached to a router. A resource refers to a unique URI on which operations can be performed. Huma resources can have middleware attached to them, which run before operation handlers.
```go
-r := huma.NewRouter("My API", "1.0.0")
+// Create a resource at a given path.
+notes := app.Resource("/notes")
-// Create a resource at a given path
-notes := r.Resource("/notes")
+// Add a middleware to all operations under `/notes`.
+notes.Middleware(MyMiddleware())
// Create another resource that includes a path parameter: /notes/{id}
-note := notes.With(huma.PathParam("id", "Note ID"))
+// Paths look like URI templates and use wrap parameters in curly braces.
+note := notes.SubResource("/{id}")
-// Create a sub-resource at /notes/{id}/likes
+// Create a sub-resource at /notes/{id}/likes.
sub := note.SubResource("/likes")
```
-The `With(...)` function is very powerful and can accept dependencies, security requirements, parameters, response headers, and response description options. It returns a copy of the resource with those values applied.
-
> :whale: Resources should be nouns, and plural if they return more than one item. Good examples: `/notes`, `/likes`, `/users`, `/videos`, etc.
## Operations
@@ -313,7 +159,6 @@ The `With(...)` function is very powerful and can accept dependencies, security
Operations perform an action on a resource using an HTTP method verb. The following verbs are available:
- Head
-- List (alias for Get)
- Get
- Post
- Put
@@ -321,168 +166,289 @@ Operations perform an action on a resource using an HTTP method verb. The follow
- Delete
- Options
-Operations can take dependencies, parameters, & request bodies and produce response headers and responses. These are each discussed in more detail below.
-
-If you don't provide a response description, then one is generated for you based on the response type with the following rules:
-
-- Boolean: If true, returns `HTTP 204 No Content`
-- String: If not empty, returns `HTTP 200 OK` with content type `text/plain`
-- Slice, map, struct pointer: If not `nil`, marshal to JSON and return `HTTP 200 OK` with content type `application/json`
+Operations can take inputs in the form of path, query, and header parameters and/or request bodies. They must declare what response status codes, content types, and structures they return.
-If you need any customization beyond the above then you must provide a response description.
+Every operation has a handler function and takes at least a `huma.Context`, described in further detail below:
```go
-r := huma.NewRouter("My API", "1.0.0")
-
-// Create a resource
-notes := r.Resource("/notes")
-
-// Create the operation with an auto-generated response.
-notes.Get("Get a list of all notes", func () []*NoteSummary {
- // Implementation goes here
-})
-
-// Manually provide the response. This is equivalent to the above, but allows
-// you to add additional options like allowed response headers.
-notes.With(
- huma.ResponseJSON(http.StatusOK, "Success"),
-).Get("Get a list of all notes", func () []*NoteSummary {
- // Implementation goes here
+app.Resource("/op").Get("get-op", "Example operation",
+ // Response declaration goes here!
+).Run(func (ctx huma.Context) {
+ // Handler implementation goes here!
})
```
> :whale: Operations map an HTTP action verb to a resource. You might `POST` a new note or `GET` a user. Sometimes the mapping is less obvious and you can consider using a sub-resource. For example, rather than unliking a post, maybe you `DELETE` the `/posts/{id}/likes` resource.
-## Handler Functions
+## Context
-The basic structure of a Huma handler function looks like this, with most arguments being optional and dependent on the declaritively described operation:
+As seen above, every handler function gets at least a `huma.Context`, which combines an `http.ResponseWriter` for creating responses, a `context.Context` for cancellation/timeouts, and some convenience functions. Any library that can use either of these interfaces will work with a Huma context object. Some examples:
```go
-func (deps..., params..., requestModel) (headers..., responseModels...)
+// Calling third-party libraries that might take too long
+results := mydb.Fetch(ctx, "some query")
+
+// Write an HTTP response
+ctx.Header().Set("Content-Type", "text/plain")
+ctx.WriteHeader(http.StatusNotFound)
+ctx.Write([]byte("Could not find foo"))
```
-Dependencies, parameters, headers, and models are covered in more detail in the following sections. For now this gives an idea of how to write handler functions based on the inputs and outputs of your operation.
+> :whale: Since you can write data to the response multiple times, the context also supports streaming responses. Just remember to set (or remove) the timeout.
+
+## Responses
-For example, the most basic "Hello world" that takes no parameters and returns a greeting message might look like this:
+In order to keep the documentation & service specification up to date with the code, you **must** declare the responses that your handler may return. This includes declaring the content type, any headers it might return, and what model it returns (if any). The `responses` package helps with declaring well-known responses with the right code/docs/model and corresponds to the statuses in the `http` package, e.g. `resposes.OK()` will create a response with the `http.StatusOK` status code.
```go
-func () string { return "Hello, world" }
-```
+// Response structures are just normal Go structs
+type Thing struct {
+ Name string `json:"name"`
+}
-Another example: you have an `id` parameter input and return a response model to be marshalled as JSON:
+// ... initialization code goes here ...
+
+things := app.Resource("/things")
+things.Get("list-things", "Get a list of things",
+ // Declare a successful response that returns a slice of things
+ responses.OK().Headers("Foo").Model([]Thing{}),
+ // Errors automatically set the right status, content type, and model for you.
+ responses.InternalServerError(),
+).Run(func(ctx huma.Context) {
+ // This works because the `Foo` header was declared above.
+ ctx.Header().Set("Foo", "Some value")
+
+ // The `WriteModel` convenience method handles content negotiation and
+ // serializaing the response for you.
+ ctx.WriteModel(http.StatusOK, []Thing{
+ Thing{Name: "Test1"},
+ Thing{Name: "Test2"},
+ })
-```go
-func (id string) *MyModel { return &MyModel{ID: id} }
+ // Alternatively, you can write an error
+ ctx.WriteError(http.StatusInternalServerError, "Some message")
+})
```
-> :whale: Confused about what a handler should look like? Just run your service and it'll print out an approximate handler function when it panics.
+If you try to set a response status code or header that was not declared you will get a runtime error. If you try to call `WriteModel` or `WriteError` more than once then you will get an error because the writer is considered closed after those methods.
-## Parameters
+### Errors
-Huma supports three types of parameters:
+Errors use [RFC 7807](https://tools.ietf.org/html/rfc7807) and return a structure that looks like:
-- Required path parameters, e.g. `/things/{thingId}`
-- Optional query string parameters, e.g. `/things?q=filter`
-- Optional header parameters, e.g. `X-MyHeader: my-value`
+```json
+{
+ "status": 504,
+ "title": "Gateway Timeout",
+ "detail": "Problem with HTTP request",
+ "errors": [
+ {
+ "message": "Get \"https://httpstat.us/418?sleep=5000\": context deadline exceeded"
+ }
+ ]
+}
+```
-Optional parameters require a default value.
+The `errors` field is optional and may contain more details about which specific errors occurred.
-Here is an example of an `id` parameter:
+It is recommended to return exhaustive errors whenever possible to prevent user frustration with having to keep retrying a bad request and getting back a different error. The context has `AddError` and `HasError()` functions for this:
```go
-r.Resource("/notes",
- huma.PathParam("id", "Note ID"),
- huma.ResponseError(404, "Note was not found"),
- huma.ResponseJSON(200, "Success"),
-).
-Get("Get a note by its ID", func(id string) (*huma.ErrorModel, *Note) {
- // Implementation goes here
+app.Resource("/exhaustive").Get("exhaustive", "Exhastive errors example",
+ responses.OK(),
+ responses.BadRequest(),
+).Run(func(ctx huma.Context) {
+ for i := 0; i < 5; i++ {
+ // Use AddError to add multiple error details to the response.
+ ctx.AddError(fmt.Errorf("Error %d", i))
+ }
+
+ // Check if the context has had any errors added yet.
+ if ctx.HasError() {
+ // Use WriteError to set the actual status code, top-level message, and
+ // any additional errors. This sends the response.
+ ctx.WriteError(http.StatusBadRequest, "Bad input")
+ return
+ }
})
```
-You can also declare parameters with additional validation logic by using the `schema` module:
+## Request Inputs
+
+Requests can have parameters and/or a body as input to the handler function. Like responses, inputs use standard Go structs but the tags are different. Here are the available tags:
+
+| Tag | Description | Example |
+| -------- | ---------------------------------- | ------------------------ |
+| `path` | Name of the path parameter | `path:"thing-id"` |
+| `query` | Name of the query string parameter | `query:"q"` |
+| `header` | Name of the header parameter | `header:"Authorization"` |
+
+The following types are supported out of the box:
+
+| Type | Example Inputs |
+| ------------------- | ---------------------- |
+| `bool` | `true`, `false` |
+| `[u]int[16/32/64]` | `1234`, `5`, `-1` |
+| `float32/64` | `1.234`, `1.0` |
+| `string` | `hello`, `t` |
+| `time.Time` | `2020-01-01T12:00:00Z` |
+| slice, e.g. `[]int` | `1,2,3`, `tag1,tag2` |
+
+For example, if the parameter is a query param and the type is `[]string` it might look like `?tags=tag1,tag2` in the URI.
+
+The special struct field `Body` will be treated as the input request body and can refer to another struct or you can embed a struct inline.
+
+Here is an example:
```go
-s := schema.Schema{
- MinLength: 1,
- MaxLength: 32,
+type MyInputBody struct {
+ Name string `json:"name"`
+}
+
+type MyInput struct {
+ ThingID string `path:"thing-id" doc:"Example path parameter"`
+ QueryParam int `query:"q" doc:"Example query string parameter"`
+ HeaderParam string `header:"Foo" doc:"Example header parameter"`
+ Body MyInputBody `doc:"Example request body"`
}
-huma.PathParam("id", "Note ID", huma.Schema(s))
+// ... Later you use the inputs
+
+// Declare a resource with a path parameter that matches the input struct. This
+// is needed because path parameter positions matter in the URL.
+thing := app.Resource("/things/{thing-id}")
+
+// Next, declare the handler with an input argument.
+thing.Get("get-thing", "Get a single thing",
+ responses.NoContent(),
+).Run(func(ctx huma.Context, input MyInput) {
+ fmt.Printf("Thing ID: %s\n", input.ThingID)
+ fmt.Printf("Query param: %s\n", input.QueryParam)
+ fmt.Printf("Header param: %s\n", input.HeaderParam)
+ fmt.Printf("Body name: %s\n", input.Body.Name)
+})
```
-Once a parameter is declared it will get parsed, validated, and then sent to your handler function. If parsing or validation fails, the client gets a 400-level HTTP error.
+Try a request against the service like:
-> :whale: If a proxy is providing e.g. authentication or rate-limiting and exposes additional internal-only information then use the internal parameters like `huma.HeaderParam("UserID", "Parsed user from the auth system", "nobody", huma.Internal())`. Internal parameters are never included in the generated OpenAPI 3 spec or documentation.
+```sh
+# Restish example
+$ restish :8888/things/abc123?q=3 -H "Foo: bar" name: Kari
+```
-## Request & Response Models
+### Parameter & Body Validation
-Request and response models are just plain Go structs with optional tags to annotate additional validation logic for their fields. From the notes API example above:
+All supported JSON Schema tags work for parameters and body fields. Validation happens before the request handler is called, and if needed an error response is returned. For example:
```go
-// Note records some content text for later reference.
-type Note struct {
- Created time.Time `readOnly:"true"`
- Content string
+type MyInput struct {
+ ThingID string `path:"thing-id" pattern:"^th-[0-9a-z]+$" doc:"..."`
+ QueryParam int `query:"q" minimum:"1" doc:"..."`
}
```
-The `Note` struct has two fields which will get serialized to JSON. The `Created` field has a special tag `readOnly` set which means it will not get used for write operations like `PUT /notes/{id}`.
+See "Validation" for more info.
-This struct provides enough information to create JSON Schema for the OpenAPI 3 spec. You can provide as much or as little information and validation as you like.
+### Input Composition
-### Request Model
-
-Request models are used by adding a new input argument that is a pointer to a struct to your handler function as the last argument. For example:
+Because inputs are just Go structs, they are composable and reusable. For example:
```go
-r.Resource("/notes", huma.PathParam("id", "Note ID")).
- Put("Create or update a note",
- // Handler without an input body looks like:
- func(id string) bool {
- // Implementation goes here
- },
+type AuthParam struct {
+ Authorization string `header:"Authorization"`
+}
+
+type PaginationParams struct {
+ Cursor string `query:"cursor"`
+ Limit int `query:"limit"`
+}
- // Handler with an input body looks like:
- func(id string, note *Note) bool {
- // Implementation goes here
- },
- )
+// ... Later in the code
+app.Resource("/things").Get("list-things", "List things",
+ responses.NoContent(),
+).Run(func (ctx huma.Context, input struct {
+ AuthParam
+ PaginationParams
+}) {
+ fmt.Printf("Auth: %s, Cursor: %s, Limit: %d\n", input.Authorization, input.Cursor, input.Limit)
+})
```
-The presence of the `note *Note` argument tells Huma to parse the request body and validate it against the generated JSON Schema for the `Note` struct.
+### Input Streaming
+
+It's possible to support input body streaming for large inputs by declaring your body as an `io.Reader`:
-### Response Model
+```go
+type StreamingBody struct {
+ Body io.Reader
+}
+```
-Response models are used by adding a response to the list of possible responses along with a new function return value that is a pointer to your struct. You can specify multiple different response models.
+You probably want to combine this with custom timeouts, or removing them altogether.
```go
-r.Resource("/notes",
- huma.ResponseError(http.NotFound, "Not found"),
- huma.ResponseJSON(http.StatusOK, "Success")).
-Get("Description", func() (*huma.ErrorModel, *Note) {
- // Implementation goes here
-})
+op := app.Resource("/streaming").Post("post-stream", "Write streamed data",
+ responses.NoContent(),
+)
+op.NoBodyReadTimeout()
+op.Run(...)
```
-Whichever model is not `nil` will get sent back to the client.
+### Resolvers
+
+Sometimes the built-in validation isn't sufficient for your use-case, or you want to do something more complex with the incoming request object. This is where resolvers come in.
-Empty responses, e.g. a `204 No Content` or `304 Not Modified` are also supported by setting a `ContentType` of `""` (the default zero value). Use `huma.Response` paired with a simple boolean to return a response without a body. Passing `false` acts like `nil` for models and prevents that response from being sent.
+Any input struct can be a resolver by implementing the `huma.Resolver` interface, including embedded structs. Each resolver takes the current context and the incoming request. For example:
```go
-r.Resource("/notes",
- huma.Response(http.StatusNoContent, "This should have no body")).
-Get("description", func() bool {
- return true
+// MyInput demonstrates inputs/transformation
+type MyInput struct {
+ Host string
+ Name string `query:"name"`
+}
+
+func (m *MyInput) Resolve(ctx huma.Context, r *http.Request) {
+ // Get request info you don't normally have access to.
+ m.Host = r.Host
+
+ // Transformations or other data validation
+ m.Name = strings.Title(m.Name)
+}
+
+// Then use it like any other input struct:
+app.Resource("/things").Get("list-things", "Get a filtered list of things",
+ responses.NoContent(),
+).Run(func(ctx huma.Context, input MyInput) {
+ fmt.Printf("Host: %s\n", input.Host)
+ fmt.Printf("Name: %s\n", input.Name)
})
-},
```
-> :whale: In some cases Huma can [auto-generate a resonse model](#operations) for you.
+It is recommended that you do not save the request. Whenever possible, use existing mechanisms for describing your input so that it becomes part of the OpenAPI description.
-### Model Tags
+#### Resolver Errors
-Go struct tags are used to annotate the model with information that gets turned into [JSON Schema](https://json-schema.org/) for documentation and validation.
+Resolvers can set errors as needed and Huma will automatically return a 400-level error response before calling your handler. This makes resolvers a good place to run additional complex validation steps so you can provide the user with a set of exhaustive errors.
+
+```go
+type MyInput struct {
+ Host string
+}
+
+func (m *MyInput) Resolve(ctx huma.Context, r *http.Request) {
+ if m.Host = r.Hostname; m.Host == "localhost" {
+ ctx.AddError(&huma.ErrorDetail{
+ Message: "Invalid value!",
+ Location: "request.host",
+ Value: m.Host,
+ })
+ }
+}
+```
+
+## Validation
+
+Go struct tags are used to annotate inputs/output structs with information that gets turned into [JSON Schema](https://json-schema.org/) for documentation and validation.
The standard `json` tag is supported and can be used to rename a field and mark fields as optional using `omitempty`. The following additional tags are supported on model fields:
@@ -511,155 +477,75 @@ The standard `json` tag is supported and can be used to rename a field and mark
| `writeOnly` | Sent in the request only | `writeOnly:"true"` |
| `deprecated` | This field is deprecated | `deprecated:"true"` |
-### Response Headers
-
-Response headers must be defined before they can be sent back to a client. This includes several steps:
-
-1. Describe the response header (name & description)
-2. Specify which responses may send this header
-3. Add the header to the handler function return values
-
-For example:
+Parameters have some additional validation tags:
-```go
-r.Resource("/notes",
- huma.ResponseHeader("expires", "Expiration date for this content"),
- huma.ResponseText(http.StatusOK, "Success", huma.Headers("expires"))
-).Get("description", func() (string, string) {
- expires := time.Now().Add(7 * 24 * time.Hour).MarshalText()
- return expires, "Hello!"
-})
-```
-
-You can make use of named return values with a naked return to disambiguate complex functions:
-
-```go
-func() (expires string, message string) {
- expires = time.Now().Add(7 * 24 * time.Hour).MarshalText()
- message = "Hello!"
- return
-},
-```
-
-> :whale: If you forget to declare a response header for a particular response and then try to set it when returning that response it will **not** be sent to the client and an error will be logged.
-
-## Dependencies
+| Tag | Description | Example |
+| ---------- | ------------------------------ | ----------------- |
+| `internal` | Internal-only (not documented) | `internal:"true"` |
-Huma includes a dependency injection system that can be used to pass additional arguments to operation handler functions. You can register global dependencies (ones that do not change from request to request) or contextual dependencies (ones that change with each request).
-
-Global dependencies are created by just setting some value, while contextual dependencies are implemented using a function that returns the value of the form `func (deps..., params...) (headers..., *YourType, error)` where the value you want injected is of `*YourType` and the function arguments can be any previously registered dependency types or one of the hard-coded types:
+## Middleware
-- `huma.ConnDependency()` the current `http.Request` connection (returns `net.Conn`)
-- `huma.ContextDependency()` the current `http.Request` context (returns `context.Context`)
-- `huma.GinContextDependency()` the current Gin request context (returns `*gin.Context`)
-- `huma.OperationIDDependency()` the current operation ID (returns `string`)
+Standard [Go HTTP middleware](https://justinas.org/writing-http-middleware-in-go) is supported. It can be attached to the main router/app or to individual resources, but **must** be added _before_ operation handlers are added.
```go
-// Register a new database connection dependency
-db := huma.SimpleDependency(db.NewConnection())
-
-// Register a new request logger dependency. This is contextual because we
-// will print out the requester's IP address with each log message.
-type MyLogger struct {
- Info: func(msg string),
-}
-
-logger := huma.Dependency(
- huma.GinContextDependency(),
- func(c *gin.Context) (*MyLogger, error) {
- return &MyLogger{
- Info: func(msg string) {
- fmt.Printf("%s [ip:%s]\n", msg, c.Request.RemoteAddr)
- },
- }, nil
- },
-)
+// Middleware from some library
+app.Middleware(somelibrary.New())
-// Use them in any handler by adding them to both `Depends` and the list of
-// handler function arguments.
-r.Resource("/foo").With(
- db, logger
-).Get("doc", func(db *db.Connection, log *MyLogger) string {
- log.Info("test")
- item := db.Fetch("query")
- return item.ID
-})
-```
+// Custom middleware
+app.Middleware(func(next http.Handler) http.Handler {
+ return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
+ // Request phase, do whatever you want before next middleware or handler
+ // gets called.
+ fmt.Println("Request coming in")
-When creating a new dependency you can use `huma.DependencyOptions` to group multiple options:
+ // Call the next middleware/handler
+ next.ServeHTTP(w, r)
-```go
-logger := huma.Dependency(huma.DependencyOptions(
- huma.GinContextDependency(),
- huma.OperationIDDependency(),
-), func (c *gin.Context, operationID string) (*MyLogger, error) {
- return ...
+ // Response phase, after handler has run.
+ fmt.Println("Response going out!")
+ })
})
```
-> :whale: Note that global dependencies cannot be functions. You can wrap them in a struct as a workaround if needed.
-
-## Custom Gin
-
-You can create a Huma router instance with a custom Gin instance. This lets you set up custom middleware, CORS configurations, logging, etc.
-
-```go
-// The following two are equivalent:
-// Default settings:
-r := huma.NewRouter("My API", "1.0.0")
-
-// And manual settings:
-g := gin.New()
-g.Use(huma.Recovery())
-g.Use(huma.LogMiddleware())
-g.Use(cors.Default())
-g.Use(huma.PreferMinimalMiddleware())
-g.Use(huma.ServiceLinkMiddleware())
-g.NoRoute(huma.Handler404())
-r := huma.NewRouter("My API", "1.0.0", huma.WithGin(g))
-```
+When using the `cli.NewRouter` convenience method, a set of default middleware is added for you. See `middleware.DefaultChain` for more info.
-## Custom CORS Handler
+### Enabling OpenTracing
-If you would like CORS preflight requests to allow specific headers, do the following:
+[OpenTracing](https://opentracing.io/) support is built-in, but you have to tell the global tracer where to send the information, otherwise it acts as a no-op. For example, if you use [DataDog APM](https://www.datadoghq.com/blog/opentracing-datadog-cncf/) and have the agent configured wherever you deploy your service:
```go
-// CORS: Allow non-standard headers "Authorization" and "X-My-Header" in preflight requests
-cfg := cors.DefaultConfig()
-cfg.AllowAllOrigins = true
-cfg.AllowHeaders = append(cfg.AllowHeaders, "Authorization", "X-My-Header")
-
-// And manual settings:
-r := huma.NewRouter("My API", "1.0.0", huma.CORSHandler(cors.New(cfg)))
-```
+import (
+ "github.com/opentracing/opentracing-go"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentracer"
+ "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
+)
-## Custom HTTP Server
+func main() {
+ t := opentracer.New(tracer.WithAgentAddr("host:port"))
+ defer tracer.Stop()
-You can have full control over the `http.Server` that is created.
+ // Set it as a Global Tracer
+ opentracing.SetGlobalTracer(t)
-```go
-// Set low timeouts to kick off slow clients.
-s := &http.Server{
- ReadTimeout: 5 * time.Seconds,
- WriteTimeout: 5 * time.Seconds,
- Handler: r
+ app := cli.NewRouter("My Cool Service", "1.0.0")
+ // register routes here
+ app.Run()
}
-
-r := huma.NewRouter("My API", "1.0.0", huma.HTTPServer(s))
-
-r.Run()
```
-### Timeouts, Deadlines, & Cancellation
+### Timeouts, Deadlines, Cancellation & Limits
-By default, only a `ReadHeaderTimeout` of _10 seconds_ and an `IdleTimeout` of _15 seconds_ are set at the server level. This allows large request and response bodies to be sent without fear of timing out in the default config, as well as the use of WebSockets.
+Huma provides utilities to prevent long-running handlers and issues with huge request bodies and slow clients with sane defaults out of the box.
+
+#### Context Timeouts
Set timeouts and deadlines on the request context and pass that along to libraries to prevent long-running handlers. For example:
```go
-r.Resource("/timeout",
- huma.ContextDependency(),
-).Get("timeout example", func(ctx context.Context) string {
+app.Resource("/timeout").Get("timeout", "Timeout example",
+ responses.String(http.StatusOK),
+ responses.GatewayTimeout(),
+).Run(func(ctx huma.Context) {
// Add a timeout to the context. No request should take longer than 2 seconds
newCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
@@ -672,119 +558,144 @@ r.Resource("/timeout",
// deadline of 2 seconds is shorter than the request duration of 5 seconds.
_, err := http.DefaultClient.Do(req)
if err != nil {
- return err.Error()
+ ctx.WriteError(http.StatusGatewayTimeout, "Problem with HTTP request", err)
+ return
}
- return "success"
+ ctx.Write([]byte("success!"))
})
```
-### Request Body Timeouts
+#### Request Timeouts
+
+By default, a `ReadHeaderTimeout` of _10 seconds_ and an `IdleTimeout` of _15 seconds_ are set at the server level and apply to every incoming request.
-By default any handler which takes in a request body parameter will have a read timeout of 15 seconds set on it. If set to nonzero for a handler which does **not** take a body, then the timeout will be set on the underlying connection before calling your handler.
+Each operation's individual read timeout defaults to _15 seconds_ and can be changed as needed. This enables large request and response bodies to be sent without fear of timing out, as well as the use of WebSockets, in an opt-in fashion with sane defaults.
-When triggered, the server sends a 408 Request Timeout as JSON with a message containing the time waited.
+When using the built-in model processing and the timeout is triggered, the server sends an error as JSON with a message containing the time waited.
```go
type Input struct {
- ID string
+ ID string `json:"id"`
}
-r := huma.NewRouter("My API", "1.0.0")
+app := cli.NewRouter("My API", "1.0.0")
+foo := app.Resource("/foo")
// Limit to 5 seconds
-r.Resource("/foo", huma.BodyReadTimeout(5 * time.Second)).Post(
- "Create item", func(input *Input) string {
- return "Hello, " + input.ID
- })
+create := foo.Post("create-item", "Create a new item",
+ responses.NoContent(),
+)
+create.BodyReadTimeout(5 * time.Second)
+create.Run(func (ctx huma.Context, input Input) {
+ // Do something here.
+})
```
You can also access the underlying TCP connection and set deadlines manually:
```go
-r.Resource("/foo", huma.ConnDependency()).Get(func (conn net.Conn) string {
+create.Run(func (ctx huma.Context, input struct {
+ Body io.Reader
+}) {
+ // Get the connection.
+ conn := huma.GetConn(ctx)
+
// Set a new deadline on connection reads.
conn.SetReadDeadline(time.Now().Add(600 * time.Second))
// Read all the data from the request.
- data, err := ioutil.ReadAll(c.Request.Body)
+ data, err := ioutil.ReadAll(input.Body)
if err != nil {
+ // If a timeout occurred, this will be a net.Error with `err.Timeout()`
+ // returning true.
panic(err)
}
- // Do something with the data...
- return fmt.Sprintf("Read %d bytes", len(data))
+ // Do something with data here...
})
```
-> :whale: Set to `-1` in order to disable the timeout.
+> :whale: Use `NoBodyReadTimeout()` to disable the default.
-### Request Body Size Limits
+#### Request Body Size Limits
By default each operation has a 1 MiB reqeuest body size limit.
-When triggered, the server sends a 413 Request Entity Too Large as JSON with a message containing the maximum body size for this operation.
+When using the built-in model processing and the timeout is triggered, the server sends an error as JSON with a message containing the maximum body size for this operation.
```go
-r := huma.NewRouter("My API", "1.0.0")
+app := cli.NewRouter("My API", "1.0.0")
+create := app.Resource("/foo").Post("create-item", "Create a new item",
+ responses.NoContent(),
+)
// Limit set to 10 MiB
-r.Resource("/foo", MaxBodyBytes(10 * 1024 * 1024)).Get(...)
+create.MaxBodyBytes(10 * 1024 * 1024)
+create.Run(func (ctx huma.Context, input Input) {
+ // Body is guaranteed to be 10MiB or less here.
+})
```
-> :whale: Set to `-1` in order to disable the check, allowing for unlimited request body size for e.g. large streaming file uploads.
+> :whale: Use `NoMaxBodyBytes()` to disable the default.
## Logging
-Huma provides a Zap-based contextual structured logger built-in. You can access it via the `huma.LogDependency()` which returns a `*zap.SugaredLogger`. It requires the use of the `huma.LogMiddleware(...)`, which is included by default. If you provide a custom Gin instance you should include the middleware.
+Huma provides a Zap-based contextual structured logger as part of the default middleware stack. You can access it via the `middleware.GetLogger(ctx)` which returns a `*zap.SugaredLogger`. It requires the use of the `middleware.Logger`, which is included by default when using either `cli.NewRouter` or `middleware.Defaults`.
```go
-r.Resource("/test",
- huma.LogDependency(),
-).Get("Logger test", func(log *zap.SugaredLogger) string {
- log.Info("I'm using the logger!")
- return "Hello, world"
+app := cli.NewRouter("Logging Example", "1.0.0")
+
+app.Resource("/log").Get("log", "Log example",
+ responses.NoContent(),
+).Run(func (ctx huma.Context) {
+ logger := middleware.GetLogger(ctx)
+ logger.Info("Hello, world!")
})
```
-## Customizing Logging
-
-Logging is completely customizable.
+Manual setup:
```go
-// Create your own logger, or use the Huma built-in:
-l, err := huma.NewLogger()
-if err != nil {
- panic(err)
-}
+router := huma.New("Loggin Example", "1.0.0")
+app := cli.New(router)
+
+app.Middleware(middleware.Logger)
+middleware.AddLoggerOptions(app)
+
+// Rest is same as above...
+```
+
+You can also modify the base logger as needed. Set this up _before_ adding any routes. Note that the function returns a low-level `Logger`, not a `SugaredLogger`.
-// Update the logger somehow with your custom logic.
-l = l.With(zap.String("some", "value"))
+```go
+middleware.NewLogger = func() (*zap.Logger, error) {
+ l, err := middleware.NewDefaultLogger()
+ if err != nil {
+ return nil, err
+ }
-// Set up the router with the default settings and your custom logger.
-g := gin.New()
-g.Use(gin.Recovery())
-g.Use(cors.Default())
-g.Use(huma.LogMiddleware(l))
+ // Add your own global tags.
+ l = l.With(zap.String("env", "prod"))
-r := huma.NewRouter("My API", "1.0.0", huma.WithGin(g))
+ return l, nil
+}
```
## Lazy-loading at Server Startup
-You can register functions to run before the server starts, allowing for things like lazy-loading dependencies.
+You can register functions to run before the server starts, allowing for things like lazy-loading dependencies. It is safe to call this method multiple times.
```go
var db *mongo.Client
-r := huma.NewRouter("My API", "1.0.0",
- huma.PreStart(func() {
- // Connect to the datastore
- var err error
- db, err = mongo.Connect(context.Background(),
- options.Client().ApplyURI("..."))
- })
-)
+app := cli.NewRouter("My API", "1.0.0")
+app.PreStart(func() {
+ // Connect to the datastore
+ var err error
+ db, err = mongo.Connect(context.Background(),
+ options.Client().ApplyURI("..."))
+})
```
> :whale: This is especially useful for external dependencies and if any custom CLI commands are set up. For example, you may not want to require a database to run `my-service openapi my-api.json`.
@@ -794,50 +705,45 @@ r := huma.NewRouter("My API", "1.0.0",
You can choose between [RapiDoc](https://mrin9.github.io/RapiDoc/), [ReDoc](https://github.com/Redocly/redoc), or [SwaggerUI](https://swagger.io/tools/swagger-ui/) to auto-generate documentation. Simply set the documentation handler on the router:
```go
-r := huma.NewRouter("My API", "1.0.0", huma.DocsHandler(huma.ReDocHandler("My API")))
+app := cli.NewRouter("My API", "1.0.0")
+app.DocsHandler(huma.ReDocHandler("My API"))
```
> :whale: Pass a custom handler function to have even more control for branding or browser authentication.
## Custom OpenAPI Fields
-You can set custom OpenAPI fields via the `Extra` field in the `OpenAPI` and `Operation` structs.
-
-```go
-r := huma.NewRouter("My API", "1.0.0", huma.Extra(map[string]interface{}{
- "x-something": "some-value",
-}))
-```
-
-Use the OpenAPI hook for additional customization. It gives you a `*gab.Container` instance that represents the root of the OpenAPI document.
+Use the OpenAPI hook for OpenAPI customization. It gives you a `*gabs.Container` instance that represents the root of the OpenAPI document.
```go
func modify(openapi *gabs.Container) {
openapi.Set("value", "paths", "/test", "get", "x-foo")
}
-r := huma.NewRouter("My API", "1.0.0", huma.OpenAPIHook(modify))
+app := cli.NewRouter("My API", "1.0.0")
+app.OpenAPIHook(modify)
```
-> :whale: See the [OpenAPI 3 spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md) for everything that can be set.
+> :whale: See the [OpenAPI 3 spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md) for everything that can be set.
## Custom CLI Arguments
-You can add additional CLI arguments, e.g. for additional logging tags. Use the `AddGlobalFlag` function along with the `viper` module to get the parsed value.
+The `cli` package provides a convenience layer to create a simple CLI for your server, which lets a user set the host, port, TLS settings, etc when running your service.
+
+You can add additional CLI arguments, e.g. for additional logging tags. Use the `Flag` method along with the `viper` module to get the parsed value.
```go
-r := huma.NewRouter("My API", "1.0.0",
- // Add a long arg (--env), short (-e), description & default
- huma.GlobalFlag("env", "e", "Environment", "local")
-)
+app := cli.NewRouter("My API", "1.0.0")
-r.Resource("/current_env").Text(http.StatusOK, "Success").Get(
- "Return the current environment",
- func() string {
- // The flag is automatically bound to viper settings.
- return viper.GetString("env")
- },
-)
+// Add a long arg (--env), short (-e), description & default
+app.Flag("env", "e", "Environment", "local")
+
+r.Resource("/current_env").Get("get-env", "Get current env",
+ responses.String(http.StatusOK),
+).Run(func(ctx huma.Context) {
+ // The flag is automatically bound to viper settings using the same name.
+ ctx.Write([]byte(viper.GetString("env")))
+})
```
Then run the service:
@@ -850,49 +756,43 @@ $ go run yourservice.go --env=prod
## Custom CLI Commands
-You can access the root `cobra.Command` via `r.Root()` and add new custom commands via `r.Root().AddCommand(...)`. The `openapi` sub-command is one such example in the default setup.
-
-> :whale: You can also overwite `r.Root().Run` to completely customize how you run the server.
-
-## Middleware
-
-You can make use of any Gin-compatible middleware via the `GinMiddleware()` router option.
-
-```go
-r := huma.NewRouter("My API", "1.0.0", huma.GinMiddleware(gin.Logger()))
-```
-
-## HTTP/2 Setup
+You can access the root `cobra.Command` via `app.Root()` and add new custom commands via `app.Root().AddCommand(...)`. The `openapi` sub-command is one such example in the default setup.
-TODO
+> :whale: You can also overwite `app.Root().Run` to completely customize how you run the server. Or just ditch the `cli` package completely.
## Testing
The Go standard library provides useful testing utilities and Huma routers implement the [`http.Handler`](https://golang.org/pkg/net/http/#Handler) interface they expect. Huma also provides a `humatest` package with utilities for creating test routers capable of e.g. capturing logs.
-You can see an example in the [`examples/test`](https://github.com/danielgtaylor/huma/tree/master/examples/test) directory:
+You can see an example in the [`examples/test`](https://github.com/istreamlabs/huma/tree/main/examples/test) directory:
```go
package main
-import "github.com/danielgtaylor/huma"
+import (
+ "github.com/istreamlabs/huma"
+ "github.com/istreamlabs/huma/cli"
+ "github.com/istreamlabs/huma/responses"
+)
func routes(r *huma.Router) {
// Register a single test route that returns a text/plain response.
- r.Resource("/test").Get("Test route", func() string {
- return "Hello, test!"
+ r.Resource("/test").Get("test", "Test route",
+ responses.OK().ContentType("text/plain"),
+ ).Run(func(ctx huma.Context) {
+ ctx.Write([]byte("Hello, test!"))
})
}
func main() {
// Create the router.
- r := huma.NewRouter("Test", "1.0.0")
+ app := cli.NewRouter("Test", "1.0.0")
// Register routes.
- routes(r)
+ routes(app.Router)
// Run the service.
- r.Run()
+ app.Run()
}
```
@@ -904,7 +804,7 @@ import (
"net/http/httptest"
"testing"
- "github.com/danielgtaylor/huma/humatest"
+ "github.com/istreamlabs/huma/humatest"
"github.com/stretchr/testify/assert"
)
@@ -924,18 +824,62 @@ func TestHandler(t *testing.T) {
}
```
-# How it Works
+# Design
+
+General Huma design principles:
+
+- HTTP/2 and streaming out of the box
+- Describe inputs/outputs and keep docs up to date
+- Generate OpenAPI for automated tooling
+- Re-use idiomatic Go concepts whenever possible
+- Encourage good behavior, e.g. exhaustive errors
+
+## High-level design
+
+The high-level design centers around a `Router` object.
+
+- CLI (optional)
+ - Router
+ - []Middleware
+ - []Resource
+ - URI path
+ - []Middleware
+ - []Operations
+ - HTTP method
+ - Inputs / outputs
+ - Go structs with tags
+ - Handler function
+
+## Router Selection
+
+- Why not Gin? Lots of stars on GitHub, but... Overkill, non-standard handlers & middlware, weird debug mode.
+- Why not fasthttp? Fiber? Not fully HTTP compliant, no HTTP/2, no streaming request/response support.
+- Why not httprouter? Non-standard handlers, no middleware.
+- HTTP/2 means HTTP pipelining benchmarks don't really matter.
+
+Ultimately using Chi because:
+
+- Fast router with support for parameterized paths & middleware
+- Standard HTTP handlers
+- Standard HTTP middleware
-Huma's philosophy is to make it harder to make mistakes by providing tools that reduce duplication and encourage practices which make it hard to forget to update some code.
+### Compatibility
-An example of this is how handler functions **must** declare all headers that they return and which responses may send those headers. You simply **cannot** return from the function without considering the values of each of those headers. If you set one that isn't appropriate for the response you return, Huma will let you know.
+Huma tries to be compatible with as many Go libraries as possible by using standard interfaces and idiomatic concepts.
-How does it work? Huma asks that you give up one compile-time static type check for handler function signatures and instead let it be a runtime startup check. It's simple enough that even the most basic unit test will invoke the runtime check, giving you most of the security you would from static typing.
+- Standard middleware `func(next http.Handler) http.Handler`
+- Standard context `huma.Context` is a `context.Context`
+- Standard HTTP writer `huma.Context` is an `http.ResponseWriter` that can check against declared response codes and models.
+- Standard streaming support via the `io.Reader` and `io.Writer` interfaces
-Using a small amount of reflection, Huma can verify the function signatures, inject dependencies and parameters, and handle responses and headers as well as making sure that they all match the declared operation.
+## Compromises
-By strictly enforcing this runtime interface you get several advantages. No more out of date API description. No more out of date documenatation. No more out of date SDKs or CLIs. Your entire ecosystem of tooling is driven off of one simple backend implementation. Stuff just works.
+Given the features of Go, the desire to strictly keep the code and docs/tools in sync, and a desire to be developer-friendly and quick to start using, Huma makes some necessary compromises.
-More docs coming soon.
+- Struct tags are used as metadata for fields to support things like JSON Schema-style validation. There are no compile-time checks for these, but basic linter support.
+- Handler functions registration uses `interface{}` to support any kind of input struct.
+- Response registration takes an _instance_ of your type since you can't pass types in Go.
+- Many checks happen at service startup rather than compile-time. Luckily the most basic unit test that creates a router should catch these.
+- `ctx.WriteModel` and `ctx.WriteError` do checks at runtime and can be at least partially bypassed with `ctx.Write` by design. We trade looser checks for a nicer interface and more compatibility.
> :whale: Thanks for reading!
diff --git a/autoconfig.go b/autoconfig.go
new file mode 100644
index 00000000..f354bcb8
--- /dev/null
+++ b/autoconfig.go
@@ -0,0 +1,24 @@
+package huma
+
+// AutoConfigVar represents a variable given by the user when prompted during
+// auto-configuration setup of an API.
+type AutoConfigVar struct {
+ Description string `json:"description,omitempty"`
+ Example string `json:"example,omitempty"`
+ Default interface{} `json:"default,omitempty"`
+ Enum []interface{} `json:"enum,omitempty"`
+
+ // Exclude the value from being sent to the server. This essentially makes
+ // it a value which is only used in param templates.
+ Exclude bool `json:"exclude,omitempty"`
+}
+
+// AutoConfig holds an API's automatic configuration settings for the CLI. These
+// are advertised via OpenAPI extension and picked up by the CLI to make it
+// easier to get started using an API.
+type AutoConfig struct {
+ Security string `json:"security"`
+ Headers map[string]string `json:"headers,omitempty"`
+ Prompt map[string]AutoConfigVar `json:"prompt,omitempty"`
+ Params map[string]string `json:"params"`
+}
diff --git a/benchmark/README.md b/benchmark/README.md
index 02c1727c..a9f3c036 100644
--- a/benchmark/README.md
+++ b/benchmark/README.md
@@ -7,7 +7,7 @@ This folder contains four implementations of the same service using the followin
- [FastAPI](https://github.com/tiangolo/fastapi) (Python, popular framework)
- [Gin](https://github.com/gin-gonic/gin) (Go, popular framework)
- [Echo](https://echo.labstack.com/) (Go, popular framework)
-- [Huma](https://github.com/danielgtaylor/huma) (Go, this project)
+- [Huma](https://github.com/istreamlabs/huma) (Go, this project)
The [wrk](https://github.com/wg/wrk) benchmarking tool is used to make requests against each implementation for 10 seconds with 10 concurrent workers. The results on a 2017 MacBook Pro are shown below:
diff --git a/benchmark/fastapi/Pipfile.lock b/benchmark/fastapi/Pipfile.lock
index fe6e6f0e..3ca201b9 100644
--- a/benchmark/fastapi/Pipfile.lock
+++ b/benchmark/fastapi/Pipfile.lock
@@ -18,10 +18,10 @@
"default": {
"click": {
"hashes": [
- "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
- "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
+ "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
+ "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
],
- "version": "==7.1.1"
+ "version": "==7.1.2"
},
"fastapi": {
"hashes": [
@@ -58,22 +58,25 @@
},
"pydantic": {
"hashes": [
- "sha256:012c422859bac2e03ab3151ea6624fecf0e249486be7eb8c6ee69c91740c6752",
- "sha256:07911aab70f3bc52bb845ce1748569c5e70478ac977e106a150dd9d0465ebf04",
- "sha256:47b8db7024ba3d46c3d4768535e1cf87b6c8cf92ccd81e76f4e1cb8ee47688b3",
- "sha256:50e4e948892a6815649ad5a9a9379ad1e5f090f17842ac206535dfaed75c6f2f",
- "sha256:51f11c8bbf794a68086540da099aae4a9107447c7a9d63151edbb7d50110cf21",
- "sha256:6100d7862371115c40be55cc4b8d766a74b1d0dbaf99dbfe72bb4bac0faf89ed",
- "sha256:61d22d36808087d3184ed6ac0d91dd71c533b66addb02e4a9930e1e30833202f",
- "sha256:72184c1421103cca128300120f8f1185fb42a9ea73a1c9845b1c53db8c026a7d",
- "sha256:831a0265a9e3933b3d0f04d1a81bba543bafbe4119c183ff2771871db70524ab",
- "sha256:8848b4eb458469739126e4c1a202d723dd092e087f8dbe3104371335f87ba5df",
- "sha256:bbbed364376f4a0aebb9ea452ff7968b306499a9e74f4db69b28ff2cd4043a11",
- "sha256:e27559cedbd7f59d2375bfd6eea29a330ea1a5b0589c34d6b4e0d7bec6027bbf",
- "sha256:f17ec336e64d4583311249fb179528e9a2c27c8a2eaf590ec6ec2c6dece7cb3f",
- "sha256:f863456d3d4bf817f2e5248553dee3974c5dc796f48e6ddb599383570f4215ac"
+ "sha256:1783c1d927f9e1366e0e0609ae324039b2479a1a282a98ed6a6836c9ed02002c",
+ "sha256:2dc946b07cf24bee4737ced0ae77e2ea6bc97489ba5a035b603bd1b40ad81f7e",
+ "sha256:2de562a456c4ecdc80cf1a8c3e70c666625f7d02d89a6174ecf63754c734592e",
+ "sha256:36dbf6f1be212ab37b5fda07667461a9219c956181aa5570a00edfb0acdfe4a1",
+ "sha256:3fa799f3cfff3e5f536cbd389368fc96a44bb30308f258c94ee76b73bd60531d",
+ "sha256:40d765fa2d31d5be8e29c1794657ad46f5ee583a565c83cea56630d3ae5878b9",
+ "sha256:418b84654b60e44c0cdd5384294b0e4bc1ebf42d6e873819424f3b78b8690614",
+ "sha256:4900b8820b687c9a3ed753684337979574df20e6ebe4227381d04b3c3c628f99",
+ "sha256:530d7222a2786a97bc59ee0e0ebbe23728f82974b1f1ad9a11cd966143410633",
+ "sha256:54122a8ed6b75fe1dd80797f8251ad2063ea348a03b77218d73ea9fe19bd4e73",
+ "sha256:6c3f162ba175678218629f446a947e3356415b6b09122dcb364e58c442c645a7",
+ "sha256:b49c86aecde15cde33835d5d6360e55f5e0067bb7143a8303bf03b872935c75b",
+ "sha256:b5b3489cb303d0f41ad4a7390cf606a5f2c7a94dcba20c051cd1c653694cb14d",
+ "sha256:cf3933c98cb5e808b62fae509f74f209730b180b1e3c3954ee3f7949e083a7df",
+ "sha256:eb75dc1809875d5738df14b6566ccf9fd9c0bcde4f36b72870f318f16b9f5c20",
+ "sha256:f769141ab0abfadf3305d4fcf36660e5cf568a666dd3efab7c3d4782f70946b1",
+ "sha256:f8af9b840a9074e08c0e6dc93101de84ba95df89b267bf7151d74c553d66833b"
],
- "version": "==1.4"
+ "version": "==1.6.1"
},
"starlette": {
"hashes": [
@@ -84,11 +87,11 @@
},
"uvicorn": {
"hashes": [
- "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd",
- "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c"
+ "sha256:1d46a22cc55a52f5567e0c66f000ae56f26263e44cef59b7c885bf10f487ce6e",
+ "sha256:b50f7f4c0c499c9b8d0280924cfbd24b90ba02456e3dc80934b9a786a291f09f"
],
"index": "pypi",
- "version": "==0.11.3"
+ "version": "==0.11.7"
},
"uvloop": {
"hashes": [
diff --git a/benchmark/fiber/main.go b/benchmark/fiber/main.go
new file mode 100644
index 00000000..2d1aa439
--- /dev/null
+++ b/benchmark/fiber/main.go
@@ -0,0 +1,48 @@
+package main
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/gofiber/fiber"
+ "github.com/gofiber/fiber/middleware"
+)
+
+// Item tracks the price of a good.
+type Item struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Price float32 `json:"price"`
+ IsOffer bool `json:"is_offer,omitempty"`
+}
+
+func main() {
+ app := fiber.New()
+ app.Use(middleware.Recover())
+
+ d := func(c *fiber.Ctx) string {
+ return strings.Split(c.Get("authorization"), " ")[0]
+ }
+
+ app.Get("/items/:id", func(c *fiber.Ctx) {
+ tmp := c.Params("id")
+ id, err := strconv.Atoi(tmp)
+ if err != nil {
+ c.Status(500)
+ return
+ }
+
+ authInfo := d(c)
+
+ c.Set("x-authinfo", authInfo)
+ c.Status(200)
+ c.JSON(&Item{
+ ID: id,
+ Name: "Hello",
+ Price: 1.25,
+ IsOffer: false,
+ })
+ })
+
+ app.Listen("127.0.0.1:8000")
+}
diff --git a/benchmark/go.mod b/benchmark/go.mod
new file mode 100644
index 00000000..bdb4e94a
--- /dev/null
+++ b/benchmark/go.mod
@@ -0,0 +1,10 @@
+module github.com/istreamlabs/huma/benchmark
+
+go 1.14
+
+require (
+ github.com/istreamlabs/huma v0.0.0-20200821183705-0c275cfd3c4a
+ github.com/gin-gonic/gin v1.5.0
+ github.com/gofiber/fiber v1.13.3
+ github.com/labstack/echo/v4 v4.1.15
+)
diff --git a/benchmark/go.sum b/benchmark/go.sum
new file mode 100644
index 00000000..7afd5ef0
--- /dev/null
+++ b/benchmark/go.sum
@@ -0,0 +1,447 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
+github.com/Jeffail/gabs/v2 v2.6.0 h1:WdCnGaDhNa4LSRTMwhLZzJ7SRDXjABNP13SOKvCpL5w=
+github.com/Jeffail/gabs/v2 v2.6.0/go.mod h1:xCn81vdHKxFUuWWAaD5jCTQDNPBMh5pPs9IJ+NcziBI=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4=
+github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/istreamlabs/huma v0.0.0-20200821183705-0c275cfd3c4a h1:Y8W74tIZW4lwYdboTq36W9/1xlmFndtJTwI4PCF0994=
+github.com/istreamlabs/huma v0.0.0-20200821183705-0c275cfd3c4a/go.mod h1:/fJWZuR8MP0WTxpPj0PVje+THtOlfd6tH9s8iVbVQdA=
+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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fxamacker/cbor/v2 v2.2.0 h1:6eXqdDDe588rSYAi1HfZKbx6YYQO4mxQ9eC6xYpU/JQ=
+github.com/fxamacker/cbor/v2 v2.2.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
+github.com/getkin/kin-openapi v0.3.0/go.mod h1:W8dhxZgpE84ciM+VIItFqkmZ4eHtuomrdIHtASQIqi0=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/autotls v0.0.0-20200314141124-cc69476aef2a/go.mod h1:GTnUDNd5zRw/BceSPHICHoH9fNaTSPHsFTU72wuK0YE=
+github.com/gin-gonic/gin v1.5.0 h1:fi+bqFAx/oLK54somfCtEZs9HeH1LHVoEPUgARpTqyc=
+github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do=
+github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec=
+github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc=
+github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
+github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM=
+github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gofiber/fiber v1.13.3 h1:14kBTW1+n5mNIJZqibsbIdb+yQdC5argcbe9vE7Nz+o=
+github.com/gofiber/fiber v1.13.3/go.mod h1:KxRvVkqzfZOO6A7mBu+j7ncX2AcT6Sm6F7oeGR3Kgmw=
+github.com/gofiber/utils v0.0.9 h1:Bu4grjEB4zof1TtpmPCG6MeX5nGv8SaQfzaUgjkf3H8=
+github.com/gofiber/utils v0.0.9/go.mod h1:9J5aHFUIjq0XfknT4+hdSMG6/jzfaAgCu4HEbWDeBlo=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
+github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg=
+github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/labstack/echo/v4 v4.1.15 h1:4aE6KfJC+wCnMjODwcpeEGWGsRfszxZMwB3QVTECj2I=
+github.com/labstack/echo/v4 v4.1.15/go.mod h1:GWO5IBVzI371K8XJe50CSvHjQCafK6cw8R/moLhEU6o=
+github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
+github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
+github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8=
+github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/magiconair/properties v1.8.2 h1:znVR8Q4g7/WlcvsxLBRWvo+vtFJUAbDn3w+Yak2xVMI=
+github.com/magiconair/properties v1.8.2/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
+github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
+github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.4.0 h1:u3Z1r+oOXJIkxqw34zVhyPgjBsm6X2wn21NWs/HfSeg=
+github.com/pelletier/go-toml v1.4.0/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw=
+github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc=
+github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
+github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
+github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
+github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.15.1 h1:eRb5jzWhbCn/cGu3gNJMcOfPUfXgXCcQIOHjh9ajAS8=
+github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4=
+github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
+github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
+github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
+github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
+github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
+github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
+github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
+github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
+github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.15.0 h1:ZZCA22JRF2gQE5FoNmhmrf7jeJJ2uhqDUNRYKm8dvmM=
+go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d h1:1ZiEyfaQIg3Qh0EoqpwAakHVhecoE5wlSg5GjnafJGw=
+golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
+golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
+golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
+golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v9 v9.29.1 h1:SvGtYmN60a5CVKTOzMSyfzWDeZRxRuGvRQyEAKbw1xc=
+gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
+gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/ini.v1 v1.60.1 h1:P5y5shSkb0CFe44qEeMBgn8JLow09MP17jlJHanke5g=
+gopkg.in/ini.v1 v1.60.1/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/benchmark/huma/main.go b/benchmark/huma/main.go
index 8653548b..0b79e660 100644
--- a/benchmark/huma/main.go
+++ b/benchmark/huma/main.go
@@ -4,9 +4,10 @@ import (
"net/http"
"strings"
- "github.com/danielgtaylor/huma"
- "github.com/gin-contrib/cors"
- "github.com/gin-gonic/gin"
+ "github.com/istreamlabs/huma"
+ "github.com/istreamlabs/huma/cli"
+ "github.com/istreamlabs/huma/middleware"
+ "github.com/istreamlabs/huma/responses"
)
// Item tracks the price of a good.
@@ -17,34 +18,30 @@ type Item struct {
IsOffer bool `json:"is_offer,omitempty"`
}
+type Input struct {
+ AuthInfo string
+ ID int `path:"id"`
+}
+
+func (i *Input) Resolve(ctx huma.Context, r *http.Request) {
+ i.AuthInfo = strings.Split(r.Header.Get("Authorization"), " ")[0]
+}
+
func main() {
- gin.SetMode(gin.ReleaseMode)
- g := gin.New()
- g.Use(huma.Recovery())
- g.Use(cors.Default())
- g.Use(huma.PreferMinimalMiddleware())
-
- r := huma.NewRouter("Benchmark", "1.0.0", huma.Gin(g))
-
- d := huma.Dependency(
- huma.HeaderParam("authorization", "Auth header", ""),
- func(auth string) (string, error) {
- return strings.Split(auth, " ")[0], nil
- },
- )
-
- r.Resource("/items", d,
- huma.PathParam("id", "The item's unique ID"),
- huma.ResponseHeader("x-authinfo", "..."),
- huma.ResponseJSON(http.StatusOK, "Successful hello response", huma.Headers("x-authinfo")),
- ).Get("Huma benchmark test", func(authInfo string, id int) (string, *Item) {
- return authInfo, &Item{
- ID: id,
+ app := cli.New(huma.New("Benchmark", "1.0.0"))
+ app.Middleware(middleware.Recovery, middleware.ContentEncoding)
+
+ app.Resource("/items/{id}").Get("get", "Huma benchmark test",
+ responses.OK().Headers("x-authinfo").Model(Item{}),
+ ).Run(func(ctx huma.Context, input Input) {
+ ctx.Header().Set("x-authinfo", input.AuthInfo)
+ ctx.WriteModel(http.StatusOK, &Item{
+ ID: input.ID,
Name: "Hello",
Price: 1.24,
IsOffer: false,
- }
+ })
})
- r.Run()
+ app.Run()
}
diff --git a/cli.go b/cli.go
deleted file mode 100644
index b02d7ff0..00000000
--- a/cli.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package huma
-
-import (
- "context"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "os"
- "os/signal"
- "path/filepath"
- "strings"
- "syscall"
- "time"
-
- "github.com/gin-gonic/autotls"
- "github.com/spf13/cobra"
- "github.com/spf13/viper"
- "go.uber.org/zap/zapcore"
-)
-
-// GlobalFlag adds a new global flag on the root command.
-func GlobalFlag(name, short, description string, defaultValue interface{}) RouterOption {
- return &routerOption{func(r *Router) {
- viper.SetDefault(name, defaultValue)
-
- flags := r.root.PersistentFlags()
- switch v := defaultValue.(type) {
- case bool:
- flags.BoolP(name, short, viper.GetBool(name), description)
- case int, int16, int32, int64, uint16, uint32, uint64:
- flags.IntP(name, short, viper.GetInt(name), description)
- case float32, float64:
- flags.Float64P(name, short, viper.GetFloat64(name), description)
- default:
- flags.StringP(name, short, fmt.Sprintf("%v", v), description)
- }
- viper.BindPFlag(name, flags.Lookup(name))
- }}
-}
-
-// Root returns the router's root command.
-func (r *Router) Root() *cobra.Command {
- return r.root
-}
-
-// setupCLI sets up the CLI commands.
-func (r *Router) setupCLI() {
- viper.SetEnvPrefix("SERVICE")
- viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
- viper.AutomaticEnv()
-
- r.root = &cobra.Command{
- Use: filepath.Base(os.Args[0]),
- Version: r.api.Version,
- Run: func(cmd *cobra.Command, args []string) {
- // Call any pre-start functions.
- for _, f := range r.prestart {
- f()
- }
-
- if viper.GetBool("debug") {
- if logLevel != nil {
- logLevel.SetLevel(zapcore.DebugLevel)
- }
- }
-
- // Start the server.
- go func() {
- // Start either an HTTP or HTTPS server based on whether TLS cert/key
- // paths were given or Let's Encrypt is used.
- autoTLS := viper.GetString("autotls")
- if autoTLS != "" {
- domains := strings.Split(autoTLS, ",")
- if err := autotls.Run(r, domains...); err != nil && err != http.ErrServerClosed {
- panic(err)
- }
- return
- }
-
- cert := viper.GetString("cert")
- key := viper.GetString("key")
- if cert == "" && key == "" {
- if err := r.Listen(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port"))); err != nil && err != http.ErrServerClosed {
- panic(err)
- }
- return
- }
-
- if cert != "" && key != "" {
- if err := r.ListenTLS(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port")), cert, key); err != nil && err != http.ErrServerClosed {
- panic(err)
- }
- return
- }
-
- panic("must pass key and cert for TLS")
- }()
-
- // Handle graceful shutdown.
- quit := make(chan os.Signal)
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
- <-quit
-
- fmt.Println("Gracefully shutting down the server...")
-
- ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("grace-period")*time.Second)
- defer cancel()
- r.Shutdown(ctx)
- },
- }
-
- r.root.AddCommand(&cobra.Command{
- Use: "openapi FILENAME.json",
- Short: "Get OpenAPI spec",
- Args: cobra.ExactArgs(1),
- Run: func(cmd *cobra.Command, args []string) {
- // Get the OpenAPI route from the server.
- w := httptest.NewRecorder()
- req, _ := http.NewRequest(http.MethodGet, "/openapi.json", nil)
- r.ServeHTTP(w, req)
-
- if w.Result().StatusCode != 200 {
- panic(w.Body.String())
- }
-
- // Dump the response to a file.
- ioutil.WriteFile(args[0], append(w.Body.Bytes(), byte('\n')), 0644)
-
- fmt.Printf("Successfully wrote OpenAPI JSON to %s\n", args[0])
- },
- })
-
- flags := []RouterOption{
- GlobalFlag("host", "", "Hostname", "0.0.0.0"),
- GlobalFlag("port", "p", "Port", 8888),
- GlobalFlag("cert", "", "SSL certificate file path", ""),
- GlobalFlag("key", "", "SSL key file path", ""),
- GlobalFlag("autotls", "", "Let's Encrypt automatic TLS domains (ignores port)", ""),
- GlobalFlag("debug", "d", "Enable debug logs", false),
- GlobalFlag("grace-period", "", "Graceful shutdown wait duration in seconds", 20),
- }
-
- for _, flag := range flags {
- flag.ApplyRouter(r)
- }
-}
diff --git a/cli/cli.go b/cli/cli.go
new file mode 100644
index 00000000..ed4baa59
--- /dev/null
+++ b/cli/cli.go
@@ -0,0 +1,168 @@
+package cli
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/istreamlabs/huma"
+ "github.com/istreamlabs/huma/middleware"
+ "github.com/spf13/cobra"
+ "github.com/spf13/viper"
+)
+
+// CLI provides a command line interface to a Huma router.
+type CLI struct {
+ *huma.Router
+
+ // Root entrypoint command
+ root *cobra.Command
+
+ // Functions to run before the server starts up.
+ prestart []func()
+}
+
+// NewRouter creates a new router, new CLI, sets the default middlware, and
+// returns the CLI/router as a convenience function.
+func NewRouter(docs, version string) *CLI {
+ // Create the router and CLI
+ r := huma.New(docs, version)
+ app := New(r)
+
+ // Set up the default middleware
+ middleware.Defaults(app)
+
+ return app
+}
+
+// New creates a new CLI instance from an existing router.
+func New(router *huma.Router) *CLI {
+ viper.SetEnvPrefix("SERVICE")
+ viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
+ viper.AutomaticEnv()
+
+ app := &CLI{
+ Router: router,
+ }
+
+ app.root = &cobra.Command{
+ Use: filepath.Base(os.Args[0]),
+ Version: app.GetVersion(),
+ Run: func(cmd *cobra.Command, args []string) {
+ fmt.Printf("Starting %s %s on %s:%v\n", app.GetTitle(), app.GetVersion(), viper.Get("host"), viper.Get("port"))
+
+ // Call any pre-start functions.
+ for _, f := range app.prestart {
+ f()
+ }
+
+ // Start the server.
+ go func() {
+ // Start either an HTTP or HTTPS server based on whether TLS cert/key
+ // paths were given or Let's Encrypt is used.
+ cert := viper.GetString("cert")
+ key := viper.GetString("key")
+ if cert == "" && key == "" {
+ if err := app.Listen(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port"))); err != nil && err != http.ErrServerClosed {
+ panic(err)
+ }
+ return
+ }
+
+ if cert != "" && key != "" {
+ if err := app.ListenTLS(fmt.Sprintf("%s:%v", viper.Get("host"), viper.Get("port")), cert, key); err != nil && err != http.ErrServerClosed {
+ panic(err)
+ }
+ return
+ }
+
+ panic("must pass key and cert for TLS")
+ }()
+
+ // Handle graceful shutdown.
+ quit := make(chan os.Signal)
+ signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
+ <-quit
+
+ fmt.Println("Gracefully shutting down the server...")
+
+ ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("grace-period")*time.Second)
+ defer cancel()
+ app.Shutdown(ctx)
+ },
+ }
+
+ app.root.AddCommand(&cobra.Command{
+ Use: "openapi FILENAME.json",
+ Short: "Get OpenAPI spec",
+ Args: cobra.ExactArgs(1),
+ Run: func(cmd *cobra.Command, args []string) {
+ // Get the OpenAPI route from the server.
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/openapi.json", nil)
+ app.ServeHTTP(w, req)
+
+ if w.Result().StatusCode != 200 {
+ panic(w.Body.String())
+ }
+
+ // Dump the response to a file.
+ ioutil.WriteFile(args[0], append(w.Body.Bytes(), byte('\n')), 0644)
+
+ fmt.Printf("Successfully wrote OpenAPI JSON to %s\n", args[0])
+ },
+ })
+
+ app.Flag("host", "", "Hostname", "0.0.0.0")
+ app.Flag("port", "p", "Port", 8888)
+ app.Flag("cert", "", "SSL certificate file path", "")
+ app.Flag("key", "", "SSL key file path", "")
+ app.Flag("grace-period", "", "Graceful shutdown wait duration in seconds", 20)
+
+ return app
+}
+
+// Root returns the CLI's root command. Use this to add flags and custom
+// commands to the CLI.
+func (c *CLI) Root() *cobra.Command {
+ return c.root
+}
+
+// Flag adds a new global flag on the root command of this router.
+func (c *CLI) Flag(name, short, description string, defaultValue interface{}) {
+ viper.SetDefault(name, defaultValue)
+
+ flags := c.root.PersistentFlags()
+ switch v := defaultValue.(type) {
+ case bool:
+ flags.BoolP(name, short, viper.GetBool(name), description)
+ case int, int16, int32, int64, uint16, uint32, uint64:
+ flags.IntP(name, short, viper.GetInt(name), description)
+ case float32, float64:
+ flags.Float64P(name, short, viper.GetFloat64(name), description)
+ default:
+ flags.StringP(name, short, fmt.Sprintf("%v", v), description)
+ }
+ viper.BindPFlag(name, flags.Lookup(name))
+}
+
+// PreStart registers a function to run before the server starts but after
+// command line arguments have been parsed.
+func (c *CLI) PreStart(f func()) {
+ c.prestart = append(c.prestart, f)
+}
+
+// Run runs the CLI.
+func (c *CLI) Run() {
+ if err := c.root.Execute(); err != nil {
+ panic(err)
+ }
+}
diff --git a/cli/cli_test.go b/cli/cli_test.go
new file mode 100644
index 00000000..abecad45
--- /dev/null
+++ b/cli/cli_test.go
@@ -0,0 +1,34 @@
+package cli
+
+import (
+ "context"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestCLI(t *testing.T) {
+ app := NewRouter("Test API", "1.0.0")
+
+ started := false
+ app.PreStart(func() {
+ started = true
+ })
+
+ go func() {
+ // Let the OS pick a random port.
+ os.Setenv("SERVICE_PORT", "0")
+ os.Setenv("SERVICE_HOST", "127.0.0.1")
+ app.Root().Run(nil, []string{})
+ }()
+
+ time.Sleep(10 * time.Millisecond)
+ ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+ defer cancel()
+
+ app.Shutdown(ctx)
+
+ assert.Equal(t, true, started)
+}
diff --git a/cli_test.go b/cli_test.go
deleted file mode 100644
index b9b5181d..00000000
--- a/cli_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package huma
-
-import (
- "context"
- "os"
- "testing"
- "time"
-)
-
-func TestServerShutdown(t *testing.T) {
- r := NewTestRouter(t)
-
- go func() {
- // Let the OS pick a random port.
- os.Setenv("SERVICE_PORT", "0")
- r.Root().Run(nil, []string{})
- }()
-
- time.Sleep(10 * time.Millisecond)
- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
- defer cancel()
-
- r.Shutdown(ctx)
-}
diff --git a/context.go b/context.go
new file mode 100644
index 00000000..ea353eeb
--- /dev/null
+++ b/context.go
@@ -0,0 +1,239 @@
+package huma
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "reflect"
+ "strings"
+
+ "github.com/fxamacker/cbor/v2"
+ "github.com/goccy/go-yaml"
+ "github.com/istreamlabs/huma/negotiation"
+)
+
+// ContextFromRequest returns a Huma context for a request, useful for
+// accessing high-level convenience functions from e.g. middleware.
+func ContextFromRequest(w http.ResponseWriter, r *http.Request) Context {
+ return &hcontext{
+ Context: r.Context(),
+ ResponseWriter: w,
+ r: r,
+ }
+}
+
+// Context provides a request context and response writer with convenience
+// functions for error and model marshaling in handler functions.
+type Context interface {
+ context.Context
+ http.ResponseWriter
+
+ // AddError adds a new error to the list of errors for this request.
+ AddError(err error)
+
+ // HasError returns true if at least one error has been added to the context.
+ HasError() bool
+
+ // WriteError writes out an HTTP status code, friendly error message, and
+ // optionally a set of error details set with `AddError` and/or passed in.
+ WriteError(status int, message string, errors ...error)
+
+ // WriteModel writes out an HTTP status code and marshalled model based on
+ // content negotiation (e.g. JSON or CBOR). This must match the registered
+ // response status code & type.
+ WriteModel(status int, model interface{})
+}
+
+type hcontext struct {
+ context.Context
+ http.ResponseWriter
+ r *http.Request
+ errors []error
+ op *Operation
+ closed bool
+}
+
+func (c *hcontext) AddError(err error) {
+ c.errors = append(c.errors, err)
+}
+
+func (c *hcontext) HasError() bool {
+ return len(c.errors) > 0
+}
+
+func (c *hcontext) WriteHeader(status int) {
+ if c.op != nil {
+ allowed := []string{}
+ for _, r := range c.op.responses {
+ if r.status == status {
+ for _, h := range r.headers {
+ allowed = append(allowed, h)
+ }
+ }
+ }
+
+ // Check that all headers were allowed to be sent.
+ for name := range c.Header() {
+ found := false
+
+ for _, h := range allowed {
+ if strings.ToLower(name) == strings.ToLower(h) {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ panic(fmt.Errorf("Response header %s is not declared for %s %s with status code %d (allowed: %s)", name, c.r.Method, c.r.URL.Path, status, allowed))
+ }
+ }
+ }
+
+ c.ResponseWriter.WriteHeader(status)
+}
+
+func (c *hcontext) Write(data []byte) (int, error) {
+ if c.closed {
+ panic(fmt.Errorf("Trying to write to response after WriteModel or WriteError for %s %s", c.r.Method, c.r.URL.Path))
+ }
+
+ return c.ResponseWriter.Write(data)
+}
+
+func (c *hcontext) WriteError(status int, message string, errors ...error) {
+ if c.closed {
+ panic(fmt.Errorf("Trying to write to response after WriteModel or WriteError for %s %s", c.r.Method, c.r.URL.Path))
+ }
+
+ details := []*ErrorDetail{}
+
+ c.errors = append(c.errors, errors...)
+ for _, err := range c.errors {
+ if d, ok := err.(ErrorDetailer); ok {
+ details = append(details, d.ErrorDetail())
+ } else {
+ details = append(details, &ErrorDetail{Message: err.Error()})
+ }
+ }
+
+ model := &ErrorModel{
+ Title: http.StatusText(status),
+ Status: status,
+ Detail: message,
+ Errors: details,
+ }
+
+ // Select content type and transform it to the appropriate error type.
+ ct := selectContentType(c.r)
+ switch ct {
+ case "application/cbor":
+ ct = "application/problem+cbor"
+ case "", "application/json":
+ ct = "application/problem+json"
+ case "application/yaml", "application/x-yaml":
+ ct = "application/problem+yaml"
+ }
+
+ c.writeModel(ct, status, model)
+}
+
+func (c *hcontext) WriteModel(status int, model interface{}) {
+ if c.closed {
+ panic(fmt.Errorf("Trying to write to response after WriteModel or WriteError for %s %s", c.r.Method, c.r.URL.Path))
+ }
+
+ // Get the negotiated content type the client wants and we are willing to
+ // provide.
+ ct := selectContentType(c.r)
+
+ c.writeModel(ct, status, model)
+}
+
+func (c *hcontext) writeModel(ct string, status int, model interface{}) {
+ // Is this allowed? Find the right response.
+ if c.op != nil {
+ responses := []Response{}
+ names := []string{}
+ statuses := []string{}
+ for _, r := range c.op.responses {
+ statuses = append(statuses, fmt.Sprintf("%d", r.status))
+ if r.status == status {
+ responses = append(responses, r)
+ if r.model != nil {
+ names = append(names, r.model.Name())
+ }
+ }
+ }
+
+ if len(responses) == 0 {
+ panic(fmt.Errorf("HTTP status %d not allowed for %s %s, expected one of %s", status, c.r.Method, c.r.URL.Path, statuses))
+ }
+
+ found := false
+ for _, r := range responses {
+ if r.model == reflect.TypeOf(model) {
+ found = true
+ break
+ }
+ }
+
+ if !found {
+ panic(fmt.Errorf("Invalid model %s, expecting %s for %s %s", reflect.TypeOf(model), strings.Join(names, ", "), c.r.Method, c.r.URL.Path))
+ }
+ }
+
+ // Do the appropriate encoding.
+ var encoded []byte
+ var err error
+ if strings.HasPrefix(ct, "application/json") || strings.HasSuffix(ct, "+json") {
+ encoded, err = json.Marshal(model)
+ if err != nil {
+ panic(fmt.Errorf("Unable to marshal JSON: %w", err))
+ }
+ } else if strings.HasPrefix(ct, "application/yaml") || strings.HasPrefix(ct, "application/x-yaml") || strings.HasSuffix(ct, "+yaml") {
+ encoded, err = yaml.Marshal(model)
+ if err != nil {
+ panic(fmt.Errorf("Unable to marshal YAML: %w", err))
+ }
+ } else if strings.HasPrefix(ct, "application/cbor") || strings.HasSuffix(ct, "+cbor") {
+ opts := cbor.CanonicalEncOptions()
+ opts.Time = cbor.TimeRFC3339Nano
+ opts.TimeTag = cbor.EncTagRequired
+ mode, err := opts.EncMode()
+ if err != nil {
+ panic(fmt.Errorf("Unable to marshal CBOR: %w", err))
+ }
+ encoded, err = mode.Marshal(model)
+ if err != nil {
+ panic(fmt.Errorf("Unable to marshal JSON: %w", err))
+ }
+ }
+
+ // Encoding succeeded, write the data!
+ c.Header().Set("Content-Type", ct)
+ c.WriteHeader(status)
+ c.Write(encoded)
+ c.closed = true
+}
+
+// selectContentType selects the best availalable content type via content
+// negotiation with the client, defaulting to JSON.
+func selectContentType(r *http.Request) string {
+ ct := "application/json"
+
+ if accept := r.Header.Get("Accept"); accept != "" {
+ best := negotiation.SelectQValue(accept, []string{
+ "application/cbor",
+ "application/json",
+ "application/yaml",
+ "application/x-yaml",
+ })
+
+ if best != "" {
+ ct = best
+ }
+ }
+
+ return ct
+}
diff --git a/context_test.go b/context_test.go
new file mode 100644
index 00000000..c773edb7
--- /dev/null
+++ b/context_test.go
@@ -0,0 +1,244 @@
+package huma
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/fxamacker/cbor"
+ "github.com/goccy/go-yaml"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestGetContextFromRequest(t *testing.T) {
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ r = r.WithContext(context.WithValue(r.Context(), contextKey("foo"), "bar"))
+ ctx := ContextFromRequest(w, r)
+ assert.Equal(t, "bar", ctx.Value(contextKey("foo")))
+ })
+
+ w := httptest.NewRecorder()
+ r, _ := http.NewRequest(http.MethodGet, "/", nil)
+ handler(w, r)
+}
+
+func TestContentNegotiation(t *testing.T) {
+ type Response struct {
+ Value string `json:"value"`
+ }
+
+ app := newTestRouter()
+
+ app.Resource("/negotiated").Get("test", "Test",
+ NewResponse(200, "desc").Model(Response{}),
+ ).Run(func(ctx Context) {
+ ctx.WriteModel(http.StatusOK, Response{
+ Value: "Hello, world!",
+ })
+ })
+
+ var parsed Response
+ expected := Response{
+ Value: "Hello, world!",
+ }
+
+ // No preference
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/negotiated", nil)
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
+ err := json.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ // Prefer JSON
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/negotiated", nil)
+ req.Header.Set("Accept", "application/json")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "application/json", w.Header().Get("Content-Type"))
+ err = json.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ // Prefer YAML
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/negotiated", nil)
+ req.Header.Set("Accept", "application/yaml")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "application/yaml", w.Header().Get("Content-Type"))
+ err = yaml.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ // Prefer CBOR
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/negotiated", nil)
+ req.Header.Set("Accept", "application/cbor")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusOK, w.Code)
+ assert.Equal(t, "application/cbor", w.Header().Get("Content-Type"))
+ err = cbor.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.EqualValues(t, expected, parsed)
+}
+
+func TestErrorNegotiation(t *testing.T) {
+ app := newTestRouter()
+
+ app.Resource("/error").Get("test", "Test",
+ NewResponse(400, "desc").Model(&ErrorModel{}),
+ ).Run(func(ctx Context) {
+ ctx.AddError(fmt.Errorf("some error"))
+ ctx.AddError(&ErrorDetail{
+ Message: "Invalid value",
+ Location: "body.field",
+ Value: "test",
+ })
+ ctx.WriteError(http.StatusBadRequest, "test error")
+ })
+
+ var parsed ErrorModel
+ expected := ErrorModel{
+ Status: http.StatusBadRequest,
+ Title: http.StatusText(http.StatusBadRequest),
+ Detail: "test error",
+ Errors: []*ErrorDetail{
+ {Message: "some error"},
+ {
+ Message: "Invalid value",
+ Location: "body.field",
+ Value: "test",
+ },
+ },
+ }
+
+ // No preference
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/error", nil)
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Equal(t, "application/problem+json", w.Header().Get("Content-Type"))
+ err := json.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ // Prefer JSON
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/error", nil)
+ req.Header.Set("Accept", "application/json")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Equal(t, "application/problem+json", w.Header().Get("Content-Type"))
+ err = json.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+
+ // Prefer YAML
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/error", nil)
+ req.Header.Set("Accept", "application/yaml")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Equal(t, "application/problem+yaml", w.Header().Get("Content-Type"))
+ err = yaml.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.EqualValues(t, expected, parsed)
+
+ // Prefer CBOR
+ w = httptest.NewRecorder()
+ req, _ = http.NewRequest(http.MethodGet, "/error", nil)
+ req.Header.Set("Accept", "application/cbor")
+ app.ServeHTTP(w, req)
+
+ assert.Equal(t, http.StatusBadRequest, w.Code)
+ assert.Equal(t, "application/problem+cbor", w.Header().Get("Content-Type"))
+ err = cbor.Unmarshal(w.Body.Bytes(), &parsed)
+ assert.NoError(t, err)
+ assert.Equal(t, expected, parsed)
+}
+
+func TestInvalidModel(t *testing.T) {
+ type R1 struct {
+ Foo string `json:"foo"`
+ }
+
+ type R2 struct {
+ Bar string `json:"bar"`
+ }
+
+ app := newTestRouter()
+
+ app.Resource("/bad-status").Get("test", "Test",
+ NewResponse(http.StatusOK, "desc").Model(R1{}),
+ ).Run(func(ctx Context) {
+ ctx.WriteModel(http.StatusNoContent, R2{Bar: "blah"})
+ })
+
+ app.Resource("/bad-model").Get("test", "Test",
+ NewResponse(http.StatusOK, "desc").Model(R1{}),
+ ).Run(func(ctx Context) {
+ ctx.WriteModel(http.StatusOK, R2{Bar: "blah"})
+ })
+
+ assert.Panics(t, func() {
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/bad-status", nil)
+ app.ServeHTTP(w, req)
+ })
+
+ assert.Panics(t, func() {
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/bad-model", nil)
+ app.ServeHTTP(w, req)
+ })
+}
+
+func TestInvalidHeader(t *testing.T) {
+ app := newTestRouter()
+
+ app.Resource("/").Get("test", "Test",
+ NewResponse(http.StatusNoContent, "desc").Headers("Extra"),
+ ).Run(func(ctx Context) {
+ // Typo in the header should not be allowed
+ ctx.Header().Set("Extra2", "some-value")
+ ctx.WriteHeader(http.StatusNoContent)
+ })
+
+ assert.Panics(t, func() {
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
+ app.ServeHTTP(w, req)
+ })
+}
+
+func TestWriteAfterClose(t *testing.T) {
+ app := newTestRouter()
+
+ app.Resource("/").Get("test", "Test",
+ NewResponse(http.StatusBadRequest, "desc").Model(&ErrorModel{}),
+ ).Run(func(ctx Context) {
+ ctx.WriteError(http.StatusBadRequest, "some error")
+ // Second write should fail
+ ctx.WriteError(http.StatusBadRequest, "some error")
+ })
+
+ assert.Panics(t, func() {
+ w := httptest.NewRecorder()
+ req, _ := http.NewRequest(http.MethodGet, "/", nil)
+ app.ServeHTTP(w, req)
+ })
+}
diff --git a/dependency.go b/dependency.go
deleted file mode 100644
index ede327e5..00000000
--- a/dependency.go
+++ /dev/null
@@ -1,249 +0,0 @@
-package huma
-
-import (
- "errors"
- "fmt"
- "net"
- "reflect"
-
- "github.com/gin-gonic/gin"
-)
-
-// ErrDependencyInvalid is returned when registering a dependency fails.
-var ErrDependencyInvalid = errors.New("dependency invalid")
-
-// openAPIDependency represents a handler function dependency and its associated
-// inputs and outputs. Value can be either a struct pointer (global dependency)
-// or a `func(dependencies, params) (headers, struct pointer, error)` style
-// function.
-type openAPIDependency struct {
- dependencies []*openAPIDependency
- params []*openAPIParam
- responseHeaders []*openAPIResponseHeader
- handler interface{}
-}
-
-// newDependency returns a dependency with the given option and a handler
-// function.
-func newDependency(option DependencyOption, handler interface{}) *openAPIDependency {
- d := &openAPIDependency{
- dependencies: make([]*openAPIDependency, 0),
- params: make([]*openAPIParam, 0),
- responseHeaders: make([]*openAPIResponseHeader, 0),
- handler: handler,
- }
-
- if option != nil {
- option.applyDependency(d)
- }
-
- return d
-}
-
-var contextDependency openAPIDependency
-var ginContextDependency openAPIDependency
-var operationIDDependency openAPIDependency
-
-// ContextDependency returns a dependency for the current request's
-// `context.Context`. This is useful for timeouts & cancellation.
-func ContextDependency() DependencyOption {
- return &dependencyOption{func(d *openAPIDependency) {
- d.dependencies = append(d.dependencies, &contextDependency)
- }}
-}
-
-// GinContextDependency returns a dependency for the current request's
-// `*gin.Context`.
-func GinContextDependency() DependencyOption {
- return &dependencyOption{func(d *openAPIDependency) {
- d.dependencies = append(d.dependencies, &ginContextDependency)
- }}
-}
-
-// OperationIDDependency returns a dependency for the current `*huma.Operation`.
-func OperationIDDependency() DependencyOption {
- return &dependencyOption{func(d *openAPIDependency) {
- d.dependencies = append(d.dependencies, &operationIDDependency)
- }}
-}
-
-// ConnDependency returns the underlying `net.Conn` for the current request.
-func ConnDependency() DependencyOption {
- dep := newDependency(GinContextDependency(),
- func(c *gin.Context) (net.Conn, error) {
- return getConn(c.Request), nil
- })
-
- return &dependencyOption{func(d *openAPIDependency) {
- d.dependencies = append(d.dependencies, dep)
- }}
-}
-
-// validate that the dependency deps/params/headers match the function
-// signature or that the value is not a function.
-func (d *openAPIDependency) validate(returnType reflect.Type) {
- if d == &contextDependency || d == &ginContextDependency || d == &operationIDDependency {
- // Hard-coded known dependencies. These are special and have no value.
- return
- }
-
- if d.handler == nil {
- panic(fmt.Errorf("handler must be set: %w", ErrDependencyInvalid))
- }
-
- v := reflect.ValueOf(d.handler)
-
- if v.Kind() != reflect.Func {
- if returnType != nil && returnType != v.Type() && !v.Type().Implements(returnType) {
- panic(fmt.Errorf("return type should be %s but got %s: %w", v.Type(), returnType, ErrDependencyInvalid))
- }
-
- // This is just a static value. It shouldn't have params/headers/etc.
- if len(d.params) > 0 {
- panic(fmt.Errorf("global dependency should not have params: %w", ErrDependencyInvalid))
- }
-
- if len(d.responseHeaders) > 0 {
- panic(fmt.Errorf("global dependency should not set headers: %w", ErrDependencyInvalid))
- }
-
- return
- }
-
- fn := v.Type()
- lenArgs := len(d.dependencies) + len(d.params)
- if fn.NumIn() != lenArgs {
- // TODO: generate suggested func signature
- panic(fmt.Errorf("function signature should have %d args but got %s: %w", lenArgs, fn, ErrDependencyInvalid))
- }
-
- for _, dep := range d.dependencies {
- dep.validate(nil)
- }
-
- for i, p := range d.params {
- p.validate(fn.In(len(d.dependencies) + i))
- }
-
- lenReturn := len(d.responseHeaders) + 2
- if fn.NumOut() != lenReturn {
- panic(fmt.Errorf("function should return %d values but got %d: %w", lenReturn, fn.NumOut(), ErrDependencyInvalid))
- }
-
- for i, h := range d.responseHeaders {
- h.validate(fn.Out(i))
- }
-}
-
-// allParams returns all parameters for all dependencies in the graph of this
-// dependency in depth-first order without duplicates.
-func (d *openAPIDependency) allParams() []*openAPIParam {
- params := []*openAPIParam{}
- seen := map[*openAPIParam]bool{}
-
- for _, p := range d.params {
- seen[p] = true
- params = append(params, p)
- }
-
- for _, d := range d.dependencies {
- for _, p := range d.allParams() {
- if _, ok := seen[p]; !ok {
- seen[p] = true
-
- params = append(params, p)
- }
- }
- }
-
- return params
-}
-
-// allResponseHeaders returns all response headers for all dependencies in
-// the graph of this dependency in depth-first order without duplicates.
-func (d *openAPIDependency) allResponseHeaders() []*openAPIResponseHeader {
- headers := []*openAPIResponseHeader{}
- seen := map[*openAPIResponseHeader]bool{}
-
- for _, h := range d.responseHeaders {
- seen[h] = true
- headers = append(headers, h)
- }
-
- for _, d := range d.dependencies {
- for _, h := range d.allResponseHeaders() {
- if _, ok := seen[h]; !ok {
- seen[h] = true
-
- headers = append(headers, h)
- }
- }
- }
-
- return headers
-}
-
-// resolve the value of the dependency. Returns (response headers, value, error).
-func (d *openAPIDependency) resolve(c *gin.Context, op *openAPIOperation) (map[string]string, interface{}, error) {
- // Identity dependencies are first. Just return if it's one of them.
- if d == &contextDependency {
- return nil, c.Request.Context(), nil
- }
-
- if d == &ginContextDependency {
- return nil, c, nil
- }
-
- if d == &operationIDDependency {
- return nil, op.id, nil
- }
-
- v := reflect.ValueOf(d.handler)
- if v.Kind() != reflect.Func {
- // Not a function, just return the global value.
- return nil, d.handler, nil
- }
-
- // Generate the input arguments
- in := make([]reflect.Value, 0, v.Type().NumIn())
- headers := map[string]string{}
-
- // Resolve each sub-dependency
- for _, dep := range d.dependencies {
- dHeaders, dVal, err := dep.resolve(c, op)
- if err != nil {
- return nil, nil, err
- }
-
- for h, hv := range dHeaders {
- headers[h] = hv
- }
-
- in = append(in, reflect.ValueOf(dVal))
- }
-
- // Get each input parameter
- for _, param := range d.params {
- v, ok := getParamValue(c, param)
- if !ok {
- return nil, nil, fmt.Errorf("could not get param value")
- }
-
- in = append(in, reflect.ValueOf(v))
- }
-
- // Call the function.
- out := v.Call(in)
-
- if last := out[len(out)-1]; !last.IsNil() {
- // There was an error!
- return nil, nil, last.Interface().(error)
- }
-
- // Get the headers & response value.
- for i, h := range d.responseHeaders {
- headers[h.Name] = out[i].Interface().(string)
- }
-
- return headers, out[len(d.responseHeaders)].Interface(), nil
-}
diff --git a/dependency_test.go b/dependency_test.go
deleted file mode 100644
index 08681f93..00000000
--- a/dependency_test.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package huma
-
-import (
- "context"
- "net/http"
- "net/http/httptest"
- "reflect"
- "testing"
-
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/assert"
-)
-
-func TestGlobalDepEmpty(t *testing.T) {
- d := openAPIDependency{}
-
- typ := reflect.TypeOf(123)
-
- assert.Panics(t, func() {
- d.validate(typ)
- })
-}
-
-func TestGlobalDepWrongType(t *testing.T) {
- d := openAPIDependency{
- handler: "test",
- }
-
- typ := reflect.TypeOf(123)
-
- assert.Panics(t, func() {
- d.validate(typ)
- })
-}
-
-func TestDepContext(t *testing.T) {
- d := openAPIDependency{
- dependencies: []*openAPIDependency{
- &contextDependency,
- },
- handler: func(ctx context.Context) (context.Context, error) { return ctx, nil },
- }
-
- mock, _ := gin.CreateTestContext(nil)
- mock.Request = httptest.NewRequest("GET", "/", nil)
-
- typ := reflect.TypeOf(mock)
- d.validate(typ)
-
- _, v, err := d.resolve(mock, &openAPIOperation{})
- assert.NoError(t, err)
- assert.Equal(t, v, mock.Request.Context())
-}
-
-func TestDepGinContext(t *testing.T) {
- d := openAPIDependency{
- dependencies: []*openAPIDependency{
- &ginContextDependency,
- },
- handler: func(c *gin.Context) (*gin.Context, error) { return c, nil },
- }
-
- mock, _ := gin.CreateTestContext(nil)
-
- typ := reflect.TypeOf(mock)
- d.validate(typ)
-
- _, v, err := d.resolve(mock, &openAPIOperation{})
- assert.NoError(t, err)
- assert.Equal(t, v, mock)
-}
-
-func TestDepOperationID(t *testing.T) {
- d := openAPIDependency{
- dependencies: []*openAPIDependency{
- &operationIDDependency,
- },
- handler: func(id string) (string, error) { return id, nil },
- }
-
- mock := &openAPIOperation{
- id: "test-id",
- }
-
- typ := reflect.TypeOf(mock)
- d.validate(typ)
-
- _, v, err := d.resolve(&gin.Context{}, mock)
- assert.NoError(t, err)
- assert.Equal(t, v, "test-id")
-}
-func TestDepFuncWrongArgs(t *testing.T) {
- d := &openAPIDependency{}
-
- Dependency(HeaderParam("foo", "desc", ""), func() (string, error) {
- return "", nil
- }).applyDependency(d)
-
- assert.Panics(t, func() {
- d.validate(reflect.TypeOf(""))
- })
-}
-
-func TestDepFunc(t *testing.T) {
- d := openAPIDependency{
- handler: func(xin string) (string, string, error) {
- return "xout", "value", nil
- },
- }
-
- DependencyOptions(
- HeaderParam("x-in", "desc", ""),
- ResponseHeader("x-out", "desc"),
- ).applyDependency(&d)
-
- c := &gin.Context{
- Request: &http.Request{
- Header: http.Header{
- "x-in": []string{"xin"},
- },
- },
- }
-
- d.validate(reflect.TypeOf(""))
- h, v, err := d.resolve(c, &openAPIOperation{})
- assert.NoError(t, err)
- assert.Equal(t, "xout", h["x-out"])
- assert.Equal(t, "value", v)
-}
diff --git a/docs.go b/docs.go
index c3cce820..7dce3514 100644
--- a/docs.go
+++ b/docs.go
@@ -2,9 +2,8 @@ package huma
import (
"fmt"
+ "net/http"
"strings"
-
- "github.com/gin-gonic/gin"
)
// splitDocs will split a single string out into a title/description combo.
@@ -19,10 +18,11 @@ func splitDocs(docs string) (title, desc string) {
return
}
-// rapiDocTemplate is the template used to generate the RapiDoc. It needs two args to render:
-// 1. the title
-// 2. the path to the openapi.yaml file
-var rapiDocTemplate = `
+// RapiDocHandler renders documentation using RapiDoc.
+func RapiDocHandler(router *Router) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html")
+ w.Write([]byte(fmt.Sprintf(`