1use std::num::NonZeroU32;
2use std::ops::Deref;
3
4use serde::Serialize;
5
6use crate::error::GieError;
7
8use super::date_range::DateRange;
9use super::serde_ext::{serialize_optional_dataset_type, serialize_optional_date};
10use super::types::{DatasetType, DateFilter, GieDate, format_date, parse_dataset_type, parse_date};
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct QueryText(String);
15
16impl QueryText {
17 pub fn try_new(value: impl Into<String>) -> Result<Self, GieError> {
22 parse_required_text_filter("value", value.into())
23 }
24
25 pub fn as_str(&self) -> &str {
27 &self.0
28 }
29
30 fn parse_lossy(value: String) -> Option<Self> {
31 let trimmed = value.trim();
32 if trimmed.is_empty() {
33 return None;
34 }
35 if trimmed.len() == value.len() {
36 return Some(Self(value));
37 }
38 Some(Self(trimmed.to_string()))
39 }
40}
41
42impl AsRef<str> for QueryText {
43 fn as_ref(&self) -> &str {
44 self.as_str()
45 }
46}
47
48impl Deref for QueryText {
49 type Target = str;
50
51 fn deref(&self) -> &Self::Target {
52 self.as_str()
53 }
54}
55
56#[must_use = "query builders are immutable; use the returned value"]
58#[derive(Debug, Clone, Default)]
59pub struct GieQuery {
60 country: Option<QueryText>,
61 company: Option<QueryText>,
62 facility: Option<QueryText>,
63 dataset_type: Option<DatasetType>,
64 date_filter: Option<DateFilter>,
65 page: Option<NonZeroU32>,
66 size: Option<NonZeroU32>,
67}
68
69impl GieQuery {
70 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn country(mut self, country: impl Into<String>) -> Self {
80 self.country = QueryText::parse_lossy(country.into());
81 self
82 }
83
84 pub fn try_country(mut self, country: impl Into<String>) -> Result<Self, GieError> {
86 self.country = Some(parse_required_text_filter("country", country.into())?);
87 Ok(self)
88 }
89
90 pub fn company(mut self, company: impl Into<String>) -> Self {
95 self.company = QueryText::parse_lossy(company.into());
96 self
97 }
98
99 pub fn try_company(mut self, company: impl Into<String>) -> Result<Self, GieError> {
101 self.company = Some(parse_required_text_filter("company", company.into())?);
102 Ok(self)
103 }
104
105 pub fn facility(mut self, facility: impl Into<String>) -> Self {
110 self.facility = QueryText::parse_lossy(facility.into());
111 self
112 }
113
114 pub fn try_facility(mut self, facility: impl Into<String>) -> Result<Self, GieError> {
116 self.facility = Some(parse_required_text_filter("facility", facility.into())?);
117 Ok(self)
118 }
119
120 pub fn dataset_type(mut self, dataset_type: DatasetType) -> Self {
122 self.dataset_type = Some(dataset_type);
123 self
124 }
125
126 pub fn try_dataset_type(mut self, dataset_type: impl AsRef<str>) -> Result<Self, GieError> {
128 self.dataset_type = Some(
129 parse_dataset_type(dataset_type.as_ref()).map_err(GieError::InvalidDatasetTypeInput)?,
130 );
131 Ok(self)
132 }
133
134 pub fn without_dataset_type(mut self) -> Self {
136 self.dataset_type = None;
137 self
138 }
139
140 pub fn date(mut self, date: GieDate) -> Self {
142 self.date_filter = Some(DateFilter::Day(date));
143 self
144 }
145
146 pub fn try_date(mut self, date: impl AsRef<str>) -> Result<Self, GieError> {
148 self.date_filter = Some(DateFilter::Day(
149 parse_date(date.as_ref()).map_err(GieError::InvalidDateInput)?,
150 ));
151 Ok(self)
152 }
153
154 pub fn range(mut self, from: GieDate, to: GieDate) -> Result<Self, GieError> {
156 let range = DateRange::new(from, to)?;
157 self.date_filter = Some(DateFilter::Range(range));
158 Ok(self)
159 }
160
161 pub fn try_range(self, from: impl AsRef<str>, to: impl AsRef<str>) -> Result<Self, GieError> {
163 let from = parse_date(from.as_ref()).map_err(GieError::InvalidDateInput)?;
164 let to = parse_date(to.as_ref()).map_err(GieError::InvalidDateInput)?;
165 self.range(from, to)
166 }
167
168 pub fn page(mut self, page: NonZeroU32) -> Self {
170 self.page = Some(page);
171 self
172 }
173
174 pub fn try_page(mut self, page: u32) -> Result<Self, GieError> {
176 self.page = Some(NonZeroU32::new(page).ok_or_else(|| {
177 GieError::InvalidPageInput("page must be greater than zero".to_string())
178 })?);
179 Ok(self)
180 }
181
182 pub fn size(mut self, size: NonZeroU32) -> Self {
184 self.size = Some(size);
185 self
186 }
187
188 pub fn try_size(mut self, size: u32) -> Result<Self, GieError> {
190 self.size = Some(NonZeroU32::new(size).ok_or_else(|| {
191 GieError::InvalidSizeInput("size must be greater than zero".to_string())
192 })?);
193 Ok(self)
194 }
195
196 pub(crate) fn initial_page(&self) -> NonZeroU32 {
197 self.page.unwrap_or_else(default_page)
198 }
199
200 pub(crate) fn as_params_with_page(
201 &self,
202 page_override: Option<NonZeroU32>,
203 ) -> GieQueryParams<'_> {
204 let (date, from, to) = match self.date_filter {
205 Some(DateFilter::Day(value)) => (Some(value), None, None),
206 Some(DateFilter::Range(value)) => (None, Some(value.from()), Some(value.to())),
207 None => (None, None, None),
208 };
209
210 GieQueryParams {
211 country: self.country.as_deref(),
212 company: self.company.as_deref(),
213 facility: self.facility.as_deref(),
214 dataset_type: self.dataset_type,
215 date,
216 from,
217 to,
218 page: page_override.or(self.page),
219 size: self.size,
220 }
221 }
222
223 pub(crate) fn visit_debug_pairs(
224 &self,
225 page_override: Option<NonZeroU32>,
226 mut visit: impl FnMut(&'static str, &str),
227 ) {
228 if let Some(value) = self.country.as_deref() {
229 visit("country", value);
230 }
231 if let Some(value) = self.company.as_deref() {
232 visit("company", value);
233 }
234 if let Some(value) = self.facility.as_deref() {
235 visit("facility", value);
236 }
237 if let Some(value) = self.dataset_type {
238 visit("type", value.as_str());
239 }
240
241 match self.date_filter {
242 Some(DateFilter::Day(value)) => {
243 let formatted_date = format_date(value);
244 visit("date", &formatted_date);
245 }
246 Some(DateFilter::Range(value)) => {
247 let from = format_date(value.from());
248 let to = format_date(value.to());
249 visit("from", &from);
250 visit("to", &to);
251 }
252 None => {}
253 }
254
255 if let Some(value) = page_override.or(self.page) {
256 let page = value.to_string();
257 visit("page", &page);
258 }
259 if let Some(value) = self.size {
260 let size = value.to_string();
261 visit("size", &size);
262 }
263 }
264}
265
266fn default_page() -> NonZeroU32 {
267 NonZeroU32::new(1).expect("1 is non-zero")
268}
269
270fn parse_required_text_filter(field_name: &str, value: String) -> Result<QueryText, GieError> {
271 let trimmed = value.trim();
272 if trimmed.is_empty() {
273 return Err(GieError::InvalidTextFilterInput(format!(
274 "{field_name} must not be blank"
275 )));
276 }
277 if trimmed.len() == value.len() {
278 return Ok(QueryText(value));
279 }
280 Ok(QueryText(trimmed.to_string()))
281}
282
283#[derive(Debug, Serialize)]
284pub(crate) struct GieQueryParams<'a> {
285 #[serde(skip_serializing_if = "Option::is_none")]
286 country: Option<&'a str>,
287 #[serde(skip_serializing_if = "Option::is_none")]
288 company: Option<&'a str>,
289 #[serde(skip_serializing_if = "Option::is_none")]
290 facility: Option<&'a str>,
291 #[serde(
292 rename = "type",
293 skip_serializing_if = "Option::is_none",
294 serialize_with = "serialize_optional_dataset_type"
295 )]
296 dataset_type: Option<DatasetType>,
297 #[serde(
298 skip_serializing_if = "Option::is_none",
299 serialize_with = "serialize_optional_date"
300 )]
301 date: Option<GieDate>,
302 #[serde(
303 skip_serializing_if = "Option::is_none",
304 serialize_with = "serialize_optional_date"
305 )]
306 from: Option<GieDate>,
307 #[serde(
308 skip_serializing_if = "Option::is_none",
309 serialize_with = "serialize_optional_date"
310 )]
311 to: Option<GieDate>,
312 #[serde(skip_serializing_if = "Option::is_none")]
313 page: Option<NonZeroU32>,
314 #[serde(skip_serializing_if = "Option::is_none")]
315 size: Option<NonZeroU32>,
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 fn collect_debug_pairs(
323 query: &GieQuery,
324 page_override: Option<NonZeroU32>,
325 ) -> Vec<(&'static str, String)> {
326 let mut pairs = Vec::new();
327 query.visit_debug_pairs(page_override, |key, value| {
328 pairs.push((key, value.to_string()));
329 });
330 pairs
331 }
332
333 fn test_date(value: &str) -> GieDate {
334 parse_date(value).unwrap()
335 }
336
337 #[test]
338 fn query_params_are_mapped_to_expected_keys() {
339 let query = GieQuery::new()
340 .country("DE")
341 .company("Comp")
342 .facility("Fac")
343 .dataset_type(DatasetType::Eu)
344 .range(test_date("2026-03-01"), test_date("2026-03-10"))
345 .unwrap()
346 .try_page(2)
347 .unwrap()
348 .try_size(50)
349 .unwrap();
350
351 let pairs = collect_debug_pairs(&query, None);
352 assert!(pairs.contains(&("country", "DE".to_string())));
353 assert!(pairs.contains(&("company", "Comp".to_string())));
354 assert!(pairs.contains(&("facility", "Fac".to_string())));
355 assert!(pairs.contains(&("type", "eu".to_string())));
356 assert!(pairs.contains(&("from", "2026-03-01".to_string())));
357 assert!(pairs.contains(&("to", "2026-03-10".to_string())));
358 assert!(pairs.contains(&("page", "2".to_string())));
359 assert!(pairs.contains(&("size", "50".to_string())));
360 assert!(!pairs.iter().any(|(key, _)| *key == "date"));
361 }
362
363 #[test]
364 fn date_builder_replaces_range_filter() {
365 let query = GieQuery::new()
366 .range(test_date("2026-03-01"), test_date("2026-03-10"))
367 .unwrap()
368 .date(test_date("2026-03-10"));
369
370 let pairs = collect_debug_pairs(&query, None);
371 assert!(pairs.contains(&("date", "2026-03-10".to_string())));
372 assert!(!pairs.iter().any(|(key, _)| *key == "from"));
373 assert!(!pairs.iter().any(|(key, _)| *key == "to"));
374 }
375
376 #[test]
377 fn try_range_rejects_invalid_order() {
378 let error = GieQuery::new()
379 .try_range("2026-03-10", "2026-03-01")
380 .unwrap_err();
381
382 assert!(matches!(error, GieError::InvalidDateRangeInput(_)));
383 }
384
385 #[test]
386 fn try_date_rejects_invalid_input() {
387 let error = GieQuery::new().try_date("2026/03/10").unwrap_err();
388 assert!(matches!(error, GieError::InvalidDateInput(_)));
389 }
390
391 #[test]
392 fn try_dataset_type_parses_supported_values() {
393 let query = GieQuery::new().try_dataset_type("NE").unwrap();
394 let pairs = collect_debug_pairs(&query, None);
395
396 assert!(pairs.contains(&("type", "ne".to_string())));
397 }
398
399 #[test]
400 fn try_dataset_type_rejects_invalid_input() {
401 let error = GieQuery::new().try_dataset_type("country").unwrap_err();
402 assert!(matches!(error, GieError::InvalidDatasetTypeInput(_)));
403 }
404
405 #[test]
406 fn try_page_and_try_size_reject_zero() {
407 assert!(matches!(
408 GieQuery::new().try_page(0).unwrap_err(),
409 GieError::InvalidPageInput(_)
410 ));
411 assert!(matches!(
412 GieQuery::new().try_size(0).unwrap_err(),
413 GieError::InvalidSizeInput(_)
414 ));
415 }
416
417 #[test]
418 fn initial_page_defaults_to_one() {
419 assert_eq!(GieQuery::new().initial_page().get(), 1);
420 }
421
422 #[test]
423 fn page_override_wins_in_debug_pairs() {
424 let query = GieQuery::new().try_page(2).unwrap();
425 let override_page = NonZeroU32::new(7).unwrap();
426 let pairs = collect_debug_pairs(&query, Some(override_page));
427
428 assert!(pairs.contains(&("page", "7".to_string())));
429 assert!(!pairs.contains(&("page", "2".to_string())));
430 }
431
432 #[test]
433 fn text_filters_are_trimmed_and_blank_values_are_dropped() {
434 let query = GieQuery::new()
435 .country(" DE ")
436 .company(" ")
437 .facility(" Site ");
438 let pairs = collect_debug_pairs(&query, None);
439
440 assert!(pairs.contains(&("country", "DE".to_string())));
441 assert!(pairs.contains(&("facility", "Site".to_string())));
442 assert!(!pairs.iter().any(|(key, _)| *key == "company"));
443 }
444
445 #[test]
446 fn try_text_filters_reject_blank_values() {
447 assert!(matches!(
448 GieQuery::new().try_country(" ").unwrap_err(),
449 GieError::InvalidTextFilterInput(_)
450 ));
451 assert!(matches!(
452 GieQuery::new().try_company(" ").unwrap_err(),
453 GieError::InvalidTextFilterInput(_)
454 ));
455 assert!(matches!(
456 GieQuery::new().try_facility(" ").unwrap_err(),
457 GieError::InvalidTextFilterInput(_)
458 ));
459 }
460
461 #[test]
462 fn query_text_is_trimmed_on_construction() {
463 let value = QueryText::try_new(" DE ").unwrap();
464 assert_eq!(value.as_str(), "DE");
465 }
466}