WIP: Introduce the zou library

This commit is contained in:
Victor Mignot 2025-01-06 22:21:55 +01:00
parent 221a740c96
commit eb3445d64c
Signed by: dala
SSH key fingerprint: SHA256:+3O9MhlDc2tJL0n+E+Myr7nL+74DP9AXdIXHmIqZTkY
4 changed files with 524 additions and 0 deletions

4
Cargo.lock generated
View file

@ -2663,3 +2663,7 @@ dependencies = [
"quote",
"syn 2.0.95",
]
[[package]]
name = "zou"
version = "0.1.0"

View file

@ -4,4 +4,5 @@ resolver = "2"
members = [
"yorokobot",
"zou"
]

6
zou/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "zou"
version = "0.1.0"
edition = "2021"
[dependencies]

513
zou/src/lib.rs Normal file
View 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;");
}
}