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