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);
|
||||
@@ -1,72 +0,0 @@
|
||||
package diyanet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
FetchParsed(ctx context.Context, url string) (*goquery.Document, error)
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
FetcherFunc func(ctx context.Context, url string) (*goquery.Document, error)
|
||||
}
|
||||
|
||||
func New(fetcherFunc func(ctx context.Context, url string) (*goquery.Document, error)) *Provider {
|
||||
return &Provider{FetcherFunc: fetcherFunc}
|
||||
}
|
||||
|
||||
var reNumeric = regexp.MustCompile(`\d+`)
|
||||
|
||||
func validateLocation(location string) error {
|
||||
if !reNumeric.MatchString(location) {
|
||||
return fmt.Errorf("invalid location id")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Provider) Get(ctx context.Context, location string) ([]prayer.Times, error) {
|
||||
if err := validateLocation(location); err != nil {
|
||||
return nil, fmt.Errorf("%w: %v", prayer.ErrInvalidLocation, err)
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("https://namazvakitleri.diyanet.gov.tr/en-US/%s", location)
|
||||
doc, err := d.FetcherFunc(ctx, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch location %q: %w", location, err)
|
||||
}
|
||||
|
||||
var times []prayer.Times
|
||||
|
||||
doc.Find("#tab-1 .vakit-table tbody tr").Each(func(_ int, el *goquery.Selection) {
|
||||
date := el.Find("td:first-of-type").Text()
|
||||
parsedDate, err := time.Parse("02.01.2006", date)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
row := prayer.Times{
|
||||
Date: parsedDate.Format(time.DateOnly),
|
||||
Fajr: el.Find("td:nth-of-type(3)").Text(),
|
||||
Sunrise: el.Find("td:nth-of-type(4)").Text(),
|
||||
Dhuhr: el.Find("td:nth-of-type(5)").Text(),
|
||||
Asr: el.Find("td:nth-of-type(6)").Text(),
|
||||
Maghrib: el.Find("td:nth-of-type(7)").Text(),
|
||||
Isha: el.Find("td:nth-of-type(8)").Text(),
|
||||
}
|
||||
times = append(times, row)
|
||||
})
|
||||
|
||||
return times, err
|
||||
}
|
||||
|
||||
func (d Provider) Name() string {
|
||||
return "diyanetweb"
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package diyanet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"prayertimes/internal/net"
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
type mockFetcher string
|
||||
|
||||
func (f mockFetcher) Fetch(_ context.Context, _ string) (*goquery.Document, error) {
|
||||
return goquery.NewDocumentFromReader(strings.NewReader(string(f)))
|
||||
}
|
||||
|
||||
const mockHtml = `
|
||||
<div id='tab-1'>
|
||||
<div class='table-responsive'>
|
||||
<table class='table vakit-table'>
|
||||
<caption>Monthly Prayer Times for Some Location</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gregorian Calendar Date</th>
|
||||
<th>Hijri Date</th>
|
||||
<th>Fajr</th>
|
||||
<th>Sun</th>
|
||||
<th>Dhuhr</th>
|
||||
<th>Asr</th>
|
||||
<th>Maghrib</th>
|
||||
<th>Isha</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>04.03.2023</td>
|
||||
<td>...</td>
|
||||
<td>05:48</td>
|
||||
<td>07:11</td>
|
||||
<td>13:05</td>
|
||||
<td>16:15</td>
|
||||
<td>18:49</td>
|
||||
<td>20:06</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>05.03.2023</td>
|
||||
<td>...</td>
|
||||
<td>05:46</td>
|
||||
<td>07:09</td>
|
||||
<td>13:04</td>
|
||||
<td>16:15</td>
|
||||
<td>18:50</td>
|
||||
<td>20:07</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
||||
func TestDiyanet_Get(t *testing.T) {
|
||||
t.Run("validates location", func(t *testing.T) {
|
||||
d := Provider{}
|
||||
_, err := d.Get(context.Background(), " not numeric ")
|
||||
assert.ErrorIs(t, err, prayer.ErrInvalidLocation)
|
||||
})
|
||||
|
||||
t.Run("extracts prayer times", func(t *testing.T) {
|
||||
d := Provider{
|
||||
FetcherFunc: mockFetcher(mockHtml).Fetch,
|
||||
}
|
||||
actual, err := d.Get(context.Background(), "1234")
|
||||
assert.NoError(t, err)
|
||||
|
||||
expected := []prayer.Times{
|
||||
{
|
||||
Date: "2023-03-04",
|
||||
Fajr: "05:48",
|
||||
Sunrise: "07:11",
|
||||
Dhuhr: "13:05",
|
||||
Asr: "16:15",
|
||||
Maghrib: "18:49",
|
||||
Isha: "20:06",
|
||||
},
|
||||
{
|
||||
Date: "2023-03-05",
|
||||
Fajr: "05:46",
|
||||
Sunrise: "07:09",
|
||||
Dhuhr: "13:04",
|
||||
Asr: "16:15",
|
||||
Maghrib: "18:50",
|
||||
Isha: "20:07",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, actual)
|
||||
})
|
||||
|
||||
t.Run("real endpoint", func(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
d := Provider{
|
||||
FetcherFunc: net.GetParsed,
|
||||
}
|
||||
times, err := d.Get(context.Background(), "11104")
|
||||
if err != nil {
|
||||
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
|
||||
}
|
||||
if len(times) == 0 {
|
||||
t.Skip("skipping live endpoint test because upstream returned no times")
|
||||
}
|
||||
|
||||
assert.Greater(t, len(times), 0)
|
||||
assert.NotZero(t, times[0].Date)
|
||||
|
||||
for _, it := range times {
|
||||
t.Logf("%+v", it)
|
||||
}
|
||||
})
|
||||
}
|
||||
+117
-29
@@ -2,7 +2,9 @@ package diyanetapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
@@ -21,48 +23,136 @@ func New(c *req.Client) Provider {
|
||||
}
|
||||
|
||||
func (d Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error) {
|
||||
locationID, err := d.getLocationIDByCoords(ctx, coords)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve location by coordinates: %w", err)
|
||||
}
|
||||
|
||||
times, err := d.Get(ctx, locationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times by coordinates: %w", err)
|
||||
}
|
||||
|
||||
return times, nil
|
||||
}
|
||||
|
||||
func (d Provider) getLocationIDByCoords(ctx context.Context, coords prayer.Coordinates) (string, error) {
|
||||
res, err := d.http.NewRequest().
|
||||
SetContext(ctx).
|
||||
SetQueryParams(map[string]string{
|
||||
"latitude": fmt.Sprintf("%f", coords.Latitude),
|
||||
"longitude": fmt.Sprintf("%f", coords.Longitude),
|
||||
}).
|
||||
Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Aylik")
|
||||
Get("https://namazvakti.diyanet.gov.tr/api/ilce/GetByCoordinat")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times by coords: %w", err)
|
||||
return "", fmt.Errorf("failed to get location by coords: %w", err)
|
||||
}
|
||||
|
||||
return d.parseResponse(res)
|
||||
var response struct {
|
||||
Success bool `json:"success"`
|
||||
ResultObject []struct {
|
||||
ID int `json:"cityID"`
|
||||
} `json:"resultObject"`
|
||||
}
|
||||
if err := res.Unmarshal(&response); err != nil {
|
||||
return "", fmt.Errorf("failed to unmarshal city response: %w", err)
|
||||
}
|
||||
if !response.Success {
|
||||
return "", fmt.Errorf("failed to get location by coordinates from upstream: %w", errors.New(res.String()))
|
||||
}
|
||||
if len(response.ResultObject) == 0 {
|
||||
return "", fmt.Errorf("failed to resolve location by coordinates: %w", errors.New("empty location result"))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%d", response.ResultObject[0].ID), nil
|
||||
}
|
||||
|
||||
func (d Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return []prayer.Location{}, nil
|
||||
}
|
||||
|
||||
res, err := d.http.NewRequest().
|
||||
SetContext(ctx).
|
||||
SetQueryParam("searchText", query).
|
||||
Get("https://namazvakti.diyanet.gov.tr/api/Search/GetByName")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search locations: %w", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Success bool `json:"success"`
|
||||
ResultObject struct {
|
||||
Results []struct {
|
||||
ID int `json:"cityID"`
|
||||
CityNameTR string `json:"cityNameTR"`
|
||||
StateNameTR string `json:"stateNameTR"`
|
||||
CountryNameTR string `json:"countryNameTR"`
|
||||
CityNameEN string `json:"cityNameEN"`
|
||||
StateNameEN string `json:"stateNameEN"`
|
||||
CountryNameEN string `json:"countryNameEN"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
} `json:"results"`
|
||||
} `json:"resultObject"`
|
||||
}
|
||||
if err := res.Unmarshal(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal search response: %w", err)
|
||||
}
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("failed to search locations in upstream: %w", errors.New(res.String()))
|
||||
}
|
||||
|
||||
locations := make([]prayer.Location, 0, len(response.ResultObject.Results))
|
||||
for _, it := range response.ResultObject.Results {
|
||||
locations = append(locations, prayer.Location{
|
||||
ID: it.ID,
|
||||
NameTR: formatLocationName(it.CountryNameTR, it.StateNameTR, it.CityNameTR),
|
||||
NameEN: formatLocationName(it.CountryNameEN, it.StateNameEN, it.CityNameEN),
|
||||
Latitude: it.Latitude,
|
||||
Longitude: it.Longitude,
|
||||
})
|
||||
}
|
||||
|
||||
return locations, nil
|
||||
}
|
||||
|
||||
func formatLocationName(country, state, city string) string {
|
||||
return fmt.Sprintf("%s / %s / %s", strings.TrimSpace(country), strings.TrimSpace(state), strings.TrimSpace(city))
|
||||
}
|
||||
|
||||
func (d Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||
res, err := d.http.NewRequest().
|
||||
SetContext(ctx).
|
||||
SetQueryParam("ilceId", locationID).
|
||||
Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Aylik")
|
||||
Get("https://namazvakti.diyanet.gov.tr/api/NamazVakti/Gunluk")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times by location id: %w", err)
|
||||
}
|
||||
|
||||
return d.parseResponse(res)
|
||||
times, err := d.parseResponse(res)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse prayer times response: %w", err)
|
||||
}
|
||||
|
||||
return times, nil
|
||||
}
|
||||
|
||||
func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
|
||||
var response struct {
|
||||
Success bool `json:"success"`
|
||||
ResultObject struct {
|
||||
Location struct {
|
||||
ID int `json:"konum_Id"`
|
||||
Timezone string `json:"timezone"`
|
||||
} `json:"konum"`
|
||||
PrayerTimes []struct {
|
||||
Date time.Time `json:"miladi_tarih_uzun_Iso8601"`
|
||||
Fajr time.Time `json:"imsak"`
|
||||
Sunrise time.Time `json:"gunes"`
|
||||
Dhuhr time.Time `json:"ogle"`
|
||||
Asr time.Time `json:"ikindi"`
|
||||
Maghrib time.Time `json:"aksam"`
|
||||
Isha time.Time `json:"yatsi"`
|
||||
Date time.Time `json:"miladi_tarih_uzun_Iso8601"`
|
||||
DateIslamic string `json:"hicri_tarih_uzun"`
|
||||
Fajr time.Time `json:"imsak"`
|
||||
Sunrise time.Time `json:"gunes"`
|
||||
Dhuhr time.Time `json:"ogle"`
|
||||
Asr time.Time `json:"ikindi"`
|
||||
Sunset time.Time `json:"gunes_batis"`
|
||||
Maghrib time.Time `json:"aksam"`
|
||||
Isha time.Time `json:"yatsi"`
|
||||
} `json:"namazVakti"`
|
||||
} `json:"resultObject"`
|
||||
}
|
||||
@@ -70,7 +160,7 @@ func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
|
||||
return nil, fmt.Errorf("failed to unmarshal as json: %w", err)
|
||||
}
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("received error: %s", res.String())
|
||||
return nil, fmt.Errorf("failed to get prayer times from upstream: %w", errors.New(res.String()))
|
||||
}
|
||||
|
||||
if len(response.ResultObject.PrayerTimes) == 0 {
|
||||
@@ -80,24 +170,22 @@ func (d Provider) parseResponse(res *req.Response) ([]prayer.Times, error) {
|
||||
var times []prayer.Times
|
||||
today := time.Now().UTC().Truncate(time.Hour * 24)
|
||||
for _, pt := range response.ResultObject.PrayerTimes {
|
||||
then := prayer.Date(pt.Date).Time()
|
||||
then := pt.Date.UTC().Truncate(time.Hour * 24)
|
||||
if then.Before(today) {
|
||||
continue
|
||||
}
|
||||
|
||||
times = append(times, prayer.Times{
|
||||
Date: pt.Date.Format(time.DateOnly),
|
||||
Fajr: pt.Fajr.Format(time.TimeOnly),
|
||||
Sunrise: pt.Sunrise.Format(time.TimeOnly),
|
||||
Dhuhr: pt.Dhuhr.Format(time.TimeOnly),
|
||||
Asr: pt.Asr.Format(time.TimeOnly),
|
||||
Maghrib: pt.Maghrib.Format(time.TimeOnly),
|
||||
Isha: pt.Isha.Format(time.TimeOnly),
|
||||
Date: pt.Date.Format(time.DateOnly),
|
||||
DateIslamic: pt.DateIslamic,
|
||||
Fajr: pt.Fajr.Format("15:04"),
|
||||
Sunrise: pt.Sunrise.Format("15:04"),
|
||||
Dhuhr: pt.Dhuhr.Format("15:04"),
|
||||
Asr: pt.Asr.Format("15:04"),
|
||||
Sunset: pt.Sunset.Format("15:04"),
|
||||
Maghrib: pt.Maghrib.Format("15:04"),
|
||||
Isha: pt.Isha.Format("15:04"),
|
||||
})
|
||||
}
|
||||
return times, nil
|
||||
}
|
||||
|
||||
func (d Provider) Name() string {
|
||||
return "diyanetapi"
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ func TestDiyanetAPI_GetByCoords(t *testing.T) {
|
||||
Latitude: 52.5100846,
|
||||
Longitude: 13.4518284,
|
||||
})
|
||||
if err != nil {
|
||||
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
|
||||
}
|
||||
if len(times) == 0 {
|
||||
t.Skip("skipping live endpoint test because upstream returned no times")
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, times)
|
||||
t.Logf("%#+v", times[0])
|
||||
@@ -28,6 +35,13 @@ func TestDiyanetAPI_GetByCoords(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
times, err := p.Get(context.Background(), "11104")
|
||||
if err != nil {
|
||||
t.Skipf("skipping live endpoint test due to upstream/network error: %v", err)
|
||||
}
|
||||
if len(times) == 0 {
|
||||
t.Skip("skipping live endpoint test because upstream returned no times")
|
||||
}
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, times)
|
||||
for _, time := range times {
|
||||
|
||||
+15
-68
@@ -1,85 +1,32 @@
|
||||
package prayer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrInvalidLocation = errors.New("invalid location")
|
||||
|
||||
type TimesProvider interface {
|
||||
Get(ctx context.Context, location string) ([]Times, error)
|
||||
Name() string
|
||||
}
|
||||
|
||||
type Coordinates struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
type LocationTimesProvider interface {
|
||||
GetByCoords(ctx context.Context, coords Coordinates) ([]Times, error)
|
||||
Name() string
|
||||
}
|
||||
|
||||
type Date time.Time
|
||||
|
||||
func (d Date) String() string {
|
||||
return time.Time(d).Format(time.DateOnly)
|
||||
}
|
||||
|
||||
func (d *Date) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, d)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), d)
|
||||
case time.Time:
|
||||
*d = Date(v)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Date) Value() (driver.Value, error) {
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
func (d *Date) UnmarshalJSON(bytes []byte) error {
|
||||
var t time.Time
|
||||
if err := json.Unmarshal(bytes, &t); err != nil {
|
||||
return err
|
||||
}
|
||||
_, offset := t.Zone()
|
||||
t = t.Add(time.Duration(offset * int(time.Second)))
|
||||
t = t.UTC()
|
||||
*d = Date(t)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d Date) MarshalJSON() ([]byte, error) {
|
||||
t := time.Time(d)
|
||||
return json.Marshal(t.Format(time.DateOnly))
|
||||
}
|
||||
|
||||
func (d Date) Time() time.Time {
|
||||
t := time.Time(d)
|
||||
_, offset := t.Zone()
|
||||
t = t.Add(time.Duration(offset * int(time.Second)))
|
||||
t = t.UTC()
|
||||
return t
|
||||
type Location struct {
|
||||
ID int `json:"id"`
|
||||
NameTR string `json:"name_tr"`
|
||||
NameEN string `json:"name_en"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type Times struct {
|
||||
Date string `json:"date"`
|
||||
Fajr string `json:"fajr"`
|
||||
Sunrise string `json:"sunrise"`
|
||||
Dhuhr string `json:"dhuhr"`
|
||||
Asr string `json:"asr"`
|
||||
Maghrib string `json:"maghrib"`
|
||||
Isha string `json:"isha"`
|
||||
Date string `json:"date"`
|
||||
DateIslamic string `json:"date_islamic,omitempty"`
|
||||
Fajr string `json:"fajr"`
|
||||
Sunrise string `json:"sunrise"`
|
||||
Dhuhr string `json:"dhuhr"`
|
||||
Asr string `json:"asr"`
|
||||
Sunset string `json:"sunset,omitempty"`
|
||||
Maghrib string `json:"maghrib"`
|
||||
Isha string `json:"isha"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user