#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)] pub enum Unit { Foot, Gram, Liter, Meter, USDollar, } /// `Quantity` measures the amount of a resource (Input or Output). #[derive(Clone, Copy, PartialEq, serde::Deserialize)] pub struct Quantity { pub amount: f32, pub unit: Option, } impl std::default::Default for Quantity { fn default() -> Self { Self { amount: 1.0, unit: None, } } } impl std::fmt::Debug for Quantity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.unit { None => write!(f, "{:#}", self.amount), Some(unit) => write!(f, "{:#} {:?}", self.amount, unit), } } } #[derive(Debug, serde::Deserialize)] pub struct Input { // Quantity defaults to "amount=1" if omitted. #[serde(default)] pub quantity: Quantity, } #[derive(Debug, PartialEq, serde::Deserialize)] pub struct Output { // Quantity defaults to "amount=1" if omitted. #[serde(default)] pub quantity: Quantity, } #[derive(Debug, serde::Deserialize)] pub struct Operator { pub skills: Vec, } #[derive(Debug, serde::Deserialize)] pub struct Dependencies { pub tools: Option>, pub operator: Option, } #[derive(Debug, serde::Deserialize)] pub struct Purchase { pub vendor: Vec, pub documentation: Option>, } #[derive(Debug, serde::Deserialize)] #[allow(non_camel_case_types)] pub enum Action { process(String), print, purchase(Purchase), } #[derive(Debug, serde::Deserialize)] pub struct Recipe { /// `[inputs]` is a Table where each key the the name (unique id) /// of a recipe in the repo, and the value is an Input object. pub inputs: std::collections::HashMap, pub dependencies: Dependencies, pub action: Action, /// If a recipe has no `[outputs]`, we assume it produces 1x of the /// thing identified by the name of the recipe. /// /// FIXME: Or is that always the case, and we should have no outputs /// section? None of the recipes we've been doodling around with /// have anything like byproducts or waste streams... pub outputs: Option>, } impl Recipe { pub fn from_file(file: &std::path::PathBuf) -> anyhow::Result { let recipe_contents = std::fs::read_to_string(file)?; let mut recipe: Recipe = toml::from_str(&recipe_contents)?; if recipe.outputs == None { if let Some(recipe_name) = file.file_stem() { let mut outputs = std::collections::HashMap::::new(); let key = recipe_name.to_string_lossy().into_owned(); let value = Output { quantity: Quantity::default(), }; outputs.insert(key, value); recipe.outputs = Some(outputs); } } // let r = recipe.validate_recipe(); Ok(recipe) } // A "Vitamin" is a recipe whose only input is "capital". pub fn is_vitamin(&self) -> bool { if self.inputs.len() != 1 { return false; } if let Some(input_name) = self.inputs.keys().into_iter().next() { if input_name == "capital" { return true; } } return false; } } impl Recipe { fn validate_recipe(self: &Self) -> anyhow::Result<()> { // if recipe.inputs.len() == 0 { // Err("recipe has no inputs!"); // } Ok(()) } } mod test { use super::*; #[test] fn is_vitamin() { let recipes = vec![ ( "../modular-recipes/recipes/peristaltic-pump/peristaltic_pump.toml", false, ), ( "../modular-recipes/recipes/peristaltic-pump/print/bearing_hub.toml", false, ), ("../modular-recipes/recipes/purchase/m4_nuts.toml", true), ("../modular-recipes/recipes/purchase/filament.toml", true), ]; for (recipe_filename, is_leaf) in recipes.iter() { println!("{recipe_filename}"); let recipe_path = std::path::PathBuf::from(recipe_filename); let recipe = Recipe::from_file(&recipe_path).unwrap(); let result = recipe.is_vitamin(); println!("recipe {:#?}, leaf={}", recipe_filename, result); assert_eq!(result, *is_leaf); } } }