use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::{
ast::Modifiers,
convert::Converter,
metadata::Metadata,
quantity::{GroupedQuantity, Quantity, QuantityAddError, QuantityValue},
scale::ScaleOutcome,
};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Recipe<D: Serialize = ()> {
pub name: String,
pub metadata: Metadata,
pub sections: Vec<Section>,
pub ingredients: Vec<Ingredient>,
pub cookware: Vec<Cookware>,
pub timers: Vec<Timer>,
pub inline_quantities: Vec<Quantity>,
#[serde(skip_deserializing)]
pub(crate) data: D,
}
pub type ScaledRecipe = Recipe<crate::scale::Scaled>;
impl Recipe {
pub(crate) fn from_content(name: String, content: crate::analysis::RecipeContent) -> Self {
Recipe {
name,
metadata: content.metadata,
sections: content.sections,
ingredients: content.ingredients,
cookware: content.cookware,
timers: content.timers,
inline_quantities: content.inline_quantities,
data: (),
}
}
}
impl ScaledRecipe {
pub fn ingredient_list(&self, converter: &Converter) -> IngredientList {
let mut list = Vec::new();
let data = self.scaled_data();
for (index, ingredient) in self
.ingredients
.iter()
.enumerate()
.filter(|(_, i)| !i.is_reference())
{
let mut grouped = GroupedQuantity::default();
for q in ingredient.all_quantities(&self.ingredients) {
grouped.add(q, converter);
}
let outcome: Option<ScaleOutcome> = data
.as_ref()
.map(|data| {
let mut outcome = &data.ingredients[index]; let all_indices = std::iter::once(index)
.chain(ingredient.relation.referenced_from().iter().copied());
for index in all_indices {
match &data.ingredients[index] {
e @ ScaleOutcome::Error(_) => return e, e @ ScaleOutcome::Fixed => outcome = e, _ => {}
}
}
outcome
})
.cloned();
list.push(IngredientListEntry {
index,
quantity: grouped,
outcome,
});
}
list
}
}
pub type IngredientList = Vec<IngredientListEntry>;
#[derive(Debug, Clone, Serialize)]
pub struct IngredientListEntry {
pub index: usize,
pub quantity: GroupedQuantity,
pub outcome: Option<ScaleOutcome>,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Section {
pub name: Option<String>,
pub steps: Vec<Step>,
}
impl Section {
pub(crate) fn new(name: Option<String>) -> Section {
Self {
name,
steps: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.name.is_none() && self.steps.is_empty()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Step {
pub items: Vec<Item>,
pub is_text: bool,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Item {
Text(String),
Component(Component),
InlineQuantity(usize),
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Ingredient {
pub name: String,
pub alias: Option<String>,
pub quantity: Option<Quantity>,
pub note: Option<String>,
pub relation: ComponentRelation,
pub(crate) modifiers: Modifiers,
pub(crate) defined_in_step: bool, }
impl Ingredient {
pub fn display_name(&self) -> Cow<str> {
let mut name = Cow::from(&self.name);
if self.modifiers.contains(Modifiers::RECIPE) {
if let Some(recipe_name) = std::path::Path::new(&self.name)
.file_stem()
.and_then(|s| s.to_str())
{
name = recipe_name.into();
}
}
self.alias.as_ref().map(Cow::from).unwrap_or(name)
}
pub fn modifiers(&self) -> Modifiers {
self.modifiers
}
pub fn is_hidden(&self) -> bool {
self.modifiers.contains(Modifiers::HIDDEN)
}
pub fn is_optional(&self) -> bool {
self.modifiers.contains(Modifiers::OPT)
}
pub fn is_recipe(&self) -> bool {
self.modifiers.contains(Modifiers::RECIPE)
}
pub fn is_reference(&self) -> bool {
self.modifiers.contains(Modifiers::REF)
}
pub fn total_quantity<'a>(
&'a self,
all_ingredients: &'a [Self],
converter: &Converter,
) -> Result<Option<Quantity>, QuantityAddError> {
let mut quantities = self.all_quantities(all_ingredients);
let Some(mut total) = quantities.next().cloned() else { return Ok(None); };
for q in quantities {
total = total.try_add(q, converter)?;
}
let _ = total.fit(converter);
Ok(Some(total))
}
pub fn all_quantities<'a>(
&'a self,
all_ingredients: &'a [Self],
) -> impl Iterator<Item = &Quantity> {
std::iter::once(self.quantity.as_ref())
.chain(
self.relation
.referenced_from()
.iter()
.copied()
.map(|i| all_ingredients[i].quantity.as_ref()),
)
.flatten()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Cookware {
pub name: String,
pub alias: Option<String>,
pub quantity: Option<QuantityValue>,
pub note: Option<String>,
pub relation: ComponentRelation,
pub(crate) modifiers: Modifiers,
}
impl Cookware {
pub fn display_name(&self) -> &str {
self.alias.as_ref().unwrap_or(&self.name)
}
pub fn modifiers(&self) -> Modifiers {
self.modifiers
}
pub fn is_hidden(&self) -> bool {
self.modifiers.contains(Modifiers::HIDDEN)
}
pub fn is_optional(&self) -> bool {
self.modifiers.contains(Modifiers::OPT)
}
pub fn is_reference(&self) -> bool {
self.modifiers.contains(Modifiers::REF)
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ComponentRelation {
Definition { referenced_from: Vec<usize> },
Reference { references_to: usize },
}
impl ComponentRelation {
pub fn referenced_from(&self) -> &[usize] {
match self {
ComponentRelation::Definition { referenced_from } => referenced_from,
ComponentRelation::Reference { .. } => &[],
}
}
pub fn references_to(&self) -> Option<usize> {
match self {
ComponentRelation::Definition { .. } => None,
ComponentRelation::Reference { references_to } => Some(*references_to),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct Timer {
pub name: Option<String>,
pub quantity: Quantity,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct Component {
pub kind: ComponentKind,
pub index: usize,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
#[serde(rename_all = "camelCase")]
pub enum ComponentKind {
Ingredient,
Cookware,
Timer,
}