use crate::constants::{Frequency, StatementType};
use crate::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Deserialize)]
struct RawTimeseriesResponse {
timeseries: RawTimeseries,
}
#[derive(Debug, Clone, Deserialize)]
struct RawTimeseries {
result: Vec<RawTimeseriesResult>,
#[allow(dead_code)]
error: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
struct RawTimeseriesResult {
meta: RawMeta,
#[serde(flatten)]
data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
struct RawMeta {
#[serde(rename = "type")]
data_type: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FinancialStatement {
pub symbol: String,
pub statement_type: String,
pub frequency: String,
pub statement: HashMap<String, HashMap<String, f64>>,
}
impl FinancialStatement {
pub(crate) fn from_response(
raw: &serde_json::Value,
symbol: &str,
statement_type: StatementType,
frequency: Frequency,
) -> Result<Self> {
let raw_response: RawTimeseriesResponse =
serde_json::from_value(raw.clone()).map_err(|e| {
crate::error::FinanceError::ResponseStructureError {
field: "timeseries".to_string(),
context: format!("Failed to parse financials response: {}", e),
}
})?;
if raw_response.timeseries.result.is_empty() {
return Err(crate::error::FinanceError::SymbolNotFound {
symbol: Some(symbol.to_string()),
context: format!(
"No {} {} data found",
frequency.as_str(),
statement_type.as_str()
),
});
}
let mut statement: HashMap<String, HashMap<String, f64>> = HashMap::new();
for result in raw_response.timeseries.result {
let metric_name_with_prefix =
result.meta.data_type.first().cloned().unwrap_or_default();
if metric_name_with_prefix.is_empty() {
continue;
}
let metric_name = strip_frequency_prefix(&metric_name_with_prefix);
let data_points = match result.data.get(&metric_name_with_prefix) {
Some(serde_json::Value::Array(arr)) => arr,
_ => continue,
};
let mut date_values: HashMap<String, f64> = HashMap::new();
for point in data_points {
if point.is_null() {
continue;
}
let as_of_date = point
.get("asOfDate")
.and_then(|v| v.as_str())
.unwrap_or_default();
if as_of_date.is_empty() {
continue;
}
let value = extract_value(point.get("reportedValue"));
if let Some(v) = value {
date_values.insert(as_of_date.to_string(), v);
}
}
if !date_values.is_empty() {
statement.insert(metric_name, date_values);
}
}
if statement.is_empty() {
return Err(crate::error::FinanceError::SymbolNotFound {
symbol: Some(symbol.to_string()),
context: format!(
"No {} {} data found",
frequency.as_str(),
statement_type.as_str()
),
});
}
Ok(Self {
symbol: symbol.to_uppercase(),
statement_type: statement_type.as_str().to_string(),
frequency: frequency.as_str().to_string(),
statement,
})
}
}
fn strip_frequency_prefix(name: &str) -> String {
for prefix in &["annual", "quarterly", "trailing"] {
if let Some(stripped) = name.strip_prefix(prefix) {
return stripped.to_string();
}
}
name.to_string()
}
fn extract_value(reported_value: Option<&serde_json::Value>) -> Option<f64> {
let rv = reported_value?;
if let Some(raw) = rv.get("raw") {
if let Some(n) = raw.as_f64() {
return Some(n);
}
if let Some(parsed) = raw.get("parsedValue") {
return parsed
.as_f64()
.or_else(|| parsed.as_i64().map(|i| i as f64));
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_frequency_prefix() {
assert_eq!(strip_frequency_prefix("annualTotalRevenue"), "TotalRevenue");
assert_eq!(strip_frequency_prefix("quarterlyNetIncome"), "NetIncome");
assert_eq!(strip_frequency_prefix("trailingMarketCap"), "MarketCap");
assert_eq!(strip_frequency_prefix("SomeOther"), "SomeOther");
}
#[test]
fn test_extract_value_simple() {
let json: serde_json::Value = serde_json::json!({
"raw": 123.45,
"fmt": "123.45"
});
assert_eq!(extract_value(Some(&json)), Some(123.45));
}
#[test]
fn test_extract_value_nested() {
let json: serde_json::Value = serde_json::json!({
"raw": {
"source": "1.23E12",
"parsedValue": 1230000000000_i64
},
"fmt": "1.23T"
});
assert_eq!(extract_value(Some(&json)), Some(1230000000000.0));
}
#[test]
fn test_from_response() {
let json: serde_json::Value = serde_json::json!({
"timeseries": {
"result": [
{
"meta": {
"symbol": ["AAPL"],
"type": ["annualTotalRevenue"]
},
"annualTotalRevenue": [
{
"asOfDate": "2024-09-30",
"periodType": "12M",
"currencyCode": "USD",
"reportedValue": {
"raw": 391035000000.0,
"fmt": "391.04B"
}
},
{
"asOfDate": "2023-09-30",
"periodType": "12M",
"currencyCode": "USD",
"reportedValue": {
"raw": 383285000000.0,
"fmt": "383.29B"
}
}
]
},
{
"meta": {
"symbol": ["AAPL"],
"type": ["annualNetIncome"]
},
"annualNetIncome": [
{
"asOfDate": "2024-09-30",
"periodType": "12M",
"currencyCode": "USD",
"reportedValue": {
"raw": 100913000000.0,
"fmt": "100.91B"
}
}
]
}
],
"error": null
}
});
let result = FinancialStatement::from_response(
&json,
"AAPL",
StatementType::Income,
Frequency::Annual,
);
assert!(result.is_ok());
let statement = result.unwrap();
assert_eq!(statement.symbol, "AAPL");
assert_eq!(statement.statement_type, "income");
assert_eq!(statement.frequency, "annual");
assert!(statement.statement.contains_key("TotalRevenue"));
assert!(statement.statement.contains_key("NetIncome"));
let revenue = statement.statement.get("TotalRevenue").unwrap();
assert_eq!(revenue.get("2024-09-30"), Some(&391035000000.0));
assert_eq!(revenue.get("2023-09-30"), Some(&383285000000.0));
}
#[test]
fn test_from_response_empty() {
let json: serde_json::Value = serde_json::json!({
"timeseries": {
"result": [],
"error": null
}
});
let result = FinancialStatement::from_response(
&json,
"INVALID",
StatementType::Income,
Frequency::Annual,
);
assert!(result.is_err());
}
}