1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3
4use crate::{Frequency, SeasonalAdjustment, SeriesId};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
17pub struct Series {
18 pub id: SeriesId,
20
21 pub title: String,
23
24 pub observation_start: NaiveDate,
26
27 pub observation_end: NaiveDate,
29
30 pub frequency: Frequency,
32
33 pub seasonal_adjustment: SeasonalAdjustment,
35
36 pub units: String,
40
41 pub popularity: u32,
43
44 #[serde(default)]
46 pub notes: Option<String>,
47
48 pub last_updated: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
57pub struct SeriesSearchResults {
58 #[serde(rename(deserialize = "seriess", serialize = "series"))]
61 pub series: Vec<Series>,
62
63 pub count: u32,
65
66 pub offset: u32,
68
69 pub limit: u32,
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 const GNPCA_JSON: &str = r#"{
81 "id": "GNPCA",
82 "realtime_start": "2024-01-01",
83 "realtime_end": "2024-01-01",
84 "title": "Real Gross National Product",
85 "observation_start": "1929-01-01",
86 "observation_end": "2023-01-01",
87 "frequency": "Annual",
88 "frequency_short": "A",
89 "units": "Billions of Chained 2017 Dollars",
90 "units_short": "Bil. of Chn. 2017 $",
91 "seasonal_adjustment": "Not Seasonally Adjusted",
92 "seasonal_adjustment_short": "NSA",
93 "last_updated": "2024-03-28 07:56:03-05",
94 "popularity": 76,
95 "notes": "BEA Account Code: A001RX"
96 }"#;
97
98 #[test]
99 fn deserializes_series_metadata() {
100 let series: Series = serde_json::from_str(GNPCA_JSON).unwrap();
101
102 assert_eq!(series.id, SeriesId::new("GNPCA"));
103 assert_eq!(series.title, "Real Gross National Product");
104 assert_eq!(
105 series.observation_start,
106 NaiveDate::from_ymd_opt(1929, 1, 1).unwrap()
107 );
108 assert_eq!(series.frequency, Frequency::Annual);
109 assert_eq!(
110 series.seasonal_adjustment,
111 SeasonalAdjustment::NotSeasonallyAdjusted
112 );
113 assert_eq!(series.units, "Billions of Chained 2017 Dollars");
114 assert_eq!(series.popularity, 76);
115 assert_eq!(series.notes.as_deref(), Some("BEA Account Code: A001RX"));
116 }
117
118 #[test]
119 fn notes_default_to_none_when_absent() {
120 let json = r#"{
121 "id": "X",
122 "title": "T",
123 "observation_start": "2000-01-01",
124 "observation_end": "2001-01-01",
125 "frequency": "Monthly",
126 "seasonal_adjustment": "Seasonally Adjusted",
127 "units": "Percent",
128 "popularity": 0,
129 "last_updated": "2024-01-01 00:00:00-05"
130 }"#;
131 let series: Series = serde_json::from_str(json).unwrap();
132 assert_eq!(series.notes, None);
133 }
134
135 #[test]
136 fn deserializes_search_results_with_pagination() {
137 let json = format!(
138 r#"{{
139 "order_by": "search_rank",
140 "sort_order": "desc",
141 "count": 1,
142 "offset": 0,
143 "limit": 1000,
144 "seriess": [{GNPCA_JSON}]
145 }}"#
146 );
147 let results: SeriesSearchResults = serde_json::from_str(&json).unwrap();
148 assert_eq!(results.count, 1);
149 assert_eq!(results.offset, 0);
150 assert_eq!(results.limit, 1000);
151 assert_eq!(results.series.len(), 1);
152 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
153 }
154
155 #[test]
156 fn serializes_with_typed_enum_labels_and_clean_keys() {
157 let series: Series = serde_json::from_str(GNPCA_JSON).unwrap();
158 let value = serde_json::to_value(&series).unwrap();
159 assert_eq!(value["id"], "GNPCA");
160 assert_eq!(value["frequency"], "Annual");
161 assert_eq!(value["seasonal_adjustment"], "Not Seasonally Adjusted");
162
163 let results = SeriesSearchResults {
164 series: vec![series],
165 count: 1,
166 offset: 0,
167 limit: 1,
168 };
169 let results_value = serde_json::to_value(&results).unwrap();
170 assert!(results_value.get("series").is_some());
172 assert!(results_value.get("seriess").is_none());
173 }
174}