summaryrefslogtreecommitdiff
path: root/rides
diff options
context:
space:
mode:
Diffstat (limited to 'rides')
-rw-r--r--rides/rides.go104
-rw-r--r--rides/rides_test.go104
2 files changed, 208 insertions, 0 deletions
diff --git a/rides/rides.go b/rides/rides.go
new file mode 100644
index 0000000..1958d20
--- /dev/null
+++ b/rides/rides.go
@@ -0,0 +1,104 @@
+package rides
+
+import (
+ "database/sql"
+ "time"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+)
+
+type Ride struct {
+ ID int64
+ ExternalID string
+ GPXPath string
+ Name string
+ Type string
+ StartDate time.Time
+ DistanceM float64
+ MovingTimeS int64
+ ElapsedTimeS int64
+ TotalElevationGainM float64
+ AverageSpeedMps float64
+ CreatedAt time.Time
+ UpdatedAt time.Time
+}
+
+const columns = "id, external_id, gpx_path, name, type, start_date, distance_m, moving_time_s, elapsed_time_s, total_elevation_gain_m, average_speed_mps, created_at, updated_at"
+
+func Save(db *sql.DB, r Ride) error {
+ _, err := db.Exec(`
+ INSERT INTO rides (external_id, gpx_path, name, type, start_date, distance_m, moving_time_s, elapsed_time_s, total_elevation_gain_m, average_speed_mps)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(external_id) DO UPDATE SET
+ gpx_path = excluded.gpx_path,
+ name = excluded.name,
+ type = excluded.type,
+ start_date = excluded.start_date,
+ distance_m = excluded.distance_m,
+ moving_time_s = excluded.moving_time_s,
+ elapsed_time_s = excluded.elapsed_time_s,
+ total_elevation_gain_m = excluded.total_elevation_gain_m,
+ average_speed_mps = excluded.average_speed_mps,
+ updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
+ `, r.ExternalID, r.GPXPath, r.Name, r.Type, r.StartDate.UTC().Format(time.RFC3339), r.DistanceM, r.MovingTimeS, r.ElapsedTimeS, r.TotalElevationGainM, r.AverageSpeedMps)
+ return err
+}
+
+func List(db *sql.DB) ([]Ride, error) {
+ rows, err := db.Query("SELECT " + columns + " FROM rides ORDER BY start_date DESC")
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var rides []Ride
+ for rows.Next() {
+ ride, err := scanRide(rows)
+ if err != nil {
+ return nil, err
+ }
+ rides = append(rides, ride)
+ }
+ return rides, rows.Err()
+}
+
+func GetByExternalID(db *sql.DB, externalID string) (Ride, bool, error) {
+ row := db.QueryRow("SELECT "+columns+" FROM rides WHERE external_id = ?", externalID)
+ ride, err := scanRide(row)
+ if err == sql.ErrNoRows {
+ return Ride{}, false, nil
+ }
+ if err != nil {
+ return Ride{}, false, err
+ }
+ return ride, true, nil
+}
+
+type scanner interface {
+ Scan(dest ...any) error
+}
+
+func scanRide(s scanner) (Ride, error) {
+ var (
+ ride Ride
+ startDate string
+ createdAt string
+ updatedAt string
+ )
+ err := s.Scan(&ride.ID, &ride.ExternalID, &ride.GPXPath, &ride.Name, &ride.Type, &startDate, &ride.DistanceM, &ride.MovingTimeS, &ride.ElapsedTimeS, &ride.TotalElevationGainM, &ride.AverageSpeedMps, &createdAt, &updatedAt)
+ if err != nil {
+ return Ride{}, err
+ }
+ ride.StartDate, err = time.Parse(time.RFC3339, startDate)
+ if err != nil {
+ return Ride{}, kcore.Wrap(err, "invalid start_date in rides row")
+ }
+ ride.CreatedAt, err = time.Parse(time.RFC3339, createdAt)
+ if err != nil {
+ return Ride{}, kcore.Wrap(err, "invalid created_at in rides row")
+ }
+ ride.UpdatedAt, err = time.Parse(time.RFC3339, updatedAt)
+ if err != nil {
+ return Ride{}, kcore.Wrap(err, "invalid updated_at in rides row")
+ }
+ return ride, nil
+}
diff --git a/rides/rides_test.go b/rides/rides_test.go
new file mode 100644
index 0000000..1ba23f9
--- /dev/null
+++ b/rides/rides_test.go
@@ -0,0 +1,104 @@
+package rides
+
+import (
+ "database/sql"
+ "testing"
+ "time"
+
+ "github.com/martinlehoux/kagamigo/kcore"
+ _ "github.com/mattn/go-sqlite3"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func newTestDB(t *testing.T) *sql.DB {
+ t.Helper()
+ db, err := sql.Open("sqlite3", ":memory:")
+ require.NoError(t, err)
+ t.Cleanup(func() { db.Close() })
+ _, err = db.Exec(`
+ create table rides (
+ id integer primary key,
+ external_id text unique not null,
+ gpx_path text not null,
+ name text not null,
+ type text not null,
+ start_date text not null,
+ distance_m real not null,
+ moving_time_s integer not null,
+ elapsed_time_s integer not null,
+ total_elevation_gain_m real not null,
+ average_speed_mps real not null,
+ created_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
+ updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+ )
+ `)
+ require.NoError(t, err)
+ return db
+}
+
+func sampleRide() Ride {
+ return Ride{
+ ExternalID: "strava:1234",
+ GPXPath: "rides/activity_1234.gpx",
+ Name: "Morning Ride",
+ Type: "Ride",
+ StartDate: time.Date(2026, 8, 1, 7, 30, 0, 0, time.UTC),
+ DistanceM: 42_195,
+ MovingTimeS: 7_200,
+ ElapsedTimeS: 7_800,
+ TotalElevationGainM: 850,
+ AverageSpeedMps: 5.86,
+ }
+}
+
+func TestUpsertAndGet(t *testing.T) {
+ db := newTestDB(t)
+ err := Save(db, sampleRide())
+ require.NoError(t, err)
+
+ got, ok, err := GetByExternalID(db, "strava:1234")
+ require.NoError(t, err)
+ require.True(t, ok)
+ assert.Equal(t, "Morning Ride", got.Name)
+ assert.Equal(t, sampleRide().StartDate, got.StartDate)
+ assert.Equal(t, 42_195.0, got.DistanceM)
+ assert.Equal(t, "rides/activity_1234.gpx", got.GPXPath)
+
+ _, ok, err = GetByExternalID(db, "strava:9999")
+ require.NoError(t, err)
+ assert.False(t, ok)
+}
+
+func TestUpsertUpdatesExisting(t *testing.T) {
+ db := newTestDB(t)
+ ride := sampleRide()
+ require.NoError(t, Save(db, ride))
+ ride.Name = "Renamed Ride"
+ ride.DistanceM = 50_000
+ require.NoError(t, Save(db, ride))
+
+ got, ok, err := GetByExternalID(db, "strava:1234")
+ require.NoError(t, err)
+ require.True(t, ok)
+ assert.Equal(t, "Renamed Ride", got.Name)
+ assert.Equal(t, 50_000.0, got.DistanceM)
+}
+
+func TestList(t *testing.T) {
+ db := newTestDB(t)
+ first := sampleRide()
+ first.StartDate = time.Date(2026, 7, 1, 7, 0, 0, 0, time.UTC)
+ second := sampleRide()
+ second.ExternalID = "strava:5678"
+ second.Name = "Evening Ride"
+ require.NoError(t, Save(db, first))
+ require.NoError(t, Save(db, second))
+
+ rides, err := List(db)
+ require.NoError(t, err)
+ require.Len(t, rides, 2)
+ assert.Equal(t, "strava:5678", rides[0].ExternalID)
+ assert.Equal(t, "strava:1234", rides[1].ExternalID)
+ kcore.Assert(len(rides) == 2, "two rides")
+}