mirror of
https://github.com/AmanTahiliani/box-box.git
synced 2026-08-07 19:56:18 -04:00
Open F1 API calls
This commit is contained in:
1
cmd/main.go
Normal file
1
cmd/main.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package cmd
|
||||||
18
internal/api/client.go
Normal file
18
internal/api/client.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpenF1Client struct {
|
||||||
|
url string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOpenF1Client(url string, timeout time.Duration) *OpenF1Client {
|
||||||
|
return &OpenF1Client{
|
||||||
|
url: url,
|
||||||
|
httpClient: &http.Client{Timeout: timeout},
|
||||||
|
}
|
||||||
|
}
|
||||||
343
internal/api/openf1.go
Normal file
343
internal/api/openf1.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/AmanTahiliani/box-box/internal/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// get performs a GET request and returns the response body, or an error if the
|
||||||
|
// status code is not 200 OK.
|
||||||
|
func (c *OpenF1Client) get(url string) (io.ReadCloser, error) {
|
||||||
|
resp, err := c.httpClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
return nil, fmt.Errorf("openf1 API returned status %d for %s", resp.StatusCode, url)
|
||||||
|
}
|
||||||
|
return resp.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetMeetingsForYear(year int) ([]models.Meeting, error) {
|
||||||
|
if year < 2023 {
|
||||||
|
return nil, errors.New("Invalid year: " + strconv.Itoa(year) + ". Year must be 2023 or later.")
|
||||||
|
}
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/meetings?year=%d", c.url, year))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Meeting
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetSessionsForMeeting(meetingKey int) ([]models.Session, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/sessions?meeting_key=%d", c.url, meetingKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Session
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetDriversForSession(sessionKey int) ([]models.Driver, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/drivers?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Driver
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetDriver(sessionKey, driverNumber int) (*models.Driver, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/drivers?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Driver
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &result[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetDriverChampionship(sessionKey int) ([]models.ChampionshipDriver, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/championship_drivers?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.ChampionshipDriver
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetTeamChampionship(sessionKey int) ([]models.ChampionshipTeam, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/championship_teams?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.ChampionshipTeam
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestDriverChampionship returns championship standings for the most recent
|
||||||
|
// race session. Note: requires the caller to supply a recent race session_key;
|
||||||
|
// the OpenF1 API does not support a "latest" shortcut for this endpoint.
|
||||||
|
func (c *OpenF1Client) GetLatestDriverChampionship() ([]models.ChampionshipDriver, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/championship_drivers?session_key=latest", c.url))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.ChampionshipDriver
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetLatestTeamChampionship() ([]models.ChampionshipTeam, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/championship_teams?session_key=latest", c.url))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.ChampionshipTeam
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetSessionResult(sessionKey int) ([]models.SessionResult, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/session_result?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.SessionResult
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetStartingGrid(sessionKey int) ([]models.StartingGrid, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/starting_grid?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.StartingGrid
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetLapsForSession(sessionKey int) ([]models.Lap, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/laps?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Lap
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetLapsForDriver(sessionKey, driverNumber int) ([]models.Lap, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/laps?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Lap
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetStintsForSession(sessionKey int) ([]models.Stint, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/stints?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Stint
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetPitStopsForSession(sessionKey int) ([]models.Pit, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/pit?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Pit
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetOvertakesForSession(sessionKey int) ([]models.Overtake, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/overtakes?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Overtake
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetPositions(sessionKey, driverNumber int) ([]models.Position, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/position?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Position
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetIntervals(sessionKey, driverNumber int) ([]models.Interval, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/intervals?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Interval
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetRaceControl(sessionKey int) ([]models.RaceControl, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/race_control?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.RaceControl
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetWeather(sessionKey int) ([]models.Weather, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/weather?session_key=%d", c.url, sessionKey))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Weather
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetCarData(sessionKey, driverNumber int) ([]models.CarData, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/car_data?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.CarData
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetLocation(sessionKey, driverNumber int) ([]models.Location, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/location?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.Location
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *OpenF1Client) GetTeamRadio(sessionKey, driverNumber int) ([]models.TeamRadio, error) {
|
||||||
|
body, err := c.get(fmt.Sprintf("%s/v1/team_radio?session_key=%d&driver_number=%d", c.url, sessionKey, driverNumber))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer body.Close()
|
||||||
|
|
||||||
|
var result []models.TeamRadio
|
||||||
|
if err := json.NewDecoder(body).Decode(&result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
211
internal/api/openf1_test.go
Normal file
211
internal/api/openf1_test.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testBaseURL = "https://api.openf1.org"
|
||||||
|
testTimeout = 10 * time.Second
|
||||||
|
abuDhabiRace = 9662 // 2024 Abu Dhabi Grand Prix Race session
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestClient() *OpenF1Client {
|
||||||
|
return NewOpenF1Client(testBaseURL, testTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipOnRateLimit skips the test if err indicates the API rate-limited us (HTTP 429).
|
||||||
|
func skipOnRateLimit(t *testing.T, err error) {
|
||||||
|
t.Helper()
|
||||||
|
if err != nil && strings.Contains(err.Error(), "status 429") {
|
||||||
|
t.Skip("skipping: OpenF1 API rate-limited this test run")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMeetingsForYear(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
meetings, err := client.GetMeetingsForYear(2024)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(meetings) == 0 {
|
||||||
|
t.Errorf("Expected meetings for 2024, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, meeting := range meetings {
|
||||||
|
if meeting.Year != 2024 {
|
||||||
|
t.Errorf("Expected meeting year to be 2024, got %d", meeting.Year)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMeetingsForYearInvalid(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
_, err := client.GetMeetingsForYear(-1)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error for invalid year, got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSessionsForMeeting(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
sessions, err := client.GetSessionsForMeeting(1304)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(sessions) == 0 {
|
||||||
|
t.Errorf("Expected sessions for meeting key 1304, got none")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, session := range sessions {
|
||||||
|
if session.MeetingKey != 1304 {
|
||||||
|
t.Errorf("Expected session meeting key to be 1304, got %d", session.MeetingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDriversForSession(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
drivers, err := client.GetDriversForSession(abuDhabiRace)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(drivers) == 0 {
|
||||||
|
t.Errorf("Expected drivers for session %d, got none", abuDhabiRace)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, d := range drivers {
|
||||||
|
if d.SessionKey != abuDhabiRace {
|
||||||
|
t.Errorf("Expected session_key %d, got %d", abuDhabiRace, d.SessionKey)
|
||||||
|
}
|
||||||
|
if d.DriverNumber == 0 {
|
||||||
|
t.Errorf("Expected non-zero driver_number")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetSessionResult(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
results, err := client.GetSessionResult(abuDhabiRace)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
t.Errorf("Expected results for session %d, got none", abuDhabiRace)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFinisher := false
|
||||||
|
for _, r := range results {
|
||||||
|
if r.SessionKey != abuDhabiRace {
|
||||||
|
t.Errorf("Expected session_key %d, got %d", abuDhabiRace, r.SessionKey)
|
||||||
|
}
|
||||||
|
if r.Position == 1 {
|
||||||
|
foundFinisher = true
|
||||||
|
// Winner should not be DNF/DNS/DSQ
|
||||||
|
if r.DNF || r.DNS || r.DSQ {
|
||||||
|
t.Errorf("Race winner (position 1) should not have DNF/DNS/DSQ set")
|
||||||
|
}
|
||||||
|
// Winner should have points
|
||||||
|
if r.Points <= 0 {
|
||||||
|
t.Errorf("Race winner should have positive points, got %f", r.Points)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundFinisher {
|
||||||
|
t.Errorf("Expected at least one driver in position 1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetStintsForSession(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
stints, err := client.GetStintsForSession(abuDhabiRace)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stints) == 0 {
|
||||||
|
t.Errorf("Expected stints for session %d, got none", abuDhabiRace)
|
||||||
|
}
|
||||||
|
|
||||||
|
validCompounds := map[string]bool{
|
||||||
|
"SOFT": true, "MEDIUM": true, "HARD": true,
|
||||||
|
"INTERMEDIATE": true, "WET": true, "UNKNOWN": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range stints {
|
||||||
|
if s.SessionKey != abuDhabiRace {
|
||||||
|
t.Errorf("Expected session_key %d, got %d", abuDhabiRace, s.SessionKey)
|
||||||
|
}
|
||||||
|
if !validCompounds[string(s.Compound)] {
|
||||||
|
t.Errorf("Unexpected tyre compound: %q", s.Compound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDriverChampionship(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
standings, err := client.GetDriverChampionship(abuDhabiRace)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(standings) == 0 {
|
||||||
|
t.Errorf("Expected driver championship standings for session %d, got none", abuDhabiRace)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range standings {
|
||||||
|
if s.DriverNumber == 0 {
|
||||||
|
t.Errorf("Expected non-zero driver_number in championship standings")
|
||||||
|
}
|
||||||
|
if s.PositionCurrent == 0 {
|
||||||
|
t.Errorf("Expected non-zero position_current in championship standings")
|
||||||
|
}
|
||||||
|
if s.PointsCurrent < 0 {
|
||||||
|
t.Errorf("Expected non-negative points_current, got %f", s.PointsCurrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRaceControl(t *testing.T) {
|
||||||
|
client := newTestClient()
|
||||||
|
|
||||||
|
messages, err := client.GetRaceControl(abuDhabiRace)
|
||||||
|
skipOnRateLimit(t, err)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(messages) == 0 {
|
||||||
|
t.Errorf("Expected race control messages for session %d, got none", abuDhabiRace)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range messages {
|
||||||
|
if m.SessionKey != abuDhabiRace {
|
||||||
|
t.Errorf("Expected session_key %d, got %d", abuDhabiRace, m.SessionKey)
|
||||||
|
}
|
||||||
|
if m.Message == "" {
|
||||||
|
t.Errorf("Expected non-empty message in race control entry")
|
||||||
|
}
|
||||||
|
// DriverNumber is a pointer — just verify it doesn't panic (nil is valid)
|
||||||
|
_ = m.DriverNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
259
internal/models/types.go
Normal file
259
internal/models/types.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
type Circuit struct {
|
||||||
|
CircuitKey int `json:"circuit_key"`
|
||||||
|
CircuitShortName string `json:"circuit_short_name"`
|
||||||
|
CircuitType string `json:"circuit_type"`
|
||||||
|
CircuitInfoURL string `json:"circuit_info_url"`
|
||||||
|
CircuitImage string `json:"circuit_image"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Meeting struct {
|
||||||
|
MeetingKey int32 `json:"meeting_key"`
|
||||||
|
MeetingName string `json:"meeting_name"`
|
||||||
|
MeetingOfficialName string `json:"meeting_official_name"`
|
||||||
|
Location string `json:"location"`
|
||||||
|
CountryKey int `json:"country_key"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
CountryName string `json:"country_name"`
|
||||||
|
CountryFlag string `json:"country_flag"`
|
||||||
|
|
||||||
|
Circuit
|
||||||
|
|
||||||
|
GMTOffset string `json:"gmt_offset"`
|
||||||
|
DateStart string `json:"date_start"`
|
||||||
|
DateEnd string `json:"date_end"`
|
||||||
|
Year int `json:"year"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
SessionName string `json:"session_name"`
|
||||||
|
SessionType string `json:"session_type"`
|
||||||
|
|
||||||
|
CircuitKey int `json:"circuit_key"`
|
||||||
|
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
|
||||||
|
DateStart string `json:"date_start"`
|
||||||
|
DateEnd string `json:"date_end"`
|
||||||
|
GMTOffset string `json:"gmt_offset"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TyreCompound represents the type of tyre compound used.
|
||||||
|
type TyreCompound string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CompoundSoft TyreCompound = "SOFT"
|
||||||
|
CompoundMedium TyreCompound = "MEDIUM"
|
||||||
|
CompoundHard TyreCompound = "HARD"
|
||||||
|
CompoundIntermediate TyreCompound = "INTERMEDIATE"
|
||||||
|
CompoundWet TyreCompound = "WET"
|
||||||
|
CompoundUnknown TyreCompound = "UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Flag represents race control flag colours/types.
|
||||||
|
type Flag string
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagGreen Flag = "GREEN"
|
||||||
|
FlagYellow Flag = "YELLOW"
|
||||||
|
FlagDoubleYellow Flag = "DOUBLE YELLOW"
|
||||||
|
FlagRed Flag = "RED"
|
||||||
|
FlagBlue Flag = "BLUE"
|
||||||
|
FlagChequered Flag = "CHEQUERED"
|
||||||
|
FlagBlack Flag = "BLACK"
|
||||||
|
FlagBlackOrange Flag = "BLACK AND ORANGE"
|
||||||
|
FlagBlackWhite Flag = "BLACK AND WHITE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RaceControlCategory classifies race control messages.
|
||||||
|
type RaceControlCategory string
|
||||||
|
|
||||||
|
const (
|
||||||
|
CategoryFlag RaceControlCategory = "Flag"
|
||||||
|
CategorySafetyCar RaceControlCategory = "SafetyCar"
|
||||||
|
CategoryDRS RaceControlCategory = "Drs"
|
||||||
|
CategoryOther RaceControlCategory = "Other"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Driver struct {
|
||||||
|
BroadcastName string `json:"broadcast_name"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
FirstName string `json:"first_name"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
HeadshotURL string `json:"headshot_url"`
|
||||||
|
LastName string `json:"last_name"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
NameAcronym string `json:"name_acronym"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
TeamColour string `json:"team_colour"` // hex RRGGBB
|
||||||
|
TeamName string `json:"team_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChampionshipDriver struct {
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
PointsCurrent float64 `json:"points_current"`
|
||||||
|
PointsStart float64 `json:"points_start"`
|
||||||
|
PositionCurrent int `json:"position_current"`
|
||||||
|
PositionStart int `json:"position_start"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChampionshipTeam struct {
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
PointsCurrent float64 `json:"points_current"`
|
||||||
|
PointsStart float64 `json:"points_start"`
|
||||||
|
PositionCurrent int `json:"position_current"`
|
||||||
|
PositionStart int `json:"position_start"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
TeamName string `json:"team_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SessionResult struct {
|
||||||
|
DNF bool `json:"dnf"`
|
||||||
|
DNS bool `json:"dns"`
|
||||||
|
DSQ bool `json:"dsq"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
Duration interface{} `json:"duration"` // float64 or []float64 (qualifying)
|
||||||
|
GapToLeader interface{} `json:"gap_to_leader"` // float64, string "+N LAP(S)", or []float64
|
||||||
|
NumberOfLaps int `json:"number_of_laps"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
Points float64 `json:"points"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StartingGrid struct {
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
LapDuration float64 `json:"lap_duration"` // qualifying lap time in seconds
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Lap struct {
|
||||||
|
DateStart string `json:"date_start"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
DurationSector1 *float64 `json:"duration_sector_1"`
|
||||||
|
DurationSector2 *float64 `json:"duration_sector_2"`
|
||||||
|
DurationSector3 *float64 `json:"duration_sector_3"`
|
||||||
|
I1Speed int `json:"i1_speed"`
|
||||||
|
I2Speed int `json:"i2_speed"`
|
||||||
|
IsPitOutLap bool `json:"is_pit_out_lap"`
|
||||||
|
LapDuration *float64 `json:"lap_duration"`
|
||||||
|
LapNumber int `json:"lap_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
SegmentsSector1 []int `json:"segments_sector_1"`
|
||||||
|
SegmentsSector2 []int `json:"segments_sector_2"`
|
||||||
|
SegmentsSector3 []int `json:"segments_sector_3"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
StSpeed int `json:"st_speed"` // speed trap
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stint struct {
|
||||||
|
Compound TyreCompound `json:"compound"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
LapEnd int `json:"lap_end"`
|
||||||
|
LapStart int `json:"lap_start"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
StintNumber int `json:"stint_number"`
|
||||||
|
TyreAgeAtStart int `json:"tyre_age_at_start"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pit struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
LaneDuration float64 `json:"lane_duration"` // pit lane time (entry to exit)
|
||||||
|
LapNumber int `json:"lap_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
PitDuration float64 `json:"pit_duration"` // deprecated, use StopDuration
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
StopDuration float64 `json:"stop_duration"` // stationary time only
|
||||||
|
}
|
||||||
|
|
||||||
|
type Position struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interval struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
GapToLeader *float64 `json:"gap_to_leader"` // null when leading
|
||||||
|
Interval *float64 `json:"interval"` // null when leading
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Overtake struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
OvertakenDriverNumber int `json:"overtaken_driver_number"`
|
||||||
|
OvertakingDriverNumber int `json:"overtaking_driver_number"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RaceControl struct {
|
||||||
|
Category RaceControlCategory `json:"category"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber *int `json:"driver_number"` // nil = all drivers
|
||||||
|
Flag Flag `json:"flag"`
|
||||||
|
LapNumber *int `json:"lap_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
QualifyingPhase *int `json:"qualifying_phase"` // 1-3, nil if N/A
|
||||||
|
Scope string `json:"scope"` // "Track", "Driver", "Sector"
|
||||||
|
Sector *int `json:"sector"` // nil if track-wide
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Weather struct {
|
||||||
|
AirTemperature float64 `json:"air_temperature"`
|
||||||
|
Date string `json:"date"`
|
||||||
|
Humidity float64 `json:"humidity"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
Pressure float64 `json:"pressure"`
|
||||||
|
Rainfall int `json:"rainfall"` // 0 = dry, 1 = rain
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
TrackTemperature float64 `json:"track_temperature"`
|
||||||
|
WindDirection int `json:"wind_direction"` // 0-359 degrees
|
||||||
|
WindSpeed float64 `json:"wind_speed"` // m/s
|
||||||
|
}
|
||||||
|
|
||||||
|
type CarData struct {
|
||||||
|
Brake int `json:"brake"` // 0-100
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
DRS int `json:"drs"` // 0=off, 8=eligible, 10=open
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
NGear int `json:"n_gear"` // 0-8
|
||||||
|
RPM int `json:"rpm"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
Speed int `json:"speed"` // km/h
|
||||||
|
Throttle int `json:"throttle"` // 0-100
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
X float64 `json:"x"`
|
||||||
|
Y float64 `json:"y"`
|
||||||
|
Z float64 `json:"z"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeamRadio struct {
|
||||||
|
Date string `json:"date"`
|
||||||
|
DriverNumber int `json:"driver_number"`
|
||||||
|
MeetingKey int `json:"meeting_key"`
|
||||||
|
RecordingURL string `json:"recording_url"`
|
||||||
|
SessionKey int `json:"session_key"`
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user