feat(openapi): sort generated tags deterministically

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 00:47:51 +00:00
parent c21c3409d7
commit e23d8e9780
2 changed files with 80 additions and 0 deletions

View file

@ -6,6 +6,7 @@ import (
"encoding/json"
"iter"
"net/http"
"sort"
"strconv"
"strings"
"unicode"
@ -609,9 +610,33 @@ func (sb *SpecBuilder) buildTags(groups []preparedRouteGroup) []map[string]any {
}
}
sortTags(tags)
return tags
}
// sortTags keeps system first and orders the remaining tags alphabetically so
// generated specs stay stable across registration order changes.
func sortTags(tags []map[string]any) {
if len(tags) < 2 {
return
}
sort.SliceStable(tags, func(i, j int) bool {
left, _ := tags[i]["name"].(string)
right, _ := tags[j]["name"].(string)
switch {
case left == "system":
return true
case right == "system":
return false
default:
return left < right
}
})
}
func graphqlPathItem(path string, operationIDs map[string]int) map[string]any {
return map[string]any{
"post": map[string]any{

View file

@ -1554,6 +1554,61 @@ func TestSpecBuilder_Good_DefaultTagsFromGroupName(t *testing.T) {
}
}
func TestSpecBuilder_Good_TagsAreSortedDeterministically(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",
Version: "1.0.0",
}
group := &specStubGroup{
name: "gamma",
basePath: "/api/gamma",
descs: []api.RouteDescription{
{
Method: "GET",
Path: "/status",
Summary: "Check status",
Tags: []string{"zeta", "alpha", "beta"},
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, ok := spec["tags"].([]any)
if !ok {
t.Fatalf("expected tags array, got %T", spec["tags"])
}
names := make([]string, 0, len(tags))
for _, raw := range tags {
tag := raw.(map[string]any)
name, _ := tag["name"].(string)
names = append(names, name)
}
expected := []string{"system", "alpha", "beta", "gamma", "zeta"}
if len(names) != len(expected) {
t.Fatalf("expected %d tags, got %d: %v", len(expected), len(names), names)
}
for i := range expected {
if names[i] != expected[i] {
t.Fatalf("expected tag order %v, got %v", expected, names)
}
}
}
func TestSpecBuilder_Good_DeprecatedOperation(t *testing.T) {
sb := &api.SpecBuilder{
Title: "Test",