Skip to content

Commit

Permalink
Merge pull request #279 from danielgtaylor/convenience
Browse files Browse the repository at this point in the history
feat: add convenience methods
  • Loading branch information
danielgtaylor authored Mar 5, 2024
2 parents 895f500 + 952d68b commit 279b6f2
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 135 deletions.
32 changes: 18 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Features include:

- Declarative interface on top of your router of choice:
- Operation & model documentation
- Request params (path, query, or header)
- Request params (path, query, header, or cookie)
- Request body
- Responses (including errors)
- Response headers
Expand Down Expand Up @@ -61,6 +61,8 @@ This project was inspired by [FastAPI](https://fastapi.tiangolo.com/). Logo & br
> Thank you Daniel for Huma. Superbly useful project and saves us a lot of time and hassle thanks to the OpenAPI gen — similar to FastAPI in Python. - [WolvesOfAllStreets](https://www.reddit.com/r/golang/comments/1aqj99d/comment/kqfqcml/?utm_source=reddit&utm_medium=web2x&context=3)
> Huma is wonderful, I've started working with it recently, and it's a pleasure, so thank you very much for your efforts 🙏 - [callmemicah](https://www.reddit.com/r/golang/comments/1b32ts4/comment/ksvr9h7/?utm_source=reddit&utm_medium=web2x&context=3)
# Install

Install via `go get`. Note that Go 1.20 or newer is required.
Expand All @@ -87,16 +89,11 @@ import (
"github.com/go-chi/chi/v5"
)

// Options for the CLI.
// Options for the CLI. Pass `--port` or set the `SERVICE_PORT` env var.
type Options struct {
Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
Body struct {
Expand All @@ -111,13 +108,10 @@ func main() {
router := chi.NewMux()
api := humachi.New(router, huma.DefaultConfig("My API", "1.0.0"))

// Register GET /greeting/{name}
huma.Register(api, huma.Operation{
OperationID: "get-greeting",
Summary: "Get a greeting",
Method: http.MethodGet,
Path: "/greeting/{name}",
}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
// Add the operation handler to the API.
huma.Get(api, "/greeting/{name}", func(ctx context.Context, input *struct{
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}) (*GreetingOutput, error) {
resp := &GreetingOutput{}
resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
return resp, nil
Expand All @@ -134,6 +128,9 @@ func main() {
}
```

> [!TIP]
> Replace `chi.NewMux()``http.NewServeMux()` and `humachi.New``humago.New` to use the standard library router from Go 1.22+. Just make sure your `go.mod` has `go 1.22` or newer listed in it. Everything else stays the same! Switch whenever you are ready.
You can test it with `go run greet.go` (optionally pass `--port` to change the default) and make a sample request using [Restish](https://rest.sh/) (or `curl`):

```sh
Expand All @@ -149,6 +146,8 @@ HTTP/1.1 200 OK

Even though the example is tiny you can also see some generated documentation at http://localhost:8888/docs. The generated OpenAPI is available at http://localhost:8888/openapi.json or http://localhost:8888/openapi.yaml.

Check out the [Huma tutorial](https://huma.rocks/tutorial/installation/) for a step-by-step guide to get started.

# Documentation

See the [https://huma.rocks/](https://huma.rocks/) website for full documentation in a presentation that's easier to navigate and search then this README. You can find the source for the site in the `docs` directory of this repo.
Expand All @@ -162,3 +161,8 @@ Official Go package documentation can always be found at https://pkg.go.dev/gith
- [Golang News & Libs & Jobs shared on Twitter/X](https://twitter.com/golangch/status/1752175499701264532)
- Featured in [Go Weekly #495](https://golangweekly.com/issues/495)
- [Bump.sh Deploying Docs from Huma](https://docs.bump.sh/guides/bump-sh-tutorials/huma/)
- Mentioned in [Composable HTTP Handlers Using Generics](https://www.willem.dev/articles/generic-http-handlers/)

Be sure to star the project if you find it useful!

[![Star History Chart](https://api.star-history.com/svg?repos=danielgtaylor/huma&type=Date)](https://star-history.com/#danielgtaylor/huma&Date)
24 changes: 23 additions & 1 deletion docs/docs/features/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,31 @@ huma.Register(api, huma.Operation{
$ restish your-api your-operation-name --param=value ...
```

### Convenience Methods

A number of convenience methods are provided if you don't want to use the `huma.Operation` struct directly. The following are available:

- `huma.Get`
- `huma.Post`
- `huma.Put`
- `huma.Patch`
- `huma.Delete`

These methods are equivalent to using `huma.Register` with the `Method` field set to the corresponding HTTP method, and they generate the operation ID for you based on the path. For example:

```go title="code.go"
huma.Get(api, "/things/{thing-id}", func(ctx context.Context, input *YourInput) (*YourOutput, error) {
// ... Implementation goes here ...
})
```

The generated operation ID for the above example would be `get-things-by-thing-id`. Override `huma.GenerateOperationID(method, path string, response any)` to customize the generated operation IDs.

This makes it easy to get started, particularly if coming from other frameworks, and you can simply switch to using `huma.Register` if/when you need to set additional fields on the operation.

## Handler Function

The operation handler function always has the following generic format, where `Input` and `Output` are custom structs defined by the developer that represent the entirety of the request (path/query/header params & body) and response (headers & body), respectively:
The operation handler function _always_ has the following generic format, where `Input` and `Output` are custom structs defined by the developer that represent the entirety of the request (path/query/header/cookie params & body) and response (headers & body), respectively:

```go title="code.go"
func(context.Context, *Input) (*Output, error)
Expand Down
19 changes: 10 additions & 9 deletions docs/docs/tutorial/sending-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Response: 201 Created

Add a new operation to our API that allows users to submit reviews of our product.

```go title="main.go" linenums="1" hl_lines="30-37 58-67"
```go title="main.go" linenums="1" hl_lines="25-32 57-68"
package main

import (
Expand All @@ -40,11 +40,6 @@ type Options struct {
Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
Body struct {
Expand All @@ -71,20 +66,26 @@ func main() {
// Register GET /greeting/{name}
huma.Register(api, huma.Operation{
OperationID: "get-greeting",
Summary: "Get a greeting",
Method: http.MethodGet,
Path: "/greeting/{name}",
}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
Summary: "Get a greeting",
Description: "Get a greeting for a person by name.",
Tags: []string{"Greetings"},
}, func(ctx context.Context, input *struct{
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}) (*GreetingOutput, error) {
resp := &GreetingOutput{}
resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
return resp, nil
})

// Register POST /reviews
huma.Register(api, huma.Operation{
OperationID: "post-review",
Summary: "Post a review",
Method: http.MethodPost,
Path: "/reviews",
Summary: "Post a review",
Tags: []string{"Reviews"},
DefaultStatus: http.StatusCreated,
}, func(ctx context.Context, i *ReviewInput) (*struct{}, error) {
// TODO: save review in data store.
Expand Down
15 changes: 7 additions & 8 deletions docs/docs/tutorial/service-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Huma includes a basic command-line and environment variable option parser that c

[Your first API](your-first-api.md#operation) can be updated to take an optional network port parameter like this:

```go title="main.go" linenums="1" hl_lines="13-16 31-32 49-57"
```go title="main.go" linenums="1" hl_lines="13-16 26-27 48-56"
package main

import (
Expand All @@ -28,11 +28,6 @@ type Options struct {
Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
Body struct {
Expand All @@ -50,10 +45,14 @@ func main() {
// Register GET /greeting/{name}
huma.Register(api, huma.Operation{
OperationID: "get-greeting",
Summary: "Get a greeting",
Method: http.MethodGet,
Path: "/greeting/{name}",
}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
Summary: "Get a greeting",
Description: "Get a greeting for a person by name.",
Tags: []string{"Greetings"},
}, func(ctx context.Context, input *struct{
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}) (*GreetingOutput, error) {
resp := &GreetingOutput{}
resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
return resp, nil
Expand Down
19 changes: 10 additions & 9 deletions docs/docs/tutorial/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Huma provides a number of helpers for testing your API. The most important is th

First, modify the service code to make it easier to test, by moving the operation registration code out of the `main` function:

```go title="main.go" linenums="1" hl_lines="39 62 71"
```go title="main.go" linenums="1" hl_lines="34 63 72"
package main

import (
Expand All @@ -28,11 +28,6 @@ type Options struct {
Port int `help:"Port to listen on" short:"p" default:"8888"`
}

// GreetingInput represents the greeting operation request.
type GreetingInput struct {
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}

// GreetingOutput represents the greeting operation response.
type GreetingOutput struct {
Body struct {
Expand All @@ -53,20 +48,26 @@ func addRoutes(api huma.API) {
// Register GET /greeting/{name}
huma.Register(api, huma.Operation{
OperationID: "get-greeting",
Summary: "Get a greeting",
Method: http.MethodGet,
Path: "/greeting/{name}",
}, func(ctx context.Context, input *GreetingInput) (*GreetingOutput, error) {
Summary: "Get a greeting",
Description: "Get a greeting for a person by name.",
Tags: []string{"Greetings"},
}, func(ctx context.Context, input *struct{
Name string `path:"name" maxLength:"30" example:"world" doc:"Name to greet"`
}) (*GreetingOutput, error) {
resp := &GreetingOutput{}
resp.Body.Message = fmt.Sprintf("Hello, %s!", input.Name)
return resp, nil
})

// Register POST /reviews
huma.Register(api, huma.Operation{
OperationID: "post-review",
Summary: "Post a review",
Method: http.MethodPost,
Path: "/reviews",
Summary: "Post a review",
Tags: []string{"Reviews"},
DefaultStatus: http.StatusCreated,
}, func(ctx context.Context, i *ReviewInput) (*struct{}, error) {
// TODO: save review in data store.
Expand Down
Loading

0 comments on commit 279b6f2

Please sign in to comment.