Skip to main content

ferric_fred/
observations_request.rs

1use chrono::NaiveDate;
2
3use crate::{
4    AggregationMethod, Client, Frequency, Observation, Result, SeriesId, SortOrder, Units,
5};
6
7/// A builder for an observations request, returned by [`Client::observations`].
8///
9/// Only parameters you set are sent; anything left unset uses FRED's default
10/// (full history, `Levels` units, ascending by date, the series' native
11/// frequency). Finish with [`send`](ObservationsRequest::send).
12///
13/// ```no_run
14/// # async fn run(client: &ferric_fred::Client) -> ferric_fred::Result<()> {
15/// use ferric_fred::{SeriesId, SortOrder, Units};
16/// let latest = client
17///     .observations(&SeriesId::new("UNRATE"))
18///     .units(Units::PercentChange)
19///     .sort_order(SortOrder::Descending)
20///     .limit(12)
21///     .send()
22///     .await?;
23/// # Ok(())
24/// # }
25/// ```
26#[derive(Debug, Clone)]
27#[must_use = "an ObservationsRequest does nothing until you call `.send()`"]
28pub struct ObservationsRequest<'a> {
29    client: &'a Client,
30    series_id: SeriesId,
31    observation_start: Option<NaiveDate>,
32    observation_end: Option<NaiveDate>,
33    units: Option<Units>,
34    frequency: Option<Frequency>,
35    aggregation_method: Option<AggregationMethod>,
36    sort_order: Option<SortOrder>,
37    limit: Option<u32>,
38    offset: Option<u32>,
39    /// FRED's `realtime_start`/`realtime_end` — a real-time period (ALFRED,
40    /// ADR-0024), stored as a pair since it reads as one window.
41    realtime: Option<(NaiveDate, NaiveDate)>,
42    vintage_dates: Vec<NaiveDate>,
43}
44
45impl<'a> ObservationsRequest<'a> {
46    pub(crate) fn new(client: &'a Client, series_id: SeriesId) -> Self {
47        Self {
48            client,
49            series_id,
50            observation_start: None,
51            observation_end: None,
52            units: None,
53            frequency: None,
54            aggregation_method: None,
55            sort_order: None,
56            limit: None,
57            offset: None,
58            realtime: None,
59            vintage_dates: Vec::new(),
60        }
61    }
62
63    /// Earliest observation date to return (`observation_start`).
64    pub fn observation_start(mut self, date: NaiveDate) -> Self {
65        self.observation_start = Some(date);
66        self
67    }
68
69    /// Latest observation date to return (`observation_end`).
70    pub fn observation_end(mut self, date: NaiveDate) -> Self {
71        self.observation_end = Some(date);
72        self
73    }
74
75    /// Convenience for setting both ends of the date range at once.
76    pub fn date_range(self, start: NaiveDate, end: NaiveDate) -> Self {
77        self.observation_start(start).observation_end(end)
78    }
79
80    /// Units transformation to apply (`units`).
81    pub fn units(mut self, units: Units) -> Self {
82        self.units = Some(units);
83        self
84    }
85
86    /// Aggregate observations down to a lower `frequency`. Pair with
87    /// [`aggregation_method`](Self::aggregation_method) to control how.
88    pub fn frequency(mut self, frequency: Frequency) -> Self {
89        self.frequency = Some(frequency);
90        self
91    }
92
93    /// How to aggregate when a lower [`frequency`](Self::frequency) is set
94    /// (`aggregation_method`).
95    pub fn aggregation_method(mut self, method: AggregationMethod) -> Self {
96        self.aggregation_method = Some(method);
97        self
98    }
99
100    /// Sort order of the returned observations by date (`sort_order`).
101    pub fn sort_order(mut self, order: SortOrder) -> Self {
102        self.sort_order = Some(order);
103        self
104    }
105
106    /// Maximum number of observations to return, `1..=100000` (`limit`).
107    pub fn limit(mut self, limit: u32) -> Self {
108        self.limit = Some(limit);
109        self
110    }
111
112    /// Number of observations to skip from the start (`offset`), for paging.
113    pub fn offset(mut self, offset: u32) -> Self {
114        self.offset = Some(offset);
115        self
116    }
117
118    /// Return the data **as it was known** over this real-time period
119    /// (`realtime_start`/`realtime_end`) — the ALFRED / point-in-time dimension
120    /// (ADR-0024). Pass the **same date twice** for a point-in-time snapshot (the
121    /// series as it looked on that day); pass a window to see each value's
122    /// real-time period. Each returned [`Observation`] carries its own
123    /// [`realtime_start`](Observation::realtime_start)/[`realtime_end`](Observation::realtime_end).
124    pub fn realtime(mut self, start: NaiveDate, end: NaiveDate) -> Self {
125        self.realtime = Some((start, end));
126        self
127    }
128
129    /// Return the data as of these specific revision dates (`vintage_dates`) —
130    /// each date selects that vintage of the series (ALFRED, ADR-0024).
131    /// Complements [`realtime`](Self::realtime); an empty list is ignored.
132    pub fn vintage_dates(mut self, dates: impl IntoIterator<Item = NaiveDate>) -> Self {
133        self.vintage_dates = dates.into_iter().collect();
134        self
135    }
136
137    /// Run the request and return the matching observations.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error if the request fails to send, FRED returns a non-success
142    /// status, or the response body cannot be deserialized.
143    pub async fn send(self) -> Result<Vec<Observation>> {
144        self.client.execute_observations(&self).await
145    }
146
147    /// Serialize the set parameters to FRED query key/value pairs. `api_key` and
148    /// `file_type` are added by the client, not here.
149    pub(crate) fn query_params(&self) -> Vec<(&'static str, String)> {
150        let mut params: Vec<(&'static str, String)> = Vec::new();
151        params.push(("series_id", self.series_id.as_str().to_owned()));
152        if let Some(date) = self.observation_start {
153            params.push(("observation_start", date.to_string()));
154        }
155        if let Some(date) = self.observation_end {
156            params.push(("observation_end", date.to_string()));
157        }
158        if let Some(units) = self.units {
159            params.push(("units", units.query_code().to_owned()));
160        }
161        if let Some(frequency) = &self.frequency {
162            params.push(("frequency", frequency.query_code().to_owned()));
163        }
164        if let Some(method) = self.aggregation_method {
165            params.push(("aggregation_method", method.query_code().to_owned()));
166        }
167        if let Some(order) = self.sort_order {
168            params.push(("sort_order", order.query_code().to_owned()));
169        }
170        if let Some(limit) = self.limit {
171            params.push(("limit", limit.to_string()));
172        }
173        if let Some(offset) = self.offset {
174            params.push(("offset", offset.to_string()));
175        }
176        if let Some((start, end)) = self.realtime {
177            params.push(("realtime_start", start.to_string()));
178            params.push(("realtime_end", end.to_string()));
179        }
180        if !self.vintage_dates.is_empty() {
181            let joined = self
182                .vintage_dates
183                .iter()
184                .map(NaiveDate::to_string)
185                .collect::<Vec<_>>()
186                .join(",");
187            params.push(("vintage_dates", joined));
188        }
189        params
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn test_client() -> Client {
198        Client::new("test-key").expect("client builds")
199    }
200
201    #[test]
202    fn defaults_send_only_the_series_id() {
203        let client = test_client();
204        let request = client.observations(&SeriesId::new("GNPCA"));
205        assert_eq!(
206            request.query_params(),
207            vec![("series_id", "GNPCA".to_owned())]
208        );
209    }
210
211    #[test]
212    fn parameters_serialize_to_fred_codes() {
213        let client = test_client();
214        let request = client
215            .observations(&SeriesId::new("GNPCA"))
216            .date_range(
217                NaiveDate::from_ymd_opt(2000, 1, 1).unwrap(),
218                NaiveDate::from_ymd_opt(2010, 12, 31).unwrap(),
219            )
220            .units(Units::PercentChange)
221            .frequency(Frequency::Quarterly)
222            .aggregation_method(AggregationMethod::Sum)
223            .sort_order(SortOrder::Descending)
224            .limit(50)
225            .offset(5);
226
227        let params = request.query_params();
228        for expected in [
229            ("series_id", "GNPCA"),
230            ("observation_start", "2000-01-01"),
231            ("observation_end", "2010-12-31"),
232            ("units", "pch"),
233            ("frequency", "q"),
234            ("aggregation_method", "sum"),
235            ("sort_order", "desc"),
236            ("limit", "50"),
237            ("offset", "5"),
238        ] {
239            assert!(
240                params.contains(&(expected.0, expected.1.to_owned())),
241                "missing {expected:?} in {params:?}"
242            );
243        }
244    }
245
246    #[test]
247    fn realtime_and_vintage_dates_serialize() {
248        let client = test_client();
249        let request = client
250            .observations(&SeriesId::new("GNPCA"))
251            .realtime(
252                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
253                NaiveDate::from_ymd_opt(2020, 1, 1).unwrap(),
254            )
255            .vintage_dates([
256                NaiveDate::from_ymd_opt(2020, 3, 26).unwrap(),
257                NaiveDate::from_ymd_opt(2021, 3, 25).unwrap(),
258            ]);
259
260        let params = request.query_params();
261        for expected in [
262            ("realtime_start", "2020-01-01"),
263            ("realtime_end", "2020-01-01"),
264            ("vintage_dates", "2020-03-26,2021-03-25"),
265        ] {
266            assert!(
267                params.contains(&(expected.0, expected.1.to_owned())),
268                "missing {expected:?} in {params:?}"
269            );
270        }
271    }
272}