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