use serde::Deserialize;
use crate::adapters::fred::build_client;
use crate::error::Result;
use crate::models::economic::{
EconomicCategory, EconomicRelease, EconomicSeries, EconomicSeriesMatch, MacroObservation,
};
#[derive(Debug, Clone, Deserialize)]
pub struct SeriesSearchResponseDTO {
#[serde(default)]
pub seriess: Vec<EconomicSeriesMatch>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CategoriesResponseDTO {
#[serde(default)]
pub categories: Vec<EconomicCategory>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ReleasesResponseDTO {
#[serde(default)]
pub releases: Vec<EconomicRelease>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ObservationsResponseDTO {
#[serde(default)]
pub observations: Vec<ObservationDTO>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ObservationDTO {
pub date: String,
pub value: String,
}
pub(crate) fn to_observations(dto: ObservationsResponseDTO) -> Vec<MacroObservation> {
dto.observations
.into_iter()
.map(|o| MacroObservation {
date: o.date,
value: (o.value != ".").then(|| o.value.parse().ok()).flatten(),
})
.collect()
}
pub async fn search(query: &str, limit: u32) -> Result<Vec<EconomicSeriesMatch>> {
let limit = limit.clamp(1, 1000).to_string();
let resp: SeriesSearchResponseDTO = build_client()?
.get_json(
"series/search",
&[
("search_text", query),
("limit", &limit),
("order_by", "popularity"),
("sort_order", "desc"),
],
)
.await?;
Ok(resp.seriess)
}
pub async fn categories(parent_id: i64) -> Result<Vec<EconomicCategory>> {
let id = parent_id.to_string();
let resp: CategoriesResponseDTO = build_client()?
.get_json("category/children", &[("category_id", &id)])
.await?;
Ok(resp.categories)
}
pub async fn releases() -> Result<Vec<EconomicRelease>> {
let resp: ReleasesResponseDTO = build_client()?.get_json("releases", &[]).await?;
Ok(resp.releases)
}
pub async fn series_as_of(series_id: &str, date: &str) -> Result<EconomicSeries> {
let resp: ObservationsResponseDTO = build_client()?
.get_json(
"series/observations",
&[
("series_id", series_id),
("realtime_start", date),
("realtime_end", date),
],
)
.await?;
Ok(EconomicSeries {
series_id: series_id.to_string(),
title: None,
units: None,
frequency: None,
observations: to_observations(resp),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maps_a_search_hit_including_fmts_misspelled_envelope_key() {
let resp: SeriesSearchResponseDTO = serde_json::from_value(serde_json::json!({
"seriess": [{
"id": "GDPC1",
"title": "Real Gross Domestic Product",
"frequency": "Quarterly",
"units": "Billions of Chained 2017 Dollars",
"seasonal_adjustment": "Seasonally Adjusted Annual Rate",
"observation_start": "1947-01-01",
"observation_end": "2024-07-01",
"popularity": 93,
"notes": "BEA Account Code: A191RX"
}]
}))
.unwrap();
let out = resp.seriess.into_iter().next().unwrap();
assert_eq!(out.id, "GDPC1");
assert_eq!(out.title.as_deref(), Some("Real Gross Domestic Product"));
assert_eq!(out.frequency.as_deref(), Some("Quarterly"));
assert_eq!(out.observation_start.as_deref(), Some("1947-01-01"));
assert_eq!(out.popularity, Some(93));
}
#[test]
fn missing_envelope_arrays_yield_empty_results() {
let search: SeriesSearchResponseDTO =
serde_json::from_value(serde_json::json!({})).unwrap();
assert!(search.seriess.is_empty());
let cats: CategoriesResponseDTO = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(cats.categories.is_empty());
let rels: ReleasesResponseDTO = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(rels.releases.is_empty());
}
#[test]
fn maps_categories_and_releases() {
let cats: CategoriesResponseDTO = serde_json::from_value(serde_json::json!({
"categories": [{ "id": 32992, "name": "Money, Banking, & Finance", "parent_id": 0 }]
}))
.unwrap();
let cat = cats.categories.into_iter().next().unwrap();
assert_eq!(cat.id, 32992);
assert_eq!(cat.parent_id, Some(0));
let rels: ReleasesResponseDTO = serde_json::from_value(serde_json::json!({
"releases": [{
"id": 50,
"name": "Employment Situation",
"press_release": true,
"link": "https://www.bls.gov/news.release/empsit.htm"
}]
}))
.unwrap();
let rel = rels.releases.into_iter().next().unwrap();
assert_eq!(rel.name.as_deref(), Some("Employment Situation"));
assert_eq!(rel.press_release, Some(true));
}
#[test]
fn missing_observations_keep_their_date() {
let resp: ObservationsResponseDTO = serde_json::from_value(serde_json::json!({
"observations": [
{ "date": "2020-01-01", "value": "21538.032" },
{ "date": "2020-04-01", "value": "." },
{ "date": "2020-07-01", "value": "not-a-number" }
]
}))
.unwrap();
let obs = to_observations(resp);
assert_eq!(obs.len(), 3);
assert_eq!(obs[0].value, Some(21538.032));
assert_eq!(obs[1].date, "2020-04-01");
assert_eq!(obs[1].value, None);
assert_eq!(obs[2].value, None);
}
}