docs + locations

This commit is contained in:
JMARyA 2024-09-02 18:40:02 +02:00
parent e1618b40ef
commit 48f00d8f6f
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
17 changed files with 390 additions and 33 deletions

47
Cargo.lock generated
View file

@ -122,18 +122,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.81" version = "0.1.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -302,6 +302,7 @@ dependencies = [
"tokio", "tokio",
"toml", "toml",
"uuid", "uuid",
"walkdir",
] ]
[[package]] [[package]]
@ -525,7 +526,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustc_version 0.4.1", "rustc_version 0.4.1",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -558,7 +559,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -735,7 +736,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1016,9 +1017,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -1440,7 +1441,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1493,7 +1494,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"version_check", "version_check",
"yansi", "yansi",
] ]
@ -1575,7 +1576,7 @@ checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -1697,7 +1698,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rocket_http", "rocket_http",
"syn 2.0.76", "syn 2.0.77",
"unicode-xid", "unicode-xid",
"version_check", "version_check",
] ]
@ -1914,7 +1915,7 @@ checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2118,7 +2119,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2140,9 +2141,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.76" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2191,7 +2192,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2276,7 +2277,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2373,7 +2374,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]
[[package]] [[package]]
@ -2658,7 +2659,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2680,7 +2681,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2955,5 +2956,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.76", "syn 2.0.77",
] ]

View file

@ -19,3 +19,4 @@ toml = "0.8.8"
uuid = { version = "1.8.0", features = ["v4"] } uuid = { version = "1.8.0", features = ["v4"] }
mongod = { git = "https://git.hydrar.de/jmarya/mongod" } mongod = { git = "https://git.hydrar.de/jmarya/mongod" }
env_logger = "0.11.5" env_logger = "0.11.5"
walkdir = "2.5.0"

View file

@ -1,3 +1,67 @@
# Item # 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.

56
docs/Location.md Normal file
View file

