diff --git a/internal/services/brew.go b/internal/services/brew.go index 9e7cceb..1de875f 100644 --- a/internal/services/brew.go +++ b/internal/services/brew.go @@ -65,6 +65,9 @@ type BrewServiceInterface interface { // BrewService provides methods to interact with Homebrew, including // retrieving formulae, casks, and handling analytics. type BrewService struct { + // Data provider for loading packages + dataProvider DataProviderInterface + // Formula lists all *[]models.Formula installed *[]models.Formula @@ -87,6 +90,7 @@ type BrewService struct { // NewBrewService creates a new instance of BrewService with initialized package lists. var NewBrewService = func() BrewServiceInterface { return &BrewService{ + dataProvider: NewDataProvider(), all: new([]models.Formula), installed: new([]models.Formula), remote: new([]models.Formula), @@ -135,4 +139,3 @@ func (s *BrewService) UpdateHomebrew() error { cmd := exec.Command("brew", "update") return cmd.Run() } - diff --git a/internal/services/data.go b/internal/services/data.go index 0cf3dbf..234dea1 100644 --- a/internal/services/data.go +++ b/internal/services/data.go @@ -2,320 +2,65 @@ package services import ( "bbrew/internal/models" - "encoding/json" "fmt" - "os/exec" - "path/filepath" - "strings" ) // SetupData initializes the BrewService by loading installed packages, remote formulae, casks, and analytics data. +// Uses the DataProvider for all data retrieval operations. func (s *BrewService) SetupData(forceDownload bool) error { - // Load formulae - if err := s.loadInstalled(forceDownload); err != nil { + // Load installed formulae + installed, err := s.dataProvider.LoadInstalledFormulae(forceDownload) + if err != nil { return fmt.Errorf("failed to load installed formulae: %w", err) } + *s.installed = installed - if err := s.loadRemote(forceDownload); err != nil { + // Load remote formulae + remote, err := s.dataProvider.LoadRemoteFormulae(forceDownload) + if err != nil { return fmt.Errorf("failed to load remote formulae: %w", err) } + *s.remote = remote - if err := s.loadAnalytics(forceDownload); err != nil { + // Load formulae analytics + analytics, err := s.dataProvider.LoadFormulaeAnalytics(forceDownload) + if err != nil { return fmt.Errorf("failed to load formulae analytics: %w", err) } + s.analytics = analytics - // Load casks - if err := s.loadInstalledCasks(forceDownload); err != nil { + // Load installed casks + installedCasks, err := s.dataProvider.LoadInstalledCasks(forceDownload) + if err != nil { return fmt.Errorf("failed to load installed casks: %w", err) } + *s.installedCasks = installedCasks - if err := s.loadRemoteCasks(forceDownload); err != nil { + // Load remote casks + remoteCasks, err := s.dataProvider.LoadRemoteCasks(forceDownload) + if err != nil { return fmt.Errorf("failed to load remote casks: %w", err) } + *s.remoteCasks = remoteCasks - if err := s.loadCaskAnalytics(forceDownload); err != nil { + // Load cask analytics + caskAnalytics, err := s.dataProvider.LoadCaskAnalytics(forceDownload) + if err != nil { return fmt.Errorf("failed to load cask analytics: %w", err) } + s.caskAnalytics = caskAnalytics return nil } -// loadInstalled retrieves installed formulae, optionally using cache. -func (s *BrewService) loadInstalled(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "installed.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 10); data != nil { - *s.installed = make([]models.Formula, 0) - if err := json.Unmarshal(data, &s.installed); err == nil { - s.markFormulaeAsInstalled() - return nil - } - } - } - - cmd := exec.Command("brew", "info", "--json=v1", "--installed") - output, err := cmd.Output() - if err != nil { - return err - } - - *s.installed = make([]models.Formula, 0) - if err := json.Unmarshal(output, &s.installed); err != nil { - return err - } - - s.markFormulaeAsInstalled() - writeCacheFile(cacheFile, output) - return nil -} - -// markFormulaeAsInstalled sets LocallyInstalled and LocalPath for all installed formulae. -func (s *BrewService) markFormulaeAsInstalled() { - prefix := s.GetPrefixPath() - for i := range *s.installed { - (*s.installed)[i].LocallyInstalled = true - (*s.installed)[i].LocalPath = filepath.Join(prefix, "Cellar", (*s.installed)[i].Name) - } -} - -// loadInstalledCasks retrieves installed casks, optionally using cache. -func (s *BrewService) loadInstalledCasks(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "installed-casks.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 10); data != nil { - var response struct { - Casks []models.Cask `json:"casks"` - } - if err := json.Unmarshal(data, &response); err == nil { - *s.installedCasks = response.Casks - s.markCasksAsInstalled() - return nil - } - } - } - - // Get list of installed cask names - listCmd := exec.Command("brew", "list", "--cask") - listOutput, err := listCmd.Output() - if err != nil { - *s.installedCasks = make([]models.Cask, 0) - return nil - } - - caskNames := strings.Split(strings.TrimSpace(string(listOutput)), "\n") - if len(caskNames) == 0 || (len(caskNames) == 1 && caskNames[0] == "") { - *s.installedCasks = make([]models.Cask, 0) - return nil - } - - // Get info for each installed cask - args := append([]string{"info", "--json=v2", "--cask"}, caskNames...) - infoCmd := exec.Command("brew", args...) - infoOutput, err := infoCmd.Output() - if err != nil { - *s.installedCasks = make([]models.Cask, 0) - return nil - } - - var response struct { - Casks []models.Cask `json:"casks"` - } - if err := json.Unmarshal(infoOutput, &response); err != nil { - return err - } - - *s.installedCasks = response.Casks - s.markCasksAsInstalled() - writeCacheFile(cacheFile, infoOutput) - return nil -} - -// markCasksAsInstalled sets LocallyInstalled and IsCask for all installed casks. -func (s *BrewService) markCasksAsInstalled() { - for i := range *s.installedCasks { - (*s.installedCasks)[i].LocallyInstalled = true - (*s.installedCasks)[i].IsCask = true - } -} - -// loadRemote retrieves the list of remote Homebrew formulae from the API and caches them locally. -func (s *BrewService) loadRemote(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "formula.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 1000); data != nil { - *s.remote = make([]models.Formula, 0) - if err := json.Unmarshal(data, &s.remote); err == nil && len(*s.remote) > 0 { - return nil - } - } - } - - body, err := fetchFromAPI(FormulaeAPIURL) - if err != nil { - return err - } - - *s.remote = make([]models.Formula, 0) - if err := json.Unmarshal(body, s.remote); err != nil { - return err - } - - writeCacheFile(cacheFile, body) - return nil -} - -// loadRemoteCasks retrieves the list of remote Homebrew casks from the API and caches them locally. -func (s *BrewService) loadRemoteCasks(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "cask.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 1000); data != nil { - *s.remoteCasks = make([]models.Cask, 0) - if err := json.Unmarshal(data, &s.remoteCasks); err == nil && len(*s.remoteCasks) > 0 { - return nil - } - } - } - - body, err := fetchFromAPI(CaskAPIURL) - if err != nil { - return err - } - - *s.remoteCasks = make([]models.Cask, 0) - if err := json.Unmarshal(body, s.remoteCasks); err != nil { - return err - } - - writeCacheFile(cacheFile, body) - return nil -} - -// loadAnalytics retrieves the analytics data for Homebrew formulae from the API and caches them locally. -func (s *BrewService) loadAnalytics(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "analytics.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 100); data != nil { - analytics := models.Analytics{} - if err := json.Unmarshal(data, &analytics); err == nil && len(analytics.Items) > 0 { - s.analytics = make(map[string]models.AnalyticsItem) - for _, f := range analytics.Items { - s.analytics[f.Formula] = f - } - return nil - } - } - } - - body, err := fetchFromAPI(AnalyticsAPIURL) - if err != nil { - return err - } - - analytics := models.Analytics{} - if err := json.Unmarshal(body, &analytics); err != nil { - return err - } - - s.analytics = make(map[string]models.AnalyticsItem) - for _, f := range analytics.Items { - s.analytics[f.Formula] = f - } - - writeCacheFile(cacheFile, body) - return nil -} - -// loadCaskAnalytics retrieves the analytics data for Homebrew casks from the API and caches them locally. -func (s *BrewService) loadCaskAnalytics(forceDownload bool) error { - if err := ensureCacheDir(); err != nil { - return err - } - - const cacheFile = "cask-analytics.json" - if !forceDownload { - if data := readCacheFile(cacheFile, 100); data != nil { - analytics := models.Analytics{} - if err := json.Unmarshal(data, &analytics); err == nil && len(analytics.Items) > 0 { - s.caskAnalytics = make(map[string]models.AnalyticsItem) - for _, c := range analytics.Items { - if c.Cask != "" { - s.caskAnalytics[c.Cask] = c - } - } - return nil - } - } - } - - body, err := fetchFromAPI(CaskAnalyticsAPIURL) - if err != nil { - return err - } - - analytics := models.Analytics{} - if err := json.Unmarshal(body, &analytics); err != nil { - return err - } - - s.caskAnalytics = make(map[string]models.AnalyticsItem) - for _, c := range analytics.Items { - if c.Cask != "" { - s.caskAnalytics[c.Cask] = c - } - } - - writeCacheFile(cacheFile, body) - return nil -} - // LoadTapPackagesCache loads cached tap packages from disk. +// Delegates to the DataProvider. func (s *BrewService) LoadTapPackagesCache() map[string]models.Package { - result := make(map[string]models.Package) - - const cacheFile = "tap_packages.json" - if data := readCacheFile(cacheFile, 10); data != nil { - var packages []models.Package - if err := json.Unmarshal(data, &packages); err == nil { - for _, pkg := range packages { - result[pkg.Name] = pkg - } - } - } - - return result + return s.dataProvider.LoadTapPackagesCache() } // SaveTapPackagesToCache saves tap packages to disk cache. +// Delegates to the DataProvider. func (s *BrewService) SaveTapPackagesToCache(packages []models.Package) error { - if err := ensureCacheDir(); err != nil { - return err - } - - data, err := json.Marshal(packages) - if err != nil { - return err - } - - writeCacheFile("tap_packages.json", data) - return nil + return s.dataProvider.SaveTapPackagesToCache(packages) } diff --git a/internal/services/dataprovider.go b/internal/services/dataprovider.go new file mode 100644 index 0000000..1588456 --- /dev/null +++ b/internal/services/dataprovider.go @@ -0,0 +1,330 @@ +package services + +import ( + "bbrew/internal/models" + "encoding/json" + "os/exec" + "path/filepath" + "strings" +) + +// Cache file names +const ( + cacheFileInstalled = "installed.json" + cacheFileInstalledCasks = "installed-casks.json" + cacheFileFormulae = "formula.json" + cacheFileCasks = "cask.json" + cacheFileAnalytics = "analytics.json" + cacheFileCaskAnalytics = "cask-analytics.json" + cacheFileTapPackages = "tap_packages.json" +) + +// DataProviderInterface defines the contract for data loading operations. +type DataProviderInterface interface { + // Formulae + LoadInstalledFormulae(forceDownload bool) ([]models.Formula, error) + LoadRemoteFormulae(forceDownload bool) ([]models.Formula, error) + LoadFormulaeAnalytics(forceDownload bool) (map[string]models.AnalyticsItem, error) + + // Casks + LoadInstalledCasks(forceDownload bool) ([]models.Cask, error) + LoadRemoteCasks(forceDownload bool) ([]models.Cask, error) + LoadCaskAnalytics(forceDownload bool) (map[string]models.AnalyticsItem, error) + + // Tap packages cache + LoadTapPackagesCache() map[string]models.Package + SaveTapPackagesToCache(packages []models.Package) error +} + +// DataProvider implements DataProviderInterface. +type DataProvider struct { + prefixPath string +} + +// NewDataProvider creates a new DataProvider instance. +func NewDataProvider() *DataProvider { + return &DataProvider{} +} + +// getPrefixPath returns the Homebrew prefix path, caching it. +func (d *DataProvider) getPrefixPath() string { + if d.prefixPath != "" { + return d.prefixPath + } + cmd := exec.Command("brew", "--prefix") + output, err := cmd.Output() + if err != nil { + d.prefixPath = "Unknown" + return d.prefixPath + } + d.prefixPath = strings.TrimSpace(string(output)) + return d.prefixPath +} + +// LoadInstalledFormulae retrieves installed formulae, optionally using cache. +func (d *DataProvider) LoadInstalledFormulae(forceDownload bool) ([]models.Formula, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileInstalled, 10); data != nil { + var formulae []models.Formula + if err := json.Unmarshal(data, &formulae); err == nil { + d.markFormulaeAsInstalled(&formulae) + return formulae, nil + } + } + } + + cmd := exec.Command("brew", "info", "--json=v1", "--installed") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + var formulae []models.Formula + if err := json.Unmarshal(output, &formulae); err != nil { + return nil, err + } + + d.markFormulaeAsInstalled(&formulae) + writeCacheFile(cacheFileInstalled, output) + return formulae, nil +} + +// markFormulaeAsInstalled sets LocallyInstalled and LocalPath for formulae. +func (d *DataProvider) markFormulaeAsInstalled(formulae *[]models.Formula) { + prefix := d.getPrefixPath() + for i := range *formulae { + (*formulae)[i].LocallyInstalled = true + (*formulae)[i].LocalPath = filepath.Join(prefix, "Cellar", (*formulae)[i].Name) + } +} + +// LoadInstalledCasks retrieves installed casks, optionally using cache. +func (d *DataProvider) LoadInstalledCasks(forceDownload bool) ([]models.Cask, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileInstalledCasks, 10); data != nil { + var response struct { + Casks []models.Cask `json:"casks"` + } + if err := json.Unmarshal(data, &response); err == nil { + d.markCasksAsInstalled(&response.Casks) + return response.Casks, nil + } + } + } + + // Get list of installed cask names + listCmd := exec.Command("brew", "list", "--cask") + listOutput, err := listCmd.Output() + if err != nil { + return []models.Cask{}, nil // No casks installed + } + + caskNames := strings.Split(strings.TrimSpace(string(listOutput)), "\n") + if len(caskNames) == 0 || (len(caskNames) == 1 && caskNames[0] == "") { + return []models.Cask{}, nil + } + + // Get info for each installed cask + args := append([]string{"info", "--json=v2", "--cask"}, caskNames...) + infoCmd := exec.Command("brew", args...) + infoOutput, err := infoCmd.Output() + if err != nil { + return []models.Cask{}, nil + } + + var response struct { + Casks []models.Cask `json:"casks"` + } + if err := json.Unmarshal(infoOutput, &response); err != nil { + return nil, err + } + + d.markCasksAsInstalled(&response.Casks) + writeCacheFile(cacheFileInstalledCasks, infoOutput) + return response.Casks, nil +} + +// markCasksAsInstalled sets LocallyInstalled and IsCask for casks. +func (d *DataProvider) markCasksAsInstalled(casks *[]models.Cask) { + for i := range *casks { + (*casks)[i].LocallyInstalled = true + (*casks)[i].IsCask = true + } +} + +// LoadRemoteFormulae retrieves remote formulae from API, optionally using cache. +func (d *DataProvider) LoadRemoteFormulae(forceDownload bool) ([]models.Formula, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileFormulae, 1000); data != nil { + var formulae []models.Formula + if err := json.Unmarshal(data, &formulae); err == nil && len(formulae) > 0 { + return formulae, nil + } + } + } + + body, err := fetchFromAPI(FormulaeAPIURL) + if err != nil { + return nil, err + } + + var formulae []models.Formula + if err := json.Unmarshal(body, &formulae); err != nil { + return nil, err + } + + writeCacheFile(cacheFileFormulae, body) + return formulae, nil +} + +// LoadRemoteCasks retrieves remote casks from API, optionally using cache. +func (d *DataProvider) LoadRemoteCasks(forceDownload bool) ([]models.Cask, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileCasks, 1000); data != nil { + var casks []models.Cask + if err := json.Unmarshal(data, &casks); err == nil && len(casks) > 0 { + return casks, nil + } + } + } + + body, err := fetchFromAPI(CaskAPIURL) + if err != nil { + return nil, err + } + + var casks []models.Cask + if err := json.Unmarshal(body, &casks); err != nil { + return nil, err + } + + writeCacheFile(cacheFileCasks, body) + return casks, nil +} + +// LoadFormulaeAnalytics retrieves formulae analytics from API, optionally using cache. +func (d *DataProvider) LoadFormulaeAnalytics(forceDownload bool) (map[string]models.AnalyticsItem, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileAnalytics, 100); data != nil { + analytics := models.Analytics{} + if err := json.Unmarshal(data, &analytics); err == nil && len(analytics.Items) > 0 { + result := make(map[string]models.AnalyticsItem) + for _, f := range analytics.Items { + result[f.Formula] = f + } + return result, nil + } + } + } + + body, err := fetchFromAPI(AnalyticsAPIURL) + if err != nil { + return nil, err + } + + analytics := models.Analytics{} + if err := json.Unmarshal(body, &analytics); err != nil { + return nil, err + } + + result := make(map[string]models.AnalyticsItem) + for _, f := range analytics.Items { + result[f.Formula] = f + } + + writeCacheFile(cacheFileAnalytics, body) + return result, nil +} + +// LoadCaskAnalytics retrieves cask analytics from API, optionally using cache. +func (d *DataProvider) LoadCaskAnalytics(forceDownload bool) (map[string]models.AnalyticsItem, error) { + if err := ensureCacheDir(); err != nil { + return nil, err + } + + if !forceDownload { + if data := readCacheFile(cacheFileCaskAnalytics, 100); data != nil { + analytics := models.Analytics{} + if err := json.Unmarshal(data, &analytics); err == nil && len(analytics.Items) > 0 { + result := make(map[string]models.AnalyticsItem) + for _, c := range analytics.Items { + if c.Cask != "" { + result[c.Cask] = c + } + } + return result, nil + } + } + } + + body, err := fetchFromAPI(CaskAnalyticsAPIURL) + if err != nil { + return nil, err + } + + analytics := models.Analytics{} + if err := json.Unmarshal(body, &analytics); err != nil { + return nil, err + } + + result := make(map[string]models.AnalyticsItem) + for _, c := range analytics.Items { + if c.Cask != "" { + result[c.Cask] = c + } + } + + writeCacheFile(cacheFileCaskAnalytics, body) + return result, nil +} + +// LoadTapPackagesCache loads cached tap packages from disk. +func (d *DataProvider) LoadTapPackagesCache() map[string]models.Package { + result := make(map[string]models.Package) + + if data := readCacheFile(cacheFileTapPackages, 10); data != nil { + var packages []models.Package + if err := json.Unmarshal(data, &packages); err == nil { + for _, pkg := range packages { + result[pkg.Name] = pkg + } + } + } + + return result +} + +// SaveTapPackagesToCache saves tap packages to disk cache. +func (d *DataProvider) SaveTapPackagesToCache(packages []models.Package) error { + if err := ensureCacheDir(); err != nil { + return err + } + + data, err := json.Marshal(packages) + if err != nil { + return err + } + + writeCacheFile(cacheFileTapPackages, data) + return nil +}