1use std::collections::BTreeMap;
5
6use chrono::DateTime;
7use chrono::Utc;
8
9use num_decimal::Num;
10
11use serde::Deserialize;
12use serde::Serialize;
13use serde_json::from_slice as from_json;
14use serde_urlencoded::to_string as to_query;
15
16use crate::data::v2::Feed;
17use crate::data::DATA_BASE_URL;
18use crate::util::string_slice_to_str;
19use crate::Str;
20
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
24pub struct GetReq {
25 #[serde(rename = "symbols", serialize_with = "string_slice_to_str")]
27 pub symbols: Vec<String>,
28 #[serde(rename = "feed")]
30 pub feed: Option<Feed>,
31 #[doc(hidden)]
33 #[serde(skip)]
34 pub _non_exhaustive: (),
35}
36
37
38#[derive(Clone, Debug, Default, Eq, PartialEq)]
40#[allow(missing_copy_implementations)]
41pub struct GetReqInit {
42 pub feed: Option<Feed>,
44 #[doc(hidden)]
46 pub _non_exhaustive: (),
47}
48
49impl GetReqInit {
50 #[inline]
52 pub fn init<I, S>(self, symbols: I) -> GetReq
53 where
54 I: IntoIterator<Item = S>,
55 S: Into<String>,
56 {
57 GetReq {
58 symbols: symbols.into_iter().map(S::into).collect(),
59 feed: self.feed,
60 _non_exhaustive: (),
61 }
62 }
63}
64
65
66#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
69pub struct Quote {
70 #[serde(rename = "t")]
72 pub time: DateTime<Utc>,
73 #[serde(rename = "ap")]
75 pub ask_price: Num,
76 #[serde(rename = "as")]
78 pub ask_size: u64,
79 #[serde(rename = "bp")]
81 pub bid_price: Num,
82 #[serde(rename = "bs")]
84 pub bid_size: u64,
85 #[doc(hidden)]
87 #[serde(skip)]
88 pub _non_exhaustive: (),
89}
90
91
92EndpointNoParse! {
93 pub Get(GetReq),
96 Ok => Vec<(String, Quote)>, [
97 OK,
99 ],
100 Err => GetError, [
101 BAD_REQUEST => InvalidInput,
104 ]
105
106 fn base_url() -> Option<Str> {
107 Some(DATA_BASE_URL.into())
108 }
109
110 fn path(_input: &Self::Input) -> Str {
111 "/v2/stocks/quotes/latest".into()
112 }
113
114 fn query(input: &Self::Input) -> Result<Option<Str>, Self::ConversionError> {
115 Ok(Some(to_query(input)?.into()))
116 }
117
118 fn parse(body: &[u8]) -> Result<Self::Output, Self::ConversionError> {
119 #[derive(Deserialize)]
125 struct Response {
126 quotes: BTreeMap<String, Quote>,
130 }
131
132 from_json::<Response>(body)
135 .map(|response| {
136 response
137 .quotes
138 .into_iter()
139 .collect()
140 })
141 .map_err(Self::ConversionError::from)
142 }
143
144 fn parse_err(body: &[u8]) -> Result<Self::ApiError, Vec<u8>> {
145 from_json::<Self::ApiError>(body).map_err(|_| body.to_vec())
146 }
147}
148
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 use chrono::Duration;
155
156 use http_endpoint::Endpoint as _;
157
158 use test_log::test;
159
160 use crate::api_info::ApiInfo;
161 use crate::Client;
162 use crate::RequestError;
163
164
165 #[test]
168 fn parse_reference_quotes() {
169 let response = br#"{
170 "quotes": {
171 "TSLA": {
172 "t": "2022-04-12T17:26:45.009288296Z",
173 "ax": "V",
174 "ap": 1020,
175 "as": 3,
176 "bx": "V",
177 "bp": 990,
178 "bs": 5,
179 "c": ["R"],
180 "z": "C"
181 },
182 "AAPL": {
183 "t": "2022-04-12T17:26:44.962998616Z",
184 "ax": "V",
185 "ap": 170,
186 "as": 1,
187 "bx": "V",
188 "bp": 168.03,
189 "bs": 1,
190 "c": ["R"],
191 "z": "C"
192 }
193 }
194 }"#;
195
196 let quotes = Get::parse(response).unwrap();
197 assert_eq!(quotes.len(), 2);
198
199 assert_eq!(quotes[0].0, "AAPL");
200 let aapl = "es[0].1;
201 assert_eq!(
202 aapl.time,
203 DateTime::parse_from_rfc3339("2022-04-12T17:26:44.962998616Z").unwrap()
204 );
205 assert_eq!(aapl.ask_price, Num::new(170, 1));
206 assert_eq!(aapl.ask_size, 1);
207 assert_eq!(aapl.bid_price, Num::new(16803, 100));
208 assert_eq!(aapl.bid_size, 1);
209
210 assert_eq!(quotes[1].0, "TSLA");
211 let tsla = "es[1].1;
212 assert_eq!(
213 tsla.time,
214 DateTime::parse_from_rfc3339("2022-04-12T17:26:45.009288296Z").unwrap()
215 );
216 assert_eq!(tsla.ask_price, Num::new(1020, 1));
217 assert_eq!(tsla.ask_size, 3);
218 assert_eq!(tsla.bid_price, Num::new(990, 1));
219 assert_eq!(tsla.bid_size, 5);
220 }
221
222 #[test(tokio::test)]
224 async fn request_last_quotes() {
225 let api_info = ApiInfo::from_env().unwrap();
226 let client = Client::new(api_info);
227
228 let req = GetReqInit::default().init(["SPY"]);
229 let quotes = client.issue::<Get>(&req).await.unwrap();
230 assert_eq!(quotes.len(), 1);
231 assert_eq!(quotes[0].0, "SPY");
232 assert!(quotes[0].1.time >= Utc::now() - Duration::try_weeks(2).unwrap());
236 }
237
238 #[test(tokio::test)]
240 async fn request_last_quotes_multi() {
241 let api_info = ApiInfo::from_env().unwrap();
242 let client = Client::new(api_info);
243
244 let req = GetReqInit::default().init(["MSFT", "SPY", "AAPL"]);
245 let quotes = client.issue::<Get>(&req).await.unwrap();
246 assert_eq!(quotes.len(), 3);
247
248 assert_eq!(quotes[0].0, "AAPL");
250 assert!(quotes[0].1.time >= Utc::now() - Duration::try_weeks(2).unwrap());
251 assert_eq!(quotes[1].0, "MSFT");
252 assert!(quotes[1].1.time >= Utc::now() - Duration::try_weeks(2).unwrap());
253 assert_eq!(quotes[2].0, "SPY");
254 assert!(quotes[2].1.time >= Utc::now() - Duration::try_weeks(2).unwrap());
255 }
256
257 #[test(tokio::test)]
259 async fn sip_feed() {
260 let api_info = ApiInfo::from_env().unwrap();
261 let client = Client::new(api_info);
262
263 let req = GetReqInit {
264 feed: Some(Feed::SIP),
265 ..Default::default()
266 }
267 .init(["SPY"]);
268
269 let result = client.issue::<Get>(&req).await;
270 match result {
274 Ok(_) | Err(RequestError::Endpoint(GetError::NotPermitted(_))) => (),
275 err => panic!("Received unexpected error: {err:?}"),
276 }
277 }
278
279 #[test(tokio::test)]
282 async fn invalid_symbol() {
283 let api_info = ApiInfo::from_env().unwrap();
284 let client = Client::new(api_info);
285
286 let req = GetReqInit::default().init(["ABC123"]);
287 let err = client.issue::<Get>(&req).await.unwrap_err();
288 match err {
289 RequestError::Endpoint(GetError::InvalidInput(_)) => (),
290 _ => panic!("Received unexpected error: {err:?}"),
291 };
292 }
293
294 #[test(tokio::test)]
297 async fn nonexistent_symbol() {
298 let api_info = ApiInfo::from_env().unwrap();
299 let client = Client::new(api_info);
300
301 let req = GetReqInit::default().init(["SPY", "NOSUCHSYMBOL"]);
302 let quotes = client.issue::<Get>(&req).await.unwrap();
303 assert_eq!(quotes.len(), 1);
304 }
305}