Skip to content

Commit b178a98

Browse files
refactor(stamus): replace global function pointers with interface-based DI
Introduce FileOpener and ContainerLister interfaces with a ConfigManager struct that holds dependencies. Move package-level functions to methods on ConfigManager and add backward-compatible wrapper functions so external callers remain unchanged. Tests now use mock implementations per test instead of mutating shared global function pointers, making them safe for t.Parallel().
1 parent 9cc56e9 commit b178a98

6 files changed

Lines changed: 425 additions & 335 deletions

File tree

internal/stamus/config.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,18 +85,3 @@ func GetConfigsList() ([]string, error) {
8585
return configs, nil
8686
}
8787

88-
// GetProjectName returns the project name for a given config folder path.
89-
// Returns empty string if the folder is not found in the instances.
90-
func GetProjectName(folder string) string {
91-
config, err := GetStamusConfig()
92-
if err != nil {
93-
return ""
94-
}
95-
if config.Instances == nil {
96-
return ""
97-
}
98-
if infos, ok := config.Instances[Folder(folder)]; ok {
99-
return infos.Project
100-
}
101-
return ""
102-
}

internal/stamus/deps.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package stamus
2+
3+
import (
4+
"context"
5+
"io"
6+
"os"
7+
8+
"github.com/docker/docker/api/types"
9+
"github.com/docker/docker/api/types/container"
10+
"github.com/docker/docker/api/types/filters"
11+
"github.com/docker/docker/client"
12+
)
13+
14+
// FileOpener abstracts file system operations for config management.
15+
type FileOpener interface {
16+
MkdirAll(path string, perm os.FileMode) error
17+
OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
18+
ReadAll(r io.Reader) ([]byte, error)
19+
}
20+
21+
// ContainerLister abstracts Docker container queries.
22+
type ContainerLister interface {
23+
GetContainersByProject(projectName string) ([]types.Container, error)
24+
}
25+
26+
// ConfigManager holds dependencies for config and instance operations.
27+
type ConfigManager struct {
28+
fs FileOpener
29+
containers ContainerLister
30+
}
31+
32+
// NewConfigManager creates a ConfigManager with the given dependencies.
33+
func NewConfigManager(fs FileOpener, containers ContainerLister) *ConfigManager {
34+
return &ConfigManager{fs: fs, containers: containers}
35+
}
36+
37+
// osFileOpener implements FileOpener with real OS calls.
38+
type osFileOpener struct{}
39+
40+
func (o *osFileOpener) MkdirAll(path string, perm os.FileMode) error {
41+
return os.MkdirAll(path, perm)
42+
}
43+
44+
func (o *osFileOpener) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
45+
return os.OpenFile(name, flag, perm)
46+
}
47+
48+
func (o *osFileOpener) ReadAll(r io.Reader) ([]byte, error) {
49+
return io.ReadAll(r)
50+
}
51+
52+
// dockerContainerLister implements ContainerLister with real Docker API.
53+
type dockerContainerLister struct{}
54+
55+
func (d *dockerContainerLister) GetContainersByProject(projectName string) ([]types.Container, error) {
56+
apiClient, err := client.NewClientWithOpts(client.FromEnv)
57+
if err != nil {
58+
return nil, err
59+
}
60+
defer apiClient.Close()
61+
62+
return apiClient.ContainerList(context.Background(), container.ListOptions{
63+
All: true,
64+
Filters: filters.NewArgs(
65+
filters.Arg("label", "com.docker.compose.project="+projectName),
66+
),
67+
})
68+
}
69+
70+
// DefaultManager is the package-level ConfigManager used by backward-compatible wrapper functions.
71+
var DefaultManager = NewConfigManager(&osFileOpener{}, &dockerContainerLister{})

internal/stamus/fs.go

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,47 @@
11
package stamus
22

33
import (
4-
// Common
54
"encoding/json"
6-
"io"
75
"os"
86
"path/filepath"
97

10-
// Custom
118
"stamus-ctl/internal/app"
129
)
1310

14-
var (
15-
osMkdirAll = os.MkdirAll
16-
osOpenFile = os.OpenFile
17-
ioReadAll = io.ReadAll
18-
)
19-
20-
func getOrCreateStamusConfigFile() (*os.File, error) {
11+
func (cm *ConfigManager) getOrCreateConfigFile() (*os.File, error) {
2112
// Create ~/stamus directory
22-
err := osMkdirAll(app.ConfigFolder, 0o755)
13+
err := cm.fs.MkdirAll(app.ConfigFolder, 0o755)
2314
if err != nil {
2415
return nil, err
2516
}
2617

2718
// Open or create ~/stamus/config.json
28-
f, err := osOpenFile(filepath.Join(app.ConfigFolder, "config.json"), os.O_RDWR|os.O_CREATE, 0o755)
19+
f, err := cm.fs.OpenFile(filepath.Join(app.ConfigFolder, "config.json"), os.O_RDWR|os.O_CREATE, 0o755)
2920
if err != nil {
3021
return nil, err
3122
}
3223

3324
return f, nil
3425
}
3526

36-
func tryGetStamusConfigFile() (*os.File, error) {
37-
// Open or create ~/stamus/config.json
38-
f, err := osOpenFile(filepath.Join(app.ConfigFolder, "config.json"), os.O_RDONLY, 0o755)
27+
func (cm *ConfigManager) tryGetConfigFile() (*os.File, error) {
28+
// Open ~/stamus/config.json
29+
f, err := cm.fs.OpenFile(filepath.Join(app.ConfigFolder, "config.json"), os.O_RDONLY, 0o755)
3930
if err != nil {
4031
return nil, err
4132
}
4233

4334
return f, nil
4435
}
4536

46-
func GetStamusConfig() (*Config, error) {
37+
func (cm *ConfigManager) GetConfig() (*Config, error) {
4738
// Open or create ~/stamus/config.json
48-
file, err := tryGetStamusConfigFile()
39+
file, err := cm.tryGetConfigFile()
4940
if err != nil {
5041
return &Config{}, nil
5142
}
5243
// Read the file contents
53-
bytes, err := ioReadAll(file)
44+
bytes, err := cm.fs.ReadAll(file)
5445
if err != nil {
5546
return &Config{}, nil
5647
}
@@ -65,3 +56,13 @@ func GetStamusConfig() (*Config, error) {
6556

6657
return config, nil
6758
}
59+
60+
// Backward-compatible package-level functions
61+
62+
func getOrCreateStamusConfigFile() (*os.File, error) {
63+
return DefaultManager.getOrCreateConfigFile()
64+
}
65+
66+
func GetStamusConfig() (*Config, error) {
67+
return DefaultManager.GetConfig()
68+
}

0 commit comments

Comments
 (0)