diff --git a/Cargo.lock b/Cargo.lock index cfb6e91..4259621 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,18 +122,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -302,6 +302,7 @@ dependencies = [ "tokio", "toml", "uuid", + "walkdir", ] [[package]] @@ -525,7 +526,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -558,7 +559,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -735,7 +736,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1016,9 +1017,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", "hashbrown", @@ -1440,7 +1441,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1493,7 +1494,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "version_check", "yansi", ] @@ -1575,7 +1576,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -1697,7 +1698,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.76", + "syn 2.0.77", "unicode-xid", "version_check", ] @@ -1914,7 +1915,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2118,7 +2119,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2140,9 +2141,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -2191,7 +2192,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2276,7 +2277,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2373,7 +2374,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2658,7 +2659,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-shared", ] @@ -2680,7 +2681,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2955,5 +2956,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] diff --git a/Cargo.toml b/Cargo.toml index 98309aa..4917619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ toml = "0.8.8" uuid = { version = "1.8.0", features = ["v4"] } mongod = { git = "https://git.hydrar.de/jmarya/mongod" } env_logger = "0.11.5" +walkdir = "2.5.0" diff --git a/docs/Item.md b/docs/Item.md index 542da9f..7a23c3c 100644 --- a/docs/Item.md +++ b/docs/Item.md @@ -1,3 +1,67 @@ # Item -An item is the base concept for CDB. Everything is an Item. An Item describes a general product or object. An item can have multiple variants, beeing the concrete instances of the Item like brand products or different flavors, etc. +An item is the base concept for CDB. Everything is an Item. An Item describes a general product or object. + +## Defining an Item +An Item is defined within a markdown file with frontmatter. Items are the root dataset of CDB. One can use [this](https://git.hydrar.de/red/itemdb) as a starting point or create their own items. + +For example, we define a "Water" Item: +```markdown +--- +name: "Water" +variants: + common: + name: "Common Water" +--- + +# Water +This is a Water Item +``` + +The file consist of the frontmatter, containing values for the item, and the rest of the markdown file containing a description. + +## Variants +Variants are different version of the same item. Each variant can have their own values. Each item needs at least one variant. + +For our water example: +```yml +name: "Water" +variant: + regular: + name: "Regular Water" + sparkling: + name: "Sparkling Water" + destilled: + name: "Destilled Water" +``` + +Here we have defined three "Water" item variants. + +## Inventory +With the items defined, you can start tracking their inventory. See [Transaction](Transaction.md). + +### Min +You can set a minimum required inventory for an item variant. This will trigger events when an item reaches a low inventory threshold. + +```yml +name: "Water" +variants: + regular: + name: "Regular Water" + min: 2 +``` + +This will ensure that at least two units of the "Regular Water" item variant are in inventory. + +### Expiry +You can set a default expiry time for an Item Variant. This value is defined as days until expiry. + +```yml +name: "Water" +variants: + regular: + name: "Regular Water" + expiry: 30 +``` + +This will mark any item variant as expired if it's older than 30 days. diff --git a/docs/Location.md b/docs/Location.md new file mode 100644 index 0000000..25ab3af --- /dev/null +++ b/docs/Location.md @@ -0,0 +1,56 @@ +# Location +A Location represents a physical location where items can be stored. + +## Defining locations +A Location is defined within a JSON file. What a Location represents is up to you. One location can be as specific as individual storage boxes or as general as a whole room. You can even define a hierarchy and additional properties. + +Basic Example: Storage Box +```json +{ + "name": "Storage Box" +} +``` + +### Hierarchy +You can add a hierarchy to your locations. Let's say for example we have one building we use for storage. + +So we create `storage_building1.json`: +```json +{ + "name": "Storage Building" +} +``` + +Then define a room for storage in `storage_room1.json`: +```json +{ + "name": "First storage room", + "parent": "storage_building1" +} +``` + +The `parent` field here defines that this location is inside another location, therefor setting up a hierarchy from larger scope locations to smaller scope. The `parent` field expects a location ID, which is typically the filename of the JSON without the extension. + +With this we can go even more detailed: +```json +{ + "name": "Black Storage Box", + "parent": "storage_room1" +} +``` + +### Properties +You can define various properties for a location. This allows for checking the best location for an item based on it's requirements. If locations are in a hierarchy they will inherit the properties of their parents by default. + +#### Conditions +Some items might be better stored under certain conditions. You can set the conditions of a location like temperature. + +Example: Freezer +```json +{ + "name": "Freezer", + "conditions": { + "temperature": -10 + } +} +``` diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3b4e969 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ +# CDB Documentation + +- Item +- Location +- Statistics +- Transaction +- Webhooks diff --git a/docs/Statistics.md b/docs/Statistics.md index 6cf69b3..a7be3c7 100644 --- a/docs/Statistics.md +++ b/docs/Statistics.md @@ -1,2 +1,3 @@ # Statistics -CDB can create various informational stats based on your dataset. \ No newline at end of file +CDB can create various informational stats based on your dataset. +#todo \ No newline at end of file diff --git a/docs/Transaction.md b/docs/Transaction.md index f6ce96e..1d04168 100644 --- a/docs/Transaction.md +++ b/docs/Transaction.md @@ -1,3 +1,17 @@ # Transaction -A Transaction represents an actual instance of an Item. This is a one to one mapping to a physical Variant of an Item. It can be consumed either manually or by a Process. -This makes individual physical goods traceable. +A Transaction represents an actual instance of an Item Variant. + +## Supply +When you obtain an item and add it to your inventory a Transaction gets created. This is one unit of the item. + +Transactions can carry some information: +- Price - The price this item was acquired at +- Origin - Where this item is from +- Location - The [Location](Location.md) of the item + +## Consume +If an item gets used or is removed from the inventory, you consume the transaction. + +This adds some additional information to it: +- Price - The price of the item at removal. e.g the selling price, or if negative cost of removal +- Destination - Where this item went, e.g a person who requested this item diff --git a/docs/Webhooks.md b/docs/Webhooks.md index edd734d..304b8d6 100644 --- a/docs/Webhooks.md +++ b/docs/Webhooks.md @@ -1,2 +1,3 @@ # Webhooks -Certain events can trigger webhooks. This allows you to independently build your automation pipeline. \ No newline at end of file +Certain events can trigger webhooks. This allows you to independently build your automation pipeline. +#todo \ No newline at end of file diff --git a/schema/location.json b/schema/location.json new file mode 100644 index 0000000..a9cca3a --- /dev/null +++ b/schema/location.json @@ -0,0 +1,32 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Location", + "description": "A storage location", + "properties": { + "name": { + "type": "string", + "title": "Location Name", + "description": "The name of the Location" + }, + "parent": { + "type": "string", + "title": "Parent Location", + "description": "The ID of a broader location containing this location." + }, + "conditions": { + "type": "object", + "title": "Storage Conditions", + "description": "The conditions of the storage location.", + "properties": { + "temperature": { + "type": "number", + "title": "Temperature", + "description": "The median temperature this storage location has." + } + } + } + }, + "required": [ + "name" + ] +} \ No newline at end of file diff --git a/src/json_store.rs b/src/json_store.rs new file mode 100644 index 0000000..1ca83f2 --- /dev/null +++ b/src/json_store.rs @@ -0,0 +1,44 @@ +use std::{collections::HashMap, ops::Deref}; + +use serde::Deserialize; + +pub struct JSONStore { + documents: HashMap, +} + +impl Deserialize<'a>> JSONStore { + pub fn new(dir: &str) -> Self { + let mut documents = HashMap::new(); + + for e in walkdir::WalkDir::new(dir) + .into_iter() + .filter_map(std::result::Result::ok) + { + if e.path().is_dir() { + continue; + } + if e.path().extension().is_none() { + continue; + } + + if e.path().extension().unwrap().to_str().unwrap() == "json" { + let path = e.path().to_str().unwrap().to_owned(); + let file_name = e.path().file_stem().unwrap().to_str().unwrap().to_string(); + let content = std::fs::read_to_string(path).unwrap(); + let json = serde_json::from_str(&content).unwrap(); + + documents.insert(file_name, json); + } + } + + Self { documents } + } +} + +impl Deref for JSONStore { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.documents + } +} diff --git a/src/location.rs b/src/location.rs new file mode 100644 index 0000000..677ca57 --- /dev/null +++ b/src/location.rs @@ -0,0 +1,70 @@ +use mongod::{ + derive::{Model, Referencable}, + Model, ToAPI, Validate, +}; +use mongodb::bson::doc; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// A Storage Location +#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)] +pub struct Location { + /// UUID + pub _id: String, + /// Name + pub name: String, + /// Parent + pub parent: Option, + /// Storage Conditions + pub conditions: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct StorageConditions { + /// Median temperature + pub temperature: i64, +} + +impl Validate for Location { + async fn validate(&self) -> Result<(), String> { + Ok(()) + } +} + +impl ToAPI for Location { + async fn api(&self) -> serde_json::Value { + json!({ + "id": self._id, + "name": self.name, + "parent": self.parent, + "conditions": self.conditions + }) + } +} + +impl Location { + pub async fn add(id: &str, o: serde_json::Value) { + let l = Location { + _id: id.to_string(), + name: o + .as_object() + .unwrap() + .get("name") + .unwrap() + .as_str() + .unwrap() + .to_string(), + parent: o + .as_object() + .unwrap() + .get("parent") + .map(|x| x.as_str().unwrap().to_string()), + conditions: serde_json::from_value( + o.as_object().unwrap().get("conditions").unwrap().clone(), + ) + .unwrap(), + }; + + l.insert_overwrite().await.unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs index 325f813..6b3249e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,15 @@ +use std::ops::Deref; + +use json_store::JSONStore; +use location::Location; +use mongod::Model; use rocket::routes as route; use rocket::{http::Method, launch}; mod db; mod item; +mod json_store; +mod location; mod process; mod routes; mod transaction; @@ -41,6 +48,11 @@ async fn rocket() -> _ { .expect("error creating CORS options"); let itemdb = db::ItemDB::new("./itemdb").await; + let locations: JSONStore = JSONStore::new("./locations"); + + for location in locations.deref() { + location.1.insert_overwrite().await.unwrap(); + } rocket::build() .mount( @@ -60,5 +72,6 @@ async fn rocket() -> _ { ], ) .manage(itemdb) + .manage(locations) .attach(cors) } diff --git a/src/routes/item/location.rs b/src/routes/item/location.rs new file mode 100644 index 0000000..29fc867 --- /dev/null +++ b/src/routes/item/location.rs @@ -0,0 +1,26 @@ +use mongod::ToAPI; +use rocket::{get, State}; + +use crate::{ + json_store::JSONStore, + location::Location, + routes::{api_error, FallibleApiResponse}, +}; + +#[get("/location/")] +pub async fn location_info( + id: &str, + locations: &State>, +) -> FallibleApiResponse { + let loc = locations + .get(id) + .ok_or_else(|| api_error("No location with that ID"))?; + Ok(loc.api().await) +} + +#[get("/locations")] +pub async fn locations_info(locations: &State>) -> FallibleApiResponse { + // todo : recursive location map + + unimplemented!() +} diff --git a/src/routes/item/mod.rs b/src/routes/item/mod.rs index 856653e..3e8cd24 100644 --- a/src/routes/item/mod.rs +++ b/src/routes/item/mod.rs @@ -1,9 +1,11 @@ mod demand; mod error; +mod location; mod supply; pub use demand::*; pub use error::*; +pub use location::*; use mongod::Model; use mongod::ToAPI; pub use supply::*; diff --git a/src/routes/item/supply.rs b/src/routes/item/supply.rs index 312471c..ae1a96f 100644 --- a/src/routes/item/supply.rs +++ b/src/routes/item/supply.rs @@ -16,6 +16,7 @@ pub struct SupplyForm { variant: String, price: String, origin: Option, + location: Option, } /// Route for supply action. Creates a new Transaction for the specified Item Variant. @@ -35,6 +36,7 @@ pub async fn supply_route(form: Json, itemdb: &State) -> Fal .try_into() .map_err(|()| api_error("Price malformed"))?, form.origin.as_deref(), + form.location.as_deref(), ) .await; diff --git a/src/transaction.rs b/src/transaction.rs index 6ec5f1d..e0dc75d 100644 --- a/src/transaction.rs +++ b/src/transaction.rs @@ -1,12 +1,13 @@ use mongod::{ + assert_reference_of, derive::{Model, Referencable}, - Model, Validate, + reference_of, Model, Referencable, Reference, Validate, }; use mongodb::bson::doc; use serde::{Deserialize, Serialize}; use serde_json::json; -use crate::item::Item; +use crate::{item::Item, location::Location}; /// A Transaction of an Item Variant #[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)] @@ -21,6 +22,8 @@ pub struct Transaction { pub price: Price, /// Origin of the Item pub origin: Option, + /// The location of the Item + pub location: Option, /// Info on consumption of the Item pub consumed: Option, /// Timestamp of the Transaction @@ -29,6 +32,10 @@ pub struct Transaction { impl Validate for Transaction { async fn validate(&self) -> Result<(), String> { + if let Some(location) = &self.location { + assert_reference_of!(location, Location); + } + Ok(()) } } @@ -45,7 +52,13 @@ pub struct Consumed { } impl Transaction { - pub fn new(item: &str, variant: &str, price: Price, origin: Option<&str>) -> Self { + pub async fn new( + item: &str, + variant: &str, + price: Price, + origin: Option<&str>, + location: Option<&str>, + ) -> Self { Self { _id: uuid::Uuid::new_v4().to_string(), item: item.to_string(), @@ -53,6 +66,11 @@ impl Transaction { price, consumed: None, origin: origin.map(std::string::ToString::to_string), + location: if let Some(location) = location { + reference_of!(Location, location) + } else { + None + }, timestamp: chrono::Utc::now().timestamp(), } } diff --git a/src/variant.rs b/src/variant.rs index a3cca01..e27265c 100644 --- a/src/variant.rs +++ b/src/variant.rs @@ -142,8 +142,13 @@ impl Variant { /// # Returns /// /// Returns a UUID string representing the transaction. - pub async fn supply(&self, price: Price, origin: Option<&str>) -> String { - let t = Transaction::new(&self.item, &self.variant, price, origin); + pub async fn supply( + &self, + price: Price, + origin: Option<&str>, + location: Option<&str>, + ) -> String { + let t = Transaction::new(&self.item, &self.variant, price, origin, location).await; t.insert().await.unwrap();