use minijinja::Value;
use serde::Serialize;
use std::fmt::{self, Display};
#[derive(Debug, Clone, Serialize)]
pub struct IngredientList {
items: Vec<IngredientListItem>,
}
#[derive(Debug, Clone, Serialize)]
pub struct IngredientListItem {
pub name: String,
pub quantities: GroupedQuantity,
}
#[derive(Clone, Debug, Serialize)]
pub struct GroupedQuantity {
quantities: Vec<Quantity>,
}
#[derive(Clone, Debug, Serialize)]
pub struct Quantity {
pub value: String,
pub unit: Option<String>,
}
impl GroupedQuantity {
pub fn from_quantities(quantities: Vec<Quantity>) -> Self {
Self { quantities }
}
pub fn is_empty(&self) -> bool {
self.quantities.is_empty()
}
}
impl Display for GroupedQuantity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let formatted: Vec<String> = self
.quantities
.iter()
.map(|q| {
if let Some(unit) = &q.unit {
format!("{} {}", q.value, unit)
} else {
q.value.clone()
}
})
.collect();
write!(f, "{}", formatted.join(", "))
}
}
impl minijinja::value::Object for GroupedQuantity {
fn repr(self: &std::sync::Arc<Self>) -> minijinja::value::ObjectRepr {
minijinja::value::ObjectRepr::Seq
}
fn render(self: &std::sync::Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result
where
Self: Sized + 'static,
{
self.fmt(f)
}
fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
if let Some(idx) = key.as_usize() {
return self
.quantities
.get(idx)
.map(minijinja::Value::from_serialize);
}
match key.as_str()? {
"list" => Some(minijinja::Value::from_serialize(&self.quantities)),
_ => None,
}
}
fn enumerate(self: &std::sync::Arc<Self>) -> minijinja::value::Enumerator {
minijinja::value::Enumerator::Seq(self.quantities.len())
}
}
impl From<GroupedQuantity> for minijinja::Value {
fn from(value: GroupedQuantity) -> Self {
Self::from_object(value)
}
}
impl IngredientListItem {
pub fn new(name: String, quantities: GroupedQuantity) -> Self {
Self { name, quantities }
}
pub fn quantities_str(&self) -> String {
self.quantities.to_string()
}
}
impl Display for IngredientListItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.quantities.to_string().is_empty() {
write!(f, "{}", self.name)
} else {
write!(f, "{}: {}", self.name, self.quantities)
}
}
}
impl minijinja::value::Object for IngredientListItem {
fn repr(self: &std::sync::Arc<Self>) -> minijinja::value::ObjectRepr {
minijinja::value::ObjectRepr::Plain
}
fn render(self: &std::sync::Arc<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result
where
Self: Sized + 'static,
{
self.fmt(f)
}
fn get_value(self: &std::sync::Arc<Self>, key: &minijinja::Value) -> Option<minijinja::Value> {
match key.as_str()? {
"name" => Some(minijinja::Value::from(&self.name)),
"quantities" => Some(minijinja::Value::from(self.quantities.clone())),
_ => None,
}
}
}
impl From<IngredientListItem> for minijinja::Value {
fn from(value: IngredientListItem) -> Self {
Self::from_object(value)
}
}
impl IngredientList {
pub fn from_cooklang(list: cooklang::ingredient_list::IngredientList) -> Self {
let mut items = Vec::new();
for (name, grouped_qty) in list {
let quantities: Vec<Quantity> = grouped_qty
.into_vec()
.into_iter()
.map(|qty| Quantity {
value: qty.value().to_string(),
unit: qty.unit().map(String::from),
})
.collect();
items.push(IngredientListItem {
name,
quantities: GroupedQuantity::from_quantities(quantities),
});
}
Self { items }
}
pub fn items(&self) -> &[IngredientListItem] {
&self.items
}
}
impl fmt::Display for IngredientList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for item in &self.items {
write!(f, "{}: {}", item.name, item.quantities)?;
writeln!(f)?;
}
Ok(())
}
}
impl From<IngredientList> for Value {
fn from(list: IngredientList) -> Self {
let values: Vec<Value> = list.items.into_iter().map(Value::from_object).collect();
Value::from(values)
}
}