This commit is contained in:
JMARyA 2024-05-03 18:22:59 +02:00
parent 897d23f917
commit c752e09f1a
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
7 changed files with 380 additions and 363 deletions

40
src/cache.rs Normal file
View file

@ -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::<mongodb::bson::Document>("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::<mongodb::bson::Document>("cache")
.find_one_and_update_with_session(doc! { "_id": "inventory"}, update, options, ses)
.await
.unwrap();
}
}

73
src/db.rs Normal file
View file

@ -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::<mongodb::bson::Document>($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<Item> {
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<String> {
let mut ret = vec![];
for item in &self.index.documents {
let item = ItemEntry::new(item.clone());
ret.push(item.name);
}
ret
}
}

View file

@ -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::<mongodb::bson::Document>($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<Self> {
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<mongodb::bson::Document> 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<String> for Price {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
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<String>,
}
impl BatchTransaction {
pub fn new(transactions: Vec<String>) -> 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<String> {
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<mongodb::bson::Document> =
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<mongodb::bson::Document> = 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::<mongodb::bson::Document>("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::<mongodb::bson::Document>("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<Item> {
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<String> {
let mut ret = vec![];
for item in &self.index.documents {
let item = ItemEntry::new(item.clone());
ret.push(item.name);
}
ret
}
}

View file

@ -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<item::ItemDB> = r.app_data().unwrap();
let itemdb: &actix_web::web::Data<db::ItemDB> = 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<_>| {

View file

@ -7,7 +7,7 @@ use crate::routes::bad_req;
macro_rules! get_itemdb {
($req:expr) => {{
let itemdb: &actix_web::web::Data<item::ItemDB> = $req.app_data().unwrap();
let itemdb: &actix_web::web::Data<crate::db::ItemDB> = $req.app_data().unwrap();
itemdb
}};
}

146
src/transaction.rs Normal file
View file

@ -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<String>,
}
impl BatchTransaction {
pub fn new(transactions: Vec<String>) -> 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<Self> {
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<mongodb::bson::Document> 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<String> for Price {
type Error = ();
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(&value).ok_or(())
}
}

111
src/variant.rs Normal file
View file

@ -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<String> {
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<mongodb::bson::Document> =
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<mongodb::bson::Document> = 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
}
}