Skip to content

Commit

Permalink
Merge pull request #158 from danielgtaylor/docs-updates-2
Browse files Browse the repository at this point in the history
docs: lots of docs updates, a few more tests/examples
  • Loading branch information
danielgtaylor authored Oct 28, 2023
2 parents b61d4d2 + 0c8a1aa commit 73a0760
Show file tree
Hide file tree
Showing 15 changed files with 1,323 additions and 140 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ This project was inspired by [FastAPI](https://fastapi.tiangolo.com/). Logo & br

# Install

Install via `go get`. Note that Go 1.20 or newer is required.

```sh
# After: go mod init ...
go get -u github.com/danielgtaylor/huma/v2
Expand Down Expand Up @@ -1112,6 +1114,7 @@ type Context interface {
Header(name string) string
EachHeader(cb func(name, value string))
BodyReader() io.Reader
GetMultipartForm() (*multipart.Form, error)
SetReadDeadline(time.Time) error
SetStatus(code int)
SetHeader(name, value string)
Expand Down
65 changes: 63 additions & 2 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ var resolverWithPathType = reflect.TypeOf((*ResolverWithPath)(nil)).Elem()
// routers and frameworks. It is designed to work with the standard library
// `http.Request` and `http.ResponseWriter` types as well as types like
// `gin.Context` or `fiber.Ctx` that provide both request and response
// functionality in one place.
// functionality in one place, by using the `huma.Context` interface which
// abstracts away those router-specific differences.
//
// The handler function takes uses the context to get request information like
// path / query / header params, the input body, and provide response data
// like a status code, response headers, and a response body.
type Adapter interface {
Handle(op *Operation, handler func(ctx Context))
ServeHTTP(http.ResponseWriter, *http.Request)
Expand All @@ -50,21 +55,54 @@ type Adapter interface {
// Context is the current request/response context. It provides a generic
// interface to get request information and write responses.
type Context interface {
// Operation returns the OpenAPI operation that matched the request.
Operation() *Operation

// Context returns the underlying request context.
Context() context.Context

// Method returns the HTTP method for the request.
Method() string

// Host returns the HTTP host for the request.
Host() string

// URL returns the full URL for the request.
URL() url.URL

// Param returns the value for the given path parameter.
Param(name string) string

// Query returns the value for the given query parameter.
Query(name string) string

// Header returns the value for the given header.
Header(name string) string

// EachHeader iterates over all headers and calls the given callback with
// the header name and value.
EachHeader(cb func(name, value string))

// BodyReader returns the request body reader.
BodyReader() io.Reader

// GetMultipartForm returns the parsed multipart form, if any.
GetMultipartForm() (*multipart.Form, error)

// SetReadDeadline sets the read deadline for the request body.
SetReadDeadline(time.Time) error

// SetStatus sets the HTTP status code for the response.
SetStatus(code int)

// SetHeader sets the given header to the given value, overwriting any
// existing value. Use `AppendHeader` to append a value instead.
SetHeader(name, value string)

// AppendHeader appends the given value to the given header.
AppendHeader(name, value string)

// BodyWriter returns the response body writer.
BodyWriter() io.Writer
}

Expand All @@ -85,7 +123,16 @@ type Config struct {
// to `/openapi` it will allow clients to get `/openapi.json` or
// `/openapi.yaml`, for example.
OpenAPIPath string
DocsPath string

// DocsPath is the path to the API documentation. If set to `/docs` it will
// allow clients to get `/docs` to view the documentation in a browser. If
// you wish to provide your own documentation renderer, you can leave this
// blank and attach it directly to the router or adapter.
DocsPath string

// SchemasPath is the path to the API schemas. If set to `/schemas` it will
// allow clients to get `/schemas/{schema}` to view the schema in a browser
// or for use in editors like VSCode to provide autocomplete & validation.
SchemasPath string

// Formats defines the supported request/response formats by content type or
Expand Down Expand Up @@ -229,6 +276,20 @@ func (a *api) Middlewares() Middlewares {
return a.middlewares
}

// NewAPI creates a new API with the given configuration and router adapter.
// You usually don't need to use this function directly, and can instead use
// the `New(...)` function provided by the adapter packages which call this
// function internally.
//
// When the API is created, this function will ensure a schema registry exists
// (or create a new map registry if not), will set a default format if not
// set, and will set up the handlers for the OpenAPI spec, documentation, and
// JSON schema routes if the paths are set in the config.
//
// router := chi.NewMux()
// adapter := humachi.NewAdapter(router)
// config := huma.DefaultConfig("Example API", "1.0.0")
// api := huma.NewAPI(config, adapter)
func NewAPI(config Config, a Adapter) API {
newAPI := &api{
config: config,
Expand Down
19 changes: 14 additions & 5 deletions autopatch/autopatch.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
// Package autopatch provides a way to automatically generate PATCH operations
// for resources which have a GET & PUT but no PATCH. This is useful for
// resources which are large and have many fields, but where the majority of
// updates are only to a few fields. This allows clients to send a partial
// update to the server without having to send the entire resource.
//
// JSON Merge Patch, JSON Patch, and Shorthand Merge Patch are supported as
// input formats.
package autopatch

import (
Expand Down Expand Up @@ -27,11 +35,12 @@ type jsonPatchOp struct {

var jsonPatchType = reflect.TypeOf([]jsonPatchOp{})

// AutoPatch generates HTTP PATCH operations for any resource which has a
// GET & PUT but no pre-existing PATCH operation. Generated PATCH operations
// will call GET, apply either `application/merge-patch+json` or
// `application/json-patch+json` patches, then call PUT with the updated
// resource. This method may be safely called multiple times.
// AutoPatch generates HTTP PATCH operations for any resource which has a GET &
// PUT but no pre-existing PATCH operation. Generated PATCH operations will call
// GET, apply either `application/merge-patch+json`,
// `application/json-patch+json`, or `application/merge-patch+shorthand`
// patches, then call PUT with the updated resource. This method may be safely
// called multiple times.
func AutoPatch(api huma.API) {
oapi := api.OpenAPI()
registry := oapi.Components.Schemas
Expand Down
9 changes: 9 additions & 0 deletions autopatch/autopatch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,15 @@ func TestPatch(t *testing.T) {
)
assert.Equal(t, http.StatusNotModified, w.Code, w.Body.String())

// Extra headers should not be a problem, including `Accept`.
w = api.Patch("/things/test",
"Content-Type: application/merge-patch+json",
"Accept: application/json",
"X-Some-Other: value",
strings.NewReader(`{"price": 1.23}`),
)
assert.Equal(t, http.StatusNotModified, w.Code, w.Body.String())

app := api.Adapter()
// New change but with wrong manual ETag, should fail!
w = httptest.NewRecorder()
Expand Down
53 changes: 53 additions & 0 deletions autoregister_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package huma_test

import (
"context"
"fmt"
"net/http"

"github.com/danielgtaylor/huma/v2"
"github.com/go-chi/chi/v5"
)

// Item represents a single item with a unique ID.
type Item struct {
ID string `json:"id"`
}

// ItemsResponse is a response containing a list of items.
type ItemsResponse struct {
Body []Item `json:"body"`
}

// ItemsHandler handles item-related CRUD operations.
type ItemsHandler struct{}

// RegisterListItems registers the `list-items` operation with the given API.
// Because the method starts with `Register` it will be automatically called
// by `huma.AutoRegister` down below.
func (s *ItemsHandler) RegisterListItems(api huma.API) {
// Register a list operation to get all the items.
huma.Register(api, huma.Operation{
OperationID: "list-items",
Method: http.MethodGet,
Path: "/items",
}, func(ctx context.Context, input *struct{}) (*ItemsResponse, error) {
resp := &ItemsResponse{}
resp.Body = []Item{{ID: "123"}}
return resp, nil
})
}

func ExampleAutoRegister() {
// Create the router and API.
router := chi.NewMux()
api := NewExampleAPI(router, huma.DefaultConfig("My Service", "1.0.0"))

// Create the item handler and register all of its operations.
itemsHandler := &ItemsHandler{}
huma.AutoRegister(api, itemsHandler)

// Confirm the list operation was registered.
fmt.Println(api.OpenAPI().Paths["/items"].Get.OperationID)
// Output: list-items
}
11 changes: 11 additions & 0 deletions conditional/params.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
// Package conditional provides utilities for working with HTTP conditional
// requests using the `If-Match`, `If-None-Match`, `If-Modified-Since`, and
// `If-Unmodified-Since` headers along with ETags and last modified times.
//
// In general, conditional requests with tight integration into your data
// store will be preferred as they are more efficient. However, this package
// provides a simple way to get started with conditional requests and once
// the functionality is in place the performance can be improved later. You
// still get the benefits of not sending extra data over the wire and
// distributed write protections that prevent different users from
// overwriting each other's changes.
package conditional

import (
Expand Down
16 changes: 16 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,19 @@ func TestNegotiateError(t *testing.T) {

assert.Error(t, huma.WriteErr(api, ctx, 400, "bad request"))
}

func TestTransformError(t *testing.T) {
config := huma.DefaultConfig("Test API", "1.0.0")
config.Transformers = []huma.Transformer{
func(ctx huma.Context, status string, v any) (any, error) {
return nil, fmt.Errorf("whoops")
},
}
_, api := humatest.New(t, config)

req, _ := http.NewRequest("GET", "/", nil)
resp := httptest.NewRecorder()
ctx := humatest.NewContext(nil, req, resp)

assert.Error(t, huma.WriteErr(api, ctx, 400, "bad request"))
}
2 changes: 1 addition & 1 deletion examples/param-reuse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Options struct {
// ReusableParam is a reusable parameter that can go in the path or the body
// of a request or response. The same validation applies to both places.
type ReusableParam struct {
User string `json:"user" path:"user" maxLength:"10"`
User string `path:"user" json:"user" maxLength:"10"`
}

type MyResponse struct {
Expand Down
7 changes: 7 additions & 0 deletions huma.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
// Package huma provides a framework for building REST APIs in Go. It is
// designed to be simple, fast, and easy to use. It is also designed to
// generate OpenAPI 3.1 specifications and JSON Schema documents
// describing the API and providing a quick & easy way to generate
// docs, mocks, SDKs, CLI clients, and more.
//
// https://huma.rocks/
package huma

import (
Expand Down
3 changes: 3 additions & 0 deletions negotiation/negotiation.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Package negotiation provides utilities for working with HTTP client-
// driven content negotiation. It provides a zero-allocation utility for
// determining the best content type for the server to encode a response.
package negotiation

import (
Expand Down
Loading

0 comments on commit 73a0760

Please sign in to comment.