Skip to content

proposal: support docs on separate adapter #775

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion adapters/humachi/humachi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,9 @@ func TestChiRouterPrefix(t *testing.T) {
mux.Route("/api", func(r chi.Router) {
config := huma.DefaultConfig("My API", "1.0.0")
config.Servers = []*huma.Server{{URL: "http://localhost:8888/api"}}
// config.CreateHooks = []func(huma.Config) huma.Config{
// huma.DefaultSchemaLinkHook("/api"),
// }
api = New(r, config)
})

Expand Down Expand Up @@ -350,7 +353,8 @@ func TestChiRouterPrefix(t *testing.T) {
// The docs HTML should point to the full URL including base path.
resp = tapi.Get("/api/docs")
assert.Equal(t, http.StatusOK, resp.Code)
assert.Contains(t, resp.Body.String(), "/api/openapi.yaml")
// The openapi.yaml should be a relative path.
assert.Contains(t, resp.Body.String(), `apiDescriptionUrl="openapi.yaml"`)
}

// func BenchmarkHumaV1Chi(t *testing.B) {
Expand Down
35 changes: 22 additions & 13 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"mime/multipart"
"net/http"
"net/url"
"path"
"reflect"
"regexp"
"strings"
Expand Down Expand Up @@ -181,6 +180,15 @@
// blank and attach it directly to the router or adapter.
DocsPath string

// DocsAdapter is an optional [Adapter] that will be used to serve the API
// documentation. If unset, the main adapter will be used.
// Setting this allows you to use a different router or middleware stack for
// the documentation.
// Note that if this [Adapter] has a different path than the main one, the
// default CreateHook set in [DefaultConfig] needs to be modified to account
// for it.
DocsAdapter Adapter

// 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.
Expand Down Expand Up @@ -362,7 +370,7 @@
// config := huma.DefaultConfig("Example API", "1.0.0")
// api := huma.NewAPI(config, adapter)
func NewAPI(config Config, a Adapter) API {
for i := 0; i < len(config.CreateHooks); i++ {
for i := range config.CreateHooks {
config = config.CreateHooks[i](config)
}

Expand Down Expand Up @@ -400,9 +408,14 @@
newAPI.formatKeys = append(newAPI.formatKeys, k)
}

docsAdapter := newAPI.adapter
if config.DocsAdapter != nil {
docsAdapter = config.DocsAdapter
}

Check warning on line 414 in api.go

View check run for this annotation

Codecov / codecov/patch

api.go#L413-L414

Added lines #L413 - L414 were not covered by tests

if config.OpenAPIPath != "" {
var specJSON []byte
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.OpenAPIPath + ".json",
}, func(ctx Context) {
Expand All @@ -413,7 +426,7 @@
ctx.BodyWriter().Write(specJSON)
})
var specJSON30 []byte
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.OpenAPIPath + "-3.0.json",
}, func(ctx Context) {
Expand All @@ -424,7 +437,7 @@
ctx.BodyWriter().Write(specJSON30)
})
var specYAML []byte
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.OpenAPIPath + ".yaml",
}, func(ctx Context) {
Expand All @@ -435,7 +448,7 @@
ctx.BodyWriter().Write(specYAML)
})
var specYAML30 []byte
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.OpenAPIPath + "-3.0.yaml",
}, func(ctx Context) {
Expand All @@ -448,14 +461,10 @@
}

