dbn/encode/json/
sync.rs

1use std::io;
2
3use super::serialize::{to_json_string, to_json_string_with_sym};
4use crate::{
5    encode::{DbnEncodable, EncodeDbn, EncodeRecord, EncodeRecordRef, EncodeRecordTextExt},
6    rtype_dispatch, Error, Metadata, Result,
7};
8
9/// Type for encoding files and streams of DBN records in JSON lines.
10pub struct Encoder<W>
11where
12    W: io::Write,
13{
14    writer: W,
15    should_pretty_print: bool,
16    use_pretty_px: bool,
17    use_pretty_ts: bool,
18}
19
20/// Helper for constructing a JSON [`Encoder`].
21///
22/// No fields are required.
23pub struct EncoderBuilder<W>
24where
25    W: io::Write,
26{
27    writer: W,
28    should_pretty_print: bool,
29    use_pretty_px: bool,
30    use_pretty_ts: bool,
31}
32
33impl<W> EncoderBuilder<W>
34where
35    W: io::Write,
36{
37    /// Creates a new JSON encoder builder.
38    pub fn new(writer: W) -> Self {
39        Self {
40            writer,
41            should_pretty_print: false,
42            use_pretty_px: false,
43            use_pretty_ts: false,
44        }
45    }
46
47    /// Sets whether the JSON encoder should encode nicely-formatted JSON objects
48    /// with indentation. Defaults to `false` where each JSON object is compact with
49    /// no spacing.
50    pub fn should_pretty_print(mut self, should_pretty_print: bool) -> Self {
51        self.should_pretty_print = should_pretty_print;
52        self
53    }
54
55    /// Sets whether the JSON encoder will serialize price fields as a decimal. Defaults
56    /// to `false`.
57    pub fn use_pretty_px(mut self, use_pretty_px: bool) -> Self {
58        self.use_pretty_px = use_pretty_px;
59        self
60    }
61
62    /// Sets whether the JSON encoder will serialize timestamp fields as ISO8601
63    /// datetime strings. Defaults to `false`.
64    pub fn use_pretty_ts(mut self, use_pretty_ts: bool) -> Self {
65        self.use_pretty_ts = use_pretty_ts;
66        self
67    }
68
69    /// Creates the new encoder with the previously specified settings and if
70    /// `write_header` is `true`, encodes the header row.
71    pub fn build(self) -> Encoder<W> {
72        Encoder::new(
73            self.writer,
74            self.should_pretty_print,
75            self.use_pretty_px,
76            self.use_pretty_ts,
77        )
78    }
79}
80
81impl<W> Encoder<W>
82where
83    W: io::Write,
84{
85    /// Creates a new instance of [`Encoder`]. If `should_pretty_print` is `true`,
86    /// each JSON object will be nicely formatted and indented, instead of the default
87    /// compact output with no whitespace between key-value pairs.
88    pub fn new(
89        writer: W,
90        should_pretty_print: bool,
91        use_pretty_px: bool,
92        use_pretty_ts: bool,
93    ) -> Self {
94        Self {
95            writer,
96            should_pretty_print,
97            use_pretty_px,
98            use_pretty_ts,
99        }
100    }
101
102    /// Creates a builder for configuring an `Encoder` object.
103    pub fn builder(writer: W) -> EncoderBuilder<W> {
104        EncoderBuilder::new(writer)
105    }
106
107    /// Encodes `metadata` into JSON.
108    ///
109    /// # Errors
110    /// This function returns an error if there's an error writing to `writer`.
111    pub fn encode_metadata(&mut self, metadata: &Metadata) -> Result<()> {
112        let json = to_json_string(
113            metadata,
114            self.should_pretty_print,
115            self.use_pretty_px,
116            self.use_pretty_ts,
117        );
118        let io_err = |e| Error::io(e, "writing metadata");
119        self.writer.write_all(json.as_bytes()).map_err(io_err)?;
120        self.writer.flush().map_err(io_err)?;
121        Ok(())
122    }
123
124    /// Returns a reference to the underlying writer.
125    pub fn get_ref(&self) -> &W {
126        &self.writer
127    }
128
129    /// Returns a mutable reference to the underlying writer.
130    pub fn get_mut(&mut self) -> &mut W {
131        &mut self.writer
132    }
133}
134
135impl<W> EncodeRecord for Encoder<W>
136where
137    W: io::Write,
138{
139    fn encode_record<R: DbnEncodable>(&mut self, record: &R) -> Result<()> {
140        let json = to_json_string(
141            record,
142            self.should_pretty_print,
143            self.use_pretty_px,
144            self.use_pretty_ts,
145        );
146        match self.writer.write_all(json.as_bytes()) {
147            Ok(()) => Ok(()),
148            Err(e) => Err(Error::io(e, "writing record")),
149        }
150    }
151
152    fn flush(&mut self) -> Result<()> {
153        self.writer
154            .flush()
155            .map_err(|e| Error::io(e, "flushing output"))
156    }
157}
158
159impl<W> EncodeRecordRef for Encoder<W>
160where
161    W: io::Write,
162{
163    fn encode_record_ref(&mut self, record: crate::RecordRef) -> Result<()> {
164        rtype_dispatch!(record, self.encode_record())?
165    }
166
167    unsafe fn encode_record_ref_ts_out(
168        &mut self,
169        record: crate::RecordRef,
170        ts_out: bool,
171    ) -> Result<()> {
172        rtype_dispatch!(record, ts_out: ts_out, self.encode_record())?
173    }
174}
175
176impl<W> EncodeDbn for Encoder<W> where W: io::Write {}
177
178impl<W> EncodeRecordTextExt for Encoder<W>
179where
180    W: io::Write,
181{
182    fn encode_record_with_sym<R: DbnEncodable>(
183        &mut self,
184        record: &R,
185        symbol: Option<&str>,
186    ) -> Result<()> {
187        let json = to_json_string_with_sym(
188            record,
189            self.should_pretty_print,
190            self.use_pretty_px,
191            self.use_pretty_ts,
192            symbol,
193        );
194        match self.writer.write_all(json.as_bytes()) {
195            Ok(()) => Ok(()),
196            Err(e) => Err(Error::io(e, "writing record")),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    #![allow(clippy::clone_on_copy)]
204
205    use std::{array, io::BufWriter, num::NonZeroU64, os::raw::c_char};
206
207    use super::*;
208    use crate::{
209        compat::SYMBOL_CSTR_LEN_V1,
210        encode::test_data::{BID_ASK, RECORD_HEADER},
211        enums::{
212            rtype, InstrumentClass, SType, Schema, SecurityUpdateAction, StatType,
213            StatUpdateAction, UserDefinedInstrument,
214        },
215        record::{
216            str_to_c_chars, ErrorMsg, ImbalanceMsg, InstrumentDefMsg, MboMsg, Mbp10Msg, Mbp1Msg,
217            OhlcvMsg, RecordHeader, StatMsg, StatusMsg, TradeMsg, WithTsOut,
218        },
219        test_utils::VecStream,
220        Dataset, MappingInterval, RecordRef, SymbolMapping, FIXED_PRICE_SCALE,
221    };
222
223    fn write_json_to_string<R>(
224        records: &[R],
225        should_pretty_print: bool,
226        use_pretty_px: bool,
227        use_pretty_ts: bool,
228    ) -> String
229    where
230        R: DbnEncodable,
231    {
232        let mut buffer = Vec::new();
233        Encoder::new(
234            &mut buffer,
235            should_pretty_print,
236            use_pretty_px,
237            use_pretty_ts,
238        )
239        .encode_records(records)
240        .unwrap();
241        String::from_utf8(buffer).expect("valid UTF-8")
242    }
243
244    fn write_json_stream_to_string<R>(
245        records: Vec<R>,
246        should_pretty_print: bool,
247        use_pretty_px: bool,
248        use_pretty_ts: bool,
249    ) -> String
250    where
251        R: DbnEncodable,
252    {
253        let mut buffer = Vec::new();
254        let writer = BufWriter::new(&mut buffer);
255        Encoder::new(writer, should_pretty_print, use_pretty_px, use_pretty_ts)
256            .encode_stream(VecStream::new(records))
257            .unwrap();
258        String::from_utf8(buffer).expect("valid UTF-8")
259    }
260
261    fn write_json_metadata_to_string(metadata: &Metadata, should_pretty_print: bool) -> String {
262        let mut buffer = Vec::new();
263        let writer = BufWriter::new(&mut buffer);
264        Encoder::new(writer, should_pretty_print, true, true)
265            .encode_metadata(metadata)
266            .unwrap();
267        String::from_utf8(buffer).expect("valid UTF-8")
268    }
269
270    const HEADER_JSON: &str =
271        r#""hd":{"ts_event":"1658441851000000000","rtype":4,"publisher_id":1,"instrument_id":323}"#;
272    const BID_ASK_JSON: &str = r#"{"bid_px":"372000.000000000","ask_px":"372500.000000000","bid_sz":10,"ask_sz":5,"bid_ct":5,"ask_ct":2}"#;
273
274    #[test]
275    fn test_mbo_write_json() {
276        let data = vec![MboMsg {
277            hd: RECORD_HEADER,
278            order_id: 16,
279            price: 5500,
280            size: 3,
281            flags: 128.into(),
282            channel_id: 14,
283            action: 'R' as c_char,
284            side: 'N' as c_char,
285            ts_recv: 1658441891000000000,
286            ts_in_delta: 22_000,
287            sequence: 1_002_375,
288        }];
289        let slice_res = write_json_to_string(data.as_slice(), false, true, false);
290        let stream_res = write_json_stream_to_string(data, false, true, false);
291
292        assert_eq!(slice_res, stream_res);
293        assert_eq!(
294            slice_res,
295            format!(
296                "{{{},{HEADER_JSON},{}}}\n",
297                r#""ts_recv":"1658441891000000000""#,
298                r#""action":"R","side":"N","price":"0.000005500","size":3,"channel_id":14,"order_id":"16","flags":128,"ts_in_delta":22000,"sequence":1002375"#
299            )
300        );
301    }
302
303    #[test]
304    fn test_mbp1_write_json() {
305        let data = vec![Mbp1Msg {
306            hd: RECORD_HEADER,
307            price: 5500,
308            size: 3,
309            action: 'B' as c_char,
310            side: 'B' as c_char,
311            flags: 128.into(),
312            depth: 9,
313            ts_recv: 1658441891000000000,
314            ts_in_delta: 22_000,
315            sequence: 1_002_375,
316            levels: [BID_ASK],
317        }];
318        let slice_res = write_json_to_string(data.as_slice(), false, true, true);
319        let stream_res = write_json_stream_to_string(data, false, true, true);
320
321        assert_eq!(slice_res, stream_res);
322        assert_eq!(
323            slice_res,
324            format!(
325                "{{{},{},{},{}}}\n",
326                r#""ts_recv":"2022-07-21T22:18:11.000000000Z""#,
327                r#""hd":{"ts_event":"2022-07-21T22:17:31.000000000Z","rtype":4,"publisher_id":1,"instrument_id":323}"#,
328                r#""action":"B","side":"B","depth":9,"price":"0.000005500","size":3,"flags":128,"ts_in_delta":22000,"sequence":1002375"#,
329                format_args!("\"levels\":[{BID_ASK_JSON}]")
330            )
331        );
332    }
333
334    #[test]
335    fn test_mbp10_write_json() {
336        let data = vec![Mbp10Msg {
337            hd: RECORD_HEADER,
338            price: 5500,
339            size: 3,
340            action: 'T' as c_char,
341            side: 'N' as c_char,
342            flags: 128.into(),
343            depth: 9,
344            ts_recv: 1658441891000000000,
345            ts_in_delta: 22_000,
346            sequence: 1_002_375,
347            levels: array::from_fn(|_| BID_ASK.clone()),
348        }];
349        let slice_res = write_json_to_string(data.as_slice(), false, true, true);
350        let stream_res = write_json_stream_to_string(data, false, true, true);
351
352        assert_eq!(slice_res, stream_res);
353        assert_eq!(
354            slice_res,
355            format!(
356                "{{{},{},{},{}}}\n",
357                r#""ts_recv":"2022-07-21T22:18:11.000000000Z""#,
358                r#""hd":{"ts_event":"2022-07-21T22:17:31.000000000Z","rtype":4,"publisher_id":1,"instrument_id":323}"#,
359                r#""action":"T","side":"N","depth":9,"price":"0.000005500","size":3,"flags":128,"ts_in_delta":22000,"sequence":1002375"#,
360                format_args!("\"levels\":[{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON},{BID_ASK_JSON}]")
361            )
362        );
363    }
364
365    #[test]
366    fn test_trade_write_json() {
367        let data = vec![TradeMsg {
368            hd: RECORD_HEADER,
369            price: 5500,
370            size: 3,
371            action: 'C' as c_char,
372            side: 'B' as c_char,
373            flags: 128.into(),
374            depth: 9,
375            ts_recv: 1658441891000000000,
376            ts_in_delta: 22_000,
377            sequence: 1_002_375,
378        }];
379        let slice_res = write_json_to_string(data.as_slice(), false, false, false);
380        let stream_res = write_json_stream_to_string(data, false, false, false);
381
382        assert_eq!(slice_res, stream_res);
383        assert_eq!(
384            slice_res,
385            format!(
386                "{{{},{HEADER_JSON},{}}}\n",
387                r#""ts_recv":"1658441891000000000""#,
388                r#""action":"C","side":"B","depth":9,"price":"5500","size":3,"flags":128,"ts_in_delta":22000,"sequence":1002375"#,
389            )
390        );
391    }
392
393    #[test]
394    fn test_ohlcv_write_json() {
395        let data = vec![OhlcvMsg {
396            hd: RECORD_HEADER,
397            open: 5000,
398            high: 8000,
399            low: 3000,
400            close: 6000,
401            volume: 55_000,
402        }];
403        let slice_res = write_json_to_string(data.as_slice(), false, true, false);
404        let stream_res = write_json_stream_to_string(data, false, true, false);
405
406        assert_eq!(slice_res, stream_res);
407        assert_eq!(
408            slice_res,
409            format!(
410                "{{{HEADER_JSON},{}}}\n",
411                r#""open":"0.000005000","high":"0.000008000","low":"0.000003000","close":"0.000006000","volume":"55000""#,
412            )
413        );
414    }
415
416    #[test]
417    fn test_status_write_json() {
418        let data = vec![StatusMsg {
419            hd: RECORD_HEADER,
420            ts_recv: 1658441891000000000,
421            action: 1,
422            reason: 2,
423            trading_event: 3,
424            is_trading: b'Y' as c_char,
425            is_quoting: b'Y' as c_char,
426            is_short_sell_restricted: b'~' as c_char,
427            _reserved: Default::default(),
428        }];
429        let slice_res = write_json_to_string(data.as_slice(), false, false, true);
430        let stream_res = write_json_stream_to_string(data, false, false, true);
431
432        assert_eq!(slice_res, stream_res);
433        assert_eq!(
434            slice_res,
435            format!(
436                "{{{},{},{}}}\n",
437                r#""ts_recv":"2022-07-21T22:18:11.000000000Z""#,
438                r#""hd":{"ts_event":"2022-07-21T22:17:31.000000000Z","rtype":4,"publisher_id":1,"instrument_id":323}"#,
439                r#""action":1,"reason":2,"trading_event":3,"is_trading":"Y","is_quoting":"Y","is_short_sell_restricted":"~""#,
440            )
441        );
442    }
443
444    #[test]
445    fn test_instrument_def_write_json() {
446        let data = vec![InstrumentDefMsg {
447            hd: RECORD_HEADER,
448            ts_recv: 1658441891000000000,
449            min_price_increment: 100,
450            display_factor: 1_000_000_000,
451            expiration: 1698450000000000000,
452            activation: 1697350000000000000,
453            high_limit_price: 1_000_000,
454            low_limit_price: -1_000_000,
455            max_price_variation: 0,
456            trading_reference_price: 500_000,
457            unit_of_measure_qty: 5_000_000_000,
458            min_price_increment_amount: 5,
459            price_ratio: 10,
460            inst_attrib_value: 10,
461            underlying_id: 256785,
462            raw_instrument_id: RECORD_HEADER.instrument_id,
463            market_depth_implied: 0,
464            market_depth: 13,
465            market_segment_id: 0,
466            max_trade_vol: 10_000,
467            min_lot_size: 1,
468            min_lot_size_block: 1000,
469            min_lot_size_round_lot: 100,
470            min_trade_vol: 1,
471            contract_multiplier: 0,
472            decay_quantity: 0,
473            original_contract_size: 0,
474            trading_reference_date: 0,
475            appl_id: 0,
476            maturity_year: 0,
477            decay_start_date: 0,
478            channel_id: 4,
479            currency: str_to_c_chars("USD").unwrap(),
480            settl_currency: str_to_c_chars("USD").unwrap(),
481            secsubtype: Default::default(),
482            raw_symbol: str_to_c_chars("ESZ4 C4100").unwrap(),
483            group: str_to_c_chars("EW").unwrap(),
484            exchange: str_to_c_chars("XCME").unwrap(),
485            asset: str_to_c_chars("ES").unwrap(),
486            cfi: str_to_c_chars("OCAFPS").unwrap(),
487            security_type: str_to_c_chars("OOF").unwrap(),
488            unit_of_measure: str_to_c_chars("IPNT").unwrap(),
489            underlying: str_to_c_chars("ESZ4").unwrap(),
490            strike_price_currency: str_to_c_chars("USD").unwrap(),
491            instrument_class: InstrumentClass::Call as c_char,
492            strike_price: 4_100_000_000_000,
493            match_algorithm: 'F' as c_char,
494            md_security_trading_status: 2,
495            main_fraction: 4,
496            price_display_format: 8,
497            settl_price_type: 9,
498            sub_fraction: 23,
499            underlying_product: 10,
500            security_update_action: SecurityUpdateAction::Add as c_char,
501            maturity_month: 8,
502            maturity_day: 9,
503            maturity_week: 11,
504            user_defined_instrument: UserDefinedInstrument::No,
505            contract_multiplier_unit: 0,
506            flow_schedule_type: 5,
507            tick_rule: 0,
508            _reserved: Default::default(),
509        }];
510        let slice_res = write_json_to_string(data.as_slice(), false, true, true);
511        let stream_res = write_json_stream_to_string(data, false, true, true);
512
513        assert_eq!(slice_res, stream_res);
514        assert_eq!(
515            slice_res,
516            format!(
517                "{{{},{},{}}}\n",
518                r#""ts_recv":"2022-07-21T22:18:11.000000000Z""#,
519                r#""hd":{"ts_event":"2022-07-21T22:17:31.000000000Z","rtype":4,"publisher_id":1,"instrument_id":323}"#,
520                concat!(
521                    r#""raw_symbol":"ESZ4 C4100","security_update_action":"A","instrument_class":"C","min_price_increment":"0.000000100","display_factor":"1.000000000","expiration":"2023-10-27T23:40:00.000000000Z","activation":"2023-10-15T06:06:40.000000000Z","#,
522                    r#""high_limit_price":"0.001000000","low_limit_price":"-0.001000000","max_price_variation":"0.000000000","trading_reference_price":"0.000500000","unit_of_measure_qty":"5.000000000","#,
523                    r#""min_price_increment_amount":"0.000000005","price_ratio":"0.000000010","inst_attrib_value":10,"underlying_id":256785,"raw_instrument_id":323,"market_depth_implied":0,"#,
524                    r#""market_depth":13,"market_segment_id":0,"max_trade_vol":10000,"min_lot_size":1,"min_lot_size_block":1000,"min_lot_size_round_lot":100,"min_trade_vol":1,"#,
525                    r#""contract_multiplier":0,"decay_quantity":0,"original_contract_size":0,"trading_reference_date":0,"appl_id":0,"#,
526                    r#""maturity_year":0,"decay_start_date":0,"channel_id":4,"currency":"USD","settl_currency":"USD","secsubtype":"","group":"EW","exchange":"XCME","asset":"ES","cfi":"OCAFPS","#,
527                    r#""security_type":"OOF","unit_of_measure":"IPNT","underlying":"ESZ4","strike_price_currency":"USD","strike_price":"4100.000000000","match_algorithm":"F","md_security_trading_status":2,"main_fraction":4,"price_display_format":8,"#,
528                    r#""settl_price_type":9,"sub_fraction":23,"underlying_product":10,"maturity_month":8,"maturity_day":9,"maturity_week":11,"#,
529                    r#""user_defined_instrument":"N","contract_multiplier_unit":0,"flow_schedule_type":5,"tick_rule":0"#
530                )
531            )
532        );
533    }
534
535    #[test]
536    fn test_imbalance_write_json() {
537        let data = vec![ImbalanceMsg {
538            hd: RECORD_HEADER,
539            ts_recv: 1,
540            ref_price: 2,
541            auction_time: 3,
542            cont_book_clr_price: 4,
543            auct_interest_clr_price: 5,
544            ssr_filling_price: 6,
545            ind_match_price: 7,
546            upper_collar: 8,
547            lower_collar: 9,
548            paired_qty: 10,
549            total_imbalance_qty: 11,
550            market_imbalance_qty: 12,
551            unpaired_qty: 13,
552            auction_type: 'B' as c_char,
553            side: 'A' as c_char,
554            auction_status: 14,
555            freeze_status: 15,
556            num_extensions: 16,
557            unpaired_side: 'A' as c_char,
558            significant_imbalance: 'N' as c_char,
559            _reserved: [0],
560        }];
561        let slice_res = write_json_to_string(data.as_slice(), false, false, false);
562        let stream_res = write_json_stream_to_string(data, false, false, false);
563
564        assert_eq!(slice_res, stream_res);
565        assert_eq!(
566            slice_res,
567            format!(
568                "{{{},{HEADER_JSON},{}}}\n",
569                r#""ts_recv":"1""#,
570                concat!(
571                    r#""ref_price":"2","auction_time":"3","cont_book_clr_price":"4","auct_interest_clr_price":"5","#,
572                    r#""ssr_filling_price":"6","ind_match_price":"7","upper_collar":"8","lower_collar":"9","paired_qty":10,"#,
573                    r#""total_imbalance_qty":11,"market_imbalance_qty":12,"unpaired_qty":13,"auction_type":"B","side":"A","#,
574                    r#""auction_status":14,"freeze_status":15,"num_extensions":16,"unpaired_side":"A","significant_imbalance":"N""#,
575                )
576            )
577        );
578    }
579
580    #[test]
581    fn test_stat_write_json() {
582        let data = vec![StatMsg {
583            hd: RECORD_HEADER,
584            ts_recv: 1,
585            ts_ref: 2,
586            price: 3,
587            quantity: 0,
588            sequence: 4,
589            ts_in_delta: 5,
590            stat_type: StatType::OpeningPrice as u16,
591            channel_id: 7,
592            update_action: StatUpdateAction::New as u8,
593            stat_flags: 0,
594            _reserved: Default::default(),
595        }];
596        let slice_res = write_json_to_string(data.as_slice(), false, true, false);
597        let stream_res = write_json_stream_to_string(data, false, true, false);
598
599        assert_eq!(slice_res, stream_res);
600        assert_eq!(
601            slice_res,
602            format!(
603                "{{{},{HEADER_JSON},{}}}\n",
604                r#""ts_recv":"1""#,
605                concat!(
606                    r#""ts_ref":"2","price":"0.000000003","quantity":0,"sequence":4,"#,
607                    r#""ts_in_delta":5,"stat_type":1,"channel_id":7,"update_action":1,"stat_flags":0"#,
608                )
609            )
610        );
611    }
612
613    #[test]
614    fn test_metadata_write_json() {
615        let metadata = Metadata {
616            version: 1,
617            dataset: Dataset::GlbxMdp3.to_string(),
618            schema: Some(Schema::Ohlcv1H),
619            start: 1662734705128748281,
620            end: NonZeroU64::new(1662734720914876944),
621            limit: None,
622            stype_in: Some(SType::InstrumentId),
623            stype_out: SType::RawSymbol,
624            ts_out: false,
625            symbol_cstr_len: SYMBOL_CSTR_LEN_V1,
626            symbols: vec!["ESZ2".to_owned()],
627            partial: Vec::new(),
628            not_found: Vec::new(),
629            mappings: vec![SymbolMapping {
630                raw_symbol: "ESZ2".to_owned(),
631                intervals: vec![MappingInterval {
632                    start_date: time::Date::from_calendar_date(2022, time::Month::September, 9)
633                        .unwrap(),
634                    end_date: time::Date::from_calendar_date(2022, time::Month::September, 10)
635                        .unwrap(),
636                    symbol: "ESH2".to_owned(),
637                }],
638            }],
639        };
640        let res = write_json_metadata_to_string(&metadata, false);
641        assert_eq!(
642            res,
643            "{\"version\":1,\"dataset\":\"GLBX.MDP3\",\"schema\":\"ohlcv-1h\",\"start\"\
644            :\"2022-09-09T14:45:05.128748281Z\",\"end\":\"2022-09-09T14:45:20.914876944Z\",\"limit\":null,\
645            \"stype_in\":\"instrument_id\",\"stype_out\":\"raw_symbol\",\"ts_out\":false,\"symbol_cstr_len\":22,\"symbols\"\
646            :[\"ESZ2\"],\"partial\":[],\"not_found\":[],\"mappings\":[{\"raw_symbol\":\"ESZ2\",\
647            \"intervals\":[{\"start_date\":\"2022-09-09\",\"end_date\":\"2022-09-10\",\"symbol\":\
648            \"ESH2\"}]}]}\n"
649        );
650    }
651
652    #[test]
653    fn test_encode_with_ts_out() {
654        let records = vec![WithTsOut {
655            rec: OhlcvMsg {
656                hd: RECORD_HEADER,
657                open: 5000,
658                high: 8000,
659                low: 3000,
660                close: 6000,
661                volume: 55_000,
662            },
663            ts_out: 1678481869000000000,
664        }];
665        let res = write_json_to_string(records.as_slice(), false, false, true);
666        assert_eq!(
667            res,
668            format!(
669                "{{{},{}}}\n",
670                r#""hd":{"ts_event":"2022-07-21T22:17:31.000000000Z","rtype":4,"publisher_id":1,"instrument_id":323}"#,
671                r#""open":"5000","high":"8000","low":"3000","close":"6000","volume":"55000","ts_out":"2023-03-10T20:57:49.000000000Z""#,
672            )
673        );
674    }
675
676    #[test]
677    fn test_serialize_quoted_str_to_json() {
678        let json = write_json_to_string(
679            vec![ErrorMsg::new(0, None, "\"A test", true)].as_slice(),
680            false,
681            true,
682            true,
683        );
684        assert_eq!(
685            json,
686            r#"{"hd":{"ts_event":null,"rtype":21,"publisher_id":0,"instrument_id":0},"err":"\"A test","code":255,"is_last":1}
687"#
688        );
689    }
690
691    #[test]
692    fn test_encode_ref_with_sym() {
693        let mut buffer = Vec::new();
694        const BAR: OhlcvMsg = OhlcvMsg {
695            hd: RecordHeader::new::<OhlcvMsg>(rtype::OHLCV_1H, 10, 9, 0),
696            open: 175 * FIXED_PRICE_SCALE,
697            high: 177 * FIXED_PRICE_SCALE,
698            low: 174 * FIXED_PRICE_SCALE,
699            close: 175 * FIXED_PRICE_SCALE,
700            volume: 4033445,
701        };
702        let rec_ref = RecordRef::from(&BAR);
703        let mut encoder = Encoder::new(&mut buffer, false, false, false);
704        encoder.encode_ref_with_sym(rec_ref, None).unwrap();
705        encoder.encode_ref_with_sym(rec_ref, Some("AAPL")).unwrap();
706        let res = String::from_utf8(buffer).unwrap();
707        assert_eq!(
708            res,
709            "{\"hd\":{\"ts_event\":\"0\",\"rtype\":34,\"publisher_id\":10,\"instrument_id\":9},\"open\":\"175000000000\",\"high\":\"177000000000\",\"low\":\"174000000000\",\"close\":\"175000000000\",\"volume\":\"4033445\",\"symbol\":null}\n\
710            {\"hd\":{\"ts_event\":\"0\",\"rtype\":34,\"publisher_id\":10,\"instrument_id\":9},\"open\":\"175000000000\",\"high\":\"177000000000\",\"low\":\"174000000000\",\"close\":\"175000000000\",\"volume\":\"4033445\",\"symbol\":\"AAPL\"}\n",
711        );
712    }
713}