Skip to main content

ramadan_cli/
api.rs

1use anyhow::{Result, anyhow};
2use chrono::{Local, NaiveDate};
3use reqwest::blocking::Client;
4use serde::Deserialize;
5use serde::de::DeserializeOwned;
6use serde_json::Value;
7
8const API_BASE: &str = "https://api.aladhan.com/v1";
9
10#[derive(Debug, Clone, Deserialize)]
11pub struct PrayerTimings {
12    #[serde(rename = "Fajr")]
13    pub fajr: String,
14    #[serde(rename = "Sunrise")]
15    pub sunrise: String,
16    #[serde(rename = "Dhuhr")]
17    pub dhuhr: String,
18    #[serde(rename = "Asr")]
19    pub asr: String,
20    #[serde(rename = "Sunset")]
21    pub sunset: String,
22    #[serde(rename = "Maghrib")]
23    pub maghrib: String,
24    #[serde(rename = "Isha")]
25    pub isha: String,
26    #[serde(rename = "Imsak")]
27    pub imsak: String,
28    #[serde(rename = "Midnight")]
29    pub midnight: String,
30    #[serde(rename = "Firstthird")]
31    pub firstthird: String,
32    #[serde(rename = "Lastthird")]
33    pub lastthird: String,
34}
35
36#[derive(Debug, Clone, Deserialize)]
37pub struct HijriMonth {
38    pub number: i64,
39    pub en: String,
40    pub ar: String,
41}
42
43#[derive(Debug, Clone, Deserialize)]
44pub struct WeekdayLocalized {
45    pub en: String,
46    pub ar: Option<String>,
47}
48
49#[derive(Debug, Clone, Deserialize)]
50pub struct HijriDate {
51    pub date: String,
52    pub day: String,
53    pub month: HijriMonth,
54    pub year: String,
55    pub weekday: WeekdayLocalized,
56}
57
58#[derive(Debug, Clone, Deserialize)]
59pub struct GregorianMonth {
60    pub number: i64,
61    pub en: String,
62}
63
64#[derive(Debug, Clone, Deserialize)]
65pub struct GregorianDate {
66    pub date: String,
67    pub day: String,
68    pub month: GregorianMonth,
69    pub year: String,
70    pub weekday: WeekdayLocalized,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct PrayerDate {
75    pub readable: String,
76    pub timestamp: String,
77    pub hijri: HijriDate,
78    pub gregorian: GregorianDate,
79}
80
81#[derive(Debug, Clone, Deserialize)]
82pub struct PrayerMethod {
83    pub id: i64,
84    pub name: String,
85}
86
87#[derive(Debug, Clone, Deserialize)]
88pub struct PrayerSchoolObj {
89    pub id: i64,
90    pub name: String,
91}
92
93#[derive(Debug, Clone, Deserialize)]
94#[serde(untagged)]
95pub enum PrayerSchool {
96    Object(PrayerSchoolObj),
97    Text(String),
98}
99
100#[derive(Debug, Clone, Deserialize)]
101pub struct PrayerMeta {
102    pub latitude: f64,
103    pub longitude: f64,
104    pub timezone: String,
105    pub method: PrayerMethod,
106    pub school: PrayerSchool,
107}
108
109#[derive(Debug, Clone, Deserialize)]
110pub struct PrayerData {
111    pub timings: PrayerTimings,
112    pub date: PrayerDate,
113    pub meta: PrayerMeta,
114}
115
116#[derive(Debug, Clone, Deserialize)]
117pub struct NextPrayerData {
118    pub timings: PrayerTimings,
119    pub date: PrayerDate,
120    pub meta: PrayerMeta,
121    #[serde(rename = "nextPrayer")]
122    pub next_prayer: String,
123    #[serde(rename = "nextPrayerTime")]
124    pub next_prayer_time: String,
125}
126
127#[derive(Debug, Clone, Deserialize)]
128pub struct CalculationMethodParams {
129    #[serde(rename = "Fajr")]
130    pub fajr: f64,
131    #[serde(rename = "Isha")]
132    pub isha: Value,
133}
134
135#[derive(Debug, Clone, Deserialize)]
136pub struct CalculationMethod {
137    pub id: i64,
138    pub name: String,
139    pub params: CalculationMethodParams,
140}
141
142pub type MethodsResponse = std::collections::HashMap<String, CalculationMethod>;
143
144#[derive(Debug, Clone, Deserialize)]
145pub struct QiblaData {
146    pub latitude: f64,
147    pub longitude: f64,
148    pub direction: f64,
149}
150
151#[derive(Debug, Deserialize)]
152struct ApiEnvelope {
153    code: i64,
154    status: String,
155    data: Value,
156}
157
158fn format_date(date: NaiveDate) -> String {
159    date.format("%d-%m-%Y").to_string()
160}
161
162fn today_date() -> NaiveDate {
163    Local::now().date_naive()
164}
165
166fn parse_api_response<T: DeserializeOwned>(payload: Value) -> Result<T> {
167    let envelope: ApiEnvelope =
168        serde_json::from_value(payload).map_err(|e| anyhow!("Invalid API response: {e}"))?;
169
170    if envelope.code != 200 {
171        return Err(anyhow!("API {}: {}", envelope.code, envelope.status));
172    }
173
174    if let Some(message) = envelope.data.as_str() {
175        return Err(anyhow!("API returned message: {message}"));
176    }
177
178    serde_json::from_value(envelope.data).map_err(|e| anyhow!("Invalid API response: {e}"))
179}
180
181fn fetch_and_parse<T: DeserializeOwned>(client: &Client, url: &str) -> Result<T> {
182    let payload: Value = client
183        .get(url)
184        .send()
185        .and_then(|r| r.error_for_status())
186        .map_err(|e| anyhow!("Network request failed: {e}"))?
187        .json()
188        .map_err(|e| anyhow!("Invalid API response: {e}"))?;
189
190    parse_api_response(payload)
191}
192
193#[derive(Debug, Clone)]
194pub struct FetchByCityOptions {
195    pub city: String,
196    pub country: String,
197    pub method: Option<i64>,
198    pub school: Option<i64>,
199    pub date: Option<NaiveDate>,
200}
201
202pub fn fetch_timings_by_city(client: &Client, opts: &FetchByCityOptions) -> Result<PrayerData> {
203    let date = format_date(opts.date.unwrap_or_else(today_date));
204    let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByCity/{date}"))?;
205    {
206        let mut qp = url.query_pairs_mut();
207        qp.append_pair("city", &opts.city);
208        qp.append_pair("country", &opts.country);
209        if let Some(method) = opts.method {
210            qp.append_pair("method", &method.to_string());
211        }
212        if let Some(school) = opts.school {
213            qp.append_pair("school", &school.to_string());
214        }
215    }
216
217    fetch_and_parse(client, url.as_str())
218}
219
220#[derive(Debug, Clone)]
221pub struct FetchByAddressOptions {
222    pub address: String,
223    pub method: Option<i64>,
224    pub school: Option<i64>,
225    pub date: Option<NaiveDate>,
226}
227
228pub fn fetch_timings_by_address(
229    client: &Client,
230    opts: &FetchByAddressOptions,
231) -> Result<PrayerData> {
232    let date = format_date(opts.date.unwrap_or_else(today_date));
233    let mut url = reqwest::Url::parse(&format!("{API_BASE}/timingsByAddress/{date}"))?;
234    {
235        let mut qp = url.query_pairs_mut();
236        qp.append_pair("address", &opts.address);
237        if let Some(method) = opts.method {
238            qp.append_pair("method", &method.to_string());
239        }
240        if let Some(school) = opts.school {
241            qp.append_pair("school", &school.to_string());
242        }
243    }
244
245    fetch_and_parse(client, url.as_str())
246}
247
248#[derive(Debug, Clone)]
249pub struct FetchByCoordsOptions {
250    pub latitude: f64,
251    pub longitude: f64,
252    pub method: Option<i64>,
253    pub school: Option<i64>,
254    pub timezone: Option<String>,
255    pub date: Option<NaiveDate>,
256}
257
258pub fn fetch_timings_by_coords(client: &Client, opts: &FetchByCoordsOptions) -> Result<PrayerData> {
259    let date = format_date(opts.date.unwrap_or_else(today_date));
260    let mut url = reqwest::Url::parse(&format!("{API_BASE}/timings/{date}"))?;
261    {
262        let mut qp = url.query_pairs_mut();
263        qp.append_pair("latitude", &opts.latitude.to_string());
264        qp.append_pair("longitude", &opts.longitude.to_string());
265        if let Some(method) = opts.method {
266            qp.append_pair("method", &method.to_string());
267        }
268        if let Some(school) = opts.school {
269            qp.append_pair("school", &school.to_string());
270        }
271        if let Some(timezone) = &opts.timezone {
272            qp.append_pair("timezonestring", timezone);
273        }
274    }
275
276    fetch_and_parse(client, url.as_str())
277}
278
279#[derive(Debug, Clone)]
280pub struct FetchNextPrayerOptions {
281    pub latitude: f64,
282    pub longitude: f64,
283    pub method: Option<i64>,
284    pub school: Option<i64>,
285    pub timezone: Option<String>,
286}
287
288pub fn fetch_next_prayer(client: &Client, opts: &FetchNextPrayerOptions) -> Result<NextPrayerData> {
289    let date = format_date(today_date());
290    let mut url = reqwest::Url::parse(&format!("{API_BASE}/nextPrayer/{date}"))?;
291    {
292        let mut qp = url.query_pairs_mut();
293        qp.append_pair("latitude", &opts.latitude.to_string());
294        qp.append_pair("longitude", &opts.longitude.to_string());
295        if let Some(method) = opts.method {
296            qp.append_pair("method", &method.to_string());
297        }
298        if let Some(school) = opts.school {
299            qp.append_pair("school", &school.to_string());
300        }
301        if let Some(timezone) = &opts.timezone {
302            qp.append_pair("timezonestring", timezone);
303        }
304    }
305
306    fetch_and_parse(client, url.as_str())
307}
308
309#[derive(Debug, Clone)]
310pub struct FetchCalendarByCityOptions {
311    pub city: String,
312    pub country: String,
313    pub year: i64,
314    pub month: Option<i64>,
315    pub method: Option<i64>,
316    pub school: Option<i64>,
317}
318
319pub fn fetch_calendar_by_city(
320    client: &Client,
321    opts: &FetchCalendarByCityOptions,
322) -> Result<Vec<PrayerData>> {
323    let path = match opts.month {
324        Some(month) => format!("{}/{month}", opts.year),
325        None => opts.year.to_string(),
326    };
327    let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByCity/{path}"))?;
328    {
329        let mut qp = url.query_pairs_mut();
330        qp.append_pair("city", &opts.city);
331        qp.append_pair("country", &opts.country);
332        if let Some(method) = opts.method {
333            qp.append_pair("method", &method.to_string());
334        }
335        if let Some(school) = opts.school {
336            qp.append_pair("school", &school.to_string());
337        }
338    }
339
340    fetch_and_parse(client, url.as_str())
341}
342
343#[derive(Debug, Clone)]
344pub struct FetchCalendarByAddressOptions {
345    pub address: String,
346    pub year: i64,
347    pub month: Option<i64>,
348    pub method: Option<i64>,
349    pub school: Option<i64>,
350}
351
352pub fn fetch_calendar_by_address(
353    client: &Client,
354    opts: &FetchCalendarByAddressOptions,
355) -> Result<Vec<PrayerData>> {
356    let path = match opts.month {
357        Some(month) => format!("{}/{month}", opts.year),
358        None => opts.year.to_string(),
359    };
360    let mut url = reqwest::Url::parse(&format!("{API_BASE}/calendarByAddress/{path}"))?;
361    {
362        let mut qp = url.query_pairs_mut();
363        qp.append_pair("address", &opts.address);
364        if let Some(method) = opts.method {
365            qp.append_pair("method", &method.to_string());
366        }
367        if let Some(school) = opts.school {
368            qp.append_pair("school", &school.to_string());
369        }
370    }
371
372    fetch_and_parse(client, url.as_str())
373}
374
375#[derive(Debug, Clone)]
376pub struct FetchHijriCalendarByAddressOptions {
377    pub address: String,
378    pub year: i64,
379    pub month: i64,
380    pub method: Option<i64>,
381    pub school: Option<i64>,
382}
383
384pub fn fetch_hijri_calendar_by_address(
385    client: &Client,
386    opts: &FetchHijriCalendarByAddressOptions,
387) -> Result<Vec<PrayerData>> {
388    let mut url = reqwest::Url::parse(&format!(
389        "{API_BASE}/hijriCalendarByAddress/{}/{}",
390        opts.year, opts.month
391    ))?;
392    {
393        let mut qp = url.query_pairs_mut();
394        qp.append_pair("address", &opts.address);
395        if let Some(method) = opts.method {
396            qp.append_pair("method", &method.to_string());
397        }
398        if let Some(school) = opts.school {
399            qp.append_pair("school", &school.to_string());
400        }
401    }
402
403    fetch_and_parse(client, url.as_str())
404}
405
406#[derive(Debug, Clone)]
407pub struct FetchHijriCalendarByCityOptions {
408    pub city: String,
409    pub country: String,
410    pub year: i64,
411    pub month: i64,
412    pub method: Option<i64>,
413    pub school: Option<i64>,
414}
415
416pub fn fetch_hijri_calendar_by_city(
417    client: &Client,
418    opts: &FetchHijriCalendarByCityOptions,
419) -> Result<Vec<PrayerData>> {
420    let mut url = reqwest::Url::parse(&format!(
421        "{API_BASE}/hijriCalendarByCity/{}/{}",
422        opts.year, opts.month
423    ))?;
424    {
425        let mut qp = url.query_pairs_mut();
426        qp.append_pair("city", &opts.city);
427        qp.append_pair("country", &opts.country);
428        if let Some(method) = opts.method {
429            qp.append_pair("method", &method.to_string());
430        }
431        if let Some(school) = opts.school {
432            qp.append_pair("school", &school.to_string());
433        }
434    }
435
436    fetch_and_parse(client, url.as_str())
437}
438
439pub fn fetch_methods(client: &Client) -> Result<MethodsResponse> {
440    fetch_and_parse(client, &format!("{API_BASE}/methods"))
441}
442
443pub fn fetch_qibla(client: &Client, latitude: f64, longitude: f64) -> Result<QiblaData> {
444    fetch_and_parse(client, &format!("{API_BASE}/qibla/{latitude}/{longitude}"))
445}