rust-recipe 0.2.0

A Rust crate that scrapes recipes from websites.
Documentation
use scraper::{Html, Selector};
use serde_json::Value;

use crate::{NutritionInformation, RecipeInformationProvider, RecipeScraper, RestrictedDiet};

pub struct SchemaScraper {}

pub struct SchemaScraperInfoProvider {
    val: Value,
}

impl RecipeScraper for SchemaScraper {
    fn scrape_recipe(
        &self,
        html: &str,
    ) -> Result<Box<dyn RecipeInformationProvider>, serde_json::Error> {
        let sel = Selector::parse(r#"script[type="application/ld+json"]"#).unwrap();
        let document = Html::parse_document(html);

        let mut val: Option<Value> = None;
        for e in document.select(&sel) {
            let s = e.text().collect::<String>();
            let v = serde_json::from_str(&s)?;

            if let Value::Object(ref o) = v {
                if o["@type"] == Value::String(String::from("Recipe")) {
                    val = Some(v);
                    break;
                }
            } else {
                continue;
            }
        }

        match val {
            Some(val) => Ok(Box::new(SchemaScraperInfoProvider { val })),
            None => panic!("couldn't parse the value"),
        }
    }
}

impl RecipeInformationProvider for SchemaScraperInfoProvider {
    fn authors(&self) -> Option<Vec<String>> {
        if let Value::Array(authors) = &self.val["author"] {
            let authors: Vec<String> = authors
                .iter()
                .filter_map(|author| {
                    if let Value::Object(o) = author {
                        match o.get("name") {
                            Some(Value::String(name)) => return Some(name.to_string()),
                            _ => return None,
                        }
                    }
                    None
                })
                .collect();
            return Some(authors);
        }
        None
    }

    fn categories(&self) -> Option<Vec<String>> {
        if let Value::String(s) = &self.val["recipeCategory"] {
            return Some(s.split(", ").map(String::from).collect());
        }
        None
    }

    fn cook_time(&self) -> Option<std::time::Duration> {
        if let Value::String(s) = &self.val["cookTime"] {
            match s.parse::<iso8601_duration::Duration>() {
                Ok(v) => return v.to_std(),
                _ => return None,
            };
        }
        None
    }

    fn cuisines(&self) -> Option<Vec<String>> {
        if let Value::String(s) = &self.val["recipeCuisine"] {
            return Some(s.split(", ").map(String::from).collect());
        }
        None
    }

    fn description(&self) -> Option<String> {
        if let Value::String(s) = &self.val["description"] {
            return Some(s.to_string());
        }
        None
    }

    fn image_url(&self) -> Option<String> {
        if let Value::Object(o) = &self.val["image"] {
            match o.get("url") {
                Some(Value::String(url)) => return Some(url.to_string()),
                _ => return None,
            }
        }
        None
    }

    fn ingredients(&self) -> Option<Vec<String>> {
        if let Value::Array(vals) = &self.val["recipeIngredient"] {
            let ingredients: Vec<String> = vals
                .iter()
                .filter_map(|v| {
                    if let Value::String(s) = v {
                        return Some(s.to_string());
                    }
                    None
                })
                .collect();
            return Some(ingredients);
        }
        None
    }

    fn instructions(&self) -> Option<Vec<String>> {
        if let Value::Array(ins) = &self.val["recipeInstructions"] {
            let instructions: Vec<String> = ins
                .iter()
                .filter_map(|cur_in| match cur_in {
                    Value::String(s) => Some(s.to_string()),
                    Value::Object(obj) => {
                        if let Some(Value::String(s)) = obj.get("@type") {
                            if s == "HowToStep" {
                                return match obj.get("text") {
                                    Some(Value::String(text)) => return Some(text.to_string()),
                                    _ => None,
                                };
                            }
                            None
                        } else {
                            None
                        }
                    }
                    _ => None,
                })
                .collect();
            return Some(instructions);
        }
        None
    }

    fn language(&self) -> Option<String> {
        match &self.val["inLanguage"] {
            Value::String(s) => Some(s.to_string()),
            Value::Object(obj) => {
                if let Some(Value::String(t)) = obj.get("@type") {
                    if t == "Language" {
                        return match obj.get("text") {
                            Some(Value::String(text)) => return Some(text.to_string()),
                            _ => None,
                        };
                    }
                }
                None
            }
            _ => None,
        }
    }

    fn name(&self) -> Option<String> {
        if let Value::String(s) = &self.val["name"] {
            return Some(s.to_string());
        }
        None
    }

    fn nutrition(&self) -> Option<NutritionInformation> {
        if let Value::Object(o) = &self.val["nutrition"] {
            let mut nutrition_info = NutritionInformation::default();
            for (field_name, val) in o {
                if let Value::String(amount) = val {
                    nutrition_info.add_info(field_name, amount);
                }
            }
            return Some(nutrition_info);
        }
        None
    }

    fn prep_time(&self) -> Option<std::time::Duration> {
        if let Value::String(s) = &self.val["prepTime"] {
            match s.parse::<iso8601_duration::Duration>() {
                Ok(v) => return v.to_std(),
                _ => return None,
            };
        }
        None
    }

    fn suitable_diets(&self) -> Option<Vec<RestrictedDiet>> {
        if let Value::String(s) = &self.val["suitableForDiet"] {
            return s
                .split(", ")
                .map(|diet| diet.parse::<RestrictedDiet>().ok())
                .collect();
        }
        None
    }

    fn total_time(&self) -> Option<std::time::Duration> {
        if let Value::String(s) = &self.val["totalTime"] {
            match s.parse::<iso8601_duration::Duration>() {
                Ok(v) => return v.to_std(),
                _ => return None,
            };
        }
        None
    }

    fn yields(&self) -> Option<String> {
        if let Value::String(s) = &self.val["recipeYield"] {
            return Some(s.to_string());
        }
        None
    }
}