1use std::time::Duration;
2
3use serde::de::DeserializeOwned;
4use serde::Deserialize;
5
6use crate::{
7 Category, CategoryId, Error, Observation, ObservationsRequest, Release, ReleaseDatesRequest,
8 ReleaseDatesResults, ReleaseId, ReleaseTable, ReleaseTablesRequest, ReleasesRequest,
9 ReleasesResults, Result, Series, SeriesId, SeriesListRequest, SeriesSearchRequest,
10 SeriesSearchResults, SeriesUpdatesRequest, Source, SourceId, SourcesRequest, SourcesResults,
11 TagsRequest, TagsResults, VintageDates, VintageDatesRequest,
12};
13
14const FRED_BASE_URL: &str = "https://api.stlouisfed.org/fred";
16
17#[derive(Debug, Clone)]
22pub struct Client {
23 http: reqwest::Client,
24 api_key: String,
25 base_url: String,
26}
27
28impl Client {
29 pub fn new(api_key: impl Into<String>) -> Result<Self> {
35 let http = reqwest::Client::builder().build()?;
36 Ok(Self {
37 http,
38 api_key: api_key.into(),
39 base_url: FRED_BASE_URL.to_owned(),
40 })
41 }
42
43 #[cfg(test)]
46 pub(crate) fn with_base_url(
47 api_key: impl Into<String>,
48 base_url: impl Into<String>,
49 ) -> Result<Self> {
50 Ok(Self {
51 http: reqwest::Client::builder().build()?,
52 api_key: api_key.into(),
53 base_url: base_url.into(),
54 })
55 }
56
57 pub fn from_env() -> Result<Self> {
65 let api_key = std::env::var("FRED_API_KEY").map_err(|_| {
66 Error::InvalidInput("FRED_API_KEY environment variable is not set".to_owned())
67 })?;
68 Self::new(api_key)
69 }
70
71 pub fn observations(&self, series_id: &SeriesId) -> ObservationsRequest<'_> {
91 ObservationsRequest::new(self, series_id.clone())
92 }
93
94 pub(crate) async fn execute_observations(
96 &self,
97 request: &ObservationsRequest<'_>,
98 ) -> Result<Vec<Observation>> {
99 let response: ObservationsResponse = self
100 .get("/series/observations", &request.query_params())
101 .await?;
102 Ok(response.observations)
103 }
104
105 pub async fn series(&self, series_id: &SeriesId) -> Result<Series> {
112 let response: SeriesResponse = self
113 .get("/series", &[("series_id", series_id.as_str().to_owned())])
114 .await?;
115 response
116 .seriess
117 .into_iter()
118 .next()
119 .ok_or_else(|| Error::Api {
120 status: 200,
121 code: None,
122 message: format!("FRED returned no series for id `{series_id}`"),
123 })
124 }
125
126 pub fn search(&self, search_text: impl Into<String>) -> SeriesSearchRequest<'_> {
145 SeriesSearchRequest::new(self, search_text.into())
146 }
147
148 pub(crate) async fn execute_search(
150 &self,
151 request: &SeriesSearchRequest<'_>,
152 ) -> Result<SeriesSearchResults> {
153 self.get("/series/search", &request.query_params()).await
154 }
155
156 pub fn series_search_tags(&self, search_text: impl Into<String>) -> TagsRequest<'_> {
172 TagsRequest::scoped(
173 self,
174 "/series/search/tags",
175 ("series_search_text", search_text.into()),
176 None,
177 "tag_search_text",
178 )
179 }
180
181 pub fn series_search_related_tags<I, S>(
201 &self,
202 search_text: impl Into<String>,
203 tag_names: I,
204 ) -> TagsRequest<'_>
205 where
206 I: IntoIterator<Item = S>,
207 S: AsRef<str>,
208 {
209 TagsRequest::scoped(
210 self,
211 "/series/search/related_tags",
212 ("series_search_text", search_text.into()),
213 Some(join_tag_names(tag_names)),
214 "tag_search_text",
215 )
216 }
217
218 pub async fn category(&self, category_id: CategoryId) -> Result<Category> {
226 let response: CategoriesResponse = self
227 .get(
228 "/category",
229 &[("category_id", category_id.get().to_string())],
230 )
231 .await?;
232 response
233 .categories
234 .into_iter()
235 .next()
236 .ok_or_else(|| Error::Api {
237 status: 200,
238 code: None,
239 message: format!("FRED returned no category for id `{category_id}`"),
240 })
241 }
242
243 pub async fn category_children(&self, category_id: CategoryId) -> Result<Vec<Category>> {
251 let response: CategoriesResponse = self
252 .get(
253 "/category/children",
254 &[("category_id", category_id.get().to_string())],
255 )
256 .await?;
257 Ok(response.categories)
258 }
259
260 pub async fn category_related(&self, category_id: CategoryId) -> Result<Vec<Category>> {
270 let response: CategoriesResponse = self
271 .get(
272 "/category/related",
273 &[("category_id", category_id.get().to_string())],
274 )
275 .await?;
276 Ok(response.categories)
277 }
278
279 pub fn category_series(&self, category_id: CategoryId) -> SeriesListRequest<'_> {
298 SeriesListRequest::new(
299 self,
300 "/category/series",
301 "category_id",
302 category_id.get().to_string(),
303 )
304 }
305
306 pub fn category_tags(&self, category_id: CategoryId) -> TagsRequest<'_> {
322 TagsRequest::scoped(
323 self,
324 "/category/tags",
325 ("category_id", category_id.get().to_string()),
326 None,
327 "search_text",
328 )
329 }
330
331 pub fn category_related_tags<I, S>(
348 &self,
349 category_id: CategoryId,
350 tag_names: I,
351 ) -> TagsRequest<'_>
352 where
353 I: IntoIterator<Item = S>,
354 S: AsRef<str>,
355 {
356 TagsRequest::scoped(
357 self,
358 "/category/related_tags",
359 ("category_id", category_id.get().to_string()),
360 Some(join_tag_names(tag_names)),
361 "search_text",
362 )
363 }
364
365 pub fn releases(&self) -> ReleasesRequest<'_> {
379 ReleasesRequest::new(self, "/releases")
380 }
381
382 pub(crate) async fn execute_releases(
385 &self,
386 request: &ReleasesRequest<'_>,
387 ) -> Result<ReleasesResults> {
388 self.get(request.path(), &request.query_params()).await
389 }
390
391 pub fn releases_dates(&self) -> ReleaseDatesRequest<'_> {
407 ReleaseDatesRequest::new(self, "/releases/dates")
408 }
409
410 pub(crate) async fn execute_release_dates(
413 &self,
414 request: &ReleaseDatesRequest<'_>,
415 ) -> Result<ReleaseDatesResults> {
416 self.get(request.path(), &request.query_params()).await
417 }
418
419 pub async fn release(&self, release_id: ReleaseId) -> Result<Release> {
426 let response: ReleaseResponse = self
427 .get("/release", &[("release_id", release_id.get().to_string())])
428 .await?;
429 response
430 .releases
431 .into_iter()
432 .next()
433 .ok_or_else(|| Error::Api {
434 status: 200,
435 code: None,
436 message: format!("FRED returned no release for id `{release_id}`"),
437 })
438 }
439
440 pub fn release_series(&self, release_id: ReleaseId) -> SeriesListRequest<'_> {
459 SeriesListRequest::new(
460 self,
461 "/release/series",
462 "release_id",
463 release_id.get().to_string(),
464 )
465 }
466
467 pub async fn release_sources(&self, release_id: ReleaseId) -> Result<Vec<Source>> {
476 let response: SourceResponse = self
477 .get(
478 "/release/sources",
479 &[("release_id", release_id.get().to_string())],
480 )
481 .await?;
482 Ok(response.sources)
483 }
484
485 pub fn release_dates(&self, release_id: ReleaseId) -> ReleaseDatesRequest<'_> {
502 ReleaseDatesRequest::with_release(self, "/release/dates", release_id.get().to_string())
503 }
504
505 pub fn release_tables(&self, release_id: ReleaseId) -> ReleaseTablesRequest<'_> {
522 ReleaseTablesRequest::new(self, release_id.get())
523 }
524
525 pub(crate) async fn execute_release_tables(
527 &self,
528 request: &ReleaseTablesRequest<'_>,
529 ) -> Result<ReleaseTable> {
530 self.get("/release/tables", &request.query_params()).await
531 }
532
533 pub fn release_tags(&self, release_id: ReleaseId) -> TagsRequest<'_> {
549 TagsRequest::scoped(
550 self,
551 "/release/tags",
552 ("release_id", release_id.get().to_string()),
553 None,
554 "search_text",
555 )
556 }
557
558 pub fn release_related_tags<I, S>(&self, release_id: ReleaseId, tag_names: I) -> TagsRequest<'_>
574 where
575 I: IntoIterator<Item = S>,
576 S: AsRef<str>,
577 {
578 TagsRequest::scoped(
579 self,
580 "/release/related_tags",
581 ("release_id", release_id.get().to_string()),
582 Some(join_tag_names(tag_names)),
583 "search_text",
584 )
585 }
586
587 pub fn tags(&self) -> TagsRequest<'_> {
601 TagsRequest::new(self, "/tags")
602 }
603
604 pub fn related_tags<I, S>(&self, tag_names: I) -> TagsRequest<'_>
620 where
621 I: IntoIterator<Item = S>,
622 S: AsRef<str>,
623 {
624 TagsRequest::with_tag_names(self, "/related_tags", join_tag_names(tag_names))
625 }
626
627 pub(crate) async fn execute_tags(&self, request: &TagsRequest<'_>) -> Result<TagsResults> {
630 self.get(request.path(), &request.query_params()).await
631 }
632
633 pub fn tags_series<I, S>(&self, tag_names: I) -> SeriesListRequest<'_>
648 where
649 I: IntoIterator<Item = S>,
650 S: AsRef<str>,
651 {
652 SeriesListRequest::new(self, "/tags/series", "tag_names", join_tag_names(tag_names))
653 }
654
655 pub(crate) async fn execute_series_list(
658 &self,
659 request: &SeriesListRequest<'_>,
660 ) -> Result<SeriesSearchResults> {
661 self.get(request.path(), &request.query_params()).await
662 }
663
664 pub async fn series_tags(&self, series_id: &SeriesId) -> Result<TagsResults> {
672 self.get(
673 "/series/tags",
674 &[("series_id", series_id.as_str().to_owned())],
675 )
676 .await
677 }
678
679 pub fn series_updates(&self) -> SeriesUpdatesRequest<'_> {
694 SeriesUpdatesRequest::new(self)
695 }
696
697 pub(crate) async fn execute_series_updates(
699 &self,
700 request: &SeriesUpdatesRequest<'_>,
701 ) -> Result<SeriesSearchResults> {
702 self.get("/series/updates", &request.query_params()).await
703 }
704
705 pub fn series_vintagedates(&self, series_id: &SeriesId) -> VintageDatesRequest<'_> {
725 VintageDatesRequest::new(self, series_id.clone())
726 }
727
728 pub(crate) async fn execute_vintage_dates(
731 &self,
732 request: &VintageDatesRequest<'_>,
733 ) -> Result<VintageDates> {
734 self.get("/series/vintagedates", &request.query_params())
735 .await
736 }
737
738 pub async fn series_categories(&self, series_id: &SeriesId) -> Result<Vec<Category>> {
746 let response: CategoriesResponse = self
747 .get(
748 "/series/categories",
749 &[("series_id", series_id.as_str().to_owned())],
750 )
751 .await?;
752 Ok(response.categories)
753 }
754
755 pub async fn series_release(&self, series_id: &SeriesId) -> Result<Release> {
763 let response: ReleaseResponse = self
764 .get(
765 "/series/release",
766 &[("series_id", series_id.as_str().to_owned())],
767 )
768 .await?;
769 response
770 .releases
771 .into_iter()
772 .next()
773 .ok_or_else(|| Error::Api {
774 status: 200,
775 code: None,
776 message: format!("FRED returned no release for series `{series_id}`"),
777 })
778 }
779
780 pub fn sources(&self) -> SourcesRequest<'_> {
794 SourcesRequest::new(self)
795 }
796
797 pub(crate) async fn execute_sources(
799 &self,
800 request: &SourcesRequest<'_>,
801 ) -> Result<SourcesResults> {
802 self.get("/sources", &request.query_params()).await
803 }
804
805 pub async fn source(&self, source_id: SourceId) -> Result<Source> {
812 let response: SourceResponse = self
813 .get("/source", &[("source_id", source_id.get().to_string())])
814 .await?;
815 response
816 .sources
817 .into_iter()
818 .next()
819 .ok_or_else(|| Error::Api {
820 status: 200,
821 code: None,
822 message: format!("FRED returned no source for id `{source_id}`"),
823 })
824 }
825
826 pub fn source_releases(&self, source_id: SourceId) -> ReleasesRequest<'_> {
841 ReleasesRequest::with_source(self, "/source/releases", source_id.get().to_string())
842 }
843
844 async fn get<T: DeserializeOwned>(
849 &self,
850 path: &str,
851 params: &[(&'static str, String)],
852 ) -> Result<T> {
853 let mut query: Vec<(&str, String)> = Vec::with_capacity(params.len() + 2);
854 query.push(("api_key", self.api_key.clone()));
855 query.push(("file_type", "json".to_owned()));
856 query.extend(params.iter().cloned());
857
858 let response = self
859 .http
860 .get(format!("{}{}", self.base_url, path))
861 .query(&query)
862 .send()
863 .await?;
864
865 let status = response.status();
866 let retry_after = parse_retry_after(response.headers());
868 let body = response.bytes().await?;
869
870 if !status.is_success() {
871 return Err(api_error(status, retry_after, &body));
872 }
873
874 serde_json::from_slice(&body).map_err(Error::from)
875 }
876}
877
878fn join_tag_names<I, S>(tag_names: I) -> String
881where
882 I: IntoIterator<Item = S>,
883 S: AsRef<str>,
884{
885 tag_names
886 .into_iter()
887 .map(|name| name.as_ref().to_owned())
888 .collect::<Vec<_>>()
889 .join(";")
890}
891
892fn api_error(status: reqwest::StatusCode, retry_after: Option<Duration>, body: &[u8]) -> Error {
896 let fred: Option<FredErrorBody> = serde_json::from_slice(body).ok();
897 let code = fred.as_ref().and_then(|e| e.error_code);
898 let message = fred.and_then(|e| e.error_message).unwrap_or_else(|| {
899 status
900 .canonical_reason()
901 .unwrap_or("unknown error")
902 .to_owned()
903 });
904
905 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
906 return Error::RateLimited { retry_after };
907 }
908
909 Error::Api {
910 status: status.as_u16(),
911 code,
912 message,
913 }
914}
915
916fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
920 let seconds: u64 = headers
921 .get(reqwest::header::RETRY_AFTER)?
922 .to_str()
923 .ok()?
924 .trim()
925 .parse()
926 .ok()?;
927 Some(Duration::from_secs(seconds))
928}
929
930#[derive(Deserialize)]
933struct ObservationsResponse {
934 observations: Vec<Observation>,
935}
936
937#[derive(Deserialize)]
940struct SeriesResponse {
941 seriess: Vec<Series>,
942}
943
944#[derive(Deserialize)]
946struct CategoriesResponse {
947 categories: Vec<Category>,
948}
949
950#[derive(Deserialize)]
954struct ReleaseResponse {
955 releases: Vec<Release>,
956}
957
958#[derive(Deserialize)]
962struct SourceResponse {
963 sources: Vec<Source>,
964}
965
966#[derive(Deserialize)]
968struct FredErrorBody {
969 error_code: Option<u32>,
970 error_message: Option<String>,
971}
972
973#[cfg(test)]
974mod tests {
975 use super::Client;
976 use std::time::Duration;
977
978 use crate::{
979 CategoryId, Error, Frequency, OrderBy, Paginate, ReleaseElementId, ReleaseId,
980 SeasonalAdjustment, SeriesId, SortOrder, SourceId, Units, UpdatesFilter,
981 };
982 use wiremock::matchers::{method, path, query_param};
983 use wiremock::{Mock, MockServer, ResponseTemplate};
984
985 const SERIES_OBJECT: &str = r#"{
987 "id": "GNPCA",
988 "title": "Real Gross National Product",
989 "observation_start": "1929-01-01",
990 "observation_end": "2023-01-01",
991 "frequency": "Annual",
992 "units": "Billions of Chained 2017 Dollars",
993 "seasonal_adjustment": "Not Seasonally Adjusted",
994 "last_updated": "2024-03-28 07:56:03-05",
995 "popularity": 76,
996 "notes": "BEA Account Code: A001RX"
997 }"#;
998
999 fn client_for(server: &MockServer) -> Client {
1000 Client::with_base_url("test-key", server.uri()).expect("client builds")
1001 }
1002
1003 #[tokio::test]
1004 async fn series_parses_metadata() {
1005 let server = MockServer::start().await;
1006 Mock::given(method("GET"))
1007 .and(path("/series"))
1008 .respond_with(
1009 ResponseTemplate::new(200)
1010 .set_body_string(format!("{{\"seriess\":[{SERIES_OBJECT}]}}")),
1011 )
1012 .mount(&server)
1013 .await;
1014
1015 let series = client_for(&server)
1016 .series(&SeriesId::new("GNPCA"))
1017 .await
1018 .expect("series parses");
1019 assert_eq!(series.id, SeriesId::new("GNPCA"));
1020 assert_eq!(series.frequency, Frequency::Annual);
1021 assert_eq!(
1022 series.seasonal_adjustment,
1023 SeasonalAdjustment::NotSeasonallyAdjusted
1024 );
1025 assert_eq!(series.popularity, 76);
1026 }
1027
1028 #[tokio::test]
1029 async fn observations_parse_missing_and_present_values() {
1030 let server = MockServer::start().await;
1031 let body = r#"{"observations":[
1032 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1930-01-01","value":"."},
1033 {"realtime_start":"2026-07-06","realtime_end":"2026-07-06","date":"1929-01-01","value":"1065.9"}
1034 ]}"#;
1035 Mock::given(method("GET"))
1036 .and(path("/series/observations"))
1037 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1038 .mount(&server)
1039 .await;
1040
1041 let observations = client_for(&server)
1042 .observations(&SeriesId::new("GNPCA"))
1043 .send()
1044 .await
1045 .expect("observations parse");
1046 assert_eq!(observations.len(), 2);
1047 assert_eq!(observations[0].value, None); assert_eq!(observations[1].value, Some(1065.9));
1049 }
1050
1051 #[tokio::test]
1052 async fn observations_point_in_time_sends_realtime_and_parses_period() {
1053 let server = MockServer::start().await;
1054 Mock::given(method("GET"))
1057 .and(path("/series/observations"))
1058 .and(query_param("realtime_start", "2020-01-01"))
1059 .and(query_param("realtime_end", "2020-01-01"))
1060 .respond_with(ResponseTemplate::new(200).set_body_string(
1061 r#"{"observations":[
1062 {"realtime_start":"2020-01-01","realtime_end":"2020-01-01","date":"2017-01-01","value":"18344.563"}
1063 ]}"#,
1064 ))
1065 .mount(&server)
1066 .await;
1067
1068 let as_of = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).unwrap();
1069 let observations = client_for(&server)
1070 .observations(&SeriesId::new("GNPCA"))
1071 .realtime(as_of, as_of)
1072 .send()
1073 .await
1074 .expect("point-in-time observations parse");
1075 assert_eq!(observations[0].realtime_start, as_of);
1076 assert_eq!(observations[0].realtime_end, as_of);
1077 assert_eq!(observations[0].value, Some(18344.563));
1078 }
1079
1080 #[tokio::test]
1081 async fn search_parses_results_with_pagination() {
1082 let server = MockServer::start().await;
1083 let body =
1084 format!("{{\"count\":1,\"offset\":0,\"limit\":1000,\"seriess\":[{SERIES_OBJECT}]}}");
1085 Mock::given(method("GET"))
1086 .and(path("/series/search"))
1087 .respond_with(ResponseTemplate::new(200).set_body_string(body))
1088 .mount(&server)
1089 .await;
1090
1091 let results = client_for(&server)
1092 .search("real gnp")
1093 .send()
1094 .await
1095 .expect("search parses");
1096 assert_eq!(results.count, 1);
1097 assert_eq!(results.series.len(), 1);
1098 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1099 }
1100
1101 #[tokio::test]
1102 async fn error_status_with_body_maps_to_api_error() {
1103 let server = MockServer::start().await;
1104 let body = r#"{"error_code":400,"error_message":"Bad Request. Invalid value for variable series_id."}"#;
1105 Mock::given(method("GET"))
1106 .and(path("/series"))
1107 .respond_with(ResponseTemplate::new(400).set_body_string(body))
1108 .mount(&server)
1109 .await;
1110
1111 let error = client_for(&server)
1112 .series(&SeriesId::new("BAD"))
1113 .await
1114 .expect_err("a 400 should be an API error");
1115 match error {
1116 Error::Api {
1117 status,
1118 code,
1119 message,
1120 } => {
1121 assert_eq!(status, 400);
1122 assert_eq!(code, Some(400));
1123 assert!(message.contains("Invalid value"), "message was {message:?}");
1124 }
1125 other => panic!("expected Error::Api, got {other:?}"),
1126 }
1127 }
1128
1129 #[tokio::test]
1130 async fn too_many_requests_maps_to_rate_limited() {
1131 let server = MockServer::start().await;
1132 Mock::given(method("GET"))
1133 .and(path("/series"))
1134 .respond_with(ResponseTemplate::new(429))
1135 .mount(&server)
1136 .await;
1137
1138 let error = client_for(&server)
1139 .series(&SeriesId::new("GNPCA"))
1140 .await
1141 .expect_err("a 429 should be rate-limited");
1142 assert!(matches!(error, Error::RateLimited { .. }), "got {error:?}");
1143 }
1144
1145 #[tokio::test]
1146 async fn rate_limited_carries_retry_after_seconds() {
1147 let server = MockServer::start().await;
1148 Mock::given(method("GET"))
1149 .and(path("/series"))
1150 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "120"))
1151 .mount(&server)
1152 .await;
1153
1154 let error = client_for(&server)
1155 .series(&SeriesId::new("GNPCA"))
1156 .await
1157 .expect_err("a 429 should be rate-limited");
1158 match error {
1159 Error::RateLimited { retry_after } => {
1160 assert_eq!(retry_after, Some(Duration::from_secs(120)));
1161 }
1162 other => panic!("expected Error::RateLimited, got {other:?}"),
1163 }
1164 }
1165
1166 #[tokio::test]
1167 async fn send_all_walks_every_page() {
1168 let server = MockServer::start().await;
1169 Mock::given(method("GET"))
1173 .and(path("/sources"))
1174 .and(query_param("offset", "0"))
1175 .respond_with(ResponseTemplate::new(200).set_body_string(
1176 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1177 {"id":1,"name":"Source One"},
1178 {"id":2,"name":"Source Two"}
1179 ]}"#,
1180 ))
1181 .mount(&server)
1182 .await;
1183 Mock::given(method("GET"))
1184 .and(path("/sources"))
1185 .and(query_param("offset", "2"))
1186 .respond_with(ResponseTemplate::new(200).set_body_string(
1187 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1188 {"id":3,"name":"Source Three"}
1189 ]}"#,
1190 ))
1191 .mount(&server)
1192 .await;
1193
1194 let sources = client_for(&server)
1195 .sources()
1196 .send_all()
1197 .await
1198 .expect("send_all walks both pages");
1199 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1200 assert_eq!(
1201 ids,
1202 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1203 );
1204 }
1205
1206 #[tokio::test]
1207 async fn send_all_treats_limit_as_a_ceiling() {
1208 let server = MockServer::start().await;
1209 Mock::given(method("GET"))
1213 .and(path("/sources"))
1214 .and(query_param("offset", "0"))
1215 .and(query_param("limit", "2"))
1216 .respond_with(ResponseTemplate::new(200).set_body_string(
1217 r#"{"count":5,"offset":0,"limit":2,"sources":[
1218 {"id":1,"name":"Source One"},
1219 {"id":2,"name":"Source Two"}
1220 ]}"#,
1221 ))
1222 .mount(&server)
1223 .await;
1224
1225 let sources = client_for(&server)
1226 .sources()
1227 .limit(2)
1228 .send_all()
1229 .await
1230 .expect("send_all stops at the ceiling");
1231 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1232 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1233 }
1234
1235 #[tokio::test]
1236 async fn send_all_retries_after_a_429() {
1237 let server = MockServer::start().await;
1238 Mock::given(method("GET"))
1242 .and(path("/sources"))
1243 .respond_with(ResponseTemplate::new(429).insert_header("retry-after", "0"))
1244 .up_to_n_times(1)
1245 .with_priority(1)
1246 .mount(&server)
1247 .await;
1248 Mock::given(method("GET"))
1249 .and(path("/sources"))
1250 .respond_with(ResponseTemplate::new(200).set_body_string(
1251 r#"{"count":1,"offset":0,"limit":1000,"sources":[
1252 {"id":1,"name":"Source One"}
1253 ]}"#,
1254 ))
1255 .with_priority(2)
1256 .mount(&server)
1257 .await;
1258
1259 let sources = client_for(&server)
1260 .sources()
1261 .send_all()
1262 .await
1263 .expect("send_all retries the 429 and then succeeds");
1264 assert_eq!(sources.len(), 1);
1265 assert_eq!(sources[0].id, SourceId::new(1));
1266 }
1267
1268 #[tokio::test]
1269 async fn stream_walks_every_page() {
1270 use futures_util::TryStreamExt;
1271
1272 let server = MockServer::start().await;
1273 Mock::given(method("GET"))
1274 .and(path("/sources"))
1275 .and(query_param("offset", "0"))
1276 .respond_with(ResponseTemplate::new(200).set_body_string(
1277 r#"{"count":3,"offset":0,"limit":1000,"sources":[
1278 {"id":1,"name":"Source One"},
1279 {"id":2,"name":"Source Two"}
1280 ]}"#,
1281 ))
1282 .mount(&server)
1283 .await;
1284 Mock::given(method("GET"))
1285 .and(path("/sources"))
1286 .and(query_param("offset", "2"))
1287 .respond_with(ResponseTemplate::new(200).set_body_string(
1288 r#"{"count":3,"offset":2,"limit":1000,"sources":[
1289 {"id":3,"name":"Source Three"}
1290 ]}"#,
1291 ))
1292 .mount(&server)
1293 .await;
1294
1295 let sources: Vec<_> = client_for(&server)
1296 .sources()
1297 .stream()
1298 .try_collect()
1299 .await
1300 .expect("stream walks both pages");
1301 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1302 assert_eq!(
1303 ids,
1304 vec![SourceId::new(1), SourceId::new(2), SourceId::new(3)]
1305 );
1306 }
1307
1308 #[tokio::test]
1309 async fn stream_treats_limit_as_a_ceiling() {
1310 use futures_util::TryStreamExt;
1311
1312 let server = MockServer::start().await;
1313 Mock::given(method("GET"))
1316 .and(path("/sources"))
1317 .and(query_param("offset", "0"))
1318 .and(query_param("limit", "2"))
1319 .respond_with(ResponseTemplate::new(200).set_body_string(
1320 r#"{"count":5,"offset":0,"limit":2,"sources":[
1321 {"id":1,"name":"Source One"},
1322 {"id":2,"name":"Source Two"}
1323 ]}"#,
1324 ))
1325 .mount(&server)
1326 .await;
1327
1328 let sources: Vec<_> = client_for(&server)
1329 .sources()
1330 .limit(2)
1331 .stream()
1332 .try_collect()
1333 .await
1334 .expect("stream stops at the ceiling");
1335 let ids: Vec<_> = sources.iter().map(|s| s.id).collect();
1336 assert_eq!(ids, vec![SourceId::new(1), SourceId::new(2)]);
1337 }
1338
1339 #[tokio::test]
1340 async fn stream_surfaces_a_mid_stream_error() {
1341 use futures_util::StreamExt;
1342
1343 let server = MockServer::start().await;
1344 Mock::given(method("GET"))
1347 .and(path("/sources"))
1348 .and(query_param("offset", "0"))
1349 .respond_with(ResponseTemplate::new(200).set_body_string(
1350 r#"{"count":4,"offset":0,"limit":1000,"sources":[
1351 {"id":1,"name":"Source One"},
1352 {"id":2,"name":"Source Two"}
1353 ]}"#,
1354 ))
1355 .mount(&server)
1356 .await;
1357 Mock::given(method("GET"))
1358 .and(path("/sources"))
1359 .and(query_param("offset", "2"))
1360 .respond_with(ResponseTemplate::new(500))
1361 .mount(&server)
1362 .await;
1363
1364 let results: Vec<_> = client_for(&server).sources().stream().collect().await;
1365 assert_eq!(results.len(), 3);
1366 assert_eq!(
1367 results[0].as_ref().expect("first item is Ok").id,
1368 SourceId::new(1)
1369 );
1370 assert_eq!(
1371 results[1].as_ref().expect("second item is Ok").id,
1372 SourceId::new(2)
1373 );
1374 assert!(
1375 matches!(results[2], Err(Error::Api { status: 500, .. })),
1376 "third item should be the page-two error, got {:?}",
1377 results[2]
1378 );
1379 }
1380
1381 #[tokio::test]
1382 async fn malformed_body_maps_to_deserialize_error() {
1383 let server = MockServer::start().await;
1384 Mock::given(method("GET"))
1385 .and(path("/series"))
1386 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"unexpected":true}"#))
1387 .mount(&server)
1388 .await;
1389
1390 let error = client_for(&server)
1391 .series(&SeriesId::new("GNPCA"))
1392 .await
1393 .expect_err("an unexpected body should fail to deserialize");
1394 assert!(matches!(error, Error::Deserialize(_)), "got {error:?}");
1395 }
1396
1397 #[tokio::test]
1398 async fn request_carries_api_key_file_type_and_params() {
1399 let server = MockServer::start().await;
1400 Mock::given(method("GET"))
1404 .and(path("/series/observations"))
1405 .and(query_param("api_key", "test-key"))
1406 .and(query_param("file_type", "json"))
1407 .and(query_param("units", "pch"))
1408 .and(query_param("limit", "5"))
1409 .respond_with(ResponseTemplate::new(200).set_body_string(r#"{"observations":[]}"#))
1410 .mount(&server)
1411 .await;
1412
1413 let observations = client_for(&server)
1414 .observations(&SeriesId::new("GNPCA"))
1415 .units(Units::PercentChange)
1416 .limit(5)
1417 .send()
1418 .await
1419 .expect("request with the expected params should match the mock");
1420 assert!(observations.is_empty());
1421 }
1422
1423 #[tokio::test]
1424 async fn category_parses() {
1425 let server = MockServer::start().await;
1426 Mock::given(method("GET"))
1427 .and(path("/category"))
1428 .respond_with(ResponseTemplate::new(200).set_body_string(
1429 r#"{"categories":[{"id":125,"name":"Trade Balance","parent_id":13}]}"#,
1430 ))
1431 .mount(&server)
1432 .await;
1433
1434 let category = client_for(&server)
1435 .category(CategoryId::new(125))
1436 .await
1437 .expect("category parses");
1438 assert_eq!(category.id, CategoryId::new(125));
1439 assert_eq!(category.name, "Trade Balance");
1440 assert_eq!(category.parent_id, CategoryId::new(13));
1441 }
1442
1443 #[tokio::test]
1444 async fn category_children_parse() {
1445 let server = MockServer::start().await;
1446 Mock::given(method("GET"))
1447 .and(path("/category/children"))
1448 .respond_with(ResponseTemplate::new(200).set_body_string(
1449 r#"{"categories":[
1450 {"id":16,"name":"Exports","parent_id":13},
1451 {"id":17,"name":"Imports","parent_id":13}
1452 ]}"#,
1453 ))
1454 .mount(&server)
1455 .await;
1456
1457 let children = client_for(&server)
1458 .category_children(CategoryId::new(13))
1459 .await
1460 .expect("children parse");
1461 assert_eq!(children.len(), 2);
1462 assert_eq!(children[0].name, "Exports");
1463 assert_eq!(children[1].id, CategoryId::new(17));
1464 }
1465
1466 #[tokio::test]
1467 async fn category_related_parse() {
1468 let server = MockServer::start().await;
1469 Mock::given(method("GET"))
1470 .and(path("/category/related"))
1471 .and(query_param("category_id", "32073"))
1472 .respond_with(ResponseTemplate::new(200).set_body_string(
1473 r#"{"categories":[
1474 {"id":149,"name":"Arkansas","parent_id":27281},
1475 {"id":150,"name":"Illinois","parent_id":27281}
1476 ]}"#,
1477 ))
1478 .mount(&server)
1479 .await;
1480
1481 let related = client_for(&server)
1482 .category_related(CategoryId::new(32073))
1483 .await
1484 .expect("category/related parse");
1485 assert_eq!(related.len(), 2);
1486 assert_eq!(related[0].name, "Arkansas");
1487 assert_eq!(related[1].id, CategoryId::new(150));
1488 }
1489
1490 #[tokio::test]
1491 async fn category_series_sends_params_and_parses() {
1492 let server = MockServer::start().await;
1493 Mock::given(method("GET"))
1495 .and(path("/category/series"))
1496 .and(query_param("category_id", "125"))
1497 .and(query_param("order_by", "popularity"))
1498 .and(query_param("limit", "2"))
1499 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1500 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1501 )))
1502 .mount(&server)
1503 .await;
1504
1505 let results = client_for(&server)
1506 .category_series(CategoryId::new(125))
1507 .order_by(OrderBy::Popularity)
1508 .limit(2)
1509 .send()
1510 .await
1511 .expect("category series parse");
1512 assert_eq!(results.count, 1);
1513 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1514 }
1515
1516 #[tokio::test]
1517 async fn releases_parse_with_pagination() {
1518 let server = MockServer::start().await;
1519 Mock::given(method("GET"))
1520 .and(path("/releases"))
1521 .respond_with(ResponseTemplate::new(200).set_body_string(
1522 r#"{"count":2,"offset":0,"limit":1000,"releases":[
1523 {"id":9,"name":"Advance Monthly Sales","press_release":false},
1524 {"id":53,"name":"Gross Domestic Product","press_release":true,"link":"http://bea.gov"}
1525 ]}"#,
1526 ))
1527 .mount(&server)
1528 .await;
1529
1530 let results = client_for(&server)
1531 .releases()
1532 .send()
1533 .await
1534 .expect("releases parse");
1535 assert_eq!(results.count, 2);
1536 assert_eq!(results.releases[1].id, ReleaseId::new(53));
1537 assert_eq!(results.releases[1].link.as_deref(), Some("http://bea.gov"));
1538 }
1539
1540 #[tokio::test]
1541 async fn release_parses() {
1542 let server = MockServer::start().await;
1543 Mock::given(method("GET"))
1544 .and(path("/release"))
1545 .and(query_param("release_id", "53"))
1546 .respond_with(ResponseTemplate::new(200).set_body_string(
1547 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
1548 ))
1549 .mount(&server)
1550 .await;
1551
1552 let release = client_for(&server)
1553 .release(ReleaseId::new(53))
1554 .await
1555 .expect("release parses");
1556 assert_eq!(release.id, ReleaseId::new(53));
1557 assert_eq!(release.name, "Gross Domestic Product");
1558 assert!(release.press_release);
1559 }
1560
1561 #[tokio::test]
1562 async fn release_series_sends_params_and_parses() {
1563 let server = MockServer::start().await;
1564 Mock::given(method("GET"))
1565 .and(path("/release/series"))
1566 .and(query_param("release_id", "53"))
1567 .and(query_param("limit", "2"))
1568 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1569 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1570 )))
1571 .mount(&server)
1572 .await;
1573
1574 let results = client_for(&server)
1575 .release_series(ReleaseId::new(53))
1576 .limit(2)
1577 .send()
1578 .await
1579 .expect("release series parse");
1580 assert_eq!(results.count, 1);
1581 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1582 }
1583
1584 #[tokio::test]
1585 async fn tags_search_sends_text_and_parses() {
1586 let server = MockServer::start().await;
1587 Mock::given(method("GET"))
1588 .and(path("/tags"))
1589 .and(query_param("search_text", "gdp"))
1590 .respond_with(ResponseTemplate::new(200).set_body_string(
1591 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1592 {"name":"gdp","group_id":"gen","popularity":80,"series_count":12345}
1593 ]}"#,
1594 ))
1595 .mount(&server)
1596 .await;
1597
1598 let results = client_for(&server)
1599 .tags()
1600 .search_text("gdp")
1601 .send()
1602 .await
1603 .expect("tags parse");
1604 assert_eq!(results.count, 1);
1605 assert_eq!(results.tags[0].name, "gdp");
1606 assert_eq!(results.tags[0].series_count, 12345);
1607 }
1608
1609 #[tokio::test]
1610 async fn related_tags_send_seed_names_and_parses() {
1611 let server = MockServer::start().await;
1612 Mock::given(method("GET"))
1614 .and(path("/related_tags"))
1615 .and(query_param("tag_names", "gdp;quarterly"))
1616 .respond_with(ResponseTemplate::new(200).set_body_string(
1617 r#"{"count":1,"offset":0,"limit":1000,"tags":[
1618 {"name":"nsa","group_id":"seas","popularity":90,"series_count":42}
1619 ]}"#,
1620 ))
1621 .mount(&server)
1622 .await;
1623
1624 let results = client_for(&server)
1625 .related_tags(["gdp", "quarterly"])
1626 .send()
1627 .await
1628 .expect("related_tags parse");
1629 assert_eq!(results.count, 1);
1630 assert_eq!(results.tags[0].name, "nsa");
1631 }
1632
1633 const ONE_TAG_BODY: &str = r#"{"count":1,"offset":0,"limit":1000,"tags":[
1635 {"name":"gdp","group_id":"gen","popularity":80,"series_count":42}
1636 ]}"#;
1637
1638 #[tokio::test]
1639 async fn category_tags_send_scope_and_parse() {
1640 let server = MockServer::start().await;
1641 Mock::given(method("GET"))
1642 .and(path("/category/tags"))
1643 .and(query_param("category_id", "125"))
1644 .and(query_param("search_text", "gdp"))
1645 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1646 .mount(&server)
1647 .await;
1648
1649 let results = client_for(&server)
1650 .category_tags(CategoryId::new(125))
1651 .search_text("gdp")
1652 .send()
1653 .await
1654 .expect("category/tags parse");
1655 assert_eq!(results.count, 1);
1656 assert_eq!(results.tags[0].name, "gdp");
1657 }
1658
1659 #[tokio::test]
1660 async fn category_related_tags_send_scope_and_seed_and_parse() {
1661 let server = MockServer::start().await;
1662 Mock::given(method("GET"))
1663 .and(path("/category/related_tags"))
1664 .and(query_param("category_id", "125"))
1665 .and(query_param("tag_names", "gdp;quarterly"))
1666 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1667 .mount(&server)
1668 .await;
1669
1670 let results = client_for(&server)
1671 .category_related_tags(CategoryId::new(125), ["gdp", "quarterly"])
1672 .send()
1673 .await
1674 .expect("category/related_tags parse");
1675 assert_eq!(results.count, 1);
1676 }
1677
1678 #[tokio::test]
1679 async fn release_tags_send_scope_and_parse() {
1680 let server = MockServer::start().await;
1681 Mock::given(method("GET"))
1682 .and(path("/release/tags"))
1683 .and(query_param("release_id", "53"))
1684 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1685 .mount(&server)
1686 .await;
1687
1688 let results = client_for(&server)
1689 .release_tags(ReleaseId::new(53))
1690 .send()
1691 .await
1692 .expect("release/tags parse");
1693 assert_eq!(results.count, 1);
1694 }
1695
1696 #[tokio::test]
1697 async fn release_related_tags_send_scope_and_seed_and_parse() {
1698 let server = MockServer::start().await;
1699 Mock::given(method("GET"))
1700 .and(path("/release/related_tags"))
1701 .and(query_param("release_id", "53"))
1702 .and(query_param("tag_names", "gdp"))
1703 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1704 .mount(&server)
1705 .await;
1706
1707 let results = client_for(&server)
1708 .release_related_tags(ReleaseId::new(53), ["gdp"])
1709 .send()
1710 .await
1711 .expect("release/related_tags parse");
1712 assert_eq!(results.count, 1);
1713 }
1714
1715 #[tokio::test]
1716 async fn series_search_tags_send_scope_and_tag_search_text() {
1717 let server = MockServer::start().await;
1718 Mock::given(method("GET"))
1721 .and(path("/series/search/tags"))
1722 .and(query_param("series_search_text", "unemployment"))
1723 .and(query_param("tag_search_text", "rate"))
1724 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1725 .mount(&server)
1726 .await;
1727
1728 let results = client_for(&server)
1729 .series_search_tags("unemployment")
1730 .search_text("rate")
1731 .send()
1732 .await
1733 .expect("series/search/tags parse");
1734 assert_eq!(results.count, 1);
1735 }
1736
1737 #[tokio::test]
1738 async fn series_search_related_tags_send_scope_and_seed() {
1739 let server = MockServer::start().await;
1740 Mock::given(method("GET"))
1741 .and(path("/series/search/related_tags"))
1742 .and(query_param("series_search_text", "unemployment"))
1743 .and(query_param("tag_names", "monthly"))
1744 .respond_with(ResponseTemplate::new(200).set_body_string(ONE_TAG_BODY))
1745 .mount(&server)
1746 .await;
1747
1748 let results = client_for(&server)
1749 .series_search_related_tags("unemployment", ["monthly"])
1750 .send()
1751 .await
1752 .expect("series/search/related_tags parse");
1753 assert_eq!(results.count, 1);
1754 }
1755
1756 #[tokio::test]
1757 async fn tags_series_joins_names_and_parses() {
1758 let server = MockServer::start().await;
1759 Mock::given(method("GET"))
1761 .and(path("/tags/series"))
1762 .and(query_param("tag_names", "gdp;quarterly"))
1763 .and(query_param("limit", "2"))
1764 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
1765 "{{\"count\":1,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
1766 )))
1767 .mount(&server)
1768 .await;
1769
1770 let results = client_for(&server)
1771 .tags_series(["gdp", "quarterly"])
1772 .limit(2)
1773 .send()
1774 .await
1775 .expect("tags/series parse");
1776 assert_eq!(results.count, 1);
1777 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
1778 }
1779
1780 #[tokio::test]
1781 async fn series_tags_parses() {
1782 let server = MockServer::start().await;
1783 Mock::given(method("GET"))
1784 .and(path("/series/tags"))
1785 .and(query_param("series_id", "GNPCA"))
1786 .respond_with(ResponseTemplate::new(200).set_body_string(
1787 r#"{"count":2,"offset":0,"limit":1000,"tags":[
1788 {"name":"gnp","group_id":"gen","popularity":50,"series_count":10},
1789 {"name":"usa","group_id":"geo","notes":null,"popularity":100,"series_count":500}
1790 ]}"#,
1791 ))
1792 .mount(&server)
1793 .await;
1794
1795 let results = client_for(&server)
1796 .series_tags(&SeriesId::new("GNPCA"))
1797 .await
1798 .expect("series/tags parse");
1799 assert_eq!(results.count, 2);
1800 assert_eq!(results.tags[0].name, "gnp");
1801 assert!(results.tags[1].notes.is_none());
1802 }
1803
1804 #[tokio::test]
1805 async fn sources_parse_with_pagination() {
1806 let server = MockServer::start().await;
1807 Mock::given(method("GET"))
1808 .and(path("/sources"))
1809 .respond_with(ResponseTemplate::new(200).set_body_string(
1810 r#"{"count":2,"offset":0,"limit":1000,"sources":[
1811 {"id":1,"name":"Board of Governors of the Federal Reserve System (US)"},
1812 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}
1813 ]}"#,
1814 ))
1815 .mount(&server)
1816 .await;
1817
1818 let results = client_for(&server)
1819 .sources()
1820 .send()
1821 .await
1822 .expect("sources parse");
1823 assert_eq!(results.count, 2);
1824 assert_eq!(results.sources[1].id, SourceId::new(18));
1825 assert_eq!(results.sources[1].link.as_deref(), Some("http://bea.gov"));
1826 }
1827
1828 #[tokio::test]
1829 async fn source_parses() {
1830 let server = MockServer::start().await;
1831 Mock::given(method("GET"))
1832 .and(path("/source"))
1833 .and(query_param("source_id", "18"))
1834 .respond_with(ResponseTemplate::new(200).set_body_string(
1835 r#"{"sources":[{"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://bea.gov"}]}"#,
1836 ))
1837 .mount(&server)
1838 .await;
1839
1840 let source = client_for(&server)
1841 .source(SourceId::new(18))
1842 .await
1843 .expect("source parses");
1844 assert_eq!(source.id, SourceId::new(18));
1845 assert_eq!(source.name, "U.S. Bureau of Economic Analysis");
1846 }
1847
1848 #[tokio::test]
1849 async fn source_releases_send_source_id_and_parse() {
1850 let server = MockServer::start().await;
1851 Mock::given(method("GET"))
1853 .and(path("/source/releases"))
1854 .and(query_param("source_id", "18"))
1855 .and(query_param("limit", "2"))
1856 .respond_with(ResponseTemplate::new(200).set_body_string(
1857 r#"{"count":1,"offset":0,"limit":2,"releases":[
1858 {"id":53,"name":"Gross Domestic Product","press_release":true}
1859 ]}"#,
1860 ))
1861 .mount(&server)
1862 .await;
1863
1864 let results = client_for(&server)
1865 .source_releases(SourceId::new(18))
1866 .limit(2)
1867 .send()
1868 .await
1869 .expect("source/releases parse");
1870 assert_eq!(results.count, 1);
1871 assert_eq!(results.releases[0].id, ReleaseId::new(53));
1872 }
1873
1874 #[tokio::test]
1875 async fn release_sources_send_release_id_and_parse() {
1876 let server = MockServer::start().await;
1877 Mock::given(method("GET"))
1880 .and(path("/release/sources"))
1881 .and(query_param("release_id", "51"))
1882 .respond_with(ResponseTemplate::new(200).set_body_string(
1883 r#"{"realtime_start":"2013-08-14","realtime_end":"2013-08-14","sources":[
1884 {"id":18,"name":"U.S. Bureau of Economic Analysis","link":"http://www.bea.gov/"},
1885 {"id":19,"name":"U.S. Census Bureau"}
1886 ]}"#,
1887 ))
1888 .mount(&server)
1889 .await;
1890
1891 let sources = client_for(&server)
1892 .release_sources(ReleaseId::new(51))
1893 .await
1894 .expect("release/sources parse");
1895 assert_eq!(sources.len(), 2);
1896 assert_eq!(sources[0].id, SourceId::new(18));
1897 assert_eq!(sources[0].link.as_deref(), Some("http://www.bea.gov/"));
1898 assert!(sources[1].link.is_none());
1899 }
1900
1901 #[tokio::test]
1902 async fn releases_dates_send_params_and_parse() {
1903 let server = MockServer::start().await;
1904 Mock::given(method("GET"))
1906 .and(path("/releases/dates"))
1907 .and(query_param("sort_order", "desc"))
1908 .and(query_param("limit", "2"))
1909 .respond_with(ResponseTemplate::new(200).set_body_string(
1910 r#"{"count":2,"offset":0,"limit":2,"release_dates":[
1911 {"release_id":9,"release_name":"Advance Monthly Sales","date":"2013-08-13"},
1912 {"release_id":10,"release_name":"Consumer Price Index","date":"2013-08-15"}
1913 ]}"#,
1914 ))
1915 .mount(&server)
1916 .await;
1917
1918 let results = client_for(&server)
1919 .releases_dates()
1920 .sort_order(SortOrder::Descending)
1921 .limit(2)
1922 .send()
1923 .await
1924 .expect("releases/dates parse");
1925 assert_eq!(results.count, 2);
1926 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(9));
1927 assert_eq!(
1928 results.release_dates[0].release_name.as_deref(),
1929 Some("Advance Monthly Sales")
1930 );
1931 }
1932
1933 #[tokio::test]
1934 async fn release_dates_send_release_id_and_include_flag_and_parse() {
1935 let server = MockServer::start().await;
1936 Mock::given(method("GET"))
1939 .and(path("/release/dates"))
1940 .and(query_param("release_id", "82"))
1941 .and(query_param("include_release_dates_with_no_data", "true"))
1942 .respond_with(ResponseTemplate::new(200).set_body_string(
1943 r#"{"count":2,"offset":0,"limit":10000,"release_dates":[
1944 {"release_id":82,"date":"1997-02-10"},
1945 {"release_id":82,"date":"1998-02-10"}
1946 ]}"#,
1947 ))
1948 .mount(&server)
1949 .await;
1950
1951 let results = client_for(&server)
1952 .release_dates(ReleaseId::new(82))
1953 .include_dates_with_no_data(true)
1954 .send()
1955 .await
1956 .expect("release/dates parse");
1957 assert_eq!(results.count, 2);
1958 assert_eq!(results.release_dates[0].release_id, ReleaseId::new(82));
1959 assert!(results.release_dates[0].release_name.is_none());
1960 }
1961
1962 #[tokio::test]
1963 async fn release_tables_send_element_and_parse_tree() {
1964 let server = MockServer::start().await;
1965 Mock::given(method("GET"))
1968 .and(path("/release/tables"))
1969 .and(query_param("release_id", "10"))
1970 .and(query_param("element_id", "34483"))
1971 .respond_with(ResponseTemplate::new(200).set_body_string(
1972 r#"{"name":"Monthly, SA","element_id":34483,"release_id":"10","elements":{
1973 "34484":{"element_id":34484,"release_id":10,"parent_id":34483,
1974 "series_id":"","type":"series","name":"All items","line":"1","level":"0",
1975 "children":[
1976 {"element_id":34485,"release_id":10,"parent_id":34484,
1977 "series_id":"CPIFABSL","type":"series","name":"Food",
1978 "line":"2","level":"1","children":[]}
1979 ]}
1980 }}"#,
1981 ))
1982 .mount(&server)
1983 .await;
1984
1985 let table = client_for(&server)
1986 .release_tables(ReleaseId::new(10))
1987 .element(ReleaseElementId::new(34483))
1988 .send()
1989 .await
1990 .expect("release/tables parse");
1991 assert_eq!(table.name.as_deref(), Some("Monthly, SA"));
1992 assert_eq!(table.roots.len(), 1);
1993 let leaf = &table.roots[0].children[0];
1994 assert_eq!(
1995 leaf.series_id.as_ref().map(|s| s.as_str()),
1996 Some("CPIFABSL")
1997 );
1998 }
1999
2000 #[tokio::test]
2001 async fn release_tables_observation_values_reach_wire_and_parse() {
2002 let server = MockServer::start().await;
2003 Mock::given(method("GET"))
2007 .and(path("/release/tables"))
2008 .and(query_param("release_id", "10"))
2009 .and(query_param("include_observation_values", "true"))
2010 .and(query_param("observation_date", "2023-06-01"))
2011 .respond_with(ResponseTemplate::new(200).set_body_string(
2012 r#"{"release_id":"10","elements":{
2013 "36715":{"element_id":36715,"release_id":10,"parent_id":36714,
2014 "series_id":"CUSR0000SA0L5","type":"series","name":"All items",
2015 "level":"1","observation_value":"292.260","observation_date":"Jun 2023",
2016 "children":[]}
2017 }}"#,
2018 ))
2019 .mount(&server)
2020 .await;
2021
2022 let table = client_for(&server)
2023 .release_tables(ReleaseId::new(10))
2024 .observation_date(chrono::NaiveDate::from_ymd_opt(2023, 6, 1).unwrap())
2025 .send()
2026 .await
2027 .expect("release/tables with values parse");
2028 let row = &table.roots[0];
2029 assert_eq!(row.observation_value, Some(292.260));
2030 assert_eq!(row.observation_date.as_deref(), Some("Jun 2023"));
2031 }
2032
2033 #[tokio::test]
2034 async fn series_categories_parse() {
2035 let server = MockServer::start().await;
2036 Mock::given(method("GET"))
2037 .and(path("/series/categories"))
2038 .and(query_param("series_id", "GNPCA"))
2039 .respond_with(ResponseTemplate::new(200).set_body_string(
2040 r#"{"categories":[
2041 {"id":106,"name":"Gross National Product","parent_id":18},
2042 {"id":18,"name":"National Income & Product Accounts","parent_id":13}
2043 ]}"#,
2044 ))
2045 .mount(&server)
2046 .await;
2047
2048 let categories = client_for(&server)
2049 .series_categories(&SeriesId::new("GNPCA"))
2050 .await
2051 .expect("series/categories parse");
2052 assert_eq!(categories.len(), 2);
2053 assert_eq!(categories[0].id, CategoryId::new(106));
2054 }
2055
2056 #[tokio::test]
2057 async fn series_release_parses() {
2058 let server = MockServer::start().await;
2059 Mock::given(method("GET"))
2060 .and(path("/series/release"))
2061 .and(query_param("series_id", "GNPCA"))
2062 .respond_with(ResponseTemplate::new(200).set_body_string(
2063 r#"{"releases":[{"id":53,"name":"Gross Domestic Product","press_release":true}]}"#,
2064 ))
2065 .mount(&server)
2066 .await;
2067
2068 let release = client_for(&server)
2069 .series_release(&SeriesId::new("GNPCA"))
2070 .await
2071 .expect("series/release parse");
2072 assert_eq!(release.id, ReleaseId::new(53));
2073 assert_eq!(release.name, "Gross Domestic Product");
2074 }
2075
2076 #[tokio::test]
2077 async fn series_updates_sends_filter_and_parses() {
2078 let server = MockServer::start().await;
2079 Mock::given(method("GET"))
2080 .and(path("/series/updates"))
2081 .and(query_param("filter_value", "macro"))
2082 .and(query_param("limit", "2"))
2083 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2084 "{{\"count\":5,\"offset\":0,\"limit\":2,\"seriess\":[{SERIES_OBJECT}]}}"
2085 )))
2086 .mount(&server)
2087 .await;
2088
2089 let results = client_for(&server)
2090 .series_updates()
2091 .filter(UpdatesFilter::Macro)
2092 .limit(2)
2093 .send()
2094 .await
2095 .expect("series/updates parse");
2096 assert_eq!(results.count, 5);
2097 assert_eq!(results.series[0].id, SeriesId::new("GNPCA"));
2098 }
2099
2100 #[tokio::test]
2101 async fn series_updates_sends_time_window_as_yyyymmddhhmm() {
2102 use chrono::NaiveDate;
2103 let server = MockServer::start().await;
2104 Mock::given(method("GET"))
2105 .and(path("/series/updates"))
2106 .and(query_param("start_time", "201803021420"))
2107 .and(query_param("end_time", "201803030905"))
2108 .respond_with(ResponseTemplate::new(200).set_body_string(format!(
2109 "{{\"count\":1,\"offset\":0,\"limit\":1,\"seriess\":[{SERIES_OBJECT}]}}"
2110 )))
2111 .mount(&server)
2112 .await;
2113
2114 let start = NaiveDate::from_ymd_opt(2018, 3, 2)
2115 .unwrap()
2116 .and_hms_opt(14, 20, 0)
2117 .unwrap();
2118 let end = NaiveDate::from_ymd_opt(2018, 3, 3)
2119 .unwrap()
2120 .and_hms_opt(9, 5, 0)
2121 .unwrap();
2122 let results = client_for(&server)
2123 .series_updates()
2124 .time_window(start, end)
2125 .send()
2126 .await
2127 .expect("series/updates time-window parse");
2128 assert_eq!(results.count, 1);
2129 }
2130
2131 #[tokio::test]
2132 async fn series_vintagedates_send_id_and_parse() {
2133 let server = MockServer::start().await;
2134 Mock::given(method("GET"))
2135 .and(path("/series/vintagedates"))
2136 .and(query_param("series_id", "GNPCA"))
2137 .and(query_param("limit", "2"))
2138 .respond_with(ResponseTemplate::new(200).set_body_string(
2139 r#"{"count":3,"offset":0,"limit":2,"vintage_dates":["1958-12-21","1959-02-19"]}"#,
2140 ))
2141 .mount(&server)
2142 .await;
2143
2144 let dates = client_for(&server)
2145 .series_vintagedates(&SeriesId::new("GNPCA"))
2146 .limit(2)
2147 .send()
2148 .await
2149 .expect("series/vintagedates parse");
2150 assert_eq!(dates.count, 3);
2151 assert_eq!(dates.vintage_dates.len(), 2);
2152 assert_eq!(
2153 dates.vintage_dates[0],
2154 chrono::NaiveDate::from_ymd_opt(1958, 12, 21).unwrap()
2155 );
2156 }
2157}