feat: Rewrite to use the Diyanet API
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,203 +0,0 @@
|
||||
package dbtimesprovider
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
//go:embed schema.sql
|
||||
var schema string
|
||||
|
||||
//go:embed locations.jsonl
|
||||
var locationsJSON string
|
||||
|
||||
type Provider struct {
|
||||
db *goqu.Database
|
||||
provider prayer.TimesProvider
|
||||
clockFunc func() time.Time
|
||||
}
|
||||
|
||||
func New(db *goqu.Database, provider prayer.TimesProvider) Provider {
|
||||
return Provider{
|
||||
provider: provider,
|
||||
clockFunc: time.Now,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Provider) Name() string {
|
||||
return "db:" + p.provider.Name()
|
||||
}
|
||||
|
||||
func (p Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
|
||||
times, err := p.loadTimes(ctx, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load prayer times from db: %w", err)
|
||||
}
|
||||
if len(times) > 0 {
|
||||
return times, nil
|
||||
}
|
||||
|
||||
times, err = p.provider.Get(ctx, location)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times: %w", err)
|
||||
}
|
||||
|
||||
if len(times) > 0 {
|
||||
if err := p.saveTimes(ctx, location, times); err != nil {
|
||||
return nil, fmt.Errorf("failed to save times to db: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return times, nil
|
||||
}
|
||||
|
||||
func Migrate(con *sql.DB) error {
|
||||
db := goqu.New("sqlite3", con)
|
||||
if _, err := db.Exec(schema); err != nil {
|
||||
return fmt.Errorf("failed to migrate: %w", err)
|
||||
}
|
||||
|
||||
count, _ := db.From("locations").Count()
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
ID int `json:"id" db:"id"`
|
||||
Country string `json:"country" db:"country"`
|
||||
Region string `json:"region" db:"region"`
|
||||
City string `json:"city" db:"city"`
|
||||
}
|
||||
s := bufio.NewScanner(strings.NewReader(locationsJSON))
|
||||
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to begin tx: %w", err)
|
||||
}
|
||||
if err := tx.Wrap(func() error {
|
||||
for s.Scan() {
|
||||
var e entry
|
||||
if err := json.Unmarshal(s.Bytes(), &e); err != nil {
|
||||
return fmt.Errorf("failed to parse as json: %w", err)
|
||||
}
|
||||
|
||||
q := tx.Insert("locations").
|
||||
OnConflict(goqu.DoNothing()).
|
||||
Rows(e)
|
||||
|
||||
if _, err := q.Executor().Exec(); err != nil {
|
||||
return fmt.Errorf("failed to insert location: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type prayerTimesRow struct {
|
||||
ProviderID int64 `db:"provider_id"`
|
||||
LocationID string `db:"location_id"`
|
||||
Date string `db:"date"`
|
||||
Fajr string `db:"fajr"`
|
||||
Sunrise string `db:"sunrise"`
|
||||
Dhuhr string `db:"dhuhr"`
|
||||
Asr string `db:"asr"`
|
||||
Maghrib string `db:"maghrib"`
|
||||
Isha string `db:"isha"`
|
||||
}
|
||||
|
||||
func (r prayerTimesRow) toDomain() prayer.Times {
|
||||
return prayer.Times{
|
||||
Date: r.Date,
|
||||
Fajr: r.Fajr,
|
||||
Sunrise: r.Sunrise,
|
||||
Dhuhr: r.Dhuhr,
|
||||
Asr: r.Asr,
|
||||
Maghrib: r.Maghrib,
|
||||
Isha: r.Isha,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Provider) saveTimes(ctx context.Context, locationID string, times []prayer.Times) error {
|
||||
providerID, err := p.saveProvider(ctx, p.provider.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows := lo.Map(times, func(item prayer.Times, _ int) prayerTimesRow {
|
||||
return prayerTimesRow{
|
||||
ProviderID: providerID,
|
||||
LocationID: locationID,
|
||||
Date: item.Date,
|
||||
Fajr: item.Fajr,
|
||||
Sunrise: item.Sunrise,
|
||||
Dhuhr: item.Dhuhr,
|
||||
Asr: item.Asr,
|
||||
Maghrib: item.Maghrib,
|
||||
Isha: item.Isha,
|
||||
}
|
||||
})
|
||||
q := p.db.
|
||||
Insert("prayer_times").
|
||||
OnConflict(goqu.DoNothing()).
|
||||
Rows(rows)
|
||||
|
||||
if _, err := q.Executor().ExecContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to save times: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Provider) loadTimes(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||
now := p.clockFunc()
|
||||
today := now.UTC().Truncate(time.Hour * 24)
|
||||
|
||||
q := p.db.
|
||||
From(goqu.T("prayer_times").As("pt")).
|
||||
Join(goqu.T("providers").As("p"), goqu.On(goqu.I("p.id").Eq(goqu.I("pt.provider_id")))).
|
||||
Where(
|
||||
goqu.I("p.name").Eq(p.provider.Name()),
|
||||
goqu.I("pt.location_id").Eq(locationID),
|
||||
goqu.I("pt.date").Gte(today.Format(time.DateOnly)),
|
||||
).
|
||||
Limit(100)
|
||||
|
||||
var rows []prayerTimesRow
|
||||
if err := q.ScanStructsContext(ctx, &rows); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan times: %w", err)
|
||||
}
|
||||
|
||||
return lo.Map(rows, func(row prayerTimesRow, _ int) prayer.Times {
|
||||
return row.toDomain()
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (p Provider) saveProvider(ctx context.Context, name string) (int64, error) {
|
||||
q := p.db.Insert("providers").
|
||||
OnConflict(goqu.DoUpdate("name", goqu.Record{"name": name})).
|
||||
Rows(goqu.Record{"name": name}).
|
||||
Returning("id")
|
||||
var id int64
|
||||
_, err := q.Executor().ScanValContext(ctx, &id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to insert provider: %w", err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package dbtimesprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"prayertimes/internal/database"
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
func testDB(t *testing.T) *goqu.Database {
|
||||
t.Helper()
|
||||
|
||||
db, err := database.NewSqliteDB(":memory:")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = Migrate(db.Db.(*sql.DB))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Insert("locations").Rows(goqu.Record{"id": 1}).Executor().Exec()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
db.Db.(*sql.DB).Close()
|
||||
})
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
type mockProvider func() ([]prayer.Times, error)
|
||||
|
||||
func (m mockProvider) Get(ctx context.Context, location string) ([]prayer.Times, error) { return m() }
|
||||
|
||||
func (m mockProvider) Name() string { return "mock" }
|
||||
|
||||
func TestProvider_Get(t *testing.T) {
|
||||
then := time.Date(2023, 3, 5, 0, 0, 0, 0, time.UTC)
|
||||
tests := []struct {
|
||||
name string
|
||||
setupDB func(t *testing.T, db *goqu.Database)
|
||||
provider prayer.TimesProvider
|
||||
clock time.Time
|
||||
assertRes func(t *testing.T, db *goqu.Database, times []prayer.Times, err error)
|
||||
}{
|
||||
{
|
||||
name: "provider succeeds, empty db",
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return []prayer.Times{
|
||||
{Date: "2023-03-04"},
|
||||
{Date: "2023-03-05"},
|
||||
}, nil
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 2)
|
||||
|
||||
cnt, err := db.From("prayer_times").Count()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(2), cnt)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider fails, empty db",
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return nil, fmt.Errorf("no")
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, times)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "provider fails, populated db",
|
||||
setupDB: func(t *testing.T, db *goqu.Database) {
|
||||
_, err := db.Insert("providers").Rows(goqu.Record{"id": 1, "name": "mock"}).Executor().Exec()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = db.Insert("prayer_times").Rows(
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-04", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-05", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
prayerTimesRow{ProviderID: 1, LocationID: "1", Date: "2023-03-06", Fajr: "01:00", Sunrise: "02:00", Dhuhr: "03:00", Asr: "04:00", Maghrib: "05:00", Isha: "06:00"},
|
||||
).Executor().Exec()
|
||||
require.NoError(t, err)
|
||||
},
|
||||
provider: mockProvider(func() ([]prayer.Times, error) {
|
||||
return nil, fmt.Errorf("no")
|
||||
}),
|
||||
clock: then,
|
||||
assertRes: func(t *testing.T, db *goqu.Database, times []prayer.Times, err error) {
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, times, 2)
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
db := testDB(t)
|
||||
p := Provider{
|
||||
db: db,
|
||||
provider: tt.provider,
|
||||
clockFunc: func() time.Time { return tt.clock },
|
||||
}
|
||||
if tt.setupDB != nil {
|
||||
tt.setupDB(t, db)
|
||||
}
|
||||
|
||||
actual, err := p.Get(context.Background(), "1")
|
||||
tt.assertRes(t, db, actual, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
CREATE TABLE IF NOT EXISTS locations
|
||||
(
|
||||
id text PRIMARY KEY,
|
||||
country text,
|
||||
city text,
|
||||
region text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS providers
|
||||
(
|
||||
id integer PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS prayer_times
|
||||
(
|
||||
provider_id integer NOT NULL REFERENCES providers (id),
|
||||
location_id text NOT NULL REFERENCES locations (id),
|
||||
date datetime NOT NULL,
|
||||
fajr text NOT NULL,
|
||||
sunrise text NOT NULL,
|
||||
dhuhr text NOT NULL,
|
||||
asr text NOT NULL,
|
||||
maghrib text NOT NULL,
|
||||
isha text NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS prayer_times__provider__location ON prayer_times (provider_id, location_id, date);
|
||||
Reference in New Issue
Block a user