use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[cfg(feature = "ts")]
use tsify::Tsify;
use crate::{
convert::Converter, metadata::Metadata, parser::Modifiers, quantity::Quantity, GroupedQuantity,
};
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
pub struct Recipe {
#[cfg_attr(feature = "ts", serde(rename = "raw_metadata"))]
pub metadata: Metadata,
pub sections: Vec<Section>,
pub ingredients: Vec<Ingredient>,
pub cookware: Vec<Cookware>,
pub timers: Vec<Timer>,
pub inline_quantities: Vec<Quantity>,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
pub struct Section {
pub name: Option<String>,
pub content: Vec<Content>,
}
impl Section {
pub(crate) fn new(name: Option<String>) -> Section {
Self {
name,
content: Vec::new(),
}
}
pub fn is_empty(&self) -> bool {
self.name.is_none() && self.content.is_empty()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
pub enum Content {
Step(Step),
Text(String),
}
impl Content {
pub fn is_step(&self) -> bool {
matches!(self, Self::Step(_))
}
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
pub fn unwrap_step(&self) -> &Step {
match self {
Content::Step(s) => s,
Content::Text(_) => panic!("content is text"),
}
}
pub fn unwrap_text(&self) -> &str {
match self {
Content::Step(_) => panic!("content is step"),
Content::Text(t) => t.as_str(),
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[non_exhaustive]
pub struct Step {
pub items: Vec<Item>,
pub number: u32,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Item {
Text {
value: String,
},
Ingredient {
index: usize,
},
Cookware {
index: usize,
},
Timer {
index: usize,
},
InlineQuantity {
index: usize,
},
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
pub struct RecipeReference {
pub name: String,
pub components: Vec<String>,
}
impl RecipeReference {
pub fn path(&self, separator: &str) -> String {
self.components.join(separator) + separator + &self.name
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[cfg_attr(feature = "ts", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Ingredient {
pub name: String,
pub alias: Option<String>,
pub quantity: Option<Quantity>,
pub note: Option<String>,
pub reference: Option<RecipeReference>,
pub relation: IngredientRelation,
#[cfg_attr(feature = "ts", serde(skip))]
pub(crate) modifiers: Modifiers,
}
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 group_quantities(
&self,
all_ingredients: &[Self],
converter: &Converter,
) -> GroupedQuantity {
let mut grouped = GroupedQuantity::default();
for q in self.all_quantities(all_ingredients) {
grouped.add(q, converter);
}
let _ = grouped.fit(converter);
grouped
}
pub fn all_quantities<'a>(
&'a self,
all_ingredients: &'a [Self],
) -> impl Iterator<Item = &'a 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)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[cfg_attr(feature = "ts", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Cookware {
pub name: String,
pub alias: Option<String>,
pub quantity: Option<Quantity>,
pub note: Option<String>,
pub relation: ComponentRelation,
#[cfg_attr(feature = "ts", serde(skip))]
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 group_quantities(
&self,
all_cookware: &[Self],
converter: &Converter,
) -> GroupedQuantity {
let mut g = GroupedQuantity::empty();
for q in self.all_quantities(all_cookware) {
g.add(q, converter);
}
let _ = g.fit(converter);
g
}
pub fn all_quantities<'a>(
&'a self,
all_cookware: &'a [Self],
) -> impl Iterator<Item = &'a Quantity> {
std::iter::once(self.quantity.as_ref())
.chain(
self.relation
.referenced_from()
.iter()
.copied()
.map(|i| all_cookware[i].quantity.as_ref()),
)
.flatten()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ComponentRelation {
Definition {
referenced_from: Vec<usize>,
defined_in_step: bool,
},
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),
}
}
pub fn is_reference(&self) -> bool {
matches!(self, ComponentRelation::Reference { .. })
}
pub fn is_definition(&self) -> bool {
matches!(self, ComponentRelation::Definition { .. })
}
pub fn is_defined_in_step(&self) -> Option<bool> {
match self {
ComponentRelation::Definition {
defined_in_step, ..
} => Some(*defined_in_step),
ComponentRelation::Reference { .. } => None,
}
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
pub struct IngredientRelation {
relation: ComponentRelation,
reference_target: Option<IngredientReferenceTarget>,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy)]
#[cfg_attr(feature = "ts", derive(Tsify))]
#[serde(rename_all = "camelCase")]
pub enum IngredientReferenceTarget {
Ingredient,
Step,
Section,
}
impl IngredientRelation {
pub(crate) fn definition(referenced_from: Vec<usize>, defined_in_step: bool) -> Self {
Self {
relation: ComponentRelation::Definition {
referenced_from,
defined_in_step,
},
reference_target: None,
}
}
pub(crate) fn reference(
references_to: usize,
reference_target: IngredientReferenceTarget,
) -> Self {
Self {
relation: ComponentRelation::Reference { references_to },
reference_target: Some(reference_target),
}
}
pub fn referenced_from(&self) -> &[usize] {
self.relation.referenced_from()
}
pub(crate) fn referenced_from_mut(&mut self) -> Option<&mut Vec<usize>> {
match &mut self.relation {
ComponentRelation::Definition {
referenced_from, ..
} => Some(referenced_from),
ComponentRelation::Reference { .. } => None,
}
}
pub fn references_to(&self) -> Option<(usize, IngredientReferenceTarget)> {
self.relation
.references_to()
.map(|index| (index, self.reference_target.unwrap()))
}
pub fn is_regular_reference(&self) -> bool {
use IngredientReferenceTarget as Target;
self.references_to()
.is_some_and(|(_, target)| target == Target::Ingredient)
}
pub fn is_intermediate_reference(&self) -> bool {
use IngredientReferenceTarget as Target;
self.references_to()
.is_some_and(|(_, target)| matches!(target, Target::Step | Target::Section))
}
pub fn is_definition(&self) -> bool {
self.relation.is_definition()
}
pub fn is_defined_in_step(&self) -> Option<bool> {
self.relation.is_defined_in_step()
}
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
#[cfg_attr(feature = "ts", derive(Tsify))]
pub struct Timer {
pub name: Option<String>,
pub quantity: Option<Quantity>,
}