use super::super::Number;
#[allow(missing_docs)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScreenerEquityField {
Symbol = 0,
Timestamp = 1,
SortField = 2,
Frequency = 3,
Items = 4,
}
impl ScreenerEquityField {
pub fn index(&self) -> u32 {
*self as u32
}
pub fn all() -> &'static [ScreenerEquityField] {
use ScreenerEquityField::*;
&[Symbol, Timestamp, SortField, Frequency, Items]
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScreenerItem {
pub description: Option<String>,
pub last_price: Option<Number>,
pub market_share: Option<Number>,
pub net_change: Option<Number>,
pub net_percent_change: Option<Number>,
pub symbol: Option<String>,
pub total_volume: Option<i64>,
pub trades: Option<i64>,
pub volume: Option<i64>,
}
fn parse_num(v: &serde_json::Value) -> Option<Number> {
serde_json::from_value::<Number>(v.clone()).ok()
}
impl ScreenerItem {
pub(crate) fn from_value(value: &serde_json::Value) -> Option<Self> {
let map = value.as_object()?;
Some(Self {
description: map
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
last_price: map.get("lastPrice").and_then(parse_num),
market_share: map.get("marketShare").and_then(parse_num),
net_change: map.get("netChange").and_then(parse_num),
net_percent_change: map.get("netPercentChange").and_then(parse_num),
symbol: map.get("symbol").and_then(|v| v.as_str()).map(String::from),
total_volume: map.get("totalVolume").and_then(|v| v.as_i64()),
trades: map.get("trades").and_then(|v| v.as_i64()),
volume: map.get("volume").and_then(|v| v.as_i64()),
})
}
}
#[allow(missing_docs)]
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScreenerEquity {
pub key: Option<String>,
pub delayed: Option<bool>,
pub asset_main_type: Option<String>,
pub asset_sub_type: Option<String>,
pub cusip: Option<String>,
pub symbol: Option<String>,
pub timestamp: Option<i64>,
pub sort_field: Option<String>,
pub frequency: Option<i64>,
pub items: Option<Vec<ScreenerItem>>,
}
impl ScreenerEquity {
pub(crate) fn from_value(value: &serde_json::Value) -> Option<Self> {
let map = value.as_object()?;
let mut s = Self {
key: map.get("key").and_then(|v| v.as_str()).map(String::from),
delayed: map.get("delayed").and_then(|v| v.as_bool()),
asset_main_type: map
.get("assetMainType")
.and_then(|v| v.as_str())
.map(String::from),
asset_sub_type: map
.get("assetSubType")
.and_then(|v| v.as_str())
.map(String::from),
cusip: map.get("cusip").and_then(|v| v.as_str()).map(String::from),
..Self::default()
};
for (key, val) in map {
match key.as_str() {
"0" => s.symbol = val.as_str().map(String::from),
"1" => s.timestamp = val.as_i64(),
"2" => s.sort_field = val.as_str().map(String::from),
"3" => s.frequency = val.as_i64(),
"4" => {
s.items = val
.as_array()
.map(|arr| arr.iter().filter_map(ScreenerItem::from_value).collect());
}
_ => {}
}
}
Some(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn field_index_first() {
assert_eq!(ScreenerEquityField::Symbol.index(), 0);
}
#[test]
fn field_index_last() {
assert_eq!(ScreenerEquityField::Items.index(), 4);
}
#[test]
fn all_fields_count() {
assert_eq!(ScreenerEquityField::all().len(), 5);
}
#[test]
fn all_fields_sequential_indices() {
for (i, field) in ScreenerEquityField::all().iter().enumerate() {
assert_eq!(
field.index() as usize,
i,
"field at position {i} has wrong index"
);
}
}
#[test]
fn item_from_value_parses_sample() {
let input = json!({
"description": "NVIDIA Corporation",
"lastPrice": 120.5,
"marketShare": 5.25,
"netChange": 3.75,
"netPercentChange": 3.2105,
"symbol": "NVDA",
"totalVolume": 85000000,
"trades": 150000,
"volume": 12000000
});
let item = ScreenerItem::from_value(&input).expect("should parse JSON object");
assert_eq!(item.description.as_deref(), Some("NVIDIA Corporation"));
assert_eq!(item.last_price, Some("120.5".parse().unwrap()));
assert_eq!(item.market_share, Some("5.25".parse().unwrap()));
assert_eq!(item.net_change, Some("3.75".parse().unwrap()));
assert_eq!(item.net_percent_change, Some("3.2105".parse().unwrap()));
assert_eq!(item.symbol.as_deref(), Some("NVDA"));
assert_eq!(item.total_volume, Some(85000000));
assert_eq!(item.trades, Some(150000));
assert_eq!(item.volume, Some(12000000));
}
#[test]
fn item_from_value_returns_none_for_non_object() {
assert!(ScreenerItem::from_value(&json!(42)).is_none());
}
#[test]
fn from_value_parses_sample() {
let input = json!({
"key": "NASDAQ_VOLUME_0",
"0": "NASDAQ_VOLUME_0",
"1": 1234567890000_i64,
"2": "VOLUME",
"3": 0,
"4": [
{
"description": "NVIDIA Corporation",
"lastPrice": 120.5,
"symbol": "NVDA",
"totalVolume": 85000000
}
]
});
let screener = ScreenerEquity::from_value(&input).expect("should parse JSON object");
assert_eq!(screener.key, Some("NASDAQ_VOLUME_0".to_string()));
assert_eq!(screener.symbol, Some("NASDAQ_VOLUME_0".to_string()));
assert_eq!(screener.timestamp, Some(1234567890000));
assert_eq!(screener.sort_field, Some("VOLUME".to_string()));
assert_eq!(screener.frequency, Some(0));
let items = screener.items.expect("should have items");
assert_eq!(items.len(), 1);
assert_eq!(items[0].symbol.as_deref(), Some("NVDA"));
assert_eq!(items[0].last_price, Some("120.5".parse().unwrap()));
}
#[test]
fn from_value_returns_none_for_non_object() {
assert!(ScreenerEquity::from_value(&json!(42)).is_none());
assert!(ScreenerEquity::from_value(&json!("text")).is_none());
assert!(ScreenerEquity::from_value(&json!(null)).is_none());
}
}