Compare commits

...

3 commits

Author SHA1 Message Date
eb3445d64c
WIP: Introduce the zou library 2025-01-06 22:21:55 +01:00
221a740c96
Make this repo a Rust workspace
This is done on prevision of the Database management system from MongoDB to PostgreSQL.
The new codebase to handle PostgreSQL will be introduced in its own lib crate within the workspace.
2025-01-06 22:16:14 +01:00
f74d2c3905
Remove target field from error logging macro 2025-01-02 16:47:24 +01:00
25 changed files with 578 additions and 47 deletions

52
Cargo.lock generated
View file

@ -105,13 +105,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.83"
version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -237,9 +237,9 @@ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cc"
version = "1.2.6"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
dependencies = [
"shlex",
]
@ -390,7 +390,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustc_version 0.4.1",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -412,7 +412,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -552,7 +552,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -885,7 +885,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -1215,9 +1215,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pin-project-lite"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@ -1584,7 +1584,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -1789,9 +1789,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.93"
version = "2.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a"
dependencies = [
"proc-macro2",
"quote",
@ -1812,7 +1812,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -1865,7 +1865,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -1949,7 +1949,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -2013,7 +2013,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -2253,7 +2253,7 @@ dependencies = [
"log",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"wasm-bindgen-shared",
]
@ -2288,7 +2288,7 @@ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -2583,7 +2583,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"synstructure",
]
@ -2618,7 +2618,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
@ -2638,7 +2638,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
"synstructure",
]
@ -2661,5 +2661,9 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.95",
]
[[package]]
name = "zou"
version = "0.1.0"

View file

@ -1,24 +1,8 @@
[package]
name = "yorokobot"
description = "Discord bot implementing a topic management system"
version = "0.2.1"
authors = [ "Victor Mignot <dala@dalaran.fr>" ]
license = "EUPL-1.2"
readme = "README.md"
repository = "https://git.dalaran.fr/dala/yorokobot"
edition = "2021"
[workspace]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
resolver = "2"
[dependencies]
serenity = { version="0.11", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector" ] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
mongodb = { version = "2.3.0", default-features = false, features = ["tokio-runtime"] }
serde = { version = "1.0", features = [ "derive" ] }
log = "0.4.17"
futures = "0.3.25"
env_logger = "0.11.6"
[lints.rust]
unsafe_code= "forbid"
missing_docs = "forbid"
members = [
"yorokobot",
"zou"
]

24
yorokobot/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "yorokobot"
description = "Discord bot implementing a topic management system"
version = "0.2.1"
authors = [ "Victor Mignot <dala@dalaran.fr>" ]
license = "EUPL-1.2"
readme = "README.md"
repository = "https://git.dalaran.fr/dala/yorokobot"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serenity = { version="0.11", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "collector" ] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
mongodb = { version = "2.3.0", default-features = false, features = ["tokio-runtime"] }
serde = { version = "1.0", features = [ "derive" ] }
log = "0.4.17"
futures = "0.3.25"
env_logger = "0.11.6"
[lints.rust]
unsafe_code= "forbid"
missing_docs = "forbid"

View file

@ -8,7 +8,7 @@ pub fn get_env_variable(var_name: &str) -> String {
match env::var(var_name) {
Ok(v) => v,
Err(_) => {
error!(target: "bot_warn_errors", "Failed to fetch the {} environment variable.", var_name);
error!("Failed to fetch the {} environment variable.", var_name);
panic!("Exiting...");
}
}

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;");
}
}