refactor(api): snapshot route metadata during spec build
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
c383d85923
commit
71c179018d
2 changed files with 103 additions and 9 deletions
20
openapi.go
20
openapi.go
|
|
@ -49,8 +49,9 @@ type SpecBuilder struct {
|
|||
}
|
||||
|
||||
type preparedRouteGroup struct {
|
||||
group RouteGroup
|
||||
descs []RouteDescription
|
||||
name string
|
||||
basePath string
|
||||
descs []RouteDescription
|
||||
}
|
||||
|
||||
const openAPIDialect = "https://spec.openapis.org/oas/3.1/dialect/base"
|
||||
|
|
@ -271,7 +272,7 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
|
|||
|
||||
for _, g := range groups {
|
||||
for _, rd := range g.descs {
|
||||
fullPath := joinOpenAPIPath(g.group.BasePath(), rd.Path)
|
||||
fullPath := joinOpenAPIPath(g.basePath, rd.Path)
|
||||
method := strings.ToLower(rd.Method)
|
||||
deprecated := rd.Deprecated || strings.TrimSpace(rd.SunsetDate) != "" || strings.TrimSpace(rd.Replacement) != ""
|
||||
deprecationHeaders := deprecationResponseHeaders(deprecated, rd.SunsetDate, rd.Replacement)
|
||||
|
|
@ -294,7 +295,7 @@ func (sb *SpecBuilder) buildPaths(groups []preparedRouteGroup) map[string]any {
|
|||
},
|
||||
}
|
||||
}
|
||||
if tags := resolvedOperationTags(g.group, rd); len(tags) > 0 {
|
||||
if tags := resolvedOperationTags(g.name, rd); len(tags) > 0 {
|
||||
operation["tags"] = tags
|
||||
}
|
||||
|
||||
|
|
@ -789,7 +790,7 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
|
|||
}
|
||||
|
||||
for _, g := range groups {
|
||||
name := strings.TrimSpace(g.group.Name())
|
||||
name := strings.TrimSpace(g.name)
|
||||
if name != "" && !seen[name] {
|
||||
tags = append(tags, map[string]any{
|
||||
"name": name,
|
||||
|
|
@ -1473,8 +1474,9 @@ func prepareRouteGroups(groups []RouteGroup) []preparedRouteGroup {
|
|||
continue
|
||||
}
|
||||
out = append(out, preparedRouteGroup{
|
||||
group: g,
|
||||
descs: collectRouteDescriptions(g),
|
||||
name: g.Name(),
|
||||
basePath: g.BasePath(),
|
||||
descs: collectRouteDescriptions(g),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1659,12 +1661,12 @@ func mergeOperationParameters(existing any, explicit []map[string]any) []map[str
|
|||
|
||||
// resolvedOperationTags returns the explicit route tags when provided, or a
|
||||
// stable fallback derived from the group's name when the route omits tags.
|
||||
func resolvedOperationTags(g RouteGroup, rd RouteDescription) []string {
|
||||
func resolvedOperationTags(groupName string, rd RouteDescription) []string {
|
||||
if tags := cleanTags(rd.Tags); len(tags) > 0 {
|
||||
return tags
|
||||
}
|
||||
|
||||
if name := strings.TrimSpace(g.Name()); name != "" {
|
||||
if name := strings.TrimSpace(groupName); name != "" {
|
||||
return []string{name}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -90,6 +90,31 @@ func (s *countingIterGroup) DescribeIter() iter.Seq[api.RouteDescription] {
|
|||
}
|
||||
}
|
||||
|
||||
type snapshottingGroup struct {
|
||||
nameCalls int
|
||||
basePathCalls int
|
||||
descs []api.RouteDescription
|
||||
}
|
||||
|
||||
func (s *snapshottingGroup) Name() string {
|
||||
s.nameCalls++
|
||||
if s.nameCalls == 1 {
|
||||
return "alpha"
|
||||
}
|
||||
return "beta"
|
||||
}
|
||||
|
||||
func (s *snapshottingGroup) BasePath() string {
|
||||
s.basePathCalls++
|
||||
if s.basePathCalls == 1 {
|
||||
return "/alpha"
|
||||
}
|
||||
return "/beta"
|
||||
}
|
||||
|
||||
func (s *snapshottingGroup) RegisterRoutes(rg *gin.RouterGroup) {}
|
||||
func (s *snapshottingGroup) Describe() []api.RouteDescription { return s.descs }
|
||||
|
||||
// ── SpecBuilder tests ─────────────────────────────────────────────────────
|
||||
|
||||
func TestSpecBuilder_Good_EmptyGroups(t *testing.T) {
|
||||
|
|
@ -1007,6 +1032,73 @@ func TestSpecBuilder_Good_DescribeIterNilFallsBackToDescribe(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_GroupMetadataIsSnapshottedOnce(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &snapshottingGroup{
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/status",
|
||||
Summary: "Snapshot status",
|
||||
Response: map[string]any{
|
||||
"type": "object",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := sb.Build([]api.RouteGroup{group})
|
||||
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["/alpha/status"]; !ok {
|
||||
t.Fatalf("expected snapshotted path /alpha/status, got %v", paths)
|
||||
}
|
||||
if _, ok := paths["/beta/status"]; ok {
|
||||
t.Fatal("did not expect mutated base path to leak into the spec")
|
||||
}
|
||||
|
||||
tags := spec["tags"].([]any)
|
||||
foundAlpha := false
|
||||
for _, tag := range tags {
|
||||
tm := tag.(map[string]any)
|
||||
if tm["name"] == "alpha" {
|
||||
foundAlpha = true
|
||||
break
|
||||
}
|
||||
if tm["name"] == "beta" {
|
||||
t.Fatal("did not expect mutated group name to leak into the spec")
|
||||
}
|
||||
}
|
||||
if !foundAlpha {
|
||||
t.Fatal("expected snapshotted group name in spec tags")
|
||||
}
|
||||
|
||||
op := paths["/alpha/status"].(map[string]any)["get"].(map[string]any)
|
||||
opTags, ok := op["tags"].([]any)
|
||||
if !ok || len(opTags) != 1 || opTags[0] != "alpha" {
|
||||
t.Fatalf("expected snapshotted operation tag alpha, got %v", op["tags"])
|
||||
}
|
||||
|
||||
if group.nameCalls != 1 {
|
||||
t.Fatalf("expected Name to be called once, got %d", group.nameCalls)
|
||||
}
|
||||
if group.basePathCalls != 1 {
|
||||
t.Fatalf("expected BasePath to be called once, got %d", group.basePathCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_SecuredResponses(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue