Skip to content

proposal: support docs on separate adapter - #775

Open
costela wants to merge 3 commits into
danielgtaylor:mainfrom
costela:support-docs-on-separate-adapter
Open

costela wants to merge 3 commits into
danielgtaylor:mainfrom
costela:support-docs-on-separate-adapter

Conversation

@costela

@costela costela commented Mar 21, 2025 •

Copy link
Copy Markdown
Contributor

This allows exposing the spec and docs on a separate adapter, potentially under a separate path and/or middleware stacks.

E.g.: (assuming internalRouter and mainRouter are two chi.Router)

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)

This is a bit of a WIP:

  • I'm not 100% sure the removal of getAPIPrefix call in the stoplight template is really as harmless as I think
  • the other remaining getAPIPrefix call will currently break the schema links if the API is mounted somewhere other than the root.

But regardless of the limitations above: WDYT about the general idea?

@costela
costela force-pushed the support-docs-on-separate-adapter branch from 236c7ef to f419fb7 Compare March 21, 2025 22:50
@codecov

codecov Bot commented Mar 21, 2025 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 94.71%. Comparing base (606bcc2) to head (d89c772).

Files with missing lines Patch % Lines
transforms.go 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #775      +/-   ##
==========================================
- Coverage   94.72%   94.71%   -0.02%     
==========================================
  Files          25       25              
  Lines        5193     5202       +9     
==========================================
+ Hits         4919     4927       +8     
- Misses        269      270       +1     
  Partials        5        5              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@costela

costela commented Mar 25, 2025

Copy link
Copy Markdown
Contributor Author

Actually, now that I look at the code, it looks like NewSchemaLinkTransformer's prefix parameter isn't even being used? 🙈

I've pushed a second take at this. The new one uses the first parameter to NewSchemaLinkTransformer, so it may be considered a breaking change?
Also I've tried to keep respecting the prefix detection via the OpenAPI.Servers, though I cannot find any place in the documentation where that's actually recommended? Maybe we can skip this backward compatibility and document explicitly how to mount stuff below the root path? There's a commented block in TestChiRouterPrefix that shows the two possible variants.

@robtme

robtme commented Jan 16, 2026

Copy link
Copy Markdown
Collaborator

Hey @costela! Thanks for this contribution, and sorry that it hasn't been reviewed yet! I'm still working on getting up-to-speed with some of the more complex parts of the codebase, but want to try and resolve issues and merge PRs where feasible to do so.

This PR may require @danielgtaylor's review, but in the meantime I'd appreciate it if you could resolve any merge conflicts :)

costela and others added 3 commits October 29, 2026 09:15
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)
```
Make sure that the main adapter does not serve the docs and the spec when
DocsAdapter is set. Also make sure that the docs link to the spec with a
relative URL, because the server prefix applies only to the main adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@costela
costela force-pushed the support-docs-on-separate-adapter branch from dbc2946 to d89c772 Compare October 29, 2026 08:17
Copilot AI balanced review requested due to automatic review settings October 29, 2026 08:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Documentation and schema URL handling can produce broken links or ignore explicit mount prefixes, while the hook-loop change can panic on shortened slices and skip newly appended hooks.

4 open issues
What changed in this PR

This PR adds an optional documentation adapter, allowing OpenAPI specifications, documentation, and schemas to use a separate router, mount path, or middleware stack from API endpoints.

Changes:

  • Routes documentation-related endpoints through DocsAdapter when configured.
  • Extracts a reusable schema-link hook and adds schema mount-prefix handling.
  • Adds tests for documentation and specification routing on a separate adapter.
File Description
transforms.go Adds a schema-path prefix fallback to link generation.
defaults.go Exports the schema reference prefix and extracts DefaultSchemaLinkHook.
api.go Adds documentation adapter routing and adjusts spec URLs and hook iteration.
api_test.go Tests separate-adapter routing and generated documentation links.
adapters/​humachi/​humachi_test.go Adds a commented-out schema-link hook example; no test behavior changes.

Review effort: Balanced

Comment thread api.go
// 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 {
Comment thread api.go

if config.SchemasPath != "" {
a.Handle(&Operation{
docsAdapter.Handle(&Operation{
Comment thread api.go
// The API prefix describes the main adapter, not the docs adapter, so
// link to the spec relative to the docs page instead.
docsAdapter = a.config.DocsAdapter
openAPIPath = strings.TrimPrefix(openAPIPath, "/")
Comment thread transforms.go
Comment on lines +99 to 103
if apiPrefix := getAPIPrefix(oapi); apiPrefix != "" {
schemasPath = path.Join(apiPrefix, schemasPath)
} else if t.apiPrefix != "" {
schemasPath = path.Join(t.apiPrefix, schemasPath)
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants