Skip to main content

nautilus_common/messages/data/
request.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::num::NonZeroUsize;
17
18use jiff::Timestamp;
19use nautilus_core::{Params, UUID4, UnixNanos};
20use nautilus_model::{
21    data::{BarType, DataType},
22    identifiers::{ClientId, InstrumentId, OptionSeriesId, Venue},
23};
24use serde::{Deserialize, Serialize};
25
26use super::{
27    IntoBarTypes, IntoInstrumentIds, check_client_id_or_venue,
28    identifiers::{
29        deserialize_bar_types, deserialize_instrument_ids, single_bar_type, single_instrument_id,
30    },
31};
32
33#[derive(Clone, Debug, Serialize, Deserialize)]
34pub struct RequestCustomData {
35    pub client_id: ClientId,
36    pub data_type: DataType,
37    pub start: Option<Timestamp>,
38    pub end: Option<Timestamp>,
39    pub limit: Option<NonZeroUsize>,
40    pub request_id: UUID4,
41    pub ts_init: UnixNanos,
42    pub params: Option<Params>,
43}
44
45impl RequestCustomData {
46    /// Creates a new [`RequestCustomData`] instance.
47    #[expect(clippy::too_many_arguments)]
48    pub fn new(
49        client_id: ClientId,
50        data_type: DataType,
51        start: Option<Timestamp>,
52        end: Option<Timestamp>,
53        limit: Option<NonZeroUsize>,
54        request_id: UUID4,
55        ts_init: UnixNanos,
56        params: Option<Params>,
57    ) -> Self {
58        Self {
59            client_id,
60            data_type,
61            start,
62            end,
63            limit,
64            request_id,
65            ts_init,
66            params,
67        }
68    }
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct RequestInstrument {
73    #[serde(deserialize_with = "deserialize_instrument_ids")]
74    pub instrument_ids: Vec<InstrumentId>,
75    pub start: Option<Timestamp>,
76    pub end: Option<Timestamp>,
77    pub client_id: Option<ClientId>,
78    pub request_id: UUID4,
79    pub ts_init: UnixNanos,
80    pub params: Option<Params>,
81}
82
83impl RequestInstrument {
84    /// Creates a new [`RequestInstrument`] instance.
85    ///
86    /// Duplicate entries are removed in first-occurrence order.
87    ///
88    /// # Panics
89    ///
90    /// Panics if the identifier list is empty.
91    pub fn new(
92        instrument_ids: impl IntoInstrumentIds,
93        start: Option<Timestamp>,
94        end: Option<Timestamp>,
95        client_id: Option<ClientId>,
96        request_id: UUID4,
97        ts_init: UnixNanos,
98        params: Option<Params>,
99    ) -> Self {
100        let instrument_ids = instrument_ids
101            .into_instrument_ids()
102            .expect("invalid instrument ID list");
103
104        Self {
105            instrument_ids,
106            start,
107            end,
108            client_id,
109            request_id,
110            ts_init,
111            params,
112        }
113    }
114
115    /// Returns the instrument ID for a single-member request path.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the request does not contain exactly one member.
120    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
121        single_instrument_id(&self.instrument_ids, "request")
122    }
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct RequestInstruments {
127    pub start: Option<Timestamp>,
128    pub end: Option<Timestamp>,
129    pub client_id: Option<ClientId>,
130    pub venue: Option<Venue>,
131    pub request_id: UUID4,
132    pub ts_init: UnixNanos,
133    pub params: Option<Params>,
134}
135
136impl RequestInstruments {
137    /// Creates a new [`RequestInstruments`] instance.
138    pub fn new(
139        start: Option<Timestamp>,
140        end: Option<Timestamp>,
141        client_id: Option<ClientId>,
142        venue: Option<Venue>,
143        request_id: UUID4,
144        ts_init: UnixNanos,
145        params: Option<Params>,
146    ) -> Self {
147        check_client_id_or_venue(&client_id, &venue);
148        Self {
149            start,
150            end,
151            client_id,
152            venue,
153            request_id,
154            ts_init,
155            params,
156        }
157    }
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
161pub struct RequestBookSnapshot {
162    #[serde(deserialize_with = "deserialize_instrument_ids")]
163    pub instrument_ids: Vec<InstrumentId>,
164    pub depth: Option<NonZeroUsize>,
165    pub client_id: Option<ClientId>,
166    pub request_id: UUID4,
167    pub ts_init: UnixNanos,
168    pub params: Option<Params>,
169}
170
171impl RequestBookSnapshot {
172    /// Creates a new [`RequestBookSnapshot`] instance.
173    ///
174    /// Duplicate entries are removed in first-occurrence order.
175    ///
176    /// # Panics
177    ///
178    /// Panics if the identifier list is empty.
179    pub fn new(
180        instrument_ids: impl IntoInstrumentIds,
181        depth: Option<NonZeroUsize>,
182        client_id: Option<ClientId>,
183        request_id: UUID4,
184        ts_init: UnixNanos,
185        params: Option<Params>,
186    ) -> Self {
187        let instrument_ids = instrument_ids
188            .into_instrument_ids()
189            .expect("invalid instrument ID list");
190
191        Self {
192            instrument_ids,
193            depth,
194            client_id,
195            request_id,
196            ts_init,
197            params,
198        }
199    }
200
201    /// Returns the instrument ID for a single-member request path.
202    ///
203    /// # Errors
204    ///
205    /// Returns an error if the request does not contain exactly one member.
206    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
207        single_instrument_id(&self.instrument_ids, "request")
208    }
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize)]
212pub struct RequestQuotes {
213    #[serde(deserialize_with = "deserialize_instrument_ids")]
214    pub instrument_ids: Vec<InstrumentId>,
215    pub start: Option<Timestamp>,
216    pub end: Option<Timestamp>,
217    pub limit: Option<NonZeroUsize>,
218    pub client_id: Option<ClientId>,
219    pub request_id: UUID4,
220    pub ts_init: UnixNanos,
221    pub params: Option<Params>,
222}
223
224impl RequestQuotes {
225    /// Creates a new [`RequestQuotes`] instance.
226    ///
227    /// Duplicate entries are removed in first-occurrence order.
228    ///
229    /// # Panics
230    ///
231    /// Panics if the identifier list is empty.
232    #[expect(clippy::too_many_arguments)]
233    pub fn new(
234        instrument_ids: impl IntoInstrumentIds,
235        start: Option<Timestamp>,
236        end: Option<Timestamp>,
237        limit: Option<NonZeroUsize>,
238        client_id: Option<ClientId>,
239        request_id: UUID4,
240        ts_init: UnixNanos,
241        params: Option<Params>,
242    ) -> Self {
243        let instrument_ids = instrument_ids
244            .into_instrument_ids()
245            .expect("invalid instrument ID list");
246
247        Self {
248            instrument_ids,
249            start,
250            end,
251            limit,
252            client_id,
253            request_id,
254            ts_init,
255            params,
256        }
257    }
258
259    /// Returns the instrument ID for a single-member request path.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the request does not contain exactly one member.
264    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
265        single_instrument_id(&self.instrument_ids, "request")
266    }
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
270pub struct RequestTrades {
271    #[serde(deserialize_with = "deserialize_instrument_ids")]
272    pub instrument_ids: Vec<InstrumentId>,
273    pub start: Option<Timestamp>,
274    pub end: Option<Timestamp>,
275    pub limit: Option<NonZeroUsize>,
276    pub client_id: Option<ClientId>,
277    pub request_id: UUID4,
278    pub ts_init: UnixNanos,
279    pub params: Option<Params>,
280}
281
282impl RequestTrades {
283    /// Creates a new [`RequestTrades`] instance.
284    ///
285    /// Duplicate entries are removed in first-occurrence order.
286    ///
287    /// # Panics
288    ///
289    /// Panics if the identifier list is empty.
290    #[expect(clippy::too_many_arguments)]
291    pub fn new(
292        instrument_ids: impl IntoInstrumentIds,
293        start: Option<Timestamp>,
294        end: Option<Timestamp>,
295        limit: Option<NonZeroUsize>,
296        client_id: Option<ClientId>,
297        request_id: UUID4,
298        ts_init: UnixNanos,
299        params: Option<Params>,
300    ) -> Self {
301        let instrument_ids = instrument_ids
302            .into_instrument_ids()
303            .expect("invalid instrument ID list");
304
305        Self {
306            instrument_ids,
307            start,
308            end,
309            limit,
310            client_id,
311            request_id,
312            ts_init,
313            params,
314        }
315    }
316
317    /// Returns the instrument ID for a single-member request path.
318    ///
319    /// # Errors
320    ///
321    /// Returns an error if the request does not contain exactly one member.
322    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
323        single_instrument_id(&self.instrument_ids, "request")
324    }
325}
326
327#[derive(Clone, Debug, Serialize, Deserialize)]
328pub struct RequestFundingRates {
329    #[serde(deserialize_with = "deserialize_instrument_ids")]
330    pub instrument_ids: Vec<InstrumentId>,
331    pub start: Option<Timestamp>,
332    pub end: Option<Timestamp>,
333    pub limit: Option<NonZeroUsize>,
334    pub client_id: Option<ClientId>,
335    pub request_id: UUID4,
336    pub ts_init: UnixNanos,
337    pub params: Option<Params>,
338}
339
340impl RequestFundingRates {
341    /// Creates a new [`RequestFundingRates`] instance.
342    ///
343    /// Duplicate entries are removed in first-occurrence order.
344    ///
345    /// # Panics
346    ///
347    /// Panics if the identifier list is empty.
348    #[expect(clippy::too_many_arguments)]
349    pub fn new(
350        instrument_ids: impl IntoInstrumentIds,
351        start: Option<Timestamp>,
352        end: Option<Timestamp>,
353        limit: Option<NonZeroUsize>,
354        client_id: Option<ClientId>,
355        request_id: UUID4,
356        ts_init: UnixNanos,
357        params: Option<Params>,
358    ) -> Self {
359        let instrument_ids = instrument_ids
360            .into_instrument_ids()
361            .expect("invalid instrument ID list");
362
363        Self {
364            instrument_ids,
365            start,
366            end,
367            limit,
368            client_id,
369            request_id,
370            ts_init,
371            params,
372        }
373    }
374
375    /// Returns the instrument ID for a single-member request path.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if the request does not contain exactly one member.
380    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
381        single_instrument_id(&self.instrument_ids, "request")
382    }
383}
384
385#[derive(Clone, Debug, Serialize, Deserialize)]
386pub struct RequestOptionChainReferencePrice {
387    pub series_id: OptionSeriesId,
388    pub instrument_id: InstrumentId,
389    pub client_id: Option<ClientId>,
390    pub request_id: UUID4,
391    pub ts_init: UnixNanos,
392    pub params: Option<Params>,
393}
394
395impl RequestOptionChainReferencePrice {
396    /// Creates a new [`RequestOptionChainReferencePrice`] instance.
397    pub fn new(
398        series_id: OptionSeriesId,
399        instrument_id: InstrumentId,
400        client_id: Option<ClientId>,
401        request_id: UUID4,
402        ts_init: UnixNanos,
403        params: Option<Params>,
404    ) -> Self {
405        Self {
406            series_id,
407            instrument_id,
408            client_id,
409            request_id,
410            ts_init,
411            params,
412        }
413    }
414}
415
416#[derive(Clone, Debug, Serialize, Deserialize)]
417pub struct RequestBookDepth {
418    #[serde(deserialize_with = "deserialize_instrument_ids")]
419    pub instrument_ids: Vec<InstrumentId>,
420    pub start: Option<Timestamp>,
421    pub end: Option<Timestamp>,
422    pub limit: Option<NonZeroUsize>,
423    pub depth: Option<NonZeroUsize>,
424    pub client_id: Option<ClientId>,
425    pub request_id: UUID4,
426    pub ts_init: UnixNanos,
427    pub params: Option<Params>,
428}
429
430impl RequestBookDepth {
431    /// Creates a new [`RequestBookDepth`] instance.
432    ///
433    /// Duplicate entries are removed in first-occurrence order.
434    ///
435    /// # Panics
436    ///
437    /// Panics if the identifier list is empty.
438    #[expect(clippy::too_many_arguments)]
439    pub fn new(
440        instrument_ids: impl IntoInstrumentIds,
441        start: Option<Timestamp>,
442        end: Option<Timestamp>,
443        limit: Option<NonZeroUsize>,
444        depth: Option<NonZeroUsize>,
445        client_id: Option<ClientId>,
446        request_id: UUID4,
447        ts_init: UnixNanos,
448        params: Option<Params>,
449    ) -> Self {
450        let instrument_ids = instrument_ids
451            .into_instrument_ids()
452            .expect("invalid instrument ID list");
453
454        Self {
455            instrument_ids,
456            start,
457            end,
458            limit,
459            depth,
460            client_id,
461            request_id,
462            ts_init,
463            params,
464        }
465    }
466
467    /// Returns the instrument ID for a single-member request path.
468    ///
469    /// # Errors
470    ///
471    /// Returns an error if the request does not contain exactly one member.
472    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
473        single_instrument_id(&self.instrument_ids, "request")
474    }
475}
476
477#[derive(Clone, Debug, Serialize, Deserialize)]
478pub struct RequestBookDeltas {
479    #[serde(deserialize_with = "deserialize_instrument_ids")]
480    pub instrument_ids: Vec<InstrumentId>,
481    pub start: Option<Timestamp>,
482    pub end: Option<Timestamp>,
483    pub limit: Option<NonZeroUsize>,
484    pub client_id: Option<ClientId>,
485    pub request_id: UUID4,
486    pub ts_init: UnixNanos,
487    pub params: Option<Params>,
488}
489
490impl RequestBookDeltas {
491    /// Creates a new [`RequestBookDeltas`] instance.
492    ///
493    /// Duplicate entries are removed in first-occurrence order.
494    ///
495    /// # Panics
496    ///
497    /// Panics if the identifier list is empty.
498    #[expect(clippy::too_many_arguments)]
499    pub fn new(
500        instrument_ids: impl IntoInstrumentIds,
501        start: Option<Timestamp>,
502        end: Option<Timestamp>,
503        limit: Option<NonZeroUsize>,
504        client_id: Option<ClientId>,
505        request_id: UUID4,
506        ts_init: UnixNanos,
507        params: Option<Params>,
508    ) -> Self {
509        let instrument_ids = instrument_ids
510            .into_instrument_ids()
511            .expect("invalid instrument ID list");
512
513        Self {
514            instrument_ids,
515            start,
516            end,
517            limit,
518            client_id,
519            request_id,
520            ts_init,
521            params,
522        }
523    }
524
525    /// Returns the instrument ID for a single-member request path.
526    ///
527    /// # Errors
528    ///
529    /// Returns an error if the request does not contain exactly one member.
530    pub fn single_instrument_id(&self) -> anyhow::Result<InstrumentId> {
531        single_instrument_id(&self.instrument_ids, "request")
532    }
533}
534
535#[derive(Clone, Debug, Serialize, Deserialize)]
536pub struct RequestBars {
537    #[serde(deserialize_with = "deserialize_bar_types")]
538    pub bar_types: Vec<BarType>,
539    pub start: Option<Timestamp>,
540    pub end: Option<Timestamp>,
541    pub limit: Option<NonZeroUsize>,
542    pub client_id: Option<ClientId>,
543    pub request_id: UUID4,
544    pub ts_init: UnixNanos,
545    pub params: Option<Params>,
546}
547
548impl RequestBars {
549    /// Creates a new [`RequestBars`] instance.
550    ///
551    /// Duplicate entries are removed in first-occurrence order.
552    ///
553    /// # Panics
554    ///
555    /// Panics if the identifier list is empty.
556    #[expect(clippy::too_many_arguments)]
557    pub fn new(
558        bar_types: impl IntoBarTypes,
559        start: Option<Timestamp>,
560        end: Option<Timestamp>,
561        limit: Option<NonZeroUsize>,
562        client_id: Option<ClientId>,
563        request_id: UUID4,
564        ts_init: UnixNanos,
565        params: Option<Params>,
566    ) -> Self {
567        let bar_types = bar_types.into_bar_types().expect("invalid bar type list");
568
569        Self {
570            bar_types,
571            start,
572            end,
573            limit,
574            client_id,
575            request_id,
576            ts_init,
577            params,
578        }
579    }
580
581    /// Returns the bar type for a single-member request path.
582    ///
583    /// # Errors
584    ///
585    /// Returns an error if the request does not contain exactly one member.
586    pub fn single_bar_type(&self) -> anyhow::Result<BarType> {
587        single_bar_type(&self.bar_types, "request")
588    }
589}
590
591/// A request to join multiple in-flight data requests under a single parent response.
592///
593/// The engine first issues a combined date-range request to bound the join window,
594/// then fans out the leg responses through the request-pipeline machinery so the
595/// caller receives one consolidated `DataResponse` keyed by the join `request_id`.
596#[derive(Clone, Debug, Serialize, Deserialize)]
597pub struct RequestJoin {
598    pub request_ids: Vec<UUID4>,
599    pub start: Option<Timestamp>,
600    pub end: Option<Timestamp>,
601    pub request_id: UUID4,
602    pub ts_init: UnixNanos,
603    pub params: Option<Params>,
604    pub correlation_id: Option<UUID4>,
605}
606
607impl RequestJoin {
608    /// Creates a new [`RequestJoin`] instance.
609    ///
610    /// # Panics
611    ///
612    /// Panics if `request_ids` is empty.
613    pub fn new(
614        request_ids: Vec<UUID4>,
615        start: Option<Timestamp>,
616        end: Option<Timestamp>,
617        request_id: UUID4,
618        ts_init: UnixNanos,
619        params: Option<Params>,
620        correlation_id: Option<UUID4>,
621    ) -> Self {
622        assert!(!request_ids.is_empty(), "request_ids must not be empty");
623        Self {
624            request_ids,
625            start,
626            end,
627            request_id,
628            ts_init,
629            params,
630            correlation_id,
631        }
632    }
633
634    /// Returns a fresh [`RequestJoin`] for the combined date-range bootstrap leg.
635    ///
636    /// The returned request inherits `request_ids` and `params`, carries the
637    /// supplied dates, and sets `correlation_id` to the original request id so
638    /// the response can be matched back to the parent join.
639    #[must_use]
640    pub fn with_dates(
641        &self,
642        start: Option<Timestamp>,
643        end: Option<Timestamp>,
644        ts_init: UnixNanos,
645    ) -> Self {
646        Self {
647            request_ids: self.request_ids.clone(),
648            start,
649            end,
650            request_id: UUID4::new(),
651            ts_init,
652            params: self.params.clone(),
653            correlation_id: Some(self.request_id),
654        }
655    }
656}
657
658#[cfg(test)]
659mod tests {
660    use rstest::rstest;
661
662    use super::*;
663    use crate::messages::data::IdentifierCountError;
664
665    macro_rules! instrument_request_tests {
666        ($test:ident, $request:ident, $($field:ident = $value:expr),* $(,)?) => {
667            #[rstest]
668            fn $test() {
669                let first = InstrumentId::from("ESM4.GLBX");
670                let second = InstrumentId::from("NQM4.GLBX");
671                let client_id = Some(ClientId::from("HISTORY"));
672                let request_id = UUID4::new();
673                let ts_init = UnixNanos::from(29);
674                let params = Some(Params::new([("source".into(), "history".into())].into()));
675                let request = $request::new(
676                    [second, first, second],
677                    $($value,)*
678                    client_id,
679                    request_id,
680                    ts_init,
681                    params.clone(),
682                );
683                let mut serialized = serde_json::to_value(&request).unwrap();
684                serialized["instrument_ids"] = serde_json::to_value([first, second, first]).unwrap();
685                let decoded: $request = serde_json::from_value(serialized.clone()).unwrap();
686                let mut expected = serialized.clone();
687                expected["instrument_ids"] = serde_json::to_value([first, second]).unwrap();
688                let single = $request::new(
689                    first,
690                    $($value,)*
691                    client_id,
692                    request_id,
693                    ts_init,
694                    params.clone(),
695                );
696                serialized["instrument_ids"] = serde_json::json!([]);
697                let empty = serde_json::from_value::<$request>(serialized);
698                let rejection = request.single_instrument_id().unwrap_err();
699
700                assert_eq!(request.instrument_ids, vec![second, first]);
701                $(assert_eq!(request.$field, $value);)*
702                assert_eq!(request.client_id, client_id);
703                assert_eq!(request.request_id, request_id);
704                assert_eq!(request.ts_init, ts_init);
705                assert_eq!(request.params, params);
706                assert!(rejection.is::<IdentifierCountError>());
707                assert_eq!(
708                    rejection.to_string(),
709                    "request requires one instrument ID on this adapter path, received 2",
710                );
711                assert_eq!(serde_json::to_value(decoded).unwrap(), expected);
712                assert_eq!(single.single_instrument_id().unwrap(), first);
713                assert!(empty.unwrap_err().to_string().contains("instrument IDs cannot be empty"));
714            }
715        };
716    }
717
718    instrument_request_tests!(
719        test_instrument_request_identifier_lists,
720        RequestInstrument,
721        start = Some(Timestamp::from_second(11).unwrap()),
722        end = Some(Timestamp::from_second(19).unwrap()),
723    );
724    instrument_request_tests!(
725        test_book_snapshot_request_identifier_lists,
726        RequestBookSnapshot,
727        depth = NonZeroUsize::new(7),
728    );
729    instrument_request_tests!(
730        test_quotes_request_identifier_lists,
731        RequestQuotes,
732        start = Some(Timestamp::from_second(11).unwrap()),
733        end = Some(Timestamp::from_second(19).unwrap()),
734        limit = NonZeroUsize::new(3),
735    );
736    instrument_request_tests!(
737        test_trades_request_identifier_lists,
738        RequestTrades,
739        start = Some(Timestamp::from_second(11).unwrap()),
740        end = Some(Timestamp::from_second(19).unwrap()),
741        limit = NonZeroUsize::new(3),
742    );
743    instrument_request_tests!(
744        test_funding_rates_request_identifier_lists,
745        RequestFundingRates,
746        start = Some(Timestamp::from_second(11).unwrap()),
747        end = Some(Timestamp::from_second(19).unwrap()),
748        limit = NonZeroUsize::new(3),
749    );
750    instrument_request_tests!(
751        test_book_depth_request_identifier_lists,
752        RequestBookDepth,
753        start = Some(Timestamp::from_second(11).unwrap()),
754        end = Some(Timestamp::from_second(19).unwrap()),
755        limit = NonZeroUsize::new(3),
756        depth = NonZeroUsize::new(7),
757    );
758    instrument_request_tests!(
759        test_book_deltas_request_identifier_lists,
760        RequestBookDeltas,
761        start = Some(Timestamp::from_second(11).unwrap()),
762        end = Some(Timestamp::from_second(19).unwrap()),
763        limit = NonZeroUsize::new(3),
764    );
765
766    #[rstest]
767    fn test_bars_request_identifier_lists() {
768        let first = BarType::from("ESM4.GLBX-1-MINUTE-LAST-EXTERNAL");
769        let second = BarType::from("ESM4.GLBX-5-MINUTE-LAST-EXTERNAL");
770        let start = Some(Timestamp::from_second(11).unwrap());
771        let end = Some(Timestamp::from_second(19).unwrap());
772        let limit = NonZeroUsize::new(3);
773        let client_id = Some(ClientId::from("HISTORY"));
774        let request_id = UUID4::new();
775        let ts_init = UnixNanos::from(29);
776        let params = Some(Params::new([("source".into(), "history".into())].into()));
777
778        let request = RequestBars::new(
779            [second, first, second],
780            start,
781            end,
782            limit,
783            client_id,
784            request_id,
785            ts_init,
786            params.clone(),
787        );
788        let mut serialized = serde_json::to_value(&request).unwrap();
789        serialized["bar_types"] = serde_json::to_value([first, second, first]).unwrap();
790        let decoded: RequestBars = serde_json::from_value(serialized.clone()).unwrap();
791        let mut expected = serialized.clone();
792        expected["bar_types"] = serde_json::to_value([first, second]).unwrap();
793        serialized["bar_types"] = serde_json::json!([]);
794        let empty = serde_json::from_value::<RequestBars>(serialized);
795        let rejection = request.single_bar_type().unwrap_err();
796
797        assert_eq!(request.bar_types, vec![second, first]);
798        assert_eq!(request.start, start);
799        assert_eq!(request.end, end);
800        assert_eq!(request.limit, limit);
801        assert_eq!(request.client_id, client_id);
802        assert_eq!(request.request_id, request_id);
803        assert_eq!(request.ts_init, ts_init);
804        assert_eq!(request.params, params);
805        assert!(rejection.is::<IdentifierCountError>());
806        assert_eq!(
807            rejection.to_string(),
808            "request requires one bar type on this adapter path, received 2",
809        );
810        assert_eq!(serde_json::to_value(decoded).unwrap(), expected);
811        assert!(
812            empty
813                .unwrap_err()
814                .to_string()
815                .contains("bar types cannot be empty")
816        );
817    }
818
819    fn leg_request_ids() -> Vec<UUID4> {
820        vec![
821            UUID4::from("00000000-0000-4000-8000-000000000001"),
822            UUID4::from("00000000-0000-4000-8000-000000000002"),
823        ]
824    }
825
826    fn join_request() -> RequestJoin {
827        let params = Params::new([("venue".into(), "SIM".into())].into());
828
829        RequestJoin::new(
830            leg_request_ids(),
831            Some(Timestamp::from_second(1_700_000_000).unwrap()),
832            Some(Timestamp::from_second(1_700_003_600).unwrap()),
833            UUID4::from("00000000-0000-4000-8000-000000000003"),
834            UnixNanos::from(7),
835            Some(params),
836            None,
837        )
838    }
839
840    #[rstest]
841    fn test_new_assigns_every_field() {
842        let request = join_request();
843
844        assert_eq!(request.request_ids, leg_request_ids());
845        assert_eq!(
846            request.request_id,
847            UUID4::from("00000000-0000-4000-8000-000000000003")
848        );
849        assert_eq!(
850            request.start,
851            Some(Timestamp::from_second(1_700_000_000).unwrap())
852        );
853        assert_eq!(
854            request.end,
855            Some(Timestamp::from_second(1_700_003_600).unwrap())
856        );
857        assert_eq!(request.ts_init, UnixNanos::from(7));
858        assert_eq!(
859            request.params.as_ref().unwrap().get("venue"),
860            Some(&"SIM".into())
861        );
862        assert_eq!(request.correlation_id, None);
863    }
864
865    #[rstest]
866    #[should_panic(expected = "request_ids must not be empty")]
867    fn test_new_rejects_empty_request_ids() {
868        let _ = RequestJoin::new(
869            Vec::new(),
870            None,
871            None,
872            UUID4::new(),
873            UnixNanos::from(1),
874            None,
875            None,
876        );
877    }
878
879    #[rstest]
880    fn test_with_dates_correlates_the_leg_back_to_the_parent() {
881        let parent = join_request();
882        let start = Timestamp::from_second(1_700_010_000).unwrap();
883        let end = Timestamp::from_second(1_700_013_600).unwrap();
884
885        let leg = parent.with_dates(Some(start), Some(end), UnixNanos::from(11));
886
887        assert_eq!(leg.correlation_id, Some(parent.request_id));
888        assert_ne!(leg.request_id, parent.request_id);
889        assert_eq!(leg.request_ids, parent.request_ids);
890        assert_eq!(leg.params, parent.params);
891        assert_eq!(leg.start, Some(start));
892        assert_eq!(leg.end, Some(end));
893        assert_eq!(leg.ts_init, UnixNanos::from(11));
894    }
895
896    #[rstest]
897    fn test_with_dates_replaces_rather_than_merges_the_parent_window() {
898        let parent = join_request();
899
900        let leg = parent.with_dates(None, None, UnixNanos::from(13));
901
902        assert_eq!(leg.start, None);
903        assert_eq!(leg.end, None);
904        assert_eq!(leg.correlation_id, Some(parent.request_id));
905    }
906
907    #[rstest]
908    fn test_with_dates_always_mints_a_fresh_request_id() {
909        let parent = join_request();
910
911        let first = parent.with_dates(None, None, UnixNanos::from(1));
912        let second = parent.with_dates(None, None, UnixNanos::from(1));
913
914        assert_ne!(first.request_id, second.request_id);
915        assert_eq!(first.correlation_id, second.correlation_id);
916    }
917}