Skip to main content

schwab_api/
config.rs

1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL, TRADER_BASE_URL};
6
7/// Runtime configuration for the Schwab API client.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ClientConfig {
10    pub app_key: String,
11    pub app_secret: String,
12    pub redirect_uri: String,
13    pub trader_base_url: String,
14    pub oauth_authorize_url: String,
15    pub oauth_token_url: String,
16    pub token_dir: PathBuf,
17}
18
19impl ClientConfig {
20    pub fn from_env() -> anyhow::Result<Self> {
21        let app_key = env_first(&["SCHWAB_APP_KEY", "SCHWAB_CLIENT_ID"])
22            .ok_or_else(|| anyhow::anyhow!("SCHWAB_APP_KEY (or SCHWAB_CLIENT_ID) is required"))?;
23        let app_secret =
24            env_first(&["SCHWAB_APP_SECRET", "SCHWAB_CLIENT_SECRET"]).ok_or_else(|| {
25                anyhow::anyhow!("SCHWAB_APP_SECRET (or SCHWAB_CLIENT_SECRET) is required")
26            })?;
27        let redirect_uri = std::env::var("SCHWAB_REDIRECT_URI")
28            .unwrap_or_else(|_| "https://127.0.0.1:8182".to_string());
29        let token_dir = std::env::var("SCHWAB_TOKEN_DIR")
30            .map(PathBuf::from)
31            .unwrap_or_else(|_| default_token_dir());
32
33        Ok(Self {
34            app_key,
35            app_secret,
36            redirect_uri,
37            trader_base_url: TRADER_BASE_URL.to_string(),
38            oauth_authorize_url: OAUTH_AUTHORIZE_URL.to_string(),
39            oauth_token_url: OAUTH_TOKEN_URL.to_string(),
40            token_dir,
41        })
42    }
43
44    pub fn for_tests() -> Self {
45        Self {
46            app_key: "test-key".into(),
47            app_secret: "test-secret".into(),
48            redirect_uri: "https://127.0.0.1:8182".into(),
49            trader_base_url: TRADER_BASE_URL.to_string(),
50            oauth_authorize_url: OAUTH_AUTHORIZE_URL.to_string(),
51            oauth_token_url: OAUTH_TOKEN_URL.to_string(),
52            token_dir: std::env::temp_dir().join("schwabinvestbot-test"),
53        }
54    }
55}
56
57fn env_first(keys: &[&str]) -> Option<String> {
58    keys.iter()
59        .find_map(|key| std::env::var(key).ok())
60        .filter(|v| !v.is_empty())
61}
62
63fn default_token_dir() -> PathBuf {
64    directories::ProjectDirs::from("", "", "schwabinvestbot")
65        .map(|dirs| dirs.config_dir().to_path_buf())
66        .unwrap_or_else(|| PathBuf::from(".schwabinvestbot"))
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn for_tests_has_defaults() {
75        let cfg = ClientConfig::for_tests();
76        assert_eq!(cfg.trader_base_url, TRADER_BASE_URL);
77    }
78}