1use std::path::{Path, PathBuf};
39
40use serde::Deserialize;
41
42use crate::cache::CacheConfig;
43use crate::error::{Error, Result};
44
45pub const DEFAULT_CDP: &str = "http://127.0.0.1:9222";
46
47#[derive(Debug, Clone, Default, Deserialize)]
48#[serde(default, deny_unknown_fields)]
49pub struct Config {
50 pub allow_ordering: bool,
51 pub payment_type_id: Option<String>,
52 pub cdp: Option<String>,
53 pub auto_refresh: Option<bool>,
54 pub hydra_mode: Option<HydraMode>,
55 pub hydra_key: Option<PathBuf>,
57 pub hydra_page: Option<String>,
60 pub user_agent: Option<String>,
63 pub phone: Option<String>,
66 pub cache: CacheConfig,
68}
69
70#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
71#[serde(rename_all = "lowercase")]
72pub enum HydraMode {
73 #[default]
74 Auto,
75 Native,
76 Cdp,
77}
78
79impl std::str::FromStr for HydraMode {
80 type Err = String;
81 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
82 match s.to_ascii_lowercase().as_str() {
83 "auto" => Ok(Self::Auto),
84 "native" => Ok(Self::Native),
85 "cdp" => Ok(Self::Cdp),
86 other => Err(format!("unknown hydra mode `{other}` (auto|native|cdp)")),
87 }
88 }
89}
90
91impl Config {
92 pub fn load(path: &Path) -> Result<Self> {
93 match std::fs::read_to_string(path) {
94 Ok(s) => Self::parse(&s)
95 .map_err(|e| Error::new(e.kind, format!("{}: {}", path.display(), e.message))),
96 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()),
97 Err(e) => Err(e.into()),
98 }
99 }
100
101 pub fn parse(s: &str) -> Result<Self> {
103 let c: Self = toml::from_str(s).map_err(|e| Error::invalid(e.to_string()))?;
104 if let Some(p) = &c.phone {
105 crate::login::split_phone(p)
107 .map_err(|e| Error::invalid(format!("`phone`: {}", e.message)))?;
108 }
109 Ok(c)
110 }
111
112 pub fn auto_refresh(&self) -> bool {
113 self.auto_refresh.unwrap_or(true)
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn defaults_keep_ordering_off() -> Result<()> {
123 let dir = tempfile::tempdir()?;
124 let c = Config::load(&dir.path().join("missing.toml"))?;
125 assert!(!c.allow_ordering);
126 assert!(c.auto_refresh());
127 Ok(())
128 }
129
130 #[test]
131 fn parses_keys() -> Result<()> {
132 let c: Config = toml::from_str(
133 "allow_ordering = true\npayment_type_id = \"42\"\nhydra_mode = \"cdp\"\n",
134 )
135 .map_err(|e| Error::invalid(e.to_string()))?;
136 assert!(c.allow_ordering);
137 assert_eq!(c.payment_type_id.as_deref(), Some("42"));
138 assert_eq!(c.hydra_mode, Some(HydraMode::Cdp));
139 Ok(())
140 }
141
142 #[test]
143 fn parses_cache_section() -> Result<()> {
144 use crate::cache::CacheKind;
145 let c: Config = toml::from_str("[cache]\nenabled = false\n[cache.ttl]\nmenu = 60\n")
146 .map_err(|e| Error::invalid(e.to_string()))?;
147 assert!(!c.cache.enabled());
148 assert_eq!(c.cache.ttl(CacheKind::Menu), 60);
149 assert_eq!(c.cache.ttl(CacheKind::List), CacheKind::List.default_ttl());
150 assert!(toml::from_str::<Config>("[cache.ttl]\nquote = 60\n").is_err());
151 Ok(())
152 }
153
154 #[test]
155 fn phone_is_optional() -> Result<()> {
156 assert_eq!(Config::parse("allow_ordering = false\n")?.phone, None);
157 let c = Config::parse("phone = \"+63 900 000 0012\"\n")?;
158 assert_eq!(c.phone.as_deref(), Some("+63 900 000 0012"));
159 Ok(())
160 }
161
162 #[test]
163 fn rejects_malformed_phone() -> Result<()> {
164 for bad in ["09000000012", "+1 555 000 0000", "+63 12", "+63abc", ""] {
165 let e = Config::parse(&format!("phone = \"{bad}\"\n")).err();
166 let e = e.ok_or_else(|| Error::invalid(format!("accepted {bad:?}")))?;
167 assert!(e.message.starts_with("`phone`: "), "{}", e.message);
168 }
169 let dir = tempfile::tempdir()?;
170 let path = dir.path().join("config.toml");
171 std::fs::write(&path, "phone = \"0900 000 0012\"\n")?;
172 let e = Config::load(&path)
173 .err()
174 .ok_or_else(|| Error::invalid("loaded"))?;
175 assert!(
176 e.message
177 .contains("config.toml: `phone`: phone must be in international form")
178 );
179 assert!(!e.message.contains("0012"));
180 Ok(())
181 }
182
183 #[test]
184 fn rejects_unknown_keys() {
185 let r: std::result::Result<Config, _> = toml::from_str("allow_orderin = true");
186 assert!(r.is_err());
187 }
188}