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);
}
}