refactor: use samber/lo wherever possible

This commit is contained in:
2026-03-24 17:14:29 +02:00
parent 0346ec0899
commit 26c5b97823
25 changed files with 252 additions and 285 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/chenasraf/sofmani/platform"
"github.com/chenasraf/sofmani/utils"
"github.com/eschao/config"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)
@@ -76,10 +77,7 @@ type AppConfigDefaults struct {
// GetCategoryDisplay returns the effective category display mode, defaulting to "border".
func (c *AppConfig) GetCategoryDisplay() CategoryDisplayMode {
if c.CategoryDisplay != nil {
return *c.CategoryDisplay
}
return CategoryDisplayBorder
return lo.FromPtrOr(c.CategoryDisplay, CategoryDisplayBorder)
}
// Environ returns the combined environment variables as a slice of strings.
@@ -172,21 +170,9 @@ func tryConfigDir(dir string) string {
// GetConfigDesc returns a string slice describing the current configuration.
func (c *AppConfig) GetConfigDesc() []string {
desc := []string{}
isDebug := false
if c.Debug != nil {
isDebug = *c.Debug
}
checkUpdates := false
if c.CheckUpdates != nil {
checkUpdates = *c.CheckUpdates
}
showSummary := true // default is enabled
if c.Summary != nil {
showSummary = *c.Summary
}
desc = append(desc, fmt.Sprintf("Debug: %t", isDebug))
desc = append(desc, fmt.Sprintf("CheckUpdates: %t", checkUpdates))
desc = append(desc, fmt.Sprintf("Summary: %t", showSummary))
desc = append(desc, fmt.Sprintf("Debug: %t", lo.FromPtrOr(c.Debug, false)))
desc = append(desc, fmt.Sprintf("CheckUpdates: %t", lo.FromPtrOr(c.CheckUpdates, false)))
desc = append(desc, fmt.Sprintf("Summary: %t", lo.FromPtrOr(c.Summary, true)))
if c.Env != nil {
desc = append(desc, "Environment Variables:")

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/chenasraf/sofmani/platform"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -16,18 +17,18 @@ func TestPlatformMapResolve(t *testing.T) {
platform string
expected *string
}{
{"MacOS", "darwin", strPtr("macos")},
{"Linux", "linux", strPtr("linux")},
{"Windows", "windows", strPtr("windows")},
{"MacOS", "darwin", lo.ToPtr("macos")},
{"Linux", "linux", lo.ToPtr("linux")},
{"Windows", "windows", lo.ToPtr("windows")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
platform.SetOS(tt.platform)
pm := platform.PlatformMap[string]{
MacOS: strPtr("macos"),
Linux: strPtr("linux"),
Windows: strPtr("windows"),
MacOS: lo.ToPtr("macos"),
Linux: lo.ToPtr("linux"),
Windows: lo.ToPtr("windows"),
}
assert.Equal(t, tt.expected, pm.Resolve())
})
@@ -135,7 +136,3 @@ func TestFindConfigFile(t *testing.T) {
assert.NoError(t, os.Chdir(dir))
assert.True(t, strings.HasSuffix(FindConfigFile(), file))
}
func strPtr(s string) *string {
return &s
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/chenasraf/sofmani/machine"
"github.com/samber/lo"
"github.com/spf13/cobra"
)
@@ -153,26 +154,26 @@ func buildCliConfig(cmd *cobra.Command, args []string) *appconfig.AppCliConfig {
// Handle debug flag
if cmd.Flags().Changed("debug") {
config.Debug = boolPtr(true)
config.Debug = lo.ToPtr(true)
}
if cmd.Flags().Changed("no-debug") {
config.Debug = boolPtr(false)
config.Debug = lo.ToPtr(false)
}
// Handle update flag
if cmd.Flags().Changed("update") {
config.CheckUpdates = boolPtr(true)
config.CheckUpdates = lo.ToPtr(true)
}
if cmd.Flags().Changed("no-update") {
config.CheckUpdates = boolPtr(false)
config.CheckUpdates = lo.ToPtr(false)
}
// Handle summary flag
if cmd.Flags().Changed("summary") {
config.Summary = boolPtr(true)
config.Summary = lo.ToPtr(true)
}
if cmd.Flags().Changed("no-summary") {
config.Summary = boolPtr(false)
config.Summary = lo.ToPtr(false)
}
// Handle log file flag
@@ -201,10 +202,5 @@ func buildCliConfig(cmd *cobra.Command, args []string) *appconfig.AppCliConfig {
return config
}
// boolPtr returns a pointer to a boolean value.
func boolPtr(b bool) *bool {
return &b
}
// RunMain is set by main.go to run the main application logic.
var RunMain func(cliConfig *appconfig.AppCliConfig)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newAptInstaller(data *appconfig.InstallerData) *AptInstaller {
@@ -21,7 +22,7 @@ func TestAptValidation(t *testing.T) {
logger.InitLogger(false)
aptInstaller := newAptInstaller(
&appconfig.InstallerData{
Name: strPtr("test-apt"),
Name: lo.ToPtr("test-apt"),
Type: appconfig.InstallerTypeApt,
},
)
@@ -33,7 +34,7 @@ func TestAptGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeApt,
}
installer := newAptInstaller(defaultData)
@@ -50,7 +51,7 @@ func TestAptGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeApt,
Opts: &map[string]any{
"flags": "-y --no-install-recommends",
@@ -64,7 +65,7 @@ func TestAptGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeApt,
Opts: &map[string]any{
"install_flags": "--no-install-recommends",
@@ -78,7 +79,7 @@ func TestAptGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeApt,
Opts: &map[string]any{
"update_flags": "--only-upgrade",
@@ -92,7 +93,7 @@ func TestAptGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeApt,
Opts: &map[string]any{
"flags": "--common",

View File

@@ -8,6 +8,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -25,14 +26,14 @@ func TestBrewValidation(t *testing.T) {
// 🟢 Valid: No tap specified (tap is optional)
emptyData := &appconfig.InstallerData{
Name: strPtr("test-brew-valid"),
Name: lo.ToPtr("test-brew-valid"),
Type: appconfig.InstallerTypeBrew,
}
assertNoValidationErrors(t, newTestBrewInstaller(emptyData).Validate())
// 🟢 Valid: Well-formed tap (contains slash, sufficient length)
validData := &appconfig.InstallerData{
Name: strPtr("test-brew-valid-tap"),
Name: lo.ToPtr("test-brew-valid-tap"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{"tap": "valid/tap"},
}
@@ -40,7 +41,7 @@ func TestBrewValidation(t *testing.T) {
// 🟢 Valid: Tap and cask used together
tapCaskData := &appconfig.InstallerData{
Name: strPtr("test-brew-tap-cask"),
Name: lo.ToPtr("test-brew-tap-cask"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"tap": "homebrew/cask-versions",
@@ -51,7 +52,7 @@ func TestBrewValidation(t *testing.T) {
// 🔴 Invalid: Tap is present but malformed (missing slash or too short)
invalidData := &appconfig.InstallerData{
Name: strPtr("test-brew-invalid-tap"),
Name: lo.ToPtr("test-brew-invalid-tap"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{"tap": "invalid-tap"},
}
@@ -85,7 +86,7 @@ func TestBrewGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := newTestBrewInstaller(defaultData)
@@ -108,7 +109,7 @@ func TestBrewGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"flags": "--verbose --debug",
@@ -122,7 +123,7 @@ func TestBrewGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"install_flags": "--force",
@@ -136,7 +137,7 @@ func TestBrewGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"update_flags": "--dry-run",
@@ -150,7 +151,7 @@ func TestBrewGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"tap": "homebrew/core",
@@ -250,7 +251,7 @@ func TestBrewGetFullName(t *testing.T) {
t.Run("returns name without tap", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := newTestBrewInstaller(data)
@@ -259,7 +260,7 @@ func TestBrewGetFullName(t *testing.T) {
t.Run("returns tap/name with tap", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("sofmani"),
Name: lo.ToPtr("sofmani"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"tap": "chenasraf/tap",
@@ -275,7 +276,7 @@ func TestBrewIsCask(t *testing.T) {
t.Run("returns false when cask is not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := newTestBrewInstaller(data)
@@ -284,7 +285,7 @@ func TestBrewIsCask(t *testing.T) {
t.Run("returns false when cask is false", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("firefox"),
Name: lo.ToPtr("firefox"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"cask": false,
@@ -296,7 +297,7 @@ func TestBrewIsCask(t *testing.T) {
t.Run("returns true when cask is true", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("firefox"),
Name: lo.ToPtr("firefox"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"cask": true,
@@ -312,7 +313,7 @@ func TestBrewGetBinName(t *testing.T) {
t.Run("returns name when bin_name is not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := newTestBrewInstaller(data)
@@ -322,7 +323,7 @@ func TestBrewGetBinName(t *testing.T) {
t.Run("returns bin_name when set", func(t *testing.T) {
binName := "nvim"
data := &appconfig.InstallerData{
Name: strPtr("neovim"),
Name: lo.ToPtr("neovim"),
Type: appconfig.InstallerTypeBrew,
BinName: &binName,
}
@@ -333,7 +334,7 @@ func TestBrewGetBinName(t *testing.T) {
t.Run("returns name when bin_name is empty", func(t *testing.T) {
binName := ""
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
BinName: &binName,
}
@@ -347,7 +348,7 @@ func TestBrewGetData(t *testing.T) {
t.Run("returns the installer data", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := newTestBrewInstaller(data)
@@ -364,7 +365,7 @@ func TestNewBrewInstaller(t *testing.T) {
t.Run("creates installer with config and data", func(t *testing.T) {
cfg := &appconfig.AppConfig{}
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
}
installer := NewBrewInstaller(cfg, data)
@@ -382,7 +383,7 @@ func TestBrewCheckIsInstalled(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "true"
data := &appconfig.InstallerData{
Name: strPtr("test-brew"),
Name: lo.ToPtr("test-brew"),
Type: appconfig.InstallerTypeBrew,
CheckInstalled: &checkCmd,
}
@@ -396,7 +397,7 @@ func TestBrewCheckIsInstalled(t *testing.T) {
t.Run("runs custom check that fails", func(t *testing.T) {
checkCmd := "false"
data := &appconfig.InstallerData{
Name: strPtr("test-brew"),
Name: lo.ToPtr("test-brew"),
Type: appconfig.InstallerTypeBrew,
CheckInstalled: &checkCmd,
}
@@ -414,7 +415,7 @@ func TestBrewCheckNeedsUpdate(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "true" // Returns exit code 0, meaning update available
data := &appconfig.InstallerData{
Name: strPtr("test-brew"),
Name: lo.ToPtr("test-brew"),
Type: appconfig.InstallerTypeBrew,
CheckHasUpdate: &checkCmd,
}
@@ -428,7 +429,7 @@ func TestBrewCheckNeedsUpdate(t *testing.T) {
t.Run("custom check returns false when no update", func(t *testing.T) {
checkCmd := "false" // Returns exit code 1, meaning no update
data := &appconfig.InstallerData{
Name: strPtr("test-brew"),
Name: lo.ToPtr("test-brew"),
Type: appconfig.InstallerTypeBrew,
CheckHasUpdate: &checkCmd,
}
@@ -445,7 +446,7 @@ func TestBrewGetOptsWrongTypes(t *testing.T) {
t.Run("handles wrong type values gracefully", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{
"tap": 123, // Wrong type

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestCargoInstaller(data *appconfig.InstallerData) *CargoInstaller {
@@ -22,7 +23,7 @@ func TestCargoValidation(t *testing.T) {
// 🟢 Valid cargo installer
validData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
}
assertNoValidationErrors(t, newTestCargoInstaller(validData).Validate())
@@ -40,7 +41,7 @@ func TestCargoGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
}
installer := newTestCargoInstaller(defaultData)
@@ -57,7 +58,7 @@ func TestCargoGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
Opts: &map[string]any{
"flags": "--locked",
@@ -71,7 +72,7 @@ func TestCargoGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
Opts: &map[string]any{
"install_flags": "--features pcre2",
@@ -85,7 +86,7 @@ func TestCargoGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
Opts: &map[string]any{
"update_flags": "--force",
@@ -99,7 +100,7 @@ func TestCargoGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
Opts: &map[string]any{
"flags": "--common",
@@ -125,7 +126,7 @@ func TestCargoGetBinName(t *testing.T) {
// Default: uses installer name
defaultData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
}
installer := newTestCargoInstaller(defaultData)
@@ -135,9 +136,9 @@ func TestCargoGetBinName(t *testing.T) {
// Override: uses bin_name
binNameData := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
BinName: strPtr("rg"),
BinName: lo.ToPtr("rg"),
}
installerWithBinName := newTestCargoInstaller(binNameData)
if installerWithBinName.GetBinName() != "rg" {
@@ -149,7 +150,7 @@ func TestCargoGetData(t *testing.T) {
logger.InitLogger(false)
data := &appconfig.InstallerData{
Name: strPtr("ripgrep"),
Name: lo.ToPtr("ripgrep"),
Type: appconfig.InstallerTypeCargo,
}
installer := newTestCargoInstaller(data)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/require"
)
@@ -23,17 +24,17 @@ func TestDockerValidation(t *testing.T) {
// 🟢 Valid: just name and type
validData := &appconfig.InstallerData{
Name: strPtr("ghcr.io/open-webui/open-webui:main"),
Name: lo.ToPtr("ghcr.io/open-webui/open-webui:main"),
Type: appconfig.InstallerTypeDocker,
BinName: strPtr("open-webui"),
BinName: lo.ToPtr("open-webui"),
}
assertNoValidationErrors(t, newTestDockerInstaller(validData).Validate())
// 🟢 Valid: with flags
withFlags := &appconfig.InstallerData{
Name: strPtr("ghcr.io/open-webui/open-webui:main"),
Name: lo.ToPtr("ghcr.io/open-webui/open-webui:main"),
Type: appconfig.InstallerTypeDocker,
BinName: strPtr("open-webui"),
BinName: lo.ToPtr("open-webui"),
Opts: &map[string]any{
"flags": "-p 3300:8080 -v open-webui:/data",
},

View File

@@ -22,19 +22,14 @@ func FilterInstaller(installer IInstaller, filters []string) bool {
return filter[1:], filter[0] == '!'
})
keep := len(positives) == 0
keep := len(positives) == 0 || lo.SomeBy(positives, func(f string) bool {
return isFilteredIn(installer, f)
})
for _, f := range positives {
if isFilteredIn(installer, f) {
keep = true
break
}
}
for _, f := range negatives {
if isFilteredIn(installer, f) {
keep = false
break
}
if keep && lo.SomeBy(negatives, func(f string) bool {
return isFilteredIn(installer, f)
}) {
return false
}
return keep

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -18,7 +19,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "No filters",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{},
expected: true,
@@ -26,7 +27,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Positive filter match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"test"},
expected: true,
@@ -34,7 +35,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Positive filter no match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"example"},
expected: false,
@@ -42,7 +43,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Negative filter match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"!test"},
expected: false,
@@ -50,7 +51,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Negative filter no match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"!example"},
expected: true,
@@ -58,7 +59,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Tag filter match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"tag:tag1"},
expected: true,
@@ -66,7 +67,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Tag filter no match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1")},
},
filters: []string{"tag:tag2"},
expected: false,
@@ -74,7 +75,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Type filter match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
},
filters: []string{"type:brew"},
expected: true,
@@ -82,7 +83,7 @@ func TestFilterInstaller(t *testing.T) {
{
name: "Type filter no match",
installer: &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
},
filters: []string{"type:npm"},
expected: false,
@@ -115,28 +116,28 @@ func TestInstallerIsEnabled(t *testing.T) {
{
name: "Enabled is true",
installer: &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("true")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("true")},
},
expected: true,
},
{
name: "Enabled is false",
installer: &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("false")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("false")},
},
expected: false,
},
{
name: "Enabled is a command that succeeds",
installer: &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("exit 0")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("exit 0")},
},
expected: true,
},
{
name: "Enabled is a command that fails",
installer: &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("exit 1")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("exit 1")},
},
expected: false,
expectErr: false,
@@ -161,7 +162,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple tags - one matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1 tag2 tag3")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1 tag2 tag3")},
}
result := FilterInstaller(installer, []string{"tag:tag2"})
assert.True(t, result)
@@ -169,7 +170,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple tags - none match", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("tag1 tag2 tag3")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("tag1 tag2 tag3")},
}
result := FilterInstaller(installer, []string{"tag:tag4"})
assert.False(t, result)
@@ -177,7 +178,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Mixed positive and negative filters - positive matches, negative matches too", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("dev prod")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("dev prod")},
}
// Should be excluded because negative filter matches
result := FilterInstaller(installer, []string{"tag:dev", "!tag:prod"})
@@ -186,7 +187,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Mixed positive and negative filters - only positive matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("dev")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("dev")},
}
result := FilterInstaller(installer, []string{"tag:dev", "!tag:prod"})
assert.True(t, result)
@@ -194,7 +195,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Name substring match", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("my-awesome-tool"), Tags: strPtr("")},
data: &appconfig.InstallerData{Name: lo.ToPtr("my-awesome-tool"), Tags: lo.ToPtr("")},
}
result := FilterInstaller(installer, []string{"awesome"})
assert.True(t, result)
@@ -202,7 +203,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Name exact match", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("vim"), Tags: strPtr("")},
data: &appconfig.InstallerData{Name: lo.ToPtr("vim"), Tags: lo.ToPtr("")},
}
result := FilterInstaller(installer, []string{"vim"})
assert.True(t, result)
@@ -210,7 +211,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Type filter case insensitive", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
}
result := FilterInstaller(installer, []string{"type:BREW"})
assert.True(t, result)
@@ -218,7 +219,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple positive filters - one matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("vim"), Tags: strPtr("editor")},
data: &appconfig.InstallerData{Name: lo.ToPtr("vim"), Tags: lo.ToPtr("editor")},
}
result := FilterInstaller(installer, []string{"neovim", "vim", "emacs"})
assert.True(t, result)
@@ -226,7 +227,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple positive filters - none match", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("vim"), Tags: strPtr("editor")},
data: &appconfig.InstallerData{Name: lo.ToPtr("vim"), Tags: lo.ToPtr("editor")},
}
result := FilterInstaller(installer, []string{"neovim", "emacs", "nano"})
assert.False(t, result)
@@ -234,7 +235,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple negative filters - all don't match", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("vim"), Tags: strPtr("editor")},
data: &appconfig.InstallerData{Name: lo.ToPtr("vim"), Tags: lo.ToPtr("editor")},
}
result := FilterInstaller(installer, []string{"!neovim", "!emacs"})
assert.True(t, result)
@@ -242,7 +243,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Multiple negative filters - one matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("vim"), Tags: strPtr("editor")},
data: &appconfig.InstallerData{Name: lo.ToPtr("vim"), Tags: lo.ToPtr("editor")},
}
result := FilterInstaller(installer, []string{"!neovim", "!vim"})
assert.False(t, result)
@@ -250,7 +251,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Negative type filter", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
}
result := FilterInstaller(installer, []string{"!type:npm"})
assert.True(t, result)
@@ -258,7 +259,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Negative type filter matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
}
result := FilterInstaller(installer, []string{"!type:brew"})
assert.False(t, result)
@@ -266,7 +267,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Negative tag filter", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("dev")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("dev")},
}
result := FilterInstaller(installer, []string{"!tag:prod"})
assert.True(t, result)
@@ -274,7 +275,7 @@ func TestFilterInstallerEdgeCases(t *testing.T) {
t.Run("Negative tag filter matches", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Tags: strPtr("dev")},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Tags: lo.ToPtr("dev")},
}
result := FilterInstaller(installer, []string{"!tag:dev"})
assert.False(t, result)
@@ -286,7 +287,7 @@ func TestInstallerIsEnabledEdgeCases(t *testing.T) {
t.Run("Enabled is TRUE (uppercase)", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("TRUE")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("TRUE")},
}
result, err := InstallerIsEnabled(installer)
assert.NoError(t, err)
@@ -295,7 +296,7 @@ func TestInstallerIsEnabledEdgeCases(t *testing.T) {
t.Run("Enabled is FALSE (uppercase)", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("FALSE")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("FALSE")},
}
result, err := InstallerIsEnabled(installer)
assert.NoError(t, err)
@@ -304,7 +305,7 @@ func TestInstallerIsEnabledEdgeCases(t *testing.T) {
t.Run("Enabled is True (mixed case)", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("True")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("True")},
}
result, err := InstallerIsEnabled(installer)
assert.NoError(t, err)
@@ -313,7 +314,7 @@ func TestInstallerIsEnabledEdgeCases(t *testing.T) {
t.Run("Enabled is a command that checks for which", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("which which")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("which which")},
}
result, err := InstallerIsEnabled(installer)
assert.NoError(t, err)
@@ -322,7 +323,7 @@ func TestInstallerIsEnabledEdgeCases(t *testing.T) {
t.Run("Enabled is a command that checks for nonexistent binary", func(t *testing.T) {
installer := &MockInstaller{
data: &appconfig.InstallerData{Enabled: strPtr("which nonexistent-binary-12345")},
data: &appconfig.InstallerData{Enabled: lo.ToPtr("which nonexistent-binary-12345")},
}
result, err := InstallerIsEnabled(installer)
assert.NoError(t, err)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestGitInstaller(data *appconfig.InstallerData) *GitInstaller {
@@ -21,7 +22,7 @@ func TestGitValidation(t *testing.T) {
// 🟢 Valid: Both destination and ref are present
validData := &appconfig.InstallerData{
Name: strPtr("test-git-valid"),
Name: lo.ToPtr("test-git-valid"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -32,7 +33,7 @@ func TestGitValidation(t *testing.T) {
// 🟢 Valid: Missing ref
missingRefData := &appconfig.InstallerData{
Name: strPtr("test-git-missing-ref"),
Name: lo.ToPtr("test-git-missing-ref"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -42,7 +43,7 @@ func TestGitValidation(t *testing.T) {
// 🔴 Invalid: Missing destination
missingDestData := &appconfig.InstallerData{
Name: strPtr("test-git-missing-destination"),
Name: lo.ToPtr("test-git-missing-destination"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"ref": "main",
@@ -56,7 +57,7 @@ func TestGitGetOpts(t *testing.T) {
// Test default opts (only destination set)
defaultData := &appconfig.InstallerData{
Name: strPtr("owner/repo"),
Name: lo.ToPtr("owner/repo"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -82,7 +83,7 @@ func TestGitGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("owner/repo"),
Name: lo.ToPtr("owner/repo"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -97,7 +98,7 @@ func TestGitGetOpts(t *testing.T) {
// Test with install_flags option (for git clone)
installFlagsData := &appconfig.InstallerData{
Name: strPtr("owner/repo"),
Name: lo.ToPtr("owner/repo"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -112,7 +113,7 @@ func TestGitGetOpts(t *testing.T) {
// Test with update_flags option (for git pull)
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("owner/repo"),
Name: lo.ToPtr("owner/repo"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
@@ -127,7 +128,7 @@ func TestGitGetOpts(t *testing.T) {
// Test with all options combined
allOptsData := &appconfig.InstallerData{
Name: strPtr("owner/repo"),
Name: lo.ToPtr("owner/repo"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",

View File

@@ -8,6 +8,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/chenasraf/sofmani/platform"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -25,7 +26,7 @@ func TestGitHubReleaseValidation(t *testing.T) {
// 🟢 Valid
validData := &appconfig.InstallerData{
Name: strPtr("ghr-valid"),
Name: lo.ToPtr("ghr-valid"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -38,7 +39,7 @@ func TestGitHubReleaseValidation(t *testing.T) {
// 🔴 Missing repository
missingRepo := &appconfig.InstallerData{
Name: strPtr("ghr-missing-repo"),
Name: lo.ToPtr("ghr-missing-repo"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": "/some/path",
@@ -49,7 +50,7 @@ func TestGitHubReleaseValidation(t *testing.T) {
// 🔴 Missing download_filename
missingDownloadFilename := &appconfig.InstallerData{
Name: strPtr("ghr-missing-download"),
Name: lo.ToPtr("ghr-missing-download"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -60,13 +61,13 @@ func TestGitHubReleaseValidation(t *testing.T) {
// 🔴 Empty per-platform download_filename
emptyPlatformFilename := &appconfig.InstallerData{
Name: strPtr("ghr-empty-platform-filename"),
Name: lo.ToPtr("ghr-empty-platform-filename"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
"destination": "/some/path",
"download_filename": map[string]*string{
string(platform.GetPlatform()): strPtr(""),
string(platform.GetPlatform()): lo.ToPtr(""),
},
},
}
@@ -74,7 +75,7 @@ func TestGitHubReleaseValidation(t *testing.T) {
// 🔴 Invalid strategy
invalidStrategy := &appconfig.InstallerData{
Name: strPtr("ghr-invalid-strategy"),
Name: lo.ToPtr("ghr-invalid-strategy"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -91,7 +92,7 @@ func TestGitHubReleaseGetOpts(t *testing.T) {
t.Run("parses all options correctly", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -110,7 +111,7 @@ func TestGitHubReleaseGetOpts(t *testing.T) {
t.Run("handles nil opts", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: nil,
}
@@ -124,7 +125,7 @@ func TestGitHubReleaseGetOpts(t *testing.T) {
t.Run("handles zip strategy", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"strategy": "zip",
@@ -138,7 +139,7 @@ func TestGitHubReleaseGetOpts(t *testing.T) {
t.Run("handles none strategy", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"strategy": "none",
@@ -157,7 +158,7 @@ func TestGitHubReleaseGetBinName(t *testing.T) {
t.Run("returns bin_name when set", func(t *testing.T) {
binName := "custom-bin"
data := &appconfig.InstallerData{
Name: strPtr("my-app"),
Name: lo.ToPtr("my-app"),
Type: appconfig.InstallerTypeGitHubRelease,
BinName: &binName,
}
@@ -167,7 +168,7 @@ func TestGitHubReleaseGetBinName(t *testing.T) {
t.Run("returns base name when bin_name not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("owner/my-app"),
Name: lo.ToPtr("owner/my-app"),
Type: appconfig.InstallerTypeGitHubRelease,
}
installer := newTestGitHubReleaseInstaller(data)
@@ -181,7 +182,7 @@ func TestGitHubReleaseGetArchiveBinName(t *testing.T) {
t.Run("returns archive_bin_name when set", func(t *testing.T) {
binName := "cospend"
data := &appconfig.InstallerData{
Name: strPtr("cospend-cli"),
Name: lo.ToPtr("cospend-cli"),
Type: appconfig.InstallerTypeGitHubRelease,
BinName: &binName,
Opts: &map[string]any{
@@ -196,7 +197,7 @@ func TestGitHubReleaseGetArchiveBinName(t *testing.T) {
t.Run("falls back to bin_name when archive_bin_name not set", func(t *testing.T) {
binName := "custom-bin"
data := &appconfig.InstallerData{
Name: strPtr("my-app"),
Name: lo.ToPtr("my-app"),
Type: appconfig.InstallerTypeGitHubRelease,
BinName: &binName,
}
@@ -206,7 +207,7 @@ func TestGitHubReleaseGetArchiveBinName(t *testing.T) {
t.Run("falls back to name when neither set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("my-app"),
Name: lo.ToPtr("my-app"),
Type: appconfig.InstallerTypeGitHubRelease,
}
installer := newTestGitHubReleaseInstaller(data)
@@ -219,7 +220,7 @@ func TestGitHubReleaseGetFilename(t *testing.T) {
t.Run("returns filename for current platform", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"download_filename": "app.tar.gz",
@@ -231,7 +232,7 @@ func TestGitHubReleaseGetFilename(t *testing.T) {
t.Run("returns empty string when not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{},
}
@@ -254,7 +255,7 @@ func TestGitHubReleaseCacheOperations(t *testing.T) {
t.Run("UpdateCache writes tag to file", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-cache-app"),
Name: lo.ToPtr("test-cache-app"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -276,7 +277,7 @@ func TestGitHubReleaseCacheOperations(t *testing.T) {
t.Run("GetCachedTag returns empty for non-existent cache", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("non-existent-app-12345"),
Name: lo.ToPtr("non-existent-app-12345"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -293,7 +294,7 @@ func TestGitHubReleaseCacheOperations(t *testing.T) {
t.Run("UpdateCache overwrites existing cache", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-overwrite-app"),
Name: lo.ToPtr("test-overwrite-app"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -323,7 +324,7 @@ func TestGitHubReleaseGetDestination(t *testing.T) {
t.Run("returns destination from opts", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": "/usr/local/bin",
@@ -335,7 +336,7 @@ func TestGitHubReleaseGetDestination(t *testing.T) {
t.Run("returns current directory when destination not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{},
}
@@ -350,7 +351,7 @@ func TestGitHubReleaseGetInstallDir(t *testing.T) {
t.Run("returns same as destination", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": "/opt/bin",
@@ -376,7 +377,7 @@ func TestGitHubReleaseCheckIsInstalled(t *testing.T) {
assert.NoError(t, err)
data := &appconfig.InstallerData{
Name: strPtr("myapp"),
Name: lo.ToPtr("myapp"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": tmpDir,
@@ -395,7 +396,7 @@ func TestGitHubReleaseCheckIsInstalled(t *testing.T) {
defer func() { _ = os.RemoveAll(tmpDir) }()
data := &appconfig.InstallerData{
Name: strPtr("nonexistent-app"),
Name: lo.ToPtr("nonexistent-app"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": tmpDir,
@@ -411,7 +412,7 @@ func TestGitHubReleaseCheckIsInstalled(t *testing.T) {
t.Run("uses custom check when provided", func(t *testing.T) {
checkCmd := "true"
data := &appconfig.InstallerData{
Name: strPtr("myapp"),
Name: lo.ToPtr("myapp"),
Type: appconfig.InstallerTypeGitHubRelease,
CheckInstalled: &checkCmd,
}
@@ -429,7 +430,7 @@ func TestGitHubReleaseCheckNeedsUpdate(t *testing.T) {
t.Run("uses custom check when provided", func(t *testing.T) {
checkCmd := "true" // returns success = update needed
data := &appconfig.InstallerData{
Name: strPtr("myapp"),
Name: lo.ToPtr("myapp"),
Type: appconfig.InstallerTypeGitHubRelease,
CheckHasUpdate: &checkCmd,
}
@@ -443,7 +444,7 @@ func TestGitHubReleaseCheckNeedsUpdate(t *testing.T) {
t.Run("returns true when no cached tag", func(t *testing.T) {
// Use a unique name that won't have a cache file
data := &appconfig.InstallerData{
Name: strPtr("unique-no-cache-app-99999"),
Name: lo.ToPtr("unique-no-cache-app-99999"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
@@ -465,7 +466,7 @@ func TestNewGitHubReleaseInstaller(t *testing.T) {
t.Run("creates installer with config and data", func(t *testing.T) {
cfg := &appconfig.AppConfig{}
data := &appconfig.InstallerData{
Name: strPtr("test-release"),
Name: lo.ToPtr("test-release"),
Type: appconfig.InstallerTypeGitHubRelease,
}
installer := NewGitHubReleaseInstaller(cfg, data)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -23,11 +24,11 @@ func TestGroupValidation(t *testing.T) {
// 🟢 Valid: one sub-installer
validStep := appconfig.InstallerData{
Name: strPtr("child-installer"),
Name: lo.ToPtr("child-installer"),
Type: appconfig.InstallerTypeBrew,
}
validData := &appconfig.InstallerData{
Name: strPtr("group-valid"),
Name: lo.ToPtr("group-valid"),
Type: appconfig.InstallerTypeGroup,
Steps: &[]appconfig.InstallerData{validStep},
}
@@ -35,7 +36,7 @@ func TestGroupValidation(t *testing.T) {
// 🔴 Invalid: empty steps slice
emptySteps := &appconfig.InstallerData{
Name: strPtr("group-empty"),
Name: lo.ToPtr("group-empty"),
Type: appconfig.InstallerTypeGroup,
Steps: &[]appconfig.InstallerData{},
}
@@ -43,7 +44,7 @@ func TestGroupValidation(t *testing.T) {
// 🔴 Invalid: nil steps
nilSteps := &appconfig.InstallerData{
Name: strPtr("group-nil"),
Name: lo.ToPtr("group-nil"),
Type: appconfig.InstallerTypeGroup,
Steps: nil,
}
@@ -55,7 +56,7 @@ func TestGroupGetData(t *testing.T) {
t.Run("returns the installer data", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
}
installer := newTestGroupInstaller(data)
@@ -71,7 +72,7 @@ func TestGroupGetOpts(t *testing.T) {
t.Run("returns empty opts", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
}
installer := newTestGroupInstaller(data)
@@ -86,7 +87,7 @@ func TestGroupGetBinName(t *testing.T) {
t.Run("returns name when bin_name is not set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("my-group"),
Name: lo.ToPtr("my-group"),
Type: appconfig.InstallerTypeGroup,
}
installer := newTestGroupInstaller(data)
@@ -96,7 +97,7 @@ func TestGroupGetBinName(t *testing.T) {
t.Run("returns bin_name when set", func(t *testing.T) {
binName := "custom-bin"
data := &appconfig.InstallerData{
Name: strPtr("my-group"),
Name: lo.ToPtr("my-group"),
Type: appconfig.InstallerTypeGroup,
BinName: &binName,
}
@@ -107,7 +108,7 @@ func TestGroupGetBinName(t *testing.T) {
t.Run("returns name when bin_name is empty", func(t *testing.T) {
binName := ""
data := &appconfig.InstallerData{
Name: strPtr("my-group"),
Name: lo.ToPtr("my-group"),
Type: appconfig.InstallerTypeGroup,
BinName: &binName,
}
@@ -122,7 +123,7 @@ func TestGroupCheckIsInstalled(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "true"
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
CheckInstalled: &checkCmd,
}
@@ -136,7 +137,7 @@ func TestGroupCheckIsInstalled(t *testing.T) {
t.Run("runs custom check that fails", func(t *testing.T) {
checkCmd := "false"
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
CheckInstalled: &checkCmd,
}
@@ -153,7 +154,7 @@ func TestGroupCheckNeedsUpdate(t *testing.T) {
t.Run("returns true when no custom check", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
}
installer := newTestGroupInstaller(data)
@@ -166,7 +167,7 @@ func TestGroupCheckNeedsUpdate(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "false" // Returns exit code 1, meaning no update
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
CheckHasUpdate: &checkCmd,
}
@@ -184,7 +185,7 @@ func TestNewGroupInstaller(t *testing.T) {
t.Run("creates installer with config and data", func(t *testing.T) {
cfg := &appconfig.AppConfig{}
data := &appconfig.InstallerData{
Name: strPtr("group-test"),
Name: lo.ToPtr("group-test"),
Type: appconfig.InstallerTypeGroup,
}
installer := NewGroupInstaller(cfg, data)
@@ -200,15 +201,15 @@ func TestGroupValidationWithMultipleSteps(t *testing.T) {
t.Run("valid with multiple steps", func(t *testing.T) {
step1 := appconfig.InstallerData{
Name: strPtr("step1"),
Name: lo.ToPtr("step1"),
Type: appconfig.InstallerTypeBrew,
}
step2 := appconfig.InstallerData{
Name: strPtr("step2"),
Name: lo.ToPtr("step2"),
Type: appconfig.InstallerTypeShell,
}
data := &appconfig.InstallerData{
Name: strPtr("group-multi"),
Name: lo.ToPtr("group-multi"),
Type: appconfig.InstallerTypeGroup,
Steps: &[]appconfig.InstallerData{step1, step2},
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -32,7 +33,7 @@ func TestInstallerWithDefaults(t *testing.T) {
func TestRunInstaller(t *testing.T) {
config := &appconfig.AppConfig{}
mockInstaller := &MockInstaller{
data: &appconfig.InstallerData{Name: strPtr("test"), Type: appconfig.InstallerTypeBrew},
data: &appconfig.InstallerData{Name: lo.ToPtr("test"), Type: appconfig.InstallerTypeBrew},
isInstalled: false,
}
result, err := RunInstaller(config, mockInstaller)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -23,7 +24,7 @@ func TestManifestValidation(t *testing.T) {
// 🟢 Valid
validData := &appconfig.InstallerData{
Name: strPtr("manifest-valid"),
Name: lo.ToPtr("manifest-valid"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
@@ -35,7 +36,7 @@ func TestManifestValidation(t *testing.T) {
// 🔴 Missing source
missingSource := &appconfig.InstallerData{
Name: strPtr("manifest-missing-source"),
Name: lo.ToPtr("manifest-missing-source"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"path": "some/path",
@@ -45,7 +46,7 @@ func TestManifestValidation(t *testing.T) {
// 🔴 Missing path
missingPath := &appconfig.InstallerData{
Name: strPtr("manifest-missing-path"),
Name: lo.ToPtr("manifest-missing-path"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
@@ -55,7 +56,7 @@ func TestManifestValidation(t *testing.T) {
// 🔴 Empty ref (not nil, just empty)
emptyRef := &appconfig.InstallerData{
Name: strPtr("manifest-empty-ref"),
Name: lo.ToPtr("manifest-empty-ref"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
@@ -67,7 +68,7 @@ func TestManifestValidation(t *testing.T) {
// 🔴 Nil opts
nilOpts := &appconfig.InstallerData{
Name: strPtr("manifest-nil-opts"),
Name: lo.ToPtr("manifest-nil-opts"),
Type: appconfig.InstallerTypeManifest,
Opts: nil,
}
@@ -79,7 +80,7 @@ func TestManifestGetOpts(t *testing.T) {
t.Run("returns all opts when set", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://github.com/user/repo.git",
@@ -100,7 +101,7 @@ func TestManifestGetOpts(t *testing.T) {
t.Run("returns nil fields when opts is nil", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
Opts: nil,
}
@@ -114,7 +115,7 @@ func TestManifestGetOpts(t *testing.T) {
t.Run("handles partial opts", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://github.com/user/repo.git",
@@ -131,7 +132,7 @@ func TestManifestGetOpts(t *testing.T) {
t.Run("handles wrong type values gracefully", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": 123, // Wrong type
@@ -154,7 +155,7 @@ func TestManifestGetData(t *testing.T) {
t.Run("returns the installer data", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
}
installer := newTestManifestInstaller(data)
@@ -170,7 +171,7 @@ func TestManifestCheckIsInstalled(t *testing.T) {
t.Run("returns false when no custom check", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
}
installer := newTestManifestInstaller(data)
@@ -183,7 +184,7 @@ func TestManifestCheckIsInstalled(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "true"
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
CheckInstalled: &checkCmd,
}
@@ -200,7 +201,7 @@ func TestManifestCheckNeedsUpdate(t *testing.T) {
t.Run("returns true when no custom check", func(t *testing.T) {
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
}
installer := newTestManifestInstaller(data)
@@ -213,7 +214,7 @@ func TestManifestCheckNeedsUpdate(t *testing.T) {
t.Run("runs custom check when provided", func(t *testing.T) {
checkCmd := "false" // Returns exit code 1, meaning no update
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
CheckHasUpdate: &checkCmd,
}
@@ -231,7 +232,7 @@ func TestNewManifestInstaller(t *testing.T) {
t.Run("creates installer with config and data", func(t *testing.T) {
cfg := &appconfig.AppConfig{}
data := &appconfig.InstallerData{
Name: strPtr("manifest-test"),
Name: lo.ToPtr("manifest-test"),
Type: appconfig.InstallerTypeManifest,
}
installer := NewManifestInstaller(cfg, data)

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestNpmInstaller(data *appconfig.InstallerData) *NpmInstaller {
@@ -23,7 +24,7 @@ func TestNpmValidation(t *testing.T) {
// 🟢 Valid npm installer
validData := &appconfig.InstallerData{
Name: strPtr("some-npm-package"),
Name: lo.ToPtr("some-npm-package"),
Type: appconfig.InstallerTypeNpm,
}
assertNoValidationErrors(t, newTestNpmInstaller(validData).Validate())
@@ -41,7 +42,7 @@ func TestNpmGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("prettier"),
Name: lo.ToPtr("prettier"),
Type: appconfig.InstallerTypeNpm,
}
installer := newTestNpmInstaller(defaultData)
@@ -58,7 +59,7 @@ func TestNpmGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("prettier"),
Name: lo.ToPtr("prettier"),
Type: appconfig.InstallerTypeNpm,
Opts: &map[string]any{
"flags": "--legacy-peer-deps",
@@ -72,7 +73,7 @@ func TestNpmGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("prettier"),
Name: lo.ToPtr("prettier"),
Type: appconfig.InstallerTypeNpm,
Opts: &map[string]any{
"install_flags": "--save-exact",
@@ -86,7 +87,7 @@ func TestNpmGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("prettier"),
Name: lo.ToPtr("prettier"),
Type: appconfig.InstallerTypeNpm,
Opts: &map[string]any{
"update_flags": "--force",
@@ -100,7 +101,7 @@ func TestNpmGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("prettier"),
Name: lo.ToPtr("prettier"),
Type: appconfig.InstallerTypeNpm,
Opts: &map[string]any{
"flags": "--common",

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestPacmanInstaller(data *appconfig.InstallerData) *PacmanInstaller {
@@ -34,14 +35,14 @@ func TestPacmanValidation(t *testing.T) {
// Valid pacman installer
validPacmanData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
}
assertNoValidationErrors(t, newTestPacmanInstaller(validPacmanData).Validate())
// Valid yay installer
validYayData := &appconfig.InstallerData{
Name: strPtr("visual-studio-code-bin"),
Name: lo.ToPtr("visual-studio-code-bin"),
Type: appconfig.InstallerTypeYay,
}
assertNoValidationErrors(t, newTestYayInstaller(validYayData).Validate())
@@ -59,7 +60,7 @@ func TestPacmanGetBinName(t *testing.T) {
// Test default bin name (uses package name)
defaultBinData := &appconfig.InstallerData{
Name: strPtr("neovim"),
Name: lo.ToPtr("neovim"),
Type: appconfig.InstallerTypePacman,
}
installer := newTestPacmanInstaller(defaultBinData)
@@ -70,7 +71,7 @@ func TestPacmanGetBinName(t *testing.T) {
// Test custom bin name
customBinName := "nvim"
customBinData := &appconfig.InstallerData{
Name: strPtr("neovim"),
Name: lo.ToPtr("neovim"),
Type: appconfig.InstallerTypePacman,
BinName: &customBinName,
}
@@ -85,7 +86,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
}
installer := newTestPacmanInstaller(defaultData)
@@ -105,7 +106,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with needed option set to true
neededData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"needed": true,
@@ -119,7 +120,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with needed option set to false
notNeededData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"needed": false,
@@ -133,7 +134,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"flags": "--asdeps --overwrite '*'",
@@ -147,7 +148,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"install_flags": "--asdeps",
@@ -161,7 +162,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"update_flags": "--ignore vim",
@@ -175,7 +176,7 @@ func TestPacmanGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("vim"),
Name: lo.ToPtr("vim"),
Type: appconfig.InstallerTypePacman,
Opts: &map[string]any{
"flags": "--common",

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestPipxInstaller(data *appconfig.InstallerData) *PipxInstaller {
@@ -22,7 +23,7 @@ func TestPipxValidation(t *testing.T) {
// 🟢 Valid pipx installer
validData := &appconfig.InstallerData{
Name: strPtr("some-pipx-package"),
Name: lo.ToPtr("some-pipx-package"),
Type: appconfig.InstallerTypePipx,
}
assertNoValidationErrors(t, newTestPipxInstaller(validData).Validate())
@@ -40,7 +41,7 @@ func TestPipxGetOpts(t *testing.T) {
// Test default opts (no options set)
defaultData := &appconfig.InstallerData{
Name: strPtr("black"),
Name: lo.ToPtr("black"),
Type: appconfig.InstallerTypePipx,
}
installer := newTestPipxInstaller(defaultData)
@@ -57,7 +58,7 @@ func TestPipxGetOpts(t *testing.T) {
// Test with flags option
flagsData := &appconfig.InstallerData{
Name: strPtr("black"),
Name: lo.ToPtr("black"),
Type: appconfig.InstallerTypePipx,
Opts: &map[string]any{
"flags": "--verbose",
@@ -71,7 +72,7 @@ func TestPipxGetOpts(t *testing.T) {
// Test with install_flags option
installFlagsData := &appconfig.InstallerData{
Name: strPtr("black"),
Name: lo.ToPtr("black"),
Type: appconfig.InstallerTypePipx,
Opts: &map[string]any{
"install_flags": "--python python3.11",
@@ -85,7 +86,7 @@ func TestPipxGetOpts(t *testing.T) {
// Test with update_flags option
updateFlagsData := &appconfig.InstallerData{
Name: strPtr("black"),
Name: lo.ToPtr("black"),
Type: appconfig.InstallerTypePipx,
Opts: &map[string]any{
"update_flags": "--force",
@@ -99,7 +100,7 @@ func TestPipxGetOpts(t *testing.T) {
// Test with all flags options combined
allFlagsData := &appconfig.InstallerData{
Name: strPtr("black"),
Name: lo.ToPtr("black"),
Type: appconfig.InstallerTypePipx,
Opts: &map[string]any{
"flags": "--common",

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestRsyncInstaller(data *appconfig.InstallerData) *RsyncInstaller {
@@ -22,7 +23,7 @@ func TestRsyncValidation(t *testing.T) {
// 🟢 Valid rsync config
validData := &appconfig.InstallerData{
Name: strPtr("rsync-valid"),
Name: lo.ToPtr("rsync-valid"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",
@@ -34,7 +35,7 @@ func TestRsyncValidation(t *testing.T) {
// 🔴 Missing source
missingSource := &appconfig.InstallerData{
Name: strPtr("rsync-missing-source"),
Name: lo.ToPtr("rsync-missing-source"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"destination": "/path/to",
@@ -44,7 +45,7 @@ func TestRsyncValidation(t *testing.T) {
// 🔴 Missing destination
missingDest := &appconfig.InstallerData{
Name: strPtr("rsync-missing-destination"),
Name: lo.ToPtr("rsync-missing-destination"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",
@@ -55,7 +56,7 @@ func TestRsyncValidation(t *testing.T) {
// 🔴 Empty flags string
emptyFlags := &appconfig.InstallerData{
Name: strPtr("rsync-empty-flags"),
Name: lo.ToPtr("rsync-empty-flags"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",

View File

@@ -5,6 +5,7 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
func newTestShellInstaller(data *appconfig.InstallerData) *ShellInstaller {
@@ -20,7 +21,7 @@ func TestShellValidation(t *testing.T) {
// 🟢 Valid shell config
validData := &appconfig.InstallerData{
Name: strPtr("shell-valid"),
Name: lo.ToPtr("shell-valid"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"command": "echo install",
@@ -31,7 +32,7 @@ func TestShellValidation(t *testing.T) {
// 🔴 Missing command
missingCommand := &appconfig.InstallerData{
Name: strPtr("shell-missing-command"),
Name: lo.ToPtr("shell-missing-command"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"update_command": "echo update",
@@ -41,7 +42,7 @@ func TestShellValidation(t *testing.T) {
// 🟢 Valid - missing update_command
missingUpdate := &appconfig.InstallerData{
Name: strPtr("shell-missing-update"),
Name: lo.ToPtr("shell-missing-update"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"command": "echo install",
@@ -51,7 +52,7 @@ func TestShellValidation(t *testing.T) {
// 🔴 Missing both
missingBoth := &appconfig.InstallerData{
Name: strPtr("shell-missing-both"),
Name: lo.ToPtr("shell-missing-both"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{},
}

View File

@@ -70,11 +70,6 @@ func (m *MockInstaller) GetTemplateVars() *TemplateVars {
return m.templateVars
}
// strPtr returns a pointer to the given string.
func strPtr(s string) *string {
return &s
}
// simulateBrewCheck simulates parsing output from `brew outdated --json`
// along with handling the exit code semantics.

View File

@@ -1,5 +1,7 @@
package machine
import "github.com/samber/lo"
// Machines defines which machines a configuration applies to.
type Machines struct {
// Only specifies a list of machine IDs or aliases where the configuration should apply.
@@ -28,20 +30,12 @@ func (m *Machines) GetShouldRunOnMachine(machineID string, aliases map[string]st
// containsMachineID checks if the machine ID is in the list, resolving aliases first.
func containsMachineID(list []string, machineID string, aliases map[string]string) bool {
for _, entry := range list {
// First, try to resolve as an alias
return lo.SomeBy(list, func(entry string) bool {
if aliases != nil {
if resolvedID, ok := aliases[entry]; ok {
if resolvedID == machineID {
return true
}
continue
return resolvedID == machineID
}
}
// Fall back to treating entry as a literal machine ID
if entry == machineID {
return true
}
}
return false
return entry == machineID
})
}

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"runtime"
"slices"
"github.com/samber/lo"
)
var osValue string = runtime.GOOS // osValue stores the current operating system.
@@ -179,16 +181,11 @@ func (p *Platforms) GetShouldRunOnOS(curOS Platform) bool {
return true
}
// strPtr returns a pointer to a string.
func strPtr(s string) *string {
return &s
}
// DockerOSMap is a PlatformMap that defines the Docker OS for each platform.
var DockerOSMap = PlatformMap[string]{
MacOS: strPtr("linux"),
Linux: strPtr("linux"),
Windows: strPtr("windows"),
MacOS: lo.ToPtr("linux"),
Linux: lo.ToPtr("linux"),
Windows: lo.ToPtr("windows"),
}
// ParsePlatformSingleValue creates a new PlatformMap with the value for all platforms

View File

@@ -4,6 +4,7 @@ import (
"runtime"
"testing"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
)
@@ -171,16 +172,16 @@ func TestPlatformMapResolve(t *testing.T) {
t.Run("returns nil when platform value not set", func(t *testing.T) {
SetOS("darwin")
pm := &PlatformMap[string]{Linux: strPtr("linux-only")}
pm := &PlatformMap[string]{Linux: lo.ToPtr("linux-only")}
assert.Nil(t, pm.Resolve())
})
t.Run("returns nil for unknown OS", func(t *testing.T) {
SetOS("freebsd")
pm := &PlatformMap[string]{
MacOS: strPtr("mac"),
Linux: strPtr("linux"),
Windows: strPtr("windows"),
MacOS: lo.ToPtr("mac"),
Linux: lo.ToPtr("linux"),
Windows: lo.ToPtr("windows"),
}
assert.Nil(t, pm.Resolve())
})
@@ -192,15 +193,15 @@ func TestPlatformMapResolveWithFallback(t *testing.T) {
t.Run("returns primary value when set", func(t *testing.T) {
SetOS("darwin")
primary := &PlatformMap[string]{MacOS: strPtr("primary")}
fallback := PlatformMap[string]{MacOS: strPtr("fallback")}
primary := &PlatformMap[string]{MacOS: lo.ToPtr("primary")}
fallback := PlatformMap[string]{MacOS: lo.ToPtr("fallback")}
assert.Equal(t, "primary", primary.ResolveWithFallback(fallback))
})
t.Run("returns fallback value when primary not set", func(t *testing.T) {
SetOS("darwin")
primary := &PlatformMap[string]{Linux: strPtr("linux-only")}
fallback := PlatformMap[string]{MacOS: strPtr("fallback")}
primary := &PlatformMap[string]{Linux: lo.ToPtr("linux-only")}
fallback := PlatformMap[string]{MacOS: lo.ToPtr("fallback")}
assert.Equal(t, "fallback", primary.ResolveWithFallback(fallback))
})
}

View File

@@ -4,6 +4,7 @@ import (
"strings"
"github.com/chenasraf/sofmani/logger"
"github.com/samber/lo"
)
// Action represents the action taken for an installer.
@@ -85,12 +86,9 @@ func (s *Summary) Print() {
// collectByAction returns all results (including nested) that match the given action.
func (s *Summary) collectByAction(action Action) []InstallResult {
var results []InstallResult
for _, r := range s.results {
collected := collectResultsByAction(r, action)
results = append(results, collected...)
}
return results
return lo.FlatMap(s.results, func(r InstallResult, _ int) []InstallResult {
return collectResultsByAction(r, action)
})
}
// isContainerType returns true if the installer type is a container (group/manifest).
@@ -179,19 +177,12 @@ func filterChildrenByAction(children []InstallResult, action Action) []InstallRe
// hasChildrenWithAction checks if any children (recursively) match the action.
func hasChildrenWithAction(children []InstallResult, action Action) bool {
for _, child := range children {
// Skip children that should be excluded from summary
return lo.SomeBy(children, func(child InstallResult) bool {
if shouldSkipSummary(child, action) {
continue
return false
}
if child.Action == action {
return true
}
if hasChildrenWithAction(child.Children, action) {
return true
}
}
return false
return child.Action == action || hasChildrenWithAction(child.Children, action)
})
}
// printResult prints a single result with the given indentation level.

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"maps"
"strings"
"github.com/samber/lo"
)
// ResolveEnvPaths takes one or more slices of environment variable strings (e.g., "KEY=VALUE"),
@@ -62,11 +64,9 @@ func EnvSliceAsMap(env []string) map[string]string {
// EnvMapAsSlice converts a map of environment variables to a slice of "KEY=VALUE" strings.
func EnvMapAsSlice(env map[string]string) []string {
out := []string{}
for k, v := range env {
out = append(out, fmt.Sprintf("%s=%s", k, v))
}
return out
return lo.MapToSlice(env, func(k string, v string) string {
return fmt.Sprintf("%s=%s", k, v)
})
}
// mergeEnvs helper function to merge a source slice of env strings into a target map (represented as a slice).