recipemd/
models.rs

1// Copyright (c) 2023 d-k-bo
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4#[cfg(feature = "serde")]
5use serde::{Deserialize, Serialize};
6
7/// A [Recipe](https://recipemd.org/specification.html#recipe) as defined by the RecipeMD specification.
8///
9/// See the [top-level documentation](crate) for details.
10#[derive(Clone, Debug)]
11#[cfg_attr(any(test, feature = "tests"), derive(PartialEq))]
12#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
13pub struct Recipe {
14    pub title: String,
15    pub description: Option<String>,
16    pub tags: Vec<String>,
17    pub yields: Vec<Amount>,
18    pub ingredients: Vec<Ingredient>,
19    pub ingredient_groups: Vec<IngredientGroup>,
20    pub instructions: Option<String>,
21}
22
23/// An [IngredientGroup](https://recipemd.org/specification.html#ingredient-group).
24#[derive(Clone, Debug)]
25#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
26#[cfg_attr(any(test, feature = "tests"), derive(PartialEq))]
27pub struct IngredientGroup {
28    pub title: String,
29    pub ingredients: Vec<Ingredient>,
30    pub ingredient_groups: Vec<IngredientGroup>,
31}
32
33/// An [Ingredient](https://recipemd.org/specification.html#ingredient).
34#[derive(Clone, Debug)]
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[cfg_attr(any(test, feature = "tests"), derive(PartialEq))]
37pub struct Ingredient {
38    pub amount: Option<Amount>,
39    pub name: String,
40    pub link: Option<String>,
41}
42
43/// An [Amount](https://recipemd.org/specification.html#amount) used for ingredients and yields.
44#[derive(Clone, Debug)]
45#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
46#[cfg_attr(any(test, feature = "tests"), derive(PartialEq))]
47pub struct Amount {
48    pub factor: Factor,
49    pub unit: Option<String>,
50}
51
52/// Represents the numerical part of an [`Amount`].
53///
54/// Integers are serialized as integers, fractions and floats are serialized as floats.
55#[derive(Copy, Clone, Debug)]
56pub enum Factor {
57    Integer(u32),
58    Fraction(u16, u16),
59    Float(f32),
60}
61
62impl From<Factor> for f32 {
63    fn from(value: Factor) -> Self {
64        match value {
65            Factor::Integer(v) => v as f32,
66            Factor::Fraction(num, denom) => num as f32 / denom as f32,
67            Factor::Float(v) => v,
68        }
69    }
70}
71
72#[cfg(feature = "serde")]
73impl Serialize for Factor {
74    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75    where
76        S: serde::Serializer,
77    {
78        match self {
79            Factor::Integer(v) => v.to_string().serialize(serializer),
80            Factor::Fraction(num, denom) => (*num as f32 / *denom as f32)
81                .to_string()
82                .serialize(serializer),
83            Factor::Float(v) => v.to_string().serialize(serializer),
84        }
85    }
86}
87
88#[cfg(feature = "serde")]
89impl<'de> Deserialize<'de> for Factor {
90    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
91    where
92        D: serde::Deserializer<'de>,
93    {
94        use std::str::FromStr;
95
96        let s = <&str>::deserialize(deserializer)?;
97        u32::from_str(s)
98            .map(Factor::Integer)
99            .or_else(|_| f32::from_str(s).map(Factor::Float))
100            .map_err(serde::de::Error::custom)
101    }
102}
103
104#[cfg(any(test, feature = "tests"))]
105impl PartialEq for Factor {
106    fn eq(&self, other: &Self) -> bool {
107        f32::from(*self).eq(&f32::from(*other))
108    }
109}