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(
896 &self,
897 series_group: &SeriesGroupId,
898 region_type: RegionType,
899 date: NaiveDate,
900 units: impl Into<String>,
901 frequency: Frequency,
902 season: SeasonalAdjustment,
903 ) -> Result<RegionalData> {
904 self.get_geofred(
905 "/regional/data",
906 &[
907 ("series_group", series_group.as_str().to_owned()),
908 ("region_type", region_type.query_code().to_owned()),
909 ("date", date.to_string()),
910 ("units", units.into()),
911 ("frequency", frequency.query_code().to_owned()),
912 ("season", season.query_code().to_owned()),
913 ],
914 )
915 .await
916 }
917
918 pub fn series_data(&self, series_id: &SeriesId) -> SeriesDataRequest<'_> {
938 SeriesDataRequest::new(self, series_id.clone())
939 }
940
941 pub(crate) async fn execute_series_data(
944 &self,
945 request: &SeriesDataRequest<'_>,
946 ) -> Result<RegionalData> {
947 self.get_geofred("/series/data", &request.query_params())
948 .await
949 }
950
951 pub async fn series_group(&self, series_id: &SeriesId) -> Result<SeriesGroup> {
963 let response: SeriesGroupResponse = self
964 .get_geofred(
965 "/series/group",
966 &[("series_id", series_id.as_str().to_owned())],
967 )
968 .await?;
969 Ok(response.series_group)
970 }
971
972 pub async fn shape_file(&self, shape: ShapeType) -> Result<ShapeFile> {
981 self.get_geofred("/shapes/file", &[("shape", shape.query_code().to_owned())])
982 .await
983 }
984
985 async fn get<T: DeserializeOwned>(
987 &self,
988 path: &str,
989 params: &[(&'static str, String)],
990 ) -> Result<T> {
991 self.get_from(&self.base_url, path, params).await
992 }
993
994 async fn get_geofred<T: DeserializeOwned>(
998 &self,
999 path: &str,
1000 params: &[(&'static str, String)],
1001 ) -> Result<T> {
1002 self.get_from(&self.geofred_base_url, path, params)
1003 .await
1004 .map_err(|error| geofred_error(path, params, error))
1005 }
1006
1007 async fn get_from<T: DeserializeOwned>(
1012 &self,
1013 base_url: &str,
1014 path: &str,
1015 params: &[(&'static str, String)],
1016 ) -> Result<T> {
1017 let mut query: Vec<(&str, String)> = Vec::with_capacity(params.len() + 2);
1018 query.push(("api_key", self.api_key.clone()));
1019 query.push(("file_type", "json".to_owned()));
1020 query.extend(params.iter().cloned());
1021
1022 let response = self
1023 .http
1024 .get(format!("{base_url}{path}"))
1025 .query(&query)
1026 .send()
1027 .await?;
1028
1029 let status = response.status();
1030 let retry_after = parse_retry_after(response.headers());
1032 let body = response.bytes().await?;
1033
1034 if !status.is_success() {
1035 return Err(api_error(status, retry_after, &body));
1036 }
1037
1038 serde_json::from_slice(&body).map_err(Error::from)
1039 }
1040}
1041
1042fn join_tag_names<I, S>(tag_names: I) -> String
1045where
1046 I: IntoIterator<Item = S>,
1047 S: AsRef<str>,
1048{
1049 tag_names
1050 .into_iter()
1051 .map(|name| name.as_ref().to_owned())
1052 .collect::<Vec<_>>()
1053 .join(";")
1054}
1055
1056fn api_error(status: reqwest::StatusCode, retry_after: Option<Duration>, body: &[u8]) -> Error {
1060 let fred: Option<FredErrorBody> = serde_json::from_slice(body).ok();
1061 let code = fred.as_ref().and_then(|e| e.error_code);
1062 let message = fred.and_then(|e| e.error_message).unwrap_or_else(|| {
1063 status
1064 .canonical_reason()
1065 .unwrap_or("unknown error")
1066 .to_owned()
1067 });
1068
1069 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
1070 return Error::RateLimited { retry_after };
1071 }
1072
1073 Error::Api {
1074 status: status.as_u16(),
1075 code,
1076 message,
1077 }
1078}
1079
1080fn geofred_error(path: &str, params: &[(&'static str, String)], error: Error) -> Error {
1095 let id = params
1096 .iter()
1097 .find(|(key, _)| *key == "series_id" || *key == "series_group");
1098 match (error, id) {
1099 (
1100 Error::Api {
1101 status: 500,
1102 code,
1103 message,
1104 },
1105 Some((key, value)),
1106 ) if message == "Internal Server Error" => Error::Api {
1107 status: 500,
1108 code,
1109 message: format!(
1110 "GeoFRED {path} ({key}={value}) returned HTTP 500 — the id is likely \
1111 invalid or not a regional (GeoFRED/Maps) series; FRED answers that case \
1112 with a 500 rather than a 404. (If the id is definitely a regional series, \
1113 FRED may be having a genuine internal error.)"
1114 ),
1115 },
1116 (other, _) => other,
1117 }
1118}
1119
1120fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
1124 let seconds: u64 = headers
1125 .get(reqwest::header::RETRY_AFTER)?
1126 .to_str()
1127 .ok()?
1128 .trim()
1129 .parse()
1130 .ok()?;
1131 Some(Duration::from_secs(seconds))
1132}
1133
1134#[derive(Deserialize)]
1137struct ObservationsResponse {
1138 observations: Vec<Observation>,
1139}
1140
1141#[derive(Deserialize)]
1144struct SeriesResponse {
1145 seriess: Vec<Series>,
1146}
1147
1148#[derive(Deserialize)]
1150struct CategoriesResponse {
1151 categories: Vec<Category>,
1152}
1153
1154#[derive(Deserialize)]
1158struct ReleaseResponse {
1159 releases: Vec<Release>,
1160}
1161
1162#[derive(Deserialize)]
1166struct SourceResponse {
1167 sources: Vec<Source>,
1168}
1169
1170#[derive(Deserialize)]
1172struct SeriesGroupResponse {
1173 series_group: SeriesGroup,
1174}
1175
1176#[derive(Deserialize)]
1178struct FredErrorBody {
1179 error_code: Option<u32>,
1180 error_message: Option<String>,
1181}
1182
1183#[cfg(test)]
1184mod tests {
1185 use super::Client;
1186 use std::time::Duration;
1187
1188 use crate::{
1189 CategoryId, Error, Frequency, OrderBy, Paginate, RegionType, ReleaseElementId, ReleaseId,
1190 SeasonalAdjustment, SeriesGroupId, SeriesId, ShapeType, SortOrder, SourceId, Units,
1191 UpdatesFilter,
1192 };
1193 use wiremock::matchers::{method, path, query_param};
1194 use wiremock::{Mock, MockServer, ResponseTemplate};
1195
1196 const SERIES_OBJECT: &str = r#"{
1198 "id": "GNPCA",
1199 "title": "Real Gross National Product",
1200 "observation_start": "1929-01-01",
1201 "observation_end": "2023-01-01",
1202 "frequency": "Annual",
1203 "units": "Billions of Chained 2017 Dollars",
1204 "seasonal_adjustment": "Not Seasonally Adjusted",
1205 "last_updated": "2024-03-28 07:56:03-05",
1206 "popularity": 76,
1207 "notes": "BEA Account Code: A001RX"
1208 }"#;
1209
1210 fn client_for(server: &MockServer) -> Client {
1211 Client::with_base_url("test-key", server.uri()).expect("client builds")
1212 }
1213
1214 #[tokio::test]
1215 async fn series_parses_metadata() {
1216 let server = MockServer::start().await;
1217 Mock::given(method("GET"))
1218 .and(path("/series"))
1219 .respond_with(
1220 ResponseTemplate::new(200)
1221 .set_body_string(format!("{{\"seriess\":[{SERIES_OBJECT}]}}")),
1222 )
1223 .mount(&server)
1224 .await;
1225
1226 let series = client_for(&server)
1227 .series(&SeriesId::new("GNPCA"))
1228 .await
1229 .expect("series parses");
1230 assert_eq!(series.id, SeriesId::new("GNPCA"));
1231 assert_eq!(series.frequency, Frequency::Annual);
1232 assert_eq!(
1233 series.seasonal_adjustment,
1234 SeasonalAdjustment::NotSeasonallyAdjusted
1235 );
1236 assert_eq!(series.popularity, 76);
1237 }
1238
1239 #[tokio::test]
1240 async fn observations_parse_missing_and_present_values() {
1241 let server = MockServer::start().await;
1242 let body = r#"{"observations":[
1243 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1930-01-01","value":"."},
1244 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"1065.9"}
1245 ]}"#;
1246 Mock::given(method("GET"))
1247 .and(path("/series/observations"))
1248 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1249 .mount(&server)
1250 .await;
1251
1252 let observations = client_for(&server)
1253 .observations(&SeriesId::new("GNPCA"))
1254 .send()
1255 .await
1256 .expect("observations parse");
1257 assert_eq!(observations.len(), 2);
1258 assert_eq!(observations[0].value, None); assert_eq!(observations[1].value, Some(1065.9));
1260 }
1261
1262 #[tokio::test]
1263 async fn observations_point_in_time_sends_realtime_and_parses_period() {
1264 let server = MockServer::start().await;
1265 Mock::given(method("GET"))
1268 .and(path("/series/observations"))
1269 .and(query_param("realtime_start", "2020-01-01"))
1270 .and(query_param("realtime_end", "2020-01-01"))
1271 .respond_with(ResponseTemplate::new(200).set_body_string(
1272 r#"{"observations":[
1273 {"realtime_start":"2020-01-01","realtime_end":"2020-01-01","date":"2017-01-01","value":"18344.563"}
1274 ]}"#,
1275 ))
1276 .mount(&server)
1277 .await;
1278
1279 let as_of = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1280 let observations = client_for(&server)
1281 .observations(&SeriesId::new("GNPCA"))
1282 .realtime(as_of, as_of)
1283 .send()
1284 .await
1285 .expect("point-in-time observations parse");
1286 assert_eq!(observations[0].realtime_start, as_of);
1287 assert_eq!(observations[0].realtime_end, as_of);
1288 assert_eq!(observations[0].value, Some(18344.563));
1289 }
1290
1291 #[tokio::test]
1292 async fn search_parses_results_with_pagination() {
1293 let server = MockServer::start().await;
1294 let body =
1295 format!("{{\"count\":1,\"offset\":0,\"limit\":1000,\"seriess\":[{SERIES_OBJECT}]}}");
1296 Mock::given(method("GET"))
1297 .and(path("/series/search"))
1298 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1299 .mount(&server)
1300 .await;
1301
1302 let results = client_for(&server)
1303 .search("real gnp")
1304 .send()
1305 .await
1306 .expect("search parses");
1307 assert_eq!(results.count, 1);
1308 assert_eq!(results.series.len(), 1);
1309 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1310 }
1311
1312 #[tokio::test]
1313 async fn error_status_with_body_maps_to_api_error() {
1314 let server = MockServer::start().await;
1315 let body = r#"{"error_code":400,"error_message":"Bad Request. Invalid value for variable series_id."}"#;
1316 Mock::given(method("GET"))
1317 .and(path("/series"))
1318 .respond_with(ResponseTemplate::new(400).set_body_string(body))
1319 .mount(&server)
1320 .await;
1321
1322 let error = client_for(&server)
1323 .series(&SeriesId::new("BAD"))
1324 .await
1325 .expect_err("a 400 should be an API error");
1326 match error {
1327 Error::Api {
1328 status,
1329 code,
1330 message,
1331 } => {
1332 assert_eq!(status, 400);
1333 assert_eq!(code, Some(400));
1334 assert!(message.contains("Invalid value"), "message was {message:?}");
1335 }
1336 other => panic!("expected Error::Api, got {other:?}"),
1337 }
1338 }
1339
1340 #[tokio::test]
1341 async fn too_many_requests_maps_to_rate_limited() {
1342 let server = MockServer::start().await;
1343 Mock::given(method("GET"))
1344 .and(path("/series"))
1345 .respond_with(ResponseTemplate::new(429))
1346 .mount(&server)
1347 .await;
1348
1349 let error = client_for(&server)
1350 .series(&SeriesId::new("GNPCA"))
1351 .await
1352 .expect_err("a 429 should be rate-limited");
1353 assert!(matches!(error, Error::RateLimited { .. }), "got {error:?}");
1354 }
1355
1356 #[tokio::test]
1357 async fn rate_limited_carries_retry_after_seconds() {
1358 let server = MockServer::start().await;
1359 Mock::given(method("GET"))
1360 .and(path("/series"))
1361 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "120"))
1362 .mount(&server)
1363 .await;
1364
1365 let error = client_for(&server)
1366 .series(&SeriesId::new("GNPCA"))
1367 .await
1368 .expect_err("a 429 should be rate-limited");
1369 match error {
1370 Error::RateLimited { retry_after } => {
1371 assert_eq!(retry_after, Some(Duration::from_secs(120)));
1372 }
1373 other => panic!("expected Error::RateLimited, got {other:?}"),
1374 }
1375 }
1376
1377 #[tokio::test]
1378 async fn send_all_walks_every_page() {
1379 let server = MockServer::start().await;
1380 Mock::given(method("GET"))
1384 .and(path("/sources"))
1385 .and(query_param("offset", "0"))
1386 .respond_with(ResponseTemplate::new(200).set_body_string(
1387 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1388 {"id":1,"name":"Source One"},
1389 {"id":2,"name":"Source Two"}
1390 ]}"#,
1391 ))
1392 .mount(&server)
1393 .await;
1394 Mock::given(method("GET"))
1395 .and(path("/sources"))
1396 .and(query_param("offset", "2"))
1397 .respond_with(ResponseTemplate::new(200).set_body_string(
1398 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1399 {"id":3,"name":"Source Three"}
1400 ]}"#,
1401 ))
1402 .mount(&server)
1403 .await;
1404
1405 let sources = client_for(&server)
1406 .sources()
1407 .send_all()
1408 .await
1409 .expect("send_all walks both pages");
1410 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1411 assert_eq!(
1412 ids,
1413 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1414 );
1415 }
1416
1417 #[tokio::test]
1418 async fn send_all_treats_limit_as_a_ceiling() {
1419 let server = MockServer::start().await;
1420 Mock::given(method("GET"))
1424 .and(path("/sources"))
1425 .and(query_param("offset", "0"))
1426 .and(query_param("limit", "2"))
1427 .respond_with(ResponseTemplate::new(200).set_body_string(
1428 r#"{"count":5,"offset":0,"limit":2,"sources":[
1429 {"id":1,"name":"Source One"},
1430 {"id":2,"name":"Source Two"}
1431 ]}"#,
1432 ))
1433 .mount(&server)
1434 .await;
1435
1436 let sources = client_for(&server)
1437 .sources()
1438 .limit(2)
1439 .send_all()
1440 .await
1441 .expect("send_all stops at the ceiling");
1442 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1443 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1444 }
1445
1446 #[tokio::test]
1447 async fn send_all_retries_after_a_429() {
1448 let server = MockServer::start().await;
1449 Mock::given(method("GET"))
1453 .and(path("/sources"))
1454 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
1455 .up_to_n_times(1)
1456 .with_priority(1)
1457 .mount(&server)
1458 .await;
1459 Mock::given(method("GET"))
1460 .and(path("/sources"))
1461 .respond_with(ResponseTemplate::new(200).set_body_string(
1462 r#"{"count":1,"offset":0,"limit":1000,"sources":[
1463 {"id":1,"name":"Source One"}
1464 ]}"#,
1465 ))
1466 .with_priority(2)
1467 .mount(&server)
1468 .await;
1469
1470 let sources = client_for(&server)
1471 .sources()
1472 .send_all()
1473 .await
1474 .expect("send_all retries the 429 and then succeeds");
1475 assert_eq!(sources.len(), 1);
1476 assert_eq!(sources[0].id, SourceId::new(1));
1477 }
1478
1479 #[tokio::test]
1480 async fn stream_walks_every_page() {
1481 use futures_util::TryStreamExt;
1482
1483 let server = MockServer::start().await;
1484 Mock::given(method("GET"))
1485 .and(path("/sources"))
1486 .and(query_param("offset", "0"))
1487 .respond_with(ResponseTemplate::new(200).set_body_string(
1488 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1489 {"id":1,"name":"Source One"},
1490 {"id":2,"name":"Source Two"}
1491 ]}"#,
1492 ))
1493 .mount(&server)
1494 .await;
1495 Mock::given(method("GET"))
1496 .and(path("/sources"))
1497 .and(query_param("offset", "2"))
1498 .respond_with(ResponseTemplate::new(200).set_body_string(
1499 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1500 {"id":3,"name":"Source Three"}
1501 ]}"#,
1502 ))
1503 .mount(&server)
1504 .await;
1505
1506 let sources: Vec<_> = client_for(&server)
1507 .sources()
1508 .stream()
1509 .try_collect()
1510 .await
1511 .expect("stream walks both pages");
1512 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1513 assert_eq!(
1514 ids,
1515 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1516 );
1517 }
1518
1519 #[tokio::test]
1520 async fn stream_treats_limit_as_a_ceiling() {
1521 use futures_util::TryStreamExt;
1522
1523 let server = MockServer::start().await;
1524 Mock::given(method("GET"))
1527 .and(path("/sources"))
1528 .and(query_param("offset", "0"))
1529 .and(query_param("limit", "2"))
1530 .respond_with(ResponseTemplate::new(200).set_body_string(
1531 r#"{"count":5,"offset":0,"limit":2,"sources":[
1532 {"id":1,"name":"Source One"},
1533 {"id":2,"name":"Source Two"}
1534 ]}"#,
1535 ))
1536 .mount(&server)
1537 .await;
1538
1539 let sources: Vec<_> = client_for(&server)
1540 .sources()
1541 .limit(2)
1542 .stream()
1543 .try_collect()
1544 .await
1545 .expect("stream stops at the ceiling");
1546 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1547 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1548 }
1549
1550 #[tokio::test]
1551 async fn stream_surfaces_a_mid_stream_error() {
1552 use futures_util::StreamExt;
1553
1554 let server = MockServer::start().await;
1555 Mock::given(method("GET"))
1558 .and(path("/sources"))
1559 .and(query_param("offset", "0"))
1560 .respond_with(ResponseTemplate::new(200).set_body_string(
1561 r#"{"count":4,"offset":0,"limit":1000,"sources":[
1562 {"id":1,"name":"Source One"},
1563 {"id":2,"name":"Source Two"}
1564 ]}"#,
1565 ))
1566 .mount(&server)
1567 .await;
1568 Mock::given(method("GET"))
1569 .and(path("/sources"))
1570 .and(query_param("offset", "2"))
1571 .respond_with(ResponseTemplate::new(500))
1572 .mount(&server)
1573 .await;
1574
1575 let results: Vec<_> = client_for(&server).sources().stream().collect().await;
1576 assert_eq!(results.len(), 3);
1577 assert_eq!(
1578 results[0].as_ref().expect("first item is Ok").id,
1579 SourceId::new(1)
1580 );
1581 assert_eq!(
1582 results[1].as_ref().expect("second item is Ok").id,
1583 SourceId::new(2)
1584 );
1585 assert!(
1586 matches!(results[2], Err(Error::Api { status: 500, .. })),
1587 "third item should be the page-two error, got {:?}",
1588 results[2]
1589 );
1590 }
1591
1592 #[tokio::test]
1593 async fn malformed_body_maps_to_deserialize_error() {
1594 let server = MockServer::start().await;
1595 Mock::given(method("GET"))
1596 .and(path("/series"))
1597 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"unexpected":true}"#))
1598 .mount(&server)
1599 .await;
1600
1601 let error = client_for(&server)
1602 .series(&SeriesId::new("GNPCA"))
1603 .await
1604 .expect_err("an unexpected body should fail to deserialize");
1605 assert!(matches!(error, Error::Deserialize(_)), "got {error:?}");
1606 }
1607
1608 #[tokio::test]
1609 async fn request_carries_api_key_file_type_and_params() {
1610 let server = MockServer::start().await;
1611 Mock::given(method("GET"))
1615 .and(path("/series/observations"))
1616 .and(query_param("api_key", "test-key"))
1617 .and(query_param("file_type", "json"))
1618 .and(query_param("units", "pch"))
1619 .and(query_param("limit", "5"))
1620 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"observations":[]}"#))
1621 .mount(&server)
1622 .await;
1623
1624 let observations = client_for(&server)
1625 .observations(&SeriesId::new("GNPCA"))
1626 .units(Units::PercentChange)
1627 .limit(5)
1628 .send()
1629 .await
1630 .expect("request with the expected params should match the mock");
1631 assert!(observations.is_empty());
1632 }
1633
1634 #[tokio::test]
1635 async fn category_parses() {
1636 let server = MockServer::start().await;
1637 Mock::given(method("GET"))
1638 .and(path("/category"))
1639 .respond_with(ResponseTemplate::new(200).set_body_string(
1640 r#"{"categories":[{"id":125,"name":"Trade Balance","parent_id":13}]}"#,
1641 ))
1642 .mount(&server)
1643 .await;
1644
1645 let category = client_for(&server)
1646 .category(CategoryId::new(125))
1647 .await
1648 .expect("category parses");
1649 assert_eq!(category.id, CategoryId::new(125));
1650 assert_eq!(category.name, "Trade Balance");
1651 assert_eq!(category.parent_id, CategoryId::new(13));
1652 }
1653
1654 #[tokio::test]
1655 async fn category_children_parse() {
1656 let server = MockServer::start().await;
1657 Mock::given(method("GET"))
1658 .and(path("/category/children"))
1659 .respond_with(ResponseTemplate::new(200).set_body_string(
1660 r#"{"categories":[
1661 {"id":16,"name":"Exports","parent_id":13},
1662 {"id":17,"name":"Imports","parent_id":13}
1663 ]}"#,
1664 ))
1665 .mount(&server)
1666 .await;
1667
1668 let children = client_for(&server)
1669 .category_children(CategoryId::new(13))
1670 .await
1671 .expect("children parse");
1672 assert_eq!(children.len(), 2);
1673 assert_eq!(children[0].name, "Exports");
1674 assert_eq!(children[1].id, CategoryId::new(17));
1675 }
1676
1677 #[tokio::test]
1678 async fn category_related_parse() {
1679 let server = MockServer::start().await;
1680 Mock::given(method("GET"))
1681 .and(path("/category/related"))
1682 .and(query_param("category_id", "32073"))
1683 .respond_with(ResponseTemplate::new(200).set_body_string(
1684 r#"{"categories":[
1685 {"id":149,"name":"Arkansas","parent_id":27281},
1686 {"id":150,"name":"Illinois","parent_id":27281}
1687 ]}"#,
1688 ))
1689 .mount(&server)
1690 .await;
1691
1692 let related = client_for(&server)
1693 .category_related(CategoryId::new(32073))
1694 .await
1695 .expect("category/related parse");
1696 assert_eq!(related.len(), 2);
1697 assert_eq!(related[0].name, "Arkansas");
1698 assert_eq!(related[1].id, CategoryId::new(150));
1699 }
1700
1701 #[tokio::test]
1702 async fn category_series_sends_params_and_parses() {
1703 let server = MockServer::start().await;
1704 Mock::given(method("GET"))
1706 .and(path("/category/series"))
1707 .and(query_param("category_id", "125"))
1708 .and(query_param("order_by", "popularity"))
1709 .and(query_param("limit", "2"))
1710 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1711 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1712 )))
1713 .mount(&server)
1714 .await;
1715
1716 let results = client_for(&server)
1717 .category_series(CategoryId::new(125))
1718 .order_by(OrderBy::Popularity)
1719 .limit(2)
1720 .send()
1721 .await
1722 .expect("category series parse");
1723 assert_eq!(results.count, 1);
1724 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1725 }
1726
1727 #[tokio::test]
1728 async fn releases_parse_with_pagination() {
1729 let server = MockServer::start().await;
1730 Mock::given(method("GET"))
1731 .and(path("/releases"))
1732 .respond_with(ResponseTemplate::new(200).set_body_string(
1733 r#"{"count":2,"offset":0,"limit":1000,"releases":[
1734 {"id":9,"name":"Advance Monthly Sales","press_release":false},
1735 {"id":53,"name":"Gross Domestic Product","press_release":true,"link":"http://bea.gov"}
1736 ]}"#,
1737 ))
1738 .mount(&server)
1739 .await;
1740
1741 let results = client_for(&server)
1742 .releases()
1743 .send()
1744 .await
1745 .expect("releases parse");
1746 assert_eq!(results.count, 2);
1747 assert_eq!(results.releases[1].id, ReleaseId::new(53));
1748 assert_eq!(results.releases[1].link.as_deref(), Some("http://bea.gov"));
1749 }
1750
1751 #[tokio::test]
1752 async fn release_parses() {
1753 let server = MockServer::start().await;
1754 Mock::given(method("GET"))
1755 .and(path("/release"))
1756 .and(query_param("release_id", "53"))
1757 .respond_with(ResponseTemplate::new(200).set_body_string(
1758 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
1759 ))
1760 .mount(&server)
1761 .await;
1762
1763 let release = client_for(&server)
1764 .release(ReleaseId::new(53))
1765 .await
1766 .expect("release parses");
1767 assert_eq!(release.id, ReleaseId::new(53));
1768 assert_eq!(release.name, "Gross Domestic Product");
1769 assert!(release.press_release);
1770 }
1771
1772 #[tokio::test]
1773 async fn release_series_sends_params_and_parses() {
1774 let server = MockServer::start().await;
1775 Mock::given(method("GET"))
1776 .and(path("/release/series"))
1777 .and(query_param("release_id", "53"))
1778 .and(query_param("limit", "2"))
1779 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1780 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1781 )))
1782 .mount(&server)
1783 .await;
1784
1785 let results = client_for(&server)
1786 .release_series(ReleaseId::new(53))
1787 .limit(2)
1788 .send()
1789 .await
1790 .expect("release series parse");
1791 assert_eq!(results.count, 1);
1792 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1793 }
1794
1795 #[tokio::test]
1796 async fn tags_search_sends_text_and_parses() {
1797 let server = MockServer::start().await;
1798 Mock::given(method("GET"))
1799 .and(path("/tags"))
1800 .and(query_param("search_text", "gdp"))
1801 .respond_with(ResponseTemplate::new(200).set_body_string(
1802 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1803 {"name":"gdp","group_id":"gen","popularity":80,"series_count":12345}
1804 ]}"#,
1805 ))
1806 .mount(&server)
1807 .await;
1808
1809 let results = client_for(&server)
1810 .tags()
1811 .search_text("gdp")
1812 .send()
1813 .await
1814 .expect("tags parse");
1815 assert_eq!(results.count, 1);
1816 assert_eq!(results.tags[0].name, "gdp");
1817 assert_eq!(results.tags[0].series_count, 12345);
1818 }
1819
1820 #[tokio::test]
1821 async fn related_tags_send_seed_names_and_parses() {
1822 let server = MockServer::start().await;
1823 Mock::given(method("GET"))
1825 .and(path("/related_tags"))
1826 .and(query_param("tag_names", "gdp;quarterly"))
1827 .respond_with(ResponseTemplate::new(200).set_body_string(
1828 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1829 {"name":"nsa","group_id":"seas","popularity":90,"series_count":42}
1830 ]}"#,
1831 ))
1832 .mount(&server)
1833 .await;
1834
1835 let results = client_for(&server)
1836 .related_tags(["gdp", "quarterly"])
1837 .send()
1838 .await
1839 .expect("related_tags parse");
1840 assert_eq!(results.count, 1);
1841 assert_eq!(results.tags[0].name, "nsa");
1842 }
1843
1844 const ONE_TAG_BODY: &str = r#"{"count":1,"offset":0,"limit":1000,"tags":[
1846 {"name":"gdp","group_id":"gen","popularity":80,"series_count":42}
1847 ]}"#;
1848
1849 #[tokio::test]
1850 async fn category_tags_send_scope_and_parse() {
1851 let server = MockServer::start().await;
1852 Mock::given(method("GET"))
1853 .and(path("/category/tags"))
1854 .and(query_param("category_id", "125"))
1855 .and(query_param("search_text", "gdp"))
1856 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1857 .mount(&server)
1858 .await;
1859
1860 let results = client_for(&server)
1861 .category_tags(CategoryId::new(125))
1862 .search_text("gdp")
1863 .send()
1864 .await
1865 .expect("category/tags parse");
1866 assert_eq!(results.count, 1);
1867 assert_eq!(results.tags[0].name, "gdp");
1868 }
1869
1870 #[tokio::test]
1871 async fn category_related_tags_send_scope_and_seed_and_parse() {
1872 let server = MockServer::start().await;
1873 Mock::given(method("GET"))
1874 .and(path("/category/related_tags"))
1875 .and(query_param("category_id", "125"))
1876 .and(query_param("tag_names", "gdp;quarterly"))
1877 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1878 .mount(&server)
1879 .await;
1880
1881 let results = client_for(&server)
1882 .category_related_tags(CategoryId::new(125), ["gdp", "quarterly"])
1883 .send()
1884 .await
1885 .expect("category/related_tags parse");
1886 assert_eq!(results.count, 1);
1887 }
1888
1889 #[tokio::test]
1890 async fn release_tags_send_scope_and_parse() {
1891 let server = MockServer::start().await;
1892 Mock::given(method("GET"))
1893 .and(path("/release/tags"))
1894 .and(query_param("release_id", "53"))
1895 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1896 .mount(&server)
1897 .await;
1898
1899 let results = client_for(&server)
1900 .release_tags(ReleaseId::new(53))
1901 .send()
1902 .await
1903 .expect("release/tags parse");
1904 assert_eq!(results.count, 1);
1905 }
1906
1907 #[tokio::test]
1908 async fn release_related_tags_send_scope_and_seed_and_parse() {
1909 let server = MockServer::start().await;
1910 Mock::given(method("GET"))
1911 .and(path("/release/related_tags"))
1912 .and(query_param("release_id", "53"))
1913 .and(query_param("tag_names", "gdp"))
1914 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1915 .mount(&server)
1916 .await;
1917
1918 let results = client_for(&server)
1919 .release_related_tags(ReleaseId::new(53), ["gdp"])
1920 .send()
1921 .await
1922 .expect("release/related_tags parse");
1923 assert_eq!(results.count, 1);
1924 }
1925
1926 #[tokio::test]
1927 async fn series_search_tags_send_scope_and_tag_search_text() {
1928 let server = MockServer::start().await;
1929 Mock::given(method("GET"))
1932 .and(path("/series/search/tags"))
1933 .and(query_param("series_search_text", "unemployment"))
1934 .and(query_param("tag_search_text", "rate"))
1935 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1936 .mount(&server)
1937 .await;
1938
1939 let results = client_for(&server)
1940 .series_search_tags("unemployment")
1941 .search_text("rate")
1942 .send()
1943 .await
1944 .expect("series/search/tags parse");
1945 assert_eq!(results.count, 1);
1946 }
1947
1948 #[tokio::test]
1949 async fn series_search_related_tags_send_scope_and_seed() {
1950 let server = MockServer::start().await;
1951 Mock::given(method("GET"))
1952 .and(path("/series/search/related_tags"))
1953 .and(query_param("series_search_text", "unemployment"))
1954 .and(query_param("tag_names", "monthly"))
1955 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1956 .mount(&server)
1957 .await;
1958
1959 let results = client_for(&server)
1960 .series_search_related_tags("unemployment", ["monthly"])
1961 .send()
1962 .await
1963 .expect("series/search/related_tags parse");
1964 assert_eq!(results.count, 1);
1965 }
1966
1967 #[tokio::test]
1968 async fn tags_series_joins_names_and_parses() {
1969 let server = MockServer::start().await;
1970 Mock::given(method("GET"))
1972 .and(path("/tags/series"))
1973 .and(query_param("tag_names", "gdp;quarterly"))
1974 .and(query_param("limit", "2"))
1975 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1976 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1977 )))
1978 .mount(&server)
1979 .await;
1980
1981 let results = client_for(&server)
1982 .tags_series(["gdp", "quarterly"])
1983 .limit(2)
1984 .send()
1985 .await
1986 .expect("tags/series parse");
1987 assert_eq!(results.count, 1);
1988 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1989 }
1990
1991 #[tokio::test]
1992 async fn series_tags_parses() {
1993 let server = MockServer::start().await;
1994 Mock::given(method("GET"))
1995 .and(path("/series/tags"))
1996 .and(query_param("series_id", "GNPCA"))
1997 .respond_with(ResponseTemplate::new(200).set_body_string(
1998 r#"{"count":2,"offset":0,"limit":1000,"tags":[
1999 {"name":"gnp","group_id":"gen","popularity":50,"series_count":10},
2000 {"name":"usa","group_id":"geo","notes":null,"popularity":100,"series_count":500}
2001 ]}"#,
2002 ))
2003 .mount(&server)
2004 .await;
2005
2006 let results = client_for(&server)
2007 .series_tags(&SeriesId::new("GNPCA"))
2008 .await
2009 .expect("series/tags parse");
2010 assert_eq!(results.count, 2);
2011 assert_eq!(results.tags[0].name, "gnp");
2012 assert!(results.tags[1].notes.is_none());
2013 }
2014
2015 #[tokio::test]
2016 async fn sources_parse_with_pagination() {
2017 let server = MockServer::start().await;
2018 Mock::given(method("GET"))
2019 .and(path("/sources"))
2020 .respond_with(ResponseTemplate::new(200).set_body_string(
2021 r#"{"count":2,"offset":0,"limit":1000,"sources":[
2022 {"id":1,"name":"Board of Governors of the Federal Reserve System (US)"},
2023 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}
2024 ]}"#,
2025 ))
2026 .mount(&server)
2027 .await;
2028
2029 let results = client_for(&server)
2030 .sources()
2031 .send()
2032 .await
2033 .expect("sources parse");
2034 assert_eq!(results.count, 2);
2035 assert_eq!(results.sources[1].id, SourceId::new(18));
2036 assert_eq!(results.sources[1].link.as_deref(), Some("http://bea.gov"));
2037 }
2038
2039 #[tokio::test]
2040 async fn source_parses() {
2041 let server = MockServer::start().await;
2042 Mock::given(method("GET"))
2043 .and(path("/source"))
2044 .and(query_param("source_id", "18"))
2045 .respond_with(ResponseTemplate::new(200).set_body_string(
2046 r#"{"sources":[{"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}]}"#,
2047 ))
2048 .mount(&server)
2049 .await;
2050
2051 let source = client_for(&server)
2052 .source(SourceId::new(18))
2053 .await
2054 .expect("source parses");
2055 assert_eq!(source.id, SourceId::new(18));
2056 assert_eq!(source.name, "U.S. Bureau of Economic Analysis");
2057 }
2058
2059 #[tokio::test]
2060 async fn source_releases_send_source_id_and_parse() {
2061 let server = MockServer::start().await;
2062 Mock::given(method("GET"))
2064 .and(path("/source/releases"))
2065 .and(query_param("source_id", "18"))
2066 .and(query_param("limit", "2"))
2067 .respond_with(ResponseTemplate::new(200).set_body_string(
2068 r#"{"count":1,"offset":0,"limit":2,"releases":[
2069 {"id":53,"name":"Gross Domestic Product","press_release":true}
2070 ]}"#,
2071 ))
2072 .mount(&server)
2073 .await;
2074
2075 let results = client_for(&server)
2076 .source_releases(SourceId::new(18))
2077 .limit(2)
2078 .send()
2079 .await
2080 .expect("source/releases parse");
2081 assert_eq!(results.count, 1);
2082 assert_eq!(results.releases[0].id, ReleaseId::new(53));
2083 }
2084
2085 #[tokio::test]
2086 async fn release_sources_send_release_id_and_parse() {
2087 let server = MockServer::start().await;
2088 Mock::given(method("GET"))
2091 .and(path("/release/sources"))
2092 .and(query_param("release_id", "51"))
2093 .respond_with(ResponseTemplate::new(200).set_body_string(
2094 r#"{"realtime_start":"2013-08-14","realtime_end":"2013-08-14","sources":[
2095 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://www.bea.gov/"},
2096 {"id":19,"name":"U.S. Census Bureau"}
2097 ]}"#,
2098 ))
2099 .mount(&server)
2100 .await;
2101
2102 let sources = client_for(&server)
2103 .release_sources(ReleaseId::new(51))
2104 .await
2105 .expect("release/sources parse");
2106 assert_eq!(sources.len(), 2);
2107 assert_eq!(sources[0].id, SourceId::new(18));
2108 assert_eq!(sources[0].link.as_deref(), Some("http://www.bea.gov/"));
2109 assert!(sources[1].link.is_none());
2110 }
2111
2112 #[tokio::test]
2113 async fn releases_dates_send_params_and_parse() {
2114 let server = MockServer::start().await;
2115 Mock::given(method("GET"))
2117 .and(path("/releases/dates"))
2118 .and(query_param("sort_order", "desc"))
2119 .and(query_param("limit", "2"))
2120 .respond_with(ResponseTemplate::new(200).set_body_string(
2121 r#"{"count":2,"offset":0,"limit":2,"release_dates":[
2122 {"release_id":9,"release_name":"Advance Monthly Sales","date":"2013-08-13"},
2123 {"release_id":10,"release_name":"Consumer Price Index","date":"2013-08-15"}
2124 ]}"#,
2125 ))
2126 .mount(&server)
2127 .await;
2128
2129 let results = client_for(&server)
2130 .releases_dates()
2131 .sort_order(SortOrder::Descending)
2132 .limit(2)
2133 .send()
2134 .await
2135 .expect("releases/dates parse");
2136 assert_eq!(results.count, 2);
2137 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(9));
2138 assert_eq!(
2139 results.release_dates[0].release_name.as_deref(),
2140 Some("Advance Monthly Sales")
2141 );
2142 }
2143
2144 #[tokio::test]
2145 async fn release_dates_send_release_id_and_include_flag_and_parse() {
2146 let server = MockServer::start().await;
2147 Mock::given(method("GET"))
2150 .and(path("/release/dates"))
2151 .and(query_param("release_id", "82"))
2152 .and(query_param("include_release_dates_with_no_data", "true"))
2153 .respond_with(ResponseTemplate::new(200).set_body_string(
2154 r#"{"count":2,"offset":0,"limit":10000,"release_dates":[
2155 {"release_id":82,"date":"1997-02-10"},
2156 {"release_id":82,"date":"1998-02-10"}
2157 ]}"#,
2158 ))
2159 .mount(&server)
2160 .await;
2161
2162 let results = client_for(&server)
2163 .release_dates(ReleaseId::new(82))
2164 .include_dates_with_no_data(true)
2165 .send()
2166 .await
2167 .expect("release/dates parse");
2168 assert_eq!(results.count, 2);
2169 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(82));
2170 assert!(results.release_dates[0].release_name.is_none());
2171 }
2172
2173 #[tokio::test]
2174 async fn release_tables_send_element_and_parse_tree() {
2175 let server = MockServer::start().await;
2176 Mock::given(method("GET"))
2179 .and(path("/release/tables"))
2180 .and(query_param("release_id", "10"))
2181 .and(query_param("element_id", "34483"))
2182 .respond_with(ResponseTemplate::new(200).set_body_string(
2183 r#"{"name":"Monthly, SA","element_id":34483,"release_id":"10","elements":{
2184 "34484":{"element_id":34484,"release_id":10,"parent_id":34483,
2185 "series_id":"","type":"series","name":"All items","line":"1","level":"0",
2186 "children":[
2187 {"element_id":34485,"release_id":10,"parent_id":34484,
2188 "series_id":"CPIFABSL","type":"series","name":"Food",
2189 "line":"2","level":"1","children":[]}
2190 ]}
2191 }}"#,
2192 ))
2193 .mount(&server)
2194 .await;
2195
2196 let table = client_for(&server)
2197 .release_tables(ReleaseId::new(10))
2198 .element(ReleaseElementId::new(34483))
2199 .send()
2200 .await
2201 .expect("release/tables parse");
2202 assert_eq!(table.name.as_deref(), Some("Monthly, SA"));
2203 assert_eq!(table.roots.len(), 1);
2204 let leaf = &table.roots[0].children[0];
2205 assert_eq!(
2206 leaf.series_id.as_ref().map(|s| s.as_str()),
2207 Some("CPIFABSL")
2208 );
2209 }
2210
2211 #[tokio::test]
2212 async fn release_tables_observation_values_reach_wire_and_parse() {
2213 let server = MockServer::start().await;
2214 Mock::given(method("GET"))
2218 .and(path("/release/tables"))
2219 .and(query_param("release_id", "10"))
2220 .and(query_param("include_observation_values", "true"))
2221 .and(query_param("observation_date", "2023-06-01"))
2222 .respond_with(ResponseTemplate::new(200).set_body_string(
2223 r#"{"release_id":"10","elements":{
2224 "36715":{"element_id":36715,"release_id":10,"parent_id":36714,
2225 "series_id":"CUSR0000SA0L5","type":"series","name":"All items",
2226 "level":"1","observation_value":"292.260","observation_date":"Jun 2023",
2227 "children":[]}
2228 }}"#,
2229 ))
2230 .mount(&server)
2231 .await;
2232
2233 let table = client_for(&server)
2234 .release_tables(ReleaseId::new(10))
2235 .observation_date(chrono::NaiveDate::from_ymd_opt(2023, 6, 1).unwrap())
2236 .send()
2237 .await
2238 .expect("release/tables with values parse");
2239 let row = &table.roots[0];
2240 assert_eq!(row.observation_value, Some(292.260));
2241 assert_eq!(row.observation_date.as_deref(), Some("Jun 2023"));
2242 }
2243
2244 #[tokio::test]
2245 async fn series_categories_parse() {
2246 let server = MockServer::start().await;
2247 Mock::given(method("GET"))
2248 .and(path("/series/categories"))
2249 .and(query_param("series_id", "GNPCA"))
2250 .respond_with(ResponseTemplate::new(200).set_body_string(
2251 r#"{"categories":[
2252 {"id":106,"name":"Gross National Product","parent_id":18},
2253 {"id":18,"name":"National Income & Product Accounts","parent_id":13}
2254 ]}"#,
2255 ))
2256 .mount(&server)
2257 .await;
2258
2259 let categories = client_for(&server)
2260 .series_categories(&SeriesId::new("GNPCA"))
2261 .await
2262 .expect("series/categories parse");
2263 assert_eq!(categories.len(), 2);
2264 assert_eq!(categories[0].id, CategoryId::new(106));
2265 }
2266
2267 #[tokio::test]
2268 async fn series_release_parses() {
2269 let server = MockServer::start().await;
2270 Mock::given(method("GET"))
2271 .and(path("/series/release"))
2272 .and(query_param("series_id", "GNPCA"))
2273 .respond_with(ResponseTemplate::new(200).set_body_string(
2274 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
2275 ))
2276 .mount(&server)
2277 .await;
2278
2279 let release = client_for(&server)
2280 .series_release(&SeriesId::new("GNPCA"))
2281 .await
2282 .expect("series/release parse");
2283 assert_eq!(release.id, ReleaseId::new(53));
2284 assert_eq!(release.name, "Gross Domestic Product");
2285 }
2286
2287 #[tokio::test]
2288 async fn series_updates_sends_filter_and_parses() {
2289 let server = MockServer::start().await;
2290 Mock::given(method("GET"))
2291 .and(path("/series/updates"))
2292 .and(query_param("filter_value", "macro"))
2293 .and(query_param("limit", "2"))
2294 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2295 "{{\"count\":5,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
2296 )))
2297 .mount(&server)
2298 .await;
2299
2300 let results = client_for(&server)
2301 .series_updates()
2302 .filter(UpdatesFilter::Macro)
2303 .limit(2)
2304 .send()
2305 .await
2306 .expect("series/updates parse");
2307 assert_eq!(results.count, 5);
2308 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
2309 }
2310
2311 #[tokio::test]
2312 async fn series_updates_sends_time_window_as_yyyymmddhhmm() {
2313 use chrono::NaiveDate;
2314 let server = MockServer::start().await;
2315 Mock::given(method("GET"))
2316 .and(path("/series/updates"))
2317 .and(query_param("start_time", "201803021420"))
2318 .and(query_param("end_time", "201803030905"))
2319 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2320 "{{\"count\":1,\"offset\":0,\"limit\":1,\"seriess\":[{SERIES_OBJECT}]}}"
2321 )))
2322 .mount(&server)
2323 .await;
2324
2325 let start = NaiveDate::from_ymd_opt(2018, 3, 2)
2326 .unwrap()
2327 .and_hms_opt(14, 20, 0)
2328 .unwrap();
2329 let end = NaiveDate::from_ymd_opt(2018, 3, 3)
2330 .unwrap()
2331 .and_hms_opt(9, 5, 0)
2332 .unwrap();
2333 let results = client_for(&server)
2334 .series_updates()
2335 .time_window(start, end)
2336 .send()
2337 .await
2338 .expect("series/updates time-window parse");
2339 assert_eq!(results.count, 1);
2340 }
2341
2342 #[tokio::test]
2343 async fn series_vintagedates_send_id_and_parse() {
2344 let server = MockServer::start().await;
2345 Mock::given(method("GET"))
2346 .and(path("/series/vintagedates"))
2347 .and(query_param("series_id", "GNPCA"))
2348 .and(query_param("limit", "2"))
2349 .respond_with(ResponseTemplate::new(200).set_body_string(
2350 r#"{"count":3,"offset":0,"limit":2,"vintage_dates":["1958-12-21","1959-02-19"]}"#,
2351 ))
2352 .mount(&server)
2353 .await;
2354
2355 let dates = client_for(&server)
2356 .series_vintagedates(&SeriesId::new("GNPCA"))
2357 .limit(2)
2358 .send()
2359 .await
2360 .expect("series/vintagedates parse");
2361 assert_eq!(dates.count, 3);
2362 assert_eq!(dates.vintage_dates.len(), 2);
2363 assert_eq!(
2364 dates.vintage_dates[0],
2365 chrono::NaiveDate::from_ymd_opt(1958, 12, 21).unwrap()
2366 );
2367 }
2368
2369 #[tokio::test]
2372 async fn geofred_regional_data_sends_all_params_and_parses() {
2373 let server = MockServer::start().await;
2374 Mock::given(method("GET"))
2378 .and(path("/regional/data"))
2379 .and(query_param("series_group", "882"))
2380 .and(query_param("region_type", "state"))
2381 .and(query_param("date", "2013-01-01"))
2382 .and(query_param("units", "Dollars"))
2383 .and(query_param("frequency", "a"))
2384 .and(query_param("season", "NSA"))
2385 .respond_with(ResponseTemplate::new(200).set_body_string(
2386 r#"{"meta":{"title":"t","region":"state","seasonality":"Not Seasonally Adjusted",
2387 "units":"Dollars","frequency":"Annual","data":{"2013-01-01":[
2388 {"region":"Alabama","code":"01","value":35706,"series_id":"ALPCPI"}
2389 ]}}}"#,
2390 ))
2391 .mount(&server)
2392 .await;
2393
2394 let data = client_for(&server)
2395 .regional_data(
2396 &SeriesGroupId::new("882"),
2397 RegionType::State,
2398 chrono::NaiveDate::from_ymd_opt(2013, 1, 1).unwrap(),
2399 "Dollars",
2400 Frequency::Annual,
2401 SeasonalAdjustment::NotSeasonallyAdjusted,
2402 )
2403 .await
2404 .expect("regional data parses");
2405 let day = &data.meta.data["2013-01-01"];
2406 assert_eq!(day[0].region, "Alabama");
2407 assert_eq!(day[0].value, Some(35706.0));
2408 assert_eq!(day[0].series_id, SeriesId::new("ALPCPI"));
2409 }
2410
2411 #[tokio::test]
2412 async fn geofred_series_data_sends_optional_date_and_parses() {
2413 let server = MockServer::start().await;
2414 Mock::given(method("GET"))
2415 .and(path("/series/data"))
2416 .and(query_param("series_id", "SMU56000000500000001"))
2417 .and(query_param("date", "2013-01-01"))
2418 .respond_with(ResponseTemplate::new(200).set_body_string(
2419 r#"{"meta":{"title":"t","region":"state","seasonality":"Not Seasonally Adjusted",
2420 "units":"Thousands of Persons","frequency":"Monthly","data":{"2013-01-01":[
2421 {"region":"Alabama","code":"01","value":1506.5,"series_id":"SMU01000000500000001"}
2422 ]}}}"#,
2423 ))
2424 .mount(&server)
2425 .await;
2426
2427 let data = client_for(&server)
2428 .series_data(&SeriesId::new("SMU56000000500000001"))
2429 .date(chrono::NaiveDate::from_ymd_opt(2013, 1, 1).unwrap())
2430 .send()
2431 .await
2432 .expect("series data parses");
2433 assert_eq!(data.meta.data["2013-01-01"][0].value, Some(1506.5));
2434 }
2435
2436 #[tokio::test]
2437 async fn geofred_series_group_unwraps_envelope_and_parses() {
2438 let server = MockServer::start().await;
2439 Mock::given(method("GET"))
2440 .and(path("/series/group"))
2441 .and(query_param("series_id", "SMU56000000500000001"))
2442 .respond_with(ResponseTemplate::new(200).set_body_string(
2443 r#"{"series_group":{"title":"All Employees: Total Private","region_type":"state",
2444 "series_group":"1223","season":"NSA","units":"Thousands of Persons",
2445 "frequency":"Monthly","min_date":"1990-01-01","max_date":"2026-05-01"}}"#,
2446 ))
2447 .mount(&server)
2448 .await;
2449
2450 let group = client_for(&server)
2451 .series_group(&SeriesId::new("SMU56000000500000001"))
2452 .await
2453 .expect("series group parses");
2454 assert_eq!(group.id, SeriesGroupId::new("1223"));
2455 assert_eq!(group.region_type, "state");
2456 }
2457
2458 #[tokio::test]
2459 async fn geofred_bad_series_500_becomes_actionable_error() {
2460 let server = MockServer::start().await;
2465 Mock::given(method("GET"))
2466 .and(path("/series/group"))
2467 .respond_with(
2468 ResponseTemplate::new(500).set_body_string(
2469 r#"{"error_code":500,"error_message":"Internal Server Error"}"#,
2470 ),
2471 )
2472 .mount(&server)
2473 .await;
2474
2475 let error = client_for(&server)
2476 .series_group(&SeriesId::new("GNPCA"))
2477 .await
2478 .expect_err("a non-regional series should error");
2479 match error {
2480 Error::Api {
2481 status,
2482 code,
2483 message,
2484 } => {
2485 assert_eq!(status, 500);
2486 assert_eq!(code, Some(500));
2487 assert_ne!(
2488 message, "Internal Server Error",
2489 "the generic message should be rewritten"
2490 );
2491 assert!(
2492 message.contains("series_id=GNPCA"),
2493 "message was {message:?}"
2494 );
2495 assert!(
2496 message.contains("regional"),
2497 "message should point at the non-regional cause: {message:?}"
2498 );
2499 }
2500 other => panic!("expected Error::Api, got {other:?}"),
2501 }
2502 }
2503
2504 #[tokio::test]
2505 async fn geofred_shape_file_sends_shape_and_parses_geojson() {
2506 let server = MockServer::start().await;
2507 Mock::given(method("GET"))
2508 .and(path("/shapes/file"))
2509 .and(query_param("shape", "bea"))
2510 .respond_with(ResponseTemplate::new(200).set_body_string(
2511 r#"{"type":"FeatureCollection","name":"state_bea_region","features":[
2512 {"type":"Feature","properties":{"bea_region":8},
2513 "geometry":{"type":"MultiPolygon","coordinates":[[[[1485,2651]]]]}}
2514 ]}"#,
2515 ))
2516 .mount(&server)
2517 .await;
2518
2519 let shapes = client_for(&server)
2520 .shape_file(ShapeType::Bea)
2521 .await
2522 .expect("shape file parses");
2523 assert_eq!(shapes.kind, "FeatureCollection");
2524 assert_eq!(shapes.features[0].geometry.kind, "MultiPolygon");
2525 }
2526}