feat: Rewrite to use the Diyanet API

This commit is contained in:
2026-02-21 01:50:59 +03:00
parent 43d4560fbb
commit 73316d4e62
17 changed files with 637 additions and 9198 deletions
File diff suppressed because it is too large Load Diff
-203
View File
@@ -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
}
-122
View File
@@ -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)
})
}
}
-28
View File
@@ -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);