Major refactor
This commit is contained in:
81
crates/siren-core/src/config.rs
Normal file
81
crates/siren-core/src/config.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::error::Result;
|
||||
use std::env;
|
||||
|
||||
pub struct EnvironmentConfiguration {
|
||||
pub rust_log: String,
|
||||
pub discord_token: String,
|
||||
pub discord_secret: String,
|
||||
pub jwt_secret: String,
|
||||
pub postgres_user: String,
|
||||
pub postgres_password: String,
|
||||
pub postgres_database: String,
|
||||
pub postgres_host: String,
|
||||
pub postgres_port: u16,
|
||||
pub api_base_url: String,
|
||||
pub api_port: u16,
|
||||
pub api_session_ttl: u64,
|
||||
pub valkey_host: String,
|
||||
pub valkey_port: u16,
|
||||
pub minio_root_user: String,
|
||||
pub minio_root_password: String,
|
||||
pub minio_host: String,
|
||||
pub minio_port: u16,
|
||||
pub minio_port_internal: u16,
|
||||
pub data_dir_path: Option<String>,
|
||||
pub force_register: bool,
|
||||
pub default_api_key: String,
|
||||
pub default_server: Option<String>,
|
||||
pub default_user: Option<String>,
|
||||
}
|
||||
|
||||
impl EnvironmentConfiguration {
|
||||
pub fn load() -> Result<Self> {
|
||||
Ok(Self {
|
||||
rust_log: env::var("RUST_LOG").unwrap_or_else(|_| "warn,siren=info".to_string()),
|
||||
discord_token: env::var("DISCORD_BOT_TOKEN")?,
|
||||
discord_secret: env::var("DISCORD_CLIENT_SECRET")?,
|
||||
jwt_secret: env::var("JWT_SECRET")?,
|
||||
postgres_user: env::var("POSTGRES_USER")?,
|
||||
postgres_password: env::var("POSTGRES_PASSWORD")?,
|
||||
postgres_database: env::var("POSTGRES_DB")?,
|
||||
postgres_host: env::var("POSTGRES_HOST")?,
|
||||
postgres_port: env::var("POSTGRES_PORT")
|
||||
.unwrap_or_else(|_| "5432".to_string())
|
||||
.parse()
|
||||
.unwrap_or(5432),
|
||||
api_base_url: env::var("API_BASE_URL")?,
|
||||
api_port: env::var("API_PORT")
|
||||
.unwrap_or_else(|_| "3000".to_string())
|
||||
.parse()
|
||||
.unwrap_or(3000),
|
||||
api_session_ttl: env::var("API_SESSION_TTL")
|
||||
.unwrap_or_else(|_| "86400".to_string())
|
||||
.parse()
|
||||
.unwrap_or(86400),
|
||||
valkey_host: env::var("VALKEY_HOST").unwrap_or_else(|_| "localhost".to_string()),
|
||||
valkey_port: env::var("VALKEY_PORT")
|
||||
.unwrap_or_else(|_| "6379".to_string())
|
||||
.parse()
|
||||
.unwrap_or(6379),
|
||||
minio_root_user: env::var("MINIO_ROOT_USER")?,
|
||||
minio_root_password: env::var("MINIO_ROOT_PASSWORD")?,
|
||||
minio_host: env::var("MINIO_HOST").unwrap_or_else(|_| "localhost".to_string()),
|
||||
minio_port: env::var("MINIO_PORT")
|
||||
.unwrap_or_else(|_| "9000".to_string())
|
||||
.parse()
|
||||
.unwrap_or(9000),
|
||||
minio_port_internal: env::var("MINIO_PORT_INTERNAL")
|
||||
.unwrap_or_else(|_| "9001".to_string())
|
||||
.parse()
|
||||
.unwrap_or(9001),
|
||||
data_dir_path: env::var("DATA_DIR_PATH").ok().filter(|s| !s.is_empty()),
|
||||
force_register: env::var("FORCE_REGISTER")
|
||||
.ok()
|
||||
.map(|v| v.to_lowercase() == "true")
|
||||
.unwrap_or(false),
|
||||
default_api_key: env::var("DEFAULT_API_KEY").unwrap_or_default(),
|
||||
default_server: env::var("DEFAULT_SERVER").ok().filter(|s| !s.is_empty()),
|
||||
default_user: env::var("DEFAULT_USER").ok().filter(|s| !s.is_empty()),
|
||||
})
|
||||
}
|
||||
}
|
||||
243
crates/siren-core/src/data/condition.rs
Normal file
243
crates/siren-core/src/data/condition.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
use crate::data::Value;
|
||||
|
||||
pub enum Condition {
|
||||
Simple(String, Vec<Value>),
|
||||
And(Box<Condition>, Box<Condition>),
|
||||
Or(Box<Condition>, Box<Condition>),
|
||||
Group(Box<Condition>),
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
pub fn new(condition: &str) -> Self {
|
||||
Condition::Simple(condition.to_string(), vec![])
|
||||
}
|
||||
|
||||
pub fn and(self, other: Self) -> Self {
|
||||
Condition::And(Box::new(self), Box::new(other))
|
||||
}
|
||||
|
||||
pub fn or(self, other: Self) -> Self {
|
||||
Condition::Or(Box::new(self), Box::new(other))
|
||||
}
|
||||
|
||||
pub fn group(self) -> Self {
|
||||
Condition::Group(Box::new(self))
|
||||
}
|
||||
|
||||
pub fn is_equal(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} = ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_equal(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} != ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_null(value: &str) -> Self {
|
||||
Condition::Simple(format!("{} IS NULL", value), vec![])
|
||||
}
|
||||
|
||||
pub fn not_null(value: &str) -> Self {
|
||||
Condition::Simple(format!("{} IS NOT NULL", value), vec![])
|
||||
}
|
||||
|
||||
pub fn is_in(left: &str, right: Vec<Value>) -> Self {
|
||||
// Use helper function to handle special cases
|
||||
if let Some(condition) = Self::handle_empty_or_all_none(left, &right, true) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
let right_list = right
|
||||
.iter()
|
||||
.map(|_v| "'?'".to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Condition::Simple(format!("{} IN ({})", left, right_list), right)
|
||||
}
|
||||
|
||||
pub fn not_in(left: &str, right: Vec<Value>) -> Self {
|
||||
// Use helper function to handle special cases
|
||||
if let Some(condition) = Self::handle_empty_or_all_none(left, &right, true) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
let right_list = right
|
||||
.iter()
|
||||
.map(|_v| "'?'".to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
Condition::Simple(format!("{} NOT IN ({})", left, right_list), right)
|
||||
}
|
||||
|
||||
pub fn like(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} LIKE '?'", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_like(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} NOT LIKE '?'", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn i_like(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} ILIKE '?'", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_i_like(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} NOT ILIKE '?'", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gt(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} > ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gte(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} >= ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lt(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} < ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lte(left: &str, right: impl Into<Value> + Clone) -> Self {
|
||||
let value = right.clone().into();
|
||||
match Self::from_optional_value(left, value) {
|
||||
Some(condition) => condition,
|
||||
None => Condition::Simple(format!("{} <= ?", left), vec![right.into()]),
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper function to handle optional values
|
||||
fn from_optional_value(left: &str, value: Value) -> Option<Self> {
|
||||
match value {
|
||||
Value::OptionalInt(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalBigInt(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalFloat(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalDouble(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalBool(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalText(None) => Some(Condition::is_null(left)),
|
||||
Value::OptionalDateTime(None) => Some(Condition::is_null(left)),
|
||||
_ => None, // For non-optional or Some(value), let the primary method handle it
|
||||
}
|
||||
}
|
||||
|
||||
// Private helper to handle `empty` or `all-None` lists
|
||||
fn handle_empty_or_all_none(left: &str, right: &[Value], negate: bool) -> Option<Self> {
|
||||
if right.is_empty() {
|
||||
// For an empty list, return an always-false condition
|
||||
// NOT IN with empty list is always TRUE, but we're defaulting to SQL-SAFE result (FALSE)
|
||||
return Some(Condition::Simple(
|
||||
if negate {
|
||||
"TRUE".to_string()
|
||||
} else {
|
||||
"FALSE".to_string()
|
||||
},
|
||||
vec![],
|
||||
));
|
||||
}
|
||||
|
||||
// Check if all elements in the `right` vector are `None` (Optional*)
|
||||
if right.iter().all(|v| {
|
||||
matches!(
|
||||
v,
|
||||
Value::OptionalInt(None)
|
||||
| Value::OptionalBigInt(None)
|
||||
| Value::OptionalFloat(None)
|
||||
| Value::OptionalDouble(None)
|
||||
| Value::OptionalBool(None)
|
||||
| Value::OptionalText(None)
|
||||
| Value::OptionalDateTime(None)
|
||||
)
|
||||
}) {
|
||||
// If all values are None, handle as NULL or NOT NULL
|
||||
return Some(if negate {
|
||||
Condition::not_null(left)
|
||||
} else {
|
||||
Condition::is_null(left)
|
||||
});
|
||||
}
|
||||
|
||||
// Otherwise, this is not an empty or all-none case
|
||||
None
|
||||
}
|
||||
|
||||
pub fn to_sql(&self, counter: &mut usize) -> (String, Vec<Value>) {
|
||||
let mut sql = String::new();
|
||||
let mut binds = Vec::new();
|
||||
|
||||
match self {
|
||||
Condition::Simple(condition, values) => {
|
||||
// Replace each instance of '?' with increasing numbered binds
|
||||
let mut numbered_condition = String::new();
|
||||
let mut chars = condition.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '?' {
|
||||
// Increment the counter and replace `?` with a numbered bind
|
||||
*counter += 1;
|
||||
numbered_condition.push_str(&format!("${}", *counter));
|
||||
} else {
|
||||
numbered_condition.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
sql.push_str(&numbered_condition);
|
||||
binds.extend(values.clone());
|
||||
}
|
||||
Condition::And(left, right) => {
|
||||
let (left_sql, left_binds) = left.to_sql(counter);
|
||||
let (right_sql, right_binds) = right.to_sql(counter);
|
||||
sql.push_str(&format!("{} AND {}", left_sql, right_sql));
|
||||
binds.extend(left_binds);
|
||||
binds.extend(right_binds);
|
||||
}
|
||||
Condition::Or(left, right) => {
|
||||
let (left_sql, left_binds) = left.to_sql(counter);
|
||||
let (right_sql, right_binds) = right.to_sql(counter);
|
||||
sql.push_str(&format!("{} OR {}", left_sql, right_sql));
|
||||
binds.extend(left_binds);
|
||||
binds.extend(right_binds);
|
||||
}
|
||||
Condition::Group(inner) => {
|
||||
let (inner_sql, inner_binds) = inner.to_sql(counter);
|
||||
sql.push_str(&format!("({})", inner_sql));
|
||||
binds.extend(inner_binds);
|
||||
}
|
||||
};
|
||||
|
||||
(sql, binds)
|
||||
}
|
||||
}
|
||||
1
crates/siren-core/src/data/dnd/backgrounds/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/backgrounds/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
crates/siren-core/src/data/dnd/bestiary/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/bestiary/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
0
crates/siren-core/src/data/dnd/campaigns/mod.rs
Normal file
0
crates/siren-core/src/data/dnd/campaigns/mod.rs
Normal file
0
crates/siren-core/src/data/dnd/characters/mod.rs
Normal file
0
crates/siren-core/src/data/dnd/characters/mod.rs
Normal file
3
crates/siren-core/src/data/dnd/classes/mod.rs
Normal file
3
crates/siren-core/src/data/dnd/classes/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod model;
|
||||
|
||||
pub use model::*;
|
||||
46
crates/siren-core/src/data/dnd/classes/model.rs
Normal file
46
crates/siren-core/src/data/dnd/classes/model.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::str::FromStr;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum AbilityType {
|
||||
#[serde(rename = "strength")]
|
||||
Strength,
|
||||
#[serde(rename = "dexterity")]
|
||||
Dexterity,
|
||||
#[serde(rename = "constitution")]
|
||||
Constitution,
|
||||
#[serde(rename = "intelligence")]
|
||||
Intelligence,
|
||||
#[serde(rename = "wisdom")]
|
||||
Wisdom,
|
||||
#[serde(rename = "charisma")]
|
||||
Charisma,
|
||||
}
|
||||
|
||||
impl AbilityType {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
AbilityType::Strength => "Strength".to_string(),
|
||||
AbilityType::Dexterity => "Dexterity".to_string(),
|
||||
AbilityType::Constitution => "Constitution".to_string(),
|
||||
AbilityType::Intelligence => "Intelligence".to_string(),
|
||||
AbilityType::Wisdom => "Wisdom".to_string(),
|
||||
AbilityType::Charisma => "Charisma".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for AbilityType {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Strength" => Ok(AbilityType::Strength),
|
||||
"Dexterity" => Ok(AbilityType::Dexterity),
|
||||
"Constitution" => Ok(AbilityType::Constitution),
|
||||
"Intelligence" => Ok(AbilityType::Intelligence),
|
||||
"Wisdom" => Ok(AbilityType::Wisdom),
|
||||
"Charisma" => Ok(AbilityType::Charisma),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
82
crates/siren-core/src/data/dnd/conditions/mod.rs
Normal file
82
crates/siren-core/src/data/dnd/conditions/mod.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use std::str::FromStr;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum ConditionType {
|
||||
#[serde(rename = "blinded")]
|
||||
Blinded,
|
||||
#[serde(rename = "charmed")]
|
||||
Charmed,
|
||||
#[serde(rename = "deafened")]
|
||||
Deafened,
|
||||
#[serde(rename = "exhaustion")]
|
||||
Exhaustion,
|
||||
#[serde(rename = "frightened")]
|
||||
Frightened,
|
||||
#[serde(rename = "grappled")]
|
||||
Grappled,
|
||||
#[serde(rename = "incapacitated")]
|
||||
Incapacitated,
|
||||
#[serde(rename = "invisible")]
|
||||
Invisible,
|
||||
#[serde(rename = "paralyzed")]
|
||||
Paralyzed,
|
||||
#[serde(rename = "petrified")]
|
||||
Petrified,
|
||||
#[serde(rename = "poisoned")]
|
||||
Poisoned,
|
||||
#[serde(rename = "prone")]
|
||||
Prone,
|
||||
#[serde(rename = "restrained")]
|
||||
Restrained,
|
||||
#[serde(rename = "stunned")]
|
||||
Stunned,
|
||||
#[serde(rename = "unconscious")]
|
||||
Unconscious,
|
||||
}
|
||||
|
||||
impl ConditionType {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
ConditionType::Blinded => "Blinded".to_string(),
|
||||
ConditionType::Charmed => "Charmed".to_string(),
|
||||
ConditionType::Deafened => "Deafened".to_string(),
|
||||
ConditionType::Exhaustion => "Exhaustion".to_string(),
|
||||
ConditionType::Frightened => "Frightened".to_string(),
|
||||
ConditionType::Grappled => "Grappled".to_string(),
|
||||
ConditionType::Incapacitated => "Incapacitated".to_string(),
|
||||
ConditionType::Invisible => "Invisible".to_string(),
|
||||
ConditionType::Paralyzed => "Paralyzed".to_string(),
|
||||
ConditionType::Petrified => "Petrified".to_string(),
|
||||
ConditionType::Poisoned => "Poisoned".to_string(),
|
||||
ConditionType::Prone => "Prone".to_string(),
|
||||
ConditionType::Restrained => "Restrained".to_string(),
|
||||
ConditionType::Stunned => "Stunned".to_string(),
|
||||
ConditionType::Unconscious => "Unconscious".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ConditionType {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"Blinded" => Ok(ConditionType::Blinded),
|
||||
"Charmed" => Ok(ConditionType::Charmed),
|
||||
"Deafened" => Ok(ConditionType::Deafened),
|
||||
"Exhaustion" => Ok(ConditionType::Exhaustion),
|
||||
"Frightened" => Ok(ConditionType::Frightened),
|
||||
"Grappled" => Ok(ConditionType::Grappled),
|
||||
"Incapacitated" => Ok(ConditionType::Incapacitated),
|
||||
"Invisible" => Ok(ConditionType::Invisible),
|
||||
"Paralyzed" => Ok(ConditionType::Paralyzed),
|
||||
"Petrified" => Ok(ConditionType::Petrified),
|
||||
"Poisoned" => Ok(ConditionType::Poisoned),
|
||||
"Prone" => Ok(ConditionType::Prone),
|
||||
"Restrained" => Ok(ConditionType::Restrained),
|
||||
"Stunned" => Ok(ConditionType::Stunned),
|
||||
"Unconscious" => Ok(ConditionType::Unconscious),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
1
crates/siren-core/src/data/dnd/feats/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/feats/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
crates/siren-core/src/data/dnd/items/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/items/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
13
crates/siren-core/src/data/dnd/mod.rs
Normal file
13
crates/siren-core/src/data/dnd/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod backgrounds;
|
||||
pub mod bestiary;
|
||||
pub mod classes;
|
||||
pub mod conditions;
|
||||
pub mod feats;
|
||||
pub mod items;
|
||||
pub mod options;
|
||||
pub mod races;
|
||||
pub mod spells;
|
||||
|
||||
pub fn load_data(data_dir_path: &str) {
|
||||
spells::load_data(data_dir_path);
|
||||
}
|
||||
1
crates/siren-core/src/data/dnd/options/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/options/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
crates/siren-core/src/data/dnd/races/mod.rs
Normal file
1
crates/siren-core/src/data/dnd/races/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
62
crates/siren-core/src/data/dnd/spells/mod.rs
Normal file
62
crates/siren-core/src/data/dnd/spells/mod.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
mod model;
|
||||
mod types;
|
||||
|
||||
use std::{
|
||||
fs::{metadata, File, read_dir},
|
||||
path::Path,
|
||||
io::BufReader,
|
||||
};
|
||||
|
||||
pub use model::*;
|
||||
pub use types::*;
|
||||
|
||||
pub fn load_data(data_dir_path: &str) {
|
||||
if Path::new(data_dir_path).exists() {
|
||||
let meta = metadata(data_dir_path).unwrap();
|
||||
if meta.is_dir() {
|
||||
let spells_dir_path = format!("{}/spells", data_dir_path);
|
||||
if Path::new(&spells_dir_path).exists() {
|
||||
let meta = metadata(&spells_dir_path).unwrap();
|
||||
if meta.is_dir() {
|
||||
for entry in read_dir(&spells_dir_path).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let file = File::open(path).unwrap();
|
||||
let reader = BufReader::new(file);
|
||||
let result: Result<Vec<Spell>, serde_json::Error> = serde_json::from_reader(reader);
|
||||
match result {
|
||||
Ok(spells) => {
|
||||
for spell in spells {
|
||||
// let mut filters = QueryFilters::default();
|
||||
// filters.by_name = Some(spell.name.clone());
|
||||
// match QuerySpell::get_all(&filters, 100, 1) {
|
||||
// Ok(spells) => {
|
||||
// if spells.len() > 0 {
|
||||
// trace!("Spell '{}' already exists", spell.name);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// Err(err) => {
|
||||
// warn!("Error checking if spell '{}' exists: {}", spell.name, err);
|
||||
// continue;
|
||||
// }
|
||||
// };
|
||||
// let spell = InsertSpell::insert(spell.into()).unwrap();
|
||||
// trace!("Inserted spell: {}", spell.name);
|
||||
}
|
||||
}
|
||||
Err(err) => log::warn!("Error reading spells from file: {}", err),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Data path '{}' does not exist, no data imported",
|
||||
data_dir_path
|
||||
);
|
||||
}
|
||||
}
|
||||
184
crates/siren-core/src/data/dnd/spells/model.rs
Normal file
184
crates/siren-core/src/data/dnd/spells/model.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::dnd::{classes::AbilityType, conditions::ConditionType};
|
||||
|
||||
use super::{
|
||||
SchoolType, CastingTime, SpellAttackType, SpellDamageType, Range, Area, Components, Duration,
|
||||
Source, Description, DurationType, Effect,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct QuerySpell {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub school: String,
|
||||
pub level: i32,
|
||||
pub ritual: bool,
|
||||
pub concentration: bool,
|
||||
pub classes: Vec<String>,
|
||||
pub damage_inflict: Vec<String>,
|
||||
pub damage_resist: Vec<String>,
|
||||
pub conditions: Vec<String>,
|
||||
pub saving_throw: Vec<String>,
|
||||
pub attack_type: Option<String>,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InsertSpell {
|
||||
pub name: String,
|
||||
pub school: String,
|
||||
pub level: i32,
|
||||
pub ritual: bool,
|
||||
pub concentration: bool,
|
||||
pub classes: Vec<String>,
|
||||
pub damage_inflict: Vec<String>,
|
||||
pub damage_resist: Vec<String>,
|
||||
pub conditions: Vec<String>,
|
||||
pub saving_throw: Vec<String>,
|
||||
pub attack_type: Option<String>,
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Spell {
|
||||
pub id: Option<i32>,
|
||||
pub name: String,
|
||||
pub school: SchoolType,
|
||||
pub level: i32,
|
||||
pub ritual: bool,
|
||||
pub casting_time: CastingTime,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub effect: Option<Effect>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub saving_throw: Option<Vec<AbilityType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attack_type: Option<SpellAttackType>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub damage_inflict: Option<Vec<SpellDamageType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub damage_resist: Option<Vec<SpellDamageType>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conditions: Option<Vec<ConditionType>>,
|
||||
pub range: Range,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub area: Option<Area>,
|
||||
pub components: Components,
|
||||
pub durations: Vec<Duration>,
|
||||
pub classes: Vec<String>,
|
||||
pub sources: Vec<Source>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub description: Option<Description>,
|
||||
}
|
||||
|
||||
impl From<QuerySpell> for Spell {
|
||||
fn from(query: QuerySpell) -> Self {
|
||||
return match serde_json::from_value(query.data) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
log::error!("Failed to parse spell: {}", err);
|
||||
Self {
|
||||
id: None,
|
||||
name: "".to_string(),
|
||||
school: SchoolType::Abjuration,
|
||||
level: 0,
|
||||
ritual: false,
|
||||
casting_time: CastingTime {
|
||||
value: 0,
|
||||
casting_type: "".to_string(),
|
||||
note: None,
|
||||
},
|
||||
effect: None,
|
||||
saving_throw: None,
|
||||
attack_type: None,
|
||||
damage_inflict: None,
|
||||
damage_resist: None,
|
||||
conditions: None,
|
||||
range: Range {
|
||||
range_type: "".to_string(),
|
||||
value: None,
|
||||
unit: None,
|
||||
},
|
||||
area: None,
|
||||
components: Components {
|
||||
verbal: false,
|
||||
somatic: false,
|
||||
material: false,
|
||||
materials_needed: None,
|
||||
materials_cost: None,
|
||||
materials_consumed: None,
|
||||
},
|
||||
durations: vec![],
|
||||
classes: vec![],
|
||||
sources: vec![],
|
||||
tags: None,
|
||||
description: None,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<InsertSpell> for Spell {
|
||||
fn into(self) -> InsertSpell {
|
||||
return InsertSpell {
|
||||
name: self.name.to_string(),
|
||||
school: self.school.to_string(),
|
||||
level: self.level,
|
||||
ritual: self.ritual,
|
||||
concentration: self
|
||||
.durations
|
||||
.iter()
|
||||
.any(|duration| match duration.duration_type {
|
||||
DurationType::Concentration => true,
|
||||
_ => false,
|
||||
}),
|
||||
classes: self
|
||||
.classes
|
||||
.iter()
|
||||
.map(|class| class.to_string())
|
||||
.collect::<Vec<String>>(),
|
||||
damage_inflict: match &self.damage_inflict {
|
||||
Some(damage_inflict) => damage_inflict
|
||||
.iter()
|
||||
.map(|damage_inflict| damage_inflict.to_string())
|
||||
.collect(),
|
||||
None => vec![],
|
||||
},
|
||||
damage_resist: match &self.damage_resist {
|
||||
Some(damage_resist) => damage_resist
|
||||
.iter()
|
||||
.map(|damage_resist| damage_resist.to_string())
|
||||
.collect(),
|
||||
None => vec![],
|
||||
},
|
||||
conditions: match &self.conditions {
|
||||
Some(conditions) => conditions
|
||||
.iter()
|
||||
.map(|condition| condition.to_string())
|
||||
.collect(),
|
||||
None => vec![],
|
||||
},
|
||||
saving_throw: match &self.saving_throw {
|
||||
Some(saving_throw) => saving_throw
|
||||
.iter()
|
||||
.map(|saving_throw| saving_throw.to_string())
|
||||
.collect(),
|
||||
None => vec![],
|
||||
},
|
||||
attack_type: self
|
||||
.attack_type
|
||||
.as_ref()
|
||||
.map(|attack_type| attack_type.to_string()),
|
||||
data: match serde_json::to_value(&self) {
|
||||
Ok(data) => data,
|
||||
Err(err) => {
|
||||
log::error!("Failed to serialize spell: {}", err);
|
||||
serde_json::Value::Null
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
366
crates/siren-core/src/data/dnd/spells/types.rs
Normal file
366
crates/siren-core/src/data/dnd/spells/types.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use std::str::FromStr;
|
||||
use serde::{Deserialize, Serialize, ser::SerializeMap};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SchoolType {
|
||||
#[serde(rename = "abjuration")]
|
||||
Abjuration,
|
||||
#[serde(rename = "conjuration")]
|
||||
Conjuration,
|
||||
#[serde(rename = "divination")]
|
||||
Divination,
|
||||
#[serde(rename = "enchantment")]
|
||||
Enchantment,
|
||||
#[serde(rename = "evocation")]
|
||||
Evocation,
|
||||
#[serde(rename = "illusion")]
|
||||
Illusion,
|
||||
#[serde(rename = "necromancy")]
|
||||
Necromancy,
|
||||
#[serde(rename = "transmutation")]
|
||||
Transmutation,
|
||||
}
|
||||
|
||||
impl SchoolType {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
SchoolType::Abjuration => "abjuration".to_string(),
|
||||
SchoolType::Conjuration => "conjuration".to_string(),
|
||||
SchoolType::Divination => "divination".to_string(),
|
||||
SchoolType::Enchantment => "enchantment".to_string(),
|
||||
SchoolType::Evocation => "evocation".to_string(),
|
||||
SchoolType::Illusion => "illusion".to_string(),
|
||||
SchoolType::Necromancy => "necromancy".to_string(),
|
||||
SchoolType::Transmutation => "transmutation".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SchoolType {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"abjuration" => Ok(SchoolType::Abjuration),
|
||||
"conjuration" => Ok(SchoolType::Conjuration),
|
||||
"divination" => Ok(SchoolType::Divination),
|
||||
"enchantment" => Ok(SchoolType::Enchantment),
|
||||
"evocation" => Ok(SchoolType::Evocation),
|
||||
"illusion" => Ok(SchoolType::Illusion),
|
||||
"necromancy" => Ok(SchoolType::Necromancy),
|
||||
"transmutation" => Ok(SchoolType::Transmutation),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CastingTime {
|
||||
pub value: i32,
|
||||
#[serde(rename = "unit")]
|
||||
pub casting_type: String,
|
||||
pub note: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SpellAttackType {
|
||||
#[serde(rename = "melee")]
|
||||
Melee,
|
||||
#[serde(rename = "ranged")]
|
||||
Ranged,
|
||||
}
|
||||
|
||||
impl SpellAttackType {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
SpellAttackType::Melee => "melee".to_string(),
|
||||
SpellAttackType::Ranged => "ranged".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SpellAttackType {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"melee" => Ok(SpellAttackType::Melee),
|
||||
"ranged" => Ok(SpellAttackType::Ranged),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum SpellDamageType {
|
||||
#[serde(rename = "acid")]
|
||||
Acid,
|
||||
#[serde(rename = "bludgeoning")]
|
||||
Bludgeoning,
|
||||
#[serde(rename = "cold")]
|
||||
Cold,
|
||||
#[serde(rename = "fire")]
|
||||
Fire,
|
||||
#[serde(rename = "force")]
|
||||
Force,
|
||||
#[serde(rename = "lightning")]
|
||||
Lightning,
|
||||
#[serde(rename = "necrotic")]
|
||||
Necrotic,
|
||||
#[serde(rename = "piercing")]
|
||||
Piercing,
|
||||
#[serde(rename = "poison")]
|
||||
Poison,
|
||||
#[serde(rename = "psychic")]
|
||||
Psychic,
|
||||
#[serde(rename = "radiant")]
|
||||
Radiant,
|
||||
#[serde(rename = "slashing")]
|
||||
Slashing,
|
||||
#[serde(rename = "thunder")]
|
||||
Thunder,
|
||||
}
|
||||
|
||||
impl SpellDamageType {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
SpellDamageType::Acid => "acid".to_string(),
|
||||
SpellDamageType::Bludgeoning => "bludgeoning".to_string(),
|
||||
SpellDamageType::Cold => "cold".to_string(),
|
||||
SpellDamageType::Fire => "fire".to_string(),
|
||||
SpellDamageType::Force => "force".to_string(),
|
||||
SpellDamageType::Lightning => "lightning".to_string(),
|
||||
SpellDamageType::Necrotic => "necrotic".to_string(),
|
||||
SpellDamageType::Piercing => "piercing".to_string(),
|
||||
SpellDamageType::Poison => "poison".to_string(),
|
||||
SpellDamageType::Psychic => "psychic".to_string(),
|
||||
SpellDamageType::Radiant => "radiant".to_string(),
|
||||
SpellDamageType::Slashing => "slashing".to_string(),
|
||||
SpellDamageType::Thunder => "thunder".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SpellDamageType {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"acid" => Ok(SpellDamageType::Acid),
|
||||
"bludgeoning" => Ok(SpellDamageType::Bludgeoning),
|
||||
"cold" => Ok(SpellDamageType::Cold),
|
||||
"fire" => Ok(SpellDamageType::Fire),
|
||||
"force" => Ok(SpellDamageType::Force),
|
||||
"lightning" => Ok(SpellDamageType::Lightning),
|
||||
"necrotic" => Ok(SpellDamageType::Necrotic),
|
||||
"piercing" => Ok(SpellDamageType::Piercing),
|
||||
"poison" => Ok(SpellDamageType::Poison),
|
||||
"psychic" => Ok(SpellDamageType::Psychic),
|
||||
"radiant" => Ok(SpellDamageType::Radiant),
|
||||
"slashing" => Ok(SpellDamageType::Slashing),
|
||||
"thunder" => Ok(SpellDamageType::Thunder),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Range {
|
||||
#[serde(rename = "type")]
|
||||
pub range_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Area {
|
||||
#[serde(rename = "type")]
|
||||
pub area_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Duration {
|
||||
#[serde(rename = "type")]
|
||||
pub duration_type: DurationType,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub value: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub unit: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum DurationType {
|
||||
#[serde(rename = "concentration")]
|
||||
Concentration,
|
||||
#[serde(rename = "instantaneous")]
|
||||
Instantaneous,
|
||||
#[serde(rename = "timed")]
|
||||
Timed,
|
||||
#[serde(rename = "dispelled")]
|
||||
UntilDispelled,
|
||||
#[serde(rename = "special")]
|
||||
Special,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Source {
|
||||
pub source: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub page: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Description {
|
||||
pub entries: Vec<Entry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Entry {
|
||||
pub text: Option<String>,
|
||||
pub list: Option<Vec<String>>,
|
||||
pub table: Option<EntryTable>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EntryTable {
|
||||
pub headers: Vec<String>,
|
||||
pub rows: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Entry {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = serde_json::Value::deserialize(deserializer)?;
|
||||
match value {
|
||||
serde_json::Value::String(s) => Ok(Entry {
|
||||
text: Some(s),
|
||||
list: None,
|
||||
table: None,
|
||||
}),
|
||||
serde_json::Value::Object(o) => {
|
||||
let text = match o.get("text") {
|
||||
Some(t) => match t.as_str() {
|
||||
Some(s) => Some(s.to_string()),
|
||||
None => return Err(serde::de::Error::custom("Invalid entry text")),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let list = match o.get("list") {
|
||||
Some(i) => match i.as_array() {
|
||||
Some(a) => {
|
||||
let mut list = Vec::new();
|
||||
for item in a {
|
||||
match item.as_str() {
|
||||
Some(s) => list.push(s.to_string()),
|
||||
None => return Err(serde::de::Error::custom("Invalid entry list item")),
|
||||
}
|
||||
}
|
||||
Some(list)
|
||||
}
|
||||
None => return Err(serde::de::Error::custom("Invalid entry list items")),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
let table = match o.get("table") {
|
||||
Some(t) => match t.as_object() {
|
||||
Some(o) => {
|
||||
let mut headers = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
match o.get("headers") {
|
||||
Some(c) => match c.as_array() {
|
||||
Some(a) => {
|
||||
for item in a {
|
||||
match item.as_str() {
|
||||
Some(s) => headers.push(s.to_string()),
|
||||
None => return Err(serde::de::Error::custom("Invalid entry table header")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return Err(serde::de::Error::custom("Invalid entry table headers")),
|
||||
},
|
||||
None => return Err(serde::de::Error::custom("Missing entry table headers")),
|
||||
};
|
||||
match o.get("rows") {
|
||||
Some(r) => match r.as_array() {
|
||||
Some(a) => {
|
||||
for row in a {
|
||||
match row.as_array() {
|
||||
Some(a) => {
|
||||
let mut row = Vec::new();
|
||||
for item in a {
|
||||
match item.as_str() {
|
||||
Some(s) => row.push(s.to_string()),
|
||||
None => {
|
||||
return Err(serde::de::Error::custom(
|
||||
"Invalid entry table row item",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
None => return Err(serde::de::Error::custom("Invalid entry table row")),
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return Err(serde::de::Error::custom("Invalid entry table rows")),
|
||||
},
|
||||
None => return Err(serde::de::Error::custom("Missing entry table rows")),
|
||||
};
|
||||
Some(EntryTable { headers, rows })
|
||||
}
|
||||
None => return Err(serde::de::Error::custom("Invalid entry table")),
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
Ok(Entry { text, list, table })
|
||||
}
|
||||
_ => Err(serde::de::Error::custom("Invalid entry")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Entry {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
if let Some(text) = &self.text {
|
||||
map.serialize_entry("text", text)?;
|
||||
}
|
||||
if let Some(list) = &self.list {
|
||||
map.serialize_entry("list", list)?;
|
||||
}
|
||||
if let Some(table) = &self.table {
|
||||
map.serialize_entry("table", table)?;
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Components {
|
||||
pub verbal: bool,
|
||||
pub somatic: bool,
|
||||
pub material: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub materials_needed: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub materials_cost: Option<i32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub materials_consumed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Effect {
|
||||
pub effect_type: Option<String>,
|
||||
}
|
||||
3
crates/siren-core/src/data/events/mod.rs
Normal file
3
crates/siren-core/src/data/events/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod model;
|
||||
|
||||
pub use model::*;
|
||||
57
crates/siren-core/src/data/events/model.rs
Normal file
57
crates/siren-core/src/data/events/model.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use crate::error::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
const TABLE_NAME: &str = "events";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Event {
|
||||
pub id: Uuid,
|
||||
pub guild_id: i64,
|
||||
pub author_id: i64,
|
||||
pub title: String,
|
||||
pub date_time: DateTime<Utc>,
|
||||
pub description: Option<String>,
|
||||
pub rsvp: Vec<i64>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub async fn insert(&self) -> Result<()> {
|
||||
let pool = crate::data::pool();
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO {} (
|
||||
id,
|
||||
guild_id,
|
||||
author_id,
|
||||
title,
|
||||
date_time,
|
||||
description,
|
||||
rsvp
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7
|
||||
)",
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(self.id)
|
||||
.bind(self.guild_id)
|
||||
.bind(self.author_id)
|
||||
.bind(&self.title)
|
||||
.bind(self.date_time)
|
||||
.bind(&self.description)
|
||||
.bind(&self.rsvp)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_by_id(id: i64) -> Result<Option<Self>> {
|
||||
let pool = crate::data::pool();
|
||||
let item = sqlx::query_as::<_, Self>(&format!("SELECT * FROM {} WHERE id = $1", TABLE_NAME))
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(item)
|
||||
}
|
||||
}
|
||||
106
crates/siren-core/src/data/executable_query.rs
Normal file
106
crates/siren-core/src/data/executable_query.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::data::Value;
|
||||
use sqlx::{FromRow, Postgres};
|
||||
|
||||
#[allow(async_fn_in_trait)]
|
||||
pub trait ExecutableQuery {
|
||||
fn build(&self) -> (String, Vec<Value>);
|
||||
|
||||
async fn execute(&self) -> Result<sqlx::postgres::PgQueryResult, sqlx::Error> {
|
||||
// Build the SQL query and its values
|
||||
let (query_string, values) = self.build();
|
||||
|
||||
// Start constructing the query
|
||||
let mut query = sqlx::query(&query_string);
|
||||
|
||||
// Bind each value to its respective placeholder
|
||||
for value in values {
|
||||
match value {
|
||||
Value::Int(n) => query = query.bind(n),
|
||||
Value::OptionalInt(n) => query = query.bind(n),
|
||||
Value::BigInt(n) => query = query.bind(n),
|
||||
Value::OptionalBigInt(n) => query = query.bind(n),
|
||||
Value::Float(n) => query = query.bind(n),
|
||||
Value::OptionalFloat(n) => query = query.bind(n),
|
||||
Value::Double(n) => query = query.bind(n),
|
||||
Value::OptionalDouble(n) => query = query.bind(n),
|
||||
Value::Bool(n) => query = query.bind(n),
|
||||
Value::OptionalBool(n) => query = query.bind(n),
|
||||
Value::Text(n) => query = query.bind(n),
|
||||
Value::OptionalText(n) => query = query.bind(n),
|
||||
Value::DateTime(n) => query = query.bind(n),
|
||||
Value::OptionalDateTime(n) => query = query.bind(n),
|
||||
}
|
||||
}
|
||||
|
||||
let pool = crate::data::pool();
|
||||
query.execute(pool).await
|
||||
}
|
||||
|
||||
async fn fetch_optional<
|
||||
T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>,
|
||||
>(
|
||||
&self,
|
||||
) -> Option<T> {
|
||||
let (query_string, values) = self.build();
|
||||
let mut query_as = sqlx::query_as(&query_string);
|
||||
for value in values {
|
||||
match value {
|
||||
Value::Int(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalInt(n) => query_as = query_as.bind(n),
|
||||
Value::BigInt(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalBigInt(n) => query_as = query_as.bind(n),
|
||||
Value::Float(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalFloat(n) => query_as = query_as.bind(n),
|
||||
Value::Double(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalDouble(n) => query_as = query_as.bind(n),
|
||||
Value::Bool(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalBool(n) => query_as = query_as.bind(n),
|
||||
Value::Text(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalText(n) => query_as = query_as.bind(n),
|
||||
Value::DateTime(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalDateTime(n) => query_as = query_as.bind(n),
|
||||
}
|
||||
}
|
||||
|
||||
let pool = crate::data::pool();
|
||||
query_as.fetch_optional(pool).await.unwrap_or_else(|err| {
|
||||
log::error!(
|
||||
"Unable to fetch optional on query '{}': {}",
|
||||
query_string,
|
||||
err
|
||||
);
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_all<T: Send + Unpin + for<'r> FromRow<'r, <Postgres as sqlx::Database>::Row>>(
|
||||
&self,
|
||||
) -> Vec<T> {
|
||||
let (query_string, values) = self.build();
|
||||
let mut query_as = sqlx::query_as(&query_string);
|
||||
for value in values {
|
||||
match value {
|
||||
Value::Int(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalInt(n) => query_as = query_as.bind(n),
|
||||
Value::BigInt(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalBigInt(n) => query_as = query_as.bind(n),
|
||||
Value::Float(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalFloat(n) => query_as = query_as.bind(n),
|
||||
Value::Double(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalDouble(n) => query_as = query_as.bind(n),
|
||||
Value::Bool(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalBool(n) => query_as = query_as.bind(n),
|
||||
Value::Text(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalText(n) => query_as = query_as.bind(n),
|
||||
Value::DateTime(n) => query_as = query_as.bind(n),
|
||||
Value::OptionalDateTime(n) => query_as = query_as.bind(n),
|
||||
}
|
||||
}
|
||||
|
||||
let pool = crate::data::pool();
|
||||
query_as.fetch_all(pool).await.unwrap_or_else(|err| {
|
||||
log::error!("Unable to fetch all on query '{}': {}", query_string, err);
|
||||
vec![]
|
||||
})
|
||||
}
|
||||
}
|
||||
3
crates/siren-core/src/data/guilds/mod.rs
Normal file
3
crates/siren-core/src/data/guilds/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod model;
|
||||
|
||||
pub use model::*;
|
||||
53
crates/siren-core/src/data/guilds/model.rs
Normal file
53
crates/siren-core/src/data/guilds/model.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::{
|
||||
data::{
|
||||
Value,
|
||||
condition::Condition,
|
||||
executable_query::ExecutableQuery,
|
||||
insert::InsertBuilder,
|
||||
query::QueryBuilder,
|
||||
update::UpdateBuilder,
|
||||
},
|
||||
error::Result,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TABLE_NAME: &str = "guilds";
|
||||
|
||||
#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)]
|
||||
pub struct GuildCache {
|
||||
pub id: i64,
|
||||
pub name: Option<String>,
|
||||
pub owner_id: Option<i64>,
|
||||
pub volume: i32,
|
||||
}
|
||||
|
||||
impl GuildCache {
|
||||
pub async fn insert(&self) -> Result<()> {
|
||||
InsertBuilder::new(TABLE_NAME)
|
||||
.column("id", Value::BigInt(self.id))
|
||||
.column("name", Value::OptionalText(self.name.clone()))
|
||||
.column("owner_id", Value::OptionalBigInt(self.owner_id))
|
||||
.column("volume", Value::Int(self.volume))
|
||||
.execute()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_id(id: i64) -> Option<Self> {
|
||||
QueryBuilder::new(TABLE_NAME)
|
||||
.where_condition(Condition::is_equal("id", Value::BigInt(id)))
|
||||
.fetch_optional()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update(&self) -> Result<()> {
|
||||
UpdateBuilder::new(TABLE_NAME)
|
||||
.column("name", Value::OptionalText(self.name.clone()))
|
||||
.column("owner_id", Value::OptionalBigInt(self.owner_id))
|
||||
.column("volume", Value::Int(self.volume))
|
||||
.where_condition(Condition::is_equal("id", Value::BigInt(self.id)))
|
||||
.execute()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
60
crates/siren-core/src/data/insert.rs
Normal file
60
crates/siren-core/src/data/insert.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::data::{Value, executable_query::ExecutableQuery};
|
||||
|
||||
pub struct InsertBuilder {
|
||||
table: String,
|
||||
columns: Vec<String>,
|
||||
returning: Vec<String>,
|
||||
values: Vec<Value>,
|
||||
}
|
||||
|
||||
impl InsertBuilder {
|
||||
pub fn new(table: &str) -> Self {
|
||||
Self {
|
||||
table: table.to_string(),
|
||||
columns: Vec::new(),
|
||||
returning: Vec::new(),
|
||||
values: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column(mut self, column: &str, value: Value) -> Self {
|
||||
self.columns.push(column.to_string());
|
||||
self.values.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn returning(mut self, columns: &[&str]) -> Self {
|
||||
self.returning = columns.iter().map(|s| s.to_string()).collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecutableQuery for InsertBuilder {
|
||||
fn build(&self) -> (String, Vec<Value>) {
|
||||
if self.columns.is_empty() || self.values.is_empty() {
|
||||
panic!("Cannot build insert query without columns and values");
|
||||
}
|
||||
|
||||
// Create the list of column names
|
||||
let columns = self.columns.join(", ");
|
||||
|
||||
// Generate placeholders for values ($1, $2, etc.)
|
||||
let placeholders = (1..=self.values.len())
|
||||
.map(|i| format!("${}", i))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
// Create the basic INSERT statement
|
||||
let mut query = format!(
|
||||
"INSERT INTO {} ({}) VALUES ({})",
|
||||
self.table, columns, placeholders
|
||||
);
|
||||
|
||||
// Add RETURNING clause if specified
|
||||
if !self.returning.is_empty() {
|
||||
query.push_str(&format!(" RETURNING {}", self.returning.join(", ")));
|
||||
}
|
||||
|
||||
(query, self.values.clone())
|
||||
}
|
||||
}
|
||||
3
crates/siren-core/src/data/messages/mod.rs
Normal file
3
crates/siren-core/src/data/messages/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod model;
|
||||
|
||||
pub use model::*;
|
||||
74
crates/siren-core/src/data/messages/model.rs
Normal file
74
crates/siren-core/src/data/messages/model.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TABLE_NAME: &str = "messages";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct MessageCache {
|
||||
pub id: String,
|
||||
pub guild_id: i64,
|
||||
pub channel_id: i64,
|
||||
pub author_id: i64,
|
||||
pub created: i64,
|
||||
pub model: String,
|
||||
pub request: String,
|
||||
pub response: String,
|
||||
pub request_tags: Vec<String>,
|
||||
pub response_tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl MessageCache {
|
||||
pub async fn insert(&self) -> Result<()> {
|
||||
let pool = crate::data::pool();
|
||||
sqlx::query(&format!(
|
||||
"INSERT INTO {} (
|
||||
id,
|
||||
guild_id,
|
||||
channel_id,
|
||||
author_id,
|
||||
created,
|
||||
model,
|
||||
request,
|
||||
response,
|
||||
request_tags,
|
||||
response_tags
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||
)",
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(&self.id)
|
||||
.bind(self.guild_id)
|
||||
.bind(self.channel_id)
|
||||
.bind(self.author_id)
|
||||
.bind(self.created)
|
||||
.bind(&self.model)
|
||||
.bind(&self.request)
|
||||
.bind(&self.response)
|
||||
.bind(&self.request_tags)
|
||||
.bind(&self.response_tags)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find(
|
||||
guild_id: i64,
|
||||
channel_id: i64,
|
||||
author_id: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<MessageCache>> {
|
||||
let pool = crate::data::pool();
|
||||
let messages = sqlx::query_as::<_, MessageCache>(&format!(
|
||||
"SELECT * FROM {} WHERE guild_id = $1 AND channel_id = $2 AND author_id = $3 ORDER BY created ASC LIMIT $4",
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(guild_id)
|
||||
.bind(channel_id)
|
||||
.bind(author_id)
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(messages)
|
||||
}
|
||||
}
|
||||
137
crates/siren-core/src/data/mod.rs
Normal file
137
crates/siren-core/src/data/mod.rs
Normal file
@@ -0,0 +1,137 @@
|
||||
use crate::error::Result;
|
||||
use chrono::{DateTime, Utc};
|
||||
use redis::{Client as RedisClient, RedisResult, aio::MultiplexedConnection as RedisConnection};
|
||||
use sqlx::{Pool, Postgres, postgres::PgPoolOptions};
|
||||
use std::{fmt, fmt::Display, sync::OnceLock, time::Duration};
|
||||
|
||||
pub mod condition;
|
||||
pub mod events;
|
||||
mod executable_query;
|
||||
pub mod guilds;
|
||||
pub mod insert;
|
||||
pub mod messages;
|
||||
pub mod query;
|
||||
pub mod update;
|
||||
use crate::config::EnvironmentConfiguration;
|
||||
pub use executable_query::ExecutableQuery;
|
||||
|
||||
static POOL: OnceLock<Pool<Postgres>> = OnceLock::new();
|
||||
static REDIS: OnceLock<RedisClient> = OnceLock::new();
|
||||
|
||||
pub async fn initialize(config: &EnvironmentConfiguration) -> Result<()> {
|
||||
log::info!("Initializing database...");
|
||||
|
||||
// Setup Postgres pool connection
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.acquire_timeout(Duration::from_secs(30))
|
||||
.connect(&format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
config.postgres_user,
|
||||
config.postgres_password,
|
||||
config.postgres_host,
|
||||
config.postgres_port,
|
||||
config.postgres_database
|
||||
))
|
||||
.await?;
|
||||
match POOL.set(pool) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
log::warn!("Database pool already initialized");
|
||||
}
|
||||
}
|
||||
|
||||
// Setup Redis connection
|
||||
let redis = {
|
||||
let host = std::env::var("VALKEY_HOST").unwrap_or("localhost".to_string());
|
||||
let port = std::env::var("VALKEY_PORT").unwrap_or("6379".to_string());
|
||||
let url = format!("redis://{}:{}", host, port);
|
||||
RedisClient::open(url).expect("Failed to create valkey client")
|
||||
};
|
||||
match REDIS.set(redis) {
|
||||
Ok(_) => {}
|
||||
Err(_) => {
|
||||
log::warn!("Valkey client already initialized");
|
||||
}
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
match run_migrations().await {
|
||||
Ok(_) => log::debug!("Successfully ran migrations"),
|
||||
Err(e) => log::error!("Failed to run migrations: {}", e),
|
||||
}
|
||||
|
||||
log::info!("Database initialized");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pool() -> &'static Pool<Postgres> {
|
||||
POOL.get().unwrap()
|
||||
}
|
||||
|
||||
fn redis() -> &'static RedisClient {
|
||||
REDIS.get().unwrap()
|
||||
}
|
||||
|
||||
pub fn redis_connection() -> RedisResult<redis::Connection> {
|
||||
let conn = redis().get_connection()?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn redis_async_connection() -> RedisResult<RedisConnection> {
|
||||
let conn = redis().get_multiplexed_async_connection().await?;
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
async fn run_migrations() -> Result<()> {
|
||||
log::debug!("Running migrations");
|
||||
let pool = pool();
|
||||
sqlx::migrate!("../../migrations").run(pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
Int(i32),
|
||||
OptionalInt(Option<i32>),
|
||||
BigInt(i64),
|
||||
OptionalBigInt(Option<i64>),
|
||||
Float(f32),
|
||||
OptionalFloat(Option<f32>),
|
||||
Double(f64),
|
||||
OptionalDouble(Option<f64>),
|
||||
Bool(bool),
|
||||
OptionalBool(Option<bool>),
|
||||
Text(String),
|
||||
OptionalText(Option<String>),
|
||||
DateTime(DateTime<Utc>),
|
||||
OptionalDateTime(Option<DateTime<Utc>>),
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Value::Int(n) => write!(f, "{}", n),
|
||||
Value::OptionalInt(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalInt(None) => write!(f, "NULL"),
|
||||
Value::BigInt(n) => write!(f, "{}", n),
|
||||
Value::OptionalBigInt(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalBigInt(None) => write!(f, "NULL"),
|
||||
Value::Float(n) => write!(f, "{}", n),
|
||||
Value::OptionalFloat(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalFloat(None) => write!(f, "NULL"),
|
||||
Value::Double(n) => write!(f, "{}", n),
|
||||
Value::OptionalDouble(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalDouble(None) => write!(f, "NULL"),
|
||||
Value::Bool(n) => write!(f, "{}", n),
|
||||
Value::OptionalBool(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalBool(None) => write!(f, "NULL"),
|
||||
Value::Text(s) => write!(f, "'{}'", s.replace("'", "''")),
|
||||
Value::OptionalText(Some(s)) => write!(f, "'{}'", s.replace("'", "''")),
|
||||
Value::OptionalText(None) => write!(f, "NULL"),
|
||||
Value::DateTime(n) => write!(f, "{}", n),
|
||||
Value::OptionalDateTime(Some(n)) => write!(f, "{}", n),
|
||||
Value::OptionalDateTime(None) => write!(f, "NULL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
110
crates/siren-core/src/data/query.rs
Normal file
110
crates/siren-core/src/data/query.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use crate::data::{Value, condition::Condition, executable_query::ExecutableQuery};
|
||||
|
||||
pub struct QueryBuilder<'a> {
|
||||
table: &'a str,
|
||||
columns: Vec<&'a str>,
|
||||
distinct_on: Option<Vec<String>>,
|
||||
condition: Option<Condition>,
|
||||
order_by: Vec<String>,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a> QueryBuilder<'a> {
|
||||
pub fn new(table: &'a str) -> Self {
|
||||
QueryBuilder {
|
||||
table,
|
||||
columns: Vec::new(),
|
||||
distinct_on: None,
|
||||
condition: None,
|
||||
order_by: Vec::new(),
|
||||
limit: None,
|
||||
offset: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select(mut self, columns: &[&'a str]) -> Self {
|
||||
self.columns.extend(columns);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn distinct_on(mut self, columns: &[&str]) -> Self {
|
||||
self.distinct_on = Some(columns.iter().map(|s| s.to_string()).collect());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn where_condition(mut self, condition: Condition) -> Self {
|
||||
self.condition = Some(condition);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn order_by(mut self, column: &str, direction: Option<OrderDirection>) -> Self {
|
||||
match direction {
|
||||
Some(order) => self
|
||||
.order_by
|
||||
.push(format!("{} {}", column, order.to_string())),
|
||||
None => self.order_by.push(column.to_string()),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn limit(mut self, limit: usize) -> Self {
|
||||
self.limit = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub enum OrderDirection {
|
||||
Asc,
|
||||
Desc,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for OrderDirection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let direction_str = match self {
|
||||
OrderDirection::Asc => "ASC",
|
||||
OrderDirection::Desc => "DESC",
|
||||
};
|
||||
write!(f, "{}", direction_str)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ExecutableQuery for QueryBuilder<'a> {
|
||||
fn build(&self) -> (String, Vec<Value>) {
|
||||
let columns = if self.columns.is_empty() {
|
||||
"*".to_string()
|
||||
} else {
|
||||
self.columns.join(",")
|
||||
};
|
||||
|
||||
let mut query = if let Some(distinct_columns) = &self.distinct_on {
|
||||
let distinct_on_clause = distinct_columns.join(",");
|
||||
format!("SELECT DISTINCT ON ({}) {}", distinct_on_clause, columns)
|
||||
} else {
|
||||
format!("SELECT {}", columns)
|
||||
};
|
||||
|
||||
query.push_str(format!(" FROM {}", self.table).as_str());
|
||||
|
||||
let mut values: Vec<Value> = Vec::new();
|
||||
if let Some(condition) = &self.condition {
|
||||
let where_condition = condition.to_sql(&mut 0);
|
||||
query.push_str(&format!(" WHERE {}", where_condition.0));
|
||||
values = where_condition.1;
|
||||
}
|
||||
|
||||
if !self.order_by.is_empty() {
|
||||
query.push_str(&format!(" ORDER BY {}", self.order_by.join(" ORDER BY")));
|
||||
}
|
||||
|
||||
if let Some(limit) = self.limit {
|
||||
query.push_str(&format!(" LIMIT {}", limit));
|
||||
}
|
||||
|
||||
if let Some(offset) = self.offset {
|
||||
query.push_str(&format!(" OFFSET {}", offset));
|
||||
}
|
||||
|
||||
(query, values)
|
||||
}
|
||||
}
|
||||
60
crates/siren-core/src/data/update.rs
Normal file
60
crates/siren-core/src/data/update.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use crate::data::{Value, condition::Condition, executable_query::ExecutableQuery};
|
||||
|
||||
pub struct UpdateBuilder {
|
||||
table: String,
|
||||
columns: Vec<String>,
|
||||
values: Vec<Value>,
|
||||
condition: Option<Condition>,
|
||||
}
|
||||
|
||||
impl UpdateBuilder {
|
||||
pub fn new(table: &str) -> Self {
|
||||
Self {
|
||||
table: table.to_string(),
|
||||
columns: Vec::new(),
|
||||
values: Vec::new(),
|
||||
condition: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn column(mut self, column: &str, value: Value) -> Self {
|
||||
self.columns.push(column.to_string());
|
||||
self.values.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn where_condition(mut self, condition: Condition) -> Self {
|
||||
self.condition = Some(condition);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecutableQuery for UpdateBuilder {
|
||||
fn build(&self) -> (String, Vec<Value>) {
|
||||
if self.columns.is_empty() {
|
||||
panic!("Cannot build update query without columns to set");
|
||||
}
|
||||
|
||||
// Generate the SET clause
|
||||
let set_clause = self
|
||||
.columns
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, col)| format!("{} = ${}", col, i + 1))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
|
||||
let mut query = format!("UPDATE {} SET {}", self.table, set_clause);
|
||||
let mut counter = self.values.len();
|
||||
let mut values: Vec<Value> = self.values.clone();
|
||||
|
||||
// Build where clause
|
||||
if let Some(condition) = &self.condition {
|
||||
let where_condition = condition.to_sql(&mut counter);
|
||||
query.push_str(&format!(" WHERE {}", where_condition.0));
|
||||
values.extend(where_condition.1);
|
||||
}
|
||||
|
||||
(query, values)
|
||||
}
|
||||
}
|
||||
108
crates/siren-core/src/error.rs
Normal file
108
crates/siren-core/src/error.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Error {
|
||||
pub status: u16,
|
||||
pub details: String,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(status: u16, details: String) -> Self {
|
||||
Self { status, details }
|
||||
}
|
||||
|
||||
pub fn not_found(details: String) -> Self {
|
||||
Self::new(404, details)
|
||||
}
|
||||
|
||||
pub fn internal_server_error(details: String) -> Self {
|
||||
Self::new(500, details)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.details.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn description(&self) -> &str {
|
||||
&self.details
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(error: std::io::Error) -> Self {
|
||||
Self::new(500, format!("IO error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::string::FromUtf8Error> for Error {
|
||||
fn from(error: std::string::FromUtf8Error) -> Self {
|
||||
Self::new(500, format!("UTF-8 error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::env::VarError> for Error {
|
||||
fn from(error: std::env::VarError) -> Self {
|
||||
Self::new(500, format!("Environment variable error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for Error {
|
||||
fn from(error: sqlx::Error) -> Self {
|
||||
match error {
|
||||
sqlx::Error::RowNotFound => Error::new(404, "Not found".to_string()),
|
||||
sqlx::Error::ColumnIndexOutOfBounds { .. } => Error::new(422, error.to_string()),
|
||||
sqlx::Error::ColumnNotFound { .. } => Error::new(422, error.to_string()),
|
||||
sqlx::Error::ColumnDecode { .. } => Error::new(422, error.to_string()),
|
||||
sqlx::Error::Decode(_) => Error::new(422, error.to_string()),
|
||||
sqlx::Error::PoolTimedOut => Error::new(503, error.to_string()),
|
||||
sqlx::Error::PoolClosed => Error::new(503, error.to_string()),
|
||||
sqlx::Error::Database(err) => {
|
||||
if let Some(code) = err.code() {
|
||||
match code.trim() {
|
||||
"23505" => return Error::new(409, err.to_string()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Error::new(500, err.to_string())
|
||||
}
|
||||
_ => Error::new(500, error.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::migrate::MigrateError> for Error {
|
||||
fn from(error: sqlx::migrate::MigrateError) -> Self {
|
||||
Error::new(500, error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for Error {
|
||||
fn from(error: redis::RedisError) -> Self {
|
||||
Self::new(500, format!("Redis error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Self::new(500, format!("HTTP client error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(error: serde_json::Error) -> Self {
|
||||
Self::new(500, format!("JSON error: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uuid::Error> for Error {
|
||||
fn from(error: uuid::Error) -> Self {
|
||||
Self::new(500, format!("UUID error: {}", error))
|
||||
}
|
||||
}
|
||||
4
crates/siren-core/src/lib.rs
Normal file
4
crates/siren-core/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod data;
|
||||
pub mod error;
|
||||
pub mod utils;
|
||||
2
crates/siren-core/src/utils/mod.rs
Normal file
2
crates/siren-core/src/utils/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod text_utils;
|
||||
pub use text_utils::*;
|
||||
62
crates/siren-core/src/utils/text_utils.rs
Normal file
62
crates/siren-core/src/utils/text_utils.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
pub fn a_or_an(word: &str) -> &'static str {
|
||||
let vowels = ['a', 'e', 'i', 'o', 'u'];
|
||||
let lowercase_word = word.to_lowercase();
|
||||
|
||||
// Special cases where the article should be "a"
|
||||
let special_cases_a = vec!["one"];
|
||||
if special_cases_a.contains(&lowercase_word.as_str()) {
|
||||
return "a";
|
||||
}
|
||||
|
||||
// Special cases where the article should be "an"
|
||||
let special_cases_an = vec!["hour"];
|
||||
if special_cases_an.contains(&lowercase_word.as_str()) {
|
||||
return "an";
|
||||
}
|
||||
|
||||
let first_char = lowercase_word.chars().next();
|
||||
|
||||
match first_char {
|
||||
// If the first character is a vowel, return "an"
|
||||
Some(c) if vowels.contains(&c) => "an",
|
||||
// Otherwise, return "a"
|
||||
_ => "a",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn number_to_words(n: i32) -> String {
|
||||
let ones = [
|
||||
"", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
|
||||
];
|
||||
let teens = [
|
||||
"ten",
|
||||
"eleven",
|
||||
"twelve",
|
||||
"thirteen",
|
||||
"fourteen",
|
||||
"fifteen",
|
||||
"sixteen",
|
||||
"seventeen",
|
||||
"eighteen",
|
||||
"nineteen",
|
||||
];
|
||||
let tens = [
|
||||
"", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
|
||||
];
|
||||
|
||||
if n < 10 {
|
||||
ones[n as usize].to_string()
|
||||
} else if n < 20 {
|
||||
teens[(n - 10) as usize].to_string()
|
||||
} else if n < 100 {
|
||||
let ten_part = tens[(n / 10) as usize];
|
||||
let one_part = ones[(n % 10) as usize];
|
||||
if n % 10 == 0 {
|
||||
ten_part.to_string() // e.g., 20 → "twenty"
|
||||
} else {
|
||||
format!("{}-{}", ten_part, one_part) // e.g., 42 → "forty-two"
|
||||
}
|
||||
} else {
|
||||
"Number out of range".to_string() // Handle numbers >= 100 (or extend the logic)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user