Skip to content

Commit 236c7ef

Browse files
committed
feat: support docs on separate adapter
This allows exposing the spec and docs on a separate adapter, potentially under a separate path and/or middleware stacks. E.g.: ``` opts := huma.DefaultConfig("foo", "0.0.1") opts.DocsAdapter = humachi.NewAdapter(internalRouter) opts.CreateHooks = []func(huma.Config) huma.Config{ huma.DefaultSchemaLinkHook(huma.DefaultSchemaRefPrefix, internalPrefix), // assuming internalRouter above is mounted under internalPrefix } adapter := humachi.NewAdapter(mainRouter) huma.NewAPI(opts, adapter) ```
1 parent 4977a7a commit 236c7ef

File tree

2 files changed

+42
-28
lines changed

2 files changed

+42
-28
lines changed

api.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"mime/multipart"
1111
"net/http"
1212
"net/url"
13-
"path"
1413
"reflect"
1514
"regexp"
1615
"strings"
@@ -181,6 +180,15 @@ type Config struct {
181180
// blank and attach it directly to the router or adapter.
182181
DocsPath string
183182

183+
// DocsAdapter is an optional [Adapter] that will be used to serve the API
184+
// documentation. If unset, the main adapter will be used.
185+
// Setting this allows you to use a different router or middleware stack for
186+
// the documentation.
187+
// Note that if this [Adapter] has a different path than the main one, the
188+
// default CreateHook set in [DefaultConfig] needs to be modified to account
189+
// for it.
190+
DocsAdapter Adapter
191+
184192
// SchemasPath is the path to the API schemas. If set to `/schemas` it will
185193
// allow clients to get `/schemas/{schema}` to view the schema in a browser
186194
// or for use in editors like VSCode to provide autocomplete & validation.
@@ -362,7 +370,7 @@ func getAPIPrefix(oapi *OpenAPI) string {
362370
// config := huma.DefaultConfig("Example API", "1.0.0")
363371
// api := huma.NewAPI(config, adapter)
364372
func NewAPI(config Config, a Adapter) API {
365-
for i := 0; i < len(config.CreateHooks); i++ {
373+
for i := range config.CreateHooks {
366374
config = config.CreateHooks[i](config)
367375
}
368376

@@ -400,9 +408,14 @@ func NewAPI(config Config, a Adapter) API {
400408
newAPI.formatKeys = append(newAPI.formatKeys, k)
401409
}
402410

411+
docsAdapter := newAPI.adapter
412+
if config.DocsAdapter != nil {
413+
docsAdapter = config.DocsAdapter
414+
}
415+
403416
if config.OpenAPIPath != "" {
404417
var specJSON []byte
405-
a.Handle(&Operation{
418+
docsAdapter.Handle(&Operation{
406419
Method: http.MethodGet,
407420
Path: config.OpenAPIPath + ".json",
408421
}, func(ctx Context) {
@@ -413,7 +426,7 @@ func NewAPI(config Config, a Adapter) API {
413426
ctx.BodyWriter().Write(specJSON)
414427
})
415428
var specJSON30 []byte
416-
a.Handle(&Operation{
429+
docsAdapter.Handle(&Operation{
417430
Method: http.MethodGet,
418431
Path: config.OpenAPIPath + "-3.0.json",
419432
}, func(ctx Context) {
@@ -424,7 +437,7 @@ func NewAPI(config Config, a Adapter) API {
424437
ctx.BodyWriter().Write(specJSON30)
425438
})
426439
var specYAML []byte
427-
a.Handle(&Operation{
440+
docsAdapter.Handle(&Operation{
428441
Method: http.MethodGet,
429442
Path: config.OpenAPIPath + ".yaml",
430443
}, func(ctx Context) {
@@ -435,7 +448,7 @@ func NewAPI(config Config, a Adapter) API {
435448
ctx.BodyWriter().Write(specYAML)
436449
})
437450
var specYAML30 []byte
438-
a.Handle(&Operation{
451+
docsAdapter.Handle(&Operation{
439452
Method: http.MethodGet,
440453
Path: config.OpenAPIPath + "-3.0.yaml",
441454
}, func(ctx Context) {
@@ -448,14 +461,10 @@ func NewAPI(config Config, a Adapter) API {
448461
}
449462

450463
if config.DocsPath != "" {
451-
a.Handle(&Operation{
464+
docsAdapter.Handle(&Operation{
452465
Method: http.MethodGet,
453466
Path: config.DocsPath,
454467
}, func(ctx Context) {
455-
openAPIPath := config.OpenAPIPath
456-
if prefix := getAPIPrefix(newAPI.OpenAPI()); prefix != "" {
457-
openAPIPath = path.Join(prefix, openAPIPath)
458-
}
459468
ctx.SetHeader("Content-Type", "text/html")
460469
title := "Elements in HTML"
461470
if config.Info != nil && config.Info.Title != "" {
@@ -476,7 +485,7 @@ func NewAPI(config Config, a Adapter) API {
476485
<body style="height: 100vh;">
477486
478487
<elements-api
479-
apiDescriptionUrl="` + openAPIPath + `.yaml"
488+
apiDescriptionUrl="` + strings.TrimPrefix(config.OpenAPIPath, "/") + `.yaml"
480489
router="hash"
481490
layout="sidebar"
482491
tryItCredentialsPolicy="same-origin"
@@ -488,7 +497,7 @@ func NewAPI(config Config, a Adapter) API {
488497
}
489498

490499
if config.SchemasPath != "" {
491-
a.Handle(&Operation{
500+
docsAdapter.Handle(&Operation{
492501
Method: http.MethodGet,
493502
Path: config.SchemasPath + "/{schema}",
494503
}, func(ctx Context) {

defaults.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ package huma
33
import (
44
"encoding/json"
55
"io"
6+
"path"
67
)
78

9+
// DefaultSchemaNamer is the default prefix used to reference schemas in the API spec.
10+
const DefaultSchemaRefPrefix = "#/components/schemas/"
11+
812
// DefaultJSONFormat is the default JSON formatter that can be set in the API's
913
// `Config.Formats` map. This is used by the `DefaultConfig` function.
1014
//
@@ -53,10 +57,7 @@ var DefaultFormats = map[string]Format{
5357
//
5458
// import _ "github.com/danielgtaylor/huma/v2/formats/cbor"
5559
func DefaultConfig(title, version string) Config {
56-
schemaPrefix := "#/components/schemas/"
57-
schemasPath := "/schemas"
58-
59-
registry := NewMapRegistry(schemaPrefix, DefaultSchemaNamer)
60+
registry := NewMapRegistry(DefaultSchemaRefPrefix, DefaultSchemaNamer)
6061

6162
return Config{
6263
OpenAPI: &OpenAPI{
@@ -71,20 +72,24 @@ func DefaultConfig(title, version string) Config {
7172
},
7273
OpenAPIPath: "/openapi",
7374
DocsPath: "/docs",
74-
SchemasPath: schemasPath,
75+
SchemasPath: "/schemas",
7576
Formats: DefaultFormats,
7677
DefaultFormat: "application/json",
7778
CreateHooks: []func(Config) Config{
78-
func(c Config) Config {
79-
// Add a link transformer to the API. This adds `Link` headers and
80-
// puts `$schema` fields in the response body which point to the JSON
81-
// Schema that describes the response structure.
82-
// This is a create hook so we get the latest schema path setting.
83-
linkTransformer := NewSchemaLinkTransformer(schemaPrefix, c.SchemasPath)
84-
c.OpenAPI.OnAddOperation = append(c.OpenAPI.OnAddOperation, linkTransformer.OnAddOperation)
85-
c.Transformers = append(c.Transformers, linkTransformer.Transform)
86-
return c
87-
},
79+
DefaultSchemaLinkHook(DefaultSchemaRefPrefix, ""),
8880
},
8981
}
9082
}
83+
84+
// DefaultSchemaLinkHook adds a link transformer to the API.
85+
// This adds `Link` headers and puts `$schema` fields in the response body which
86+
// point to the JSON Schema that describes the response structure.
87+
// This is a create hook so we get the latest schema path setting.
88+
func DefaultSchemaLinkHook(schemaRefPrefix, schemaPathPrefix string) func(c Config) Config {
89+
return func(c Config) Config {
90+
linkTransformer := NewSchemaLinkTransformer(schemaRefPrefix, path.Join(schemaPathPrefix, c.SchemasPath))
91+
c.OpenAPI.OnAddOperation = append(c.OpenAPI.OnAddOperation, linkTransformer.OnAddOperation)
92+
c.Transformers = append(c.Transformers, linkTransformer.Transform)
93+
return c
94+
}
95+
}

0 commit comments

Comments
 (0)