feat: validations

This commit is contained in:
2025-06-19 11:19:10 +03:00
parent 35501d232d
commit 70357d1436
15 changed files with 711 additions and 35 deletions

View File

@@ -21,6 +21,11 @@ const (
PackageManagerApt PackageManager = "apt"
)
func (i *AptInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
return errors
}
// Install implements IInstaller.
func (i *AptInstaller) Install() error {
name := *i.Info.Name

View File

@@ -1,6 +1,8 @@
package installer
import (
"strings"
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/utils"
)
@@ -15,6 +17,18 @@ type BrewOpts struct {
Tap *string
}
func (i *BrewInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Tap != nil {
if !strings.Contains(*opts.Tap, "/") || len(*opts.Tap) < 3 {
errors = append(errors, ValidationError{FieldName: "tap", Message: validationInvalidFormat(), InstallerName: *info.Name})
}
}
return errors
}
// Install implements IInstaller.
func (i *BrewInstaller) Install() error {
name := *i.Info.Name

View File

@@ -21,6 +21,19 @@ type GitOpts struct {
Ref *string
}
func (i *GitInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Destination == nil || len(*opts.Destination) == 0 {
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Ref == nil || len(*opts.Ref) == 0 {
errors = append(errors, ValidationError{FieldName: "ref", Message: validationIsRequired(), InstallerName: *info.Name})
}
return errors
}
// Install implements IInstaller.
func (i *GitInstaller) Install() error {
args := []string{"clone", i.GetRepositoryUrl(), i.GetInstallDir()}

View File

@@ -19,7 +19,7 @@ import (
type GitHubReleaseInstaller struct {
InstallerBase
Config *appconfig.AppConfig
Data *appconfig.InstallerData
Info *appconfig.InstallerData
}
type GitHubReleaseOpts struct {
@@ -37,6 +37,31 @@ const (
GitHubReleaseInstallStrategyZip GitHubReleaseInstallStrategy = "zip"
)
func (i *GitHubReleaseInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Repository == nil || len(*opts.Repository) == 0 {
errors = append(errors, ValidationError{FieldName: "repository", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Destination == nil || len(*opts.Destination) == 0 {
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.DownloadFilename == nil || len(*opts.DownloadFilename.Resolve()) == 0 {
errors = append(errors, ValidationError{FieldName: "download_filename", Message: validationIsRequired(), InstallerName: *info.Name})
} else {
if (*opts.DownloadFilename).Resolve() == nil || len(*(*opts.DownloadFilename).Resolve()) == 0 {
errors = append(errors, ValidationError{FieldName: fmt.Sprintf("download_filename.%s", platform.GetPlatform()), Message: validationIsRequired(), InstallerName: *info.Name})
}
}
if opts.Strategy != nil {
if *opts.Strategy != GitHubReleaseInstallStrategyNone && *opts.Strategy != GitHubReleaseInstallStrategyTar && *opts.Strategy != GitHubReleaseInstallStrategyZip {
errors = append(errors, ValidationError{FieldName: "strategy", Message: validationInvalidFormat(), InstallerName: *info.Name})
}
}
return errors
}
// Install implements IInstaller.
func (i *GitHubReleaseInstaller) Install() error {
opts := i.GetOpts()
@@ -60,10 +85,7 @@ func (i *GitHubReleaseInstaller) Install() error {
return err
}
version := tag
if strings.HasPrefix(tag, "v") {
version = strings.TrimPrefix(tag, "v")
}
version, _ := strings.CutPrefix(tag, "v")
filename := i.GetFilename()
filename = strings.ReplaceAll(filename, "{tag}", tag)
filename = strings.ReplaceAll(filename, "{version}", version)
@@ -152,8 +174,8 @@ func (i *GitHubReleaseInstaller) CheckIsInstalled() (bool, error) {
if i.HasCustomInstallCheck() {
return i.RunCustomInstallCheck()
}
logger.Debug("Checking if %s is installed on %s", *i.Data.Name, filepath.Join(i.GetInstallDir(), *i.Data.Name))
return utils.PathExists(filepath.Join(i.GetInstallDir(), *i.Data.Name))
logger.Debug("Checking if %s is installed on %s", *i.Info.Name, filepath.Join(i.GetInstallDir(), *i.Info.Name))
return utils.PathExists(filepath.Join(i.GetInstallDir(), *i.Info.Name))
}
// CheckNeedsUpdate implements IInstaller.
@@ -179,10 +201,10 @@ func (i *GitHubReleaseInstaller) CheckNeedsUpdate() (bool, error) {
}
func (i *GitHubReleaseInstaller) GetBinName() string {
if i.Data.BinName != nil {
return *i.Data.BinName
if i.Info.BinName != nil {
return *i.Info.BinName
}
return filepath.Base(*i.Data.Name)
return filepath.Base(*i.Info.Name)
}
func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string) (bool, error) {
@@ -205,12 +227,12 @@ func (i *GitHubReleaseInstaller) CopyExtractedFile(out *os.File, tmpDir string)
}
func (i *GitHubReleaseInstaller) GetCachedTag() (string, error) {
logger.Debug("Getting cached tag for %s", *i.Data.Name)
logger.Debug("Getting cached tag for %s", *i.Info.Name)
cacheDir, err := utils.GetCacheDir()
if err != nil {
return "", err
}
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Data.Name)
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Info.Name)
exists, err := utils.PathExists(cacheFile)
if err != nil {
return "", err
@@ -223,7 +245,7 @@ func (i *GitHubReleaseInstaller) GetCachedTag() (string, error) {
if err != nil {
return "", err
}
logger.Debug("Got cached tag %s for %s", strings.TrimSpace(string(contents)), *i.Data.Name)
logger.Debug("Got cached tag %s for %s", strings.TrimSpace(string(contents)), *i.Info.Name)
return strings.TrimSpace(string(contents)), nil
}
@@ -232,7 +254,7 @@ func (i *GitHubReleaseInstaller) UpdateCache(tag string) error {
if err != nil {
return err
}
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Data.Name)
cacheFile := fmt.Sprintf("%s/%s", cacheDir, *i.Info.Name)
logger.Debug("Updating cache file %s with %s", cacheFile, tag)
err = os.WriteFile(cacheFile, []byte(tag), 0644)
if err != nil {
@@ -243,12 +265,12 @@ func (i *GitHubReleaseInstaller) UpdateCache(tag string) error {
// GetData implements IInstaller.
func (i *GitHubReleaseInstaller) GetData() *appconfig.InstallerData {
return i.Data
return i.Info
}
func (i *GitHubReleaseInstaller) GetOpts() *GitHubReleaseOpts {
opts := &GitHubReleaseOpts{}
info := i.Data
info := i.Info
if info.Opts != nil {
if repository, ok := (*info.Opts)["repository"].(string); ok {
repository = utils.GetRealPath(i.GetData().Environ(), repository)
@@ -328,7 +350,7 @@ func NewGitHubReleaseInstaller(cfg *appconfig.AppConfig, installer *appconfig.In
i := &GitHubReleaseInstaller{
InstallerBase: InstallerBase{Data: installer},
Config: cfg,
Data: installer,
Info: installer,
}
return i

View File

@@ -16,6 +16,15 @@ type GroupOpts struct {
//
}
func (i *GroupInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
if info.Steps == nil || len(*info.Steps) == 0 {
errors = append(errors, ValidationError{FieldName: "steps", Message: "Must have at least one step", InstallerName: *info.Name})
}
return errors
}
// Install implements IInstaller.
func (i *GroupInstaller) Install() error {
info := i.GetData()

View File

@@ -15,6 +15,7 @@ type IInstaller interface {
CheckNeedsUpdate() (bool, error)
Install() error
Update() error
Validate() []ValidationError
}
type InstallerBase struct {
@@ -52,6 +53,15 @@ func (i *InstallerBase) GetData() *appconfig.InstallerData {
return i.Data
}
func (i *InstallerBase) BaseValidate() []ValidationError {
errors := []ValidationError{}
info := i.GetData()
if info.Name == nil || len(*info.Name) == 0 {
errors = append(errors, ValidationError{FieldName: "name", Message: "Name is required"})
}
return errors
}
func (i *InstallerBase) RunCustomUpdateCheck() (bool, error) {
envShell := utils.GetOSShell(i.GetData().EnvShell)
args := utils.GetOSShellArgs(*i.GetData().CheckHasUpdate)

View File

@@ -5,17 +5,19 @@ import (
"github.com/chenasraf/sofmani/appconfig"
"github.com/chenasraf/sofmani/logger"
"github.com/chenasraf/sofmani/platform"
"github.com/stretchr/testify/assert"
)
type MockInstaller struct {
data *appconfig.InstallerData
isInstalled bool
needsUpdate bool
installError error
updateError error
checkInstall error
checkUpdate error
data *appconfig.InstallerData
isInstalled bool
needsUpdate bool
installError error
updateError error
checkInstall error
checkUpdate error
validationErrors []ValidationError
}
func (m *MockInstaller) GetData() *appconfig.InstallerData {
@@ -38,6 +40,10 @@ func (m *MockInstaller) Update() error {
return m.updateError
}
func (m *MockInstaller) Validate() []ValidationError {
return m.validationErrors
}
func TestGetInstaller(t *testing.T) {
config := &appconfig.AppConfig{}
logger.InitLogger(false)
@@ -69,6 +75,492 @@ func TestRunInstaller(t *testing.T) {
assert.NoError(t, err)
}
func TestAptValidation(t *testing.T) {
logger.InitLogger(false)
aptInstaller := &AptInstaller{
InstallerBase: InstallerBase{
Data: &appconfig.InstallerData{
Name: strPtr("test-apt"),
Type: appconfig.InstallerTypeApt,
},
},
}
errors := aptInstaller.Validate()
assert.Empty(t, errors)
}
func newTestBrewInstaller(data *appconfig.InstallerData) *BrewInstaller {
return &BrewInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Info: data,
}
}
func TestBrewValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid: No tap specified (tap is optional)
emptyData := &appconfig.InstallerData{
Name: strPtr("test-brew-valid"),
Type: appconfig.InstallerTypeBrew,
}
assert.Empty(t, newTestBrewInstaller(emptyData).Validate())
// 🟢 Valid: Well-formed tap (contains slash, sufficient length)
validData := &appconfig.InstallerData{
Name: strPtr("test-brew-valid-tap"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{"tap": "valid/tap"},
}
assert.Empty(t, newTestBrewInstaller(validData).Validate())
// 🔴 Invalid: Tap is present but malformed (missing slash or too short)
invalidData := &appconfig.InstallerData{
Name: strPtr("test-brew-invalid-tap"),
Type: appconfig.InstallerTypeBrew,
Opts: &map[string]any{"tap": "invalid-tap"},
}
assert.NotEmpty(t, newTestBrewInstaller(invalidData).Validate())
}
func newTestGitInstaller(data *appconfig.InstallerData) *GitInstaller {
return &GitInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Info: data,
}
}
func TestGitValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid: Both destination and ref are present
validData := &appconfig.InstallerData{
Name: strPtr("test-git-valid"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
"ref": "main",
},
}
errors := newTestGitInstaller(validData).Validate()
assert.Empty(t, errors)
// 🔴 Invalid: Missing ref
missingRefData := &appconfig.InstallerData{
Name: strPtr("test-git-missing-ref"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"destination": "/some/path",
},
}
errors = newTestGitInstaller(missingRefData).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "ref", errors[0].FieldName)
// 🔴 Invalid: Missing destination
missingDestData := &appconfig.InstallerData{
Name: strPtr("test-git-missing-destination"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{
"ref": "main",
},
}
errors = newTestGitInstaller(missingDestData).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "destination", errors[0].FieldName)
// 🔴 Invalid: Missing both destination and ref
missingBothData := &appconfig.InstallerData{
Name: strPtr("test-git-missing-both"),
Type: appconfig.InstallerTypeGit,
Opts: &map[string]any{},
}
errors = newTestGitInstaller(missingBothData).Validate()
assert.Len(t, errors, 2)
assert.ElementsMatch(t, []string{"destination", "ref"}, []string{errors[0].FieldName, errors[1].FieldName})
}
func newTestGitHubReleaseInstaller(data *appconfig.InstallerData) *GitHubReleaseInstaller {
return &GitHubReleaseInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Info: data,
}
}
func TestGitHubReleaseValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid
validData := &appconfig.InstallerData{
Name: strPtr("ghr-valid"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
"destination": "/some/path",
"download_filename": "file.tar.gz", // valid string
"strategy": "tar",
},
}
assert.Empty(t, newTestGitHubReleaseInstaller(validData).Validate())
// 🔴 Missing repository
missingRepo := &appconfig.InstallerData{
Name: strPtr("ghr-missing-repo"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"destination": "/some/path",
"download_filename": "file.tar.gz",
},
}
errors := newTestGitHubReleaseInstaller(missingRepo).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "repository", errors[0].FieldName)
// 🔴 Missing download_filename
missingDownloadFilename := &appconfig.InstallerData{
Name: strPtr("ghr-missing-download"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
"destination": "/some/path",
},
}
errors = newTestGitHubReleaseInstaller(missingDownloadFilename).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "download_filename", errors[0].FieldName)
// 🔴 Empty per-platform download_filename
emptyPlatformFilename := &appconfig.InstallerData{
Name: strPtr("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(""),
},
},
}
errors = newTestGitHubReleaseInstaller(emptyPlatformFilename).Validate()
assert.Len(t, errors, 1)
assert.Contains(t, errors[0].FieldName, "download_filename")
// 🔴 Invalid strategy
invalidStrategy := &appconfig.InstallerData{
Name: strPtr("ghr-invalid-strategy"),
Type: appconfig.InstallerTypeGitHubRelease,
Opts: &map[string]any{
"repository": "owner/repo",
"destination": "/some/path",
"download_filename": "file.tar.gz",
"strategy": "exe", // invalid
},
}
errors = newTestGitHubReleaseInstaller(invalidStrategy).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "strategy", errors[0].FieldName)
}
func newTestGroupInstaller(data *appconfig.InstallerData) *GroupInstaller {
return &GroupInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
Data: data,
}
}
func TestGroupValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid: one sub-installer
validStep := appconfig.InstallerData{
Name: strPtr("child-installer"),
Type: appconfig.InstallerTypeBrew,
}
validData := &appconfig.InstallerData{
Name: strPtr("group-valid"),
Type: appconfig.InstallerTypeGroup,
Steps: &[]appconfig.InstallerData{validStep},
}
assert.Empty(t, newTestGroupInstaller(validData).Validate())
// 🔴 Invalid: empty steps slice
emptySteps := &appconfig.InstallerData{
Name: strPtr("group-empty"),
Type: appconfig.InstallerTypeGroup,
Steps: &[]appconfig.InstallerData{},
}
errors := newTestGroupInstaller(emptySteps).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "steps", errors[0].FieldName)
// 🔴 Invalid: nil steps
nilSteps := &appconfig.InstallerData{
Name: strPtr("group-nil"),
Type: appconfig.InstallerTypeGroup,
Steps: nil,
}
errors = newTestGroupInstaller(nilSteps).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "steps", errors[0].FieldName)
}
func newTestManifestInstaller(data *appconfig.InstallerData) *ManifestInstaller {
return &ManifestInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
Info: data,
}
}
func TestManifestValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid
validData := &appconfig.InstallerData{
Name: strPtr("manifest-valid"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
"path": "manifests/installer.yml",
"ref": "main",
},
}
assert.Empty(t, newTestManifestInstaller(validData).Validate())
// 🔴 Missing source
missingSource := &appconfig.InstallerData{
Name: strPtr("manifest-missing-source"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"path": "some/path",
},
}
errors := newTestManifestInstaller(missingSource).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "source", errors[0].FieldName)
// 🔴 Missing path
missingPath := &appconfig.InstallerData{
Name: strPtr("manifest-missing-path"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
},
}
errors = newTestManifestInstaller(missingPath).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "path", errors[0].FieldName)
// 🔴 Empty ref (not nil, just empty)
emptyRef := &appconfig.InstallerData{
Name: strPtr("manifest-empty-ref"),
Type: appconfig.InstallerTypeManifest,
Opts: &map[string]any{
"source": "https://example.com/repo.git",
"path": "install.yml",
"ref": "",
},
}
errors = newTestManifestInstaller(emptyRef).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "ref", errors[0].FieldName)
}
func newTestNpmInstaller(data *appconfig.InstallerData) *NpmInstaller {
return &NpmInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
PackageManager: PackageManagerNpm,
Info: data,
}
}
func TestNpmValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid npm installer
validData := &appconfig.InstallerData{
Name: strPtr("some-npm-package"),
Type: appconfig.InstallerTypeNpm,
}
assert.Empty(t, newTestNpmInstaller(validData).Validate())
// 🔴 Edge case: nil name (will panic or fail in BaseValidate if implemented to check it)
nilNameData := &appconfig.InstallerData{
Name: nil,
Type: appconfig.InstallerTypeNpm,
}
errors := newTestNpmInstaller(nilNameData).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "name", errors[0].FieldName)
}
func newTestPipxInstaller(data *appconfig.InstallerData) *PipxInstaller {
return &PipxInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
Info: data,
}
}
func TestPipxValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid pipx installer
validData := &appconfig.InstallerData{
Name: strPtr("some-pipx-package"),
Type: appconfig.InstallerTypePipx,
}
assert.Empty(t, newTestPipxInstaller(validData).Validate())
// 🔴 Optional: test nil name if BaseValidate handles it
nilNameData := &appconfig.InstallerData{
Name: nil,
Type: appconfig.InstallerTypePipx,
}
errors := newTestPipxInstaller(nilNameData).Validate()
// Uncomment if BaseValidate checks for nil
assert.Len(t, errors, 1)
assert.Equal(t, "name", errors[0].FieldName)
}
func newTestRsyncInstaller(data *appconfig.InstallerData) *RsyncInstaller {
return &RsyncInstaller{
InstallerBase: InstallerBase{
Data: data,
},
Config: nil,
Info: data,
}
}
func TestRsyncValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid rsync config
validData := &appconfig.InstallerData{
Name: strPtr("rsync-valid"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",
"destination": "/path/to",
"flags": "-avz",
},
}
assert.Empty(t, newTestRsyncInstaller(validData).Validate())
// 🔴 Missing source
missingSource := &appconfig.InstallerData{
Name: strPtr("rsync-missing-source"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"destination": "/path/to",
},
}
errors := newTestRsyncInstaller(missingSource).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "source", errors[0].FieldName)
// 🔴 Missing destination
missingDest := &appconfig.InstallerData{
Name: strPtr("rsync-missing-destination"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",
},
}
errors = newTestRsyncInstaller(missingDest).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "destination", errors[0].FieldName)
// 🔴 Empty flags string
emptyFlags := &appconfig.InstallerData{
Name: strPtr("rsync-empty-flags"),
Type: appconfig.InstallerTypeRsync,
Opts: &map[string]any{
"source": "/path/from",
"destination": "/path/to",
"flags": "",
},
}
errors = newTestRsyncInstaller(emptyFlags).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "flags", errors[0].FieldName)
}
func newTestShellInstaller(data *appconfig.InstallerData) *ShellInstaller {
return &ShellInstaller{
InstallerBase: InstallerBase{Data: data},
Config: nil,
Info: data,
}
}
func TestShellValidation(t *testing.T) {
logger.InitLogger(false)
// 🟢 Valid shell config
validData := &appconfig.InstallerData{
Name: strPtr("shell-valid"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"command": "echo install",
"update_command": "echo update",
},
}
assert.Empty(t, newTestShellInstaller(validData).Validate())
// 🔴 Missing command
missingCommand := &appconfig.InstallerData{
Name: strPtr("shell-missing-command"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"update_command": "echo update",
},
}
errors := newTestShellInstaller(missingCommand).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "command", errors[0].FieldName)
// 🔴 Missing update_command
missingUpdate := &appconfig.InstallerData{
Name: strPtr("shell-missing-update"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{
"command": "echo install",
},
}
errors = newTestShellInstaller(missingUpdate).Validate()
assert.Len(t, errors, 1)
assert.Equal(t, "update_command", errors[0].FieldName)
// 🔴 Missing both
missingBoth := &appconfig.InstallerData{
Name: strPtr("shell-missing-both"),
Type: appconfig.InstallerTypeShell,
Opts: &map[string]any{},
}
errors = newTestShellInstaller(missingBoth).Validate()
assert.Len(t, errors, 2)
assert.ElementsMatch(t, []string{"command", "update_command"},
[]string{errors[0].FieldName, errors[1].FieldName})
}
func strPtr(s string) *string {
return &s
}

