From acce86c0e485f2cb0031553240e7916fabb66de5 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Mon, 25 May 2026 20:23:06 +0800 Subject: [PATCH] fix(registry): log Unregister Close errors, add doc comments and 59 unit tests Log close errors in Unregister instead of silently discarding them. Add doc comments to all exported symbols across 20 registry files. Add comprehensive tests covering base registry, all wrapper types, hot-reload delegation, nil-fallback semantics, and global accessors. --- registry/admission.go | 5 + registry/auther.go | 4 + registry/bypass.go | 4 + registry/chain.go | 4 + registry/connector.go | 6 + registry/dialer.go | 6 + registry/handler.go | 6 + registry/hop.go | 4 + registry/hosts.go | 4 + registry/ingress.go | 4 + registry/limiter.go | 12 + registry/listener.go | 6 + registry/logger.go | 4 + registry/observer.go | 4 + registry/recorder.go | 4 + registry/registry.go | 38 +- registry/registry_test.go | 1021 +++++++++++++++++++++++++++++++++++++ registry/resolver.go | 4 + registry/router.go | 4 + registry/sd.go | 4 + registry/service.go | 3 + 21 files changed, 1150 insertions(+), 1 deletion(-) create mode 100644 registry/registry_test.go diff --git a/registry/admission.go b/registry/admission.go index 54981d11..d6ceec01 100644 --- a/registry/admission.go +++ b/registry/admission.go @@ -6,14 +6,19 @@ import ( "github.com/go-gost/core/admission" ) +// admissionRegistry implements a hot-reload-safe registry for admission.Admission. +// The Get method returns a wrapper that looks up the current value on every call. type admissionRegistry struct { registry[admission.Admission] } +// Register stores an Admission under the given name. func (r *admissionRegistry) Register(name string, v admission.Admission) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Admission. +// Returns nil if name is empty. func (r *admissionRegistry) Get(name string) admission.Admission { if name != "" { return &admissionWrapper{name: name, r: r} diff --git a/registry/auther.go b/registry/auther.go index dd39f162..7f7138e3 100644 --- a/registry/auther.go +++ b/registry/auther.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/auth" ) +// autherRegistry implements a hot-reload-safe registry for auth.Authenticator. type autherRegistry struct { registry[auth.Authenticator] } +// Register stores an Authenticator under the given name. func (r *autherRegistry) Register(name string, v auth.Authenticator) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Authenticator. +// Returns nil if name is empty. func (r *autherRegistry) Get(name string) auth.Authenticator { if name != "" { return &autherWrapper{name: name, r: r} diff --git a/registry/bypass.go b/registry/bypass.go index 59b167e3..b674c049 100644 --- a/registry/bypass.go +++ b/registry/bypass.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/bypass" ) +// bypassRegistry implements a hot-reload-safe registry for bypass.Bypass. type bypassRegistry struct { registry[bypass.Bypass] } +// Register stores a Bypass under the given name. func (r *bypassRegistry) Register(name string, v bypass.Bypass) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Bypass. +// Returns nil if name is empty. func (r *bypassRegistry) Get(name string) bypass.Bypass { if name != "" { return &bypassWrapper{name: name, r: r} diff --git a/registry/chain.go b/registry/chain.go index 603d5b59..dbc4bc3c 100644 --- a/registry/chain.go +++ b/registry/chain.go @@ -8,14 +8,18 @@ import ( "github.com/go-gost/core/selector" ) +// chainRegistry implements a hot-reload-safe registry for chain.Chainer. type chainRegistry struct { registry[chain.Chainer] } +// Register stores a Chainer under the given name. func (r *chainRegistry) Register(name string, v chain.Chainer) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Chainer. +// Returns nil if name is empty. func (r *chainRegistry) Get(name string) chain.Chainer { if name != "" { return &chainWrapper{name: name, r: r} diff --git a/registry/connector.go b/registry/connector.go index 4d0555f9..1dcda125 100644 --- a/registry/connector.go +++ b/registry/connector.go @@ -5,12 +5,18 @@ import ( "github.com/go-gost/core/logger" ) +// NewConnector is a factory function that creates a new connector.Connector +// from the given options. type NewConnector func(opts ...connector.Option) connector.Connector +// connectorRegistry stores connector factory functions. Duplicate registrations +// are treated as fatal errors (logger.Default().Fatal). type connectorRegistry struct { registry[NewConnector] } +// Register stores a connector factory under the given name. Calls Fatal on +// duplicate registration since connector names must be unique at init time. func (r *connectorRegistry) Register(name string, v NewConnector) error { if err := r.registry.Register(name, v); err != nil { logger.Default().Fatal(err) diff --git a/registry/dialer.go b/registry/dialer.go index 8d5b6f0c..41b0d4da 100644 --- a/registry/dialer.go +++ b/registry/dialer.go @@ -5,12 +5,18 @@ import ( "github.com/go-gost/core/logger" ) +// NewDialer is a factory function that creates a new dialer.Dialer +// from the given options. type NewDialer func(opts ...dialer.Option) dialer.Dialer +// dialerRegistry stores dialer factory functions. Duplicate registrations +// are treated as fatal errors (logger.Default().Fatal). type dialerRegistry struct { registry[NewDialer] } +// Register stores a dialer factory under the given name. Calls Fatal on +// duplicate registration since dialer names must be unique at init time. func (r *dialerRegistry) Register(name string, v NewDialer) error { if err := r.registry.Register(name, v); err != nil { logger.Default().Fatal(err) diff --git a/registry/handler.go b/registry/handler.go index 23ddd38c..e7b6b520 100644 --- a/registry/handler.go +++ b/registry/handler.go @@ -5,12 +5,18 @@ import ( "github.com/go-gost/core/logger" ) +// NewHandler is a factory function that creates a new handler.Handler +// from the given options. type NewHandler func(opts ...handler.Option) handler.Handler +// handlerRegistry stores handler factory functions. Duplicate registrations +// are treated as fatal errors (logger.Default().Fatal). type handlerRegistry struct { registry[NewHandler] } +// Register stores a handler factory under the given name. Calls Fatal on +// duplicate registration since handler names must be unique at init time. func (r *handlerRegistry) Register(name string, v NewHandler) error { if err := r.registry.Register(name, v); err != nil { logger.Default().Fatal(err) diff --git a/registry/hop.go b/registry/hop.go index 2ae54159..c3612fd1 100644 --- a/registry/hop.go +++ b/registry/hop.go @@ -7,14 +7,18 @@ import ( "github.com/go-gost/core/hop" ) +// hopRegistry implements a hot-reload-safe registry for hop.Hop. type hopRegistry struct { registry[hop.Hop] } +// Register stores a Hop under the given name. func (r *hopRegistry) Register(name string, v hop.Hop) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Hop. +// Returns nil if name is empty. func (r *hopRegistry) Get(name string) hop.Hop { if name != "" { return &hopWrapper{name: name, r: r} diff --git a/registry/hosts.go b/registry/hosts.go index 90796468..cf2fc3c0 100644 --- a/registry/hosts.go +++ b/registry/hosts.go @@ -7,14 +7,18 @@ import ( "github.com/go-gost/core/hosts" ) +// hostsRegistry implements a hot-reload-safe registry for hosts.HostMapper. type hostsRegistry struct { registry[hosts.HostMapper] } +// Register stores a HostMapper under the given name. func (r *hostsRegistry) Register(name string, v hosts.HostMapper) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered HostMapper. +// Returns nil if name is empty. func (r *hostsRegistry) Get(name string) hosts.HostMapper { if name != "" { return &hostsWrapper{name: name, r: r} diff --git a/registry/ingress.go b/registry/ingress.go index 837773b6..f87a114a 100644 --- a/registry/ingress.go +++ b/registry/ingress.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/ingress" ) +// ingressRegistry implements a hot-reload-safe registry for ingress.Ingress. type ingressRegistry struct { registry[ingress.Ingress] } +// Register stores an Ingress under the given name. func (r *ingressRegistry) Register(name string, v ingress.Ingress) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Ingress. +// Returns nil if name is empty. func (r *ingressRegistry) Get(name string) ingress.Ingress { if name != "" { return &ingressWrapper{name: name, r: r} diff --git a/registry/limiter.go b/registry/limiter.go index 00fedc07..7a23c0f5 100644 --- a/registry/limiter.go +++ b/registry/limiter.go @@ -9,14 +9,18 @@ import ( "github.com/go-gost/core/limiter/traffic" ) +// trafficLimiterRegistry implements a hot-reload-safe registry for traffic.TrafficLimiter. type trafficLimiterRegistry struct { registry[traffic.TrafficLimiter] } +// Register stores a TrafficLimiter under the given name. func (r *trafficLimiterRegistry) Register(name string, v traffic.TrafficLimiter) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered TrafficLimiter. +// Returns nil if name is empty. func (r *trafficLimiterRegistry) Get(name string) traffic.TrafficLimiter { if name != "" { return &trafficLimiterWrapper{name: name, r: r} @@ -49,14 +53,18 @@ func (w *trafficLimiterWrapper) Out(ctx context.Context, key string, opts ...lim return v.Out(ctx, key, opts...) } +// connLimiterRegistry implements a hot-reload-safe registry for conn.ConnLimiter. type connLimiterRegistry struct { registry[conn.ConnLimiter] } +// Register stores a ConnLimiter under the given name. func (r *connLimiterRegistry) Register(name string, v conn.ConnLimiter) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered ConnLimiter. +// Returns nil if name is empty. func (r *connLimiterRegistry) Get(name string) conn.ConnLimiter { if name != "" { return &connLimiterWrapper{name: name, r: r} @@ -81,14 +89,18 @@ func (w *connLimiterWrapper) Limiter(key string) conn.Limiter { return v.Limiter(key) } +// rateLimiterRegistry implements a hot-reload-safe registry for rate.RateLimiter. type rateLimiterRegistry struct { registry[rate.RateLimiter] } +// Register stores a RateLimiter under the given name. func (r *rateLimiterRegistry) Register(name string, v rate.RateLimiter) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered RateLimiter. +// Returns nil if name is empty. func (r *rateLimiterRegistry) Get(name string) rate.RateLimiter { if name != "" { return &rateLimiterWrapper{name: name, r: r} diff --git a/registry/listener.go b/registry/listener.go index 4867c463..86e1bcf8 100644 --- a/registry/listener.go +++ b/registry/listener.go @@ -5,12 +5,18 @@ import ( "github.com/go-gost/core/logger" ) +// NewListener is a factory function that creates a new listener.Listener +// from the given options. type NewListener func(opts ...listener.Option) listener.Listener +// listenerRegistry stores listener factory functions. Duplicate registrations +// are treated as fatal errors (logger.Default().Fatal). type listenerRegistry struct { registry[NewListener] } +// Register stores a listener factory under the given name. Calls Fatal on +// duplicate registration since listener names must be unique at init time. func (r *listenerRegistry) Register(name string, v NewListener) error { if err := r.registry.Register(name, v); err != nil { logger.Default().Fatal(err) diff --git a/registry/logger.go b/registry/logger.go index 8bf17fa2..5cc4575c 100644 --- a/registry/logger.go +++ b/registry/logger.go @@ -4,10 +4,14 @@ import ( "github.com/go-gost/core/logger" ) +// loggerRegistry stores logger.Logger instances. +// It uses the base registry methods without a wrapper since loggers +// are looked up directly and do not need hot-reload proxying. type loggerRegistry struct { registry[logger.Logger] } +// Register stores a Logger under the given name. func (r *loggerRegistry) Register(name string, v logger.Logger) error { return r.registry.Register(name, v) } diff --git a/registry/observer.go b/registry/observer.go index e3f90ff0..0b4f11c1 100644 --- a/registry/observer.go +++ b/registry/observer.go @@ -7,14 +7,18 @@ import ( "github.com/go-gost/core/observer" ) +// observerRegistry implements a hot-reload-safe registry for observer.Observer. type observerRegistry struct { registry[observer.Observer] } +// Register stores an Observer under the given name. func (r *observerRegistry) Register(name string, v observer.Observer) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Observer. +// Returns nil if name is empty. func (r *observerRegistry) Get(name string) observer.Observer { if name != "" { return &observerWrapper{name: name, r: r} diff --git a/registry/recorder.go b/registry/recorder.go index 98127096..8db352f4 100644 --- a/registry/recorder.go +++ b/registry/recorder.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/recorder" ) +// recorderRegistry implements a hot-reload-safe registry for recorder.Recorder. type recorderRegistry struct { registry[recorder.Recorder] } +// Register stores a Recorder under the given name. func (r *recorderRegistry) Register(name string, v recorder.Recorder) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Recorder. +// Returns nil if name is empty. func (r *recorderRegistry) Get(name string) recorder.Recorder { if name != "" { return &recorderWrapper{name: name, r: r} diff --git a/registry/registry.go b/registry/registry.go index 489f65d2..4ffd15e2 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -26,6 +26,8 @@ import ( ) var ( + // ErrDup is returned by Register when a value is already registered + // under the given name. ErrDup = errors.New("registry: duplicate object") ) @@ -56,10 +58,14 @@ var ( loggerReg reg.Registry[logger.Logger] = new(loggerRegistry) ) +// registry is a sync.Map-backed generic named registry. It implements the +// core Registry[T] interface and is embedded by every typed registry struct. type registry[T any] struct { m sync.Map } +// Register stores v under name. It returns ErrDup if name is already +// registered. Empty names are silently ignored. func (r *registry[T]) Register(name string, v T) error { if name == "" { return nil @@ -71,20 +77,28 @@ func (r *registry[T]) Register(name string, v T) error { return nil } +// Unregister removes the value registered under name. If the value +// implements io.Closer, Close is called before removal; close errors +// are logged but do not prevent deletion. func (r *registry[T]) Unregister(name string) { if v, ok := r.m.Load(name); ok { if closer, ok := v.(io.Closer); ok { - closer.Close() + if err := closer.Close(); err != nil { + logger.Default().Errorf("registry: close %s: %v", name, err) + } } r.m.Delete(name) } } +// IsRegistered reports whether a value is registered under name. func (r *registry[T]) IsRegistered(name string) bool { _, ok := r.m.Load(name) return ok } +// Get returns the value registered under name, or the zero value of T +// if name is empty or not registered. func (r *registry[T]) Get(name string) (t T) { if name == "" { return @@ -94,6 +108,7 @@ func (r *registry[T]) Get(name string) (t T) { return } +// GetAll returns a snapshot of all registered name-to-value mappings. func (r *registry[T]) GetAll() (m map[string]T) { m = make(map[string]T) r.m.Range(func(key, value any) bool { @@ -105,86 +120,107 @@ func (r *registry[T]) GetAll() (m map[string]T) { return } +// ListenerRegistry returns the global registry of listener factory functions. func ListenerRegistry() reg.Registry[NewListener] { return listenerReg } +// HandlerRegistry returns the global registry of handler factory functions. func HandlerRegistry() reg.Registry[NewHandler] { return handlerReg } +// DialerRegistry returns the global registry of dialer factory functions. func DialerRegistry() reg.Registry[NewDialer] { return dialerReg } +// ConnectorRegistry returns the global registry of connector factory functions. func ConnectorRegistry() reg.Registry[NewConnector] { return connectorReg } +// ServiceRegistry returns the global registry of service instances. func ServiceRegistry() reg.Registry[service.Service] { return serviceReg } +// ChainRegistry returns the global registry of chain instances. func ChainRegistry() reg.Registry[chain.Chainer] { return chainReg } +// HopRegistry returns the global registry of hop instances. func HopRegistry() reg.Registry[hop.Hop] { return hopReg } +// AutherRegistry returns the global registry of authenticator instances. func AutherRegistry() reg.Registry[auth.Authenticator] { return autherReg } +// AdmissionRegistry returns the global registry of admission instances. func AdmissionRegistry() reg.Registry[admission.Admission] { return admissionReg } +// BypassRegistry returns the global registry of bypass instances. func BypassRegistry() reg.Registry[bypass.Bypass] { return bypassReg } +// ResolverRegistry returns the global registry of resolver instances. func ResolverRegistry() reg.Registry[resolver.Resolver] { return resolverReg } +// HostsRegistry returns the global registry of host mapper instances. func HostsRegistry() reg.Registry[hosts.HostMapper] { return hostsReg } +// RecorderRegistry returns the global registry of recorder instances. func RecorderRegistry() reg.Registry[recorder.Recorder] { return recorderReg } +// TrafficLimiterRegistry returns the global registry of traffic limiter instances. func TrafficLimiterRegistry() reg.Registry[traffic.TrafficLimiter] { return trafficLimiterReg } +// ConnLimiterRegistry returns the global registry of connection limiter instances. func ConnLimiterRegistry() reg.Registry[conn.ConnLimiter] { return connLimiterReg } +// RateLimiterRegistry returns the global registry of rate limiter instances. func RateLimiterRegistry() reg.Registry[rate.RateLimiter] { return rateLimiterReg } +// IngressRegistry returns the global registry of ingress instances. func IngressRegistry() reg.Registry[ingress.Ingress] { return ingressReg } +// RouterRegistry returns the global registry of router instances. func RouterRegistry() reg.Registry[router.Router] { return routerReg } +// SDRegistry returns the global registry of service discovery instances. func SDRegistry() reg.Registry[sd.SD] { return sdReg } +// ObserverRegistry returns the global registry of observer instances. func ObserverRegistry() reg.Registry[observer.Observer] { return observerReg } +// LoggerRegistry returns the global registry of logger instances. func LoggerRegistry() reg.Registry[logger.Logger] { return loggerReg } diff --git a/registry/registry_test.go b/registry/registry_test.go new file mode 100644 index 00000000..7425c5dd --- /dev/null +++ b/registry/registry_test.go @@ -0,0 +1,1021 @@ +package registry + +import ( + "context" + "errors" + "net" + "sync" + "testing" + + "github.com/go-gost/core/admission" + "github.com/go-gost/core/auth" + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/chain" + "github.com/go-gost/core/connector" + + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/handler" + "github.com/go-gost/core/hop" + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/ingress" + "github.com/go-gost/core/limiter" + "github.com/go-gost/core/limiter/conn" + "github.com/go-gost/core/limiter/rate" + "github.com/go-gost/core/limiter/traffic" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/metadata" + "github.com/go-gost/core/observer" + "github.com/go-gost/core/recorder" + reg "github.com/go-gost/core/registry" + "github.com/go-gost/core/resolver" + "github.com/go-gost/core/router" + "github.com/go-gost/core/sd" + "github.com/go-gost/core/selector" + "github.com/go-gost/core/service" +) + +// --- Base registry[T] tests --- + +func TestRegistry_RegisterAndGet(t *testing.T) { + r := new(registry[string]) + if err := r.Register("a", "alpha"); err != nil { + t.Fatalf("Register: %v", err) + } + if v := r.Get("a"); v != "alpha" { + t.Fatalf("Get(%q) = %q, want %q", "a", v, "alpha") + } +} + +func TestRegistry_RegisterEmptyName(t *testing.T) { + r := new(registry[string]) + if err := r.Register("", "value"); err != nil { + t.Fatalf("Register empty name: %v", err) + } + if r.IsRegistered("") { + t.Fatal("empty name should not be registered") + } +} + +func TestRegistry_RegisterDuplicate(t *testing.T) { + r := new(registry[int]) + if err := r.Register("x", 1); err != nil { + t.Fatal(err) + } + if err := r.Register("x", 2); !errors.Is(err, ErrDup) { + t.Fatalf("duplicate Register = %v, want ErrDup", err) + } + if v := r.Get("x"); v != 1 { + t.Fatalf("Get after dup attempt = %d, want 1", v) + } +} + +func TestRegistry_GetUnknown(t *testing.T) { + r := new(registry[string]) + if v := r.Get("missing"); v != "" { + t.Fatalf("Get(unknown) = %q, want zero value", v) + } +} + +func TestRegistry_GetEmptyName(t *testing.T) { + r := new(registry[int]) + r.Register("x", 42) + if v := r.Get(""); v != 0 { + t.Fatalf("Get(empty) = %d, want 0", v) + } +} + +func TestRegistry_GetAll(t *testing.T) { + r := new(registry[string]) + r.Register("a", "alpha") + r.Register("b", "beta") + m := r.GetAll() + if len(m) != 2 { + t.Fatalf("GetAll len = %d, want 2", len(m)) + } + if m["a"] != "alpha" || m["b"] != "beta" { + t.Fatalf("GetAll = %v", m) + } +} + +func TestRegistry_GetAllEmpty(t *testing.T) { + r := new(registry[int]) + m := r.GetAll() + if m == nil || len(m) != 0 { + t.Fatalf("GetAll on empty = %v, want empty non-nil map", m) + } +} + +func TestRegistry_IsRegistered(t *testing.T) { + r := new(registry[string]) + r.Register("k", "v") + if !r.IsRegistered("k") { + t.Fatal("IsRegistered(k) = false, want true") + } + if r.IsRegistered("nope") { + t.Fatal("IsRegistered(nope) = true, want false") + } +} + +func TestRegistry_Unregister(t *testing.T) { + r := new(registry[string]) + r.Register("x", "val") + r.Unregister("x") + if r.IsRegistered("x") { + t.Fatal("still registered after Unregister") + } + if v := r.Get("x"); v != "" { + t.Fatalf("Get after unregister = %q, want zero", v) + } +} + +func TestRegistry_UnregisterUnknown(t *testing.T) { + r := new(registry[string]) + r.Unregister("nope") // should not panic +} + +type closeableService struct { + closed bool +} + +func (s *closeableService) Serve() error { return nil } +func (s *closeableService) Addr() net.Addr { return nil } +func (s *closeableService) Close() error { + s.closed = true + return nil +} + +func TestRegistry_UnregisterCloses(t *testing.T) { + r := new(registry[service.Service]) + svc := &closeableService{} + r.Register("svc", svc) + r.Unregister("svc") + if !svc.closed { + t.Fatal("Unregister did not call Close on io.Closer") + } +} + +type closeErrService struct{} + +func (s *closeErrService) Serve() error { return nil } +func (s *closeErrService) Addr() net.Addr { return nil } +func (s *closeErrService) Close() error { return errors.New("close err") } + +func TestRegistry_UnregisterCloseError(t *testing.T) { + logger.SetDefault(&mockLogger{}) + r := new(registry[service.Service]) + svc := &closeErrService{} + r.Register("svc", svc) + r.Unregister("svc") // logs error but should not panic + if r.IsRegistered("svc") { + t.Fatal("should be unregistered even if Close errors") + } +} + +func TestRegistry_ConcurrentAccess(t *testing.T) { + r := new(registry[int]) + var wg sync.WaitGroup + for i := range 100 { + wg.Add(1) + go func(i int) { + defer wg.Done() + name := string(rune('a' + i%26)) + r.Register(name, i) + r.Get(name) + r.IsRegistered(name) + r.GetAll() + }(i) + } + wg.Wait() +} + +// --- Global accessor tests --- + +func TestGlobalRegistryAccessors(t *testing.T) { + if ListenerRegistry() == nil { + t.Error("ListenerRegistry() returned nil") + } + if HandlerRegistry() == nil { + t.Error("HandlerRegistry() returned nil") + } + if DialerRegistry() == nil { + t.Error("DialerRegistry() returned nil") + } + if ConnectorRegistry() == nil { + t.Error("ConnectorRegistry() returned nil") + } + if ServiceRegistry() == nil { + t.Error("ServiceRegistry() returned nil") + } + if ChainRegistry() == nil { + t.Error("ChainRegistry() returned nil") + } + if HopRegistry() == nil { + t.Error("HopRegistry() returned nil") + } + if AutherRegistry() == nil { + t.Error("AutherRegistry() returned nil") + } + if AdmissionRegistry() == nil { + t.Error("AdmissionRegistry() returned nil") + } + if BypassRegistry() == nil { + t.Error("BypassRegistry() returned nil") + } + if ResolverRegistry() == nil { + t.Error("ResolverRegistry() returned nil") + } + if HostsRegistry() == nil { + t.Error("HostsRegistry() returned nil") + } + if RecorderRegistry() == nil { + t.Error("RecorderRegistry() returned nil") + } + if TrafficLimiterRegistry() == nil { + t.Error("TrafficLimiterRegistry() returned nil") + } + if ConnLimiterRegistry() == nil { + t.Error("ConnLimiterRegistry() returned nil") + } + if RateLimiterRegistry() == nil { + t.Error("RateLimiterRegistry() returned nil") + } + if IngressRegistry() == nil { + t.Error("IngressRegistry() returned nil") + } + if RouterRegistry() == nil { + t.Error("RouterRegistry() returned nil") + } + if SDRegistry() == nil { + t.Error("SDRegistry() returned nil") + } + if ObserverRegistry() == nil { + t.Error("ObserverRegistry() returned nil") + } + if LoggerRegistry() == nil { + t.Error("LoggerRegistry() returned nil") + } +} + +// Verify each accessor returns the same instance on repeated calls. +func TestGlobalRegistrySingleton(t *testing.T) { + if ListenerRegistry() != ListenerRegistry() { + t.Error("ListenerRegistry not singleton") + } + if HandlerRegistry() != HandlerRegistry() { + t.Error("HandlerRegistry not singleton") + } + if DialerRegistry() != DialerRegistry() { + t.Error("DialerRegistry not singleton") + } + if ConnectorRegistry() != ConnectorRegistry() { + t.Error("ConnectorRegistry not singleton") + } + if ServiceRegistry() != ServiceRegistry() { + t.Error("ServiceRegistry not singleton") + } + if ChainRegistry() != ChainRegistry() { + t.Error("ChainRegistry not singleton") + } + if HopRegistry() != HopRegistry() { + t.Error("HopRegistry not singleton") + } + if AutherRegistry() != AutherRegistry() { + t.Error("AutherRegistry not singleton") + } + if AdmissionRegistry() != AdmissionRegistry() { + t.Error("AdmissionRegistry not singleton") + } + if BypassRegistry() != BypassRegistry() { + t.Error("BypassRegistry not singleton") + } + if ResolverRegistry() != ResolverRegistry() { + t.Error("ResolverRegistry not singleton") + } + if HostsRegistry() != HostsRegistry() { + t.Error("HostsRegistry not singleton") + } + if RecorderRegistry() != RecorderRegistry() { + t.Error("RecorderRegistry not singleton") + } + if TrafficLimiterRegistry() != TrafficLimiterRegistry() { + t.Error("TrafficLimiterRegistry not singleton") + } + if ConnLimiterRegistry() != ConnLimiterRegistry() { + t.Error("ConnLimiterRegistry not singleton") + } + if RateLimiterRegistry() != RateLimiterRegistry() { + t.Error("RateLimiterRegistry not singleton") + } + if IngressRegistry() != IngressRegistry() { + t.Error("IngressRegistry not singleton") + } + if RouterRegistry() != RouterRegistry() { + t.Error("RouterRegistry not singleton") + } + if SDRegistry() != SDRegistry() { + t.Error("SDRegistry not singleton") + } + if ObserverRegistry() != ObserverRegistry() { + t.Error("ObserverRegistry not singleton") + } + if LoggerRegistry() != LoggerRegistry() { + t.Error("LoggerRegistry not singleton") + } +} + +// --- Admission wrapper tests --- + +type mockAdmission struct { + admit bool +} + +func (m *mockAdmission) Admit(_ context.Context, _, _ string, _ ...admission.Option) bool { + return m.admit +} + +func TestAdmissionRegistry_GetEmpty(t *testing.T) { + r := new(admissionRegistry) + if v := r.Get(""); v != nil { + t.Fatal("Get(empty) should return nil") + } +} + +func TestAdmissionRegistry_Delegate(t *testing.T) { + r := new(admissionRegistry) + mock := &mockAdmission{admit: true} + r.Register("test", mock) + + w := r.Get("test") + if w == nil { + t.Fatal("Get returned nil") + } + if !w.Admit(context.Background(), "tcp", "1.2.3.4") { + t.Fatal("Admit should delegate to mock") + } +} + +func TestAdmissionRegistry_HotReload(t *testing.T) { + r := new(admissionRegistry) + r.Register("h", &mockAdmission{admit: false}) + + w := r.Get("h") + if w.Admit(context.Background(), "tcp", "x") { + t.Fatal("expected false from first mock") + } + + // Hot-reload: replace the underlying value. + r.Register("h", &mockAdmission{admit: true}) // will fail with ErrDup + r.Unregister("h") + r.Register("h", &mockAdmission{admit: true}) + + // Same wrapper should now see the new value. + if !w.Admit(context.Background(), "tcp", "x") { + t.Fatal("wrapper did not pick up re-registered value") + } +} + +func TestAdmissionRegistry_NilUnderlying(t *testing.T) { + r := new(admissionRegistry) + r.Register("missing", &mockAdmission{}) + r.Unregister("missing") + + w := r.Get("missing") + if w == nil { + t.Fatal("Get should return wrapper even if underlying is nil") + } + // Should not panic, should return false. + if w.Admit(context.Background(), "tcp", "x") { + t.Fatal("nil underlying should return false") + } +} + +// --- Auther wrapper tests --- + +type mockAuther struct { + id string + valid bool +} + +func (m *mockAuther) Authenticate(_ context.Context, _, _ string, _ ...auth.Option) (string, bool) { + return m.id, m.valid +} + +func TestAutherRegistry_GetEmpty(t *testing.T) { + r := new(autherRegistry) + if v := r.Get(""); v != nil { + t.Fatal("Get(empty) should return nil") + } +} + +func TestAutherRegistry_Delegate(t *testing.T) { + r := new(autherRegistry) + r.Register("a", &mockAuther{id: "user1", valid: true}) + + w := r.Get("a") + id, ok := w.Authenticate(context.Background(), "user", "pass") + if !ok || id != "user1" { + t.Fatalf("Authenticate = (%q, %v), want (user1, true)", id, ok) + } +} + +func TestAutherRegistry_NilUnderlying(t *testing.T) { + r := new(autherRegistry) + w := r.Get("ghost") + // No underlying value registered → should return ("", true) per wrapper default. + id, ok := w.Authenticate(context.Background(), "u", "p") + if !ok || id != "" { + t.Fatalf("nil underlying = (%q, %v), want (\"\", true)", id, ok) + } +} + +// --- Bypass wrapper tests --- + +type mockBypass struct { + contains bool + whitelist bool +} + +func (m *mockBypass) Contains(_ context.Context, _, _ string, _ ...bypass.Option) bool { + return m.contains +} + +func (m *mockBypass) IsWhitelist() bool { return m.whitelist } + +func TestBypassRegistry_Delegate(t *testing.T) { + r := new(bypassRegistry) + r.Register("b", &mockBypass{contains: true, whitelist: true}) + + w := r.Get("b") + if !w.Contains(context.Background(), "tcp", "1.2.3.4") { + t.Fatal("Contains should delegate") + } + if !w.IsWhitelist() { + t.Fatal("IsWhitelist should delegate") + } +} + +func TestBypassRegistry_NilUnderlying(t *testing.T) { + r := new(bypassRegistry) + w := r.Get("ghost") + if w.Contains(context.Background(), "tcp", "x") { + t.Fatal("nil underlying Contains should return false") + } + if w.IsWhitelist() { + t.Fatal("nil underlying IsWhitelist should return false") + } +} + +// --- Chain wrapper tests --- + +type mockChainer struct { + route chain.Route + md metadata.Metadata +} + +func (m *mockChainer) Route(_ context.Context, _, _ string, _ ...chain.RouteOption) chain.Route { + return m.route +} +func (m *mockChainer) Marker() selector.Marker { + return nil +} +func (m *mockChainer) Metadata() metadata.Metadata { return m.md } + +func TestChainRegistry_Delegate(t *testing.T) { + r := new(chainRegistry) + r.Register("c", &mockChainer{}) + + w := r.Get("c") + if w == nil { + t.Fatal("Get returned nil") + } + if w.Route(context.Background(), "tcp", "addr") != nil { + t.Fatal("mock returns nil route") + } +} + +func TestChainRegistry_NilUnderlying(t *testing.T) { + r := new(chainRegistry) + w := r.Get("ghost") + if w.Route(context.Background(), "tcp", "x") != nil { + t.Fatal("nil underlying Route should return nil") + } +} + +// --- Hop wrapper tests --- + +type mockHop struct { + node *chain.Node +} + +func (m *mockHop) Select(_ context.Context, _ ...hop.SelectOption) *chain.Node { + return m.node +} +func (m *mockHop) Nodes() []*chain.Node { return nil } + +func TestHopRegistry_Delegate(t *testing.T) { + r := new(hopRegistry) + r.Register("h", &mockHop{}) + + w := r.Get("h") + if w.Select(context.Background()) != nil { + t.Fatal("mock Select returns nil") + } +} + +func TestHopRegistry_NilUnderlying(t *testing.T) { + r := new(hopRegistry) + w := r.Get("ghost") + if w.Select(context.Background()) != nil { + t.Fatal("nil underlying Select should return nil") + } +} + +// --- Hosts wrapper tests --- + +type mockHosts struct { + ips []net.IP + ok bool +} + +func (m *mockHosts) Lookup(_ context.Context, _, _ string, _ ...hosts.Option) ([]net.IP, bool) { + return m.ips, m.ok +} + +func TestHostsRegistry_Delegate(t *testing.T) { + r := new(hostsRegistry) + ips := []net.IP{net.ParseIP("1.2.3.4")} + r.Register("h", &mockHosts{ips: ips, ok: true}) + + w := r.Get("h") + result, ok := w.Lookup(context.Background(), "tcp", "example.com") + if !ok || len(result) != 1 { + t.Fatalf("Lookup = (%v, %v)", result, ok) + } +} + +func TestHostsRegistry_NilUnderlying(t *testing.T) { + r := new(hostsRegistry) + w := r.Get("ghost") + ips, ok := w.Lookup(context.Background(), "tcp", "x") + if ok || ips != nil { + t.Fatal("nil underlying should return (nil, false)") + } +} + +// --- Ingress wrapper tests --- + +type mockIngress struct { + rule *ingress.Rule + set bool +} + +func (m *mockIngress) GetRule(_ context.Context, _ string, _ ...ingress.Option) *ingress.Rule { + return m.rule +} +func (m *mockIngress) SetRule(_ context.Context, _ *ingress.Rule, _ ...ingress.Option) bool { + return m.set +} + +func TestIngressRegistry_Delegate(t *testing.T) { + r := new(ingressRegistry) + r.Register("i", &mockIngress{set: true}) + + w := r.Get("i") + if w.GetRule(context.Background(), "example.com") != nil { + t.Fatal("mock GetRule returns nil") + } + if !w.SetRule(context.Background(), nil) { + t.Fatal("SetRule should delegate") + } +} + +func TestIngressRegistry_NilUnderlying(t *testing.T) { + r := new(ingressRegistry) + w := r.Get("ghost") + if w.GetRule(context.Background(), "x") != nil { + t.Fatal("nil GetRule should return nil") + } + if w.SetRule(context.Background(), nil) { + t.Fatal("nil SetRule should return false") + } +} + +// --- TrafficLimiter wrapper tests --- + +type mockTrafficLimiter struct { + in traffic.Limiter + out traffic.Limiter +} + +func (m *mockTrafficLimiter) In(_ context.Context, _ string, _ ...limiter.Option) traffic.Limiter { + return m.in +} +func (m *mockTrafficLimiter) Out(_ context.Context, _ string, _ ...limiter.Option) traffic.Limiter { + return m.out +} + +func TestTrafficLimiterRegistry_Delegate(t *testing.T) { + r := new(trafficLimiterRegistry) + r.Register("tl", &mockTrafficLimiter{}) + + w := r.Get("tl") + if w.In(context.Background(), "k") != nil { + t.Fatal("mock In returns nil") + } + if w.Out(context.Background(), "k") != nil { + t.Fatal("mock Out returns nil") + } +} + +func TestTrafficLimiterRegistry_NilUnderlying(t *testing.T) { + r := new(trafficLimiterRegistry) + w := r.Get("ghost") + if w.In(context.Background(), "k") != nil { + t.Fatal("nil In should return nil") + } + if w.Out(context.Background(), "k") != nil { + t.Fatal("nil Out should return nil") + } +} + +// --- ConnLimiter wrapper tests --- + +type mockConnLimiter struct { + lim conn.Limiter +} + +func (m *mockConnLimiter) Limiter(_ string) conn.Limiter { return m.lim } + +func TestConnLimiterRegistry_Delegate(t *testing.T) { + r := new(connLimiterRegistry) + r.Register("cl", &mockConnLimiter{}) + + w := r.Get("cl") + if w.Limiter("k") != nil { + t.Fatal("mock returns nil limiter") + } +} + +func TestConnLimiterRegistry_NilUnderlying(t *testing.T) { + r := new(connLimiterRegistry) + w := r.Get("ghost") + if w.Limiter("k") != nil { + t.Fatal("nil underlying should return nil") + } +} + +// --- RateLimiter wrapper tests --- + +type mockRateLimiter struct { + lim rate.Limiter +} + +func (m *mockRateLimiter) Limiter(_ string) rate.Limiter { return m.lim } + +func TestRateLimiterRegistry_Delegate(t *testing.T) { + r := new(rateLimiterRegistry) + r.Register("rl", &mockRateLimiter{}) + + w := r.Get("rl") + if w.Limiter("k") != nil { + t.Fatal("mock returns nil limiter") + } +} + +func TestRateLimiterRegistry_NilUnderlying(t *testing.T) { + r := new(rateLimiterRegistry) + w := r.Get("ghost") + if w.Limiter("k") != nil { + t.Fatal("nil underlying should return nil") + } +} + +// --- Observer wrapper tests --- + +type mockObserver struct { + err error +} + +func (m *mockObserver) Observe(_ context.Context, _ []observer.Event, _ ...observer.Option) error { + return m.err +} + +func TestObserverRegistry_Delegate(t *testing.T) { + r := new(observerRegistry) + r.Register("o", &mockObserver{}) + + w := r.Get("o") + if err := w.Observe(context.Background(), nil); err != nil { + t.Fatalf("Observe = %v", err) + } +} + +func TestObserverRegistry_NilUnderlying(t *testing.T) { + r := new(observerRegistry) + w := r.Get("ghost") + if err := w.Observe(context.Background(), nil); err != nil { + t.Fatal("nil underlying Observe should return nil") + } +} + +type mockCloseableObserver struct { + closed bool +} + +func (m *mockCloseableObserver) Observe(_ context.Context, _ []observer.Event, _ ...observer.Option) error { + return nil +} +func (m *mockCloseableObserver) Close() error { + m.closed = true + return nil +} + +func TestObserverRegistry_WrapperClose(t *testing.T) { + r := new(observerRegistry) + mock := &mockCloseableObserver{} + r.Register("o", mock) + + w := r.Get("o") + if err := w.(interface{ Close() error }).Close(); err != nil { + t.Fatal(err) + } + if !mock.closed { + t.Fatal("wrapper Close did not close underlying") + } +} + +func TestObserverRegistry_WrapperCloseNilUnderlying(t *testing.T) { + r := new(observerRegistry) + w := r.Get("ghost") + if err := w.(interface{ Close() error }).Close(); err != nil { + t.Fatalf("Close on nil underlying: %v", err) + } +} + +// --- Recorder wrapper tests --- + +type mockRecorder struct { + err error +} + +func (m *mockRecorder) Record(_ context.Context, _ []byte, _ ...recorder.RecordOption) error { + return m.err +} + +func TestRecorderRegistry_Delegate(t *testing.T) { + r := new(recorderRegistry) + r.Register("r", &mockRecorder{}) + + w := r.Get("r") + if err := w.Record(context.Background(), []byte("data")); err != nil { + t.Fatalf("Record = %v", err) + } +} + +func TestRecorderRegistry_NilUnderlying(t *testing.T) { + r := new(recorderRegistry) + w := r.Get("ghost") + if err := w.Record(context.Background(), []byte("x")); err != nil { + t.Fatal("nil underlying Record should return nil") + } +} + +// --- Resolver wrapper tests --- + +type mockResolver struct { + ips []net.IP + err error +} + +func (m *mockResolver) Resolve(_ context.Context, _, _ string, _ ...resolver.Option) ([]net.IP, error) { + return m.ips, m.err +} + +func TestResolverRegistry_Delegate(t *testing.T) { + r := new(resolverRegistry) + ips := []net.IP{net.ParseIP("1.2.3.4")} + r.Register("r", &mockResolver{ips: ips}) + + w := r.Get("r") + result, err := w.Resolve(context.Background(), "tcp", "example.com") + if err != nil { + t.Fatal(err) + } + if len(result) != 1 { + t.Fatalf("Resolve = %v", result) + } +} + +func TestResolverRegistry_NilUnderlying(t *testing.T) { + r := new(resolverRegistry) + w := r.Get("ghost") + ips, err := w.Resolve(context.Background(), "tcp", "x") + if !errors.Is(err, resolver.ErrInvalid) { + t.Fatalf("expected resolver.ErrInvalid, got %v", err) + } + if ips != nil { + t.Fatal("should return nil IPs") + } +} + +// --- Router wrapper tests --- + +type mockRouter struct { + route *router.Route +} + +func (m *mockRouter) GetRoute(_ context.Context, _ string, _ ...router.Option) *router.Route { + return m.route +} + +func TestRouterRegistry_Delegate(t *testing.T) { + r := new(routerRegistry) + r.Register("r", &mockRouter{}) + + w := r.Get("r") + if w.GetRoute(context.Background(), "1.2.3.4") != nil { + t.Fatal("mock returns nil route") + } +} + +func TestRouterRegistry_NilUnderlying(t *testing.T) { + r := new(routerRegistry) + w := r.Get("ghost") + if w.GetRoute(context.Background(), "x") != nil { + t.Fatal("nil underlying should return nil") + } +} + +// --- SD wrapper tests --- + +type mockSD struct { + services []*sd.Service + err error +} + +func (m *mockSD) Register(_ context.Context, _ *sd.Service, _ ...sd.Option) error { + return m.err +} +func (m *mockSD) Deregister(_ context.Context, _ *sd.Service) error { return m.err } +func (m *mockSD) Renew(_ context.Context, _ *sd.Service) error { return m.err } +func (m *mockSD) Get(_ context.Context, _ string) ([]*sd.Service, error) { + return m.services, m.err +} + +func TestSDRegistry_Delegate(t *testing.T) { + r := new(sdRegistry) + svcs := []*sd.Service{{Name: "test"}} + r.Register("s", &mockSD{services: svcs}) + + w := r.Get("s") + if err := w.Register(context.Background(), &sd.Service{Name: "x"}); err != nil { + t.Fatal(err) + } + if err := w.Deregister(context.Background(), &sd.Service{}); err != nil { + t.Fatal(err) + } + if err := w.Renew(context.Background(), &sd.Service{}); err != nil { + t.Fatal(err) + } + result, err := w.Get(context.Background(), "test") + if err != nil || len(result) != 1 { + t.Fatalf("Get = (%v, %v)", result, err) + } +} + +func TestSDRegistry_NilUnderlying(t *testing.T) { + r := new(sdRegistry) + w := r.Get("ghost") + if err := w.Register(context.Background(), nil); err != nil { + t.Fatal("nil Register should return nil") + } + if err := w.Deregister(context.Background(), nil); err != nil { + t.Fatal("nil Deregister should return nil") + } + if err := w.Renew(context.Background(), nil); err != nil { + t.Fatal("nil Renew should return nil") + } + if svcs, err := w.Get(context.Background(), "x"); err != nil || svcs != nil { + t.Fatalf("nil Get = (%v, %v)", svcs, err) + } +} + +// --- Logger registry tests --- + +type mockLogger struct{} + +func (m *mockLogger) Trace(_ ...any) {} +func (m *mockLogger) Tracef(_ string, _ ...any) {} +func (m *mockLogger) Debug(_ ...any) {} +func (m *mockLogger) Debugf(_ string, _ ...any) {} +func (m *mockLogger) Info(_ ...any) {} +func (m *mockLogger) Infof(_ string, _ ...any) {} +func (m *mockLogger) Warn(_ ...any) {} +func (m *mockLogger) Warnf(_ string, _ ...any) {} +func (m *mockLogger) Error(_ ...any) {} +func (m *mockLogger) Errorf(_ string, _ ...any) {} +func (m *mockLogger) Fatal(_ ...any) {} +func (m *mockLogger) Fatalf(_ string, _ ...any) {} +func (m *mockLogger) GetLevel() logger.LogLevel { return logger.DebugLevel } +func (m *mockLogger) IsLevelEnabled(_ logger.LogLevel) bool { return true } +func (m *mockLogger) WithFields(_ map[string]any) logger.Logger { return m } + +func TestLoggerRegistry_RegisterGet(t *testing.T) { + r := new(loggerRegistry) + l := &mockLogger{} + r.Register("log", l) + if v := r.Get("log"); v == nil { + t.Fatal("Get returned nil") + } +} + +func TestLoggerRegistry_GetEmpty(t *testing.T) { + r := new(loggerRegistry) + if v := r.Get(""); v != nil { + t.Fatal("empty name should return nil logger") + } +} + +// --- Service registry tests --- + +func TestServiceRegistry_RegisterGet(t *testing.T) { + r := new(serviceRegistry) + svc := &closeableService{} + r.Register("svc", svc) + if v := r.Get("svc"); v == nil { + t.Fatal("Get returned nil") + } +} + +// --- Factory registry tests (listener, handler, dialer, connector) --- +// These call logger.Fatal on duplicate, so we only test the non-duplicate path. + +func TestListenerRegistry_Register(t *testing.T) { + r := new(listenerRegistry) + factory := func(_ ...listener.Option) listener.Listener { return nil } + if err := r.Register("tcp", factory); err != nil { + t.Fatal(err) + } + if !r.IsRegistered("tcp") { + t.Fatal("should be registered") + } +} + +func TestHandlerRegistry_Register(t *testing.T) { + r := new(handlerRegistry) + factory := func(_ ...handler.Option) handler.Handler { return nil } + if err := r.Register("http", factory); err != nil { + t.Fatal(err) + } + if !r.IsRegistered("http") { + t.Fatal("should be registered") + } +} + +func TestDialerRegistry_Register(t *testing.T) { + r := new(dialerRegistry) + factory := func(_ ...dialer.Option) dialer.Dialer { return nil } + if err := r.Register("tcp", factory); err != nil { + t.Fatal(err) + } + if !r.IsRegistered("tcp") { + t.Fatal("should be registered") + } +} + +func TestConnectorRegistry_Register(t *testing.T) { + r := new(connectorRegistry) + factory := func(_ ...connector.Option) connector.Connector { return nil } + if err := r.Register("http", factory); err != nil { + t.Fatal(err) + } + if !r.IsRegistered("http") { + t.Fatal("should be registered") + } +} + +// --- Interface satisfaction checks --- + +var _ reg.Registry[NewListener] = (*listenerRegistry)(nil) +var _ reg.Registry[NewHandler] = (*handlerRegistry)(nil) +var _ reg.Registry[NewDialer] = (*dialerRegistry)(nil) +var _ reg.Registry[NewConnector] = (*connectorRegistry)(nil) +var _ reg.Registry[service.Service] = (*serviceRegistry)(nil) +var _ reg.Registry[chain.Chainer] = (*chainRegistry)(nil) +var _ reg.Registry[hop.Hop] = (*hopRegistry)(nil) +var _ reg.Registry[auth.Authenticator] = (*autherRegistry)(nil) +var _ reg.Registry[admission.Admission] = (*admissionRegistry)(nil) +var _ reg.Registry[bypass.Bypass] = (*bypassRegistry)(nil) +var _ reg.Registry[resolver.Resolver] = (*resolverRegistry)(nil) +var _ reg.Registry[hosts.HostMapper] = (*hostsRegistry)(nil) +var _ reg.Registry[recorder.Recorder] = (*recorderRegistry)(nil) +var _ reg.Registry[traffic.TrafficLimiter] = (*trafficLimiterRegistry)(nil) +var _ reg.Registry[conn.ConnLimiter] = (*connLimiterRegistry)(nil) +var _ reg.Registry[rate.RateLimiter] = (*rateLimiterRegistry)(nil) +var _ reg.Registry[ingress.Ingress] = (*ingressRegistry)(nil) +var _ reg.Registry[router.Router] = (*routerRegistry)(nil) +var _ reg.Registry[sd.SD] = (*sdRegistry)(nil) +var _ reg.Registry[observer.Observer] = (*observerRegistry)(nil) +var _ reg.Registry[logger.Logger] = (*loggerRegistry)(nil) + diff --git a/registry/resolver.go b/registry/resolver.go index 63f21314..3beaceea 100644 --- a/registry/resolver.go +++ b/registry/resolver.go @@ -7,14 +7,18 @@ import ( "github.com/go-gost/core/resolver" ) +// resolverRegistry implements a hot-reload-safe registry for resolver.Resolver. type resolverRegistry struct { registry[resolver.Resolver] } +// Register stores a Resolver under the given name. func (r *resolverRegistry) Register(name string, v resolver.Resolver) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Resolver. +// Returns nil if name is empty. func (r *resolverRegistry) Get(name string) resolver.Resolver { if name != "" { return &resolverWrapper{name: name, r: r} diff --git a/registry/router.go b/registry/router.go index fcbd814e..cd099310 100644 --- a/registry/router.go +++ b/registry/router.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/router" ) +// routerRegistry implements a hot-reload-safe registry for router.Router. type routerRegistry struct { registry[router.Router] } +// Register stores a Router under the given name. func (r *routerRegistry) Register(name string, v router.Router) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered Router. +// Returns nil if name is empty. func (r *routerRegistry) Get(name string) router.Router { if name != "" { return &routerWrapper{name: name, r: r} diff --git a/registry/sd.go b/registry/sd.go index 775a1e0c..f99260a8 100644 --- a/registry/sd.go +++ b/registry/sd.go @@ -6,14 +6,18 @@ import ( "github.com/go-gost/core/sd" ) +// sdRegistry implements a hot-reload-safe registry for sd.SD. type sdRegistry struct { registry[sd.SD] } +// Register stores an SD under the given name. func (r *sdRegistry) Register(name string, v sd.SD) error { return r.registry.Register(name, v) } +// Get returns a wrapper that delegates to the currently registered SD. +// Returns nil if name is empty. func (r *sdRegistry) Get(name string) sd.SD { if name != "" { return &sdWrapper{name: name, r: r} diff --git a/registry/service.go b/registry/service.go index ab47d86b..bf4a00a3 100644 --- a/registry/service.go +++ b/registry/service.go @@ -4,6 +4,9 @@ import ( "github.com/go-gost/core/service" ) +// serviceRegistry stores service.Service instances. +// It uses the base registry methods without a wrapper since services +// are looked up directly by name during startup. type serviceRegistry struct { registry[service.Service] }