WIP: Introduce the zou library
This commit is contained in:
parent
221a740c96
commit
eb3445d64c
4 changed files with 524 additions and 0 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -2663,3 +2663,7 @@ dependencies = [
|
|||
"quote",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zou"
|
||||
version = "0.1.0"
|
||||
|
|
|
@ -4,4 +4,5 @@ resolver = "2"
|
|||
|
||||
members = [
|
||||
"yorokobot",
|
||||
"zou"
|
||||
]
|
||||
|
|
6
zou/Cargo.toml
Normal file
6
zou/Cargo.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[package]
|
||||
name = "zou"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
513
zou/src/lib.rs
Normal file
513
zou/src/lib.rs
Normal file
|
@ -0,0 +1,513 @@
|
|||
pub trait ObjectModel: Sized {
|
||||
fn table_name() -> &'static str;
|
||||
}
|
||||
|
||||
enum QueryOperation {
|
||||
Select,
|
||||
}
|
||||
|
||||
impl QueryOperation {
|
||||
fn format(&self) -> &'static str {
|
||||
match self {
|
||||
QueryOperation::Select => "SELECT $f FROM $t $j $w $o;",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Operand = String;
|
||||
|
||||
enum Filter {
|
||||
Equal {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
Greater {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
Less {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
GreaterOrEqual {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
LessOrEqual {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
NotEqual {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
And {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
Or {
|
||||
op1: Operand,
|
||||
op2: Operand,
|
||||
},
|
||||
In {
|
||||
value: Operand,
|
||||
valid_range: Vec<Operand>,
|
||||
},
|
||||
Between {
|
||||
value: Operand,
|
||||
lower: Operand,
|
||||
upper: Operand,
|
||||
},
|
||||
Like {
|
||||
value: Operand,
|
||||
pattern: Operand,
|
||||
},
|
||||
IsNull {
|
||||
value: Operand,
|
||||
},
|
||||
IsNotNull {
|
||||
value: Operand,
|
||||
},
|
||||
Not {
|
||||
op: Operand,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToString for Filter {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Filter::Equal { op1, op2 } => format!("{} = {}", op1.to_string(), op2.to_string()),
|
||||
Filter::Greater { op1, op2 } => format!("{} > {}", op1.to_string(), op2.to_string()),
|
||||
Filter::Less { op1, op2 } => format!("{} < {}", op1.to_string(), op2.to_string()),
|
||||
Filter::GreaterOrEqual { op1, op2 } => {
|
||||
format!("{} >= {}", op1.to_string(), op2.to_string())
|
||||
}
|
||||
Filter::LessOrEqual { op1, op2 } => {
|
||||
format!("{} <= {}", op1.to_string(), op2.to_string())
|
||||
}
|
||||
Filter::NotEqual { op1, op2 } => format!("{} != {}", op1.to_string(), op2.to_string()),
|
||||
Filter::And { op1, op2 } => format!("{} AND {}", op1.to_string(), op2.to_string()),
|
||||
Filter::Or { op1, op2 } => format!("{} OR {}", op1.to_string(), op2.to_string()),
|
||||
Filter::In { value, valid_range } => {
|
||||
let mut formatted_range: String = "(".to_string();
|
||||
|
||||
for element in valid_range {
|
||||
formatted_range.push_str(&element.to_string());
|
||||
formatted_range.push(',');
|
||||
}
|
||||
|
||||
formatted_range.pop();
|
||||
formatted_range.push(')');
|
||||
format!("{} in {formatted_range}", value.to_string())
|
||||
}
|
||||
Filter::Between {
|
||||
value,
|
||||
lower,
|
||||
upper,
|
||||
} => format!(
|
||||
"{} BETWEEN {} AND {}",
|
||||
value.to_string(),
|
||||
lower.to_string(),
|
||||
upper.to_string()
|
||||
),
|
||||
Filter::Like { value, pattern } => {
|
||||
format!("{} LIKE {}", value.to_string(), pattern.to_string())
|
||||
}
|
||||
Filter::IsNull { value } => format!("{} IS NULL", value.to_string()),
|
||||
Filter::IsNotNull { value } => format!("{} IS NOT NULL", value.to_string()),
|
||||
Filter::Not { op } => format!("NOT {}", op.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilterData(Option<Filter>);
|
||||
impl FilterData {
|
||||
fn new() -> Self {
|
||||
FilterData(None)
|
||||
}
|
||||
|
||||
pub fn equal<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::Equal {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
pub fn greater<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::Greater {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn less<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::Less {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn greater_or_equal<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::GreaterOrEqual {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn less_or_equal<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::LessOrEqual {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn not_equal<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::NotEqual {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn and<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::And {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn or<T: ToString>(mut self, op1: T, op2: T) -> Self {
|
||||
self.0 = Some(Filter::Or {
|
||||
op1: op1.to_string(),
|
||||
op2: op2.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn in_values<T: ToString>(mut self, value: T, valid_values: Vec<T>) -> Self {
|
||||
self.0 = Some(Filter::In {
|
||||
value: value.to_string(),
|
||||
valid_range: valid_values.iter().map(|v| v.to_string()).collect(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn between<T: ToString>(mut self, value: T, lower: T, upper: T) -> Self {
|
||||
self.0 = Some(Filter::Between {
|
||||
value: value.to_string(),
|
||||
lower: lower.to_string(),
|
||||
upper: upper.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn like<T: ToString>(mut self, value: T, pattern: String) -> Self {
|
||||
self.0 = Some(Filter::Like {
|
||||
value: value.to_string(),
|
||||
pattern,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_null<T: ToString>(mut self, value: T) -> Self {
|
||||
self.0 = Some(Filter::IsNull {
|
||||
value: value.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_not_null<T: ToString>(mut self, value: T) -> Self {
|
||||
self.0 = Some(Filter::IsNotNull {
|
||||
value: value.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn not<T: ToString>(mut self, value: T) -> Self {
|
||||
self.0 = Some(Filter::IsNotNull {
|
||||
value: value.to_string(),
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
enum Order {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
impl ToString for Order {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
Order::Asc => "ASC",
|
||||
Order::Desc => "DESC",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct OrderingClause {
|
||||
field: String,
|
||||
order: Order,
|
||||
}
|
||||
|
||||
impl ToString for OrderingClause {
|
||||
fn to_string(&self) -> String {
|
||||
format!("{} {}", self.field, self.order.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
enum JoinKind {
|
||||
Inner,
|
||||
Left,
|
||||
Right,
|
||||
Full,
|
||||
}
|
||||
|
||||
impl ToString for JoinKind {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
JoinKind::Inner => "INNER JOIN",
|
||||
JoinKind::Left => "LEFT JOIN",
|
||||
JoinKind::Right => "RIGHT JOIN",
|
||||
JoinKind::Full => "FULL JOIN",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
struct Join {
|
||||
kind: JoinKind,
|
||||
dest_table: String,
|
||||
join_columns: (String, String),
|
||||
}
|
||||
|
||||
impl ToString for Join {
|
||||
fn to_string(&self) -> String {
|
||||
format!(
|
||||
" {} {} ON {} = {}",
|
||||
self.kind.to_string(),
|
||||
self.dest_table,
|
||||
self.join_columns.0,
|
||||
self.join_columns.1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Query {
|
||||
operation: QueryOperation,
|
||||
table: String,
|
||||
filter: Option<Filter>,
|
||||
ordering: Vec<OrderingClause>,
|
||||
join: Option<Join>,
|
||||
}
|
||||
|
||||
impl Query {
|
||||
pub fn new<T: ObjectModel>(operation: QueryOperation) -> Self {
|
||||
Query {
|
||||
operation,
|
||||
table: T::table_name().to_string(),
|
||||
filter: None,
|
||||
ordering: vec![],
|
||||
join: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter<F: FnOnce(FilterData) -> FilterData>(mut self, filter_data_callback: F) -> Self {
|
||||
let filter_data = FilterData::new();
|
||||
self.filter = filter_data_callback(filter_data).0;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn order(mut self, field: String, order: Order) -> Self {
|
||||
self.ordering.push(OrderingClause { field, order });
|
||||
self
|
||||
}
|
||||
|
||||
pub fn join(
|
||||
mut self,
|
||||
kind: JoinKind,
|
||||
dest_table: String,
|
||||
join_columns: (String, String),
|
||||
) -> Self {
|
||||
self.join = Some(Join {
|
||||
kind,
|
||||
dest_table,
|
||||
join_columns,
|
||||
});
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> String {
|
||||
let where_clause = if let Some(f) = self.filter {
|
||||
format!("WHERE {}", f.to_string())
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let order_clause = if self.ordering.len() != 0 {
|
||||
let mut clause_str = String::from(" ORDER BY ");
|
||||
|
||||
for o in self.ordering {
|
||||
clause_str.push_str(&o.to_string());
|
||||
clause_str.push(',');
|
||||
clause_str.push(' ');
|
||||
}
|
||||
|
||||
clause_str.pop();
|
||||
clause_str.pop();
|
||||
|
||||
clause_str
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let join_clause = if let Some(j) = self.join {
|
||||
j.to_string()
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
self.operation
|
||||
.format()
|
||||
.replace("$f", "*")
|
||||
.replace("$t", &self.table)
|
||||
.replace(" $j", &join_clause)
|
||||
.replace(" $w", &where_clause)
|
||||
.replace(" $o", &order_clause)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ObjectModel;
|
||||
|
||||
use super::Query;
|
||||
|
||||
struct TestModel;
|
||||
impl ObjectModel for TestModel {
|
||||
fn table_name() -> &'static str {
|
||||
"test_objects"
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select).build();
|
||||
|
||||
assert_eq!(query, "SELECT * FROM test_objects;")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_join_inner() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.join(
|
||||
crate::JoinKind::Inner,
|
||||
"test_join".to_owned(),
|
||||
("a".to_owned(), "b".to_owned()),
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
query,
|
||||
"SELECT * FROM test_objects INNER JOIN test_join ON a = b;",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_join_left() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.join(
|
||||
crate::JoinKind::Left,
|
||||
"test_join".to_owned(),
|
||||
("a".to_owned(), "b".to_owned()),
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
query,
|
||||
"SELECT * FROM test_objects LEFT JOIN test_join ON a = b;",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_join_right() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.join(
|
||||
crate::JoinKind::Right,
|
||||
"test_join".to_owned(),
|
||||
("a".to_owned(), "b".to_owned()),
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
query,
|
||||
"SELECT * FROM test_objects RIGHT JOIN test_join ON a = b;",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_join_full() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.join(
|
||||
crate::JoinKind::Full,
|
||||
"test_join".to_owned(),
|
||||
("a".to_owned(), "b".to_owned()),
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
query,
|
||||
"SELECT * FROM test_objects FULL JOIN test_join ON a = b;",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_ordering_single_asc() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.order("a".to_owned(), crate::Order::Asc)
|
||||
.build();
|
||||
|
||||
assert_eq!(query, "SELECT * FROM test_objects ORDER BY a ASC;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_ordering_single_desc() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.order("a".to_owned(), crate::Order::Desc)
|
||||
.build();
|
||||
|
||||
assert_eq!(query, "SELECT * FROM test_objects ORDER BY a DESC;");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_ordering_multiple() {
|
||||
let query = Query::new::<TestModel>(super::QueryOperation::Select)
|
||||
.order("a".to_owned(), crate::Order::Desc)
|
||||
.order("b".to_owned(), crate::Order::Asc)
|
||||
.build();
|
||||
|
||||
assert_eq!(query, "SELECT * FROM test_objects ORDER BY a DESC, b ASC;");
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue