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 = env_first(&["SCHWAB_APP_SECRET", "SCHWAB_CLIENT_SECRET"])
24            .ok_or_else(|| anyhow::anyhow!("SCHWAB_APP_SECRET (or SCHWAB_CLIENT_SECRET) is required"))?;
25        let redirect_uri = std::env::var("SCHWAB_REDIRECT_URI")
26            .unwrap_or_else(|_| "https://127.0.0.1:8182".to_string());
27        let token_dir = std::env::var("SCHWAB_TOKEN_DIR")
28            .map(PathBuf::from)
29            .unwrap_or_else(|_| default_token_dir());
30
31        Ok(Self {
32            app_key,
33            app_secret,
34            redirect_uri,
35            trader_base_url: TRADER_BASE_URL.to_string(),
36            oauth_authorize_url: OAUTH_AUTHORIZE_URL.to_string(),
37            oauth_token_url: OAUTH_TOKEN_URL.to_string(),
38            token_dir,
39        })
40    }
41
42    pub fn for_tests() -> Self {
43        Self {
44            app_key: "test-key".into(),
45            app_secret: "test-secret".into(),
46            redirect_uri: "https://127.0.0.1:8182".into(),
47            trader_base_url: TRADER_BASE_URL.to_string(),
48            oauth_authorize_url: OAUTH_AUTHORIZE_URL.to_string(),
49            oauth_token_url: OAUTH_TOKEN_URL.to_string(),
50            token_dir: std::env::temp_dir().join("schwabinvestbot-test"),
51        }
52    }
53}
54
55fn env_first(keys: &[&str]) -> Option<String> {
56    keys.iter()
57        .find_map(|key| std::env::var(key).ok())
58        .filter(|v| !v.is_empty())
59}
60
61fn default_token_dir() -> PathBuf {
62    directories::ProjectDirs::from("", "", "schwabinvestbot")
63        .map(|dirs| dirs.config_dir().to_path_buf())
64        .unwrap_or_else(|| PathBuf::from(".schwabinvestbot"))
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn for_tests_has_defaults() {
73        let cfg = ClientConfig::for_tests();
74        assert_eq!(cfg.trader_base_url, TRADER_BASE_URL);
75    }
76}