// Package ops holds the list operations shared by the CLI and the MCP server. // // Both surfaces expose the same three modes; keeping the fetch loop here is // what keeps them congruent instead of drifting apart. package ops import ( "context" "errors" "fmt" "gogs.netdome.biz/paul/cheapo/internal/game" ) // Mode selects what List returns. type Mode string const ( // Free returns games that are free right now. Free Mode = "free" // Upcoming returns announced future giveaways (stores with that capability only). Upcoming Mode = "upcoming" // Deals returns discounted games (stores with that capability only). Deals Mode = "deals" ) // Modes lists the accepted Mode values in a stable order. var Modes = []Mode{Free, Upcoming, Deals} // ParseMode maps a mode name to a Mode. The empty string means Free. func ParseMode(name string) (Mode, error) { switch m := Mode(name); m { case "", Free: return Free, nil case Upcoming, Deals: return m, nil default: return "", fmt.Errorf("unknown mode %q (free, upcoming, deals)", name) } } // List queries provs for mode. // // Stores without the requested capability are skipped silently (Steam has no // upcoming window, GOG no deals). A failing store becomes a warning instead of // aborting, so one flaky endpoint does not hide the others. The error return // is only for an unknown mode. func List(ctx context.Context, provs []game.Provider, mode Mode) ([]game.Game, []string, error) { m, err := ParseMode(string(mode)) if err != nil { return nil, nil, err } var ( all []game.Game warnings []string ) for _, p := range provs { games, err := fetch(ctx, p, m) if errors.Is(err, errUnsupported) { continue } if err != nil { warnings = append(warnings, fmt.Sprintf("%s: %v", p.Name(), err)) continue } all = append(all, games...) } return all, warnings, nil } // errUnsupported marks a provider that cannot answer the requested mode. var errUnsupported = errors.New("mode not supported by provider") // fetch asks a single provider for the games of mode. func fetch(ctx context.Context, p game.Provider, mode Mode) ([]game.Game, error) { switch mode { case Upcoming: u, ok := p.(game.UpcomingProvider) if !ok { return nil, errUnsupported } return u.GetUpcomingGames(ctx) case Deals: d, ok := p.(game.DealProvider) if !ok { return nil, errUnsupported } return d.GetDeals(ctx) default: return p.GetFreeGames(ctx) } }