Skip to main content

longbridge/
config.rs

1use std::{
2    collections::HashMap,
3    fmt::{self, Display},
4    path::{Path, PathBuf},
5    str::FromStr,
6    sync::Arc,
7};
8
9pub(crate) use http::{HeaderName, HeaderValue, Request, header};
10use longbridge_httpcli::{
11    DC_REGION_HEADER, DcRegion, HttpClient, HttpClientConfig, Json, Method, is_cn,
12};
13use longbridge_oauth::OAuth;
14use num_enum::IntoPrimitive;
15use serde::{Deserialize, Serialize};
16use time::OffsetDateTime;
17use tokio_tungstenite::tungstenite::client::IntoClientRequest;
18use tracing::{Level, Subscriber, subscriber::NoSubscriber};
19use tracing_appender::rolling::{RollingFileAppender, Rotation};
20use tracing_subscriber::{filter::Targets, layer::SubscriberExt};
21
22use crate::error::Result;
23
24const DEFAULT_QUOTE_WS_URL: &str = "wss://openapi-quote.longbridge.com/v2";
25const DEFAULT_TRADE_WS_URL: &str = "wss://openapi-trade.longbridge.com/v2";
26const DEFAULT_QUOTE_WS_URL_CN: &str = "wss://openapi-quote.longbridge.cn/v2";
27const DEFAULT_TRADE_WS_URL_CN: &str = "wss://openapi-trade.longbridge.cn/v2";
28
29/// Language identifier
30#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, IntoPrimitive)]
31#[allow(non_camel_case_types)]
32#[repr(i32)]
33pub enum Language {
34    /// zh-CN
35    ZH_CN = 0,
36    /// zh-HK
37    ZH_HK = 2,
38    /// en
39    #[default]
40    EN = 1,
41}
42
43impl Language {
44    pub(crate) fn as_str(&self) -> &'static str {
45        match self {
46            Language::ZH_CN => "zh-CN",
47            Language::ZH_HK => "zh-HK",
48            Language::EN => "en",
49        }
50    }
51}
52
53impl FromStr for Language {
54    type Err = ();
55
56    fn from_str(s: &str) -> ::std::result::Result<Self, Self::Err> {
57        match s {
58            "zh-CN" => Ok(Language::ZH_CN),
59            "zh-HK" => Ok(Language::ZH_HK),
60            "en" => Ok(Language::EN),
61            _ => Err(()),
62        }
63    }
64}
65
66impl Display for Language {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.write_str(self.as_str())
69    }
70}
71
72/// Push mode for candlestick
73#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
74pub enum PushCandlestickMode {
75    /// Realtime mode
76    #[default]
77    Realtime,
78    /// Confirmed mode
79    Confirmed,
80}
81
82/// Internal authentication mode (not part of the public API)
83pub(crate) enum AuthMode {
84    /// Legacy API Key mode (HMAC-SHA256 signed requests)
85    ApiKey {
86        app_key: String,
87        app_secret: String,
88        access_token: String,
89    },
90    /// OAuth 2.0 mode
91    OAuth(OAuth),
92}
93
94impl Clone for AuthMode {
95    fn clone(&self) -> Self {
96        match self {
97            AuthMode::ApiKey {
98                app_key,
99                app_secret,
100                access_token,
101            } => AuthMode::ApiKey {
102                app_key: app_key.clone(),
103                app_secret: app_secret.clone(),
104                access_token: access_token.clone(),
105            },
106            AuthMode::OAuth(oauth) => AuthMode::OAuth(oauth.clone()),
107        }
108    }
109}
110
111impl fmt::Debug for AuthMode {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            AuthMode::ApiKey { app_key, .. } => {
115                f.debug_struct("ApiKey").field("app_key", app_key).finish()
116            }
117            AuthMode::OAuth(_) => f.debug_struct("OAuth").finish(),
118        }
119    }
120}
121
122/// Configuration options for Longbridge SDK
123#[derive(Debug, Clone)]
124pub struct Config {
125    pub(crate) auth: AuthMode,
126    pub(crate) http_url: Option<String>,
127    pub(crate) quote_ws_url: Option<String>,
128    pub(crate) trade_ws_url: Option<String>,
129    pub(crate) enable_overnight: Option<bool>,
130    pub(crate) push_candlestick_mode: Option<PushCandlestickMode>,
131    pub(crate) enable_print_quote_packages: bool,
132    pub(crate) language: Language,
133    pub(crate) log_path: Option<PathBuf>,
134    /// Extra headers injected into every HTTP and WebSocket upgrade request.
135    pub(crate) custom_headers: HashMap<String, String>,
136    pub(crate) enable_papertrading: bool,
137}
138
139/// Reads an env var by trying `LONGBRIDGE_<suffix>` first, then falling back
140/// to `LONGPORT_<suffix>`.  Returns `None` if neither is set.
141fn env_var(suffix: &str) -> Option<String> {
142    std::env::var(format!("LONGBRIDGE_{suffix}"))
143        .ok()
144        .or_else(|| std::env::var(format!("LONGPORT_{suffix}")).ok())
145}
146
147/// Like [`env_var`] but returns an error if the variable is not set.
148fn env_var_required(suffix: &str) -> Result<String> {
149    env_var(suffix).ok_or_else(|| {
150        longbridge_httpcli::HttpClientError::MissingEnvVar {
151            name: format!("LONGBRIDGE_{suffix}"),
152        }
153        .into()
154    })
155}
156
157/// Non-credential environment variables shared by `from_apikey` and
158/// `from_oauth`.  Callers must have already invoked `dotenv::dotenv()`.
159struct ConfigExtras {
160    http_url: Option<String>,
161    quote_ws_url: Option<String>,
162    trade_ws_url: Option<String>,
163    language: Language,
164    enable_overnight: Option<bool>,
165    push_candlestick_mode: Option<PushCandlestickMode>,
166    enable_print_quote_packages: bool,
167    log_path: Option<PathBuf>,
168    enable_papertrading: bool,
169}
170
171impl ConfigExtras {
172    fn from_env() -> Self {
173        let language = env_var("LANGUAGE")
174            .and_then(|v| v.parse::<Language>().ok())
175            .unwrap_or(Language::EN);
176        let enable_overnight = env_var("ENABLE_OVERNIGHT").map(|v| v == "true");
177        let push_candlestick_mode = env_var("PUSH_CANDLESTICK_MODE").map(|v| match v.as_str() {
178            "confirmed" => PushCandlestickMode::Confirmed,
179            _ => PushCandlestickMode::Realtime,
180        });
181        let enable_print_quote_packages =
182            env_var("PRINT_QUOTE_PACKAGES").as_deref().unwrap_or("true") == "true";
183        let enable_papertrading = env_var("PAPERTRADING").as_deref() == Some("true");
184        Self {
185            http_url: env_var("HTTP_URL"),
186            quote_ws_url: env_var("QUOTE_WS_URL"),
187            trade_ws_url: env_var("TRADE_WS_URL"),
188            language,
189            enable_overnight,
190            push_candlestick_mode,
191            enable_print_quote_packages,
192            log_path: env_var("LOG_PATH").map(PathBuf::from),
193            enable_papertrading,
194        }
195    }
196}
197
198impl Config {
199    /// Create a new `Config` using API Key authentication.
200    ///
201    /// All optional environment variables (`LONGBRIDGE_HTTP_URL`,
202    /// `LONGBRIDGE_LANGUAGE`, `LONGBRIDGE_QUOTE_WS_URL`,
203    /// `LONGBRIDGE_TRADE_WS_URL`, `LONGBRIDGE_ENABLE_OVERNIGHT`,
204    /// `LONGBRIDGE_PUSH_CANDLESTICK_MODE`,
205    /// `LONGBRIDGE_PRINT_QUOTE_PACKAGES`, `LONGBRIDGE_LOG_PATH`) are read from
206    /// the environment (or `.env` file) and applied automatically if set.
207    ///
208    /// For OAuth 2.0, use [`Config::from_oauth`] together with
209    /// [`longbridge::oauth::OAuthBuilder`] instead.
210    pub fn from_apikey(
211        app_key: impl Into<String>,
212        app_secret: impl Into<String>,
213        access_token: impl Into<String>,
214    ) -> Self {
215        let _ = dotenv::dotenv();
216        let extras = ConfigExtras::from_env();
217        Self {
218            auth: AuthMode::ApiKey {
219                app_key: app_key.into(),
220                app_secret: app_secret.into(),
221                access_token: access_token.into(),
222            },
223            http_url: extras.http_url,
224            quote_ws_url: extras.quote_ws_url,
225            trade_ws_url: extras.trade_ws_url,
226            language: extras.language,
227            enable_overnight: extras.enable_overnight,
228            push_candlestick_mode: extras.push_candlestick_mode,
229            enable_print_quote_packages: extras.enable_print_quote_packages,
230            log_path: extras.log_path,
231            custom_headers: Default::default(),
232            enable_papertrading: extras.enable_papertrading,
233        }
234    }
235
236    /// Create a new `Config` for OAuth 2.0 authentication.
237    ///
238    /// All optional environment variables (`LONGBRIDGE_HTTP_URL`,
239    /// `LONGBRIDGE_LANGUAGE`, `LONGBRIDGE_QUOTE_WS_URL`,
240    /// `LONGBRIDGE_TRADE_WS_URL`, `LONGBRIDGE_ENABLE_OVERNIGHT`,
241    /// `LONGBRIDGE_PUSH_CANDLESTICK_MODE`,
242    /// `LONGBRIDGE_PRINT_QUOTE_PACKAGES`, `LONGBRIDGE_LOG_PATH`) are read from
243    /// the environment (or `.env` file) and applied automatically if set.
244    ///
245    /// # Arguments
246    ///
247    /// * `oauth` - An [`OAuth`] client obtained from
248    ///   [`longbridge::oauth::OAuthBuilder`].
249    ///
250    /// # Example
251    ///
252    /// ```rust,no_run
253    /// use std::sync::Arc;
254    ///
255    /// use longbridge::{Config, oauth::OAuthBuilder};
256    ///
257    /// #[tokio::main]
258    /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
259    ///     let oauth = OAuthBuilder::new("your-client-id")
260    ///         .build(|url| println!("Visit: {url}"))
261    ///         .await?;
262    ///     let config = Arc::new(Config::from_oauth(oauth));
263    ///
264    ///     let (ctx, receiver) = longbridge::quote::QuoteContext::new(config);
265    ///     Ok(())
266    /// }
267    /// ```
268    pub fn from_oauth(oauth: OAuth) -> Self {
269        let _ = dotenv::dotenv();
270        let extras = ConfigExtras::from_env();
271        Self {
272            auth: AuthMode::OAuth(oauth),
273            http_url: extras.http_url,
274            quote_ws_url: extras.quote_ws_url,
275            trade_ws_url: extras.trade_ws_url,
276            language: extras.language,
277            enable_overnight: extras.enable_overnight,
278            push_candlestick_mode: extras.push_candlestick_mode,
279            enable_print_quote_packages: extras.enable_print_quote_packages,
280            log_path: extras.log_path,
281            custom_headers: Default::default(),
282            enable_papertrading: extras.enable_papertrading,
283        }
284    }
285
286    /// Create a new `Config` from environment variables (API Key
287    /// authentication).
288    ///
289    /// It first loads the environment variables from the `.env` file in the
290    /// current directory.
291    ///
292    /// # Variables
293    ///
294    /// - `LONGBRIDGE_APP_KEY` - App key
295    /// - `LONGBRIDGE_APP_SECRET` - App secret
296    /// - `LONGBRIDGE_ACCESS_TOKEN` - Access token
297    /// - `LONGBRIDGE_LANGUAGE` - Language identifier, `zh-CN`, `zh-HK` or `en`
298    ///   (Default: `en`)
299    /// - `LONGBRIDGE_HTTP_URL` - HTTP endpoint url (Default: `https://openapi.longbridge.com`)
300    /// - `LONGBRIDGE_QUOTE_WS_URL` - Quote websocket endpoint url (Default:
301    ///   `wss://openapi-quote.longbridge.com/v2`)
302    /// - `LONGBRIDGE_TRADE_WS_URL` - Trade websocket endpoint url (Default:
303    ///   `wss://openapi-trade.longbridge.com/v2`)
304    /// - `LONGBRIDGE_ENABLE_OVERNIGHT` - Enable overnight quote, `true` or
305    ///   `false` (Default: `false`)
306    /// - `LONGBRIDGE_PUSH_CANDLESTICK_MODE` - `realtime` or `confirmed`
307    ///   (Default: `realtime`)
308    /// - `LONGBRIDGE_PRINT_QUOTE_PACKAGES` - Print quote packages when
309    ///   connected, `true` or `false` (Default: `true`)
310    /// - `LONGBRIDGE_LOG_PATH` - Set the path of the log files (Default: `no
311    ///   logs`)
312    ///
313    /// For OAuth 2.0 authentication use [`from_oauth`](Config::from_oauth)
314    /// together with [`OAuthBuilder`](longbridge_oauth::OAuthBuilder).
315    pub fn from_apikey_env() -> Result<Self> {
316        let _ = dotenv::dotenv();
317
318        let app_key = env_var_required("APP_KEY")?;
319        let app_secret = env_var_required("APP_SECRET")?;
320        let access_token = env_var_required("ACCESS_TOKEN")?;
321        let extras = ConfigExtras::from_env();
322
323        Ok(Config {
324            auth: AuthMode::ApiKey {
325                app_key,
326                app_secret,
327                access_token,
328            },
329            http_url: extras.http_url,
330            quote_ws_url: extras.quote_ws_url,
331            trade_ws_url: extras.trade_ws_url,
332            language: extras.language,
333            enable_overnight: extras.enable_overnight,
334            push_candlestick_mode: extras.push_candlestick_mode,
335            enable_print_quote_packages: extras.enable_print_quote_packages,
336            log_path: extras.log_path,
337            custom_headers: Default::default(),
338            enable_papertrading: extras.enable_papertrading,
339        })
340    }
341
342    /// Specifies the url of the OpenAPI server.
343    ///
344    /// Default: `https://openapi.longbridge.com`
345    ///
346    /// NOTE: Usually you don't need to change it.
347    #[must_use]
348    pub fn http_url(mut self, url: impl Into<String>) -> Self {
349        self.http_url = Some(url.into());
350        self
351    }
352
353    /// Specifies the url of the OpenAPI quote websocket server.
354    ///
355    /// Default: `wss://openapi-quote.longbridge.com`
356    ///
357    /// NOTE: Usually you don't need to change it.
358    #[must_use]
359    pub fn quote_ws_url(self, url: impl Into<String>) -> Self {
360        Self {
361            quote_ws_url: Some(url.into()),
362            ..self
363        }
364    }
365
366    /// Specifies the url of the OpenAPI trade websocket server.
367    ///
368    /// Default: `wss://openapi-trade.longbridge.com/v2`
369    ///
370    /// NOTE: Usually you don't need to change it.
371    #[must_use]
372    pub fn trade_ws_url(self, url: impl Into<String>) -> Self {
373        Self {
374            trade_ws_url: Some(url.into()),
375            ..self
376        }
377    }
378
379    /// Specifies the language
380    ///
381    /// Default: `Language::EN`
382    pub fn language(self, language: Language) -> Self {
383        Self { language, ..self }
384    }
385
386    /// Enable overnight quote
387    ///
388    /// Default: `false`
389    pub fn enable_overnight(self) -> Self {
390        Self {
391            enable_overnight: Some(true),
392            ..self
393        }
394    }
395
396    /// Specifies the push candlestick mode
397    ///
398    /// Default: `PushCandlestickMode::Realtime`
399    pub fn push_candlestick_mode(self, mode: PushCandlestickMode) -> Self {
400        Self {
401            push_candlestick_mode: Some(mode),
402            ..self
403        }
404    }
405
406    /// Disable printing the opened quote packages when connected to the server.
407    pub fn dont_print_quote_packages(self) -> Self {
408        Self {
409            enable_print_quote_packages: false,
410            ..self
411        }
412    }
413
414    /// Enable paper trading mode.
415    ///
416    /// When enabled, all API calls target the paper trading (simulation)
417    /// environment.  The server validates the token: if it belongs to a
418    /// real-money account the server returns an error.
419    ///
420    /// By default this option is disabled (`false`): the server imposes no
421    /// restrictions and accepts requests from both paper trading and real-money
422    /// accounts.
423    ///
424    /// Paper trading users should enable this option as a safety guard to avoid
425    /// accidentally submitting orders against their real-money account.
426    pub fn enable_papertrading(mut self) -> Self {
427        self.enable_papertrading = true;
428        self
429    }
430
431    /// Create metadata for auth/reconnect request
432    pub fn create_metadata(&self) -> HashMap<String, String> {
433        let mut metadata = HashMap::new();
434        metadata.insert("accept-language".to_string(), self.language.to_string());
435        if self.enable_overnight.unwrap_or_default() {
436            metadata.insert("need_over_night_quote".to_string(), "true".to_string());
437        }
438        metadata
439    }
440
441    #[inline]
442    pub(crate) fn create_http_client(&self) -> HttpClient {
443        let mut config = match &self.auth {
444            AuthMode::ApiKey {
445                app_key,
446                app_secret,
447                access_token,
448            } => HttpClientConfig::from_apikey(app_key, app_secret, access_token),
449            AuthMode::OAuth(oauth) => HttpClientConfig::from_oauth(oauth.clone()),
450        };
451        if let Some(url) = &self.http_url {
452            config = config.http_url(url.clone());
453        }
454
455        let mut client =
456            HttpClient::new(config).header(header::ACCEPT_LANGUAGE, self.language.as_str());
457        for (key, value) in &self.custom_headers {
458            client = client.header(key.as_str(), value.as_str());
459        }
460        if self.enable_papertrading {
461            client = client.header("x-papertrading", "true");
462        }
463        client
464    }
465
466    /// Gets a new `access_token`
467    ///
468    /// This method is only available when using **Legacy API Key**
469    /// authentication (i.e. [`Config::from_apikey`]). It is not supported
470    /// for OAuth 2.0 mode.
471    ///
472    /// `expired_at` - The expiration time of the access token, defaults to `90`
473    /// days.
474    ///
475    /// Reference: <https://open.longportapp.com/en/docs/refresh-token-api>
476    pub async fn refresh_access_token(&self, expired_at: Option<OffsetDateTime>) -> Result<String> {
477        #[derive(Debug, Serialize)]
478        struct Request {
479            expired_at: String,
480        }
481
482        #[derive(Debug, Deserialize)]
483        struct Response {
484            token: String,
485        }
486
487        let request = Request {
488            expired_at: expired_at
489                .unwrap_or_else(|| OffsetDateTime::now_utc() + time::Duration::days(90))
490                .format(&time::format_description::well_known::Rfc3339)
491                .unwrap(),
492        };
493
494        let new_token = self
495            .create_http_client()
496            .request(Method::GET, "/v1/token/refresh")
497            .query_params(request)
498            .response::<Json<Response>>()
499            .send()
500            .await?
501            .0
502            .token;
503        Ok(new_token)
504    }
505
506    /// Gets a new `access_token`, and also replaces the `access_token` in
507    /// `Config`.
508    ///
509    /// This method is only available when using **Legacy API Key**
510    /// authentication (i.e. [`Config::from_apikey`]). It is not supported
511    /// for OAuth 2.0 mode.
512    ///
513    /// `expired_at` - The expiration time of the access token, defaults to `90`
514    /// days.
515    ///
516    /// Reference: <https://open.longportapp.com/en/docs/refresh-token-api>
517    #[cfg(feature = "blocking")]
518    #[cfg_attr(docsrs, doc(cfg(feature = "blocking")))]
519    pub fn refresh_access_token_blocking(
520        &self,
521        expired_at: Option<OffsetDateTime>,
522    ) -> Result<String> {
523        tokio::runtime::Builder::new_current_thread()
524            .enable_all()
525            .build()
526            .expect("create tokio runtime")
527            .block_on(self.refresh_access_token(expired_at))
528    }
529
530    /// Resolve the data-center region from the auth credentials. For OAuth the
531    /// access token is resolved (and refreshed if needed); for legacy API-key
532    /// mode the `app_key`/`app_secret`/`access_token` prefixes are inspected.
533    async fn auth_dc_region(&self) -> DcRegion {
534        match &self.auth {
535            AuthMode::ApiKey {
536                app_key,
537                app_secret,
538                access_token,
539            } => DcRegion::from_credentials(&[app_key, access_token, app_secret]),
540            AuthMode::OAuth(oauth) => oauth
541                .access_token()
542                .await
543                .map(|token| DcRegion::from_credential(&token))
544                .unwrap_or(DcRegion::Ap),
545        }
546    }
547
548    fn create_ws_request(
549        &self,
550        url: &str,
551        dc_region: DcRegion,
552    ) -> tokio_tungstenite::tungstenite::Result<Request<()>> {
553        let mut request = url.into_client_request()?;
554        request.headers_mut().append(
555            header::ACCEPT_LANGUAGE,
556            HeaderValue::from_str(self.language.as_str()).unwrap(),
557        );
558        for (key, value) in &self.custom_headers {
559            if let (Ok(name), Ok(val)) = (
560                HeaderName::from_bytes(key.as_bytes()),
561                HeaderValue::from_str(value),
562            ) {
563                request.headers_mut().append(name, val);
564            }
565        }
566        // Route the upgrade to the data center matching the credential's region,
567        // unless the caller already set the header via a custom header.
568        if !self
569            .custom_headers
570            .keys()
571            .any(|key| key.eq_ignore_ascii_case(DC_REGION_HEADER))
572        {
573            request.headers_mut().append(
574                HeaderName::from_static(DC_REGION_HEADER),
575                HeaderValue::from_static(dc_region.as_str()),
576            );
577        }
578        Ok(request)
579    }
580
581    pub(crate) async fn create_quote_ws_request(
582        &self,
583    ) -> (&str, tokio_tungstenite::tungstenite::Result<Request<()>>) {
584        let dc_region = self.auth_dc_region().await;
585        match self.quote_ws_url.as_deref() {
586            Some(url) => (url, self.create_ws_request(url, dc_region)),
587            None => {
588                let url = if is_cn().await {
589                    DEFAULT_QUOTE_WS_URL_CN
590                } else {
591                    DEFAULT_QUOTE_WS_URL
592                };
593                (url, self.create_ws_request(url, dc_region))
594            }
595        }
596    }
597
598    pub(crate) async fn create_trade_ws_request(
599        &self,
600    ) -> (&str, tokio_tungstenite::tungstenite::Result<Request<()>>) {
601        let dc_region = self.auth_dc_region().await;
602        match self.trade_ws_url.as_deref() {
603            Some(url) => (url, self.create_ws_request(url, dc_region)),
604            None => {
605                let url = if is_cn().await {
606                    DEFAULT_TRADE_WS_URL_CN
607                } else {
608                    DEFAULT_TRADE_WS_URL
609                };
610                (url, self.create_ws_request(url, dc_region))
611            }
612        }
613    }
614
615    /// Specifies the path of the log file
616    ///
617    /// Default: `None`
618    pub fn log_path(mut self, path: impl Into<PathBuf>) -> Self {
619        self.log_path = Some(path.into());
620        self
621    }
622
623    /// Add a custom header to every HTTP request and WebSocket upgrade request.
624    #[must_use]
625    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
626        self.custom_headers.insert(key.into(), value.into());
627        self
628    }
629
630    /// Override the data-center region for every HTTP and WebSocket request by
631    /// setting the [`DC_REGION_HEADER`](crate::DC_REGION_HEADER) explicitly.
632    ///
633    /// This is rarely needed: the region is otherwise derived automatically
634    /// from the auth credentials' prefix (`us_`/`ap_`). Use this only to
635    /// force a specific data center regardless of the credential.
636    #[must_use]
637    pub fn dc_region(self, region: DcRegion) -> Self {
638        self.header(DC_REGION_HEADER, region.as_str())
639    }
640
641    /// Set the HTTP endpoint URL in place.
642    pub fn set_http_url(&mut self, url: impl Into<String>) {
643        self.http_url = Some(url.into());
644    }
645
646    /// Set the quote websocket endpoint URL in place.
647    pub fn set_quote_ws_url(&mut self, url: impl Into<String>) {
648        self.quote_ws_url = Some(url.into());
649    }
650
651    /// Set the trade websocket endpoint URL in place.
652    pub fn set_trade_ws_url(&mut self, url: impl Into<String>) {
653        self.trade_ws_url = Some(url.into());
654    }
655
656    /// Set the language in place.
657    pub fn set_language(&mut self, language: Language) {
658        self.language = language;
659    }
660
661    /// Enable overnight quote in place.
662    pub fn set_enable_overnight(&mut self) {
663        self.enable_overnight = Some(true);
664    }
665
666    /// Set the push candlestick mode in place.
667    pub fn set_push_candlestick_mode(&mut self, mode: PushCandlestickMode) {
668        self.push_candlestick_mode = Some(mode);
669    }
670
671    /// Disable printing quote packages in place.
672    pub fn set_dont_print_quote_packages(&mut self) {
673        self.enable_print_quote_packages = false;
674    }
675
676    /// Enable paper trading mode in place.
677    ///
678    /// See [`Config::enable_papertrading`] for full semantics.
679    pub fn set_enable_papertrading(&mut self) {
680        self.enable_papertrading = true;
681    }
682
683    /// Set the log path in place.
684    pub fn set_log_path(&mut self, path: impl Into<PathBuf>) {
685        self.log_path = Some(path.into());
686    }
687
688    pub(crate) fn create_log_subscriber(
689        &self,
690        path: impl AsRef<Path>,
691    ) -> Arc<dyn Subscriber + Send + Sync> {
692        fn internal_create_log_subscriber(
693            config: &Config,
694            path: impl AsRef<Path>,
695        ) -> Option<Arc<dyn Subscriber + Send + Sync>> {
696            let log_path = config.log_path.as_ref()?;
697            let appender = RollingFileAppender::builder()
698                .rotation(Rotation::DAILY)
699                .filename_suffix("log")
700                .build(log_path.join(path))
701                .ok()?;
702            Some(Arc::new(
703                tracing_subscriber::fmt()
704                    .with_writer(appender)
705                    .with_ansi(false)
706                    .finish()
707                    .with(Targets::new().with_targets([("longbridge", Level::INFO)])),
708            ))
709        }
710
711        internal_create_log_subscriber(self, path).unwrap_or_else(|| Arc::new(NoSubscriber::new()))
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_config_from_apikey() {
721        let config = Config::from_apikey("app-key", "app-secret", "token");
722        assert_eq!(config.language, Language::EN);
723        match &config.auth {
724            AuthMode::ApiKey {
725                app_key,
726                app_secret,
727                access_token,
728            } => {
729                assert_eq!(app_key, "app-key");
730                assert_eq!(app_secret, "app-secret");
731                assert_eq!(access_token, "token");
732            }
733            _ => panic!("Expected ApiKey auth mode"),
734        }
735    }
736
737    #[test]
738    fn test_config_default_values() {
739        let config = Config::from_apikey("key", "secret", "token");
740
741        // Fields not controlled by environment variables
742        assert_eq!(config.enable_overnight, None);
743        assert_eq!(config.push_candlestick_mode, None);
744        assert!(config.enable_print_quote_packages);
745    }
746
747    #[test]
748    fn test_enable_papertrading_builder() {
749        let config = Config::from_apikey("key", "secret", "token").enable_papertrading();
750        assert!(config.enable_papertrading);
751    }
752
753    #[test]
754    fn test_enable_papertrading_setter() {
755        let mut config = Config::from_apikey("key", "secret", "token");
756        config.set_enable_papertrading();
757        assert!(config.enable_papertrading);
758    }
759}