api/spec_registry.go
Virgil 8149b0abf2 refactor(api): centralise spec group iterator
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-02 07:57:58 +00:00

154 lines
3.5 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package api
import (
"iter"
"sync"
"slices"
)
// specRegistry stores RouteGroups that should be included in CLI-generated
// OpenAPI documents. Packages can register their groups during init and the
// API CLI will pick them up when building specs or SDKs.
var specRegistry struct {
mu sync.RWMutex
groups []RouteGroup
}
// RegisterSpecGroups adds route groups to the package-level spec registry.
// Nil groups are ignored. Registered groups are returned by RegisteredSpecGroups
// in the order they were added.
//
// Example:
//
// api.RegisterSpecGroups(api.NewToolBridge("/mcp"))
func RegisterSpecGroups(groups ...RouteGroup) {
RegisterSpecGroupsIter(slices.Values(groups))
}
// RegisterSpecGroupsIter adds route groups from an iterator to the package-level
// spec registry.
//
// Nil groups are ignored. Registered groups are returned by RegisteredSpecGroups
// in the order they were added.
//
// Example:
//
// api.RegisterSpecGroupsIter(api.RegisteredSpecGroupsIter())
func RegisterSpecGroupsIter(groups iter.Seq[RouteGroup]) {
if groups == nil {
return
}
specRegistry.mu.Lock()
defer specRegistry.mu.Unlock()
for group := range groups {
if group == nil {
continue
}
if specRegistryContains(group) {
continue
}
specRegistry.groups = append(specRegistry.groups, group)
}
}
// RegisteredSpecGroups returns a copy of the route groups registered for
// CLI-generated OpenAPI documents.
//
// Example:
//
// groups := api.RegisteredSpecGroups()
func RegisteredSpecGroups() []RouteGroup {
specRegistry.mu.RLock()
defer specRegistry.mu.RUnlock()
out := make([]RouteGroup, len(specRegistry.groups))
copy(out, specRegistry.groups)
return out
}
// RegisteredSpecGroupsIter returns an iterator over the route groups registered
// for CLI-generated OpenAPI documents.
//
// The iterator snapshots the current registry contents so callers can range
// over it without holding the registry lock.
//
// Example:
//
// for g := range api.RegisteredSpecGroupsIter() {
// _ = g
// }
func RegisteredSpecGroupsIter() iter.Seq[RouteGroup] {
specRegistry.mu.RLock()
groups := slices.Clone(specRegistry.groups)
specRegistry.mu.RUnlock()
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.
//
// Example:
//
// api.ResetSpecGroups()
func ResetSpecGroups() {
specRegistry.mu.Lock()
defer specRegistry.mu.Unlock()
specRegistry.groups = nil
}
func specRegistryContains(group RouteGroup) bool {
key := specGroupKey(group)
for _, existing := range specRegistry.groups {
if specGroupKey(existing) == key {
return true
}
}
return false
}
func specGroupKey(group RouteGroup) string {
if group == nil {
return ""
}
return group.Name() + "\x00" + group.BasePath()
}