-
Notifications
You must be signed in to change notification settings - Fork 2
Define Provider interface and ProviderRegistry #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| package providers | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| ) | ||
|
|
||
| // Provider is the core abstraction for deploying database infrastructure. | ||
| // Phase 2a defines a minimal interface (Name, ValidateVersion, DefaultPorts). | ||
| // Phase 2b will add CreateSandbox, Start, Stop, Destroy, HealthCheck when | ||
| // ProxySQL and other providers need them. | ||
| type Provider interface { | ||
| Name() string | ||
| ValidateVersion(version string) error | ||
| DefaultPorts() PortRange | ||
| } | ||
|
|
||
| type PortRange struct { | ||
| BasePort int | ||
| PortsPerInstance int | ||
| } | ||
|
|
||
| type Registry struct { | ||
| providers map[string]Provider | ||
| } | ||
|
|
||
| func NewRegistry() *Registry { | ||
| return &Registry{providers: make(map[string]Provider)} | ||
| } | ||
|
Comment on lines
+23
to
+29
|
||
|
|
||
| func (r *Registry) Register(p Provider) error { | ||
| name := p.Name() | ||
| if _, exists := r.providers[name]; exists { | ||
| return fmt.Errorf("provider %q already registered", name) | ||
| } | ||
| r.providers[name] = p | ||
| return nil | ||
|
Comment on lines
+31
to
+37
|
||
| } | ||
|
|
||
| func (r *Registry) Get(name string) (Provider, error) { | ||
| p, exists := r.providers[name] | ||
| if !exists { | ||
| return nil, fmt.Errorf("provider %q not found", name) | ||
| } | ||
| return p, nil | ||
| } | ||
|
|
||
| func (r *Registry) List() []string { | ||
| names := make([]string, 0, len(r.providers)) | ||
| for name := range r.providers { | ||
| names = append(names, name) | ||
| } | ||
| sort.Strings(names) | ||
| return names | ||
| } | ||
|
|
||
| var DefaultRegistry = NewRegistry() | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| package providers | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import "testing" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| type mockProvider struct{ name string } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func (m *mockProvider) Name() string { return m.name } | ||||||||||||||||||||||||||||||||||||||||||||||
| func (m *mockProvider) ValidateVersion(version string) error { return nil } | ||||||||||||||||||||||||||||||||||||||||||||||
| func (m *mockProvider) DefaultPorts() PortRange { return PortRange{BasePort: 9999, PortsPerInstance: 1} } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func TestRegistryRegisterAndGet(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||
| reg := NewRegistry() | ||||||||||||||||||||||||||||||||||||||||||||||
| mock := &mockProvider{name: "test"} | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := reg.Register(mock); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("Register failed: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| p, err := reg.Get("test") | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("Get failed: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| if p.Name() != "test" { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("expected name 'test', got %q", p.Name()) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| func TestRegistryDuplicateRegister(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||
| reg := NewRegistry() | ||||||||||||||||||||||||||||||||||||||||||||||
| mock := &mockProvider{name: "test"} | ||||||||||||||||||||||||||||||||||||||||||||||
| _ = reg.Register(mock) | ||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a good practice to check for errors even on the first registration call. While it's unlikely to fail in this test, explicitly checking the error makes the test more robust and clearly states the assumption that the first registration should succeed. if err := reg.Register(mock); err != nil {
t.Fatalf("initial register failed: %v", err)
} |
||||||||||||||||||||||||||||||||||||||||||||||
| if err := reg.Register(mock); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatal("expected error on duplicate register") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| func TestRegistryRegisterNilProvider(t *testing.T) { | |
| reg := NewRegistry() | |
| if err := reg.Register(nil); err == nil { | |
| t.Fatal("expected error when registering nil provider") | |
| } | |
| if names := reg.List(); len(names) != 0 { | |
| t.Fatalf("expected no providers to be registered, got %v", names) | |
| } | |
| } | |
| func TestRegistryRegisterEmptyName(t *testing.T) { | |
| reg := NewRegistry() | |
| mock := &mockProvider{name: ""} | |
| if err := reg.Register(mock); err == nil { | |
| t.Fatal("expected error when registering provider with empty name") | |
| } | |
| if names := reg.List(); len(names) != 0 { | |
| t.Fatalf("expected no providers to be registered, got %v", names) | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a good practice to check for errors on registration calls. While it's unlikely to fail in this test, explicitly checking the errors makes the test more robust and clearly states the assumption that these registrations should succeed.
if err := reg.Register(&mockProvider{name: "b"}); err != nil {
t.Fatalf("register 'b' failed: %v", err)
}
if err := reg.Register(&mockProvider{name: "a"}); err != nil {
t.Fatalf("register 'a' failed: %v", err)
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For comparing slices, it's more idiomatic and scalable to use reflect.DeepEqual. This also checks the length, so you can combine the length and content checks into one. This makes the test easier to read and maintain.
You will need to add "reflect" to your imports.
expected := []string{"a", "b"}
if !reflect.DeepEqual(names, expected) {
t.Errorf("expected sorted %v, got %v", expected, names)
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Registryis not safe for concurrent use. TheDefaultRegistryis a global variable, which makes concurrent access from multiple goroutines likely as the application grows. This can lead to race conditions when reading from and writing to theprovidersmap.To fix this, you should add a
sync.RWMutexto theRegistrystruct and use it to protect access to theprovidersmap inRegister,Get, andListmethods.For example:
You will also need to add
"sync"to your imports.