这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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