nextcloud-cooking-schedule/src/recipe.rs

133 lines
3.6 KiB
Rust
Raw Normal View History

// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// SPDX-License-Identifier: AGPL-3.0-or-later
use {
chrono::Duration,
serde::{Deserialize, Deserializer},
};
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
#[serde(rename = "recipe_id")]
pub id: u32,
pub name: String,
pub keywords: Option<String>,
pub date_created: DateTime,
pub date_modified: DateTime,
}
/// A recipe according to [schema.org](http://schema.org/Recipe)
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Recipe {
pub id: isize,
pub name: String,
pub description: String,
pub url: Option<String>,
pub keywords: Option<String>,
#[serde(rename = "dateCreated")]
pub created: DateTime,
#[serde(rename = "dateModified")]
pub modified: Option<DateTime>,
pub image_url: String,
#[serde(deserialize_with = "deserialize_duration")]
pub prep_time: Duration,
#[serde(default)]
#[serde(deserialize_with = "deserialize_maybe_duration")]
pub cook_time: Option<Duration>,
#[serde(default)]
#[serde(deserialize_with = "deserialize_maybe_duration")]
pub total_time: Option<Duration>,
pub image: Option<String>,
pub recipe_yield: isize,
#[serde(rename = "recipeCategory")]
pub category: Option<String>,
pub tools: Option<Vec<Tool>>,
#[serde(rename = "recipeIngredient")]
pub ingredients: Vec<Ingredient>,
#[serde(rename = "recipeInstructions")]
pub instructions: Vec<Instruction>,
//pub nutrition: Nutrition,
}
#[derive(Deserialize, Debug, Ord, Eq, PartialEq, PartialOrd)]
#[serde(rename_all = "camelCase")]
pub struct Ingredient(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Tool(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Instruction(pub String);
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Nutrition {
pub calories: Option<String>,
pub carbohydrates_content: Option<String>,
pub cholesterol_content: Option<String>,
pub fat_content: Option<String>,
pub fiber_content: Option<String>,
pub protein_content: Option<String>,
pub saturated_fat_content: Option<String>,
pub serving_size: Option<String>,
pub sodium_content: Option<String>,
pub sugar_content: Option<String>,
pub trans_fat_content: Option<String>,
pub unsaturated_fat_content: Option<String>,
}
type DateTime = chrono::DateTime<chrono::Utc>;
fn deserialize_maybe_duration<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Some(deserialize_duration(deserializer)?))
}
fn deserialize_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(DurationVisitor)
}
struct DurationVisitor;
impl<'de> serde::de::Visitor<'de> for DurationVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a duration in ISO 8601 format")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
speedate::Duration::parse_str(value)
.map(|dt| Duration::seconds(dt.signed_total_seconds()))
.map_err(|e| E::custom(e.to_string()))
}
}
impl Recipe {
pub fn total_time(&self) -> Duration {
self.total_time
.unwrap_or_else(|| self.prep_time + self.cook_time.unwrap_or(Duration::zero()))
}
}