Skip to main content

ramadan_cli/commands/
config.rs

1use anyhow::{Result, anyhow};
2
3use crate::ramadan_config::{
4    StoredLocation, clear_ramadan_config, get_stored_first_roza_date, get_stored_location,
5    get_stored_prayer_settings, set_stored_location, set_stored_method, set_stored_school,
6    set_stored_timezone,
7};
8
9#[derive(Debug, Clone, Default)]
10pub struct ConfigCommandOptions {
11    pub city: Option<String>,
12    pub country: Option<String>,
13    pub latitude: Option<String>,
14    pub longitude: Option<String>,
15    pub method: Option<String>,
16    pub school: Option<String>,
17    pub timezone: Option<String>,
18    pub show: bool,
19    pub clear: bool,
20}
21
22#[derive(Debug, Clone, Default)]
23pub struct ParsedConfigUpdates {
24    pub city: Option<String>,
25    pub country: Option<String>,
26    pub latitude: Option<f64>,
27    pub longitude: Option<f64>,
28    pub method: Option<i64>,
29    pub school: Option<i64>,
30    pub timezone: Option<String>,
31}
32
33fn parse_optional_i64_in_range(
34    value: Option<&str>,
35    min: i64,
36    max: i64,
37    label: &str,
38) -> Result<Option<i64>> {
39    let Some(raw) = value else {
40        return Ok(None);
41    };
42
43    let parsed = raw
44        .parse::<i64>()
45        .map_err(|_| anyhow!("Invalid {label}."))?;
46    if parsed < min || parsed > max {
47        return Err(anyhow!("Invalid {label}."));
48    }
49
50    Ok(Some(parsed))
51}
52
53fn parse_optional_f64_in_range(
54    value: Option<&str>,
55    min: f64,
56    max: f64,
57    label: &str,
58) -> Result<Option<f64>> {
59    let Some(raw) = value else {
60        return Ok(None);
61    };
62
63    let parsed = raw
64        .parse::<f64>()
65        .map_err(|_| anyhow!("Invalid {label}."))?;
66    if parsed < min || parsed > max {
67        return Err(anyhow!("Invalid {label}."));
68    }
69
70    Ok(Some(parsed))
71}
72
73pub fn parse_config_updates(options: &ConfigCommandOptions) -> Result<ParsedConfigUpdates> {
74    Ok(ParsedConfigUpdates {
75        city: options
76            .city
77            .as_ref()
78            .map(|v| v.trim().to_string())
79            .filter(|v| !v.is_empty()),
80        country: options
81            .country
82            .as_ref()
83            .map(|v| v.trim().to_string())
84            .filter(|v| !v.is_empty()),
85        latitude: parse_optional_f64_in_range(
86            options.latitude.as_deref(),
87            -90.0,
88            90.0,
89            "latitude",
90        )?,
91        longitude: parse_optional_f64_in_range(
92            options.longitude.as_deref(),
93            -180.0,
94            180.0,
95            "longitude",
96        )?,
97        method: parse_optional_i64_in_range(options.method.as_deref(), 0, 23, "method")?,
98        school: parse_optional_i64_in_range(options.school.as_deref(), 0, 1, "school")?,
99        timezone: options
100            .timezone
101            .as_ref()
102            .map(|v| v.trim().to_string())
103            .filter(|v| !v.is_empty()),
104    })
105}
106
107pub fn merge_location_updates(
108    current: &StoredLocation,
109    updates: &ParsedConfigUpdates,
110) -> StoredLocation {
111    StoredLocation {
112        city: updates.city.clone().or_else(|| current.city.clone()),
113        country: updates.country.clone().or_else(|| current.country.clone()),
114        latitude: updates.latitude.or(current.latitude),
115        longitude: updates.longitude.or(current.longitude),
116    }
117}
118
119fn print_current_config() {
120    let location = get_stored_location();
121    let settings = get_stored_prayer_settings();
122    let first_roza_date = get_stored_first_roza_date();
123
124    println!("Current configuration:");
125    if let Some(city) = location.city {
126        println!("  City: {city}");
127    }
128    if let Some(country) = location.country {
129        println!("  Country: {country}");
130    }
131    if let Some(latitude) = location.latitude {
132        println!("  Latitude: {latitude}");
133    }
134    if let Some(longitude) = location.longitude {
135        println!("  Longitude: {longitude}");
136    }
137    println!("  Method: {}", settings.method);
138    println!("  School: {}", settings.school);
139    if let Some(timezone) = settings.timezone {
140        println!("  Timezone: {timezone}");
141    }
142    if let Some(date) = first_roza_date {
143        println!("  First Roza Date: {date}");
144    }
145}
146
147fn has_config_update_flags(options: &ConfigCommandOptions) -> bool {
148    options.city.is_some()
149        || options.country.is_some()
150        || options.latitude.is_some()
151        || options.longitude.is_some()
152        || options.method.is_some()
153        || options.school.is_some()
154        || options.timezone.is_some()
155}
156
157pub fn config_command(options: &ConfigCommandOptions) -> Result<()> {
158    if options.clear {
159        clear_ramadan_config()?;
160        println!("Configuration cleared.");
161        return Ok(());
162    }
163
164    if options.show {
165        print_current_config();
166        return Ok(());
167    }
168
169    if !has_config_update_flags(options) {
170        println!("No config updates provided. Use `ramadan-cli config --show` to inspect.");
171        return Ok(());
172    }
173
174    let updates = parse_config_updates(options)?;
175    let current_location = get_stored_location();
176    let next_location = merge_location_updates(&current_location, &updates);
177
178    set_stored_location(&next_location)?;
179
180    if let Some(method) = updates.method {
181        set_stored_method(method)?;
182    }
183    if let Some(school) = updates.school {
184        set_stored_school(school)?;
185    }
186    if let Some(timezone) = updates.timezone {
187        set_stored_timezone(Some(&timezone))?;
188    }
189
190    println!("Configuration updated.");
191    Ok(())
192}
193
194#[cfg(test)]
195mod tests {
196    use super::{ConfigCommandOptions, merge_location_updates, parse_config_updates};
197
198    #[test]
199    fn parse_config_updates_validates_ranges() {
200        let parsed = parse_config_updates(&ConfigCommandOptions {
201            method: Some("2".to_string()),
202            school: Some("1".to_string()),
203            latitude: Some("37.7749".to_string()),
204            longitude: Some("-122.4194".to_string()),
205            ..ConfigCommandOptions::default()
206        })
207        .expect("valid updates");
208
209        assert_eq!(parsed.method, Some(2));
210        assert_eq!(parsed.school, Some(1));
211        assert_eq!(parsed.latitude, Some(37.7749));
212        assert_eq!(parsed.longitude, Some(-122.4194));
213    }
214
215    #[test]
216    fn merge_location_updates_only_overrides_provided_fields() {
217        let current = crate::ramadan_config::StoredLocation {
218            city: Some("Lahore".to_string()),
219            country: Some("Pakistan".to_string()),
220            latitude: Some(31.5204),
221            longitude: Some(74.3587),
222        };
223
224        let merged = merge_location_updates(
225            &current,
226            &super::ParsedConfigUpdates {
227                city: Some("San Francisco".to_string()),
228                ..super::ParsedConfigUpdates::default()
229            },
230        );
231
232        assert_eq!(merged.city.as_deref(), Some("San Francisco"));
233        assert_eq!(merged.country.as_deref(), Some("Pakistan"));
234        assert_eq!(merged.latitude, Some(31.5204));
235        assert_eq!(merged.longitude, Some(74.3587));
236    }
237}