Skip to main content

wme_models/
request.rs

1//! Request parameter types for API queries.
2//!
3//! This module provides types for building API requests with filters,
4//! field selection, and limits. These parameters allow you to customize
5//! API responses to only include the data you need.
6//!
7//! # Building Requests
8//!
9//! Use [`RequestParams`] with its builder methods to construct requests:
10//!
11//! ```
12//! use wme_models::RequestParams;
13//!
14//! let params = RequestParams::new()
15//!     .field("name")
16//!     .field("url")
17//!     .filter("in_language.identifier", "en")
18//!     .filter("is_part_of.identifier", "enwiki")
19//!     .limit(5);
20//! ```
21//!
22//! # Filters
23//!
24//! Filters narrow results to specific subsets. Field names use dot notation
25//! (e.g., `is_part_of.identifier`). Only single-value fields can be filtered.
26//!
27//! # Field Selection
28//!
29//! The `fields` parameter specifies which fields to include. When fields are
30//! specified, only those fields are returned (sparse fieldset). Omitting the
31//! fields parameter returns all available fields.
32//!
33//! # Limits
34//!
35//! Limits restrict the number of results. Default is 3, maximum is 10.
36//! Limits only work with On-demand endpoints.
37
38use serde::{Deserialize, Serialize};
39
40/// Filter for API requests to narrow down results.
41///
42/// Filters specify a field and value to match. Multiple filters can be
43/// combined to narrow results further. Filters use AND logic - all
44/// filters must match.
45///
46/// # Field Names
47///
48/// Use dot notation for nested fields:
49/// - `is_part_of.identifier`
50/// - `in_language.identifier`
51/// - `namespace.identifier`
52/// - `version.is_minor_edit`
53///
54/// # Example
55///
56/// ```
57/// use wme_models::{Filter, FilterValue};
58///
59/// let filter = Filter {
60///     field: "in_language.identifier".to_string(),
61///     value: FilterValue::String("en".to_string()),
62/// };
63/// ```
64#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
65pub struct Filter {
66    /// Field name to filter on (using dot notation, e.g., "is_part_of.identifier")
67    pub field: String,
68    /// Value to match for the specified field
69    pub value: FilterValue,
70}
71
72/// Value types that can be used in filters.
73///
74/// Supports strings, integers, floats, and booleans. Arrays and objects
75/// cannot be used in filters.
76///
77/// # Type Coercion
78///
79/// The `From` trait implementations allow easy conversion from standard types:
80///
81/// ```
82/// use wme_models::FilterValue;
83///
84/// let string_val: FilterValue = "en".into();
85/// let int_val: FilterValue = 42i64.into();
86/// let bool_val: FilterValue = true.into();
87/// ```
88#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
89#[serde(untagged)]
90pub enum FilterValue {
91    /// String value
92    String(String),
93    /// Integer value
94    Integer(i64),
95    /// Float value
96    Float(f64),
97    /// Boolean value
98    Boolean(bool),
99}
100
101/// Request parameters for API endpoints that support filtering, field selection, and limits.
102///
103/// This struct provides a builder pattern for constructing API requests.
104/// All fields are optional and will only be serialized if set.
105///
106/// # Builder Pattern
107///
108/// ```
109/// use wme_models::RequestParams;
110///
111/// let params = RequestParams::new()
112///     .field("name")
113///     .field("url")
114///     .filter("in_language.identifier", "en")
115///     .limit(5);
116/// ```
117#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Default)]
118pub struct RequestParams {
119    /// Fields to include in the response (dot notation)
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub fields: Option<Vec<String>>,
122    /// Filters to apply to narrow down results
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub filters: Option<Vec<Filter>>,
125    /// Maximum number of results to return (default: 3, max: 10, On-demand API only)
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub limit: Option<u32>,
128}
129
130impl RequestParams {
131    /// Create a new empty request parameters builder.
132    ///
133    /// # Example
134    ///
135    /// ```
136    /// use wme_models::RequestParams;
137    ///
138    /// let params = RequestParams::new();
139    /// ```
140    pub fn new() -> Self {
141        Self::default()
142    }
143
144    /// Add a field to include in the response.
145    ///
146    /// # Example
147    ///
148    /// ```
149    /// use wme_models::RequestParams;
150    ///
151    /// let params = RequestParams::new()
152    ///     .field("name")
153    ///     .field("url");
154    /// ```
155    pub fn field(mut self, field: impl Into<String>) -> Self {
156        self.fields.get_or_insert_with(Vec::new).push(field.into());
157        self
158    }
159
160    /// Add multiple fields to include in the response.
161    ///
162    /// # Example
163    ///
164    /// ```
165    /// use wme_models::RequestParams;
166    ///
167    /// let params = RequestParams::new()
168    ///     .fields(vec!["name", "url", "identifier"]);
169    /// ```
170    pub fn fields(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
171        self.fields
172            .get_or_insert_with(Vec::new)
173            .extend(fields.into_iter().map(Into::into));
174        self
175    }
176
177    /// Add a filter to narrow down results.
178    ///
179    /// # Example
180    ///
181    /// ```
182    /// use wme_models::RequestParams;
183    ///
184    /// let params = RequestParams::new()
185    ///     .filter("in_language.identifier", "en")
186    ///     .filter("is_part_of.identifier", "enwiki");
187    /// ```
188    pub fn filter(mut self, field: impl Into<String>, value: impl Into<FilterValue>) -> Self {
189        self.filters.get_or_insert_with(Vec::new).push(Filter {
190            field: field.into(),
191            value: value.into(),
192        });
193        self
194    }
195
196    /// Set the limit for number of results (On-demand API only, max 10).
197    ///
198    /// Values above 10 will be clamped to 10.
199    ///
200    /// # Example
201    ///
202    /// ```
203    /// use wme_models::RequestParams;
204    ///
205    /// let params = RequestParams::new().limit(5);
206    /// ```
207    pub fn limit(mut self, limit: u32) -> Self {
208        self.limit = Some(limit.min(10));
209        self
210    }
211}
212
213impl From<String> for FilterValue {
214    fn from(s: String) -> Self {
215        FilterValue::String(s)
216    }
217}
218
219impl From<&str> for FilterValue {
220    fn from(s: &str) -> Self {
221        FilterValue::String(s.to_string())
222    }
223}
224
225impl From<i64> for FilterValue {
226    fn from(i: i64) -> Self {
227        FilterValue::Integer(i)
228    }
229}
230
231impl From<i32> for FilterValue {
232    fn from(i: i32) -> Self {
233        FilterValue::Integer(i as i64)
234    }
235}
236
237impl From<u64> for FilterValue {
238    fn from(u: u64) -> Self {
239        FilterValue::Integer(u as i64)
240    }
241}
242
243impl From<u32> for FilterValue {
244    fn from(u: u32) -> Self {
245        FilterValue::Integer(u as i64)
246    }
247}
248
249impl From<f64> for FilterValue {
250    fn from(f: f64) -> Self {
251        FilterValue::Float(f)
252    }
253}
254
255impl From<bool> for FilterValue {
256    fn from(b: bool) -> Self {
257        FilterValue::Boolean(b)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_request_params_builder() {
267        let params = RequestParams::new()
268            .field("name")
269            .field("url")
270            .filter("in_language.identifier", "en")
271            .filter("is_part_of.identifier", "enwiki")
272            .limit(5);
273
274        assert_eq!(params.fields.as_ref().unwrap().len(), 2);
275        assert_eq!(params.filters.as_ref().unwrap().len(), 2);
276        assert_eq!(params.limit, Some(5));
277    }
278
279    #[test]
280    fn test_filter_value_conversions() {
281        let string_val: FilterValue = "test".into();
282        let int_val: FilterValue = 42i64.into();
283        let bool_val: FilterValue = true.into();
284
285        assert!(matches!(string_val, FilterValue::String(_)));
286        assert!(matches!(int_val, FilterValue::Integer(42)));
287        assert!(matches!(bool_val, FilterValue::Boolean(true)));
288    }
289
290    #[test]
291    fn test_filter_serialization() {
292        let filter = Filter {
293            field: "in_language.identifier".to_string(),
294            value: FilterValue::String("en".to_string()),
295        };
296
297        let json = serde_json::to_string(&filter).unwrap();
298        assert!(json.contains("in_language.identifier"));
299        assert!(json.contains("en"));
300    }
301
302    #[test]
303    fn test_request_params_serialization() {
304        let params = RequestParams::new()
305            .field("name")
306            .field("url")
307            .filter("in_language.identifier", "en")
308            .limit(5);
309
310        let json = serde_json::to_string(&params).unwrap();
311        assert!(json.contains("fields"));
312        assert!(json.contains("name"));
313        assert!(json.contains("url"));
314        assert!(json.contains("filters"));
315        assert!(json.contains("limit"));
316    }
317
318    #[test]
319    fn test_limit_clamping() {
320        let params = RequestParams::new().limit(15);
321        assert_eq!(params.limit, Some(10)); // Clamped to max
322    }
323}