diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..305ef84 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,40 @@ +use mongodb::{bson::doc, ClientSession}; + +struct InventoryCache {} + +impl InventoryCache { + pub async fn push(uuid: &str, ses: &mut ClientSession) { + // todo : if not exists? + let update = doc! { "$push": { "transactions": uuid } }; + + let options = mongodb::options::FindOneAndUpdateOptions::builder() + .upsert(true) + .return_document(mongodb::options::ReturnDocument::After) + .build(); + + let result = ses + .client() + .database("cdb") + .collection::("cache") + .find_one_and_update_with_session(doc! { "_id": "inventory"}, update, options, ses) + .await + .unwrap(); + } + + pub async fn remove(uuid: &str, ses: &mut ClientSession) { + let update = doc! { "$pull": { "transactions": uuid}}; + + let options = mongodb::options::FindOneAndUpdateOptions::builder() + .upsert(true) + .return_document(mongodb::options::ReturnDocument::After) + .build(); + + let result = ses + .client() + .database("cdb") + .collection::("cache") + .find_one_and_update_with_session(doc! { "_id": "inventory"}, update, options, ses) + .await + .unwrap(); + } +} diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..ba94835 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,73 @@ +use crate::item::{Item, ItemEntry}; + +#[macro_export] +macro_rules! collect_results { + ($res:expr) => {{ + let mut ret = vec![]; + + while let Some(doc) = $res.try_next().await.unwrap() { + ret.push(doc); + } + + ret + }}; +} + +#[macro_export] +macro_rules! cdb_col { + ($db:expr, $col:expr) => { + $db.database("cdb") + .collection::($col) + }; +} + +#[macro_export] +macro_rules! get_mongo { + () => { + mongodb::Client::with_uri_str(std::env::var("DB_URI").unwrap()) + .await + .unwrap() + }; +} + +pub struct ItemDB { + index: mdq::Index, + mongodb: mongodb::Client, +} + +impl ItemDB { + pub async fn new(dir: &str, mongodb: &str) -> Self { + // scan for markdown item entries + let index = mdq::Index::new(dir, true); + let mongodb = get_mongo!(); + + for item in &index.documents { + let item = ItemEntry::new(item.clone()); + item.init_db(&mongodb).await; + } + + Self { index, mongodb } + } + + /// Retrieves an item by name + pub fn get_item(&self, item: &str) -> Option { + Some(Item::new( + self.mongodb.clone(), + self.index + .documents + .iter() + .map(|x| ItemEntry::new(x.clone())) + .find(|x| x.name == item)?, + )) + } + + /// Get all items + pub fn items(&self) -> Vec { + let mut ret = vec![]; + for item in &self.index.documents { + let item = ItemEntry::new(item.clone()); + ret.push(item.name); + } + ret + } +} diff --git a/src/item.rs b/src/item.rs index 0a85175..31015c8 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,8 +1,11 @@ use std::collections::HashSet; +use crate::collect_results; use futures::TryStreamExt; use mongodb::{bson::doc, ClientSession, Collection}; +use crate::variant::Variant; + // todo : api key auth // ITEM @@ -13,33 +16,6 @@ use mongodb::{bson::doc, ClientSession, Collection}; // DEMAND STATS // SEASONAL REVIEWS -macro_rules! collect_results { - ($res:expr) => {{ - let mut ret = vec![]; - - while let Some(doc) = $res.try_next().await.unwrap() { - ret.push(doc); - } - - ret - }}; -} - -macro_rules! cdb_col { - ($db:expr, $col:expr) => { - $db.database("cdb") - .collection::($col) - }; -} - -macro_rules! get_mongo { - () => { - mongodb::Client::with_uri_str(std::env::var("DB_URI").unwrap()) - .await - .unwrap() - }; -} - #[derive(serde::Deserialize, serde::Serialize)] pub struct ItemVariantEntry { pub item_name: String, @@ -202,336 +178,3 @@ impl Item { .unwrap(); } } - -#[derive(Debug, Clone)] -pub struct Price { - pub value: f64, - pub currency: String, -} - -impl Price { - pub fn new(value: f64, currency: &str) -> Self { - Self { - value, - currency: currency.to_string(), - } - } - - fn parse(price: &str) -> Option { - let (value, currency) = price.split_once(' ')?; - - Some(Self { - value: value.parse().ok()?, - currency: currency.to_string(), - }) - } - - fn as_bson(&self) -> mongodb::bson::Bson { - mongodb::bson::bson!({ - "value": self.value, - "currency": self.currency.clone() - }) - } -} - -impl From for Price { - fn from(value: mongodb::bson::Document) -> Self { - Self { - value: value.get_f64("value").unwrap(), - currency: value.get_str("currency").unwrap().to_string(), - } - } -} - -impl TryFrom for Price { - type Error = (); - - fn try_from(value: String) -> Result { - Self::parse(&value).ok_or(()) - } -} - -/// Represents a specific instance of an item with potential variations. -/// -/// This struct is used to describe a particular variation or instance of an item -/// in the real world. It may include attributes or properties that deviate from -/// the standard definition of the item. For example, different colors, sizes, or -/// configurations. -pub struct Variant { - pub item: String, - pub variant: String, -} - -pub struct BatchTransaction { - pub uuid: String, - pub transactions: Vec, -} - -impl BatchTransaction { - pub fn new(transactions: Vec) -> Self { - Self { - uuid: uuid::Uuid::new_v4().to_string(), - transactions, - } - } - - fn as_doc(&self) -> mongodb::bson::Document { - mongodb::bson::doc! { - "_id": &self.uuid, - "kind": "batch", - "transactions": &self.transactions - } - } - - fn from(b: mongodb::bson::Document) -> Self { - let uuid = b.get_str("_id").unwrap().to_string(); - let transactions = b - .get_array("transactions") - .unwrap() - .into_iter() - .map(|x| x.as_str().unwrap().to_string()) - .collect(); - Self { uuid, transactions } - } -} - -pub enum TransactionType { - Batch(BatchTransaction), - Normal(Transaction), -} - -impl TransactionType { - pub fn from(b: mongodb::bson::Document) -> Self { - if let Ok(kind) = b.get_str("kind") { - if kind == "batch" { - return Self::Batch(BatchTransaction::from(b)); - } - } - Self::Normal(Transaction::from(b)) - } -} - -pub struct Transaction { - pub uuid: String, - pub item: String, - pub variant: String, - pub price: Price, - pub origin: String, - pub timestamp: i64, -} - -impl Transaction { - pub fn new(item: &str, variant: &str, price: Price, origin: &str) -> Self { - Self { - uuid: uuid::Uuid::new_v4().to_string(), - item: item.to_string(), - variant: variant.to_string(), - price, - origin: origin.to_string(), - timestamp: chrono::Utc::now().timestamp(), - } - } - - fn as_doc(&self) -> mongodb::bson::Document { - mongodb::bson::doc! { - "_id": &self.uuid, - "item": &self.item, - "variant": &self.variant, - "price": self.price.as_bson(), - "origin": &self.origin, - "timestamp": self.timestamp - } - } - - fn from(b: mongodb::bson::Document) -> Self { - let uuid = b.get_str("oid").unwrap().to_string(); - let item = b.get_str("item").unwrap().to_string(); - let variant = b.get_str("variant").unwrap().to_string(); - let origin = b.get_str("origin").unwrap().to_string(); - let timestamp = b.get_i64("timestamp").unwrap(); - let price = Price::from(b); - Self { - uuid, - item, - variant, - price, - origin, - timestamp, - } - } -} - -impl Variant { - pub async fn demand(&self, uuid: &str, price: Price, destination: String) -> Option { - let db = get_mongo!(); - - // check if supply transaction exists - let supply_t = cdb_col!(db, "supply"); - if supply_t - .find_one(doc! { "_id": uuid }, None) - .await - .unwrap() - .is_none() - { - return None; - } - - // todo : demand batch - - // mark as used - let demand_t = cdb_col!(db, "demand"); - demand_t - .insert_one( - doc! { - "_id": uuid, - "destination": destination, - "price": price.as_bson() - }, - None, - ) - .await - .unwrap(); - - Some(uuid.to_string()) - } - - /// Records a supply transaction in the database. - /// - /// # Arguments - /// - /// * `amount` - The quantity of items supplied. - /// * `price` - The price of the supplied items. - /// * `origin` - The origin or source of the supplied items. - /// - /// # Returns - /// - /// Returns a UUID string representing the transaction. - pub async fn supply(&self, amount: usize, price: Price, origin: &str) -> String { - let db = get_mongo!(); - - let mut ses = db.start_session(None).await.unwrap(); - - let col: mongodb::Collection = - ses.client().database("cdb").collection("supply"); - - let mut transactions = vec![]; - for _ in 0..amount { - transactions.push(Transaction::new( - &self.item, - &self.variant, - price.clone(), - origin, - )); - } - - for transaction in &transactions { - let r = col - .insert_one_with_session(transaction.as_doc(), None, &mut ses) - .await - .unwrap(); - } - - // batch transaction - let ret_uuid = if amount == 1 { - transactions.first().unwrap().uuid.clone() - } else { - let batch = - BatchTransaction::new(transactions.iter().map(|x| x.uuid.clone()).collect()); - let col: mongodb::Collection = ses - .client() - .database("cdb") - .collection("transactions_batch"); - col.insert_one_with_session(batch.as_doc(), None, &mut ses) - .await - .unwrap(); - batch.uuid - }; - - // todo : transaction overlap cache -> scale - - ses.commit_transaction().await.unwrap(); - - ret_uuid - } -} - -struct InventoryCache {} - -impl InventoryCache { - pub async fn push(uuid: &str, ses: &mut ClientSession) { - // todo : if not exists? - let update = doc! { "$push": { "transactions": uuid } }; - - let options = mongodb::options::FindOneAndUpdateOptions::builder() - .upsert(true) - .return_document(mongodb::options::ReturnDocument::After) - .build(); - - let result = ses - .client() - .database("cdb") - .collection::("cache") - .find_one_and_update_with_session(doc! { "_id": "inventory"}, update, options, ses) - .await - .unwrap(); - } - - pub async fn remove(uuid: &str, ses: &mut ClientSession) { - let update = doc! { "$pull": { "transactions": uuid}}; - - let options = mongodb::options::FindOneAndUpdateOptions::builder() - .upsert(true) - .return_document(mongodb::options::ReturnDocument::After) - .build(); - - let result = ses - .client() - .database("cdb") - .collection::("cache") - .find_one_and_update_with_session(doc! { "_id": "inventory"}, update, options, ses) - .await - .unwrap(); - } -} - -pub struct ItemDB { - index: mdq::Index, - mongodb: mongodb::Client, -} - -impl ItemDB { - pub async fn new(dir: &str, mongodb: &str) -> Self { - // scan for markdown item entries - let index = mdq::Index::new(dir, true); - let mongodb = get_mongo!(); - - for item in &index.documents { - let item = ItemEntry::new(item.clone()); - item.init_db(&mongodb).await; - } - - Self { index, mongodb } - } - - /// Retrieves an item by name - pub fn get_item(&self, item: &str) -> Option { - Some(Item::new( - self.mongodb.clone(), - self.index - .documents - .iter() - .map(|x| ItemEntry::new(x.clone())) - .find(|x| x.name == item)?, - )) - } - - /// Get all items - pub fn items(&self) -> Vec { - let mut ret = vec![]; - for item in &self.index.documents { - let item = ItemEntry::new(item.clone()); - ret.push(item.name); - } - ret - } -} diff --git a/src/main.rs b/src/main.rs index 69e90e6..9ec2c23 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,12 @@ use actix_web::{get, HttpRequest, Responder}; use maud::html; +mod cache; +mod db; mod item; mod routes; +mod transaction; +mod variant; // ░░░░░░░░░░▀▀▀██████▄▄▄░░░░░░░░░░ // ░░░░░░░░░░░░░░░░░▀▀▀████▄░░░░░░░ @@ -21,7 +25,7 @@ mod routes; #[get("/")] pub(crate) async fn index(r: HttpRequest) -> impl Responder { - let itemdb: &actix_web::web::Data = r.app_data().unwrap(); + let itemdb: &actix_web::web::Data = r.app_data().unwrap(); let content = ""; web_base::func::build_site(&r, "Index", &content) } @@ -30,7 +34,7 @@ pub(crate) async fn index(r: HttpRequest) -> impl Responder { async fn main() -> std::io::Result<()> { env_logger::init(); - let itemdb = item::ItemDB::new("./itemdb", "mongodb://user:pass@127.0.0.1:27017").await; + let itemdb = db::ItemDB::new("./itemdb", "mongodb://user:pass@127.0.0.1:27017").await; let itemdb = actix_web::web::Data::new(itemdb); web_base::map!(web_base::Site::new(), |app: actix_web::App<_>| { diff --git a/src/routes/item/mod.rs b/src/routes/item/mod.rs index 584a405..1ce731d 100644 --- a/src/routes/item/mod.rs +++ b/src/routes/item/mod.rs @@ -7,7 +7,7 @@ use crate::routes::bad_req; macro_rules! get_itemdb { ($req:expr) => {{ - let itemdb: &actix_web::web::Data = $req.app_data().unwrap(); + let itemdb: &actix_web::web::Data = $req.app_data().unwrap(); itemdb }}; } diff --git a/src/transaction.rs b/src/transaction.rs new file mode 100644 index 0000000..5b92a31 --- /dev/null +++ b/src/transaction.rs @@ -0,0 +1,146 @@ +pub enum TransactionType { + Batch(BatchTransaction), + Normal(Transaction), +} + +impl TransactionType { + pub fn from(b: mongodb::bson::Document) -> Self { + if let Ok(kind) = b.get_str("kind") { + if kind == "batch" { + return Self::Batch(BatchTransaction::from(b)); + } + } + Self::Normal(Transaction::from(b)) + } +} + +pub struct Transaction { + pub uuid: String, + pub item: String, + pub variant: String, + pub price: Price, + pub origin: String, + pub timestamp: i64, +} + +impl Transaction { + pub fn new(item: &str, variant: &str, price: Price, origin: &str) -> Self { + Self { + uuid: uuid::Uuid::new_v4().to_string(), + item: item.to_string(), + variant: variant.to_string(), + price, + origin: origin.to_string(), + timestamp: chrono::Utc::now().timestamp(), + } + } + + pub fn as_doc(&self) -> mongodb::bson::Document { + mongodb::bson::doc! { + "_id": &self.uuid, + "item": &self.item, + "variant": &self.variant, + "price": self.price.as_bson(), + "origin": &self.origin, + "timestamp": self.timestamp + } + } + + fn from(b: mongodb::bson::Document) -> Self { + let uuid = b.get_str("oid").unwrap().to_string(); + let item = b.get_str("item").unwrap().to_string(); + let variant = b.get_str("variant").unwrap().to_string(); + let origin = b.get_str("origin").unwrap().to_string(); + let timestamp = b.get_i64("timestamp").unwrap(); + let price = Price::from(b); + Self { + uuid, + item, + variant, + price, + origin, + timestamp, + } + } +} + +pub struct BatchTransaction { + pub uuid: String, + pub transactions: Vec, +} + +impl BatchTransaction { + pub fn new(transactions: Vec) -> Self { + Self { + uuid: uuid::Uuid::new_v4().to_string(), + transactions, + } + } + + pub fn as_doc(&self) -> mongodb::bson::Document { + mongodb::bson::doc! { + "_id": &self.uuid, + "kind": "batch", + "transactions": &self.transactions + } + } + + fn from(b: mongodb::bson::Document) -> Self { + let uuid = b.get_str("_id").unwrap().to_string(); + let transactions = b + .get_array("transactions") + .unwrap() + .into_iter() + .map(|x| x.as_str().unwrap().to_string()) + .collect(); + Self { uuid, transactions } + } +} + +#[derive(Debug, Clone)] +pub struct Price { + pub value: f64, + pub currency: String, +} + +impl Price { + pub fn new(value: f64, currency: &str) -> Self { + Self { + value, + currency: currency.to_string(), + } + } + + fn parse(price: &str) -> Option { + let (value, currency) = price.split_once(' ')?; + + Some(Self { + value: value.parse().ok()?, + currency: currency.to_string(), + }) + } + + pub fn as_bson(&self) -> mongodb::bson::Bson { + mongodb::bson::bson!({ + "value": self.value, + "currency": self.currency.clone() + }) + } +} + +impl From for Price { + fn from(value: mongodb::bson::Document) -> Self { + Self { + value: value.get_f64("value").unwrap(), + currency: value.get_str("currency").unwrap().to_string(), + } + } +} + +impl TryFrom for Price { + type Error = (); + + fn try_from(value: String) -> Result { + Self::parse(&value).ok_or(()) + } +} diff --git a/src/variant.rs b/src/variant.rs new file mode 100644 index 0000000..f3be34f --- /dev/null +++ b/src/variant.rs @@ -0,0 +1,111 @@ +use mongodb::bson::doc; + +use crate::{ + cdb_col, get_mongo, + transaction::{BatchTransaction, Price, Transaction}, +}; + +/// Represents a specific instance of an item with potential variations. +/// +/// This struct is used to describe a particular variation or instance of an item +/// in the real world. It may include attributes or properties that deviate from +/// the standard definition of the item. For example, different colors, sizes, or +/// configurations. +pub struct Variant { + pub item: String, + pub variant: String, +} + +impl Variant { + pub async fn demand(&self, uuid: &str, price: Price, destination: String) -> Option { + let db = get_mongo!(); + + // check if supply transaction exists + let supply_t = cdb_col!(db, "supply"); + if supply_t + .find_one(doc! { "_id": uuid }, None) + .await + .unwrap() + .is_none() + { + return None; + } + + // todo : demand batch + + // mark as used + let demand_t = cdb_col!(db, "demand"); + demand_t + .insert_one( + doc! { + "_id": uuid, + "destination": destination, + "price": price.as_bson() + }, + None, + ) + .await + .unwrap(); + + Some(uuid.to_string()) + } + + /// Records a supply transaction in the database. + /// + /// # Arguments + /// + /// * `amount` - The quantity of items supplied. + /// * `price` - The price of the supplied items. + /// * `origin` - The origin or source of the supplied items. + /// + /// # Returns + /// + /// Returns a UUID string representing the transaction. + pub async fn supply(&self, amount: usize, price: Price, origin: &str) -> String { + let db = get_mongo!(); + + let mut ses = db.start_session(None).await.unwrap(); + + let col: mongodb::Collection = + ses.client().database("cdb").collection("supply"); + + let mut transactions = vec![]; + for _ in 0..amount { + transactions.push(Transaction::new( + &self.item, + &self.variant, + price.clone(), + origin, + )); + } + + for transaction in &transactions { + let r = col + .insert_one_with_session(transaction.as_doc(), None, &mut ses) + .await + .unwrap(); + } + + // batch transaction + let ret_uuid = if amount == 1 { + transactions.first().unwrap().uuid.clone() + } else { + let batch = + BatchTransaction::new(transactions.iter().map(|x| x.uuid.clone()).collect()); + let col: mongodb::Collection = ses + .client() + .database("cdb") + .collection("transactions_batch"); + col.insert_one_with_session(batch.as_doc(), None, &mut ses) + .await + .unwrap(); + batch.uuid + }; + + // todo : transaction overlap cache -> scale + + ses.commit_transaction().await.unwrap(); + + ret_uuid + } +}