diff --git a/tools/src/recipe.rs b/tools/src/recipe.rs index a03d9f9..28f2108 100644 --- a/tools/src/recipe.rs +++ b/tools/src/recipe.rs @@ -8,6 +8,16 @@ pub enum RecipeLoadError { TomlDeserializeError(#[from] toml::de::Error), } +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum VitaminError { + #[error("not a vitamin")] + NotAVitamin, + #[error("vitamin has bogus inputs")] + InputsError, + #[error("vitamon has no output")] + OutputError, +} + #[derive(Debug, serde::Deserialize, PartialEq)] pub struct Input { // Quantity defaults to "amount=1" if omitted. @@ -85,6 +95,45 @@ impl Recipe { Ok(recipe) } + /// Compute the cost (of quantity 1). Currently only work on vitamins. + pub fn unit_cost(&self) -> Result { + if !self.is_vitamin() { + return Err(VitaminError::NotAVitamin); + } + + // Vitamins must have exactly one Input, and it must be Capital. + if self.inputs.len() != 1 { + return Err(VitaminError::InputsError); + } + + let capital = match self.inputs.get("capital") { + Some(capital) => capital, + None => return Err(VitaminError::InputsError), + }; + + if capital.quantity.unit != Some(crate::quantity::Unit::USDollar) { + return Err(VitaminError::InputsError); + } + + let total_cost = capital.quantity.amount; + + let outputs = match &self.outputs { + Some(outputs) => outputs, + None => return Err(VitaminError::OutputError), + }; + + // FIXME: For now Vitamins must produce exactly one output. + if outputs.len() != 1 { + return Err(VitaminError::OutputError); + } + + let (_output_name, output_info) = outputs.iter().next().unwrap(); + let output_quantity = output_info.quantity; + + // compute the "unit cost" of this input + return Ok(total_cost / output_quantity.amount); + } + // A "Vitamin" is a recipe whose only input is "capital". pub fn is_vitamin(&self) -> bool { if self.inputs.len() != 1 { diff --git a/tools/src/repos.rs b/tools/src/repos.rs index c79491d..5088973 100644 --- a/tools/src/repos.rs +++ b/tools/src/repos.rs @@ -52,6 +52,8 @@ pub enum RecipeCompileError { VitaminCapitalHasWrongUnit(String), #[error("input produces no output: {0}")] InputProducesNoOutput(String), + #[error(transparent)] + VitaminError(#[from] crate::recipe::VitaminError), } impl Repos { @@ -130,17 +132,8 @@ impl Repos { true => { let amount_needed = input_info.quantity * quantity; - // FIXME: for now Vitamins must have exactly one Input, and it must be Capital. - assert_eq!(input_recipe.inputs.len(), 1); - let input_capital = input_recipe.inputs.get("capital").ok_or_else(|| { - RecipeCompileError::VitaminLacksCapital(input_name.into()) - })?; - if input_capital.quantity.unit != Some(crate::quantity::Unit::USDollar) { - return Err(RecipeCompileError::VitaminCapitalHasWrongUnit( - input_name.into(), - )); - } - let cost = input_capital.quantity.amount; + let cost_each = input_recipe.unit_cost()?; + let total_cost = amount_needed.amount * cost_each; let outputs = match &input_recipe.outputs { None => { @@ -155,11 +148,6 @@ impl Repos { assert_eq!(outputs.len(), 1); let (_output_name, output_info) = outputs.iter().next().unwrap(); - let output_quantity = output_info.quantity; - - // compute the "unit cost" of this input - let cost_each = cost / output_quantity.amount; - let total_cost = amount_needed.amount * cost_each; // FIXME: Try to coerce the units here - if we buy // hose in rolls measured in feet, but we use lengths