use anyhow::{Result, anyhow};
use crate::ramadan_config::{
StoredLocation, clear_ramadan_config, get_stored_first_roza_date, get_stored_location,
get_stored_prayer_settings, set_stored_location, set_stored_method, set_stored_school,
set_stored_timezone,
};
#[derive(Debug, Clone, Default)]
pub struct ConfigCommandOptions {
pub city: Option<String>,
pub country: Option<String>,
pub latitude: Option<String>,
pub longitude: Option<String>,
pub method: Option<String>,
pub school: Option<String>,
pub timezone: Option<String>,
pub show: bool,
pub clear: bool,
}
#[derive(Debug, Clone, Default)]
pub struct ParsedConfigUpdates {
pub city: Option<String>,
pub country: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub method: Option<i64>,
pub school: Option<i64>,
pub timezone: Option<String>,
}
fn parse_optional_i64_in_range(
value: Option<&str>,
min: i64,
max: i64,
label: &str,
) -> Result<Option<i64>> {
let Some(raw) = value else {
return Ok(None);
};
let parsed = raw
.parse::<i64>()
.map_err(|_| anyhow!("Invalid {label}."))?;
if parsed < min || parsed > max {
return Err(anyhow!("Invalid {label}."));
}
Ok(Some(parsed))
}
fn parse_optional_f64_in_range(
value: Option<&str>,
min: f64,
max: f64,
label: &str,
) -> Result<Option<f64>> {
let Some(raw) = value else {
return Ok(None);
};
let parsed = raw
.parse::<f64>()
.map_err(|_| anyhow!("Invalid {label}."))?;
if parsed < min || parsed > max {
return Err(anyhow!("Invalid {label}."));
}
Ok(Some(parsed))
}
pub fn parse_config_updates(options: &ConfigCommandOptions) -> Result<ParsedConfigUpdates> {
Ok(ParsedConfigUpdates {
city: options
.city
.as_ref()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty()),
country: options
.country
.as_ref()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty()),
latitude: parse_optional_f64_in_range(
options.latitude.as_deref(),
-90.0,
90.0,
"latitude",
)?,
longitude: parse_optional_f64_in_range(
options.longitude.as_deref(),
-180.0,
180.0,
"longitude",
)?,
method: parse_optional_i64_in_range(options.method.as_deref(), 0, 23, "method")?,
school: parse_optional_i64_in_range(options.school.as_deref(), 0, 1, "school")?,
timezone: options
.timezone
.as_ref()
.map(|v| v.trim().to_string())
.filter(|v| !v.is_empty()),
})
}
pub fn merge_location_updates(
current: &StoredLocation,
updates: &ParsedConfigUpdates,
) -> StoredLocation {
StoredLocation {
city: updates.city.clone().or_else(|| current.city.clone()),
country: updates.country.clone().or_else(|| current.country.clone()),
latitude: updates.latitude.or(current.latitude),
longitude: updates.longitude.or(current.longitude),
}
}
fn print_current_config() {
let location = get_stored_location();
let settings = get_stored_prayer_settings();
let first_roza_date = get_stored_first_roza_date();
println!("Current configuration:");
if let Some(city) = location.city {
println!(" City: {city}");
}
if let Some(country) = location.country {
println!(" Country: {country}");
}
if let Some(latitude) = location.latitude {
println!(" Latitude: {latitude}");
}
if let Some(longitude) = location.longitude {
println!(" Longitude: {longitude}");
}
println!(" Method: {}", settings.method);
println!(" School: {}", settings.school);
if let Some(timezone) = settings.timezone {
println!(" Timezone: {timezone}");
}
if let Some(date) = first_roza_date {
println!(" First Roza Date: {date}");
}
}
fn has_config_update_flags(options: &ConfigCommandOptions) -> bool {
options.city.is_some()
|| options.country.is_some()
|| options.latitude.is_some()
|| options.longitude.is_some()
|| options.method.is_some()
|| options.school.is_some()
|| options.timezone.is_some()
}
pub fn config_command(options: &ConfigCommandOptions) -> Result<()> {
if options.clear {
clear_ramadan_config()?;
println!("Configuration cleared.");
return Ok(());
}
if options.show {
print_current_config();
return Ok(());
}
if !has_config_update_flags(options) {
println!("No config updates provided. Use `ramadan-cli config --show` to inspect.");
return Ok(());
}
let updates = parse_config_updates(options)?;
let current_location = get_stored_location();
let next_location = merge_location_updates(¤t_location, &updates);
set_stored_location(&next_location)?;
if let Some(method) = updates.method {
set_stored_method(method)?;
}
if let Some(school) = updates.school {
set_stored_school(school)?;
}
if let Some(timezone) = updates.timezone {
set_stored_timezone(Some(&timezone))?;
}
println!("Configuration updated.");
Ok(())
}
#[cfg(test)]
mod tests {
use super::{ConfigCommandOptions, merge_location_updates, parse_config_updates};
#[test]
fn parse_config_updates_validates_ranges() {
let parsed = parse_config_updates(&ConfigCommandOptions {
method: Some("2".to_string()),
school: Some("1".to_string()),
latitude: Some("37.7749".to_string()),
longitude: Some("-122.4194".to_string()),
..ConfigCommandOptions::default()
})
.expect("valid updates");
assert_eq!(parsed.method, Some(2));
assert_eq!(parsed.school, Some(1));
assert_eq!(parsed.latitude, Some(37.7749));
assert_eq!(parsed.longitude, Some(-122.4194));
}
#[test]
fn merge_location_updates_only_overrides_provided_fields() {
let current = crate::ramadan_config::StoredLocation {
city: Some("Lahore".to_string()),
country: Some("Pakistan".to_string()),
latitude: Some(31.5204),
longitude: Some(74.3587),
};
let merged = merge_location_updates(
¤t,
&super::ParsedConfigUpdates {
city: Some("San Francisco".to_string()),
..super::ParsedConfigUpdates::default()
},
);
assert_eq!(merged.city.as_deref(), Some("San Francisco"));
assert_eq!(merged.country.as_deref(), Some("Pakistan"));
assert_eq!(merged.latitude, Some(31.5204));
assert_eq!(merged.longitude, Some(74.3587));
}
}