Skip to main content

grabcli_core/
config.rs

1//! `config.toml`: user settings. Every key is optional. The library only
2//! parses text ([`Config::parse`]) or takes a struct; reading the file (and
3//! the hydra key it points to) is the CLI's job.
4//!
5//! ```toml
6//! # Placing real orders stays impossible until the user sets this to true
7//! # themselves. grabcli never writes this key.
8//! allow_ordering = false
9//!
10//! # Default payment method for quotes and orders (Grab's paymentTypeID).
11//! payment_type_id = "123456789"
12//!
13//! # Chrome DevTools Protocol endpoint of a logged-in browser, used by
14//! # `grab auth import` and as hydra/cookie refresh fallback.
15//! cdp = "http://127.0.0.1:9222"
16//!
17//! # Refresh hydra / cookies automatically when they expire (default true).
18//! auto_refresh = true
19//!
20//! # How to mint the hydra device JWT: "auto" (native if a key is present,
21//! # else CDP), "native" or "cdp".
22//! hydra_mode = "auto"
23//!
24//! # Phone number `grab auth login` uses when --phone is not given
25//! # (international form). Only ever printed masked.
26//! phone = "+63XXXXXXXXXX"
27//!
28//! # Local cache for read-only browse data (see cache/mod.rs). TTLs in seconds.
29//! [cache]
30//! enabled = true
31//! image_max_mb = 500
32//! index_max_age_days = 30
33//! [cache.ttl]
34//! list = 600
35//! menu = 1800
36//! details = 21600
37//! categories = 86400
38//! ```
39
40use 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    /// Path to the hydra signing key (PEM), read by the CLI (default
58    /// `<config dir>/hydra-key.pem`) and passed via
59    /// [`Context::with_hydra_key`](crate::Context::with_hydra_key).
60    pub hydra_key: Option<PathBuf>,
61    /// Page the CDP hydra minter opens. Must be a food.grab.com page that
62    /// makes a hydra-protected API call.
63    pub hydra_page: Option<String>,
64    /// Override the User-Agent sent to Grab (default: the browser's, saved at
65    /// import time).
66    pub user_agent: Option<String>,
67    /// Default phone for `grab auth login` (international form, `+63…`).
68    /// Validated on load; only ever printed masked.
69    pub phone: Option<String>,
70    /// `[cache]`: response cache switch and per-kind TTLs.
71    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    /// Parse and validate `config.toml` text.
97    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            // The error never echoes the number.
101            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}