brokapi_alor_protocol/config.rs
1//! Client configuration: the refresh token, the contour, the timeout and the base URLs.
2
3use std::time::Duration;
4
5/// The production contour: real exchanges, real money. Orders go to the exchange.
6pub const PROD_HTTP_BASE_URL: &str = "https://api.alor.ru";
7/// The authorization server of the production contour (the access token is issued here).
8pub const PROD_AUTH_BASE_URL: &str = "https://oauth.alor.ru";
9/// The WebSocket host of the production contour.
10pub const PROD_WS_BASE_URL: &str = "wss://api.alor.ru";
11/// The test contour: simulated data and a test account, no effect on the market.
12pub const TEST_HTTP_BASE_URL: &str = "https://apidev.alor.ru";
13/// The authorization server of the test contour.
14pub const TEST_AUTH_BASE_URL: &str = "https://oauthdev.alor.ru";
15/// The WebSocket host of the test contour.
16pub const TEST_WS_BASE_URL: &str = "wss://apidev.alor.ru";
17
18/// Which contour of the broker a client talks to. The two are isolated: a token of one is useless in the other.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum AlorEnv {
21 /// The production contour (real money).
22 Production,
23 /// The test contour (a test account on request from the broker, data is simulated).
24 Test,
25}
26
27/// Configuration of the HTTP and WebSocket clients.
28///
29/// The one secret in it is the refresh token, which you issue on the developer portal after binding a trade
30/// account (it lives a year in the production contour and a month in the test one). The broker has no
31/// separate read and trade tokens: the access token made from it reaches everything the account may do.
32///
33/// The `Debug` output hides the refresh token (`<redacted>`), so a config can be logged.
34///
35/// # Examples
36///
37/// ```
38/// use brokapi_alor_protocol::config::{AlorConfig, AlorEnv};
39///
40/// let cfg = AlorConfig::new("my-refresh-token".to_string()).with_portfolio("D12345");
41/// assert_eq!(cfg.env, AlorEnv::Production);
42/// assert_eq!(cfg.portfolio.as_deref(), Some("D12345"));
43/// assert_eq!(cfg.exchange, "MOEX");
44/// assert_eq!(cfg.request_timeout.as_secs(), 30);
45///
46/// // The test contour switches all three addresses at once.
47/// let test = cfg.with_env(AlorEnv::Test);
48/// assert_eq!(test.http_base_url, "https://apidev.alor.ru");
49/// assert_eq!(test.auth_base_url, "https://oauthdev.alor.ru");
50/// ```
51#[derive(Clone)]
52pub struct AlorConfig {
53 /// The long-lived refresh token from the developer portal.
54 pub refresh_token: String,
55 /// Timeout of one REST request, from sending to the end of the body (default 30 seconds). A request
56 /// that times out ends with [`AlorApiError::Http`](crate::error::AlorApiError::Http) and no status.
57 /// For an order command that is an unknown outcome: ask the broker before you send it again.
58 pub request_timeout: Duration,
59 /// The contour. Set by [`AlorConfig::with_env`], which also sets the three addresses below.
60 pub env: AlorEnv,
61 /// Base URL of the REST API, without a trailing slash.
62 pub http_base_url: String,
63 /// Base URL of the authorization server, without a trailing slash.
64 pub auth_base_url: String,
65 /// Base URL of the WebSocket API, without a trailing slash. The channels are `/ws` and `/cws`.
66 pub ws_base_url: String,
67 /// The portfolio the user data connector works with (for example `D12345`). If it is empty, the connector
68 /// takes the only portfolio named in the access token, and refuses to start when the token names several.
69 pub portfolio: Option<String>,
70 /// The exchange of the portfolio: `MOEX` (default) or `SPBX`. Orders, positions and trades are read per
71 /// exchange, so one connector works with one.
72 pub exchange: String,
73}
74
75impl std::fmt::Debug for AlorConfig {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 f.debug_struct("AlorConfig")
78 .field("refresh_token", &"<redacted>")
79 .field("request_timeout", &self.request_timeout)
80 .field("env", &self.env)
81 .field("http_base_url", &self.http_base_url)
82 .field("auth_base_url", &self.auth_base_url)
83 .field("ws_base_url", &self.ws_base_url)
84 .field("portfolio", &self.portfolio)
85 .field("exchange", &self.exchange)
86 .finish()
87 }
88}
89
90const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
91
92impl AlorConfig {
93 /// A configuration for the production contour from a refresh token.
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// use brokapi_alor_protocol::config::AlorConfig;
99 ///
100 /// let cfg = AlorConfig::new("t".to_string());
101 /// assert!(cfg.http_base_url.starts_with("https://"));
102 /// assert!(cfg.ws_base_url.starts_with("wss://"));
103 /// ```
104 pub fn new(refresh_token: String) -> Self {
105 Self {
106 refresh_token,
107 request_timeout: DEFAULT_REQUEST_TIMEOUT,
108 env: AlorEnv::Production,
109 http_base_url: PROD_HTTP_BASE_URL.to_string(),
110 auth_base_url: PROD_AUTH_BASE_URL.to_string(),
111 ws_base_url: PROD_WS_BASE_URL.to_string(),
112 portfolio: None,
113 exchange: "MOEX".to_string(),
114 }
115 }
116
117 /// Reads the configuration from the environment (and from a `.env` file of the working directory, if
118 /// there is one; variables that are already set win).
119 ///
120 /// * `ALOR_REFRESH_TOKEN`: required.
121 /// * `ALOR_PORTFOLIO`: optional, see [`AlorConfig::portfolio`].
122 /// * `ALOR_EXCHANGE`: optional, `MOEX` (default) or `SPBX`.
123 /// * `ALOR_ENV`: optional, the exact text `test` selects the test contour.
124 ///
125 /// No network call is made.
126 ///
127 /// # Errors
128 ///
129 /// [`std::env::VarError::NotPresent`] when `ALOR_REFRESH_TOKEN` is not set. A wrong or expired token shows
130 /// up as [`AlorApiError::Auth`](crate::error::AlorApiError::Auth) at the first request.
131 ///
132 /// # Examples
133 ///
134 /// ```
135 /// use brokapi_alor_protocol::config::AlorConfig;
136 ///
137 /// match AlorConfig::from_env() {
138 /// Ok(cfg) => assert_eq!(cfg.request_timeout.as_secs(), 30),
139 /// Err(std::env::VarError::NotPresent) => { /* ALOR_REFRESH_TOKEN is not set */ }
140 /// Err(other) => panic!("unexpected: {other}"),
141 /// }
142 /// ```
143 pub fn from_env() -> Result<Self, std::env::VarError> {
144 dotenvy::from_path(".env").ok();
145 Self::from_vars()
146 }
147
148 /// Like [`AlorConfig::from_env`], but loads the `.env` file at `path` (a file that is not there is skipped).
149 ///
150 /// # Errors
151 ///
152 /// The same as [`AlorConfig::from_env`].
153 pub fn from_env_file(path: impl AsRef<std::path::Path>) -> Result<Self, std::env::VarError> {
154 dotenvy::from_path(path.as_ref()).ok();
155 Self::from_vars()
156 }
157
158 fn from_vars() -> Result<Self, std::env::VarError> {
159 let mut cfg = Self::new(std::env::var("ALOR_REFRESH_TOKEN")?);
160 cfg.portfolio = std::env::var("ALOR_PORTFOLIO")
161 .ok()
162 .filter(|p| !p.is_empty());
163 if let Ok(ex) = std::env::var("ALOR_EXCHANGE")
164 && !ex.is_empty()
165 {
166 cfg.exchange = ex;
167 }
168 if std::env::var("ALOR_ENV").is_ok_and(|v| v == "test") {
169 cfg = cfg.with_env(AlorEnv::Test);
170 }
171 Ok(cfg)
172 }
173
174 /// Points all three addresses at the production or the test contour. Returns the changed config.
175 pub fn with_env(mut self, env: AlorEnv) -> Self {
176 let (http, auth, ws) = match env {
177 AlorEnv::Production => (PROD_HTTP_BASE_URL, PROD_AUTH_BASE_URL, PROD_WS_BASE_URL),
178 AlorEnv::Test => (TEST_HTTP_BASE_URL, TEST_AUTH_BASE_URL, TEST_WS_BASE_URL),
179 };
180 self.env = env;
181 self.http_base_url = http.to_string();
182 self.auth_base_url = auth.to_string();
183 self.ws_base_url = ws.to_string();
184 self
185 }
186
187 /// Sets the portfolio (see [`AlorConfig::portfolio`]). Returns the changed config.
188 pub fn with_portfolio(mut self, portfolio: impl Into<String>) -> Self {
189 self.portfolio = Some(portfolio.into());
190 self
191 }
192
193 /// Sets the exchange, `MOEX` or `SPBX` (see [`AlorConfig::exchange`]). Returns the changed config.
194 pub fn with_exchange(mut self, exchange: impl Into<String>) -> Self {
195 self.exchange = exchange.into();
196 self
197 }
198
199 /// Points the REST, authorization and WebSocket clients at one local server, for tests and examples.
200 ///
201 /// `http_base_url` is the base of the local server (for example `http://127.0.0.1:34567`). The
202 /// authorization server is the same host, and the WebSocket base is derived by swapping the scheme. The token
203 /// is only a label for the imitation, it is never sent to the real broker. The portfolio is `D00001`.
204 ///
205 /// # Examples
206 ///
207 /// ```
208 /// use brokapi_alor_protocol::config::AlorConfig;
209 ///
210 /// let cfg = AlorConfig::for_mock("mock-refresh-token", "http://127.0.0.1:9000");
211 /// assert_eq!(cfg.auth_base_url, "http://127.0.0.1:9000");
212 /// assert_eq!(cfg.ws_base_url, "ws://127.0.0.1:9000");
213 /// ```
214 pub fn for_mock(refresh_token: impl Into<String>, http_base_url: impl Into<String>) -> Self {
215 let http = http_base_url.into();
216 let ws = http
217 .replace("https://", "wss://")
218 .replace("http://", "ws://");
219 let mut cfg = Self::new(refresh_token.into());
220 cfg.auth_base_url = http.clone();
221 cfg.http_base_url = http;
222 cfg.ws_base_url = ws;
223 cfg.portfolio = Some("D00001".to_string());
224 cfg
225 }
226}