这是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
55 changes: 44 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Features include:
- SDKs with [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator)
- CLI with [Restish](https://rest.sh/)
- And [plenty](https://openapi.tools/) [more](https://apis.guru/awesome-openapi3/category.html)
- Generates JSON Schema for each resource using `describedby` link relation headers as well as optional `$schema` properties in returned objects that integrate into editors for validation & completion.

This project was inspired by [FastAPI](https://fastapi.tiangolo.com/). Look at the [benchmarks](https://github.com/danielgtaylor/huma/tree/master/benchmark) to see how Huma compares.

Expand Down Expand Up @@ -788,7 +789,16 @@ 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
## OpenAPI

By default, the generated OpenAPI and autogenerated documentation are served in the root at `/openapi.json` and `/docs` respectively. This default path can be modified:

```go
// Serve `/public/openapi.json and `/public/docs`:
app.DocsPrefix("/public")
```

### Custom OpenAPI Fields

Use the OpenAPI hook for OpenAPI customization. It gives you a `*gabs.Container` instance that represents the root of the OpenAPI document.

Expand All @@ -803,23 +813,28 @@ app.OpenAPIHook(modify)

> :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.

## CLI
### JSON Schema

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.
Each resource operation also returns a `describedby` HTTP link relation which references a JSON-Schema file. These schemas re-use the `DocsPrefix` described above and default to the server root. For example:

```go
app := cli.NewRouter("My API", "1.0.0")
```http
Link: </schemas/Note.json>; rel="describedby"
```

// Do resource/operation setup here...
Object resources (i.e. not arrays) can also optionally return a `$schema` property with such a link, which enables the described-by relationship to outlive the HTTP request (i.e. saving the body to a file for later editing) and enables some editors like [VSCode](https://code.visualstudio.com/docs/languages/json#_mapping-in-the-json) to provide code completion and validation as you type.

app.Run()
```json
{
"$schema": "http://localhost:8888/schemas/Note.json",
"title": "I am a note title",
"contents": "Example note contents",
"labels": ["todo"]
}
```

Then run the service:
Operations which accept objects as input will ignore the `$schema` property, so it is safe to submit back to the API.

```sh
$ go run yourservice.go --help
```
This feature can be disabled if desired by using `app.DisableSchemaProperty`.

## GraphQL

Expand Down Expand Up @@ -981,6 +996,24 @@ Result:
220 complexity
```

## CLI

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.

```go
app := cli.NewRouter("My API", "1.0.0")

// Do resource/operation setup here...

app.Run()
```

Then run the service:

```sh
$ go run yourservice.go --help
```

## CLI Runtime Arguments & Configuration

The CLI can be configured in multiple ways. In order of decreasing precedence:
Expand Down
67 changes: 61 additions & 6 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/danielgtaylor/huma/negotiation"
"github.com/fxamacker/cbor/v2"
"github.com/goccy/go-yaml"
"github.com/mitchellh/mapstructure"
)

// allowedHeaders is a list of built-in headers that are always allowed without
Expand Down Expand Up @@ -70,10 +71,13 @@ type Context interface {
type hcontext struct {
context.Context
http.ResponseWriter
r *http.Request
errors []error
op *Operation
closed bool
r *http.Request
errors []error
op *Operation
closed bool
docsPrefix string
urlPrefix string
disableSchemaProperty bool
}

func (c *hcontext) WithValue(key, value interface{}) Context {
Expand Down Expand Up @@ -193,8 +197,24 @@ func (c *hcontext) WriteModel(status int, model interface{}) {
c.writeModel(ct, status, model)
}

// URLPrefix returns the prefix to use for non-relative URL links.
func (c *hcontext) URLPrefix() string {
if c.urlPrefix != "" {
return c.urlPrefix
}

scheme := "https"
if strings.HasPrefix(c.r.Host, "localhost") {
scheme = "http"
}

return scheme + "://" + c.r.Host
}

func (c *hcontext) writeModel(ct string, status int, model interface{}) {
// Is this allowed? Find the right response.
modelRef := ""
modelType := reflect.TypeOf(model)
if c.op != nil {
responses := []Response{}
names := []string{}
Expand All @@ -215,14 +235,49 @@ func (c *hcontext) writeModel(ct string, status int, model interface{}) {

found := false
for _, r := range responses {
if r.model == reflect.TypeOf(model) {
if r.model == modelType {
found = true
modelRef = r.modelRef
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))
panic(fmt.Errorf("Invalid model %s, expecting %s for %s %s", modelType, strings.Join(names, ", "), c.r.Method, c.r.URL.Path))
}
}

// If possible, insert a link relation header to the JSON Schema describing
// this response. If it's an object (not an array), then we can also try
// inserting the `$schema` key to make editing & validation easier.
parts := strings.Split(modelRef, "/")
if len(parts) > 0 {
id := parts[len(parts)-1]

link := c.Header().Get("Link")
if link != "" {
link += ", "
}
link += "<" + c.docsPrefix + "/schemas/" + id + ".json>; rel=\"describedby\""
c.Header().Set("Link", link)

if !c.disableSchemaProperty && modelType != nil && modelType.Kind() == reflect.Struct {
tmp := map[string]interface{}{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
TagName: "json",
Result: &tmp,
})
if err != nil {
panic(fmt.Errorf("Unable to initialize struct decoder: %w", err))
}
err = decoder.Decode(model)
if err != nil {
panic(fmt.Errorf("Unable to convert struct to map: %w", err))
}
if tmp["$schema"] == nil {
tmp["$schema"] = c.URLPrefix() + c.docsPrefix + "/schemas/" + id + ".json"
}
model = tmp
}
}

Expand Down
2 changes: 1 addition & 1 deletion context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"net/http/httptest"
"testing"

"github.com/fxamacker/cbor"
"github.com/fxamacker/cbor/v2"
"github.com/goccy/go-yaml"
"github.com/stretchr/testify/assert"
)
Expand Down
36 changes: 16 additions & 20 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,32 @@ module github.com/danielgtaylor/huma
go 1.13

require (
github.com/Jeffail/gabs/v2 v2.6.0
github.com/andybalholm/brotli v1.0.0
github.com/Jeffail/gabs/v2 v2.6.1
github.com/andybalholm/brotli v1.0.4
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/danielgtaylor/casing v0.0.0-20210126043903-4e55e6373ac3
github.com/fatih/structs v1.1.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/fxamacker/cbor v1.5.1
github.com/fxamacker/cbor/v2 v2.2.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/go-chi/chi v4.1.2+incompatible
github.com/goccy/go-yaml v1.8.1
github.com/goccy/go-yaml v1.9.5
github.com/graphql-go/graphql v0.8.0
github.com/graphql-go/handler v0.2.3
github.com/koron-go/gqlcost v0.2.2
github.com/magiconair/properties v1.8.2 // indirect
github.com/mattn/go-isatty v0.0.12
github.com/mitchellh/mapstructure v1.3.3 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mattn/go-isatty v0.0.14
github.com/mitchellh/mapstructure v1.4.3
github.com/opentracing/opentracing-go v1.2.0
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/spf13/afero v1.3.4 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/cobra v1.0.0
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.7.1
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tent/http-link-go v0.0.0-20130702225549-ac974c61c2f9
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonschema v1.2.0
go.uber.org/zap v1.15.0
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
gopkg.in/ini.v1 v1.60.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
)
Loading