From 8149b0abf226efadbb6af3eee2afc499373861c3 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 07:57:58 +0000 Subject: [PATCH] refactor(api): centralise spec group iterator Co-Authored-By: Virgil --- cmd/api/spec_groups_iter.go | 27 +-------------------------- spec_registry.go | 32 ++++++++++++++++++++++++++++++++ spec_registry_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/cmd/api/spec_groups_iter.go b/cmd/api/spec_groups_iter.go index 5c5a87a..208de61 100644 --- a/cmd/api/spec_groups_iter.go +++ b/cmd/api/spec_groups_iter.go @@ -12,30 +12,5 @@ import ( // extra group. It keeps the command paths iterator-backed while preserving the // existing ordering guarantees. func specGroupsIter(extra goapi.RouteGroup) iter.Seq[goapi.RouteGroup] { - return func(yield func(goapi.RouteGroup) bool) { - seen := map[string]struct{}{} - for group := range goapi.RegisteredSpecGroupsIter() { - key := specGroupIterKey(group) - seen[key] = struct{}{} - if !yield(group) { - return - } - } - if extra != nil { - if _, ok := seen[specGroupIterKey(extra)]; ok { - return - } - if !yield(extra) { - return - } - } - } -} - -func specGroupIterKey(group goapi.RouteGroup) string { - if group == nil { - return "" - } - - return group.Name() + "\x00" + group.BasePath() + return goapi.SpecGroupsIter(extra) } diff --git a/spec_registry.go b/spec_registry.go index d6eed0b..44902d7 100644 --- a/spec_registry.go +++ b/spec_registry.go @@ -90,6 +90,38 @@ func RegisteredSpecGroupsIter() iter.Seq[RouteGroup] { return slices.Values(groups) } +// SpecGroupsIter returns the registered spec groups plus one optional extra +// group, deduplicated by group identity. +// +// The iterator snapshots the registry before yielding so callers can range +// over it without holding the registry lock. +// +// Example: +// +// for g := range api.SpecGroupsIter(api.NewToolBridge("/tools")) { +// _ = g +// } +func SpecGroupsIter(extra RouteGroup) iter.Seq[RouteGroup] { + return func(yield func(RouteGroup) bool) { + seen := map[string]struct{}{} + for group := range RegisteredSpecGroupsIter() { + key := specGroupKey(group) + seen[key] = struct{}{} + if !yield(group) { + return + } + } + if extra != nil { + if _, ok := seen[specGroupKey(extra)]; ok { + return + } + if !yield(extra) { + return + } + } + } +} + // ResetSpecGroups clears the package-level spec registry. // It is primarily intended for tests that need to isolate global state. // diff --git a/spec_registry_test.go b/spec_registry_test.go index b3d22eb..b144653 100644 --- a/spec_registry_test.go +++ b/spec_registry_test.go @@ -110,3 +110,29 @@ func TestRegisterSpecGroupsIter_Good_DeduplicatesAndRegisters(t *testing.T) { t.Fatalf("expected second group to be gamma at /gamma, got %s at %s", registered[1].Name(), registered[1].BasePath()) } } + +func TestSpecGroupsIter_Good_DeduplicatesExtraBridge(t *testing.T) { + snapshot := api.RegisteredSpecGroups() + api.ResetSpecGroups() + t.Cleanup(func() { + api.ResetSpecGroups() + api.RegisterSpecGroups(snapshot...) + }) + + first := &specRegistryStubGroup{name: "alpha", basePath: "/alpha"} + extra := &specRegistryStubGroup{name: "alpha", basePath: "/alpha"} + + api.RegisterSpecGroups(first) + + var groups []api.RouteGroup + for group := range api.SpecGroupsIter(extra) { + groups = append(groups, group) + } + + if len(groups) != 1 { + t.Fatalf("expected deduplicated iterator to return 1 group, got %d", len(groups)) + } + if groups[0].Name() != "alpha" || groups[0].BasePath() != "/alpha" { + t.Fatalf("expected alpha at /alpha, got %s at %s", groups[0].Name(), groups[0].BasePath()) + } +}