1use crate::Error;
2use crate::common::query::QueryWriter;
3use crate::common::validate::{validate_required_symbol, validate_required_symbols};
4use crate::transport::pagination::PaginatedRequest;
5
6use super::{Adjustment, AuctionFeed, Currency, DataFeed, Sort, Tape, TickType, TimeFrame};
7
8#[derive(Clone, Debug, Default)]
9pub struct BarsRequest {
10 pub symbols: Vec<String>,
11 pub timeframe: TimeFrame,
12 pub start: Option<String>,
13 pub end: Option<String>,
14 pub limit: Option<u32>,
15 pub adjustment: Option<Adjustment>,
16 pub feed: Option<DataFeed>,
17 pub sort: Option<Sort>,
18 pub asof: Option<String>,
19 pub currency: Option<Currency>,
20 pub page_token: Option<String>,
21}
22
23#[derive(Clone, Debug, Default)]
24pub struct BarsSingleRequest {
25 pub symbol: String,
26 pub timeframe: TimeFrame,
27 pub start: Option<String>,
28 pub end: Option<String>,
29 pub limit: Option<u32>,
30 pub adjustment: Option<Adjustment>,
31 pub feed: Option<DataFeed>,
32 pub sort: Option<Sort>,
33 pub asof: Option<String>,
34 pub currency: Option<Currency>,
35 pub page_token: Option<String>,
36}
37
38#[derive(Clone, Debug, Default)]
39pub struct AuctionsRequest {
40 pub symbols: Vec<String>,
41 pub start: Option<String>,
42 pub end: Option<String>,
43 pub limit: Option<u32>,
44 pub asof: Option<String>,
45 pub feed: Option<AuctionFeed>,
46 pub currency: Option<Currency>,
47 pub page_token: Option<String>,
48 pub sort: Option<Sort>,
49}
50
51#[derive(Clone, Debug, Default)]
52pub struct AuctionsSingleRequest {
53 pub symbol: String,
54 pub start: Option<String>,
55 pub end: Option<String>,
56 pub limit: Option<u32>,
57 pub asof: Option<String>,
58 pub feed: Option<AuctionFeed>,
59 pub currency: Option<Currency>,
60 pub page_token: Option<String>,
61 pub sort: Option<Sort>,
62}
63
64#[derive(Clone, Debug, Default)]
65pub struct QuotesRequest {
66 pub symbols: Vec<String>,
67 pub start: Option<String>,
68 pub end: Option<String>,
69 pub limit: Option<u32>,
70 pub feed: Option<DataFeed>,
71 pub sort: Option<Sort>,
72 pub asof: Option<String>,
73 pub currency: Option<Currency>,
74 pub page_token: Option<String>,
75}
76
77#[derive(Clone, Debug, Default)]
78pub struct QuotesSingleRequest {
79 pub symbol: String,
80 pub start: Option<String>,
81 pub end: Option<String>,
82 pub limit: Option<u32>,
83 pub feed: Option<DataFeed>,
84 pub sort: Option<Sort>,
85 pub asof: Option<String>,
86 pub currency: Option<Currency>,
87 pub page_token: Option<String>,
88}
89
90#[derive(Clone, Debug, Default)]
91pub struct TradesRequest {
92 pub symbols: Vec<String>,
93 pub start: Option<String>,
94 pub end: Option<String>,
95 pub limit: Option<u32>,
96 pub feed: Option<DataFeed>,
97 pub sort: Option<Sort>,
98 pub asof: Option<String>,
99 pub currency: Option<Currency>,
100 pub page_token: Option<String>,
101}
102
103#[derive(Clone, Debug, Default)]
104pub struct TradesSingleRequest {
105 pub symbol: String,
106 pub start: Option<String>,
107 pub end: Option<String>,
108 pub limit: Option<u32>,
109 pub feed: Option<DataFeed>,
110 pub sort: Option<Sort>,
111 pub asof: Option<String>,
112 pub currency: Option<Currency>,
113 pub page_token: Option<String>,
114}
115
116#[derive(Clone, Debug, Default)]
117pub struct LatestBarsRequest {
118 pub symbols: Vec<String>,
119 pub feed: Option<DataFeed>,
120 pub currency: Option<Currency>,
121}
122
123#[derive(Clone, Debug, Default)]
124pub struct LatestBarRequest {
125 pub symbol: String,
126 pub feed: Option<DataFeed>,
127 pub currency: Option<Currency>,
128}
129
130#[derive(Clone, Debug, Default)]
131pub struct LatestQuotesRequest {
132 pub symbols: Vec<String>,
133 pub feed: Option<DataFeed>,
134 pub currency: Option<Currency>,
135}
136
137#[derive(Clone, Debug, Default)]
138pub struct LatestQuoteRequest {
139 pub symbol: String,
140 pub feed: Option<DataFeed>,
141 pub currency: Option<Currency>,
142}
143
144#[derive(Clone, Debug, Default)]
145pub struct LatestTradesRequest {
146 pub symbols: Vec<String>,
147 pub feed: Option<DataFeed>,
148 pub currency: Option<Currency>,
149}
150
151#[derive(Clone, Debug, Default)]
152pub struct LatestTradeRequest {
153 pub symbol: String,
154 pub feed: Option<DataFeed>,
155 pub currency: Option<Currency>,
156}
157
158#[derive(Clone, Debug, Default)]
159pub struct SnapshotsRequest {
160 pub symbols: Vec<String>,
161 pub feed: Option<DataFeed>,
162 pub currency: Option<Currency>,
163}
164
165#[derive(Clone, Debug, Default)]
166pub struct SnapshotRequest {
167 pub symbol: String,
168 pub feed: Option<DataFeed>,
169 pub currency: Option<Currency>,
170}
171
172#[derive(Clone, Debug, Default)]
173pub struct ConditionCodesRequest {
174 pub ticktype: TickType,
175 pub tape: Tape,
176}
177
178impl BarsRequest {
179 pub(crate) fn validate(&self) -> Result<(), Error> {
180 validate_required_symbols(&self.symbols)?;
181 validate_limit(self.limit, 1, 10_000)
182 }
183
184 pub(crate) fn to_query(self) -> Vec<(String, String)> {
185 let mut query = QueryWriter::default();
186 query.push_csv("symbols", self.symbols);
187 query.push_opt("timeframe", Some(self.timeframe));
188 query.push_opt("start", self.start);
189 query.push_opt("end", self.end);
190 query.push_opt("limit", self.limit);
191 query.push_opt("adjustment", self.adjustment);
192 query.push_opt("feed", self.feed);
193 query.push_opt("currency", self.currency);
194 query.push_opt("page_token", self.page_token);
195 query.push_opt("sort", self.sort);
196 query.push_opt("asof", self.asof);
197 query.finish()
198 }
199}
200
201impl BarsSingleRequest {
202 pub(crate) fn validate(&self) -> Result<(), Error> {
203 validate_required_symbol(&self.symbol, "symbol")?;
204 validate_limit(self.limit, 1, 10_000)
205 }
206
207 pub(crate) fn to_query(self) -> Vec<(String, String)> {
208 let mut query = QueryWriter::default();
209 query.push_opt("timeframe", Some(self.timeframe));
210 query.push_opt("start", self.start);
211 query.push_opt("end", self.end);
212 query.push_opt("limit", self.limit);
213 query.push_opt("adjustment", self.adjustment);
214 query.push_opt("feed", self.feed);
215 query.push_opt("currency", self.currency);
216 query.push_opt("page_token", self.page_token);
217 query.push_opt("sort", self.sort);
218 query.push_opt("asof", self.asof);
219 query.finish()
220 }
221}
222
223impl AuctionsRequest {
224 pub(crate) fn validate(&self) -> Result<(), Error> {
225 validate_required_symbols(&self.symbols)?;
226 validate_limit(self.limit, 1, 10_000)
227 }
228
229 pub(crate) fn to_query(self) -> Vec<(String, String)> {
230 let mut query = QueryWriter::default();
231 query.push_csv("symbols", self.symbols);
232 query.push_opt("start", self.start);
233 query.push_opt("end", self.end);
234 query.push_opt("limit", self.limit);
235 query.push_opt("feed", self.feed);
236 query.push_opt("currency", self.currency);
237 query.push_opt("page_token", self.page_token);
238 query.push_opt("sort", self.sort);
239 query.push_opt("asof", self.asof);
240 query.finish()
241 }
242}
243
244impl AuctionsSingleRequest {
245 pub(crate) fn validate(&self) -> Result<(), Error> {
246 validate_required_symbol(&self.symbol, "symbol")?;
247 validate_limit(self.limit, 1, 10_000)
248 }
249
250 pub(crate) fn to_query(self) -> Vec<(String, String)> {
251 let mut query = QueryWriter::default();
252 query.push_opt("start", self.start);
253 query.push_opt("end", self.end);
254 query.push_opt("limit", self.limit);
255 query.push_opt("feed", self.feed);
256 query.push_opt("currency", self.currency);
257 query.push_opt("page_token", self.page_token);
258 query.push_opt("sort", self.sort);
259 query.push_opt("asof", self.asof);
260 query.finish()
261 }
262}
263
264impl QuotesRequest {
265 pub(crate) fn validate(&self) -> Result<(), Error> {
266 validate_required_symbols(&self.symbols)?;
267 validate_limit(self.limit, 1, 10_000)
268 }
269
270 pub(crate) fn to_query(self) -> Vec<(String, String)> {
271 let mut query = QueryWriter::default();
272 query.push_csv("symbols", self.symbols);
273 query.push_opt("start", self.start);
274 query.push_opt("end", self.end);
275 query.push_opt("limit", self.limit);
276 query.push_opt("feed", self.feed);
277 query.push_opt("currency", self.currency);
278 query.push_opt("page_token", self.page_token);
279 query.push_opt("sort", self.sort);
280 query.push_opt("asof", self.asof);
281 query.finish()
282 }
283}
284
285impl QuotesSingleRequest {
286 pub(crate) fn validate(&self) -> Result<(), Error> {
287 validate_required_symbol(&self.symbol, "symbol")?;
288 validate_limit(self.limit, 1, 10_000)
289 }
290
291 pub(crate) fn to_query(self) -> Vec<(String, String)> {
292 let mut query = QueryWriter::default();
293 query.push_opt("start", self.start);
294 query.push_opt("end", self.end);
295 query.push_opt("limit", self.limit);
296 query.push_opt("feed", self.feed);
297 query.push_opt("currency", self.currency);
298 query.push_opt("page_token", self.page_token);
299 query.push_opt("sort", self.sort);
300 query.push_opt("asof", self.asof);
301 query.finish()
302 }
303}
304
305impl TradesRequest {
306 pub(crate) fn validate(&self) -> Result<(), Error> {
307 validate_required_symbols(&self.symbols)?;
308 validate_limit(self.limit, 1, 10_000)
309 }
310
311 pub(crate) fn to_query(self) -> Vec<(String, String)> {
312 let mut query = QueryWriter::default();
313 query.push_csv("symbols", self.symbols);
314 query.push_opt("start", self.start);
315 query.push_opt("end", self.end);
316 query.push_opt("limit", self.limit);
317 query.push_opt("feed", self.feed);
318 query.push_opt("currency", self.currency);
319 query.push_opt("page_token", self.page_token);
320 query.push_opt("sort", self.sort);
321 query.push_opt("asof", self.asof);
322 query.finish()
323 }
324}
325
326impl TradesSingleRequest {
327 pub(crate) fn validate(&self) -> Result<(), Error> {
328 validate_required_symbol(&self.symbol, "symbol")?;
329 validate_limit(self.limit, 1, 10_000)
330 }
331
332 pub(crate) fn to_query(self) -> Vec<(String, String)> {
333 let mut query = QueryWriter::default();
334 query.push_opt("start", self.start);
335 query.push_opt("end", self.end);
336 query.push_opt("limit", self.limit);
337 query.push_opt("feed", self.feed);
338 query.push_opt("currency", self.currency);
339 query.push_opt("page_token", self.page_token);
340 query.push_opt("sort", self.sort);
341 query.push_opt("asof", self.asof);
342 query.finish()
343 }
344}
345
346impl LatestBarsRequest {
347 pub(crate) fn validate(&self) -> Result<(), Error> {
348 validate_required_symbols(&self.symbols)
349 }
350
351 pub(crate) fn to_query(self) -> Vec<(String, String)> {
352 latest_batch_query(self.symbols, self.feed, self.currency)
353 }
354}
355
356impl LatestBarRequest {
357 pub(crate) fn validate(&self) -> Result<(), Error> {
358 validate_required_symbol(&self.symbol, "symbol")
359 }
360
361 pub(crate) fn to_query(self) -> Vec<(String, String)> {
362 latest_single_query(self.feed, self.currency)
363 }
364}
365
366impl LatestQuotesRequest {
367 pub(crate) fn validate(&self) -> Result<(), Error> {
368 validate_required_symbols(&self.symbols)
369 }
370
371 pub(crate) fn to_query(self) -> Vec<(String, String)> {
372 latest_batch_query(self.symbols, self.feed, self.currency)
373 }
374}
375
376impl LatestQuoteRequest {
377 pub(crate) fn validate(&self) -> Result<(), Error> {
378 validate_required_symbol(&self.symbol, "symbol")
379 }
380
381 pub(crate) fn to_query(self) -> Vec<(String, String)> {
382 latest_single_query(self.feed, self.currency)
383 }
384}
385
386impl LatestTradesRequest {
387 pub(crate) fn validate(&self) -> Result<(), Error> {
388 validate_required_symbols(&self.symbols)
389 }
390
391 pub(crate) fn to_query(self) -> Vec<(String, String)> {
392 latest_batch_query(self.symbols, self.feed, self.currency)
393 }
394}
395
396impl LatestTradeRequest {
397 pub(crate) fn validate(&self) -> Result<(), Error> {
398 validate_required_symbol(&self.symbol, "symbol")
399 }
400
401 pub(crate) fn to_query(self) -> Vec<(String, String)> {
402 latest_single_query(self.feed, self.currency)
403 }
404}
405
406impl SnapshotsRequest {
407 pub(crate) fn validate(&self) -> Result<(), Error> {
408 validate_required_symbols(&self.symbols)
409 }
410
411 pub(crate) fn to_query(self) -> Vec<(String, String)> {
412 latest_batch_query(self.symbols, self.feed, self.currency)
413 }
414}
415
416impl SnapshotRequest {
417 pub(crate) fn validate(&self) -> Result<(), Error> {
418 validate_required_symbol(&self.symbol, "symbol")
419 }
420
421 pub(crate) fn to_query(self) -> Vec<(String, String)> {
422 latest_single_query(self.feed, self.currency)
423 }
424}
425
426impl ConditionCodesRequest {
427 pub(crate) fn to_query(self) -> Vec<(String, String)> {
428 let mut query = QueryWriter::default();
429 query.push_opt("tape", Some(self.tape));
430 query.finish()
431 }
432}
433
434impl PaginatedRequest for BarsSingleRequest {
435 fn with_page_token(&self, page_token: Option<String>) -> Self {
436 let mut next = self.clone();
437 next.page_token = page_token;
438 next
439 }
440}
441
442impl PaginatedRequest for QuotesSingleRequest {
443 fn with_page_token(&self, page_token: Option<String>) -> Self {
444 let mut next = self.clone();
445 next.page_token = page_token;
446 next
447 }
448}
449
450impl PaginatedRequest for AuctionsSingleRequest {
451 fn with_page_token(&self, page_token: Option<String>) -> Self {
452 let mut next = self.clone();
453 next.page_token = page_token;
454 next
455 }
456}
457
458fn latest_batch_query(
459 symbols: Vec<String>,
460 feed: Option<DataFeed>,
461 currency: Option<Currency>,
462) -> Vec<(String, String)> {
463 let mut query = QueryWriter::default();
464 query.push_csv("symbols", symbols);
465 query.push_opt("feed", feed);
466 query.push_opt("currency", currency);
467 query.finish()
468}
469
470fn validate_limit(limit: Option<u32>, min: u32, max: u32) -> Result<(), Error> {
471 if let Some(limit) = limit {
472 if !(min..=max).contains(&limit) {
473 return Err(Error::InvalidRequest(format!(
474 "limit must be between {min} and {max}"
475 )));
476 }
477 }
478
479 Ok(())
480}
481
482fn latest_single_query(
483 feed: Option<DataFeed>,
484 currency: Option<Currency>,
485) -> Vec<(String, String)> {
486 let mut query = QueryWriter::default();
487 query.push_opt("feed", feed);
488 query.push_opt("currency", currency);
489 query.finish()
490}
491
492impl PaginatedRequest for TradesSingleRequest {
493 fn with_page_token(&self, page_token: Option<String>) -> Self {
494 let mut next = self.clone();
495 next.page_token = page_token;
496 next
497 }
498}
499
500impl PaginatedRequest for BarsRequest {
501 fn with_page_token(&self, page_token: Option<String>) -> Self {
502 let mut next = self.clone();
503 next.page_token = page_token;
504 next
505 }
506}
507
508impl PaginatedRequest for AuctionsRequest {
509 fn with_page_token(&self, page_token: Option<String>) -> Self {
510 let mut next = self.clone();
511 next.page_token = page_token;
512 next
513 }
514}
515
516impl PaginatedRequest for QuotesRequest {
517 fn with_page_token(&self, page_token: Option<String>) -> Self {
518 let mut next = self.clone();
519 next.page_token = page_token;
520 next
521 }
522}
523
524impl PaginatedRequest for TradesRequest {
525 fn with_page_token(&self, page_token: Option<String>) -> Self {
526 let mut next = self.clone();
527 next.page_token = page_token;
528 next
529 }
530}
531
532#[cfg(test)]
533mod tests {
534 use crate::Error;
535
536 use super::*;
537
538 #[test]
539 fn stocks_data_feed_serializes_to_official_strings() {
540 assert_eq!(DataFeed::DelayedSip.to_string(), "delayed_sip");
541 assert_eq!(DataFeed::Iex.to_string(), "iex");
542 assert_eq!(DataFeed::Otc.to_string(), "otc");
543 assert_eq!(DataFeed::Sip.to_string(), "sip");
544 assert_eq!(DataFeed::Boats.to_string(), "boats");
545 assert_eq!(DataFeed::Overnight.to_string(), "overnight");
546 }
547
548 #[test]
549 fn stocks_adjustment_serializes_to_official_strings() {
550 assert_eq!(Adjustment::raw().to_string(), "raw");
551 assert_eq!(Adjustment::split().to_string(), "split");
552 assert_eq!(Adjustment::dividend().to_string(), "dividend");
553 assert_eq!(Adjustment::spin_off().to_string(), "spin-off");
554 assert_eq!(Adjustment::all().to_string(), "all");
555 assert_eq!(
556 Adjustment::from("split,dividend").to_string(),
557 "split,dividend"
558 );
559 }
560
561 #[test]
562 fn stocks_timeframe_serializes_to_official_strings() {
563 assert_eq!(TimeFrame::from("1Min").to_string(), "1Min");
564 assert_eq!(TimeFrame::from("5Min").to_string(), "5Min");
565 assert_eq!(TimeFrame::from("1Day").to_string(), "1Day");
566 assert_eq!(TimeFrame::from("1Week").to_string(), "1Week");
567 assert_eq!(TimeFrame::from("3Month").to_string(), "3Month");
568 }
569
570 #[test]
571 fn bars_request_serializes_official_query_words() {
572 let request = BarsRequest {
573 symbols: vec!["AAPL".into(), "MSFT".into()],
574 timeframe: TimeFrame::from("1Day"),
575 start: Some("2024-03-01T00:00:00Z".into()),
576 end: Some("2024-03-05T00:00:00Z".into()),
577 limit: Some(50),
578 adjustment: Some(Adjustment::from("split,dividend")),
579 feed: Some(DataFeed::Boats),
580 sort: Some(Sort::Desc),
581 asof: Some("2024-03-04".into()),
582 currency: Some(Currency::from("USD")),
583 page_token: Some("page-123".into()),
584 };
585
586 assert_eq!(
587 request.to_query(),
588 vec![
589 ("symbols".to_string(), "AAPL,MSFT".to_string()),
590 ("timeframe".to_string(), "1Day".to_string()),
591 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
592 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
593 ("limit".to_string(), "50".to_string()),
594 ("adjustment".to_string(), "split,dividend".to_string()),
595 ("feed".to_string(), "boats".to_string()),
596 ("currency".to_string(), "USD".to_string()),
597 ("page_token".to_string(), "page-123".to_string()),
598 ("sort".to_string(), "desc".to_string()),
599 ("asof".to_string(), "2024-03-04".to_string()),
600 ]
601 );
602 }
603
604 #[test]
605 fn bars_single_request_serializes_official_query_words() {
606 let request = BarsSingleRequest {
607 symbol: "AAPL".into(),
608 timeframe: TimeFrame::from("1Day"),
609 start: Some("2024-03-01T00:00:00Z".into()),
610 end: Some("2024-03-05T00:00:00Z".into()),
611 limit: Some(50),
612 adjustment: Some(Adjustment::from("split,dividend")),
613 feed: Some(DataFeed::Boats),
614 sort: Some(Sort::Desc),
615 asof: Some("2024-03-04".into()),
616 currency: Some(Currency::from("USD")),
617 page_token: Some("page-123".into()),
618 };
619
620 assert_eq!(
621 request.to_query(),
622 vec![
623 ("timeframe".to_string(), "1Day".to_string()),
624 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
625 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
626 ("limit".to_string(), "50".to_string()),
627 ("adjustment".to_string(), "split,dividend".to_string()),
628 ("feed".to_string(), "boats".to_string()),
629 ("currency".to_string(), "USD".to_string()),
630 ("page_token".to_string(), "page-123".to_string()),
631 ("sort".to_string(), "desc".to_string()),
632 ("asof".to_string(), "2024-03-04".to_string()),
633 ]
634 );
635 }
636
637 #[test]
638 fn stocks_auction_feed_serializes_to_official_strings() {
639 assert_eq!(AuctionFeed::Sip.to_string(), "sip");
640 }
641
642 #[test]
643 fn auctions_request_serializes_official_query_words() {
644 let request = AuctionsRequest {
645 symbols: vec!["AAPL".into(), "MSFT".into()],
646 start: Some("2024-03-01T00:00:00Z".into()),
647 end: Some("2024-03-05T00:00:00Z".into()),
648 limit: Some(10),
649 asof: Some("2024-03-04".into()),
650 feed: Some(AuctionFeed::Sip),
651 currency: Some(Currency::from("USD")),
652 page_token: Some("page-auctions".into()),
653 sort: Some(Sort::Asc),
654 };
655
656 assert_eq!(
657 request.to_query(),
658 vec![
659 ("symbols".to_string(), "AAPL,MSFT".to_string()),
660 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
661 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
662 ("limit".to_string(), "10".to_string()),
663 ("feed".to_string(), "sip".to_string()),
664 ("currency".to_string(), "USD".to_string()),
665 ("page_token".to_string(), "page-auctions".to_string()),
666 ("sort".to_string(), "asc".to_string()),
667 ("asof".to_string(), "2024-03-04".to_string()),
668 ]
669 );
670 }
671
672 #[test]
673 fn auctions_single_request_serializes_official_query_words() {
674 let request = AuctionsSingleRequest {
675 symbol: "AAPL".into(),
676 start: Some("2024-03-01T00:00:00Z".into()),
677 end: Some("2024-03-05T00:00:00Z".into()),
678 limit: Some(10),
679 asof: Some("2024-03-04".into()),
680 feed: Some(AuctionFeed::Sip),
681 currency: Some(Currency::from("USD")),
682 page_token: Some("page-auctions-single".into()),
683 sort: Some(Sort::Desc),
684 };
685
686 assert_eq!(
687 request.to_query(),
688 vec![
689 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
690 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
691 ("limit".to_string(), "10".to_string()),
692 ("feed".to_string(), "sip".to_string()),
693 ("currency".to_string(), "USD".to_string()),
694 ("page_token".to_string(), "page-auctions-single".to_string()),
695 ("sort".to_string(), "desc".to_string()),
696 ("asof".to_string(), "2024-03-04".to_string()),
697 ]
698 );
699 }
700
701 #[test]
702 fn quotes_request_serializes_official_query_words() {
703 let request = QuotesRequest {
704 symbols: vec!["AAPL".into(), "MSFT".into()],
705 start: Some("2024-03-01T00:00:00Z".into()),
706 end: Some("2024-03-05T00:00:00Z".into()),
707 limit: Some(25),
708 feed: Some(DataFeed::Iex),
709 sort: Some(Sort::Asc),
710 asof: Some("2024-03-04".into()),
711 currency: Some(Currency::from("USD")),
712 page_token: Some("page-456".into()),
713 };
714
715 assert_eq!(
716 request.to_query(),
717 vec![
718 ("symbols".to_string(), "AAPL,MSFT".to_string()),
719 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
720 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
721 ("limit".to_string(), "25".to_string()),
722 ("feed".to_string(), "iex".to_string()),
723 ("currency".to_string(), "USD".to_string()),
724 ("page_token".to_string(), "page-456".to_string()),
725 ("sort".to_string(), "asc".to_string()),
726 ("asof".to_string(), "2024-03-04".to_string()),
727 ]
728 );
729 }
730
731 #[test]
732 fn quotes_single_request_serializes_official_query_words() {
733 let request = QuotesSingleRequest {
734 symbol: "AAPL".into(),
735 start: Some("2024-03-01T00:00:00Z".into()),
736 end: Some("2024-03-05T00:00:00Z".into()),
737 limit: Some(25),
738 feed: Some(DataFeed::Iex),
739 sort: Some(Sort::Asc),
740 asof: Some("2024-03-04".into()),
741 currency: Some(Currency::from("USD")),
742 page_token: Some("page-456".into()),
743 };
744
745 assert_eq!(
746 request.to_query(),
747 vec![
748 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
749 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
750 ("limit".to_string(), "25".to_string()),
751 ("feed".to_string(), "iex".to_string()),
752 ("currency".to_string(), "USD".to_string()),
753 ("page_token".to_string(), "page-456".to_string()),
754 ("sort".to_string(), "asc".to_string()),
755 ("asof".to_string(), "2024-03-04".to_string()),
756 ]
757 );
758 }
759
760 #[test]
761 fn trades_request_serializes_official_query_words() {
762 let request = TradesRequest {
763 symbols: vec!["AAPL".into(), "MSFT".into()],
764 start: Some("2024-03-01T00:00:00Z".into()),
765 end: Some("2024-03-05T00:00:00Z".into()),
766 limit: Some(10),
767 feed: Some(DataFeed::Sip),
768 sort: Some(Sort::Desc),
769 asof: Some("2024-03-04".into()),
770 currency: Some(Currency::from("USD")),
771 page_token: Some("page-789".into()),
772 };
773
774 assert_eq!(
775 request.to_query(),
776 vec![
777 ("symbols".to_string(), "AAPL,MSFT".to_string()),
778 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
779 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
780 ("limit".to_string(), "10".to_string()),
781 ("feed".to_string(), "sip".to_string()),
782 ("currency".to_string(), "USD".to_string()),
783 ("page_token".to_string(), "page-789".to_string()),
784 ("sort".to_string(), "desc".to_string()),
785 ("asof".to_string(), "2024-03-04".to_string()),
786 ]
787 );
788 }
789
790 #[test]
791 fn trades_single_request_serializes_official_query_words() {
792 let request = TradesSingleRequest {
793 symbol: "AAPL".into(),
794 start: Some("2024-03-01T00:00:00Z".into()),
795 end: Some("2024-03-05T00:00:00Z".into()),
796 limit: Some(10),
797 feed: Some(DataFeed::Sip),
798 sort: Some(Sort::Desc),
799 asof: Some("2024-03-04".into()),
800 currency: Some(Currency::from("USD")),
801 page_token: Some("page-789".into()),
802 };
803
804 assert_eq!(
805 request.to_query(),
806 vec![
807 ("start".to_string(), "2024-03-01T00:00:00Z".to_string()),
808 ("end".to_string(), "2024-03-05T00:00:00Z".to_string()),
809 ("limit".to_string(), "10".to_string()),
810 ("feed".to_string(), "sip".to_string()),
811 ("currency".to_string(), "USD".to_string()),
812 ("page_token".to_string(), "page-789".to_string()),
813 ("sort".to_string(), "desc".to_string()),
814 ("asof".to_string(), "2024-03-04".to_string()),
815 ]
816 );
817 }
818
819 #[test]
820 fn latest_batch_requests_serialize_official_query_words() {
821 let bars = LatestBarsRequest {
822 symbols: vec!["AAPL".into(), "MSFT".into()],
823 feed: Some(DataFeed::DelayedSip),
824 currency: Some(Currency::from("USD")),
825 };
826
827 assert_eq!(
828 bars.to_query(),
829 vec![
830 ("symbols".to_string(), "AAPL,MSFT".to_string()),
831 ("feed".to_string(), "delayed_sip".to_string()),
832 ("currency".to_string(), "USD".to_string()),
833 ]
834 );
835
836 let trades = LatestTradesRequest {
837 symbols: vec!["AAPL".into(), "MSFT".into()],
838 feed: Some(DataFeed::Iex),
839 currency: Some(Currency::from("USD")),
840 };
841
842 assert_eq!(
843 trades.to_query(),
844 vec![
845 ("symbols".to_string(), "AAPL,MSFT".to_string()),
846 ("feed".to_string(), "iex".to_string()),
847 ("currency".to_string(), "USD".to_string()),
848 ]
849 );
850 }
851
852 #[test]
853 fn latest_single_requests_serialize_official_query_words() {
854 let bar = LatestBarRequest {
855 symbol: "AAPL".into(),
856 feed: Some(DataFeed::Sip),
857 currency: Some(Currency::from("USD")),
858 };
859
860 assert_eq!(
861 bar.to_query(),
862 vec![
863 ("feed".to_string(), "sip".to_string()),
864 ("currency".to_string(), "USD".to_string()),
865 ]
866 );
867
868 let trade = LatestTradeRequest {
869 symbol: "AAPL".into(),
870 feed: Some(DataFeed::Boats),
871 currency: Some(Currency::from("USD")),
872 };
873
874 assert_eq!(
875 trade.to_query(),
876 vec![
877 ("feed".to_string(), "boats".to_string()),
878 ("currency".to_string(), "USD".to_string()),
879 ]
880 );
881 }
882
883 #[test]
884 fn snapshot_requests_serialize_official_query_words() {
885 let batch = SnapshotsRequest {
886 symbols: vec!["AAPL".into(), "MSFT".into()],
887 feed: Some(DataFeed::Overnight),
888 currency: Some(Currency::from("USD")),
889 };
890
891 assert_eq!(
892 batch.to_query(),
893 vec![
894 ("symbols".to_string(), "AAPL,MSFT".to_string()),
895 ("feed".to_string(), "overnight".to_string()),
896 ("currency".to_string(), "USD".to_string()),
897 ]
898 );
899
900 let single = SnapshotRequest {
901 symbol: "AAPL".into(),
902 feed: Some(DataFeed::Otc),
903 currency: Some(Currency::from("USD")),
904 };
905
906 assert_eq!(
907 single.to_query(),
908 vec![
909 ("feed".to_string(), "otc".to_string()),
910 ("currency".to_string(), "USD".to_string()),
911 ]
912 );
913 }
914
915 #[test]
916 fn stocks_ticktype_and_tape_serialize_to_official_strings() {
917 assert_eq!(TickType::Trade.as_str(), "trade");
918 assert_eq!(TickType::Quote.as_str(), "quote");
919 assert_eq!(Tape::A.as_str(), "A");
920 assert_eq!(Tape::B.as_str(), "B");
921 assert_eq!(Tape::C.as_str(), "C");
922 }
923
924 #[test]
925 fn metadata_request_serializes_official_query_words() {
926 let request = ConditionCodesRequest {
927 ticktype: TickType::Trade,
928 tape: Tape::A,
929 };
930
931 assert_eq!(
932 request.to_query(),
933 vec![("tape".to_string(), "A".to_string()),]
934 );
935 }
936
937 #[test]
938 fn batch_requests_reject_empty_symbols_for_required_symbol_endpoints() {
939 let errors = [
940 BarsRequest::default()
941 .validate()
942 .expect_err("bars symbols must be required"),
943 AuctionsRequest::default()
944 .validate()
945 .expect_err("auctions symbols must be required"),
946 QuotesRequest::default()
947 .validate()
948 .expect_err("quotes symbols must be required"),
949 TradesRequest::default()
950 .validate()
951 .expect_err("trades symbols must be required"),
952 LatestBarsRequest::default()
953 .validate()
954 .expect_err("latest bars symbols must be required"),
955 LatestQuotesRequest::default()
956 .validate()
957 .expect_err("latest quotes symbols must be required"),
958 LatestTradesRequest::default()
959 .validate()
960 .expect_err("latest trades symbols must be required"),
961 SnapshotsRequest::default()
962 .validate()
963 .expect_err("snapshots symbols must be required"),
964 ];
965
966 for error in errors {
967 assert!(matches!(
968 error,
969 Error::InvalidRequest(message)
970 if message.contains("symbols") && message.contains("empty")
971 ));
972 }
973 }
974
975 #[test]
976 fn latest_single_requests_reject_blank_symbols() {
977 let errors = [
978 LatestQuoteRequest::default()
979 .validate()
980 .expect_err("latest quote symbol must be required"),
981 LatestQuoteRequest {
982 symbol: " ".into(),
983 ..LatestQuoteRequest::default()
984 }
985 .validate()
986 .expect_err("latest quote symbol must reject whitespace-only input"),
987 LatestTradeRequest::default()
988 .validate()
989 .expect_err("latest trade symbol must be required"),
990 LatestTradeRequest {
991 symbol: " ".into(),
992 ..LatestTradeRequest::default()
993 }
994 .validate()
995 .expect_err("latest trade symbol must reject whitespace-only input"),
996 SnapshotRequest::default()
997 .validate()
998 .expect_err("snapshot symbol must be required"),
999 SnapshotRequest {
1000 symbol: " ".into(),
1001 ..SnapshotRequest::default()
1002 }
1003 .validate()
1004 .expect_err("snapshot symbol must reject whitespace-only input"),
1005 ];
1006
1007 for error in errors {
1008 assert!(matches!(
1009 error,
1010 Error::InvalidRequest(message)
1011 if message.contains("symbol") && message.contains("invalid")
1012 ));
1013 }
1014 }
1015
1016 #[test]
1017 fn historical_requests_reject_limits_outside_documented_range() {
1018 let errors = [
1019 BarsRequest {
1020 symbols: vec!["AAPL".into()],
1021 limit: Some(0),
1022 ..BarsRequest::default()
1023 }
1024 .validate()
1025 .expect_err("bars limit below one must fail"),
1026 BarsSingleRequest {
1027 symbol: "AAPL".into(),
1028 limit: Some(10_001),
1029 ..BarsSingleRequest::default()
1030 }
1031 .validate()
1032 .expect_err("single bars limit above ten thousand must fail"),
1033 AuctionsRequest {
1034 symbols: vec!["AAPL".into()],
1035 limit: Some(0),
1036 ..AuctionsRequest::default()
1037 }
1038 .validate()
1039 .expect_err("auctions limit below one must fail"),
1040 AuctionsSingleRequest {
1041 symbol: "AAPL".into(),
1042 limit: Some(10_001),
1043 ..AuctionsSingleRequest::default()
1044 }
1045 .validate()
1046 .expect_err("single auctions limit above ten thousand must fail"),
1047 QuotesRequest {
1048 symbols: vec!["AAPL".into()],
1049 limit: Some(0),
1050 ..QuotesRequest::default()
1051 }
1052 .validate()
1053 .expect_err("quotes limit below one must fail"),
1054 QuotesSingleRequest {
1055 symbol: "AAPL".into(),
1056 limit: Some(10_001),
1057 ..QuotesSingleRequest::default()
1058 }
1059 .validate()
1060 .expect_err("single quotes limit above ten thousand must fail"),
1061 TradesRequest {
1062 symbols: vec!["AAPL".into()],
1063 limit: Some(0),
1064 ..TradesRequest::default()
1065 }
1066 .validate()
1067 .expect_err("trades limit below one must fail"),
1068 TradesSingleRequest {
1069 symbol: "AAPL".into(),
1070 limit: Some(10_001),
1071 ..TradesSingleRequest::default()
1072 }
1073 .validate()
1074 .expect_err("single trades limit above ten thousand must fail"),
1075 ];
1076
1077 for error in errors {
1078 assert!(matches!(
1079 error,
1080 Error::InvalidRequest(message)
1081 if message.contains("limit") && message.contains("10000")
1082 ));
1083 }
1084 }
1085}