Skip to main content

gie_client/common/
query.rs

1use std::num::NonZeroU32;
2
3use serde::Serialize;
4
5use crate::error::GieError;
6
7use super::date_range::DateRange;
8use super::serde_ext::{serialize_optional_dataset_type, serialize_optional_date};
9use super::types::{DatasetType, DateFilter, GieDate, format_date, parse_dataset_type, parse_date};
10
11/// Query builder shared by AGSI and ALSI endpoints.
12#[derive(Debug, Clone, Default)]
13pub struct GieQuery {
14    country: Option<String>,
15    company: Option<String>,
16    facility: Option<String>,
17    dataset_type: Option<DatasetType>,
18    date_filter: Option<DateFilter>,
19    page: Option<NonZeroU32>,
20    size: Option<NonZeroU32>,
21}
22
23impl GieQuery {
24    /// Creates an empty query.
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Sets `country`.
30    pub fn country(mut self, country: impl Into<String>) -> Self {
31        self.country = Some(country.into());
32        self
33    }
34
35    /// Sets `company`.
36    pub fn company(mut self, company: impl Into<String>) -> Self {
37        self.company = Some(company.into());
38        self
39    }
40
41    /// Sets `facility`.
42    pub fn facility(mut self, facility: impl Into<String>) -> Self {
43        self.facility = Some(facility.into());
44        self
45    }
46
47    /// Sets dataset `type`.
48    pub fn dataset_type(mut self, dataset_type: DatasetType) -> Self {
49        self.dataset_type = Some(dataset_type);
50        self
51    }
52
53    /// Parses and sets dataset `type` from string (`eu`, `ne`, `ai`).
54    pub fn try_dataset_type(mut self, dataset_type: impl AsRef<str>) -> Result<Self, GieError> {
55        self.dataset_type = Some(
56            parse_dataset_type(dataset_type.as_ref()).map_err(GieError::InvalidDatasetTypeInput)?,
57        );
58        Ok(self)
59    }
60
61    /// Clears dataset `type`.
62    pub fn without_dataset_type(mut self) -> Self {
63        self.dataset_type = None;
64        self
65    }
66
67    /// Sets the single-day `date` filter.
68    pub fn date(mut self, date: GieDate) -> Self {
69        self.date_filter = Some(DateFilter::Day(date));
70        self
71    }
72
73    /// Parses and sets the single-day `date` filter from `YYYY-MM-DD`.
74    pub fn try_date(mut self, date: impl AsRef<str>) -> Result<Self, GieError> {
75        self.date_filter = Some(DateFilter::Day(
76            parse_date(date.as_ref()).map_err(GieError::InvalidDateInput)?,
77        ));
78        Ok(self)
79    }
80
81    /// Sets `from` and `to` range.
82    pub fn range(mut self, from: GieDate, to: GieDate) -> Result<Self, GieError> {
83        let range = DateRange::new(from, to)?;
84        self.date_filter = Some(DateFilter::Range(range));
85        Ok(self)
86    }
87
88    /// Parses and sets `from` and `to` from `YYYY-MM-DD`.
89    pub fn try_range(self, from: impl AsRef<str>, to: impl AsRef<str>) -> Result<Self, GieError> {
90        let from = parse_date(from.as_ref()).map_err(GieError::InvalidDateInput)?;
91        let to = parse_date(to.as_ref()).map_err(GieError::InvalidDateInput)?;
92        self.range(from, to)
93    }
94
95    /// Sets the page number.
96    pub fn page(mut self, page: NonZeroU32) -> Self {
97        self.page = Some(page);
98        self
99    }
100
101    /// Parses and sets the page number.
102    pub fn try_page(mut self, page: u32) -> Result<Self, GieError> {
103        self.page = Some(NonZeroU32::new(page).ok_or_else(|| {
104            GieError::InvalidPageInput("page must be greater than zero".to_string())
105        })?);
106        Ok(self)
107    }
108
109    /// Sets requested page size.
110    pub fn size(mut self, size: NonZeroU32) -> Self {
111        self.size = Some(size);
112        self
113    }
114
115    /// Parses and sets requested page size.
116    pub fn try_size(mut self, size: u32) -> Result<Self, GieError> {
117        self.size = Some(NonZeroU32::new(size).ok_or_else(|| {
118            GieError::InvalidSizeInput("size must be greater than zero".to_string())
119        })?);
120        Ok(self)
121    }
122
123    pub(crate) fn initial_page(&self) -> NonZeroU32 {
124        self.page.unwrap_or_else(default_page)
125    }
126
127    pub(crate) fn as_params_with_page(
128        &self,
129        page_override: Option<NonZeroU32>,
130    ) -> GieQueryParams<'_> {
131        let (date, from, to) = match self.date_filter {
132            Some(DateFilter::Day(value)) => (Some(value), None, None),
133            Some(DateFilter::Range(value)) => (None, Some(value.from()), Some(value.to())),
134            None => (None, None, None),
135        };
136
137        GieQueryParams {
138            country: self.country.as_deref(),
139            company: self.company.as_deref(),
140            facility: self.facility.as_deref(),
141            dataset_type: self.dataset_type,
142            date,
143            from,
144            to,
145            page: page_override.or(self.page),
146            size: self.size,
147        }
148    }
149
150    pub(crate) fn to_debug_pairs(
151        &self,
152        page_override: Option<NonZeroU32>,
153    ) -> Vec<(&'static str, String)> {
154        let mut pairs = Vec::new();
155
156        if let Some(value) = &self.country {
157            pairs.push(("country", value.clone()));
158        }
159        if let Some(value) = &self.company {
160            pairs.push(("company", value.clone()));
161        }
162        if let Some(value) = &self.facility {
163            pairs.push(("facility", value.clone()));
164        }
165        if let Some(value) = self.dataset_type {
166            pairs.push(("type", value.to_string()));
167        }
168
169        match self.date_filter {
170            Some(DateFilter::Day(value)) => pairs.push(("date", format_date(value))),
171            Some(DateFilter::Range(value)) => {
172                pairs.push(("from", format_date(value.from())));
173                pairs.push(("to", format_date(value.to())));
174            }
175            None => {}
176        }
177
178        if let Some(value) = page_override.or(self.page) {
179            pairs.push(("page", value.to_string()));
180        }
181        if let Some(value) = self.size {
182            pairs.push(("size", value.to_string()));
183        }
184
185        pairs
186    }
187}
188
189fn default_page() -> NonZeroU32 {
190    NonZeroU32::new(1).expect("1 is non-zero")
191}
192
193#[derive(Debug, Serialize)]
194pub(crate) struct GieQueryParams<'a> {
195    #[serde(skip_serializing_if = "Option::is_none")]
196    country: Option<&'a str>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    company: Option<&'a str>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    facility: Option<&'a str>,
201    #[serde(
202        rename = "type",
203        skip_serializing_if = "Option::is_none",
204        serialize_with = "serialize_optional_dataset_type"
205    )]
206    dataset_type: Option<DatasetType>,
207    #[serde(
208        skip_serializing_if = "Option::is_none",
209        serialize_with = "serialize_optional_date"
210    )]
211    date: Option<GieDate>,
212    #[serde(
213        skip_serializing_if = "Option::is_none",
214        serialize_with = "serialize_optional_date"
215    )]
216    from: Option<GieDate>,
217    #[serde(
218        skip_serializing_if = "Option::is_none",
219        serialize_with = "serialize_optional_date"
220    )]
221    to: Option<GieDate>,
222    #[serde(skip_serializing_if = "Option::is_none")]
223    page: Option<NonZeroU32>,
224    #[serde(skip_serializing_if = "Option::is_none")]
225    size: Option<NonZeroU32>,
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    fn test_date(value: &str) -> GieDate {
233        parse_date(value).unwrap()
234    }
235
236    #[test]
237    fn query_params_are_mapped_to_expected_keys() {
238        let query = GieQuery::new()
239            .country("DE")
240            .company("Comp")
241            .facility("Fac")
242            .dataset_type(DatasetType::Eu)
243            .range(test_date("2026-03-01"), test_date("2026-03-10"))
244            .unwrap()
245            .try_page(2)
246            .unwrap()
247            .try_size(50)
248            .unwrap();
249
250        let pairs = query.to_debug_pairs(None);
251        assert!(pairs.contains(&("country", "DE".to_string())));
252        assert!(pairs.contains(&("company", "Comp".to_string())));
253        assert!(pairs.contains(&("facility", "Fac".to_string())));
254        assert!(pairs.contains(&("type", "eu".to_string())));
255        assert!(pairs.contains(&("from", "2026-03-01".to_string())));
256        assert!(pairs.contains(&("to", "2026-03-10".to_string())));
257        assert!(pairs.contains(&("page", "2".to_string())));
258        assert!(pairs.contains(&("size", "50".to_string())));
259        assert!(!pairs.iter().any(|(key, _)| *key == "date"));
260    }
261
262    #[test]
263    fn date_builder_replaces_range_filter() {
264        let query = GieQuery::new()
265            .range(test_date("2026-03-01"), test_date("2026-03-10"))
266            .unwrap()
267            .date(test_date("2026-03-10"));
268
269        let pairs = query.to_debug_pairs(None);
270        assert!(pairs.contains(&("date", "2026-03-10".to_string())));
271        assert!(!pairs.iter().any(|(key, _)| *key == "from"));
272        assert!(!pairs.iter().any(|(key, _)| *key == "to"));
273    }
274
275    #[test]
276    fn try_range_rejects_invalid_order() {
277        let error = GieQuery::new()
278            .try_range("2026-03-10", "2026-03-01")
279            .unwrap_err();
280
281        assert!(matches!(error, GieError::InvalidDateRangeInput(_)));
282    }
283
284    #[test]
285    fn try_date_rejects_invalid_input() {
286        let error = GieQuery::new().try_date("2026/03/10").unwrap_err();
287        assert!(matches!(error, GieError::InvalidDateInput(_)));
288    }
289
290    #[test]
291    fn try_dataset_type_parses_supported_values() {
292        let query = GieQuery::new().try_dataset_type("NE").unwrap();
293        let pairs = query.to_debug_pairs(None);
294
295        assert!(pairs.contains(&("type", "ne".to_string())));
296    }
297
298    #[test]
299    fn try_dataset_type_rejects_invalid_input() {
300        let error = GieQuery::new().try_dataset_type("country").unwrap_err();
301        assert!(matches!(error, GieError::InvalidDatasetTypeInput(_)));
302    }
303
304    #[test]
305    fn try_page_and_try_size_reject_zero() {
306        assert!(matches!(
307            GieQuery::new().try_page(0).unwrap_err(),
308            GieError::InvalidPageInput(_)
309        ));
310        assert!(matches!(
311            GieQuery::new().try_size(0).unwrap_err(),
312            GieError::InvalidSizeInput(_)
313        ));
314    }
315
316    #[test]
317    fn initial_page_defaults_to_one() {
318        assert_eq!(GieQuery::new().initial_page().get(), 1);
319    }
320}