1use std::path::PathBuf;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{OAUTH_AUTHORIZE_URL, OAUTH_TOKEN_URL, TRADER_BASE_URL};
6
7#[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}