query_lite/
lib.rs

1pub mod error;
2
3use error::{Error, Result};
4use indexmap::IndexMap;
5use std::str::FromStr;
6use url::form_urlencoded;
7
8// Main types
9#[derive(Clone, Debug, PartialEq)]
10pub struct Query {
11    pub parameters: Parameters,
12    pub order: Order,
13    pub limit: usize,
14    pub offset: usize,
15}
16
17impl Query {
18    pub fn new() -> Self {
19        Self {
20            parameters: Parameters::new(),
21            order: Order::new(),
22            limit: Parameters::DEFAULT_LIMIT,
23            offset: Parameters::DEFAULT_OFFSET,
24        }
25    }
26
27    pub fn init(parameters: Parameters, order: Order, limit: usize, offset: usize) -> Self {
28        Self {
29            parameters,
30            order,
31            limit,
32            offset,
33        }
34    }
35
36    pub fn to_http(&self) -> String {
37        let mut params_str = self
38            .parameters
39            .inner()
40            .iter()
41            .filter(|(_, param)| param.values().len() > 0)
42            .map(|(key, param)| {
43                let similarity_str = param.similarity().to_string();
44                let values_str = param
45                    .values()
46                    .iter()
47                    .map(|v| url_encode(v))
48                    .collect::<Vec<String>>()
49                    .join(&format!("{COMMA}"));
50                format!("{key}{EQUAL}{similarity_str}{COLON}{values_str}",)
51            })
52            .collect::<Vec<String>>()
53            .join("&");
54
55        let order_str = self
56            .order
57            .inner()
58            .iter()
59            .filter(|(name, _)| name.len() > 0)
60            .map(|(name, sort_order)| format!("{name}{COLON}{}", sort_order.to_string()))
61            .collect::<Vec<String>>()
62            .join(&format!("{COMMA}"));
63
64        if params_str.len() > 0 {
65            params_str.push_str(&format!("{AMPERSAND}"));
66        }
67
68        if order_str.len() > 0 {
69            params_str.push_str(&format!("{}{EQUAL}{}", Parameters::ORDER, order_str));
70            params_str.push_str(&format!("{AMPERSAND}"));
71        }
72
73        format!(
74            "{params_str}{}{EQUAL}{}{AMPERSAND}{}{EQUAL}{}",
75            Parameters::LIMIT,
76            self.limit,
77            Parameters::OFFSET,
78            self.offset,
79        )
80    }
81
82    // name=contains:damian&surname=equals:black,steel,wood&order=date_created:desc&limit=40&offset=0
83    pub fn from_http(search: String) -> Result<Self> {
84        let mut query = Self::new();
85        let trimmed_search = search.trim_start_matches(QUESTION).trim();
86
87        if trimmed_search.is_empty() {
88            return Ok(query);
89        }
90
91        for k_v in trimmed_search.split(AMPERSAND) {
92            let trimmed_kv = k_v.trim();
93            if trimmed_kv.is_empty() {
94                continue;
95            }
96
97            let mut parts = trimmed_kv.splitn(2, EQUAL);
98            if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
99                let trimmed_key = key.trim();
100                let trimmed_value = value.trim();
101
102                if trimmed_key.is_empty() || trimmed_value.is_empty() {
103                    continue;
104                }
105
106                match trimmed_key {
107                    Parameters::ORDER => {
108                        // Check if the value looks like a sort field format (contains colon)
109                        if !trimmed_value.contains(COLON) {
110                            // Fail on clearly invalid formats (like "invalid")
111                            return Err(Error::InvalidOrderField(trimmed_value.into()));
112                        }
113
114                        if let Ok(order) = Order::from_str(trimmed_value) {
115                            query.order = order;
116                        }
117                        // Skip malformed sort fields (like ":desc")
118                    }
119                    Parameters::LIMIT => {
120                        query.limit = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_LIMIT);
121                    }
122                    Parameters::OFFSET => {
123                        query.offset = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_OFFSET);
124                    }
125                    _k => {
126                        // Check if this is a similarity-based parameter (contains colon)
127                        if trimmed_value.contains(COLON) {
128                            // Parse as similarity-based parameter
129                            let Parameter(similarity, values) = parse_parameter(trimmed_value)?;
130                            // Only add parameters that have values
131                            if values.is_empty() {
132                                continue;
133                            }
134                            // Replace any existing parameter (similarity-based takes precedence)
135                            query
136                                .parameters
137                                .0
138                                .insert(trimmed_key.to_string(), Parameter(similarity, values));
139                        } else {
140                            // Handle as normal query parameter (default to equals similarity)
141                            let decoded_value = url_decode(trimmed_value);
142
143                            // Check if parameter already exists and is not similarity-based
144                            if let Some(existing_param) =
145                                query.parameters.0.get_mut(&trimmed_key.to_string())
146                            {
147                                // Only append if the existing parameter is also equals similarity
148                                if *existing_param.similarity() == Similarity::Equals {
149                                    existing_param.1.push(decoded_value);
150                                }
151                                // If existing parameter is similarity-based, ignore this normal parameter
152                            } else {
153                                // Create new parameter with equals similarity
154                                query.parameters.0.insert(
155                                    trimmed_key.to_string(),
156                                    Parameter::init(Similarity::Equals, vec![decoded_value]),
157                                );
158                            }
159                        }
160                    }
161                }
162            } else {
163                return Err(Error::InvalidSearchParameters(search));
164            }
165        }
166
167        Ok(query)
168    }
169
170    #[cfg(feature = "sql")]
171    pub fn to_sql(&self) -> String {
172        let mut sql_parts = Vec::new();
173
174        // Build WHERE clause from parameters
175        if let Some(where_clause) = self.where_clause() {
176            sql_parts.push(format!("WHERE {}", where_clause));
177        }
178
179        // Build ORDER BY clause from order
180        if let Some(order_clause) = self.order_clause() {
181            sql_parts.push(format!("ORDER BY {}", order_clause));
182        }
183
184        // Add LIMIT and OFFSET
185        sql_parts.push(format!("LIMIT ? OFFSET ?"));
186
187        sql_parts.join(" ")
188    }
189
190    #[cfg(feature = "sql")]
191    pub fn where_clause(&self) -> Option<String> {
192        let mut conditions = Vec::new();
193
194        for (key, param) in &self.parameters.0 {
195            let similarity = param.similarity();
196            let values = param.values();
197            if values.is_empty() {
198                continue;
199            }
200
201            let condition = match similarity {
202                Similarity::Equals => {
203                    if values.len() == 1 {
204                        if values[0] == "null" {
205                            format!("{} IS ?", key)
206                        } else {
207                            format!("{} = ?", key)
208                        }
209                    } else {
210                        let placeholders = vec!["?"; values.len()].join(", ");
211                        format!("{} IN ({})", key, placeholders)
212                    }
213                }
214                Similarity::Contains => {
215                    if values.len() == 1 {
216                        format!("{} LIKE ?", key)
217                    } else {
218                        let like_conditions: Vec<String> =
219                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
220                        format!("({})", like_conditions.join(" OR "))
221                    }
222                }
223                Similarity::StartsWith => {
224                    if values.len() == 1 {
225                        format!("{} LIKE ?", key)
226                    } else {
227                        let like_conditions: Vec<String> =
228                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
229                        format!("({})", like_conditions.join(" OR "))
230                    }
231                }
232                Similarity::EndsWith => {
233                    if values.len() == 1 {
234                        format!("{} LIKE ?", key)
235                    } else {
236                        let like_conditions: Vec<String> =
237                            values.iter().map(|_| format!("{} LIKE ?", key)).collect();
238                        format!("({})", like_conditions.join(" OR "))
239                    }
240                }
241                Similarity::Between => {
242                    if values.len() >= 2 {
243                        // Group values into pairs, ignoring any odd value
244                        let pairs: Vec<&[String]> = values.chunks(2).collect();
245                        let between_conditions: Vec<String> = pairs
246                            .iter()
247                            .map(|pair| {
248                                if pair.len() == 2 {
249                                    format!("{} BETWEEN ? AND ?", key)
250                                } else {
251                                    String::new() // Skip incomplete pairs
252                                }
253                            })
254                            .filter(|condition| !condition.is_empty())
255                            .collect();
256
257                        if between_conditions.is_empty() {
258                            continue; // Skip if no valid pairs
259                        } else if between_conditions.len() == 1 {
260                            between_conditions[0].clone()
261                        } else {
262                            format!("({})", between_conditions.join(" OR "))
263                        }
264                    } else {
265                        continue; // Skip invalid between conditions
266                    }
267                }
268                Similarity::Lesser => {
269                    if values.len() == 1 {
270                        format!("{} < ?", key)
271                    } else {
272                        let conditions: Vec<String> =
273                            values.iter().map(|_| format!("{} < ?", key)).collect();
274                        format!("({})", conditions.join(" OR "))
275                    }
276                }
277                Similarity::LesserOrEqual => {
278                    if values.len() == 1 {
279                        format!("{} <= ?", key)
280                    } else {
281                        let conditions: Vec<String> =
282                            values.iter().map(|_| format!("{} <= ?", key)).collect();
283                        format!("({})", conditions.join(" OR "))
284                    }
285                }
286                Similarity::Greater => {
287                    if values.len() == 1 {
288                        format!("{} > ?", key)
289                    } else {
290                        let conditions: Vec<String> =
291                            values.iter().map(|_| format!("{} > ?", key)).collect();
292                        format!("({})", conditions.join(" OR "))
293                    }
294                }
295                Similarity::GreaterOrEqual => {
296                    if values.len() == 1 {
297                        format!("{} >= ?", key)
298                    } else {
299                        let conditions: Vec<String> =
300                            values.iter().map(|_| format!("{} >= ?", key)).collect();
301                        format!("({})", conditions.join(" OR "))
302                    }
303                }
304            };
305
306            conditions.push(condition);
307        }
308
309        if conditions.is_empty() {
310            None
311        } else {
312            Some(conditions.join(" AND "))
313        }
314    }
315
316    #[cfg(feature = "sql")]
317    pub fn order_clause(&self) -> Option<String> {
318        let mut order_parts = Vec::new();
319
320        for (name, sort_order) in &self.order.0 {
321            if !name.is_empty() {
322                let direction = match sort_order {
323                    SortOrder::Ascending => "ASC",
324                    SortOrder::Descending => "DESC",
325                };
326                order_parts.push(format!("{} {}", name, direction));
327            }
328        }
329
330        if order_parts.is_empty() {
331            None
332        } else {
333            Some(order_parts.join(", "))
334        }
335    }
336
337    #[cfg(feature = "sql")]
338    pub fn to_values(&self) -> Vec<SqlValue> {
339        let mut sql_values = self.parameter_values();
340        sql_values.extend(self.pagination_values());
341        sql_values
342    }
343
344    #[cfg(feature = "sql")]
345    /// Get SQL values for parameters only (without limit and offset)
346    pub fn parameter_values(&self) -> Vec<SqlValue> {
347        let mut sql_values = Vec::new();
348
349        for (_k, param) in self.parameters.inner() {
350            let param_similarity = param.similarity();
351            let param_values = param.values();
352            for cur_val in param_values {
353                // Skip empty values
354                if cur_val.trim().is_empty() {
355                    continue;
356                }
357
358                if cur_val == "null" {
359                    sql_values.push(SqlValue::Null);
360                    continue;
361                }
362
363                let sql_value = match *param_similarity {
364                    Similarity::Contains => SqlValue::Text(format!("%{}%", cur_val)),
365                    Similarity::StartsWith => SqlValue::Text(format!("{}%", cur_val)),
366                    Similarity::EndsWith => SqlValue::Text(format!("%{}", cur_val)),
367                    _ => {
368                        // Try to parse as integer first, then float, then text
369                        if let Ok(i) = cur_val.parse::<i64>() {
370                            SqlValue::Integer(i)
371                        } else if let Ok(f) = cur_val.parse::<f64>() {
372                            SqlValue::Real(f)
373                        } else {
374                            SqlValue::Text(cur_val.clone())
375                        }
376                    }
377                };
378
379                sql_values.push(sql_value);
380            }
381        }
382
383        sql_values
384    }
385
386    #[cfg(feature = "sql")]
387    /// Get SQL values for pagination (limit and offset only)
388    pub fn pagination_values(&self) -> Vec<SqlValue> {
389        vec![
390            SqlValue::Integer(self.limit as i64),
391            SqlValue::Integer(self.offset as i64),
392        ]
393    }
394
395    #[cfg(feature = "sql")]
396    /// Get the total number of SQL parameter values (parameters + pagination)
397    /// This counts only non-empty values, matching the behavior of to_values()
398    pub fn total_parameters(&self) -> usize {
399        let parameter_count: usize = self
400            .parameters
401            .inner()
402            .values()
403            .map(|param| {
404                param
405                    .values()
406                    .iter()
407                    .filter(|v| !v.trim().is_empty())
408                    .count()
409            })
410            .sum();
411
412        parameter_count + 2 // +2 for limit and offset
413    }
414}
415
416#[derive(Clone, Debug, PartialEq)]
417pub struct Parameters(IndexMap<String, Parameter>);
418
419impl Parameters {
420    pub const ORDER: &str = "order";
421    pub const LIMIT: &str = "limit";
422    pub const OFFSET: &str = "offset";
423
424    pub const EXCLUDE: [&str; 3] = [Parameters::ORDER, Parameters::LIMIT, Parameters::OFFSET];
425
426    pub const DEFAULT_LIMIT: usize = 50;
427    pub const DEFAULT_OFFSET: usize = 0;
428
429    pub fn new() -> Self {
430        Self(IndexMap::new())
431    }
432
433    pub fn inner(&self) -> &IndexMap<String, Parameter> {
434        &self.0
435    }
436
437    pub fn inner_mut(&mut self) -> &mut IndexMap<String, Parameter> {
438        &mut self.0
439    }
440
441    pub fn equals(&mut self, key: String, values: Vec<String>) -> &mut Self {
442        self.0
443            .insert(key, Parameter::init(Similarity::Equals, values));
444        self
445    }
446
447    pub fn contains(&mut self, key: String, values: Vec<String>) -> &mut Self {
448        self.0
449            .insert(key, Parameter::init(Similarity::Contains, values));
450        self
451    }
452
453    pub fn starts_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
454        self.0
455            .insert(key, Parameter::init(Similarity::StartsWith, values));
456        self
457    }
458
459    pub fn ends_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
460        self.0
461            .insert(key, Parameter::init(Similarity::EndsWith, values));
462        self
463    }
464
465    pub fn between(&mut self, key: String, values: Vec<String>) -> &mut Self {
466        self.0
467            .insert(key, Parameter::init(Similarity::Between, values));
468        self
469    }
470
471    pub fn lesser(&mut self, key: String, values: Vec<String>) -> &mut Self {
472        self.0
473            .insert(key, Parameter::init(Similarity::Lesser, values));
474        self
475    }
476
477    pub fn lesser_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
478        self.0
479            .insert(key, Parameter::init(Similarity::LesserOrEqual, values));
480        self
481    }
482
483    pub fn greater(&mut self, key: String, values: Vec<String>) -> &mut Self {
484        self.0
485            .insert(key, Parameter::init(Similarity::Greater, values));
486        self
487    }
488
489    pub fn greater_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
490        self.0
491            .insert(key, Parameter::init(Similarity::GreaterOrEqual, values));
492        self
493    }
494
495    pub fn keep(&self, keys: Vec<String>) -> Self {
496        let mut result = Self::new();
497        for key in keys {
498            if let Some(value) = self.0.get(&key) {
499                result.0.insert(key, value.clone());
500            }
501        }
502        result
503    }
504
505    pub fn remove(&self, keys: Vec<String>) -> Self {
506        let mut result = self.clone();
507        for key in keys {
508            result.0.shift_remove(&key);
509        }
510        result
511    }
512}
513
514impl Default for Parameters {
515    fn default() -> Self {
516        Self::new()
517    }
518}
519
520impl FromStr for Parameters {
521    type Err = Error;
522
523    // EXAMPLE INPUT
524    // name=contains:damian&surname=equals:black,steel,wood&order=date_created:desc&limit=40&offset=0
525    fn from_str(s: &str) -> Result<Self> {
526        let trimmed = s.trim();
527        if trimmed.is_empty() {
528            return Ok(Parameters::new());
529        }
530
531        let str_parameters: Vec<&str> = trimmed.split(AMPERSAND).collect();
532        let mut parameters: Self = Parameters(IndexMap::new());
533
534        for str_param in str_parameters {
535            let trimmed_param = str_param.trim();
536            if trimmed_param.is_empty() {
537                continue;
538            }
539
540            let mut parts = trimmed_param.splitn(2, EQUAL);
541            let (key, value) = match (parts.next(), parts.next()) {
542                (Some(k), Some(v)) => (k, v),
543                _ => return Err(Error::InvalidParameter(trimmed_param.into())),
544            };
545
546            let trimmed_key = key.trim();
547            if trimmed_key.is_empty() || Parameters::EXCLUDE.contains(&trimmed_key) {
548                continue;
549            }
550
551            let Parameter(similarity, values) = parse_parameter(value)?;
552            // Only add parameters that have values
553            if values.is_empty() {
554                continue;
555            }
556
557            parameters
558                .0
559                .insert(trimmed_key.to_string(), Parameter(similarity, values));
560        }
561
562        Ok(parameters)
563    }
564}
565
566#[derive(Clone, Debug, PartialEq)]
567pub struct Parameter(Similarity, Vec<String>);
568
569impl Parameter {
570    pub fn init(similarity: Similarity, values: Vec<String>) -> Self {
571        Self(similarity, values)
572    }
573
574    pub fn similarity(&self) -> &Similarity {
575        &self.0
576    }
577
578    pub fn values(&self) -> &Vec<String> {
579        &self.1
580    }
581
582    pub fn values_mut(&mut self) -> &mut Vec<String> {
583        &mut self.1
584    }
585}
586
587#[derive(Clone, Debug, PartialEq)]
588pub struct Order(IndexMap<String, SortOrder>);
589
590impl Order {
591    pub fn new() -> Self {
592        Self(IndexMap::new())
593    }
594
595    pub fn inner(&self) -> &IndexMap<String, SortOrder> {
596        &self.0
597    }
598
599    pub fn inner_mut(&mut self) -> &mut IndexMap<String, SortOrder> {
600        &mut self.0
601    }
602
603    pub fn ascending(&mut self, name: String) -> &mut Self {
604        self.0.insert(name, SortOrder::Ascending);
605        self
606    }
607
608    pub fn descending(&mut self, name: String) -> &mut Self {
609        self.0.insert(name, SortOrder::Descending);
610        self
611    }
612
613    pub fn keep(&self, keys: Vec<String>) -> Self {
614        let mut result = Self::new();
615        for key in keys {
616            if let Some(value) = self.0.get(&key) {
617                result.0.insert(key, value.clone());
618            }
619        }
620        result
621    }
622
623    pub fn remove(&self, keys: Vec<String>) -> Self {
624        let mut result = self.clone();
625        for key in keys {
626            result.0.shift_remove(&key);
627        }
628        result
629    }
630}
631
632impl Default for Order {
633    fn default() -> Self {
634        Self::new()
635    }
636}
637
638impl FromStr for Order {
639    type Err = Error;
640
641    // EXAMPLE INPUT
642    // date_created:desc,name:asc,surname:asc
643    fn from_str(s: &str) -> Result<Self> {
644        let trimmed = s.trim();
645        if trimmed.is_empty() {
646            return Ok(Order::new());
647        }
648
649        let str_fields: Vec<&str> = trimmed.split(COMMA).collect();
650        let mut order: Self = Order(IndexMap::new());
651
652        for str_field in str_fields {
653            let trimmed_field = str_field.trim();
654            if trimmed_field.is_empty() {
655                continue;
656            }
657
658            let (name, sort_order) = parse_order_field(trimmed_field)?;
659            order.0.insert(name, sort_order);
660        }
661
662        Ok(order)
663    }
664}
665
666// Utility enums (needed by main types)
667#[derive(Clone, Debug, PartialEq)]
668pub enum Similarity {
669    Equals,
670    Contains,
671    StartsWith,
672    EndsWith,
673
674    Between,
675    Lesser,
676    LesserOrEqual,
677    Greater,
678    GreaterOrEqual,
679}
680
681impl Similarity {
682    pub const EQUALS: &str = "equals";
683    pub const CONTAINS: &str = "contains";
684    pub const STARTS_WITH: &str = "starts-with";
685    pub const ENDS_WITH: &str = "ends-with";
686
687    pub const BETWEEN: &str = "between";
688    pub const LESSER: &str = "lesser";
689    pub const LESSER_OR_EQUAL: &str = "lesser-or-equal";
690    pub const GREATER: &str = "greater";
691    pub const GREATER_OR_EQUAL: &str = "greater-or-equal";
692}
693
694impl Default for Similarity {
695    fn default() -> Self {
696        Self::Equals
697    }
698}
699
700impl FromStr for Similarity {
701    type Err = Error;
702    fn from_str(s: &str) -> Result<Self> {
703        match s {
704            Similarity::EQUALS => Ok(Similarity::Equals),
705            Similarity::CONTAINS => Ok(Similarity::Contains),
706            Similarity::STARTS_WITH => Ok(Similarity::StartsWith),
707            Similarity::ENDS_WITH => Ok(Similarity::EndsWith),
708
709            Similarity::BETWEEN => Ok(Similarity::Between),
710            Similarity::LESSER => Ok(Similarity::Lesser),
711            Similarity::LESSER_OR_EQUAL => Ok(Similarity::LesserOrEqual),
712            Similarity::GREATER => Ok(Similarity::Greater),
713            Similarity::GREATER_OR_EQUAL => Ok(Similarity::GreaterOrEqual),
714
715            val => Err(Error::InvalidSimilarity(val.into())),
716        }
717    }
718}
719
720impl ToString for Similarity {
721    fn to_string(&self) -> String {
722        match self {
723            Self::Equals => Self::EQUALS.to_string(),
724            Self::Contains => Self::CONTAINS.to_string(),
725            Self::StartsWith => Self::STARTS_WITH.to_string(),
726            Self::EndsWith => Self::ENDS_WITH.to_string(),
727
728            Self::Between => Self::BETWEEN.to_string(),
729            Self::Lesser => Self::LESSER.to_string(),
730            Self::LesserOrEqual => Self::LESSER_OR_EQUAL.to_string(),
731            Self::Greater => Self::GREATER.to_string(),
732            Self::GreaterOrEqual => Self::GREATER_OR_EQUAL.to_string(),
733        }
734    }
735}
736
737#[derive(Clone, Debug, PartialEq)]
738pub enum SortOrder {
739    Ascending,
740    Descending,
741}
742
743impl SortOrder {
744    pub const ASCENDING: &str = "asc";
745    pub const DESCENDING: &str = "desc";
746}
747
748impl Default for SortOrder {
749    fn default() -> Self {
750        Self::Ascending
751    }
752}
753
754impl FromStr for SortOrder {
755    type Err = Error;
756    fn from_str(s: &str) -> Result<Self> {
757        match s {
758            SortOrder::ASCENDING => Ok(SortOrder::Ascending),
759            SortOrder::DESCENDING => Ok(SortOrder::Descending),
760            val => Err(Error::InvalidSortOrder(val.into())),
761        }
762    }
763}
764
765impl ToString for SortOrder {
766    fn to_string(&self) -> String {
767        match self {
768            Self::Ascending => SortOrder::ASCENDING.to_string(),
769            Self::Descending => SortOrder::DESCENDING.to_string(),
770        }
771    }
772}
773
774// Utility types
775#[cfg(feature = "sql")]
776#[derive(Clone, Debug, PartialEq)]
777pub enum SqlValue {
778    /// The value is a `NULL` value.
779    Null,
780    /// The value is a signed integer.
781    Integer(i64),
782    /// The value is a floating point number.
783    Real(f64),
784    /// The value is a text string.
785    Text(String),
786    /// The value is a blob of data
787    Blob(Vec<u8>),
788}
789
790// Utility functions
791/// Parse a parameter string into similarity and values
792///
793/// # Examples
794/// - "contains:damian" -> (Similarity::Contains, vec!["damian"])
795/// - "equals:black,steel,wood" -> (Similarity::Equals, vec!["black", "steel", "wood"])
796/// - "between:20,30" -> (Similarity::Between, vec!["20", "30"])
797pub(crate) fn parse_parameter(s: &str) -> Result<Parameter> {
798    let trimmed = s.trim();
799    if trimmed.is_empty() {
800        return Err(Error::InvalidParameter(s.into()));
801    }
802
803    let parts: Vec<&str> = trimmed.split(COLON).collect();
804    if parts.len() != 2 {
805        return Err(Error::InvalidParameter(s.into()));
806    }
807
808    let similarity_str = parts[0].trim();
809    let values_str = parts[1].trim();
810
811    if similarity_str.is_empty() {
812        return Err(Error::InvalidParameter(s.into()));
813    }
814
815    let values: Vec<String> = if values_str.is_empty() {
816        vec![]
817    } else {
818        values_str
819            .split(COMMA)
820            .map(|v| url_decode(v.trim()))
821            .filter(|v| !v.is_empty())
822            .collect()
823    };
824
825    let similarity = Similarity::from_str(similarity_str)?;
826    Ok(Parameter(similarity, values))
827}
828
829/// Parse an order field string into name and order
830///
831/// # Examples
832/// - "name:asc" -> ("name", SortOrder::Ascending)
833/// - "date_created:desc" -> ("date_created", SortOrder::Descending)
834pub(crate) fn parse_order_field(s: &str) -> Result<(String, SortOrder)> {
835    let trimmed = s.trim();
836    if trimmed.is_empty() {
837        return Err(Error::InvalidOrderField(s.into()));
838    }
839
840    let parts: Vec<&str> = trimmed.split(COLON).collect();
841    if parts.len() != 2 {
842        return Err(Error::InvalidOrderField(s.into()));
843    }
844
845    let name = url_decode(parts[0].trim());
846    let order_str = parts[1].trim();
847
848    if name.is_empty() || order_str.is_empty() {
849        return Err(Error::InvalidOrderField(s.into()));
850    }
851
852    let order = SortOrder::from_str(order_str)?;
853    Ok((name, order))
854}
855
856pub(crate) const QUESTION: char = '?';
857pub(crate) const AMPERSAND: char = '&';
858pub(crate) const EQUAL: char = '=';
859pub(crate) const COLON: char = ':';
860pub(crate) const COMMA: char = ',';
861pub(crate) const PERCENT: char = '%';
862
863/// URL decode a string, handling percent-encoded characters
864pub(crate) fn url_decode(input: &str) -> String {
865    // Only decode if the string contains percent-encoded characters
866    if input.contains(PERCENT) {
867        // Use form_urlencoded to decode individual values by treating it as a query parameter
868        let query_str = format!("key={}", input);
869        form_urlencoded::parse(query_str.as_bytes())
870            .next()
871            .map(|(_, v)| v.to_string())
872            .unwrap_or_else(|| input.to_string())
873    } else {
874        input.to_string()
875    }
876}
877
878/// URL encode a string, converting special characters to percent-encoded format
879pub(crate) fn url_encode(input: &str) -> String {
880    form_urlencoded::byte_serialize(input.as_bytes()).collect()
881}
882
883#[cfg(test)]
884mod parse_tests;