apca/
api_info.rs

1// Copyright (C) 2019-2023 The apca Developers
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::env::var_os;
5use std::ffi::OsString;
6
7use url::Url;
8
9use crate::api::API_BASE_URL;
10use crate::data::DATA_BASE_URL;
11use crate::data::DATA_STREAM_BASE_URL;
12use crate::Error;
13
14/// The base URL of the Trading API to use.
15const ENV_API_BASE_URL: &str = "APCA_API_BASE_URL";
16/// The URL of the websocket stream portion of the Trading API to use.
17const ENV_API_STREAM_URL: &str = "APCA_API_STREAM_URL";
18/// The environment variable representing the key ID.
19const ENV_KEY_ID: &str = "APCA_API_KEY_ID";
20/// The environment variable representing the secret key.
21const ENV_SECRET: &str = "APCA_API_SECRET_KEY";
22
23
24/// Convert a Trading API base URL into the corresponding one for
25/// websocket streaming.
26fn make_api_stream_url(base_url: Url) -> Result<Url, Error> {
27  let mut url = base_url;
28  url.set_scheme("wss").map_err(|()| {
29    Error::Str(format!("unable to change URL scheme for {url}: invalid URL?").into())
30  })?;
31  url.set_path("stream");
32  Ok(url)
33}
34
35
36/// An object encapsulating the information used for working with the
37/// Alpaca API.
38#[derive(Clone, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub struct ApiInfo {
41  /// The base URL for the Trading API.
42  pub api_base_url: Url,
43  /// The websocket stream URL for the Trading API.
44  pub api_stream_url: Url,
45  /// The base URL for data retrieval.
46  pub data_base_url: Url,
47  /// The websocket base URL for streaming of data.
48  pub data_stream_base_url: Url,
49  /// The key ID to use for authentication.
50  pub key_id: String,
51  /// The secret to use for authentication.
52  pub secret: String,
53}
54
55impl ApiInfo {
56  /// Create an `ApiInfo` from the required data. Note that using this
57  /// constructor the websocket URL will be inferred based on the base
58  /// URL provided.
59  ///
60  /// # Errors
61  /// - [`Error::Url`](crate::Error::Url) If `api_base_url` cannot be parsed
62  ///   into a [`url::Url`](url::Url).
63  pub fn from_parts(
64    api_base_url: impl AsRef<str>,
65    key_id: impl ToString,
66    secret: impl ToString,
67  ) -> Result<Self, Error> {
68    let api_base_url = Url::parse(api_base_url.as_ref())?;
69    let api_stream_url = make_api_stream_url(api_base_url.clone())?;
70
71    Ok(Self {
72      api_base_url,
73      api_stream_url,
74      // We basically only work with statically defined URL parts here
75      // which we know can be parsed successfully, so unwrapping is
76      // fine.
77      data_base_url: Url::parse(DATA_BASE_URL).unwrap(),
78      data_stream_base_url: Url::parse(DATA_STREAM_BASE_URL).unwrap(),
79      key_id: key_id.to_string(),
80      secret: secret.to_string(),
81    })
82  }
83
84  /// Create an `ApiInfo` object with information from the environment.
85  ///
86  /// This constructor retrieves API related information from the
87  /// environment and performs some preliminary validation on it. The
88  /// following information is used:
89  /// - the Alpaca Trading API base URL is retrieved from the
90  ///   `APCA_API_BASE_URL` variable
91  /// - the Alpaca Trading API stream URL is retrieved from the
92  ///   `APCA_API_STREAM_URL` variable
93  /// - the Alpaca account key ID is retrieved from the
94  ///   `APCA_API_KEY_ID` variable
95  /// - the Alpaca account secret is retrieved from the
96  ///   `APCA_API_SECRET_KEY` variable
97  ///
98  /// # Notes
99  /// - Neither of the two data APIs can be configured via the
100  ///   environment currently; defaults will be used
101  #[allow(unused_qualifications)]
102  pub fn from_env() -> Result<Self, Error> {
103    let api_base_url = var_os(ENV_API_BASE_URL)
104      .unwrap_or_else(|| OsString::from(API_BASE_URL))
105      .into_string()
106      .map_err(|_| {
107        Error::Str(format!("{ENV_API_BASE_URL} environment variable is not a valid string").into())
108      })?;
109    let api_base_url = Url::parse(&api_base_url)?;
110
111    let api_stream_url = var_os(ENV_API_STREAM_URL)
112      .map(Result::<_, Error>::Ok)
113      .unwrap_or_else(|| {
114        // If the user did not provide an explicit websocket URL then
115        // infer the one to use based on the API base URL.
116        let url = make_api_stream_url(api_base_url.clone())?;
117        Ok(OsString::from(url.as_str()))
118      })?
119      .into_string()
120      .map_err(|_| {
121        Error::Str(
122          format!("{ENV_API_STREAM_URL} environment variable is not a valid string").into(),
123        )
124      })?;
125    let api_stream_url = Url::parse(&api_stream_url)?;
126
127    let key_id = var_os(ENV_KEY_ID)
128      .ok_or_else(|| Error::Str(format!("{ENV_KEY_ID} environment variable not found").into()))?
129      .into_string()
130      .map_err(|_| {
131        Error::Str(format!("{ENV_KEY_ID} environment variable is not a valid string").into())
132      })?;
133
134    let secret = var_os(ENV_SECRET)
135      .ok_or_else(|| Error::Str(format!("{ENV_SECRET} environment variable not found").into()))?
136      .into_string()
137      .map_err(|_| {
138        Error::Str(format!("{ENV_SECRET} environment variable is not a valid string").into())
139      })?;
140
141    Ok(Self {
142      api_base_url,
143      api_stream_url,
144      // We basically only work with statically defined URL parts here
145      // which we know can be parsed successfully, so unwrapping is
146      // fine.
147      data_base_url: Url::parse(DATA_BASE_URL).unwrap(),
148      data_stream_base_url: Url::parse(DATA_STREAM_BASE_URL).unwrap(),
149      key_id,
150      secret,
151    })
152  }
153}
154
155
156#[cfg(test)]
157mod tests {
158  use super::*;
159
160
161  /// Check that we can create an [`ApiInfo`] object from its
162  /// constituent parts.
163  #[test]
164  fn from_parts() {
165    let api_base_url = "https://paper-api.alpaca.markets/";
166    let key_id = "XXXXXXXXXXXXXXXXXXXX";
167    let secret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY";
168
169    let api_info = ApiInfo::from_parts(api_base_url, key_id, secret).unwrap();
170    assert_eq!(api_info.api_base_url.as_str(), api_base_url);
171    assert_eq!(api_info.key_id, key_id);
172    assert_eq!(api_info.secret, secret);
173  }
174}