tvdata_rs/scanner/
response.rs1use std::collections::BTreeMap;
2
3use serde::Deserialize;
4use serde_json::Value;
5
6use crate::error::Error;
7use crate::scanner::field::Column;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct ScanResponse {
11 pub total_count: usize,
12 pub rows: Vec<ScanRow>,
13 pub params: Option<Value>,
14}
15
16#[derive(Debug, Clone, PartialEq, Deserialize)]
17pub struct ScanRow {
18 #[serde(rename = "s")]
19 pub symbol: String,
20 #[serde(
21 rename = "d",
22 default = "Vec::new",
23 deserialize_with = "deserialize_nullable_vec"
24 )]
25 pub values: Vec<Value>,
26}
27
28impl ScanRow {
29 pub fn as_record(&self, columns: &[Column]) -> BTreeMap<String, Value> {
30 columns
31 .iter()
32 .zip(self.values.iter())
33 .map(|(column, value)| (column.as_str().to_owned(), value.clone()))
34 .collect()
35 }
36}
37
38#[derive(Debug, Clone, PartialEq, Deserialize)]
39pub struct RawScanResponse {
40 #[serde(rename = "totalCount", default)]
41 pub total_count: usize,
42 #[serde(default = "Vec::new", deserialize_with = "deserialize_nullable_vec")]
43 pub data: Vec<ScanRow>,
44 #[serde(default)]
45 pub params: Option<Value>,
46 #[serde(default)]
47 pub error: Option<String>,
48}
49
50impl RawScanResponse {
51 pub fn into_response(self) -> crate::error::Result<ScanResponse> {
52 if let Some(error) = self.error {
53 return Err(Error::ApiMessage(error));
54 }
55
56 Ok(ScanResponse {
57 total_count: self.total_count,
58 rows: self.data,
59 params: self.params,
60 })
61 }
62}
63
64fn deserialize_nullable_vec<'de, D, T>(deserializer: D) -> std::result::Result<Vec<T>, D::Error>
65where
66 D: serde::Deserializer<'de>,
67 T: Deserialize<'de>,
68{
69 Ok(Option::<Vec<T>>::deserialize(deserializer)?.unwrap_or_default())
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn converts_rows_into_named_records() {
78 let row = ScanRow {
79 symbol: "NASDAQ:AAPL".to_owned(),
80 values: vec![Value::String("AAPL".to_owned()), Value::from(247.99)],
81 };
82 let record = row.as_record(&[Column::from_static("name"), Column::from_static("close")]);
83 assert_eq!(record["name"], Value::String("AAPL".to_owned()));
84 assert_eq!(record["close"], Value::from(247.99));
85 }
86
87 #[test]
88 fn raw_response_handles_null_data() {
89 let raw: RawScanResponse =
90 serde_json::from_str(r#"{"totalCount":0,"error":"Unknown field","data":null}"#)
91 .unwrap();
92 assert!(raw.data.is_empty());
93 }
94
95 #[test]
96 fn raw_response_fixture_round_trips_realistic_rows() {
97 let raw: RawScanResponse = serde_json::from_str(include_str!(
98 "../../tests/fixtures/scanner/scan_response.json"
99 ))
100 .unwrap();
101 let response = raw.into_response().unwrap();
102
103 assert_eq!(response.total_count, 2);
104 assert_eq!(response.rows[0].symbol, "NASDAQ:AAPL");
105 assert_eq!(response.rows[1].values[2], Value::Null);
106 assert_eq!(
107 response
108 .params
109 .as_ref()
110 .and_then(|params| params.get("markets")),
111 Some(&serde_json::json!(["america"]))
112 );
113 }
114}