Skip to main content

lino_rest_api/
query.rs

1//! Collection query parsing (specification sections 6.2 - 6.4).
2//!
3//! Turns the reserved query parameters (`limit`, `offset`, `sort`, `fields`) and
4//! the free-form field filters of a request URL into a structured query.
5
6use lino_objects_codec::LinoValue;
7
8use crate::problem::LinoHttpError;
9
10/// Query parameters that are not field filters.
11pub const RESERVED_QUERY_PARAMETERS: [&str; 4] = ["limit", "offset", "sort", "fields"];
12
13/// Page size used when a request does not ask for one.
14pub const DEFAULT_LIMIT: usize = 20;
15
16/// Largest page a server will serve, whatever the request asks for.
17pub const MAX_LIMIT: usize = 100;
18
19/// The query parameters of a request, in the order they were sent.
20///
21/// A name may repeat, which is how `?tag=a&tag=b` asks for either tag.
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct QueryParams {
24    entries: Vec<(String, String)>,
25}
26
27impl QueryParams {
28    /// No parameters at all.
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Parse a raw query string, with or without its leading `?`.
34    ///
35    /// # Examples
36    ///
37    /// ```
38    /// use lino_rest_api::query::QueryParams;
39    ///
40    /// let query = QueryParams::parse("?tag=a&tag=b&limit=5");
41    /// assert_eq!(query.get_all("tag"), vec!["a", "b"]);
42    /// assert_eq!(query.get_last("limit"), Some("5"));
43    /// ```
44    pub fn parse(query_string: &str) -> Self {
45        let trimmed = query_string.strip_prefix('?').unwrap_or(query_string);
46        let mut entries = Vec::new();
47        for pair in trimmed.split('&') {
48            if pair.is_empty() {
49                continue;
50            }
51            let (name, value) = match pair.split_once('=') {
52                Some((name, value)) => (name, value),
53                None => (pair, ""),
54            };
55            entries.push((form_decode(name), form_decode(value)));
56        }
57        Self { entries }
58    }
59
60    /// Add one parameter, keeping any parameter of the same name.
61    pub fn append<N: Into<String>, V: Into<String>>(&mut self, name: N, value: V) {
62        self.entries.push((name.into(), value.into()));
63    }
64
65    /// Every value sent for a name, in order.
66    pub fn get_all(&self, name: &str) -> Vec<&str> {
67        self.entries
68            .iter()
69            .filter(|(existing, _)| existing == name)
70            .map(|(_, value)| value.as_str())
71            .collect()
72    }
73
74    /// The last value sent for a name, which is the one a scalar parameter means.
75    pub fn get_last(&self, name: &str) -> Option<&str> {
76        self.entries
77            .iter()
78            .rev()
79            .find(|(existing, _)| existing == name)
80            .map(|(_, value)| value.as_str())
81    }
82
83    /// Whether a name was sent at all.
84    pub fn contains(&self, name: &str) -> bool {
85        self.get_last(name).is_some()
86    }
87
88    /// The parameters, in the order they were sent.
89    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
90        self.entries
91            .iter()
92            .map(|(name, value)| (name.as_str(), value.as_str()))
93    }
94
95    /// The distinct parameter names, in the order they first appeared.
96    pub fn names(&self) -> Vec<&str> {
97        let mut names: Vec<&str> = Vec::new();
98        for (name, _) in &self.entries {
99            if !names.contains(&name.as_str()) {
100                names.push(name);
101            }
102        }
103        names
104    }
105
106    /// Whether any parameter was sent.
107    pub fn is_empty(&self) -> bool {
108        self.entries.is_empty()
109    }
110
111    /// How many parameters were sent, counting repetitions.
112    pub fn len(&self) -> usize {
113        self.entries.len()
114    }
115
116    /// Render the parameters back as a query string.
117    pub fn to_query_string(&self) -> String {
118        self.entries
119            .iter()
120            .map(|(name, value)| format!("{}={}", form_encode(name), form_encode(value)))
121            .collect::<Vec<_>>()
122            .join("&")
123    }
124}
125
126/// Percent-encode one query component the way a form does, with `+` for a space.
127pub fn form_encode(text: &str) -> String {
128    let mut encoded = String::with_capacity(text.len());
129    for byte in text.as_bytes() {
130        match byte {
131            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
132                encoded.push(*byte as char)
133            }
134            b' ' => encoded.push('+'),
135            other => encoded.push_str(&format!("%{other:02X}")),
136        }
137    }
138    encoded
139}
140
141/// Decode one query component, reading `+` as a space.
142pub fn form_decode(text: &str) -> String {
143    let bytes = text.as_bytes();
144    let mut decoded: Vec<u8> = Vec::with_capacity(bytes.len());
145    let mut index = 0;
146    while index < bytes.len() {
147        match bytes[index] {
148            b'+' => {
149                decoded.push(b' ');
150                index += 1;
151            }
152            b'%' if index + 2 < bytes.len() => {
153                match u8::from_str_radix(&text[index + 1..index + 3], 16) {
154                    Ok(byte) => {
155                        decoded.push(byte);
156                        index += 3;
157                    }
158                    Err(_) => {
159                        decoded.push(b'%');
160                        index += 1;
161                    }
162                }
163            }
164            byte => {
165                decoded.push(byte);
166                index += 1;
167            }
168        }
169    }
170    String::from_utf8_lossy(&decoded).into_owned()
171}
172
173/// One ordering key of a collection query.
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct SortKey {
176    /// Field the items are ordered by.
177    pub field: String,
178    /// Whether the order is descending.
179    pub descending: bool,
180}
181
182/// One field filter: a name and the value, or values, it must equal.
183#[derive(Debug, Clone, PartialEq)]
184pub struct Filter {
185    /// Field the items are filtered on.
186    pub field: String,
187    /// Accepted values; more than one means "any of these".
188    pub values: Vec<LinoValue>,
189}
190
191/// A parsed collection query.
192#[derive(Debug, Clone, PartialEq)]
193pub struct CollectionQuery {
194    /// Page size.
195    pub limit: usize,
196    /// Index of the first item of the page.
197    pub offset: usize,
198    /// Ordering keys, most significant first.
199    pub sort: Vec<SortKey>,
200    /// Sparse fieldset, or [`None`] when every field is requested.
201    pub fields: Option<Vec<String>>,
202    /// Field filters.
203    pub filters: Vec<Filter>,
204}
205
206impl Default for CollectionQuery {
207    fn default() -> Self {
208        Self {
209            limit: DEFAULT_LIMIT,
210            offset: 0,
211            sort: Vec::new(),
212            fields: None,
213            filters: Vec::new(),
214        }
215    }
216}
217
218/// Parse a filter value with the Links Notation scalar rules.
219///
220/// `?done=true` filters on the boolean, `?id=7` on the integer, and anything
221/// else stays a string.
222///
223/// # Examples
224///
225/// ```
226/// use lino_objects_codec::LinoValue;
227/// use lino_rest_api::query::parse_scalar;
228///
229/// assert_eq!(parse_scalar("true"), LinoValue::Bool(true));
230/// assert_eq!(parse_scalar("7"), LinoValue::Int(7));
231/// assert_eq!(parse_scalar("seven"), LinoValue::String("seven".into()));
232/// ```
233pub fn parse_scalar(raw: &str) -> LinoValue {
234    match raw {
235        "true" => return LinoValue::Bool(true),
236        "false" => return LinoValue::Bool(false),
237        "null" => return LinoValue::Null,
238        "" => return LinoValue::String(String::new()),
239        _ => {}
240    }
241    if let Ok(number) = raw.parse::<i64>() {
242        return LinoValue::Int(number);
243    }
244    if let Ok(number) = raw.parse::<f64>() {
245        // Rust parses "inf" and "nan", which Links Notation has no spelling for,
246        // so those stay the strings they were sent as.
247        if number.is_finite() {
248            return LinoValue::Float(number);
249        }
250    }
251    LinoValue::String(raw.to_string())
252}
253
254/// Parse a bounded non-negative integer query parameter.
255///
256/// # Errors
257///
258/// A `400` when the value is not a non-negative integer.
259pub fn parse_bounded_integer(
260    raw: Option<&str>,
261    fallback: usize,
262    name: &str,
263    maximum: Option<usize>,
264) -> Result<usize, LinoHttpError> {
265    let raw = match raw {
266        None | Some("") => return Ok(fallback),
267        Some(raw) => raw,
268    };
269    let value: usize = raw.parse().map_err(|_| {
270        LinoHttpError::new(
271            400,
272            Some(&format!(
273                "Query parameter \"{name}\" must be a non-negative integer"
274            )),
275        )
276    })?;
277    Ok(match maximum {
278        Some(maximum) => value.min(maximum),
279        None => value,
280    })
281}
282
283/// Parse a `sort` parameter into ordered sort keys.
284pub fn parse_sort(raw: Option<&str>) -> Vec<SortKey> {
285    let Some(raw) = raw.filter(|raw| !raw.is_empty()) else {
286        return Vec::new();
287    };
288    raw.split(',')
289        .map(str::trim)
290        .filter(|part| !part.is_empty())
291        .map(|part| match part.strip_prefix('-') {
292            Some(field) => SortKey {
293                field: field.to_string(),
294                descending: true,
295            },
296            None => SortKey {
297                field: part.to_string(),
298                descending: false,
299            },
300        })
301        .collect()
302}
303
304/// Parse a `fields` parameter into a sparse fieldset.
305pub fn parse_fields(raw: Option<&str>) -> Option<Vec<String>> {
306    let raw = raw.filter(|raw| !raw.is_empty())?;
307    let fields: Vec<String> = raw
308        .split(',')
309        .map(str::trim)
310        .filter(|part| !part.is_empty())
311        .map(str::to_string)
312        .collect();
313    if fields.is_empty() {
314        None
315    } else {
316        Some(fields)
317    }
318}
319
320/// Parse a full collection query from request query parameters.
321///
322/// # Errors
323///
324/// Whatever [`parse_bounded_integer`] refuses.
325pub fn parse_collection_query(
326    query: &QueryParams,
327    default_limit: usize,
328    max_limit: usize,
329) -> Result<CollectionQuery, LinoHttpError> {
330    let mut filters = Vec::new();
331    for name in query.names() {
332        if RESERVED_QUERY_PARAMETERS.contains(&name) {
333            continue;
334        }
335        filters.push(Filter {
336            field: name.to_string(),
337            values: query.get_all(name).into_iter().map(parse_scalar).collect(),
338        });
339    }
340
341    Ok(CollectionQuery {
342        limit: parse_bounded_integer(
343            query.get_last("limit"),
344            default_limit,
345            "limit",
346            Some(max_limit),
347        )?,
348        offset: parse_bounded_integer(query.get_last("offset"), 0, "offset", None)?,
349        sort: parse_sort(query.get_last("sort")),
350        fields: parse_fields(query.get_last("fields")),
351        filters,
352    })
353}