1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
// Copyright (C) 2019-2023 The apca Developers
// SPDX-License-Identifier: GPL-3.0-or-later

use std::env::var_os;
use std::ffi::OsString;

use url::Url;

use crate::api::API_BASE_URL;
use crate::data::DATA_BASE_URL;
use crate::data::DATA_STREAM_BASE_URL;
use crate::Error;

/// The base URL of the Trading API to use.
const ENV_API_BASE_URL: &str = "APCA_API_BASE_URL";
/// The URL of the websocket stream portion of the Trading API to use.
const ENV_API_STREAM_URL: &str = "APCA_API_STREAM_URL";
/// The environment variable representing the key ID.
const ENV_KEY_ID: &str = "APCA_API_KEY_ID";
/// The environment variable representing the secret key.
const ENV_SECRET: &str = "APCA_API_SECRET_KEY";


/// Convert a Trading API base URL into the corresponding one for
/// websocket streaming.
fn make_api_stream_url(base_url: Url) -> Result<Url, Error> {
  let mut url = base_url;
  url.set_scheme("wss").map_err(|()| {
    Error::Str(format!("unable to change URL scheme for {url}: invalid URL?").into())
  })?;
  url.set_path("stream");
  Ok(url)
}


/// An object encapsulating the information used for working with the
/// Alpaca API.
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct ApiInfo {
  /// The base URL for the Trading API.
  pub api_base_url: Url,
  /// The websocket stream URL for the Trading API.
  pub api_stream_url: Url,
  /// The base URL for data retrieval.
  pub data_base_url: Url,
  /// The websocket base URL for streaming of data.
  pub data_stream_base_url: Url,
  /// The key ID to use for authentication.
  pub key_id: String,
  /// The secret to use for authentication.
  pub secret: String,
}

impl ApiInfo {
  /// Create an `ApiInfo` from the required data. Note that using this
  /// constructor the websocket URL will be inferred based on the base
  /// URL provided.
  ///
  /// # Errors
  /// - [`Error::Url`](crate::Error::Url) If `api_base_url` cannot be parsed
  ///   into a [`url::Url`](url::Url).
  pub fn from_parts(
    api_base_url: impl AsRef<str>,
    key_id: impl ToString,
    secret: impl ToString,
  ) -> Result<Self, Error> {
    let api_base_url = Url::parse(api_base_url.as_ref())?;
    let api_stream_url = make_api_stream_url(api_base_url.clone())?;

    Ok(Self {
      api_base_url,
      api_stream_url,
      // We basically only work with statically defined URL parts here
      // which we know can be parsed successfully, so unwrapping is
      // fine.
      data_base_url: Url::parse(DATA_BASE_URL).unwrap(),
      data_stream_base_url: Url::parse(DATA_STREAM_BASE_URL).unwrap(),
      key_id: key_id.to_string(),
      secret: secret.to_string(),
    })
  }

  /// Create an `ApiInfo` object with information from the environment.
  ///
  /// This constructor retrieves API related information from the
  /// environment and performs some preliminary validation on it. The
  /// following information is used:
  /// - the Alpaca Trading API base URL is retrieved from the
  ///   `APCA_API_BASE_URL` variable
  /// - the Alpaca Trading API stream URL is retrieved from the
  ///   `APCA_API_STREAM_URL` variable
  /// - the Alpaca account key ID is retrieved from the
  ///   `APCA_API_KEY_ID` variable
  /// - the Alpaca account secret is retrieved from the
  ///   `APCA_API_SECRET_KEY` variable
  ///
  /// # Notes
  /// - Neither of the two data APIs can be configured via the
  ///   environment currently; defaults will be used
  #[allow(unused_qualifications)]
  pub fn from_env() -> Result<Self, Error> {
    let api_base_url = var_os(ENV_API_BASE_URL)
      .unwrap_or_else(|| OsString::from(API_BASE_URL))
      .into_string()
      .map_err(|_| {
        Error::Str(format!("{ENV_API_BASE_URL} environment variable is not a valid string").into())
      })?;
    let api_base_url = Url::parse(&api_base_url)?;

    let api_stream_url = var_os(ENV_API_STREAM_URL)
      .map(Result::<_, Error>::Ok)
      .unwrap_or_else(|| {
        // If the user did not provide an explicit websocket URL then
        // infer the one to use based on the API base URL.
        let url = make_api_stream_url(api_base_url.clone())?;
        Ok(OsString::from(url.as_str()))
      })?
      .into_string()
      .map_err(|_| {
        Error::Str(
          format!("{ENV_API_STREAM_URL} environment variable is not a valid string").into(),
        )
      })?;
    let api_stream_url = Url::parse(&api_stream_url)?;

    let key_id = var_os(ENV_KEY_ID)
      .ok_or_else(|| Error::Str(format!("{ENV_KEY_ID} environment variable not found").into()))?
      .into_string()
      .map_err(|_| {
        Error::Str(format!("{ENV_KEY_ID} environment variable is not a valid string").into())
      })?;

    let secret = var_os(ENV_SECRET)
      .ok_or_else(|| Error::Str(format!("{ENV_SECRET} environment variable not found").into()))?
      .into_string()
      .map_err(|_| {
        Error::Str(format!("{ENV_SECRET} environment variable is not a valid string").into())
      })?;

    Ok(Self {
      api_base_url,
      api_stream_url,
      // We basically only work with statically defined URL parts here
      // which we know can be parsed successfully, so unwrapping is
      // fine.
      data_base_url: Url::parse(DATA_BASE_URL).unwrap(),
      data_stream_base_url: Url::parse(DATA_STREAM_BASE_URL).unwrap(),
      key_id,
      secret,
    })
  }
}


#[cfg(test)]
mod tests {
  use super::*;


  /// Check that we can create an [`ApiInfo`] object from its
  /// constituent parts.
  #[test]
  fn from_parts() {
    let api_base_url = "https://paper-api.alpaca.markets/";
    let key_id = "XXXXXXXXXXXXXXXXXXXX";
    let secret = "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY";

    let api_info = ApiInfo::from_parts(api_base_url, key_id, secret).unwrap();
    assert_eq!(api_info.api_base_url.as_str(), api_base_url);
    assert_eq!(api_info.key_id, key_id);
    assert_eq!(api_info.secret, secret);
  }
}