use chrono::{DateTime, NaiveDate, Utc};
use serde_json::Value;
use crate::error::{Result, YfError};
pub fn get<'a>(v: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut cur = v;
for p in path {
cur = cur.get(p)?;
}
if let Some(raw) = cur.get("raw") {
return Some(raw);
}
Some(cur)
}
pub fn get_raw<'a>(v: &'a Value, path: &[&str]) -> Option<&'a Value> {
let mut cur = v;
for p in path {
cur = cur.get(p)?;
}
Some(cur)
}
pub fn get_f64(v: &Value, path: &[&str]) -> Option<f64> {
get(v, path).and_then(|x| x.as_f64())
}
pub fn get_i64(v: &Value, path: &[&str]) -> Option<i64> {
get(v, path).and_then(|x| x.as_i64().or_else(|| x.as_f64().map(|f| f as i64)))
}
pub fn get_u64(v: &Value, path: &[&str]) -> Option<u64> {
get(v, path).and_then(|x| x.as_u64().or_else(|| x.as_f64().map(|f| f as u64)))
}
pub fn get_str(v: &Value, path: &[&str]) -> Option<String> {
get(v, path).and_then(|x| x.as_str()).map(String::from)
}
pub fn get_date(v: &Value, path: &[&str]) -> Option<DateTime<Utc>> {
match get(v, path) {
Some(Value::Number(n)) => {
let secs = n.as_f64()?;
DateTime::from_timestamp(secs as i64, 0)
}
Some(Value::String(s)) => NaiveDate::parse_from_str(s, "%Y-%m-%d")
.ok()
.and_then(|d| d.and_hms_opt(0, 0, 0))
.map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
_ => None,
}
}
pub fn yf_result<'a>(v: &'a Value, key: &str) -> Result<&'a Value> {
v.get(key)
.and_then(|k| k.get("result"))
.ok_or_else(|| YfError::DataMissing(format!("{key}.result missing")))
}
pub fn yf_result_first<'a>(v: &'a Value, key: &str) -> Result<&'a Value> {
let r = yf_result(v, key)?;
r.as_array()
.and_then(|a| a.first())
.ok_or_else(|| YfError::DataMissing(format!("{key}.result[0] missing")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn unwraps_raw_by_default() {
let v = json!({"price": {"marketCap": {"raw": 3.0e12, "fmt": "3T"}}});
assert_eq!(get_f64(&v, &["price", "marketCap"]), Some(3.0e12));
assert_eq!(get_f64(&v, &["price", "marketCap", "raw"]), Some(3.0e12));
}
#[test]
fn bare_number_no_wrapper() {
let v = json!({"strike": 123.45});
assert_eq!(get_f64(&v, &["strike"]), Some(123.45));
}
#[test]
fn missing_node_is_none() {
let v = json!({"a": 1});
assert_eq!(get_f64(&v, &["b", "c"]), None);
assert_eq!(get_str(&v, &["a"]), None);
assert_eq!(get_date(&json!({}), &["a"]), None);
}
#[test]
fn get_raw_returns_wrapper() {
let v = json!({"x": {"raw": 5, "fmt": "5"}});
let w = get_raw(&v, &["x"]).unwrap();
assert!(w.get("raw").is_some());
assert!(w.get("fmt").is_some());
}
#[test]
fn get_str_unwraps_raw() {
let v = json!({"name": {"raw": "Apple", "fmt": "Apple"}});
assert_eq!(get_str(&v, &["name"]).as_deref(), Some("Apple"));
}
#[test]
fn get_i64_and_u64_from_float() {
let v = json!({"shares": {"raw": 1.5e9, "fmt": "1.5B"}});
assert_eq!(get_i64(&v, &["shares"]), Some(1_500_000_000));
assert_eq!(get_u64(&v, &["shares"]), Some(1_500_000_000));
}
#[test]
fn get_date_epoch_and_string() {
let epoch = json!({"d": {"raw": 1700000000, "fmt": "2023"}});
let dt = get_date(&epoch, &["d"]).unwrap();
assert_eq!(dt.format("%Y-%m-%d").to_string(), "2023-11-14");
let s = json!({"d": {"raw": "2023-01-15", "fmt": "Jan 15, 2023"}});
let dt = get_date(&s, &["d"]).unwrap();
assert_eq!(dt.format("%Y-%m-%d").to_string(), "2023-01-15");
assert_eq!(get_date(&json!({}), &["d"]), None);
}
#[test]
fn yf_result_returns_array() {
let v = json!({"finance": {"result": [{"a": 1}, {"b": 2}]}});
let r = yf_result(&v, "finance").unwrap();
assert_eq!(r.as_array().unwrap().len(), 2);
let first = yf_result_first(&v, "finance").unwrap();
assert_eq!(first.get("a").and_then(|x| x.as_i64()), Some(1));
}
#[test]
fn yf_result_missing_errors() {
assert!(yf_result(&json!({}), "finance").is_err());
assert!(yf_result_first(&json!({"finance": {"result": []}}), "finance").is_err());
}
}