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