1use std::time::Duration;
2
3use chrono::NaiveDate;
4use serde::de::DeserializeOwned;
5use serde::Deserialize;
6
7use crate::{
8 Category, CategoryId, Error, Frequency, Observation, ObservationsRequest, RegionType,
9 RegionalData, Release, ReleaseDatesRequest, ReleaseDatesResults, ReleaseId, ReleaseTable,
10 ReleaseTablesRequest, ReleasesRequest, ReleasesResults, Result, SeasonalAdjustment, Series,
11 SeriesDataRequest, SeriesGroup, SeriesGroupId, SeriesId, SeriesListRequest,
12 SeriesSearchRequest, SeriesSearchResults, SeriesUpdatesRequest, ShapeFile, ShapeType, Source,
13 SourceId, SourcesRequest, SourcesResults, TagsRequest, TagsResults, VintageDates,
14 VintageDatesRequest,
15};
16
17const FRED_BASE_URL: &str = "https://api.stlouisfed.org/fred";
19
20const GEOFRED_BASE_URL: &str = "https://api.stlouisfed.org/geofred";
23
24#[derive(Debug, Clone)]
29pub struct Client {
30 http: reqwest::Client,
31 api_key: String,
32 base_url: String,
33 geofred_base_url: String,
34}
35
36impl Client {
37 pub fn new(api_key: impl Into<String>) -> Result<Self> {
43 let http = reqwest::Client::builder().build()?;
44 Ok(Self {
45 http,
46 api_key: api_key.into(),
47 base_url: FRED_BASE_URL.to_owned(),
48 geofred_base_url: GEOFRED_BASE_URL.to_owned(),
49 })
50 }
51
52 #[cfg(test)]
55 pub(crate) fn with_base_url(
56 api_key: impl Into<String>,
57 base_url: impl Into<String>,
58 ) -> Result<Self> {
59 let base_url = base_url.into();
62 Ok(Self {
63 http: reqwest::Client::builder().build()?,
64 api_key: api_key.into(),
65 geofred_base_url: base_url.clone(),
66 base_url,
67 })
68 }
69
70 pub fn from_env() -> Result<Self> {
78 let api_key = std::env::var("FRED_API_KEY").map_err(|_| {
79 Error::InvalidInput("FRED_API_KEY environment variable is not set".to_owned())
80 })?;
81 Self::new(api_key)
82 }
83
84 pub fn observations(&self, series_id: &SeriesId) -> ObservationsRequest<'_> {
104 ObservationsRequest::new(self, series_id.clone())
105 }
106
107 pub(crate) async fn execute_observations(
109 &self,
110 request: &ObservationsRequest<'_>,
111 ) -> Result<Vec<Observation>> {
112 let response: ObservationsResponse = self
113 .get("/series/observations", &request.query_params())
114 .await?;
115 Ok(response.observations)
116 }
117
118 pub async fn series(&self, series_id: &SeriesId) -> Result<Series> {
125 let response: SeriesResponse = self
126 .get("/series", &[("series_id", series_id.as_str().to_owned())])
127 .await?;
128 response
129 .seriess
130 .into_iter()
131 .next()
132 .ok_or_else(|| Error::Api {
133 status: 200,
134 code: None,
135 message: format!("FRED returned no series for id `{series_id}`"),
136 })
137 }
138
139 pub fn search(&self, search_text: impl Into<String>) -> SeriesSearchRequest<'_> {
158 SeriesSearchRequest::new(self, search_text.into())
159 }
160
161 pub(crate) async fn execute_search(
163 &self,
164 request: &SeriesSearchRequest<'_>,
165 ) -> Result<SeriesSearchResults> {
166 self.get("/series/search", &request.query_params()).await
167 }
168
169 pub fn series_search_tags(&self, search_text: impl Into<String>) -> TagsRequest<'_> {
185 TagsRequest::scoped(
186 self,
187 "/series/search/tags",
188 ("series_search_text", search_text.into()),
189 None,
190 "tag_search_text",
191 )
192 }
193
194 pub fn series_search_related_tags<I, S>(
214 &self,
215 search_text: impl Into<String>,
216 tag_names: I,
217 ) -> TagsRequest<'_>
218 where
219 I: IntoIterator<Item = S>,
220 S: AsRef<str>,
221 {
222 TagsRequest::scoped(
223 self,
224 "/series/search/related_tags",
225 ("series_search_text", search_text.into()),
226 Some(join_tag_names(tag_names)),
227 "tag_search_text",
228 )
229 }
230
231 pub async fn category(&self, category_id: CategoryId) -> Result<Category> {
239 let response: CategoriesResponse = self
240 .get(
241 "/category",
242 &[("category_id", category_id.get().to_string())],
243 )
244 .await?;
245 response
246 .categories
247 .into_iter()
248 .next()
249 .ok_or_else(|| Error::Api {
250 status: 200,
251 code: None,
252 message: format!("FRED returned no category for id `{category_id}`"),
253 })
254 }
255
256 pub async fn category_children(&self, category_id: CategoryId) -> Result<Vec<Category>> {
264 let response: CategoriesResponse = self
265 .get(
266 "/category/children",
267 &[("category_id", category_id.get().to_string())],
268 )
269 .await?;
270 Ok(response.categories)
271 }
272
273 pub async fn category_related(&self, category_id: CategoryId) -> Result<Vec<Category>> {
283 let response: CategoriesResponse = self
284 .get(
285 "/category/related",
286 &[("category_id", category_id.get().to_string())],
287 )
288 .await?;
289 Ok(response.categories)
290 }
291
292 pub fn category_series(&self, category_id: CategoryId) -> SeriesListRequest<'_> {
311 SeriesListRequest::new(
312 self,
313 "/category/series",
314 "category_id",
315 category_id.get().to_string(),
316 )
317 }
318
319 pub fn category_tags(&self, category_id: CategoryId) -> TagsRequest<'_> {
335 TagsRequest::scoped(
336 self,
337 "/category/tags",
338 ("category_id", category_id.get().to_string()),
339 None,
340 "search_text",
341 )
342 }
343
344 pub fn category_related_tags<I, S>(
361 &self,
362 category_id: CategoryId,
363 tag_names: I,
364 ) -> TagsRequest<'_>
365 where
366 I: IntoIterator<Item = S>,
367 S: AsRef<str>,
368 {
369 TagsRequest::scoped(
370 self,
371 "/category/related_tags",
372 ("category_id", category_id.get().to_string()),
373 Some(join_tag_names(tag_names)),
374 "search_text",
375 )
376 }
377
378 pub fn releases(&self) -> ReleasesRequest<'_> {
392 ReleasesRequest::new(self, "/releases")
393 }
394
395 pub(crate) async fn execute_releases(
398 &self,
399 request: &ReleasesRequest<'_>,
400 ) -> Result<ReleasesResults> {
401 self.get(request.path(), &request.query_params()).await
402 }
403
404 pub fn releases_dates(&self) -> ReleaseDatesRequest<'_> {
420 ReleaseDatesRequest::new(self, "/releases/dates")
421 }
422
423 pub(crate) async fn execute_release_dates(
426 &self,
427 request: &ReleaseDatesRequest<'_>,
428 ) -> Result<ReleaseDatesResults> {
429 self.get(request.path(), &request.query_params()).await
430 }
431
432 pub async fn release(&self, release_id: ReleaseId) -> Result<Release> {
439 let response: ReleaseResponse = self
440 .get("/release", &[("release_id", release_id.get().to_string())])
441 .await?;
442 response
443 .releases
444 .into_iter()
445 .next()
446 .ok_or_else(|| Error::Api {
447 status: 200,
448 code: None,
449 message: format!("FRED returned no release for id `{release_id}`"),
450 })
451 }
452
453 pub fn release_series(&self, release_id: ReleaseId) -> SeriesListRequest<'_> {
472 SeriesListRequest::new(
473 self,
474 "/release/series",
475 "release_id",
476 release_id.get().to_string(),
477 )
478 }
479
480 pub async fn release_sources(&self, release_id: ReleaseId) -> Result<Vec<Source>> {
489 let response: SourceResponse = self
490 .get(
491 "/release/sources",
492 &[("release_id", release_id.get().to_string())],
493 )
494 .await?;
495 Ok(response.sources)
496 }
497
498 pub fn release_dates(&self, release_id: ReleaseId) -> ReleaseDatesRequest<'_> {
515 ReleaseDatesRequest::with_release(self, "/release/dates", release_id.get().to_string())
516 }
517
518 pub fn release_tables(&self, release_id: ReleaseId) -> ReleaseTablesRequest<'_> {
535 ReleaseTablesRequest::new(self, release_id.get())
536 }
537
538 pub(crate) async fn execute_release_tables(
540 &self,
541 request: &ReleaseTablesRequest<'_>,
542 ) -> Result<ReleaseTable> {
543 self.get("/release/tables", &request.query_params()).await
544 }
545
546 pub fn release_tags(&self, release_id: ReleaseId) -> TagsRequest<'_> {
562 TagsRequest::scoped(
563 self,
564 "/release/tags",
565 ("release_id", release_id.get().to_string()),
566 None,
567 "search_text",
568 )
569 }
570
571 pub fn release_related_tags<I, S>(&self, release_id: ReleaseId, tag_names: I) -> TagsRequest<'_>
587 where
588 I: IntoIterator<Item = S>,
589 S: AsRef<str>,
590 {
591 TagsRequest::scoped(
592 self,
593 "/release/related_tags",
594 ("release_id", release_id.get().to_string()),
595 Some(join_tag_names(tag_names)),
596 "search_text",
597 )
598 }
599
600 pub fn tags(&self) -> TagsRequest<'_> {
614 TagsRequest::new(self, "/tags")
615 }
616
617 pub fn related_tags<I, S>(&self, tag_names: I) -> TagsRequest<'_>
633 where
634 I: IntoIterator<Item = S>,
635 S: AsRef<str>,
636 {
637 TagsRequest::with_tag_names(self, "/related_tags", join_tag_names(tag_names))
638 }
639
640 pub(crate) async fn execute_tags(&self, request: &TagsRequest<'_>) -> Result<TagsResults> {
643 self.get(request.path(), &request.query_params()).await
644 }
645
646 pub fn tags_series<I, S>(&self, tag_names: I) -> SeriesListRequest<'_>
661 where
662 I: IntoIterator<Item = S>,
663 S: AsRef<str>,
664 {
665 SeriesListRequest::new(self, "/tags/series", "tag_names", join_tag_names(tag_names))
666 }
667
668 pub(crate) async fn execute_series_list(
671 &self,
672 request: &SeriesListRequest<'_>,
673 ) -> Result<SeriesSearchResults> {
674 self.get(request.path(), &request.query_params()).await
675 }
676
677 pub async fn series_tags(&self, series_id: &SeriesId) -> Result<TagsResults> {
685 self.get(
686 "/series/tags",
687 &[("series_id", series_id.as_str().to_owned())],
688 )
689 .await
690 }
691
692 pub fn series_updates(&self) -> SeriesUpdatesRequest<'_> {
707 SeriesUpdatesRequest::new(self)
708 }
709
710 pub(crate) async fn execute_series_updates(
712 &self,
713 request: &SeriesUpdatesRequest<'_>,
714 ) -> Result<SeriesSearchResults> {
715 self.get("/series/updates", &request.query_params()).await
716 }
717
718 pub fn series_vintagedates(&self, series_id: &SeriesId) -> VintageDatesRequest<'_> {
738 VintageDatesRequest::new(self, series_id.clone())
739 }
740
741 pub(crate) async fn execute_vintage_dates(
744 &self,
745 request: &VintageDatesRequest<'_>,
746 ) -> Result<VintageDates> {
747 self.get("/series/vintagedates", &request.query_params())
748 .await
749 }
750
751 pub async fn series_categories(&self, series_id: &SeriesId) -> Result<Vec<Category>> {
759 let response: CategoriesResponse = self
760 .get(
761 "/series/categories",
762 &[("series_id", series_id.as_str().to_owned())],
763 )
764 .await?;
765 Ok(response.categories)
766 }
767
768 pub async fn series_release(&self, series_id: &SeriesId) -> Result<Release> {
776 let response: ReleaseResponse = self
777 .get(
778 "/series/release",
779 &[("series_id", series_id.as_str().to_owned())],
780 )
781 .await?;
782 response
783 .releases
784 .into_iter()
785 .next()
786 .ok_or_else(|| Error::Api {
787 status: 200,
788 code: None,
789 message: format!("FRED returned no release for series `{series_id}`"),
790 })
791 }
792
793 pub fn sources(&self) -> SourcesRequest<'_> {
807 SourcesRequest::new(self)
808 }
809
810 pub(crate) async fn execute_sources(
812 &self,
813 request: &SourcesRequest<'_>,
814 ) -> Result<SourcesResults> {
815 self.get("/sources", &request.query_params()).await
816 }
817
818 pub async fn source(&self, source_id: SourceId) -> Result<Source> {
825 let response: SourceResponse = self
826 .get("/source", &[("source_id", source_id.get().to_string())])
827 .await?;
828 response
829 .sources
830 .into_iter()
831 .next()
832 .ok_or_else(|| Error::Api {
833 status: 200,
834 code: None,
835 message: format!("FRED returned no source for id `{source_id}`"),
836 })
837 }
838
839 pub fn source_releases(&self, source_id: SourceId) -> ReleasesRequest<'_> {
854 ReleasesRequest::with_source(self, "/source/releases", source_id.get().to_string())
855 }
856
857 pub async fn regional_data(
893 &self,
894 series_group: &SeriesGroupId,
895 region_type: RegionType,
896 date: NaiveDate,
897 units: impl Into<String>,
898 frequency: Frequency,
899 season: SeasonalAdjustment,
900 ) -> Result<RegionalData> {
901 self.get_geofred(
902 "/regional/data",
903 &[
904 ("series_group", series_group.as_str().to_owned()),
905 ("region_type", region_type.query_code().to_owned()),
906 ("date", date.to_string()),
907 ("units", units.into()),
908 ("frequency", frequency.query_code().to_owned()),
909 ("season", season.query_code().to_owned()),
910 ],
911 )
912 .await
913 }
914
915 pub fn series_data(&self, series_id: &SeriesId) -> SeriesDataRequest<'_> {
935 SeriesDataRequest::new(self, series_id.clone())
936 }
937
938 pub(crate) async fn execute_series_data(
941 &self,
942 request: &SeriesDataRequest<'_>,
943 ) -> Result<RegionalData> {
944 self.get_geofred("/series/data", &request.query_params())
945 .await
946 }
947
948 pub async fn series_group(&self, series_id: &SeriesId) -> Result<SeriesGroup> {
957 let response: SeriesGroupResponse = self
958 .get_geofred(
959 "/series/group",
960 &[("series_id", series_id.as_str().to_owned())],
961 )
962 .await?;
963 Ok(response.series_group)
964 }
965
966 pub async fn shape_file(&self, shape: ShapeType) -> Result<ShapeFile> {
975 self.get_geofred("/shapes/file", &[("shape", shape.query_code().to_owned())])
976 .await
977 }
978
979 async fn get<T: DeserializeOwned>(
981 &self,
982 path: &str,
983 params: &[(&'static str, String)],
984 ) -> Result<T> {
985 self.get_from(&self.base_url, path, params).await
986 }
987
988 async fn get_geofred<T: DeserializeOwned>(
991 &self,
992 path: &str,
993 params: &[(&'static str, String)],
994 ) -> Result<T> {
995 self.get_from(&self.geofred_base_url, path, params).await
996 }
997
998 async fn get_from<T: DeserializeOwned>(
1003 &self,
1004 base_url: &str,
1005 path: &str,
1006 params: &[(&'static str, String)],
1007 ) -> Result<T> {
1008 let mut query: Vec<(&str, String)> = Vec::with_capacity(params.len() + 2);
1009 query.push(("api_key", self.api_key.clone()));
1010 query.push(("file_type", "json".to_owned()));
1011 query.extend(params.iter().cloned());
1012
1013 let response = self
1014 .http
1015 .get(format!("{base_url}{path}"))
1016 .query(&query)
1017 .send()
1018 .await?;
1019
1020 let status = response.status();
1021 let retry_after = parse_retry_after(response.headers());
1023 let body = response.bytes().await?;
1024
1025 if !status.is_success() {
1026 return Err(api_error(status, retry_after, &body));
1027 }
1028
1029 serde_json::from_slice(&body).map_err(Error::from)
1030 }
1031}
1032
1033fn join_tag_names<I, S>(tag_names: I) -> String
1036where
1037 I: IntoIterator<Item = S>,
1038 S: AsRef<str>,
1039{
1040 tag_names
1041 .into_iter()
1042 .map(|name| name.as_ref().to_owned())
1043 .collect::<Vec<_>>()
1044 .join(";")
1045}
1046
1047fn api_error(status: reqwest::StatusCode, retry_after: Option<Duration>, body: &[u8]) -> Error {
1051 let fred: Option<FredErrorBody> = serde_json::from_slice(body).ok();
1052 let code = fred.as_ref().and_then(|e| e.error_code);
1053 let message = fred.and_then(|e| e.error_message).unwrap_or_else(|| {
1054 status
1055 .canonical_reason()
1056 .unwrap_or("unknown error")
1057 .to_owned()
1058 });
1059
1060 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
1061 return Error::RateLimited { retry_after };
1062 }
1063
1064 Error::Api {
1065 status: status.as_u16(),
1066 code,
1067 message,
1068 }
1069}
1070
1071fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
1075 let seconds: u64 = headers
1076 .get(reqwest::header::RETRY_AFTER)?
1077 .to_str()
1078 .ok()?
1079 .trim()
1080 .parse()
1081 .ok()?;
1082 Some(Duration::from_secs(seconds))
1083}
1084
1085#[derive(Deserialize)]
1088struct ObservationsResponse {
1089 observations: Vec<Observation>,
1090}
1091
1092#[derive(Deserialize)]
1095struct SeriesResponse {
1096 seriess: Vec<Series>,
1097}
1098
1099#[derive(Deserialize)]
1101struct CategoriesResponse {
1102 categories: Vec<Category>,
1103}
1104
1105#[derive(Deserialize)]
1109struct ReleaseResponse {
1110 releases: Vec<Release>,
1111}
1112
1113#[derive(Deserialize)]
1117struct SourceResponse {
1118 sources: Vec<Source>,
1119}
1120
1121#[derive(Deserialize)]
1123struct SeriesGroupResponse {
1124 series_group: SeriesGroup,
1125}
1126
1127#[derive(Deserialize)]
1129struct FredErrorBody {
1130 error_code: Option<u32>,
1131 error_message: Option<String>,
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136 use super::Client;
1137 use std::time::Duration;
1138
1139 use crate::{
1140 CategoryId, Error, Frequency, OrderBy, Paginate, RegionType, ReleaseElementId, ReleaseId,
1141 SeasonalAdjustment, SeriesGroupId, SeriesId, ShapeType, SortOrder, SourceId, Units,
1142 UpdatesFilter,
1143 };
1144 use wiremock::matchers::{method, path, query_param};
1145 use wiremock::{Mock, MockServer, ResponseTemplate};
1146
1147 const SERIES_OBJECT: &str = r#"{
1149 "id": "GNPCA",
1150 "title": "Real Gross National Product",
1151 "observation_start": "1929-01-01",
1152 "observation_end": "2023-01-01",
1153 "frequency": "Annual",
1154 "units": "Billions of Chained 2017 Dollars",
1155 "seasonal_adjustment": "Not Seasonally Adjusted",
1156 "last_updated": "2024-03-28 07:56:03-05",
1157 "popularity": 76,
1158 "notes": "BEA Account Code: A001RX"
1159 }"#;
1160
1161 fn client_for(server: &MockServer) -> Client {
1162 Client::with_base_url("test-key", server.uri()).expect("client builds")
1163 }
1164
1165 #[tokio::test]
1166 async fn series_parses_metadata() {
1167 let server = MockServer::start().await;
1168 Mock::given(method("GET"))
1169 .and(path("/series"))
1170 .respond_with(
1171 ResponseTemplate::new(200)
1172 .set_body_string(format!("{{\"seriess\":[{SERIES_OBJECT}]}}")),
1173 )
1174 .mount(&server)
1175 .await;
1176
1177 let series = client_for(&server)
1178 .series(&SeriesId::new("GNPCA"))
1179 .await
1180 .expect("series parses");
1181 assert_eq!(series.id, SeriesId::new("GNPCA"));
1182 assert_eq!(series.frequency, Frequency::Annual);
1183 assert_eq!(
1184 series.seasonal_adjustment,
1185 SeasonalAdjustment::NotSeasonallyAdjusted
1186 );
1187 assert_eq!(series.popularity, 76);
1188 }
1189
1190 #[tokio::test]
1191 async fn observations_parse_missing_and_present_values() {
1192 let server = MockServer::start().await;
1193 let body = r#"{"observations":[
1194 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1930-01-01","value":"."},
1195 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"1065.9"}
1196 ]}"#;
1197 Mock::given(method("GET"))
1198 .and(path("/series/observations"))
1199 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1200 .mount(&server)
1201 .await;
1202
1203 let observations = client_for(&server)
1204 .observations(&SeriesId::new("GNPCA"))
1205 .send()
1206 .await
1207 .expect("observations parse");
1208 assert_eq!(observations.len(), 2);
1209 assert_eq!(observations[0].value, None); assert_eq!(observations[1].value, Some(1065.9));
1211 }
1212
1213 #[tokio::test]
1214 async fn observations_point_in_time_sends_realtime_and_parses_period() {
1215 let server = MockServer::start().await;
1216 Mock::given(method("GET"))
1219 .and(path("/series/observations"))
1220 .and(query_param("realtime_start", "2020-01-01"))
1221 .and(query_param("realtime_end", "2020-01-01"))
1222 .respond_with(ResponseTemplate::new(200).set_body_string(
1223 r#"{"observations":[
1224 {"realtime_start":"2020-01-01","realtime_end":"2020-01-01","date":"2017-01-01","value":"18344.563"}
1225 ]}"#,
1226 ))
1227 .mount(&server)
1228 .await;
1229
1230 let as_of = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1231 let observations = client_for(&server)
1232 .observations(&SeriesId::new("GNPCA"))
1233 .realtime(as_of, as_of)
1234 .send()
1235 .await
1236 .expect("point-in-time observations parse");
1237 assert_eq!(observations[0].realtime_start, as_of);
1238 assert_eq!(observations[0].realtime_end, as_of);
1239 assert_eq!(observations[0].value, Some(18344.563));
1240 }
1241
1242 #[tokio::test]
1243 async fn search_parses_results_with_pagination() {
1244 let server = MockServer::start().await;
1245 let body =
1246 format!("{{\"count\":1,\"offset\":0,\"limit\":1000,\"seriess\":[{SERIES_OBJECT}]}}");
1247 Mock::given(method("GET"))
1248 .and(path("/series/search"))
1249 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1250 .mount(&server)
1251 .await;
1252
1253 let results = client_for(&server)
1254 .search("real gnp")
1255 .send()
1256 .await
1257 .expect("search parses");
1258 assert_eq!(results.count, 1);
1259 assert_eq!(results.series.len(), 1);
1260 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1261 }
1262
1263 #[tokio::test]
1264 async fn error_status_with_body_maps_to_api_error() {
1265 let server = MockServer::start().await;
1266 let body = r#"{"error_code":400,"error_message":"Bad Request. Invalid value for variable series_id."}"#;
1267 Mock::given(method("GET"))
1268 .and(path("/series"))
1269 .respond_with(ResponseTemplate::new(400).set_body_string(body))
1270 .mount(&server)
1271 .await;
1272
1273 let error = client_for(&server)
1274 .series(&SeriesId::new("BAD"))
1275 .await
1276 .expect_err("a 400 should be an API error");
1277 match error {
1278 Error::Api {
1279 status,
1280 code,
1281 message,
1282 } => {
1283 assert_eq!(status, 400);
1284 assert_eq!(code, Some(400));
1285 assert!(message.contains("Invalid value"), "message was {message:?}");
1286 }
1287 other => panic!("expected Error::Api, got {other:?}"),
1288 }
1289 }
1290
1291 #[tokio::test]
1292 async fn too_many_requests_maps_to_rate_limited() {
1293 let server = MockServer::start().await;
1294 Mock::given(method("GET"))
1295 .and(path("/series"))
1296 .respond_with(ResponseTemplate::new(429))
1297 .mount(&server)
1298 .await;
1299
1300 let error = client_for(&server)
1301 .series(&SeriesId::new("GNPCA"))
1302 .await
1303 .expect_err("a 429 should be rate-limited");
1304 assert!(matches!(error, Error::RateLimited { .. }), "got {error:?}");
1305 }
1306
1307 #[tokio::test]
1308 async fn rate_limited_carries_retry_after_seconds() {
1309 let server = MockServer::start().await;
1310 Mock::given(method("GET"))
1311 .and(path("/series"))
1312 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "120"))
1313 .mount(&server)
1314 .await;
1315
1316 let error = client_for(&server)
1317 .series(&SeriesId::new("GNPCA"))
1318 .await
1319 .expect_err("a 429 should be rate-limited");
1320 match error {
1321 Error::RateLimited { retry_after } => {
1322 assert_eq!(retry_after, Some(Duration::from_secs(120)));
1323 }
1324 other => panic!("expected Error::RateLimited, got {other:?}"),
1325 }
1326 }
1327
1328 #[tokio::test]
1329 async fn send_all_walks_every_page() {
1330 let server = MockServer::start().await;
1331 Mock::given(method("GET"))
1335 .and(path("/sources"))
1336 .and(query_param("offset", "0"))
1337 .respond_with(ResponseTemplate::new(200).set_body_string(
1338 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1339 {"id":1,"name":"Source One"},
1340 {"id":2,"name":"Source Two"}
1341 ]}"#,
1342 ))
1343 .mount(&server)
1344 .await;
1345 Mock::given(method("GET"))
1346 .and(path("/sources"))
1347 .and(query_param("offset", "2"))
1348 .respond_with(ResponseTemplate::new(200).set_body_string(
1349 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1350 {"id":3,"name":"Source Three"}
1351 ]}"#,
1352 ))
1353 .mount(&server)
1354 .await;
1355
1356 let sources = client_for(&server)
1357 .sources()
1358 .send_all()
1359 .await
1360 .expect("send_all walks both pages");
1361 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1362 assert_eq!(
1363 ids,
1364 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1365 );
1366 }
1367
1368 #[tokio::test]
1369 async fn send_all_treats_limit_as_a_ceiling() {
1370 let server = MockServer::start().await;
1371 Mock::given(method("GET"))
1375 .and(path("/sources"))
1376 .and(query_param("offset", "0"))
1377 .and(query_param("limit", "2"))
1378 .respond_with(ResponseTemplate::new(200).set_body_string(
1379 r#"{"count":5,"offset":0,"limit":2,"sources":[
1380 {"id":1,"name":"Source One"},
1381 {"id":2,"name":"Source Two"}
1382 ]}"#,
1383 ))
1384 .mount(&server)
1385 .await;
1386
1387 let sources = client_for(&server)
1388 .sources()
1389 .limit(2)
1390 .send_all()
1391 .await
1392 .expect("send_all stops at the ceiling");
1393 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1394 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1395 }
1396
1397 #[tokio::test]
1398 async fn send_all_retries_after_a_429() {
1399 let server = MockServer::start().await;
1400 Mock::given(method("GET"))
1404 .and(path("/sources"))
1405 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
1406 .up_to_n_times(1)
1407 .with_priority(1)
1408 .mount(&server)
1409 .await;
1410 Mock::given(method("GET"))
1411 .and(path("/sources"))
1412 .respond_with(ResponseTemplate::new(200).set_body_string(
1413 r#"{"count":1,"offset":0,"limit":1000,"sources":[
1414 {"id":1,"name":"Source One"}
1415 ]}"#,
1416 ))
1417 .with_priority(2)
1418 .mount(&server)
1419 .await;
1420
1421 let sources = client_for(&server)
1422 .sources()
1423 .send_all()
1424 .await
1425 .expect("send_all retries the 429 and then succeeds");
1426 assert_eq!(sources.len(), 1);
1427 assert_eq!(sources[0].id, SourceId::new(1));
1428 }
1429
1430 #[tokio::test]
1431 async fn stream_walks_every_page() {
1432 use futures_util::TryStreamExt;
1433
1434 let server = MockServer::start().await;
1435 Mock::given(method("GET"))
1436 .and(path("/sources"))
1437 .and(query_param("offset", "0"))
1438 .respond_with(ResponseTemplate::new(200).set_body_string(
1439 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1440 {"id":1,"name":"Source One"},
1441 {"id":2,"name":"Source Two"}
1442 ]}"#,
1443 ))
1444 .mount(&server)
1445 .await;
1446 Mock::given(method("GET"))
1447 .and(path("/sources"))
1448 .and(query_param("offset", "2"))
1449 .respond_with(ResponseTemplate::new(200).set_body_string(
1450 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1451 {"id":3,"name":"Source Three"}
1452 ]}"#,
1453 ))
1454 .mount(&server)
1455 .await;
1456
1457 let sources: Vec<_> = client_for(&server)
1458 .sources()
1459 .stream()
1460 .try_collect()
1461 .await
1462 .expect("stream walks both pages");
1463 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1464 assert_eq!(
1465 ids,
1466 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1467 );
1468 }
1469
1470 #[tokio::test]
1471 async fn stream_treats_limit_as_a_ceiling() {
1472 use futures_util::TryStreamExt;
1473
1474 let server = MockServer::start().await;
1475 Mock::given(method("GET"))
1478 .and(path("/sources"))
1479 .and(query_param("offset", "0"))
1480 .and(query_param("limit", "2"))
1481 .respond_with(ResponseTemplate::new(200).set_body_string(
1482 r#"{"count":5,"offset":0,"limit":2,"sources":[
1483 {"id":1,"name":"Source One"},
1484 {"id":2,"name":"Source Two"}
1485 ]}"#,
1486 ))
1487 .mount(&server)
1488 .await;
1489
1490 let sources: Vec<_> = client_for(&server)
1491 .sources()
1492 .limit(2)
1493 .stream()
1494 .try_collect()
1495 .await
1496 .expect("stream stops at the ceiling");
1497 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1498 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1499 }
1500
1501 #[tokio::test]
1502 async fn stream_surfaces_a_mid_stream_error() {
1503 use futures_util::StreamExt;
1504
1505 let server = MockServer::start().await;
1506 Mock::given(method("GET"))
1509 .and(path("/sources"))
1510 .and(query_param("offset", "0"))
1511 .respond_with(ResponseTemplate::new(200).set_body_string(
1512 r#"{"count":4,"offset":0,"limit":1000,"sources":[
1513 {"id":1,"name":"Source One"},
1514 {"id":2,"name":"Source Two"}
1515 ]}"#,
1516 ))
1517 .mount(&server)
1518 .await;
1519 Mock::given(method("GET"))
1520 .and(path("/sources"))
1521 .and(query_param("offset", "2"))
1522 .respond_with(ResponseTemplate::new(500))
1523 .mount(&server)
1524 .await;
1525
1526 let results: Vec<_> = client_for(&server).sources().stream().collect().await;
1527 assert_eq!(results.len(), 3);
1528 assert_eq!(
1529 results[0].as_ref().expect("first item is Ok").id,
1530 SourceId::new(1)
1531 );
1532 assert_eq!(
1533 results[1].as_ref().expect("second item is Ok").id,
1534 SourceId::new(2)
1535 );
1536 assert!(
1537 matches!(results[2], Err(Error::Api { status: 500, .. })),
1538 "third item should be the page-two error, got {:?}",
1539 results[2]
1540 );
1541 }
1542
1543 #[tokio::test]
1544 async fn malformed_body_maps_to_deserialize_error() {
1545 let server = MockServer::start().await;
1546 Mock::given(method("GET"))
1547 .and(path("/series"))
1548 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"unexpected":true}"#))
1549 .mount(&server)
1550 .await;
1551
1552 let error = client_for(&server)
1553 .series(&SeriesId::new("GNPCA"))
1554 .await
1555 .expect_err("an unexpected body should fail to deserialize");
1556 assert!(matches!(error, Error::Deserialize(_)), "got {error:?}");
1557 }
1558
1559 #[tokio::test]
1560 async fn request_carries_api_key_file_type_and_params() {
1561 let server = MockServer::start().await;
1562 Mock::given(method("GET"))
1566 .and(path("/series/observations"))
1567 .and(query_param("api_key", "test-key"))
1568 .and(query_param("file_type", "json"))
1569 .and(query_param("units", "pch"))
1570 .and(query_param("limit", "5"))
1571 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"observations":[]}"#))
1572 .mount(&server)
1573 .await;
1574
1575 let observations = client_for(&server)
1576 .observations(&SeriesId::new("GNPCA"))
1577 .units(Units::PercentChange)
1578 .limit(5)
1579 .send()
1580 .await
1581 .expect("request with the expected params should match the mock");
1582 assert!(observations.is_empty());
1583 }
1584
1585 #[tokio::test]
1586 async fn category_parses() {
1587 let server = MockServer::start().await;
1588 Mock::given(method("GET"))
1589 .and(path("/category"))
1590 .respond_with(ResponseTemplate::new(200).set_body_string(
1591 r#"{"categories":[{"id":125,"name":"Trade Balance","parent_id":13}]}"#,
1592 ))
1593 .mount(&server)
1594 .await;
1595
1596 let category = client_for(&server)
1597 .category(CategoryId::new(125))
1598 .await
1599 .expect("category parses");
1600 assert_eq!(category.id, CategoryId::new(125));
1601 assert_eq!(category.name, "Trade Balance");
1602 assert_eq!(category.parent_id, CategoryId::new(13));
1603 }
1604
1605 #[tokio::test]
1606 async fn category_children_parse() {
1607 let server = MockServer::start().await;
1608 Mock::given(method("GET"))
1609 .and(path("/category/children"))
1610 .respond_with(ResponseTemplate::new(200).set_body_string(
1611 r#"{"categories":[
1612 {"id":16,"name":"Exports","parent_id":13},
1613 {"id":17,"name":"Imports","parent_id":13}
1614 ]}"#,
1615 ))
1616 .mount(&server)
1617 .await;
1618
1619 let children = client_for(&server)
1620 .category_children(CategoryId::new(13))
1621 .await
1622 .expect("children parse");
1623 assert_eq!(children.len(), 2);
1624 assert_eq!(children[0].name, "Exports");
1625 assert_eq!(children[1].id, CategoryId::new(17));
1626 }
1627
1628 #[tokio::test]
1629 async fn category_related_parse() {
1630 let server = MockServer::start().await;
1631 Mock::given(method("GET"))
1632 .and(path("/category/related"))
1633 .and(query_param("category_id", "32073"))
1634 .respond_with(ResponseTemplate::new(200).set_body_string(
1635 r#"{"categories":[
1636 {"id":149,"name":"Arkansas","parent_id":27281},
1637 {"id":150,"name":"Illinois","parent_id":27281}
1638 ]}"#,
1639 ))
1640 .mount(&server)
1641 .await;
1642
1643 let related = client_for(&server)
1644 .category_related(CategoryId::new(32073))
1645 .await
1646 .expect("category/related parse");
1647 assert_eq!(related.len(), 2);
1648 assert_eq!(related[0].name, "Arkansas");
1649 assert_eq!(related[1].id, CategoryId::new(150));
1650 }
1651
1652 #[tokio::test]
1653 async fn category_series_sends_params_and_parses() {
1654 let server = MockServer::start().await;
1655 Mock::given(method("GET"))
1657 .and(path("/category/series"))
1658 .and(query_param("category_id", "125"))
1659 .and(query_param("order_by", "popularity"))
1660 .and(query_param("limit", "2"))
1661 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1662 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1663 )))
1664 .mount(&server)
1665 .await;
1666
1667 let results = client_for(&server)
1668 .category_series(CategoryId::new(125))
1669 .order_by(OrderBy::Popularity)
1670 .limit(2)
1671 .send()
1672 .await
1673 .expect("category series parse");
1674 assert_eq!(results.count, 1);
1675 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1676 }
1677
1678 #[tokio::test]
1679 async fn releases_parse_with_pagination() {
1680 let server = MockServer::start().await;
1681 Mock::given(method("GET"))
1682 .and(path("/releases"))
1683 .respond_with(ResponseTemplate::new(200).set_body_string(
1684 r#"{"count":2,"offset":0,"limit":1000,"releases":[
1685 {"id":9,"name":"Advance Monthly Sales","press_release":false},
1686 {"id":53,"name":"Gross Domestic Product","press_release":true,"link":"http://bea.gov"}
1687 ]}"#,
1688 ))
1689 .mount(&server)
1690 .await;
1691
1692 let results = client_for(&server)
1693 .releases()
1694 .send()
1695 .await
1696 .expect("releases parse");
1697 assert_eq!(results.count, 2);
1698 assert_eq!(results.releases[1].id, ReleaseId::new(53));
1699 assert_eq!(results.releases[1].link.as_deref(), Some("http://bea.gov"));
1700 }
1701
1702 #[tokio::test]
1703 async fn release_parses() {
1704 let server = MockServer::start().await;
1705 Mock::given(method("GET"))
1706 .and(path("/release"))
1707 .and(query_param("release_id", "53"))
1708 .respond_with(ResponseTemplate::new(200).set_body_string(
1709 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
1710 ))
1711 .mount(&server)
1712 .await;
1713
1714 let release = client_for(&server)
1715 .release(ReleaseId::new(53))
1716 .await
1717 .expect("release parses");
1718 assert_eq!(release.id, ReleaseId::new(53));
1719 assert_eq!(release.name, "Gross Domestic Product");
1720 assert!(release.press_release);
1721 }
1722
1723 #[tokio::test]
1724 async fn release_series_sends_params_and_parses() {
1725 let server = MockServer::start().await;
1726 Mock::given(method("GET"))
1727 .and(path("/release/series"))
1728 .and(query_param("release_id", "53"))
1729 .and(query_param("limit", "2"))
1730 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1731 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1732 )))
1733 .mount(&server)
1734 .await;
1735
1736 let results = client_for(&server)
1737 .release_series(ReleaseId::new(53))
1738 .limit(2)
1739 .send()
1740 .await
1741 .expect("release series parse");
1742 assert_eq!(results.count, 1);
1743 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1744 }
1745
1746 #[tokio::test]
1747 async fn tags_search_sends_text_and_parses() {
1748 let server = MockServer::start().await;
1749 Mock::given(method("GET"))
1750 .and(path("/tags"))
1751 .and(query_param("search_text", "gdp"))
1752 .respond_with(ResponseTemplate::new(200).set_body_string(
1753 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1754 {"name":"gdp","group_id":"gen","popularity":80,"series_count":12345}
1755 ]}"#,
1756 ))
1757 .mount(&server)
1758 .await;
1759
1760 let results = client_for(&server)
1761 .tags()
1762 .search_text("gdp")
1763 .send()
1764 .await
1765 .expect("tags parse");
1766 assert_eq!(results.count, 1);
1767 assert_eq!(results.tags[0].name, "gdp");
1768 assert_eq!(results.tags[0].series_count, 12345);
1769 }
1770
1771 #[tokio::test]
1772 async fn related_tags_send_seed_names_and_parses() {
1773 let server = MockServer::start().await;
1774 Mock::given(method("GET"))
1776 .and(path("/related_tags"))
1777 .and(query_param("tag_names", "gdp;quarterly"))
1778 .respond_with(ResponseTemplate::new(200).set_body_string(
1779 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1780 {"name":"nsa","group_id":"seas","popularity":90,"series_count":42}
1781 ]}"#,
1782 ))
1783 .mount(&server)
1784 .await;
1785
1786 let results = client_for(&server)
1787 .related_tags(["gdp", "quarterly"])
1788 .send()
1789 .await
1790 .expect("related_tags parse");
1791 assert_eq!(results.count, 1);
1792 assert_eq!(results.tags[0].name, "nsa");
1793 }
1794
1795 const ONE_TAG_BODY: &str = r#"{"count":1,"offset":0,"limit":1000,"tags":[
1797 {"name":"gdp","group_id":"gen","popularity":80,"series_count":42}
1798 ]}"#;
1799
1800 #[tokio::test]
1801 async fn category_tags_send_scope_and_parse() {
1802 let server = MockServer::start().await;
1803 Mock::given(method("GET"))
1804 .and(path("/category/tags"))
1805 .and(query_param("category_id", "125"))
1806 .and(query_param("search_text", "gdp"))
1807 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1808 .mount(&server)
1809 .await;
1810
1811 let results = client_for(&server)
1812 .category_tags(CategoryId::new(125))
1813 .search_text("gdp")
1814 .send()
1815 .await
1816 .expect("category/tags parse");
1817 assert_eq!(results.count, 1);
1818 assert_eq!(results.tags[0].name, "gdp");
1819 }
1820
1821 #[tokio::test]
1822 async fn category_related_tags_send_scope_and_seed_and_parse() {
1823 let server = MockServer::start().await;
1824 Mock::given(method("GET"))
1825 .and(path("/category/related_tags"))
1826 .and(query_param("category_id", "125"))
1827 .and(query_param("tag_names", "gdp;quarterly"))
1828 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1829 .mount(&server)
1830 .await;
1831
1832 let results = client_for(&server)
1833 .category_related_tags(CategoryId::new(125), ["gdp", "quarterly"])
1834 .send()
1835 .await
1836 .expect("category/related_tags parse");
1837 assert_eq!(results.count, 1);
1838 }
1839
1840 #[tokio::test]
1841 async fn release_tags_send_scope_and_parse() {
1842 let server = MockServer::start().await;
1843 Mock::given(method("GET"))
1844 .and(path("/release/tags"))
1845 .and(query_param("release_id", "53"))
1846 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1847 .mount(&server)
1848 .await;
1849
1850 let results = client_for(&server)
1851 .release_tags(ReleaseId::new(53))
1852 .send()
1853 .await
1854 .expect("release/tags parse");
1855 assert_eq!(results.count, 1);
1856 }
1857
1858 #[tokio::test]
1859 async fn release_related_tags_send_scope_and_seed_and_parse() {
1860 let server = MockServer::start().await;
1861 Mock::given(method("GET"))
1862 .and(path("/release/related_tags"))
1863 .and(query_param("release_id", "53"))
1864 .and(query_param("tag_names", "gdp"))
1865 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1866 .mount(&server)
1867 .await;
1868
1869 let results = client_for(&server)
1870 .release_related_tags(ReleaseId::new(53), ["gdp"])
1871 .send()
1872 .await
1873 .expect("release/related_tags parse");
1874 assert_eq!(results.count, 1);
1875 }
1876
1877 #[tokio::test]
1878 async fn series_search_tags_send_scope_and_tag_search_text() {
1879 let server = MockServer::start().await;
1880 Mock::given(method("GET"))
1883 .and(path("/series/search/tags"))
1884 .and(query_param("series_search_text", "unemployment"))
1885 .and(query_param("tag_search_text", "rate"))
1886 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1887 .mount(&server)
1888 .await;
1889
1890 let results = client_for(&server)
1891 .series_search_tags("unemployment")
1892 .search_text("rate")
1893 .send()
1894 .await
1895 .expect("series/search/tags parse");
1896 assert_eq!(results.count, 1);
1897 }
1898
1899 #[tokio::test]
1900 async fn series_search_related_tags_send_scope_and_seed() {
1901 let server = MockServer::start().await;
1902 Mock::given(method("GET"))
1903 .and(path("/series/search/related_tags"))
1904 .and(query_param("series_search_text", "unemployment"))
1905 .and(query_param("tag_names", "monthly"))
1906 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1907 .mount(&server)
1908 .await;
1909
1910 let results = client_for(&server)
1911 .series_search_related_tags("unemployment", ["monthly"])
1912 .send()
1913 .await
1914 .expect("series/search/related_tags parse");
1915 assert_eq!(results.count, 1);
1916 }
1917
1918 #[tokio::test]
1919 async fn tags_series_joins_names_and_parses() {
1920 let server = MockServer::start().await;
1921 Mock::given(method("GET"))
1923 .and(path("/tags/series"))
1924 .and(query_param("tag_names", "gdp;quarterly"))
1925 .and(query_param("limit", "2"))
1926 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1927 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1928 )))
1929 .mount(&server)
1930 .await;
1931
1932 let results = client_for(&server)
1933 .tags_series(["gdp", "quarterly"])
1934 .limit(2)
1935 .send()
1936 .await
1937 .expect("tags/series parse");
1938 assert_eq!(results.count, 1);
1939 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1940 }
1941
1942 #[tokio::test]
1943 async fn series_tags_parses() {
1944 let server = MockServer::start().await;
1945 Mock::given(method("GET"))
1946 .and(path("/series/tags"))
1947 .and(query_param("series_id", "GNPCA"))
1948 .respond_with(ResponseTemplate::new(200).set_body_string(
1949 r#"{"count":2,"offset":0,"limit":1000,"tags":[
1950 {"name":"gnp","group_id":"gen","popularity":50,"series_count":10},
1951 {"name":"usa","group_id":"geo","notes":null,"popularity":100,"series_count":500}
1952 ]}"#,
1953 ))
1954 .mount(&server)
1955 .await;
1956
1957 let results = client_for(&server)
1958 .series_tags(&SeriesId::new("GNPCA"))
1959 .await
1960 .expect("series/tags parse");
1961 assert_eq!(results.count, 2);
1962 assert_eq!(results.tags[0].name, "gnp");
1963 assert!(results.tags[1].notes.is_none());
1964 }
1965
1966 #[tokio::test]
1967 async fn sources_parse_with_pagination() {
1968 let server = MockServer::start().await;
1969 Mock::given(method("GET"))
1970 .and(path("/sources"))
1971 .respond_with(ResponseTemplate::new(200).set_body_string(
1972 r#"{"count":2,"offset":0,"limit":1000,"sources":[
1973 {"id":1,"name":"Board of Governors of the Federal Reserve System (US)"},
1974 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}
1975 ]}"#,
1976 ))
1977 .mount(&server)
1978 .await;
1979
1980 let results = client_for(&server)
1981 .sources()
1982 .send()
1983 .await
1984 .expect("sources parse");
1985 assert_eq!(results.count, 2);
1986 assert_eq!(results.sources[1].id, SourceId::new(18));
1987 assert_eq!(results.sources[1].link.as_deref(), Some("http://bea.gov"));
1988 }
1989
1990 #[tokio::test]
1991 async fn source_parses() {
1992 let server = MockServer::start().await;
1993 Mock::given(method("GET"))
1994 .and(path("/source"))
1995 .and(query_param("source_id", "18"))
1996 .respond_with(ResponseTemplate::new(200).set_body_string(
1997 r#"{"sources":[{"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}]}"#,
1998 ))
1999 .mount(&server)
2000 .await;
2001
2002 let source = client_for(&server)
2003 .source(SourceId::new(18))
2004 .await
2005 .expect("source parses");
2006 assert_eq!(source.id, SourceId::new(18));
2007 assert_eq!(source.name, "U.S. Bureau of Economic Analysis");
2008 }
2009
2010 #[tokio::test]
2011 async fn source_releases_send_source_id_and_parse() {
2012 let server = MockServer::start().await;
2013 Mock::given(method("GET"))
2015 .and(path("/source/releases"))
2016 .and(query_param("source_id", "18"))
2017 .and(query_param("limit", "2"))
2018 .respond_with(ResponseTemplate::new(200).set_body_string(
2019 r#"{"count":1,"offset":0,"limit":2,"releases":[
2020 {"id":53,"name":"Gross Domestic Product","press_release":true}
2021 ]}"#,
2022 ))
2023 .mount(&server)
2024 .await;
2025
2026 let results = client_for(&server)
2027 .source_releases(SourceId::new(18))
2028 .limit(2)
2029 .send()
2030 .await
2031 .expect("source/releases parse");
2032 assert_eq!(results.count, 1);
2033 assert_eq!(results.releases[0].id, ReleaseId::new(53));
2034 }
2035
2036 #[tokio::test]
2037 async fn release_sources_send_release_id_and_parse() {
2038 let server = MockServer::start().await;
2039 Mock::given(method("GET"))
2042 .and(path("/release/sources"))
2043 .and(query_param("release_id", "51"))
2044 .respond_with(ResponseTemplate::new(200).set_body_string(
2045 r#"{"realtime_start":"2013-08-14","realtime_end":"2013-08-14","sources":[
2046 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://www.bea.gov/"},
2047 {"id":19,"name":"U.S. Census Bureau"}
2048 ]}"#,
2049 ))
2050 .mount(&server)
2051 .await;
2052
2053 let sources = client_for(&server)
2054 .release_sources(ReleaseId::new(51))
2055 .await
2056 .expect("release/sources parse");
2057 assert_eq!(sources.len(), 2);
2058 assert_eq!(sources[0].id, SourceId::new(18));
2059 assert_eq!(sources[0].link.as_deref(), Some("http://www.bea.gov/"));
2060 assert!(sources[1].link.is_none());
2061 }
2062
2063 #[tokio::test]
2064 async fn releases_dates_send_params_and_parse() {
2065 let server = MockServer::start().await;
2066 Mock::given(method("GET"))
2068 .and(path("/releases/dates"))
2069 .and(query_param("sort_order", "desc"))
2070 .and(query_param("limit", "2"))
2071 .respond_with(ResponseTemplate::new(200).set_body_string(
2072 r#"{"count":2,"offset":0,"limit":2,"release_dates":[
2073 {"release_id":9,"release_name":"Advance Monthly Sales","date":"2013-08-13"},
2074 {"release_id":10,"release_name":"Consumer Price Index","date":"2013-08-15"}
2075 ]}"#,
2076 ))
2077 .mount(&server)
2078 .await;
2079
2080 let results = client_for(&server)
2081 .releases_dates()
2082 .sort_order(SortOrder::Descending)
2083 .limit(2)
2084 .send()
2085 .await
2086 .expect("releases/dates parse");
2087 assert_eq!(results.count, 2);
2088 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(9));
2089 assert_eq!(
2090 results.release_dates[0].release_name.as_deref(),
2091 Some("Advance Monthly Sales")
2092 );
2093 }
2094
2095 #[tokio::test]
2096 async fn release_dates_send_release_id_and_include_flag_and_parse() {
2097 let server = MockServer::start().await;
2098 Mock::given(method("GET"))
2101 .and(path("/release/dates"))
2102 .and(query_param("release_id", "82"))
2103 .and(query_param("include_release_dates_with_no_data", "true"))
2104 .respond_with(ResponseTemplate::new(200).set_body_string(
2105 r#"{"count":2,"offset":0,"limit":10000,"release_dates":[
2106 {"release_id":82,"date":"1997-02-10"},
2107 {"release_id":82,"date":"1998-02-10"}
2108 ]}"#,
2109 ))
2110 .mount(&server)
2111 .await;
2112
2113 let results = client_for(&server)
2114 .release_dates(ReleaseId::new(82))
2115 .include_dates_with_no_data(true)
2116 .send()
2117 .await
2118 .expect("release/dates parse");
2119 assert_eq!(results.count, 2);
2120 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(82));
2121 assert!(results.release_dates[0].release_name.is_none());
2122 }
2123
2124 #[tokio::test]
2125 async fn release_tables_send_element_and_parse_tree() {
2126 let server = MockServer::start().await;
2127 Mock::given(method("GET"))
2130 .and(path("/release/tables"))
2131 .and(query_param("release_id", "10"))
2132 .and(query_param("element_id", "34483"))
2133 .respond_with(ResponseTemplate::new(200).set_body_string(
2134 r#"{"name":"Monthly, SA","element_id":34483,"release_id":"10","elements":{
2135 "34484":{"element_id":34484,"release_id":10,"parent_id":34483,
2136 "series_id":"","type":"series","name":"All items","line":"1","level":"0",
2137 "children":[
2138 {"element_id":34485,"release_id":10,"parent_id":34484,
2139 "series_id":"CPIFABSL","type":"series","name":"Food",
2140 "line":"2","level":"1","children":[]}
2141 ]}
2142 }}"#,
2143 ))
2144 .mount(&server)
2145 .await;
2146
2147 let table = client_for(&server)
2148 .release_tables(ReleaseId::new(10))
2149 .element(ReleaseElementId::new(34483))
2150 .send()
2151 .await
2152 .expect("release/tables parse");
2153 assert_eq!(table.name.as_deref(), Some("Monthly, SA"));
2154 assert_eq!(table.roots.len(), 1);
2155 let leaf = &table.roots[0].children[0];
2156 assert_eq!(
2157 leaf.series_id.as_ref().map(|s| s.as_str()),
2158 Some("CPIFABSL")
2159 );
2160 }
2161
2162 #[tokio::test]
2163 async fn release_tables_observation_values_reach_wire_and_parse() {
2164 let server = MockServer::start().await;
2165 Mock::given(method("GET"))
2169 .and(path("/release/tables"))
2170 .and(query_param("release_id", "10"))
2171 .and(query_param("include_observation_values", "true"))
2172 .and(query_param("observation_date", "2023-06-01"))
2173 .respond_with(ResponseTemplate::new(200).set_body_string(
2174 r#"{"release_id":"10","elements":{
2175 "36715":{"element_id":36715,"release_id":10,"parent_id":36714,
2176 "series_id":"CUSR0000SA0L5","type":"series","name":"All items",
2177 "level":"1","observation_value":"292.260","observation_date":"Jun 2023",
2178 "children":[]}
2179 }}"#,
2180 ))
2181 .mount(&server)
2182 .await;
2183
2184 let table = client_for(&server)
2185 .release_tables(ReleaseId::new(10))
2186 .observation_date(chrono::NaiveDate::from_ymd_opt(2023, 6, 1).unwrap())
2187 .send()
2188 .await
2189 .expect("release/tables with values parse");
2190 let row = &table.roots[0];
2191 assert_eq!(row.observation_value, Some(292.260));
2192 assert_eq!(row.observation_date.as_deref(), Some("Jun 2023"));
2193 }
2194
2195 #[tokio::test]
2196 async fn series_categories_parse() {
2197 let server = MockServer::start().await;
2198 Mock::given(method("GET"))
2199 .and(path("/series/categories"))
2200 .and(query_param("series_id", "GNPCA"))
2201 .respond_with(ResponseTemplate::new(200).set_body_string(
2202 r#"{"categories":[
2203 {"id":106,"name":"Gross National Product","parent_id":18},
2204 {"id":18,"name":"National Income & Product Accounts","parent_id":13}
2205 ]}"#,
2206 ))
2207 .mount(&server)
2208 .await;
2209
2210 let categories = client_for(&server)
2211 .series_categories(&SeriesId::new("GNPCA"))
2212 .await
2213 .expect("series/categories parse");
2214 assert_eq!(categories.len(), 2);
2215 assert_eq!(categories[0].id, CategoryId::new(106));
2216 }
2217
2218 #[tokio::test]
2219 async fn series_release_parses() {
2220 let server = MockServer::start().await;
2221 Mock::given(method("GET"))
2222 .and(path("/series/release"))
2223 .and(query_param("series_id", "GNPCA"))
2224 .respond_with(ResponseTemplate::new(200).set_body_string(
2225 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
2226 ))
2227 .mount(&server)
2228 .await;
2229
2230 let release = client_for(&server)
2231 .series_release(&SeriesId::new("GNPCA"))
2232 .await
2233 .expect("series/release parse");
2234 assert_eq!(release.id, ReleaseId::new(53));
2235 assert_eq!(release.name, "Gross Domestic Product");
2236 }
2237
2238 #[tokio::test]
2239 async fn series_updates_sends_filter_and_parses() {
2240 let server = MockServer::start().await;
2241 Mock::given(method("GET"))
2242 .and(path("/series/updates"))
2243 .and(query_param("filter_value", "macro"))
2244 .and(query_param("limit", "2"))
2245 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2246 "{{\"count\":5,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
2247 )))
2248 .mount(&server)
2249 .await;
2250
2251 let results = client_for(&server)
2252 .series_updates()
2253 .filter(UpdatesFilter::Macro)
2254 .limit(2)
2255 .send()
2256 .await
2257 .expect("series/updates parse");
2258 assert_eq!(results.count, 5);
2259 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
2260 }
2261
2262 #[tokio::test]
2263 async fn series_updates_sends_time_window_as_yyyymmddhhmm() {
2264 use chrono::NaiveDate;
2265 let server = MockServer::start().await;
2266 Mock::given(method("GET"))
2267 .and(path("/series/updates"))
2268 .and(query_param("start_time", "201803021420"))
2269 .and(query_param("end_time", "201803030905"))
2270 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2271 "{{\"count\":1,\"offset\":0,\"limit\":1,\"seriess\":[{SERIES_OBJECT}]}}"
2272 )))
2273 .mount(&server)
2274 .await;
2275
2276 let start = NaiveDate::from_ymd_opt(2018, 3, 2)
2277 .unwrap()
2278 .and_hms_opt(14, 20, 0)
2279 .unwrap();
2280 let end = NaiveDate::from_ymd_opt(2018, 3, 3)
2281 .unwrap()
2282 .and_hms_opt(9, 5, 0)
2283 .unwrap();
2284 let results = client_for(&server)
2285 .series_updates()
2286 .time_window(start, end)
2287 .send()
2288 .await
2289 .expect("series/updates time-window parse");
2290 assert_eq!(results.count, 1);
2291 }
2292
2293 #[tokio::test]
2294 async fn series_vintagedates_send_id_and_parse() {
2295 let server = MockServer::start().await;
2296 Mock::given(method("GET"))
2297 .and(path("/series/vintagedates"))
2298 .and(query_param("series_id", "GNPCA"))
2299 .and(query_param("limit", "2"))
2300 .respond_with(ResponseTemplate::new(200).set_body_string(
2301 r#"{"count":3,"offset":0,"limit":2,"vintage_dates":["1958-12-21","1959-02-19"]}"#,
2302 ))
2303 .mount(&server)
2304 .await;
2305
2306 let dates = client_for(&server)
2307 .series_vintagedates(&SeriesId::new("GNPCA"))
2308 .limit(2)
2309 .send()
2310 .await
2311 .expect("series/vintagedates parse");
2312 assert_eq!(dates.count, 3);
2313 assert_eq!(dates.vintage_dates.len(), 2);
2314 assert_eq!(
2315 dates.vintage_dates[0],
2316 chrono::NaiveDate::from_ymd_opt(1958, 12, 21).unwrap()
2317 );
2318 }
2319
2320 #[tokio::test]
2323 async fn geofred_regional_data_sends_all_params_and_parses() {
2324 let server = MockServer::start().await;
2325 Mock::given(method("GET"))
2329 .and(path("/regional/data"))
2330 .and(query_param("series_group", "882"))
2331 .and(query_param("region_type", "state"))
2332 .and(query_param("date", "2013-01-01"))
2333 .and(query_param("units", "Dollars"))
2334 .and(query_param("frequency", "a"))
2335 .and(query_param("season", "NSA"))
2336 .respond_with(ResponseTemplate::new(200).set_body_string(
2337 r#"{"meta":{"title":"t","region":"state","seasonality":"Not Seasonally Adjusted",
2338 "units":"Dollars","frequency":"Annual","data":{"2013-01-01":[
2339 {"region":"Alabama","code":"01","value":35706,"series_id":"ALPCPI"}
2340 ]}}}"#,
2341 ))
2342 .mount(&server)
2343 .await;
2344
2345 let data = client_for(&server)
2346 .regional_data(
2347 &SeriesGroupId::new("882"),
2348 RegionType::State,
2349 chrono::NaiveDate::from_ymd_opt(2013, 1, 1).unwrap(),
2350 "Dollars",
2351 Frequency::Annual,
2352 SeasonalAdjustment::NotSeasonallyAdjusted,
2353 )
2354 .await
2355 .expect("regional data parses");
2356 let day = &data.meta.data["2013-01-01"];
2357 assert_eq!(day[0].region, "Alabama");
2358 assert_eq!(day[0].value, Some(35706.0));
2359 assert_eq!(day[0].series_id, SeriesId::new("ALPCPI"));
2360 }
2361
2362 #[tokio::test]
2363 async fn geofred_series_data_sends_optional_date_and_parses() {
2364 let server = MockServer::start().await;
2365 Mock::given(method("GET"))
2366 .and(path("/series/data"))
2367 .and(query_param("series_id", "SMU56000000500000001"))
2368 .and(query_param("date", "2013-01-01"))
2369 .respond_with(ResponseTemplate::new(200).set_body_string(
2370 r#"{"meta":{"title":"t","region":"state","seasonality":"Not Seasonally Adjusted",
2371 "units":"Thousands of Persons","frequency":"Monthly","data":{"2013-01-01":[
2372 {"region":"Alabama","code":"01","value":1506.5,"series_id":"SMU01000000500000001"}
2373 ]}}}"#,
2374 ))
2375 .mount(&server)
2376 .await;
2377
2378 let data = client_for(&server)
2379 .series_data(&SeriesId::new("SMU56000000500000001"))
2380 .date(chrono::NaiveDate::from_ymd_opt(2013, 1, 1).unwrap())
2381 .send()
2382 .await
2383 .expect("series data parses");
2384 assert_eq!(data.meta.data["2013-01-01"][0].value, Some(1506.5));
2385 }
2386
2387 #[tokio::test]
2388 async fn geofred_series_group_unwraps_envelope_and_parses() {
2389 let server = MockServer::start().await;
2390 Mock::given(method("GET"))
2391 .and(path("/series/group"))
2392 .and(query_param("series_id", "SMU56000000500000001"))
2393 .respond_with(ResponseTemplate::new(200).set_body_string(
2394 r#"{"series_group":{"title":"All Employees: Total Private","region_type":"state",
2395 "series_group":"1223","season":"NSA","units":"Thousands of Persons",
2396 "frequency":"Monthly","min_date":"1990-01-01","max_date":"2026-05-01"}}"#,
2397 ))
2398 .mount(&server)
2399 .await;
2400
2401 let group = client_for(&server)
2402 .series_group(&SeriesId::new("SMU56000000500000001"))
2403 .await
2404 .expect("series group parses");
2405 assert_eq!(group.id, SeriesGroupId::new("1223"));
2406 assert_eq!(group.region_type, "state");
2407 }
2408
2409 #[tokio::test]
2410 async fn geofred_shape_file_sends_shape_and_parses_geojson() {
2411 let server = MockServer::start().await;
2412 Mock::given(method("GET"))
2413 .and(path("/shapes/file"))
2414 .and(query_param("shape", "bea"))
2415 .respond_with(ResponseTemplate::new(200).set_body_string(
2416 r#"{"type":"FeatureCollection","name":"state_bea_region","features":[
2417 {"type":"Feature","properties":{"bea_region":8},
2418 "geometry":{"type":"MultiPolygon","coordinates":[[[[1485,2651]]]]}}
2419 ]}"#,
2420 ))
2421 .mount(&server)
2422 .await;
2423
2424 let shapes = client_for(&server)
2425 .shape_file(ShapeType::Bea)
2426 .await
2427 .expect("shape file parses");
2428 assert_eq!(shapes.kind, "FeatureCollection");
2429 assert_eq!(shapes.features[0].geometry.kind, "MultiPolygon");
2430 }
2431}