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