@ -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
}
}
```

7
docs/README.md Normal file
View file

@ -0,0 +1,7 @@
# CDB Documentation
- Item
- Location
- Statistics
- Transaction
- Webhooks

View file

@ -1,2 +1,3 @@
# Statistics # Statistics
CDB can create various informational stats based on your dataset. CDB can create various informational stats based on your dataset.
#todo

View file

@ -1,3 +1,17 @@
# Transaction # 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. A Transaction represents an actual instance of an Item Variant.
This makes individual physical goods traceable.
## 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

View file

@ -1,2 +1,3 @@
# Webhooks # Webhooks
Certain events can trigger webhooks. This allows you to independently build your automation pipeline. Certain events can trigger webhooks. This allows you to independently build your automation pipeline.
#todo

32
schema/location.json Normal file
View file

@ -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"
]
}

44
src/json_store.rs Normal file
View file

@ -0,0 +1,44 @@
use std::{collections::HashMap, ops::Deref};
use serde::Deserialize;
pub struct JSONStore<T> {
documents: HashMap<String, T>,
}
impl<T: for<'a> Deserialize<'a>> JSONStore<T> {
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<T> Deref for JSONStore<T> {
type Target = HashMap<String, T>;
fn deref(&self) -> &Self::Target {
&self.documents
}
}

70
src/location.rs Normal file
View file

@ -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<String>,
/// Storage Conditions
pub conditions: Option<StorageConditions>,
}
#[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();
}
}

View file

@ -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::routes as route;
use rocket::{http::Method, launch}; use rocket::{http::Method, launch};
mod db; mod db;
mod item; mod item;
mod json_store;
mod location;
mod process; mod process;
mod routes; mod routes;
mod transaction; mod transaction;
@ -41,6 +48,11 @@ async fn rocket() -> _ {
.expect("error creating CORS options"); .expect("error creating CORS options");
let itemdb = db::ItemDB::new("./itemdb").await; let itemdb = db::ItemDB::new("./itemdb").await;
let locations: JSONStore<Location> = JSONStore::new("./locations");
for location in locations.deref() {
location.1.insert_overwrite().await.unwrap();
}
rocket::build() rocket::build()
.mount( .mount(
@ -60,5 +72,6 @@ async fn rocket() -> _ {
], ],
) )
.manage(itemdb) .manage(itemdb)
.manage(locations)
.attach(cors) .attach(cors)
} }

View file

@ -0,0 +1,26 @@
use mongod::ToAPI;
use rocket::{get, State};
use crate::{
json_store::JSONStore,
location::Location,
routes::{api_error, FallibleApiResponse},
};
#[get("/location/<id>")]
pub async fn location_info(
id: &str,
locations: &State<JSONStore<Location>>,
) -> 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<JSONStore<Location>>) -> FallibleApiResponse {
// todo : recursive location map
unimplemented!()
}

View file

@ -1,9 +1,11 @@
mod demand; mod demand;
mod error; mod error;
mod location;
mod supply; mod supply;
pub use demand::*; pub use demand::*;
pub use error::*; pub use error::*;
pub use location::*;
use mongod::Model; use mongod::Model;
use mongod::ToAPI; use mongod::ToAPI;
pub use supply::*; pub use supply::*;

View file

@ -16,6 +16,7 @@ pub struct SupplyForm {
variant: String, variant: String,
price: String, price: String,
origin: Option<String>, origin: Option<String>,
location: Option<String>,
} }
/// Route for supply action. Creates a new Transaction for the specified Item Variant. /// Route for supply action. Creates a new Transaction for the specified Item Variant.
@ -35,6 +36,7 @@ pub async fn supply_route(form: Json<SupplyForm>, itemdb: &State<ItemDB>) -> Fal
.try_into() .try_into()
.map_err(|()| api_error("Price malformed"))?, .map_err(|()| api_error("Price malformed"))?,
form.origin.as_deref(), form.origin.as_deref(),
form.location.as_deref(),
) )
.await; .await;

View file

@ -1,12 +1,13 @@
use mongod::{ use mongod::{
assert_reference_of,
derive::{Model, Referencable}, derive::{Model, Referencable},
Model, Validate, reference_of, Model, Referencable, Reference, Validate,
}; };
use mongodb::bson::doc; use mongodb::bson::doc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
use crate::item::Item; use crate::{item::Item, location::Location};
/// A Transaction of an Item Variant /// A Transaction of an Item Variant
#[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)] #[derive(Debug, Clone, Serialize, Deserialize, Model, Referencable)]
@ -21,6 +22,8 @@ pub struct Transaction {
pub price: Price, pub price: Price,
/// Origin of the Item /// Origin of the Item
pub origin: Option<String>, pub origin: Option<String>,
/// The location of the Item
pub location: Option<Reference>,
/// Info on consumption of the Item /// Info on consumption of the Item
pub consumed: Option<Consumed>, pub consumed: Option<Consumed>,
/// Timestamp of the Transaction /// Timestamp of the Transaction
@ -29,6 +32,10 @@ pub struct Transaction {
impl Validate for Transaction { impl Validate for Transaction {
async fn validate(&self) -> Result<(), String> { async fn validate(&self) -> Result<(), String> {
if let Some(location) = &self.location {
assert_reference_of!(location, Location);
}
Ok(()) Ok(())
} }
} }
@ -45,7 +52,13 @@ pub struct Consumed {
} }
impl Transaction { 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 { Self {
_id: uuid::Uuid::new_v4().to_string(), _id: uuid::Uuid::new_v4().to_string(),
item: item.to_string(), item: item.to_string(),
@ -53,6 +66,11 @@ impl Transaction {
price, price,
consumed: None, consumed: None,
origin: origin.map(std::string::ToString::to_string), 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(), timestamp: chrono::Utc::now().timestamp(),
} }
} }

View file

@ -142,8 +142,13 @@ impl Variant {
/// # Returns /// # Returns
/// ///
/// Returns a UUID string representing the transaction. /// Returns a UUID string representing the transaction.
pub async fn supply(&self, price: Price, origin: Option<&str>) -> String { pub async fn supply(
let t = Transaction::new(&self.item, &self.variant, price, origin); &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(); t.insert().await.unwrap();