if config.DocsPath != "" {
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.DocsPath,
}, func(ctx Context) {
openAPIPath := config.OpenAPIPath
if prefix := getAPIPrefix(newAPI.OpenAPI()); prefix != "" {
openAPIPath = path.Join(prefix, openAPIPath)
}
ctx.SetHeader("Content-Type", "text/html")
title := "Elements in HTML"
if config.Info != nil && config.Info.Title != "" {
Expand All @@ -476,7 +485,7 @@
<body style="height: 100vh;">

<elements-api
apiDescriptionUrl="` + openAPIPath + `.yaml"
apiDescriptionUrl="` + strings.TrimPrefix(config.OpenAPIPath, "/") + `.yaml"
router="hash"
layout="sidebar"
tryItCredentialsPolicy="same-origin"
Expand All @@ -488,7 +497,7 @@
}

if config.SchemasPath != "" {
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Method: http.MethodGet,
Path: config.SchemasPath + "/{schema}",
}, func(ctx Context) {
Expand Down
34 changes: 19 additions & 15 deletions defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"io"
)

// DefaultSchemaNamer is the default prefix used to reference schemas in the API spec.
const DefaultSchemaRefPrefix = "#/components/schemas/"

// DefaultJSONFormat is the default JSON formatter that can be set in the API's
// `Config.Formats` map. This is used by the `DefaultConfig` function.
//
Expand Down Expand Up @@ -53,10 +56,7 @@ var DefaultFormats = map[string]Format{
//
// import _ "github.com/danielgtaylor/huma/v2/formats/cbor"
func DefaultConfig(title, version string) Config {
schemaPrefix := "#/components/schemas/"
schemasPath := "/schemas"

registry := NewMapRegistry(schemaPrefix, DefaultSchemaNamer)
registry := NewMapRegistry(DefaultSchemaRefPrefix, DefaultSchemaNamer)

return Config{
OpenAPI: &OpenAPI{
Expand All @@ -71,20 +71,24 @@ func DefaultConfig(title, version string) Config {
},
OpenAPIPath: "/openapi",
DocsPath: "/docs",
SchemasPath: schemasPath,
SchemasPath: "/schemas",
Formats: DefaultFormats,
DefaultFormat: "application/json",
CreateHooks: []func(Config) Config{
func(c Config) Config {
// Add a link transformer to the API. This adds `Link` headers and
// puts `$schema` fields in the response body which point to the JSON
// Schema that describes the response structure.
// This is a create hook so we get the latest schema path setting.
linkTransformer := NewSchemaLinkTransformer(schemaPrefix, c.SchemasPath)
c.OpenAPI.OnAddOperation = append(c.OpenAPI.OnAddOperation, linkTransformer.OnAddOperation)
c.Transformers = append(c.Transformers, linkTransformer.Transform)
return c
},
DefaultSchemaLinkHook(""), // assume schemas are on mounted the root
},
}
}

// DefaultSchemaLinkHook adds a link transformer to the API.
// This adds `Link` headers and puts `$schema` fields in the response body which
// point to the JSON Schema that describes the response structure.
// This is a create hook so we get the latest schema path setting.
func DefaultSchemaLinkHook(schemaPathPrefix string) func(c Config) Config {
return func(c Config) Config {
linkTransformer := NewSchemaLinkTransformer(schemaPathPrefix, c.SchemasPath)
c.OpenAPI.OnAddOperation = append(c.OpenAPI.OnAddOperation, linkTransformer.OnAddOperation)
c.Transformers = append(c.Transformers, linkTransformer.Transform)
return c
}
}
12 changes: 7 additions & 5 deletions transforms.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
// as-you-type validation & completion of HTTP resources in editors like
// VSCode.
type SchemaLinkTransformer struct {
prefix string
apiPrefix string
schemasPath string
types map[any]struct {
t reflect.Type
Expand All @@ -36,9 +36,9 @@
// to understand the structure of the response and enables things like
// as-you-type validation & completion of HTTP resources in editors like
// VSCode.
func NewSchemaLinkTransformer(prefix, schemasPath string) *SchemaLinkTransformer {
func NewSchemaLinkTransformer(apiPrefix, schemasPath string) *SchemaLinkTransformer {
return &SchemaLinkTransformer{
prefix: prefix,
apiPrefix: apiPrefix,
schemasPath: schemasPath,
types: map[any]struct {
t reflect.Type
Expand Down Expand Up @@ -94,8 +94,10 @@
// Figure out if there should be a base path prefix. This might be set when
// using a sub-router / group or if the gateway consumes a part of the path.
schemasPath := t.schemasPath
if prefix := getAPIPrefix(oapi); prefix != "" {
schemasPath = path.Join(prefix, schemasPath)
if apiPrefix := getAPIPrefix(oapi); apiPrefix != "" {
schemasPath = path.Join(apiPrefix, schemasPath)

Check warning on line 98 in transforms.go

View check run for this annotation

Codecov / codecov/patch

transforms.go#L98

Added line #L98 was not covered by tests
} else if t.apiPrefix != "" {
schemasPath = path.Join(t.apiPrefix, schemasPath)

Check warning on line 100 in transforms.go

View check run for this annotation

Codecov / codecov/patch

transforms.go#L100

Added line #L100 was not covered by tests
}

registry := oapi.Components.Schemas
Expand Down
Loading