1use lino_objects_codec::LinoValue;
7
8use crate::problem::LinoHttpError;
9
10pub const RESERVED_QUERY_PARAMETERS: [&str; 4] = ["limit", "offset", "sort", "fields"];
12
13pub const DEFAULT_LIMIT: usize = 20;
15
16pub const MAX_LIMIT: usize = 100;
18
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct QueryParams {
24 entries: Vec<(String, String)>,
25}
26
27impl QueryParams {
28 pub fn new() -> Self {
30 Self::default()
31 }
32
33 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 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 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 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 pub fn contains(&self, name: &str) -> bool {
85 self.get_last(name).is_some()
86 }
87
88 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 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 pub fn is_empty(&self) -> bool {
108 self.entries.is_empty()
109 }
110
111 pub fn len(&self) -> usize {
113 self.entries.len()
114 }
115
116 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
126pub 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
141pub 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#[derive(Debug, Clone, PartialEq, Eq)]
175pub struct SortKey {
176 pub field: String,
178 pub descending: bool,
180}
181
182#[derive(Debug, Clone, PartialEq)]
184pub struct Filter {
185 pub field: String,
187 pub values: Vec<LinoValue>,
189}
190
191#[derive(Debug, Clone, PartialEq)]
193pub struct CollectionQuery {
194 pub limit: usize,
196 pub offset: usize,
198 pub sort: Vec<SortKey>,
200 pub fields: Option<Vec<String>>,
202 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
218pub 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 if number.is_finite() {
248 return LinoValue::Float(number);
249 }
250 }
251 LinoValue::String(raw.to_string())
252}
253
254pub 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
283pub 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
304pub 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
320pub 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}