query_x/
lib.rs

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