fix(openapi): skip blank tags in generated specs
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
3f010b855e
commit
13cc93f4f4
2 changed files with 92 additions and 5 deletions
39
openapi.go
39
openapi.go
|
|
@ -122,7 +122,6 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
operation := map[string]any{
|
||||
"summary": rd.Summary,
|
||||
"description": rd.Description,
|
||||
"tags": resolvedOperationTags(g, rd),
|
||||
"operationId": operationID(method, fullPath, operationIDs),
|
||||
"security": []any{
|
||||
map[string]any{
|
||||
|
|
@ -131,6 +130,9 @@ func (sb *SpecBuilder) buildPaths(groups []RouteGroup) map[string]any {
|
|||
},
|
||||
"responses": operationResponses(method, rd.Response),
|
||||
}
|
||||
if tags := resolvedOperationTags(g, rd); len(tags) > 0 {
|
||||
operation["tags"] = tags
|
||||
}
|
||||
|
||||
if params := pathParameters(fullPath); len(params) > 0 {
|
||||
operation["parameters"] = params
|
||||
|
|
@ -341,8 +343,8 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
seen := map[string]bool{"system": true}
|
||||
|
||||
for _, g := range groups {
|
||||
name := g.Name()
|
||||
if !seen[name] {
|
||||
name := strings.TrimSpace(g.Name())
|
||||
if name != "" && !seen[name] {
|
||||
tags = append(tags, map[string]any{
|
||||
"name": name,
|
||||
"description": name + " endpoints",
|
||||
|
|
@ -357,6 +359,7 @@ func (sb *SpecBuilder) buildTags(groups []RouteGroup) []map[string]any {
|
|||
|
||||
for _, rd := range dg.Describe() {
|
||||
for _, tag := range rd.Tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" || seen[tag] {
|
||||
continue
|
||||
}
|
||||
|
|
@ -491,16 +494,42 @@ func mergeOperationParameters(existing any, explicit []map[string]any) []map[str
|
|||
// stable fallback derived from the group's name when the route omits tags.
|
||||
func resolvedOperationTags(g RouteGroup, rd RouteDescription) []string {
|
||||
if len(rd.Tags) > 0 {
|
||||
return rd.Tags
|
||||
return cleanTags(rd.Tags)
|
||||
}
|
||||
|
||||
if name := g.Name(); name != "" {
|
||||
if name := strings.TrimSpace(g.Name()); name != "" {
|
||||
return []string{name}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanTags trims whitespace and removes empty or duplicate tags while
|
||||
// preserving the first occurrence of each name.
|
||||
func cleanTags(tags []string) []string {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cleaned := make([]string, 0, len(tags))
|
||||
seen := make(map[string]struct{}, len(tags))
|
||||
for _, tag := range tags {
|
||||
tag = strings.TrimSpace(tag)
|
||||
if tag == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[tag]; ok {
|
||||
continue
|
||||
}
|
||||
seen[tag] = struct{}{}
|
||||
cleaned = append(cleaned, tag)
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
return nil
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// envelopeSchema wraps a data schema in the standard Response[T] envelope.
|
||||
func envelopeSchema(dataSchema map[string]any) map[string]any {
|
||||
properties := map[string]any{
|
||||
|
|
|
|||
|
|
@ -833,6 +833,64 @@ func TestSpecBuilder_Good_DefaultTagsFromGroupName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_BlankTagsAreIgnored(t *testing.T) {
|
||||
sb := &api.SpecBuilder{
|
||||
Title: "Test",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
group := &specStubGroup{
|
||||
name: " ",
|
||||
basePath: "/api/blank",
|
||||
descs: []api.RouteDescription{
|
||||
{
|
||||
Method: "GET",
|
||||
Path: "/status",
|
||||
Summary: "Check status",
|
||||
Tags: []string{"", " ", "data", "data"},
|
||||
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)
|
||||
}
|
||||
|
||||
tags := spec["tags"].([]any)
|
||||
var foundData bool
|
||||
for _, raw := range tags {
|
||||
tag := raw.(map[string]any)
|
||||
name, _ := tag["name"].(string)
|
||||
if name == "" {
|
||||
t.Fatal("expected blank tag names to be ignored")
|
||||
}
|
||||
if name == "data" {
|
||||
foundData = true
|
||||
}
|
||||
}
|
||||
if !foundData {
|
||||
t.Fatal("expected data tag to be retained")
|
||||
}
|
||||
|
||||
op := spec["paths"].(map[string]any)["/api/blank/status"].(map[string]any)["get"].(map[string]any)
|
||||
opTags, ok := op["tags"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected tags array, got %T", op["tags"])
|
||||
}
|
||||
if len(opTags) != 1 || opTags[0] != "data" {
|
||||
t.Fatalf("expected operation tags to be cleaned to [data], got %v", opTags)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpecBuilder_Good_ToolBridgeIntegration(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue