use crate::error::{Error, Result};
#[cfg(feature = "sql")]
use crate::sql;
use indexmap::IndexMap;
use std::fmt;
use std::str::FromStr;
use url::form_urlencoded;
#[derive(Clone, Debug, PartialEq)]
pub struct Query {
pub parameters: Parameters,
pub order: Order,
pub limit: usize,
pub offset: usize,
}
impl Query {
pub fn new() -> Self {
Self {
parameters: Parameters::new(),
order: Order::new(),
limit: Parameters::DEFAULT_LIMIT,
offset: Parameters::DEFAULT_OFFSET,
}
}
pub fn init(parameters: Parameters, order: Order, limit: usize, offset: usize) -> Self {
Self {
parameters,
order,
limit,
offset,
}
}
#[cfg(feature = "http")]
pub fn to_http(&self) -> String {
let params_str = format!("{}", self.parameters);
let order_str = format!("{}", self.order);
let mut result = String::new();
if !params_str.is_empty() {
result.push_str(¶ms_str);
result.push(AMPERSAND);
}
if !order_str.is_empty() {
result.push_str(&format!("{}{EQUAL}{}", Parameters::ORDER, order_str));
result.push(AMPERSAND);
}
let pagination_str = format!(
"{}{EQUAL}{}{AMPERSAND}{}{EQUAL}{}",
Parameters::LIMIT,
self.limit,
Parameters::OFFSET,
self.offset,
);
result.push_str(&pagination_str);
result
}
#[cfg(feature = "http")]
pub fn from_http(search: String) -> Result<Self> {
let mut query = Self::new();
let trimmed_search = search.trim_start_matches(QUESTION).trim();
if trimmed_search.is_empty() {
return Ok(query);
}
for k_v in trimmed_search.split(AMPERSAND) {
let trimmed_kv = k_v.trim();
if trimmed_kv.is_empty() {
continue;
}
let mut parts = trimmed_kv.splitn(2, EQUAL);
if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
let trimmed_key = key.trim();
let trimmed_value = value.trim();
if trimmed_key.is_empty() || trimmed_value.is_empty() {
continue;
}
match trimmed_key {
Parameters::ORDER => {
if !trimmed_value.contains(COLON) {
return Err(Error::InvalidOrderField(trimmed_value.into()));
}
if let Ok(order) = trimmed_value.parse::<Order>() {
query.order = order;
}
}
Parameters::LIMIT => {
query.limit = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_LIMIT);
}
Parameters::OFFSET => {
query.offset = trimmed_value.parse().unwrap_or(Parameters::DEFAULT_OFFSET);
}
_k => {
if trimmed_value.contains(COLON) {
let param = trimmed_value.parse::<Parameter>()?;
if param.values().is_empty() {
continue;
}
query.parameters.0.insert(trimmed_key.to_string(), param);
} else {
let decoded_value = url_decode(trimmed_value);
if let Some(existing_param) =
query.parameters.0.get_mut(&trimmed_key.to_string())
{
if *existing_param.similarity() == Similarity::Equals {
existing_param.1.push(decoded_value);
}
} else {
query.parameters.0.insert(
trimmed_key.to_string(),
Parameter::init(Similarity::Equals, vec![decoded_value]),
);
}
}
}
}
} else {
return Err(Error::InvalidSearchParameters(search));
}
}
Ok(query)
}
#[cfg(feature = "sql")]
pub fn to_sql(&self) -> String {
let mut sql_parts = Vec::new();
if let Some(where_clause) = self.where_clause() {
sql_parts.push(format!("WHERE {}", where_clause));
}
if let Some(order_clause) = self.order_clause() {
sql_parts.push(format!("ORDER BY {}", order_clause));
}
sql_parts.push(format!("LIMIT ? OFFSET ?"));
sql_parts.join(" ")
}
#[cfg(feature = "sql")]
pub fn where_clause(&self) -> Option<String> {
let mut conditions = Vec::new();
for (key, param) in &self.parameters.0 {
let similarity = param.similarity();
let values = param.values();
if values.is_empty() {
continue;
}
let condition = match similarity {
Similarity::Equals => {
if values.len() == 1 {
if values[0] == sql::NULL {
format!("{} IS ?", key)
} else {
format!("{} = ?", key)
}
} else {
let placeholders = vec!["?"; values.len()].join(", ");
format!("{} IN ({})", key, placeholders)
}
}
Similarity::Contains => {
if values.len() == 1 {
format!("{} LIKE ?", key)
} else {
let like_conditions: Vec<String> =
values.iter().map(|_| format!("{} LIKE ?", key)).collect();
format!("({})", like_conditions.join(" OR "))
}
}
Similarity::StartsWith => {
if values.len() == 1 {
format!("{} LIKE ?", key)
} else {
let like_conditions: Vec<String> =
values.iter().map(|_| format!("{} LIKE ?", key)).collect();
format!("({})", like_conditions.join(" OR "))
}
}
Similarity::EndsWith => {
if values.len() == 1 {
format!("{} LIKE ?", key)
} else {
let like_conditions: Vec<String> =
values.iter().map(|_| format!("{} LIKE ?", key)).collect();
format!("({})", like_conditions.join(" OR "))
}
}
Similarity::Between => {
if values.len() >= 2 {
let pairs: Vec<&[String]> = values.chunks(2).collect();
let between_conditions: Vec<String> = pairs
.iter()
.map(|pair| {
if pair.len() == 2 {
format!("{} BETWEEN ? AND ?", key)
} else {
String::new() }
})
.filter(|condition| !condition.is_empty())
.collect();
if between_conditions.is_empty() {
continue; } else if between_conditions.len() == 1 {
between_conditions[0].clone()
} else {
format!("({})", between_conditions.join(" OR "))
}
} else {
continue; }
}
Similarity::Lesser => {
if values.len() == 1 {
format!("{} < ?", key)
} else {
let conditions: Vec<String> =
values.iter().map(|_| format!("{} < ?", key)).collect();
format!("({})", conditions.join(" OR "))
}
}
Similarity::LesserOrEqual => {
if values.len() == 1 {
format!("{} <= ?", key)
} else {
let conditions: Vec<String> =
values.iter().map(|_| format!("{} <= ?", key)).collect();
format!("({})", conditions.join(" OR "))
}
}
Similarity::Greater => {
if values.len() == 1 {
format!("{} > ?", key)
} else {
let conditions: Vec<String> =
values.iter().map(|_| format!("{} > ?", key)).collect();
format!("({})", conditions.join(" OR "))
}
}
Similarity::GreaterOrEqual => {
if values.len() == 1 {
format!("{} >= ?", key)
} else {
let conditions: Vec<String> =
values.iter().map(|_| format!("{} >= ?", key)).collect();
format!("({})", conditions.join(" OR "))
}
}
};
conditions.push(condition);
}
if conditions.is_empty() {
None
} else {
Some(conditions.join(" AND "))
}
}
#[cfg(feature = "sql")]
pub fn order_clause(&self) -> Option<String> {
let mut order_parts = Vec::new();
for (name, direction) in &self.order.0 {
if !name.is_empty() {
let direction = match direction {
SortDirection::Ascending => "ASC",
SortDirection::Descending => "DESC",
};
order_parts.push(format!("{} {}", name, direction));
}
}
if order_parts.is_empty() {
None
} else {
Some(order_parts.join(", "))
}
}
#[cfg(feature = "sql")]
pub fn to_values(&self) -> Vec<sql::Value> {
let mut sql_values = self.parameter_values();
sql_values.extend(self.pagination_values());
sql_values
}
#[cfg(feature = "sql")]
pub fn parameter_values(&self) -> Vec<sql::Value> {
let mut sql_values = Vec::new();
for (_k, param) in self.parameters.inner() {
let param_similarity = param.similarity();
let param_values = param.values();
for cur_val in param_values {
if cur_val.trim().is_empty() {
continue;
}
if cur_val == sql::NULL {
sql_values.push(sql::Value::Null);
continue;
}
let sql_value = match *param_similarity {
Similarity::Contains => sql::Value::Text(format!("%{}%", cur_val)),
Similarity::StartsWith => sql::Value::Text(format!("{}%", cur_val)),
Similarity::EndsWith => sql::Value::Text(format!("%{}", cur_val)),
_ => {
if let Ok(i) = cur_val.parse::<i64>() {
sql::Value::Integer(i)
} else if let Ok(f) = cur_val.parse::<f64>() {
sql::Value::Real(f)
} else {
sql::Value::Text(cur_val.clone())
}
}
};
sql_values.push(sql_value);
}
}
sql_values
}
#[cfg(feature = "sql")]
pub fn pagination_values(&self) -> Vec<sql::Value> {
vec![
sql::Value::Integer(self.limit as i64),
sql::Value::Integer(self.offset as i64),
]
}
#[cfg(feature = "sql")]
pub fn total_parameters(&self) -> usize {
let parameter_count: usize = self
.parameters
.inner()
.values()
.map(|param| {
param
.values()
.iter()
.filter(|v| !v.trim().is_empty())
.count()
})
.sum();
parameter_count + 2 }
}
#[derive(Clone, Debug, PartialEq)]
pub struct Parameters(IndexMap<String, Parameter>);
impl Parameters {
pub const ORDER: &str = "order";
pub const LIMIT: &str = "limit";
pub const OFFSET: &str = "offset";
pub const EXCLUDE: [&str; 3] = [Parameters::ORDER, Parameters::LIMIT, Parameters::OFFSET];
pub const DEFAULT_LIMIT: usize = 50;
pub const DEFAULT_OFFSET: usize = 0;
pub fn new() -> Self {
Self(IndexMap::new())
}
pub fn inner(&self) -> &IndexMap<String, Parameter> {
&self.0
}
pub fn inner_mut(&mut self) -> &mut IndexMap<String, Parameter> {
&mut self.0
}
pub fn equals(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::Equals, values));
self
}
pub fn contains(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::Contains, values));
self
}
pub fn starts_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::StartsWith, values));
self
}
pub fn ends_with(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::EndsWith, values));
self
}
pub fn between(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::Between, values));
self
}
pub fn lesser(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::Lesser, values));
self
}
pub fn lesser_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::LesserOrEqual, values));
self
}
pub fn greater(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::Greater, values));
self
}
pub fn greater_or_equal(&mut self, key: String, values: Vec<String>) -> &mut Self {
self.0
.insert(key, Parameter::init(Similarity::GreaterOrEqual, values));
self
}
pub fn keep(&self, keys: Vec<String>) -> Self {
let mut result = Self::new();
for key in keys {
if let Some(value) = self.0.get(&key) {
result.0.insert(key, value.clone());
}
}
result
}
pub fn remove(&self, keys: Vec<String>) -> Self {
let mut result = self.clone();
for key in keys {
result.0.shift_remove(&key);
}
result
}
}
impl Default for Parameters {
fn default() -> Self {
Self::new()
}
}
impl FromStr for Parameters {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(Parameters::new());
}
let str_parameters: Vec<&str> = trimmed.split(AMPERSAND).collect();
let mut parameters: Self = Parameters::new();
for str_param in str_parameters {
let trimmed_param = str_param.trim();
if trimmed_param.is_empty() {
continue;
}
let mut parts = trimmed_param.splitn(2, EQUAL);
let (key, value) = match (parts.next(), parts.next()) {
(Some(k), Some(v)) => (k, v),
_ => return Err(Error::InvalidParameter(trimmed_param.into())),
};
let trimmed_key = key.trim();
if trimmed_key.is_empty() || Parameters::EXCLUDE.contains(&trimmed_key) {
continue;
}
let param = value.parse::<Parameter>()?;
if param.values().is_empty() {
continue;
}
parameters.0.insert(trimmed_key.to_string(), param);
}
Ok(parameters)
}
}
impl fmt::Display for Parameters {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let params_str = self
.inner()
.iter()
.filter(|(_, param)| param.values().len() > 0)
.map(|(key, param)| format!("{key}{EQUAL}{param}"))
.collect::<Vec<String>>()
.join(&format!("{AMPERSAND}"));
write!(f, "{}", params_str)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Parameter(Similarity, Vec<String>);
impl Parameter {
pub fn init(similarity: Similarity, values: Vec<String>) -> Self {
Self(similarity, values)
}
pub fn similarity(&self) -> &Similarity {
&self.0
}
pub fn values(&self) -> &Vec<String> {
&self.1
}
pub fn values_mut(&mut self) -> &mut Vec<String> {
&mut self.1
}
}
impl FromStr for Parameter {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(Error::InvalidParameter(s.into()));
}
let parts: Vec<&str> = trimmed.split(COLON).collect();
if parts.len() != 2 {
return Err(Error::InvalidParameter(s.into()));
}
let similarity_str = parts[0].trim();
let values_str = parts[1].trim();
if similarity_str.is_empty() {
return Err(Error::InvalidParameter(s.into()));
}
let values: Vec<String> = if values_str.is_empty() {
vec![]
} else {
values_str
.split(COMMA)
.map(|v| url_decode(v.trim()))
.filter(|v| !v.is_empty())
.collect()
};
let similarity = similarity_str.parse::<Similarity>()?;
Ok(Parameter(similarity, values))
}
}
impl fmt::Display for Parameter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let similarity_str = self.similarity().to_string();
let values_str = self
.values()
.iter()
.map(|v| url_encode(v))
.collect::<Vec<String>>()
.join(&format!("{COMMA}"));
write!(f, "{similarity_str}{COLON}{values_str}")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Order(IndexMap<String, SortDirection>);
impl Order {
pub fn new() -> Self {
Self(IndexMap::new())
}
pub fn inner(&self) -> &IndexMap<String, SortDirection> {
&self.0
}
pub fn inner_mut(&mut self) -> &mut IndexMap<String, SortDirection> {
&mut self.0
}
pub fn ascending(&mut self, name: String) -> &mut Self {
self.0.insert(name, SortDirection::Ascending);
self
}
pub fn descending(&mut self, name: String) -> &mut Self {
self.0.insert(name, SortDirection::Descending);
self
}
pub fn keep(&self, keys: Vec<String>) -> Self {
let mut result = Self::new();
for key in keys {
if let Some(value) = self.0.get(&key) {
result.0.insert(key, value.clone());
}
}
result
}
pub fn remove(&self, keys: Vec<String>) -> Self {
let mut result = self.clone();
for key in keys {
result.0.shift_remove(&key);
}
result
}
}
impl Default for Order {
fn default() -> Self {
Self::new()
}
}
impl FromStr for Order {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Ok(Order::new());
}
let str_fields: Vec<&str> = trimmed.split(COMMA).collect();
let mut order: Self = Order::new();
for str_field in str_fields {
let trimmed_field = str_field.trim();
if trimmed_field.is_empty() {
continue;
}
let OrderField(name, direction) = trimmed_field.parse::<OrderField>()?;
order.0.insert(name, direction);
}
Ok(order)
}
}
impl fmt::Display for Order {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let order_str = self
.inner()
.iter()
.filter(|(name, _)| name.len() > 0)
.map(|(name, direction)| format!("{}", OrderField(name.clone(), direction.clone())))
.collect::<Vec<String>>()
.join(&format!("{COMMA}"));
write!(f, "{}", order_str)
}
}
pub struct OrderField(String, SortDirection);
impl OrderField {
pub fn name(&self) -> &String {
&self.0
}
pub fn sort_direction(&self) -> &SortDirection {
&self.1
}
}
impl FromStr for OrderField {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(Error::InvalidOrderField(s.into()));
}
let parts: Vec<&str> = trimmed.split(COLON).collect();
if parts.len() != 2 {
return Err(Error::InvalidOrderField(s.into()));
}
let name = url_decode(parts[0].trim());
let order = parts[1].trim();
if name.is_empty() || order.is_empty() {
return Err(Error::InvalidOrderField(s.into()));
}
let order = order.parse::<SortDirection>()?;
Ok(OrderField(name, order))
}
}
impl fmt::Display for OrderField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{COLON}{}", self.name(), self.sort_direction())
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Similarity {
Equals,
Contains,
StartsWith,
EndsWith,
Between,
Lesser,
LesserOrEqual,
Greater,
GreaterOrEqual,
}
impl Similarity {
pub const EQUALS: &str = "equals";
pub const CONTAINS: &str = "contains";
pub const STARTS_WITH: &str = "starts-with";
pub const ENDS_WITH: &str = "ends-with";
pub const BETWEEN: &str = "between";
pub const LESSER: &str = "lesser";
pub const LESSER_OR_EQUAL: &str = "lesser-or-equal";
pub const GREATER: &str = "greater";
pub const GREATER_OR_EQUAL: &str = "greater-or-equal";
}
impl Default for Similarity {
fn default() -> Self {
Self::Equals
}
}
impl FromStr for Similarity {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
Similarity::EQUALS => Ok(Similarity::Equals),
Similarity::CONTAINS => Ok(Similarity::Contains),
Similarity::STARTS_WITH => Ok(Similarity::StartsWith),
Similarity::ENDS_WITH => Ok(Similarity::EndsWith),
Similarity::BETWEEN => Ok(Similarity::Between),
Similarity::LESSER => Ok(Similarity::Lesser),
Similarity::LESSER_OR_EQUAL => Ok(Similarity::LesserOrEqual),
Similarity::GREATER => Ok(Similarity::Greater),
Similarity::GREATER_OR_EQUAL => Ok(Similarity::GreaterOrEqual),
val => Err(Error::InvalidSimilarity(val.into())),
}
}
}
impl fmt::Display for Similarity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Equals => Self::EQUALS,
Self::Contains => Self::CONTAINS,
Self::StartsWith => Self::STARTS_WITH,
Self::EndsWith => Self::ENDS_WITH,
Self::Between => Self::BETWEEN,
Self::Lesser => Self::LESSER,
Self::LesserOrEqual => Self::LESSER_OR_EQUAL,
Self::Greater => Self::GREATER,
Self::GreaterOrEqual => Self::GREATER_OR_EQUAL,
};
write!(f, "{}", s)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SortDirection {
Ascending,
Descending,
}
impl SortDirection {
pub const ASCENDING: &str = "asc";
pub const DESCENDING: &str = "desc";
}
impl Default for SortDirection {
fn default() -> Self {
Self::Ascending
}
}
impl FromStr for SortDirection {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
SortDirection::ASCENDING => Ok(SortDirection::Ascending),
SortDirection::DESCENDING => Ok(SortDirection::Descending),
val => Err(Error::InvalidSortDirection(val.into())),
}
}
}
impl fmt::Display for SortDirection {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Self::Ascending => SortDirection::ASCENDING,
Self::Descending => SortDirection::DESCENDING,
};
write!(f, "{}", s)
}
}
pub(crate) const QUESTION: char = '?';
pub(crate) const AMPERSAND: char = '&';
pub(crate) const EQUAL: char = '=';
pub(crate) const COLON: char = ':';
pub(crate) const COMMA: char = ',';
pub(crate) const PERCENT: char = '%';
pub(crate) fn url_decode(input: &str) -> String {
if input.contains(PERCENT) {
let query_str = format!("key={}", input);
form_urlencoded::parse(query_str.as_bytes())
.next()
.map(|(_, v)| v.to_string())
.unwrap_or_else(|| input.to_string())
} else {
input.to_string()
}
}
pub(crate) fn url_encode(input: &str) -> String {
form_urlencoded::byte_serialize(input.as_bytes()).collect()
}