longport_httpcli/config.rs
1use longport_oauth::OAuth;
2
3use crate::HttpClientError;
4
5fn env_var(suffix: &str) -> Option<String> {
6 std::env::var(format!("LONGPORT_{suffix}")).ok()
7}
8
9/// Like [`env_var`] but returns an error when the variable is not set.
10fn env_var_required(suffix: &str) -> Result<String, HttpClientError> {
11 env_var(suffix).ok_or_else(|| HttpClientError::MissingEnvVar {
12 name: format!("LONGPORT_{suffix}"),
13 })
14}
15
16/// Authentication configuration
17#[derive(Debug, Clone)]
18pub enum AuthConfig {
19 /// Legacy API Key mode: HMAC-SHA256 signed requests
20 ApiKey {
21 /// App key
22 app_key: String,
23 /// App secret (used for HMAC-SHA256 signing)
24 app_secret: String,
25 /// Static access token
26 access_token: String,
27 },
28 /// OAuth 2.0 mode: Bearer token, auto-refreshed via the [`OAuth`] client
29 OAuth(OAuth),
30}
31
32/// Configuration options for Http client
33#[derive(Debug, Clone)]
34pub struct HttpClientConfig {
35 /// HTTP API url
36 pub(crate) http_url: Option<String>,
37 /// Authentication configuration
38 pub(crate) auth: AuthConfig,
39}
40
41impl HttpClientConfig {
42 /// Create a new `HttpClientConfig` using API Key authentication.
43 ///
44 /// `LONGPORT_HTTP_URL` is read from the environment (or `.env` file) and
45 /// applied automatically if set.
46 ///
47 /// # Arguments
48 ///
49 /// * `app_key` - Application key
50 /// * `app_secret` - Application secret (used for request signing)
51 /// * `access_token` - Access token
52 pub fn from_apikey(
53 app_key: impl Into<String>,
54 app_secret: impl Into<String>,
55 access_token: impl Into<String>,
56 ) -> Self {
57 let _ = dotenv::dotenv();
58 Self {
59 http_url: env_var("HTTP_URL"),
60 auth: AuthConfig::ApiKey {
61 app_key: app_key.into(),
62 app_secret: app_secret.into(),
63 access_token: access_token.into(),
64 },
65 }
66 }
67
68 /// Create a new `HttpClientConfig` for OAuth 2.0 authentication.
69 ///
70 /// `LONGPORT_HTTP_URL` is read from the environment (or `.env` file) and
71 /// applied automatically if set.
72 ///
73 /// The [`OAuth`] client handles token lifecycle automatically, including
74 /// expiry checks and token refresh.
75 ///
76 /// # Arguments
77 ///
78 /// * `oauth` - An [`OAuth`] client obtained from
79 /// [`longport_oauth::OAuthBuilder`]
80 pub fn from_oauth(oauth: OAuth) -> Self {
81 let _ = dotenv::dotenv();
82 Self {
83 http_url: env_var("HTTP_URL"),
84 auth: AuthConfig::OAuth(oauth),
85 }
86 }
87
88 /// Create a new `HttpClientConfig` from environment variables (API Key
89 /// mode).
90 ///
91 /// # Variables
92 ///
93 /// - `LONGPORT_APP_KEY` - App key (required)
94 /// - `LONGPORT_APP_SECRET` - App secret (required)
95 /// - `LONGPORT_ACCESS_TOKEN` - Access token (required)
96 /// - `LONGPORT_HTTP_URL` - HTTP endpoint URL (optional)
97 ///
98 /// # Note
99 ///
100 /// For OAuth 2.0 authentication, use
101 /// [`from_oauth`](HttpClientConfig::from_oauth) instead.
102 pub fn from_apikey_env() -> Result<Self, HttpClientError> {
103 let _ = dotenv::dotenv();
104
105 let app_key = env_var_required("APP_KEY")?;
106 let app_secret = env_var_required("APP_SECRET")?;
107 let access_token = env_var_required("ACCESS_TOKEN")?;
108
109 Ok(Self {
110 http_url: env_var("HTTP_URL"),
111 auth: AuthConfig::ApiKey {
112 app_key,
113 app_secret,
114 access_token,
115 },
116 })
117 }
118
119 /// Specifies the url of the OpenAPI server.
120 ///
121 /// Default: <https://openapi.longportapp.com>
122 /// NOTE: Usually you don't need to change it.
123 #[must_use]
124 pub fn http_url(self, url: impl Into<String>) -> Self {
125 Self {
126 http_url: Some(url.into()),
127 ..self
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_httpclient_config_new() {
138 let config = HttpClientConfig::from_apikey("app-key", "app-secret", "access-token");
139
140 match &config.auth {
141 AuthConfig::ApiKey {
142 app_key,
143 app_secret,
144 access_token,
145 } => {
146 assert_eq!(app_key, "app-key");
147 assert_eq!(app_secret, "app-secret");
148 assert_eq!(access_token, "access-token");
149 }
150 _ => panic!("Expected ApiKey auth config"),
151 }
152 assert_eq!(config.http_url, None);
153 }
154
155 #[test]
156 fn test_httpclient_config_http_url() {
157 let config = HttpClientConfig::from_apikey("app-key", "app-secret", "access-token")
158 .http_url("https://custom.example.com");
159
160 assert_eq!(
161 config.http_url,
162 Some("https://custom.example.com".to_string())
163 );
164 }
165}