1use 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
14const ENV_API_BASE_URL: &str = "APCA_API_BASE_URL";
16const ENV_API_STREAM_URL: &str = "APCA_API_STREAM_URL";
18const ENV_KEY_ID: &str = "APCA_API_KEY_ID";
20const ENV_SECRET: &str = "APCA_API_SECRET_KEY";
22
23
24fn 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#[derive(Clone, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub struct ApiInfo {
41 pub api_base_url: Url,
43 pub api_stream_url: Url,
45 pub data_base_url: Url,
47 pub data_stream_base_url: Url,
49 pub key_id: String,
51 pub secret: String,
53}
54
55impl ApiInfo {
56 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 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 #[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 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 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 #[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}