Skip to main content

tvdata_rs/scanner/
response.rs

1use 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}