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(¶ms).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}