diff --git a/client/client.go b/client/client.go index 8e8903b..34a00f3 100644 --- a/client/client.go +++ b/client/client.go @@ -19,7 +19,8 @@ type Client struct { rateLimit *rateLimiter // services - Players *endpoints.PlayerService + Players *endpoints.PlayerService + Fixtures *endpoints.FixtureService } func NewClient(opts ...Option) *Client { @@ -39,13 +40,14 @@ func NewClient(opts ...Option) *Client { rateLimit: newRateLimiter(50, time.Minute), } - // Apply options for _, opt := range opts { opt(c) } - // Initialize services + // services + c.Players = endpoints.NewPlayerService(c) + c.Fixtures = endpoints.NewFixtureService(c) return c } diff --git a/endpoints/fixtures.go b/endpoints/fixtures.go index a29b68f..ec17af1 100644 --- a/endpoints/fixtures.go +++ b/endpoints/fixtures.go @@ -1 +1,84 @@ package endpoints + +import ( + "encoding/json" + "fmt" + "time" + + "github.com/AbdoAnss/go-fantasy-pl/api" + "github.com/AbdoAnss/go-fantasy-pl/internal/cache" + "github.com/AbdoAnss/go-fantasy-pl/models" +) + +const ( + fixturesEndpoint = "/fixtures/" +) + +type FixtureService struct { + client api.Client +} + +type FixtureNotFoundError struct { + ID int +} + +func (e *FixtureNotFoundError) Error() string { + return fmt.Sprintf("fixture with ID %d not found", e.ID) +} + +func NewFixtureService(client api.Client) *FixtureService { + return &FixtureService{ + client: client, + } +} + +var fixturesCache = cache.NewCache() + +func init() { + fixturesCache.StartCleanupTask(5 * time.Minute) +} + +func (fs *FixtureService) GetAllFixtures() ([]models.Fixture, error) { + if cached, found := fixturesCache.Get("fixtures"); found { + if fixtures, ok := cached.([]models.Fixture); ok { + return fixtures, nil + } + } + + resp, err := fs.client.Get(fixturesEndpoint) + if err != nil { + return nil, fmt.Errorf("failed to get fixtures: %w", err) + } + defer resp.Body.Close() + + var fixtures []models.Fixture + if err := json.NewDecoder(resp.Body).Decode(&fixtures); err != nil { + return nil, fmt.Errorf("failed to decode fixtures: %w", err) + } + + fixturesCache.Set("fixtures", fixtures, defaultCacheTTL) + + return fixtures, nil +} + +func (fs *FixtureService) GetFixture(id int) (*models.Fixture, error) { + if cached, found := fixturesCache.Get(fmt.Sprintf("fixture_%d", id)); found { + if fixture, ok := cached.(*models.Fixture); ok { + return fixture, nil + } + } + + fixtures, err := fs.GetAllFixtures() + if err != nil { + return nil, err + } + + for _, f := range fixtures { + if f.ID == id { + fixturesCache.Set(fmt.Sprintf("fixture_%d", id), &f, defaultCacheTTL) + return &f, nil + } + } + + return nil, &FixtureNotFoundError{ID: id} +} diff --git a/endpoints/fixtures_test.go b/endpoints/fixtures_test.go new file mode 100644 index 0000000..7c37296 --- /dev/null +++ b/endpoints/fixtures_test.go @@ -0,0 +1,106 @@ +package endpoints_test + +import ( + "testing" + + "github.com/AbdoAnss/go-fantasy-pl/client" + "github.com/AbdoAnss/go-fantasy-pl/endpoints" + "github.com/stretchr/testify/assert" +) + +var fixtureID int + +func setupFixtureTestService() *endpoints.FixtureService { + c := client.NewClient() + fixtureID = 8 + + return endpoints.NewFixtureService(c) +} + +func TestGetAllFixtures(t *testing.T) { + fs := setupFixtureTestService() + fixtures, err := fs.GetAllFixtures() + + assert.NoError(t, err, "expected no error when getting all fixtures") + assert.NotEmpty(t, fixtures, "expected fixtures to be returned from API") + + t.Logf("Retrieved %d fixtures from the API.", len(fixtures)) + + // TODO: tests to improve when adding teams logic + // convert ID to team ShortName for better readability + + for i, fixture := range fixtures { + t.Logf("Fixture %d: ID: %d, Team A: %d, Team H: %d", + i+1, + fixture.ID, + fixture.TeamA, + fixture.TeamH) + + if i >= 3 { + break + } + } +} + +func TestGetFixture(t *testing.T) { + fs := setupFixtureTestService() + + fixture, err := fs.GetFixture(fixtureID) + + assert.NoError(t, err, "expected no error when getting fixture") + assert.NotNil(t, fixture, "expected fixture to be returned, got nil") + + // Log fixture details + t.Logf("Fixture ID: %d", fixture.ID) + t.Logf("Team A: %d vs Team H: %d", fixture.TeamA, fixture.TeamH) + t.Logf("Fixture Finished: %v", fixture.Finished) + t.Logf("Kickoff Time: %v", fixture.KickoffTime) +} + +func TestGetNonExistentFixture(t *testing.T) { + fs := setupFixtureTestService() + + fixture, err := fs.GetFixture(999) + + assert.Error(t, err, "expected an error when getting a non-existent fixture") + assert.Nil(t, fixture, "expected fixture to be nil for non-existent fixture") + assert.Equal(t, "fixture with ID 999 not found", err.Error()) + + t.Logf("Error encountered: %s", err.Error()) +} + +func TestGetGoalscorers(t *testing.T) { + fs := setupFixtureTestService() + fixture, err := fs.GetFixture(fixtureID) + + assert.NoError(t, err, "expected no error when getting fixture") + assert.NotNil(t, fixture, "expected fixture to be returned, got nil") + + goalscorers, err := fixture.GetGoalscorers() + assert.NoError(t, err, "expected no error when getting goalscorers") + t.Logf("Goalscorers: %+v", goalscorers) +} + +func TestGetAssisters(t *testing.T) { + fs := setupFixtureTestService() + fixture, err := fs.GetFixture(fixtureID) + + assert.NoError(t, err, "expected no error when getting fixture") + assert.NotNil(t, fixture, "expected fixture to be returned, got nil") + + assisters, err := fixture.GetAssisters() + assert.NoError(t, err, "expected no error when getting assisters") + t.Logf("Assisters: %+v", assisters) +} + +func TestGetBonus(t *testing.T) { + fs := setupFixtureTestService() + fixture, err := fs.GetFixture(fixtureID) + + assert.NoError(t, err, "expected no error when getting fixture") + assert.NotNil(t, fixture, "expected fixture to be returned, got nil") + + bonus, err := fixture.GetBonus() + assert.NoError(t, err, "expected no error when getting bonus points") + t.Logf("Bonus Points: %+v", bonus) +} diff --git a/endpoints/players_test.go b/endpoints/players_test.go index 71f4c18..38e0a80 100644 --- a/endpoints/players_test.go +++ b/endpoints/players_test.go @@ -10,7 +10,7 @@ import ( var playerID int -func setupTestService() *endpoints.PlayerService { +func setupPlayersTestService() *endpoints.PlayerService { c := client.NewClient() playerID = 328 // Example player ID for Mohamed Salah @@ -18,7 +18,7 @@ func setupTestService() *endpoints.PlayerService { } func TestGetAllPlayers(t *testing.T) { - ps := setupTestService() + ps := setupPlayersTestService() players, err := ps.GetAllPlayers() assert.NoError(t, err, "expected no error when getting all players") @@ -39,7 +39,7 @@ func TestGetAllPlayers(t *testing.T) { } func TestGetPlayer(t *testing.T) { - ps := setupTestService() + ps := setupPlayersTestService() player, err := ps.GetPlayer(playerID) @@ -57,7 +57,7 @@ func TestGetPlayer(t *testing.T) { } func TestGetPlayerHistory(t *testing.T) { - ps := setupTestService() + ps := setupPlayersTestService() history, err := ps.GetPlayerHistory(playerID) diff --git a/examples/fixtures/main.go b/examples/fixtures/main.go new file mode 100644 index 0000000..6ec8c5b --- /dev/null +++ b/examples/fixtures/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "log" + + "github.com/AbdoAnss/go-fantasy-pl/client" +) + +func main() { + c := client.NewClient() + + fixtureID := 8 // Example fixture ID + + // Get fixture details + fmt.Printf("Getting fixture details for ID %d...\n", fixtureID) + fixture, err := c.Fixtures.GetFixture(fixtureID) + if err != nil { + log.Printf("Warning: Could not get fixture details: %v\n", err) + return + } + + fmt.Println("----------------------------------------") + fmt.Printf("Fixture ID: %d\n", fixture.ID) + fmt.Printf("Team A: %d vs Team H: %d\n", fixture.TeamA, fixture.TeamH) + fmt.Printf("Kickoff Time: %v\n", fixture.KickoffTime) + fmt.Printf("Finished: %v\n", fixture.Finished) + fmt.Printf("Team A Score: %v, Team H Score: %v\n", fixture.GetTeamAScore(), fixture.GetTeamHScore()) + fmt.Println("----------------------------------------") + + // Get goalscorers + goalscorers, err := fixture.GetGoalscorers() + if err != nil { + log.Printf("Warning: Could not get goalscorers: %v\n", err) + } else { + fmt.Println("Goalscorers:") + for team, players := range goalscorers { + fmt.Printf("Team %s:\n", team) + for _, player := range players { + fmt.Printf("Player ID: %d, Goals: %d\n", player.Element, player.Value) + } + } + } + + // Get assisters + assisters, err := fixture.GetAssisters() + if err != nil { + log.Printf("Warning: Could not get assisters: %v\n", err) + } else { + fmt.Println("Assisters:") + for team, players := range assisters { + fmt.Printf("Team %s:\n", team) + for _, player := range players { + fmt.Printf("Player ID: %d, Assists: %d\n", player.Element, player.Value) + } + } + } + + // Get bonus points + bonus, err := fixture.GetBonus() + if err != nil { + log.Printf("Warning: Could not get bonus points: %v\n", err) + } else { + fmt.Println("Bonus Points:") + for team, players := range bonus { + fmt.Printf("Team %s:\n", team) + for _, player := range players { + fmt.Printf("Player ID: %d, Bonus Points: %d\n", player.Element, player.Value) + } + } + } +} diff --git a/examples/players/main.go b/examples/players/main.go index 9090a64..3143949 100644 --- a/examples/players/main.go +++ b/examples/players/main.go @@ -8,18 +8,14 @@ import ( ) func main() { - // Initialize client c := client.NewClient() playerID := 328 // Salah's ID - // Get player details first fmt.Printf("Getting player details for ID %d...\n", playerID) player, err := c.Players.GetPlayer(playerID) if err != nil { log.Printf("Warning: Could not get player details: %v\n", err) - // Continue execution even if player details fail } else { - // Print player details only if we got them fmt.Println("----------------------------------------") fmt.Printf("Mo Salah Details:\n") fmt.Printf("Full Name: %s\n", player.GetDisplayName()) diff --git a/models/fixture.go b/models/fixture.go index 2640e7f..ac07992 100644 --- a/models/fixture.go +++ b/models/fixture.go @@ -1 +1,152 @@ package models + +import ( + "time" +) + +type Stat struct { + Identifier string `json:"identifier"` + A []StatDetail `json:"a"` + H []StatDetail `json:"h"` +} + +type StatDetail struct { + Value int `json:"value"` + Element int `json:"element"` +} + +type Fixture struct { + Code int `json:"code"` + Event *int `json:"event"` + Finished bool `json:"finished"` + FinishedProvisional bool `json:"finished_provisional"` + ID int `json:"id"` + KickoffTime *time.Time `json:"kickoff_time"` + Minutes int `json:"minutes"` + ProvisionalStartTime bool `json:"provisional_start_time"` + Started *bool `json:"started"` + TeamA int `json:"team_a"` + TeamAScore *int `json:"team_a_score"` + TeamH int `json:"team_h"` + TeamHScore *int `json:"team_h_score"` + Stats []Stat `json:"stats"` + TeamHDifficulty int `json:"team_h_difficulty"` + TeamADifficulty int `json:"team_a_difficulty"` + PulseID int `json:"pulse_id"` +} + +func (f *Fixture) GetTeamAScore() int { + return *f.TeamAScore +} + +func (f *Fixture) GetTeamHScore() int { + return *f.TeamHScore +} + +func (f *Fixture) GetGoalscorers() (map[string][]StatDetail, error) { + goals := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "goals_scored" { + goals["a"] = stat.A + goals["h"] = stat.H + return goals, nil + } + } + return goals, nil +} + +func (f *Fixture) GetAssisters() (map[string][]StatDetail, error) { + assists := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "assists" { + assists["a"] = stat.A + assists["h"] = stat.H + return assists, nil + } + } + return assists, nil +} + +func (f *Fixture) GetOwnGoalscorers() (map[string][]StatDetail, error) { + ownGoals := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "own_goals" { + ownGoals["a"] = stat.A + ownGoals["h"] = stat.H + return ownGoals, nil + } + } + return ownGoals, nil +} + +func (f *Fixture) GetYellowCards() (map[string][]StatDetail, error) { + yellowCards := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "yellow_cards" { + yellowCards["a"] = stat.A + yellowCards["h"] = stat.H + return yellowCards, nil + } + } + return yellowCards, nil +} + +func (f *Fixture) GetRedCards() (map[string][]StatDetail, error) { + redCards := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "red_cards" { + redCards["a"] = stat.A + redCards["h"] = stat.H + return redCards, nil + } + } + return redCards, nil +} + +func (f *Fixture) GetPenaltySaves() (map[string][]StatDetail, error) { + penaltySaves := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "penalties_saved" { + penaltySaves["a"] = stat.A + penaltySaves["h"] = stat.H + return penaltySaves, nil + } + } + return penaltySaves, nil +} + +func (f *Fixture) GetPenaltyMisses() (map[string][]StatDetail, error) { + penaltyMisses := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "penalties_missed" { + penaltyMisses["a"] = stat.A + penaltyMisses["h"] = stat.H + return penaltyMisses, nil + } + } + return penaltyMisses, nil +} + +func (f *Fixture) GetSaves() (map[string][]StatDetail, error) { + saves := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "saves" { + saves["a"] = stat.A + saves["h"] = stat.H + return saves, nil + } + } + return saves, nil +} + +func (f *Fixture) GetBonus() (map[string][]StatDetail, error) { + bonus := make(map[string][]StatDetail) + for _, stat := range f.Stats { + if stat.Identifier == "bonus" { + bonus["a"] = stat.A + bonus["h"] = stat.H + return bonus, nil + } + } + return bonus, nil +} diff --git a/models/team.go b/models/team.go index 2640e7f..277d97a 100644 --- a/models/team.go +++ b/models/team.go @@ -1 +1,29 @@ package models + +type Team struct { + Code int `json:"code"` + Draw int `json:"draw"` + Form *string `json:"form"` // Using pointer to handle null values + ID int `json:"id"` + Loss int `json:"loss"` + Name string `json:"name"` + Played int `json:"played"` + Points int `json:"points"` + Position int `json:"position"` + ShortName string `json:"short_name"` + Strength int `json:"strength"` + TeamDivision *string `json:"team_division"` // Using pointer to handle null values + Unavailable bool `json:"unavailable"` + Win int `json:"win"` + StrengthOverallHome int `json:"strength_overall_home"` + StrengthOverallAway int `json:"strength_overall_away"` + StrengthAttackHome int `json:"strength_attack_home"` + StrengthAttackAway int `json:"strength_attack_away"` + StrengthDefenceHome int `json:"strength_defence_home"` + StrengthDefenceAway int `json:"strength_defence_away"` + PulseID int `json:"pulse_id"` +} + +func (t *Team) GetShortName() string { + return t.ShortName +}