shaum_rules/
query.rs

1//! Fluent query engine for finding fasting dates.
2 
3use chrono::NaiveDate;
4use crate::rules::{check, RuleContext};
5use shaum_types::{FastingAnalysis, FastingType};
6use shaum_types::ShaumError;
7
8/// Query filter mode.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FilterMode {
11    All,
12    Wajib,
13    Sunnah,
14    Haram,
15    Makruh,
16    Mubah,
17}
18
19/// Fluent query builder for fasting dates.
20#[derive(Debug, Clone)]
21pub struct FastingQuery {
22    current: NaiveDate,
23    end: Option<NaiveDate>,
24    context: RuleContext,
25    filter: FilterMode,
26    exclude_haram: bool,
27    exclude_makruh: bool,
28    require_type: Option<FastingType>,
29}
30
31impl FastingQuery {
32    /// Creates query starting from `date`.
33    pub fn starting_from(date: NaiveDate) -> Self {
34        Self {
35            current: date,
36            end: None,
37            context: RuleContext::default(),
38            filter: FilterMode::All,
39            exclude_haram: false,
40            exclude_makruh: false,
41            require_type: None,
42        }
43    }
44
45    /// Sets end date (inclusive).
46    pub fn until(mut self, date: NaiveDate) -> Self { self.end = Some(date); self }
47    
48    /// Sets custom context.
49    pub fn with_context(mut self, ctx: RuleContext) -> Self { self.context = ctx; self }
50    
51    /// Filters to Wajib only.
52    pub fn wajib(mut self) -> Self { self.filter = FilterMode::Wajib; self }
53    
54    /// Filters to Sunnah only.
55    pub fn sunnah(mut self) -> Self { self.filter = FilterMode::Sunnah; self }
56    
57    /// Filters to Haram only.
58    pub fn haram(mut self) -> Self { self.filter = FilterMode::Haram; self }
59    
60    /// Filters to Makruh only.
61    pub fn makruh(mut self) -> Self { self.filter = FilterMode::Makruh; self }
62    
63    /// Excludes Haram days.
64    pub fn exclude_haram(mut self) -> Self { self.exclude_haram = true; self }
65    
66    /// Excludes Makruh days.
67    pub fn exclude_makruh(mut self) -> Self { self.exclude_makruh = true; self }
68    
69    /// Requires specific fasting type.
70    pub fn with_type(mut self, ftype: FastingType) -> Self { self.require_type = Some(ftype); self }
71
72    fn matches(&self, analysis: &FastingAnalysis) -> bool {
73        if self.exclude_haram && analysis.primary_status.is_haram() { return false; }
74        if self.exclude_makruh && analysis.primary_status.is_makruh() { return false; }
75        if let Some(ref t) = self.require_type { if !analysis.has_reason(t) { return false; } }
76
77        match self.filter {
78            FilterMode::All => true,
79            FilterMode::Wajib => analysis.primary_status.is_wajib(),
80            FilterMode::Sunnah => analysis.primary_status.is_sunnah(),
81            FilterMode::Haram => analysis.primary_status.is_haram(),
82            FilterMode::Makruh => analysis.primary_status.is_makruh(),
83            FilterMode::Mubah => analysis.primary_status.is_mubah(),
84        }
85    }
86}
87
88impl Iterator for FastingQuery {
89    type Item = Result<FastingAnalysis, ShaumError>;
90
91    fn next(&mut self) -> Option<Self::Item> {
92        loop {
93            if let Some(end) = self.end { if self.current > end { return None; } }
94            let date = self.current;
95            self.current = self.current.succ_opt()?;
96
97            // Propagate errors from check
98            let analysis = match check(date, &self.context) {
99                Ok(a) => a,
100                Err(e) => return Some(Err(e)),
101            };
102
103            if self.matches(&analysis) {
104                return Some(Ok(analysis));
105            }
106        }
107    }
108}
109
110/// Extension for query creation.
111pub trait QueryExt {
112    /// Creates query for upcoming fasts.
113    fn upcoming_fasts(&self) -> FastingQuery;
114}
115
116impl QueryExt for NaiveDate {
117    fn upcoming_fasts(&self) -> FastingQuery { FastingQuery::starting_from(*self) }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_basic_query() {
126        let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
127        let results: Vec<_> = FastingQuery::starting_from(start).take(5).collect();
128        assert_eq!(results.len(), 5);
129        assert!(results.iter().all(|r| r.is_ok()));
130    }
131
132    #[test]
133    fn test_sunnah_filter() {
134        let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
135        let results: Vec<_> = FastingQuery::starting_from(start).sunnah().take(3).collect();
136        for r in &results { 
137            assert!(r.as_ref().unwrap().primary_status.is_sunnah()); 
138        }
139    }
140
141    #[test]
142    fn test_until_bound() {
143        let start = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
144        let end = NaiveDate::from_ymd_opt(2024, 3, 5).unwrap();
145        let results: Vec<_> = FastingQuery::starting_from(start).until(end).collect();
146        assert!(results.len() <= 5);
147    }
148
149    #[test]
150    fn test_query_ext_trait() {
151        let date = NaiveDate::from_ymd_opt(2024, 3, 1).unwrap();
152        let results: Vec<_> = date.upcoming_fasts().take(3).collect();
153        assert_eq!(results.len(), 3);
154    }
155
156    #[test]
157    fn test_error_propagation() {
158        // Year 3000 should fail
159        let start = NaiveDate::from_ymd_opt(2077, 1, 1).unwrap();
160        let mut query = FastingQuery::starting_from(start);
161        let result = query.next();
162        assert!(result.is_some());
163        assert!(result.unwrap().is_err());
164    }
165}