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] }