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 = 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}