feat(openapi): hide undocumented routes
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
68f5abefd0
commit
0bb07f43f0
3 changed files with 147 additions and 5 deletions
2
group.go
2
group.go
|
|
@ -64,6 +64,8 @@ type RouteDescription struct {
|
|||
Summary string // Short summary
|
||||
Description string // Long description
|
||||
Tags []string // OpenAPI tags for grouping
|
||||
// Hidden omits the route from generated documentation.
|
||||
Hidden bool
|
||||
// Deprecated marks the operation as deprecated in OpenAPI.
|
||||
Deprecated bool
|
||||
// SunsetDate marks when a deprecated operation will be removed.
|
||||
|
|
|
|||
38
openapi.go
38
openapi.go
|
|
@ -36,8 +36,9 @@ type SpecBuilder struct {
|
|||
}
|
||||
|
||||
type preparedRouteGroup struct {
|
||||
group RouteGroup
|
||||
descs []RouteDescription
|
||||
group RouteGroup
|
||||
descs []RouteDescription
|
||||
describable bool
|
||||
}
|
||||
|
||||
// Build generates the complete OpenAPI 3.1 JSON spec.
|
||||
|
|
@ -585,7 +586,7 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
|
|||
|
||||
for _, g := range groups {
|
||||
name := strings.TrimSpace(g.group.Name())
|
||||
if name != "" && !seen[name] {
|
||||
if name != "" && !seen[name] && (!g.describable || len(g.descs) > 0) {
|
||||
tags = append(tags, map[string]any{
|
||||
"name": name,
|
||||
"description": name + " endpoints",
|
||||
|
|
@ -759,9 +760,14 @@ func prepareRouteGroups(groups []RouteGroup) []preparedRouteGroup {
|
|||
if g == nil {
|
||||
continue
|
||||
}
|
||||
if isHiddenRouteGroup(g) {
|
||||
continue
|
||||
}
|
||||
describable := isDescribableRouteGroup(g)
|
||||
out = append(out, preparedRouteGroup{
|
||||
group: g,
|
||||
descs: collectRouteDescriptions(g),
|
||||
group: g,
|
||||
descs: collectRouteDescriptions(g),
|
||||
describable: describable,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -789,12 +795,34 @@ func collectRouteDescriptions(g RouteGroup) []RouteDescription {
|
|||
|
||||
descs := make([]RouteDescription, 0)
|
||||
for rd := range descIter {
|
||||
if rd.Hidden {
|
||||
continue
|
||||
}
|
||||
descs = append(descs, rd)
|
||||
}
|
||||
|
||||
return descs
|
||||
}
|
||||
|
||||
func isHiddenRouteGroup(g RouteGroup) bool {
|
||||
type hiddenRouteGroup interface {
|
||||
Hidden() bool
|
||||
}
|
||||
|
||||
hg, ok := g.(hiddenRouteGroup)
|
||||
return ok && hg.Hidden()
|
||||
}
|
||||
|
||||
func isDescribableRouteGroup(g RouteGroup) bool {
|
||||
if _, ok := g.(DescribableGroupIter); ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := g.(DescribableGroup); ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// routeDescriptions returns OpenAPI route descriptions for a group.
|
||||
// Iterator-backed implementations are preferred when available so builders
|
||||
// can avoid slice allocation.
|
||||
|
|
|
|||
112
openapi_test.go
112
openapi_test.go
|
|
@ -18,6 +18,7 @@ import (
|
|||
type specStubGroup struct {
|
||||
name string
|
||||
basePath string
|
||||
hidden bool
|
||||
descs []api.RouteDescription
|
||||
}
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ func (s *specStubGroup) Name() string { return s.name }
|
|||
func (s *specStubGroup) BasePath() string { return s.basePath }
|
||||
func (s *specStubGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (s *specStubGroup) Describe() []api.RouteDescription { return s.descs }
|
||||
func (s *specStubGroup) Hidden() bool { return s.hidden }
|
||||
|
||||
type plainStubGroup struct{}
|
||||
|
||||
|
|
@ -1674,6 +1676,116 @@ func TestSpecBuilder_Good_BlankRouteTagsFallBackToGroupName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_HiddenRoutesAreOmitted(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
visible := &specStubGroup{
|
||||
name: "visible",
|
||||
basePath: "/api",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/public",
|
||||
Summary: "Public endpoint",
|
||||
Tags: []string{"public"},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/internal",
|
||||
Summary: "Internal endpoint",
|
||||
Tags: []string{"internal"},
|
||||
Hidden: true,
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
hidden := &specStubGroup{
|
||||
name: "hidden-group",
|
||||
basePath: "/api/internal",
|
||||
hidden: true,
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/status",
|
||||
Summary: "Hidden group endpoint",
|
||||
Tags: []string{"hidden"},
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{visible, hidden})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var spec map[string]any
|
||||
if err := json.Unmarshal(data, &spec); err != nil {
|
||||
t.Fatalf("invalid JSON: %v", err)
|
||||
}
|
||||
|
||||
paths := spec["paths"].(map[string]any)
|
||||
if _, ok := paths["/api/public"]; !ok {
|
||||
t.Fatal("expected visible route to remain in the spec")
|
||||
}
|
||||
if _, ok := paths["/api/internal"]; ok {
|
||||
t.Fatal("did not expect hidden route to appear in the spec")
|
||||
}
|
||||
if _, ok := paths["/api/internal/status"]; ok {
|
||||
t.Fatal("did not expect hidden group routes to appear in the spec")
|
||||
}
|
||||
|
||||
tags := spec["tags"].([]any)
|
||||
foundPublic := false
|
||||
foundInternal := false
|
||||
foundHidden := false
|
||||
foundVisibleGroup := false
|
||||
foundHiddenGroup := false
|
||||
for _, raw := range tags {
|
||||
tag := raw.(map[string]any)
|
||||
name, _ := tag["name"].(string)
|
||||
switch name {
|
||||
case "public":
|
||||
foundPublic = true
|
||||
case "internal":
|
||||
foundInternal = true
|
||||
case "hidden":
|
||||
foundHidden = true
|
||||
case "visible":
|
||||
foundVisibleGroup = true
|
||||
case "hidden-group":
|
||||
foundHiddenGroup = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPublic {
|
||||
t.Fatal("expected public tag to remain in the spec")
|
||||
}
|
||||
if !foundVisibleGroup {
|
||||
t.Fatal("expected visible group tag to remain in the spec")
|
||||
}
|
||||
if foundInternal {
|
||||
t.Fatal("did not expect hidden route tag to appear in the spec")
|
||||
}
|
||||
if foundHidden {
|
||||
t.Fatal("did not expect hidden group route tag to appear in the spec")
|
||||
}
|
||||
if foundHiddenGroup {
|
||||
t.Fatal("did not expect hidden group tag to appear in the spec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ToolBridgeIntegration(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue