mirror of https://github.com/terhechte/postsack
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
243 lines
5.9 KiB
Rust
243 lines
5.9 KiB
Rust
3 years ago
|
use rsql_builder;
|
||
|
use serde_json;
|
||
|
pub use serde_json::Value;
|
||
|
use strum::{self, IntoEnumIterator};
|
||
|
use strum_macros::{EnumIter, IntoStaticStr};
|
||
|
|
||
|
use std::ops::Range;
|
||
|
|
||
|
pub const AMOUNT_FIELD_NAME: &str = "amount";
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub enum Filter {
|
||
|
/// A database Like Operation
|
||
|
Like(ValueField),
|
||
|
NotLike(ValueField),
|
||
|
/// A extended like that implies:
|
||
|
/// - wildcards on both sides (like '%test%')
|
||
|
/// - case in-sensitive comparison
|
||
|
/// - Trying to handle values as strings
|
||
|
Contains(ValueField),
|
||
|
Is(ValueField),
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoStaticStr, EnumIter)]
|
||
|
#[strum(serialize_all = "snake_case")]
|
||
|
pub enum Field {
|
||
|
Path,
|
||
|
SenderDomain,
|
||
|
SenderLocalPart,
|
||
|
SenderName,
|
||
|
Year,
|
||
|
Month,
|
||
|
Day,
|
||
|
Timestamp,
|
||
|
ToGroup,
|
||
|
ToName,
|
||
|
ToAddress,
|
||
|
IsReply,
|
||
|
IsSend,
|
||
|
Subject,
|
||
|
MetaIsSeen,
|
||
|
MetaTags,
|
||
|
}
|
||
|
|
||
|
const INVALID_FIELDS: &[Field] = &[
|
||
|
Field::Path,
|
||
|
Field::Subject,
|
||
|
Field::Timestamp,
|
||
|
Field::IsReply,
|
||
|
Field::IsSend,
|
||
|
Field::MetaIsSeen,
|
||
|
Field::MetaTags,
|
||
|
];
|
||
|
|
||
|
impl Field {
|
||
|
pub fn all_cases() -> impl Iterator<Item = Field> {
|
||
|
Field::iter().filter(|f| !INVALID_FIELDS.contains(f))
|
||
|
}
|
||
|
|
||
|
/// Just a wrapper to offer `into` without the type ambiguity
|
||
|
/// that sometimes arises
|
||
|
pub fn as_str(&self) -> &'static str {
|
||
|
self.into()
|
||
|
}
|
||
|
|
||
|
/// A human readable name
|
||
|
pub fn name(&self) -> &str {
|
||
|
use Field::*;
|
||
|
match self {
|
||
|
SenderDomain => "Domain",
|
||
|
SenderLocalPart => "Address",
|
||
|
SenderName => "Name",
|
||
|
ToGroup => "Group",
|
||
|
ToName => "To name",
|
||
|
ToAddress => "To address",
|
||
|
Year => "Year",
|
||
|
Month => "Month",
|
||
|
Day => "Day",
|
||
|
Subject => "Subject",
|
||
|
_ => self.as_str(),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl std::fmt::Display for Field {
|
||
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||
|
write!(f, "{}", self.name())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||
|
pub struct ValueField {
|
||
|
field: Field,
|
||
|
value: Value,
|
||
|
}
|
||
|
|
||
|
impl ValueField {
|
||
|
pub fn string<S: AsRef<str>>(field: &Field, value: S) -> ValueField {
|
||
|
ValueField {
|
||
|
field: *field,
|
||
|
value: Value::String(value.as_ref().to_string()),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn bool(field: &Field, value: bool) -> ValueField {
|
||
|
ValueField {
|
||
|
field: *field,
|
||
|
value: Value::Bool(value),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn usize(field: &Field, value: usize) -> ValueField {
|
||
|
ValueField {
|
||
|
field: *field,
|
||
|
value: Value::Number(value.into()),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn array(field: &Field, value: Vec<Value>) -> ValueField {
|
||
|
ValueField {
|
||
|
field: *field,
|
||
|
value: Value::Array(value),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn field(&self) -> &Field {
|
||
|
&self.field
|
||
|
}
|
||
|
|
||
|
pub fn value(&self) -> &Value {
|
||
|
&self.value
|
||
|
}
|
||
|
|
||
|
#[allow(clippy::inherent_to_string)]
|
||
|
pub fn to_string(&self) -> String {
|
||
|
match &self.value {
|
||
|
Value::String(s) => s.clone(),
|
||
|
_ => format!("{}", &self.value),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, Clone)]
|
||
|
pub enum OtherQuery {
|
||
|
/// Get all contents of a specific field
|
||
|
All(Field),
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Debug)]
|
||
|
pub enum Query {
|
||
|
Grouped {
|
||
|
filters: Vec<Filter>,
|
||
|
group_by: Field,
|
||
|
},
|
||
|
Normal {
|
||
|
fields: Vec<Field>,
|
||
|
filters: Vec<Filter>,
|
||
|
range: Range<usize>,
|
||
|
},
|
||
|
Other {
|
||
|
query: OtherQuery,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
impl Query {
|
||
|
fn filters(&self) -> &[Filter] {
|
||
|
match self {
|
||
|
Query::Grouped { ref filters, .. } => filters,
|
||
|
Query::Normal { ref filters, .. } => filters,
|
||
|
Query::Other { .. } => &[],
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Query {
|
||
|
pub fn to_sql(&self) -> (String, Vec<serde_json::Value>) {
|
||
|
let mut conditions = {
|
||
|
let mut whr = rsql_builder::B::new_where();
|
||
|
for filter in self.filters() {
|
||
|
match filter {
|
||
|
Filter::Like(f) => whr.like(f.field.into(), f.value()),
|
||
|
Filter::NotLike(f) => whr.not_like(f.field.into(), f.value()),
|
||
|
Filter::Contains(f) => whr.like(
|
||
|
f.field.into(),
|
||
|
&format!("%{}%", f.to_string().to_lowercase()),
|
||
|
),
|
||
|
Filter::Is(f) => whr.eq(f.field.into(), f.value()),
|
||
|
};
|
||
|
}
|
||
|
whr
|
||
|
};
|
||
|
|
||
|
let (header, group_by) = match self {
|
||
|
Query::Grouped { group_by, .. } => (
|
||
|
format!(
|
||
|
"SELECT count(path) as {}, {} FROM emails",
|
||
|
AMOUNT_FIELD_NAME,
|
||
|
group_by.as_str()
|
||
|
),
|
||
|
format!("GROUP BY {}", group_by.as_str()),
|
||
|
),
|
||
|
Query::Normal { fields, range, .. } => {
|
||
|
let fields: Vec<&str> = fields.iter().map(|e| e.into()).collect();
|
||
|
(
|
||
|
format!("SELECT {} FROM emails", fields.join(", ")),
|
||
|
format!("LIMIT {}, {}", range.start, range.end - range.start),
|
||
|
)
|
||
|
}
|
||
|
Query::Other {
|
||
|
query: OtherQuery::All(field),
|
||
|
} => (
|
||
|
format!("SELECT {} FROM emails", field.as_str()),
|
||
|
format!(""),
|
||
|
),
|
||
|
};
|
||
|
|
||
|
let (sql, values) = rsql_builder::B::prepare(
|
||
|
rsql_builder::B::new_sql(&header)
|
||
|
.push_build(&mut conditions)
|
||
|
.push_sql(&group_by),
|
||
|
);
|
||
|
|
||
|
(sql, values)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[cfg(test)]
|
||
|
mod tests {
|
||
|
use super::*;
|
||
|
|
||
|
#[test]
|
||
|
fn test_test() {
|
||
|
let query = Query::Grouped {
|
||
|
filters: vec![
|
||
|
Filter::Like(ValueField::string(&Field::SenderDomain, "gmail.com")),
|
||
|
Filter::Is(ValueField::usize(&Field::Year, 2021)),
|
||
|
],
|
||
|
group_by: Field::Month,
|
||
|
};
|
||
|
dbg!(&query.to_sql());
|
||
|
}
|
||
|
}
|