Skip to main content

query_lite/
query.rs

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