This commit is contained in:
JMARyA 2024-07-24 16:38:56 +02:00
parent 3b0e8c1866
commit ca364453a7
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
10 changed files with 341 additions and 604 deletions

497
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,3 +17,4 @@ serde_yaml = "0.9.34"
tokio = { version = "1.35.1", features = ["full"] }
toml = "0.8.8"
uuid = { version = "1.8.0", features = ["v4"] }
mongod = { git = "https://git.hydrar.de/jmarya/mongod" }

View file

@ -1,59 +0,0 @@
use mongodb::bson::doc;
use crate::{get_mongo, id_of};
pub struct InventoryCache {}
impl InventoryCache {
pub async fn push(uuid: &str) {
let db = get_mongo!();
// 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();
db.database("cdb")
.collection::<mongodb::bson::Document>("cache")
.find_one_and_update(id_of!("inventory"), update, options)
.await
.unwrap();
}
pub async fn get() -> Vec<String> {
let db = get_mongo!();
let inventorycache = db
.database("cdb")
.collection::<mongodb::bson::Document>("cache")
.find_one(id_of!("inventory"), None)
.await
.unwrap()
.unwrap();
inventorycache
.get_array("transactions")
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect()
}
pub async fn remove(uuid: &str) {
let db = get_mongo!();
let update = doc! { "$pull": { "transactions": uuid}};
let options = mongodb::options::FindOneAndUpdateOptions::builder()
.upsert(true)
.return_document(mongodb::options::ReturnDocument::After)
.build();
db.database("cdb")
.collection::<mongodb::bson::Document>("cache")
.find_one_and_update(id_of!("inventory"), update, options)
.await
.unwrap();
}
}

View file

