feat: Add location search
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package citydb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"prayertimes/pkg/prayer"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
db *goqu.Database
|
||||
}
|
||||
|
||||
func New(db *goqu.Database) Provider {
|
||||
return Provider{db: db}
|
||||
}
|
||||
|
||||
func Open(path string) (Provider, error) {
|
||||
conn, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return Provider{}, fmt.Errorf("failed to open cities database: %w", err)
|
||||
}
|
||||
|
||||
if err := conn.Ping(); err != nil {
|
||||
return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err)
|
||||
}
|
||||
|
||||
return New(goqu.New("sqlite3", conn)), nil
|
||||
}
|
||||
|
||||
func (p Provider) Close() error {
|
||||
return p.db.Db.(*sql.DB).Close()
|
||||
}
|
||||
|
||||
type locationRow struct {
|
||||
ID int `db:"geoname_id"`
|
||||
Name string `db:"name"`
|
||||
ASCIIName string `db:"ascii_name"`
|
||||
AlternateNames string `db:"alternate_names"`
|
||||
CountryCode string `db:"country_code"`
|
||||
Latitude float64 `db:"latitude"`
|
||||
Longitude float64 `db:"longitude"`
|
||||
}
|
||||
|
||||
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
||||
query = strings.TrimSpace(query)
|
||||
if query == "" {
|
||||
return []prayer.Location{}, nil
|
||||
}
|
||||
|
||||
pattern := "%" + query + "%"
|
||||
|
||||
q := p.db.
|
||||
From("cities").
|
||||
Select(
|
||||
goqu.I("geoname_id"),
|
||||
goqu.I("name"),
|
||||
goqu.I("ascii_name"),
|
||||
goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"),
|
||||
goqu.I("country_code"),
|
||||
goqu.I("latitude"),
|
||||
goqu.I("longitude"),
|
||||
).
|
||||
Where(
|
||||
goqu.Or(
|
||||
goqu.L("name LIKE ? COLLATE NOCASE", pattern),
|
||||
goqu.L("ascii_name LIKE ? COLLATE NOCASE", pattern),
|
||||
goqu.L("alternate_names LIKE ? COLLATE NOCASE", pattern),
|
||||
),
|
||||
).
|
||||
Order(goqu.I("population").Desc(), goqu.I("name").Asc()).
|
||||
Limit(50)
|
||||
|
||||
var rows []locationRow
|
||||
if err := q.ScanStructsContext(ctx, &rows); err != nil {
|
||||
return nil, fmt.Errorf("failed to query locations from cities database: %w", err)
|
||||
}
|
||||
|
||||
locations := make([]prayer.Location, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
locations = append(locations, prayer.Location{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
ASCIIName: row.ASCIIName,
|
||||
AlternateNames: row.AlternateNames,
|
||||
CountryCode: row.CountryCode,
|
||||
Latitude: row.Latitude,
|
||||
Longitude: row.Longitude,
|
||||
})
|
||||
}
|
||||
|
||||
return locations, nil
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
@@ -67,61 +66,6 @@ func (d Provider) getLocationIDByCoords(ctx context.Context, coords prayer.Coord
|
||||
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).
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
type APIProvider interface {
|
||||
Get(ctx context.Context, locationID string) ([]prayer.Times, error)
|
||||
GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error)
|
||||
SearchLocations(ctx context.Context, query string) ([]prayer.Location, error)
|
||||
}
|
||||
|
||||
type FallbackProvider interface {
|
||||
@@ -29,18 +28,11 @@ func New(api APIProvider, fallback FallbackProvider) Provider {
|
||||
return Provider{
|
||||
api: api,
|
||||
fallback: fallback,
|
||||
timeout: time.Second,
|
||||
timeout: 2 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
|
||||
locations, err := p.api.SearchLocations(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search locations from api provider: %w", err)
|
||||
}
|
||||
|
||||
return locations, nil
|
||||
}
|
||||
var ErrEmptyTimes = errors.New("diyanet did not return any prayer times")
|
||||
|
||||
func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, p.timeout)
|
||||
@@ -51,7 +43,7 @@ func (p Provider) Get(ctx context.Context, locationID string) ([]prayer.Times, e
|
||||
return nil, fmt.Errorf("failed to get prayer times from api provider: %w", err)
|
||||
}
|
||||
if len(times) == 0 {
|
||||
return nil, fmt.Errorf("failed to get prayer times from api provider: %w", errors.New("empty prayer times result"))
|
||||
return nil, ErrEmptyTimes
|
||||
}
|
||||
|
||||
return times, nil
|
||||
@@ -66,16 +58,14 @@ func (p Provider) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([
|
||||
return times, nil
|
||||
}
|
||||
|
||||
fallbackTimes, fallbackErr := p.fallback.GetByCoords(ctx, coords)
|
||||
if fallbackErr != nil {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", errors.Join(err, fallbackErr))
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", fallbackErr)
|
||||
}
|
||||
if len(fallbackTimes) == 0 {
|
||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", errors.New("empty prayer times result"))
|
||||
times, err = p.fallback.GetByCoords(ctx, coords)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get prayer times from fallback provider: %w", err)
|
||||
}
|
||||
|
||||
return fallbackTimes, nil
|
||||
if len(times) == 0 {
|
||||
return nil, fmt.Errorf("fallback provider did not return any prayer times")
|
||||
}
|
||||
|
||||
return times, nil
|
||||
}
|
||||
|
||||
+7
-5
@@ -12,11 +12,13 @@ type Coordinates struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ASCIIName string `json:"ascii_name"`
|
||||
AlternateNames string `json:"alternate_names"`
|
||||
CountryCode string `json:"country_code"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type Times struct {
|
||||
|
||||
Reference in New Issue
Block a user