ops.go 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. // Package ops holds the list operations shared by the CLI and the MCP server.
  2. //
  3. // Both surfaces expose the same three modes; keeping the fetch loop here is
  4. // what keeps them congruent instead of drifting apart.
  5. package ops
  6. import (
  7. "context"
  8. "errors"
  9. "fmt"
  10. "gogs.netdome.biz/paul/cheapo/internal/game"
  11. )
  12. // Mode selects what List returns.
  13. type Mode string
  14. const (
  15. // Free returns games that are free right now.
  16. Free Mode = "free"
  17. // Upcoming returns announced future giveaways (stores with that capability only).
  18. Upcoming Mode = "upcoming"
  19. // Deals returns discounted games (stores with that capability only).
  20. Deals Mode = "deals"
  21. )
  22. // Modes lists the accepted Mode values in a stable order.
  23. var Modes = []Mode{Free, Upcoming, Deals}
  24. // ParseMode maps a mode name to a Mode. The empty string means Free.
  25. func ParseMode(name string) (Mode, error) {
  26. switch m := Mode(name); m {
  27. case "", Free:
  28. return Free, nil
  29. case Upcoming, Deals:
  30. return m, nil
  31. default:
  32. return "", fmt.Errorf("unknown mode %q (free, upcoming, deals)", name)
  33. }
  34. }
  35. // List queries provs for mode.
  36. //
  37. // Stores without the requested capability are skipped silently (Steam has no
  38. // upcoming window, GOG no deals). A failing store becomes a warning instead of
  39. // aborting, so one flaky endpoint does not hide the others. The error return
  40. // is only for an unknown mode.
  41. func List(ctx context.Context, provs []game.Provider, mode Mode) ([]game.Game, []string, error) {
  42. m, err := ParseMode(string(mode))
  43. if err != nil {
  44. return nil, nil, err
  45. }
  46. var (
  47. all []game.Game
  48. warnings []string
  49. )
  50. for _, p := range provs {
  51. games, err := fetch(ctx, p, m)
  52. if errors.Is(err, errUnsupported) {
  53. continue
  54. }
  55. if err != nil {
  56. warnings = append(warnings, fmt.Sprintf("%s: %v", p.Name(), err))
  57. continue
  58. }
  59. all = append(all, games...)
  60. }
  61. return all, warnings, nil
  62. }
  63. // errUnsupported marks a provider that cannot answer the requested mode.
  64. var errUnsupported = errors.New("mode not supported by provider")
  65. // fetch asks a single provider for the games of mode.
  66. func fetch(ctx context.Context, p game.Provider, mode Mode) ([]game.Game, error) {
  67. switch mode {
  68. case Upcoming:
  69. u, ok := p.(game.UpcomingProvider)
  70. if !ok {
  71. return nil, errUnsupported
  72. }
  73. return u.GetUpcomingGames(ctx)
  74. case Deals:
  75. d, ok := p.(game.DealProvider)
  76. if !ok {
  77. return nil, errUnsupported
  78. }
  79. return d.GetDeals(ctx)
  80. default:
  81. return p.GetFreeGames(ctx)
  82. }
  83. }