@ -1,47 +1,5 @@
use crate::item::Item;
/// Collect database results into a `Vec<_>`
#[macro_export]
macro_rules! collect_results {
($res:expr) => {{
use futures::stream::TryStreamExt;
let mut ret = vec![];
while let Some(doc) = $res.try_next().await.unwrap() {
ret.push(doc);
}
ret
}};
}
/// Get a database collection
#[macro_export]
macro_rules! cdb_col {
($db:expr, $col:expr) => {
$db.database("cdb")
.collection::<mongodb::bson::Document>($col)
};
}
/// Get a MongoDB Client from the environment
#[macro_export]
macro_rules! get_mongo {
() => {
mongodb::Client::with_uri_str(std::env::var("DB_URI").unwrap())
.await
.unwrap()
};
}
/// MongoDB filter for the `_id` field.
#[macro_export]
macro_rules! id_of {
($id:expr) => {
doc! { "_id": $id}
};
}
/// Item database
pub struct ItemDB {
index: mdq::Index,
@ -54,11 +12,12 @@ impl ItemDB {
pub async fn new(dir: &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 = Item::new(item);
item.init_db(&mongodb).await;
log::info!("Adding item {} to DB", item.name);
}
Self { index }

View file

@ -1,7 +1,8 @@
use std::collections::HashMap;
use crate::id_of;
use mongodb::{bson::doc, Collection};
use mongod::{derive::{Model, Referencable}, Validate};
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::variant::Variant;
@ -16,22 +17,15 @@ use crate::variant::Variant;
// DEMAND STATS
// SEASONAL REVIEWS
fn hashmap_to_bson_document(
hashmap: HashMap<String, mongodb::bson::Document>,
) -> mongodb::bson::Document {
let mut document = mongodb::bson::Document::new();
for (key, value) in hashmap {
document.insert(key, value);
}
document
}
/// Represents a single item in a collection of physical goods.
///
/// This struct serves as a blueprint for describing individual items within
/// a larger inventory or database of physical goods. It includes fields for
/// the name of the item and its category.
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
pub struct Item {
/// The ID is the same as the name
pub _id: String,
/// The name of the Item
pub name: String,
/// Category of the Item
@ -41,6 +35,12 @@ pub struct Item {
pub variants: HashMap<String, Variant>,
}
impl Validate for Item {
async fn validate(&self) -> Result<(), String> {
Ok(())
}
}
impl Item {
/// Creates a new `Item` from a parsed markdown document
pub fn new(doc: &mdq::Document) -> Self {
@ -77,39 +77,12 @@ impl Item {
}
Self {
_id: name.clone(),
name,
category,
variants,
}
}
/// Adds the `Item` to the database
pub async fn init_db(&self, mongodb: &mongodb::Client) {
log::info!("Adding item {} to DB", self.name);
let items: Collection<mongodb::bson::Document> =
mongodb.database("cdb").collection("items");
let doc = mongodb::bson::doc! {
"_id": self.name.clone(),
"name": self.name.clone(),
"category": self.category.clone(),
"variants": hashmap_to_bson_document(self.variants.iter().map(|x| (x.0.clone(), x.1.as_bson())).collect())
};
if items
.find_one(id_of!(&self.name), None)
.await
.unwrap()
.is_none()
{
items.insert_one(doc, None).await.unwrap();
} else {
items
.find_one_and_update(id_of!(&self.name), doc! { "$set": doc }, None)
.await
.unwrap();
}
}
}
impl Item {

View file

@ -1,7 +1,7 @@
use rocket::routes as route;
use rocket::{http::Method, launch};
mod cache;
mod db;
mod item;
mod routes;

View file

@ -4,6 +4,7 @@ mod supply;
pub use demand::*;
pub use error::*;
use mongod::Model;
pub use supply::*;
use rocket::get;
@ -23,6 +24,7 @@ pub fn get_items_route(itemdb: &State<ItemDB>) -> serde_json::Value {
json!({"items": items})
}
/// Return an API Response for an `Item`
#[get("/item/<item_id>")]
pub fn item_route(item_id: &str, itemdb: &State<ItemDB>) -> FallibleApiResponse {
let item = itemdb
@ -31,6 +33,7 @@ pub fn item_route(item_id: &str, itemdb: &State<ItemDB>) -> FallibleApiResponse
Ok(item.api_json())
}
/// Returns all variants of an Item
#[get("/item/<item_id>/variants")]
pub fn item_variants_page(item_id: &str, itemdb: &State<ItemDB>) -> FallibleApiResponse {
let item = itemdb
@ -44,6 +47,7 @@ pub fn item_variants_page(item_id: &str, itemdb: &State<ItemDB>) -> FallibleApiR
}))
}
/// Returns an API Response for a `Transaction`
#[get("/transaction/<transaction>")]
pub async fn transaction_route(transaction: &str) -> FallibleApiResponse {
let t = Transaction::get(transaction)

View file

@ -14,9 +14,8 @@ use super::{item_does_not_exist_error, variant_does_not_exist_error};
pub struct SupplyForm {
item: String,
variant: String,
amount: Option<usize>,
price: String,
origin: String,
origin: Option<String>,
}
#[post("/supply", data = "<form>")]
@ -30,12 +29,11 @@ pub async fn supply_route(form: Json<SupplyForm>, itemdb: &State<ItemDB>) -> Fal
let transaction_id = variant
.supply(
form.amount.unwrap_or(1),
form.price
.clone()
.try_into()
.map_err(|()| api_error("Price malformed"))?,
&form.origin,
form.origin.as_deref(),
)
.await;

View file

@ -1,56 +1,47 @@
use mongod::{derive::{Model, Referencable}, Validate};
use mongodb::bson::doc;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{cdb_col, get_mongo, id_of};
pub enum TransactionType {
Batch(BatchTransaction),
Normal(Transaction),
}
impl From<mongodb::bson::Document> for TransactionType {
fn from(value: mongodb::bson::Document) -> Self {
if let Ok(kind) = value.get_str("kind") {
if kind == "batch" {
return Self::Batch(value.into());
}
}
Self::Normal(value.into())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
pub struct Transaction {
pub uuid: String,
pub _id: String,
pub item: String,
pub variant: String,
pub price: Price,
pub origin: String,
pub origin: Option<String>,
pub consumed: Option<Consumed>,
pub timestamp: i64,
}
impl Validate for Transaction {
async fn validate(&self) -> Result<(), String> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Consumed {
pub destination: String,
pub price: Price,
}
impl Transaction {
pub fn new(item: &str, variant: &str, price: Price, origin: &str) -> Self {
pub fn new(item: &str, variant: &str, price: Price, origin: Option<&str>) -> Self {
Self {
uuid: uuid::Uuid::new_v4().to_string(),
_id: uuid::Uuid::new_v4().to_string(),
item: item.to_string(),
variant: variant.to_string(),
price,
origin: origin.to_string(),
consumed: None,
origin: origin.map(|x| x.to_string()),
timestamp: chrono::Utc::now().timestamp(),
}
}
pub async fn get(uuid: &str) -> Option<Self> {
let db = get_mongo!();
let transactions = cdb_col!(db, "transactions");
let doc = transactions.find_one(id_of!(uuid), None).await.ok()??;
Some(doc.into())
}
pub fn api_json(&self) -> serde_json::Value {
json!({
"uuid": self.uuid,
"uuid": self._id,
"item": self.item,
"variant": self.variant,
"price": self.price,
@ -58,90 +49,9 @@ impl Transaction {
"timestamp": self.timestamp
})
}
pub fn as_doc(&self) -> mongodb::bson::Document {
mongodb::bson::doc! {
"_id": &self.uuid,
"item": &self.item,
"variant": &self.variant,
"price": Into::<mongodb::bson::Bson>::into(self.price.clone()),
"origin": &self.origin,
"timestamp": self.timestamp
}
}
}
impl From<mongodb::bson::Document> for Transaction {
fn from(b: mongodb::bson::Document) -> Self {
let uuid = b.get_str("_id").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 = b.get_document("price").unwrap().clone().into();
Self {
uuid,
item,
variant,
price,
origin,
timestamp,
}
}
}
impl From<Transaction> for mongodb::bson::Document {
fn from(value: Transaction) -> Self {
mongodb::bson::doc! {
"_id": &value.uuid,
"item": &value.item,
"variant": &value.variant,
"price": Into::<mongodb::bson::Bson>::into(value.price),
"origin": &value.origin,
"timestamp": value.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,
}
}
}
impl From<mongodb::bson::Document> for BatchTransaction {
fn from(value: mongodb::bson::Document) -> Self {
let uuid = value.get_str("_id").unwrap().to_string();
let transactions = value
.get_array("transactions")
.unwrap()
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect();
Self { uuid, transactions }
}
}
impl From<BatchTransaction> for mongodb::bson::Document {
fn from(value: BatchTransaction) -> Self {
mongodb::bson::doc! {
"_id": value.uuid,
"kind": "batch",
"transactions": value.transactions
}
}
}
#[derive(Debug, Clone, Serialize)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Price {
pub value: f64,
pub currency: String,
@ -165,24 +75,6 @@ impl Price {
}
}
impl From<Price> for mongodb::bson::Bson {
fn from(value: Price) -> Self {
mongodb::bson::bson!({
"value": value.value,
"currency": value.currency
})
}
}
impl From<mongodb::bson::Document> for Price {
fn from(value: mongodb::bson::Document) -> Self {
Self {
value: value.get_f64("value").unwrap_or(0.0),
currency: value.get_str("currency").unwrap().to_string(),
}
}
}
impl TryFrom<String> for Price {
type Error = ();

View file

@ -1,11 +1,9 @@
use mongod::Model;
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{
cache::InventoryCache,
cdb_col, collect_results, get_mongo, id_of,
transaction::{BatchTransaction, Price, Transaction},
};
use crate::transaction::{Consumed, Price, Transaction};
/// Represents a specific instance of an item with potential variations.
///
@ -13,7 +11,7 @@ use crate::{
/// 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.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Variant {
/// Associated Item
pub item: String,
@ -52,133 +50,66 @@ impl Variant {
}
pub async fn supply_log(&self) -> Vec<String> {
let db = get_mongo!();
let supply = cdb_col!(db, "supply");
let filter = doc! {
"item": &self.item,
"variant": &self.variant
};
let mut db_res = supply.find(filter, None).await.unwrap();
let result: Vec<mongodb::bson::Document> = collect_results!(db_res);
let result = Transaction::find_partial(filter, json!({}), None).await.unwrap();
let mut ret = Vec::new();
for doc in result {
ret.push(doc.get("_id").unwrap().as_str().unwrap().to_string());
ret.push(doc._id);
}
ret
}
pub async fn demand_log(&self) -> Vec<String> {
// todo : add referenced supply
let db = get_mongo!();
let demand = cdb_col!(db, "demand");
let filter = doc! {
"item": &self.item,
"variant": &self.variant
"variant": &self.variant,
"consumed": { "$exists": true }
};
let mut db_res = demand.find(filter, None).await.unwrap();
let result: Vec<mongodb::bson::Document> = collect_results!(db_res);
let result = Transaction::find_partial(filter, json!({}), None).await.unwrap();
let mut ret = Vec::new();
for doc in result {
ret.push(doc.get("_id").unwrap().as_str().unwrap().to_string());
ret.push(doc._id);
}
ret
}
pub async fn demand(uuid: &str, price: Price, destination: &str) -> Option<String> {
let db = get_mongo!();
pub async fn demand(uuid: &str, price: Price, destination: &str) -> Option<()> {
// check if transaction exists
let mut t = Transaction::get(uuid).await?;
t.update(&json!({
"consumed": Consumed{ destination: destination.to_string(), price }
})).await.ok()?;
// check if supply transaction exists
let supply_t = cdb_col!(db, "supply");
supply_t.find_one(id_of!(uuid), None).await.unwrap()?;
// todo : demand batch
// mark as used
let demand_t = cdb_col!(db, "demand");
demand_t
.insert_one(
doc! {
"_id": uuid,
"destination": destination,
"price": Into::<mongodb::bson::Bson>::into(price)
},
None,
)
.await
.unwrap();
// update cache
InventoryCache::remove(uuid).await;
Some(uuid.to_string())
Some(())
}
/// 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!();
pub async fn supply(&self, price: Price, origin: Option<&str>) -> String {
let t = Transaction::new(&self.item, &self.variant, price, origin);
let col: mongodb::Collection<mongodb::bson::Document> =
db.database("cdb").collection("supply");
t.insert().await.unwrap();
let mut transactions = vec![];
for _ in 0..amount {
transactions.push(Transaction::new(
&self.item,
&self.variant,
price.clone(),
origin,
));
}
for transaction in &transactions {
col.insert_one(transaction.as_doc(), None).await.unwrap();
// update cache
InventoryCache::push(&transaction.uuid).await;
}
// 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> =
db.database("cdb").collection("transactions_batch");
let batch_uuid = batch.uuid.clone();
col.insert_one(Into::<mongodb::bson::Document>::into(batch), None)
.await
.unwrap();
batch_uuid
};
ret_uuid
t._id
}
pub fn api_json(&self) -> serde_json::Value {
@ -189,13 +120,4 @@ impl Variant {
"depends": self.depends
})
}
pub fn as_bson(&self) -> mongodb::bson::Document {
mongodb::bson::doc! {
"item": &self.item,
"variant": &self.variant,
"amount": self.amount as u32,
"depends": &self.depends
}
}
}