View File

@@ -23,6 +23,22 @@ type ManifestOpts struct {
Ref *string
}
func (i *ManifestInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Source == nil || len(*opts.Source) == 0 {
errors = append(errors, ValidationError{FieldName: "source", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Path == nil || len(*opts.Path) == 0 {
errors = append(errors, ValidationError{FieldName: "path", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Ref != nil && len(*opts.Ref) == 0 {
errors = append(errors, ValidationError{FieldName: "ref", Message: validationIsNotEmpty(), InstallerName: *info.Name})
}
return errors
}
// Install implements IInstaller.
func (i *ManifestInstaller) Install() error {
logger.Debug("Getting manifest info...")

View File

@@ -24,6 +24,11 @@ const (
PackageManagerPnpm PackageManager = "pnpm"
)
func (i *NpmInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
return errors
}
// Install implements IInstaller.
func (i *NpmInstaller) Install() error {
return i.RunCmdPassThrough(string(i.PackageManager), "install", "--global", *i.Info.Name)

View File

@@ -15,6 +15,11 @@ type PipxOpts struct {
//
}
func (i *PipxInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
return errors
}
// Install implements IInstaller.
func (i *PipxInstaller) Install() error {
name := *i.Info.Name

View File

@@ -20,6 +20,22 @@ type RsyncOpts struct {
Flags *string
}
func (i *RsyncInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Source == nil || len(*opts.Source) == 0 {
errors = append(errors, ValidationError{FieldName: "source", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Destination == nil || len(*opts.Destination) == 0 {
errors = append(errors, ValidationError{FieldName: "destination", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.Flags != nil && len(*opts.Flags) == 0 {
errors = append(errors, ValidationError{FieldName: "flags", Message: validationIsNotEmpty(), InstallerName: *info.Name})
}
return errors
}
// Install implements IInstaller.
func (i *RsyncInstaller) Install() error {
defaultFlags := "-tr"

View File

@@ -16,6 +16,19 @@ type ShellOpts struct {
UpdateCommand *string
}
func (i *ShellInstaller) Validate() []ValidationError {
errors := i.BaseValidate()
info := i.GetData()
opts := i.GetOpts()
if opts.Command == nil || len(*opts.Command) == 0 {
errors = append(errors, ValidationError{FieldName: "command", Message: validationIsRequired(), InstallerName: *info.Name})
}
if opts.UpdateCommand == nil || len(*opts.UpdateCommand) == 0 {
errors = append(errors, ValidationError{FieldName: "update_command", Message: validationIsRequired(), InstallerName: *info.Name})
}
return errors
}
// Install implements IInstaller.
func (i *ShellInstaller) Install() error {
return i.RunCmdAsFile(*i.GetOpts().Command)

View File

@@ -0,0 +1,25 @@
package installer
import "fmt"
type ValidationError struct {
FieldName string
Message string
InstallerName string
}
func (v ValidationError) Error() string {
return fmt.Sprintf("Validation Error in %s - Field '%s' is invalid: %s.", v.InstallerName, v.FieldName, v.Message)
}
func validationIsRequired() string {
return "Must be specified"
}
func validationInvalidFormat() string {
return "Invalid format"
}
func validationIsNotEmpty() string {
return "Cannot be empty"
}

26
main.go
View File

@@ -37,6 +37,9 @@ func main() {
}
logger.Info("Checking all installers...")
instances := []installer.IInstaller{}
hasValidationErrors := false
for _, i := range cfg.Install {
installerInstance, err := installer.GetInstaller(cfg, &i)
if err != nil {
@@ -46,12 +49,27 @@ func main() {
if installerInstance == nil {
logger.Warn("Installer type %s is not supported, skipping", i.Type)
} else {
err = installer.RunInstaller(cfg, installerInstance)
if err != nil {
logger.Error("%s", err)
os.Exit(1)
errors := installerInstance.Validate()
if len(errors) > 0 {
hasValidationErrors = true
for _, e := range errors {
logger.Error(e.Error())
}
}
}
}
if hasValidationErrors {
logger.Error("Validation errors found, exiting. Please fix the errors and try again.")
os.Exit(1)
}
for _, i := range instances {
err = installer.RunInstaller(cfg, i)
if err != nil {
logger.Error("%s", err)
os.Exit(1)
}
}
logger.Info("Complete")
}

View File

@@ -3,6 +3,7 @@ package platform
import (
"fmt"
"runtime"
"slices"
)
var osValue string = runtime.GOOS
@@ -83,14 +84,8 @@ func (o *PlatformMap[T]) ResolveWithFallback(fallback PlatformMap[T]) T {
}
func ContainsPlatform(platforms *[]Platform, platform Platform) bool {
for _, p := range *platforms {
if p == platform {
return true
}
}
return false
return slices.Contains(*platforms, platform)
}
func (p *Platforms) GetShouldRunOnOS(curOS Platform) bool {
if p == nil {
return true
@@ -104,3 +99,21 @@ func (p *Platforms) GetShouldRunOnOS(curOS Platform) bool {
}
return true
}
func NewPlatformMap[T any](values map[string]T) *PlatformMap[T] {
p := &PlatformMap[T]{}
for k, v := range values {
val := v // capture value for pointer
switch Platform(k) {
case PlatformMacos:
p.MacOS = &val
case PlatformLinux:
p.Linux = &val
case PlatformWindows:
p.Windows = &val
default:
panic(fmt.Sprintf("Unsupported platform key: %q", k))
}
}
return p
}