#[macro_use]
extern crate serde;
#[macro_use]
extern crate derive_builder;
use std::fmt;
use std::ops::Add;
use std::result::Result;
use std::time::{Duration, SystemTime};
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
pub struct ItemType {
#[builder(setter(skip))]
pub id: u32,
pub name: String,
#[builder(default)]
pub minimum_quantity: f32,
#[builder(default)]
pub ttl: Option<Duration>,
#[builder(default)]
pub opened_by_default: bool,
}
impl fmt::Display for ItemType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{};{};{};{};{}",
self.id,
self.name,
self.minimum_quantity,
self.ttl
.map(|ttl| humantime::format_duration(ttl).to_string())
.unwrap_or("".to_string()),
self.opened_by_default
)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Builder)]
pub struct ItemInstance {
#[builder(setter(skip))]
pub id: u32,
pub item_type: u32,
#[builder(default = "1.0")]
pub quantity: f32,
#[builder(default)]
pub model: Option<String>,
#[builder(default)]
pub serial: Option<String>,
#[builder(default)]
pub extra: Option<String>,
#[builder(default)]
pub location: Option<String>,
#[builder(default)]
pub value: Option<f32>,
#[builder(default)]
pub opened_at: Option<SystemTime>,
#[builder(default)]
pub expires_at: Option<SystemTime>,
#[builder(setter(skip))]
pub added_at: Option<SystemTime>,
#[builder(setter(skip))]
pub removed_at: Option<SystemTime>,
}
impl fmt::Display for ItemInstance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{};{};{};{};{};{};{};{};{};{}",
self.id,
self.item_type,
self.quantity,
conv(&self.model),
conv(&self.serial),
conv(&self.extra),
conv(&self.location),
conv(&self.value),
self.opened_at
.map(|t| humantime::format_rfc3339(t).to_string())
.unwrap_or("".to_string()),
self.expires_at
.map(|t| humantime::format_rfc3339(t).to_string())
.unwrap_or("".to_string())
)
}
}
pub fn conv<T: ToString>(s: &Option<T>) -> String {
s.as_ref().map(|m| m.to_string()).unwrap_or_default()
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum UseState {
New,
Used,
}
impl Default for UseState {
fn default() -> Self {
UseState::New
}
}
#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct Inventory {
pub item_types: Vec<ItemType>,
pub item_instances: Vec<ItemInstance>,
}
impl Inventory {
pub fn add_item_type(&mut self, mut item_type: ItemType) -> u32 {
let free_id = self.free_type_id();
item_type.id = free_id;
self.item_types.push(item_type);
free_id
}
pub fn add_item_instance(
&mut self,
mut item_instance: ItemInstance,
) -> Result<u32, InventoryError> {
let free_id = self.free_instance_id();
item_instance.id = free_id;
if let Some(it) = self
.item_types
.iter()
.find(|it| it.id == item_instance.item_type)
{
if it.opened_by_default {
item_instance.opened_at = Some(SystemTime::now());
if let Some(ttl) = it.ttl {
item_instance.expires_at = Some(SystemTime::now().add(ttl.clone()));
}
}
} else {
return Err(InventoryError::UnknownItemType);
}
item_instance.added_at = Some(SystemTime::now());
self.item_instances.push(item_instance);
Ok(free_id)
}
pub fn use_instance<'a>(&mut self, type_id: u32, quantity: Option<f32>) {
let mut remaining = 0.0;
let mut trash_id = 0;
let mut item_instances = self
.item_instances
.iter_mut()
.filter(|t| t.item_type == type_id && t.removed_at.is_none())
.collect::<Vec<_>>();
let mut target = item_instances.iter_mut().find(|ii| ii.opened_at.is_some());
if target.is_none() {
target = item_instances.first_mut();
}
if let Some(item_instance) = target {
if let Some(e) = quantity {
item_instance.quantity = item_instance.quantity - e;
if item_instance.quantity < 0.0 {
remaining = item_instance.quantity;
trash_id = item_instance.id;
item_instance.quantity = 0.0;
}
} else {
item_instance.quantity -= 1.0;
}
if item_instance.opened_at.is_none() {
item_instance.opened_at = Some(SystemTime::now());
let it = self
.item_types
.iter()
.find(|it| it.id == type_id)
.expect("No item type found with the specified id");
if let Some(ttl) = it.ttl {
let candidate_exp = SystemTime::now().add(ttl);
let new_exp = if let Some(old) = item_instance.expires_at {
if old < candidate_exp {
old
} else {
candidate_exp
}
} else {
candidate_exp
};
item_instance.expires_at = Some(new_exp);
}
}
} else {
eprintln!("Could not find an item instance with the specified type id to use (or all items were used.)");
}
if remaining < -0.0005 {
self.trash(trash_id);
self.use_instance(type_id, Some(-remaining));
}
}
pub fn trash<'a>(&mut self, instance_id: u32) {
if let Some(mut item_instance) =
self.item_instances.iter_mut().find(|t| t.id == instance_id)
{
item_instance.removed_at = Some(SystemTime::now());
} else {
eprintln!("Could not find an item instance with the specified id to trash");
}
}
pub fn delete_item_type(&mut self, id: u32) {
self.item_types.retain(|t| t.id != id);
self.item_instances.retain(|i| i.item_type != id);
}
pub fn delete_item_instance(&mut self, id: u32) -> Result<(), InventoryError> {
if let Some(inst) = self.item_instances.iter_mut().find(|inst| inst.id == id) {
inst.removed_at = Some(SystemTime::now());
Ok(())
} else {
Err(InventoryError::UnknownItemInstance)
}
}
pub fn get_instances_for_type(&self, id: u32) -> Result<Vec<&ItemInstance>, InventoryError> {
if !self.has_item_type(id) {
return Err(InventoryError::UnknownItemType);
}
Ok(self
.item_instances
.iter()
.filter(|inst| inst.item_type == id && inst.removed_at.is_none())
.collect::<Vec<_>>())
}
fn has_item_type(&self, id: u32) -> bool {
self.item_types.iter().any(|ty| ty.id == id)
}
fn free_type_id(&self) -> u32 {
self.item_types.iter().map(|it| it.id).max().unwrap_or(0) + 1
}
fn free_instance_id(&self) -> u32 {
self.item_instances
.iter()
.map(|ii| ii.id)
.max()
.unwrap_or(0)
+ 1
}
pub fn get_types_for_name(&self, name: &String) -> Vec<&ItemType> {
self.item_types
.iter()
.filter(|t| t.name.to_lowercase().contains(&name.to_lowercase()))
.collect::<Vec<_>>()
}
pub fn quantity_for_type(&self, type_id: u32) -> f32 {
self.item_instances
.iter()
.filter(|ii| ii.item_type == type_id && ii.removed_at.is_none())
.map(|ii| ii.quantity)
.fold(0.0, |accum, e| accum + e)
}
}
#[derive(Debug, Clone)]
pub enum InventoryError {
UnknownItemType,
UnknownItemInstance,
}