use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Metadata {
#[serde(flatten)]
pub(super) data: HashMap<String, Value>,
}
impl Metadata {
pub fn title(&self) -> Option<&str> {
self.data.get("title").and_then(|v| v.as_str())
}
pub fn get(&self, key: &str) -> Option<&Value> {
self.data.get(key)
}
pub fn servings(&self) -> Option<i64> {
self.data.get("servings").and_then(|v| v.as_i64())
}
pub fn image_url(&self) -> Option<String> {
const IMAGE_KEYS: &[&str] = &["image", "images", "picture", "pictures"];
for key in IMAGE_KEYS {
if let Some(value) = self.data.get(*key) {
if let Some(url) = value.as_str() {
return Some(url.to_string());
}
if let Some(arr) = value.as_sequence() {
if let Some(first) = arr.first() {
if let Some(url) = first.as_str() {
return Some(url.to_string());
}
}
}
}
}
None
}
pub fn tags(&self) -> Vec<String> {
const TAG_KEYS: &[&str] = &["tags", "tag"];
for key in TAG_KEYS {
if let Some(value) = self.data.get(*key) {
if let Some(tag_str) = value.as_str() {
return tag_str
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
if let Some(arr) = value.as_sequence() {
return arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
}
}
}
Vec::new()
}
}
pub(super) fn parse_yaml_content(yaml_content: &str) -> Option<Metadata> {
if yaml_content.trim().is_empty() {
return None;
}
serde_yaml::from_str::<HashMap<String, Value>>(yaml_content)
.ok()
.map(|data| Metadata { data })
}
pub(super) fn extract_yaml_from_lines<I, E>(mut lines: I) -> Result<Option<String>, E>
where
I: Iterator<Item = Result<String, E>>,
{
let first_line = match lines.next() {
Some(Ok(line)) => line,
Some(Err(e)) => return Err(e),
None => return Ok(None), };
if !first_line.trim().eq("---") {
return Ok(None); }
let mut yaml_lines = Vec::new();
for line_result in lines {
let line = line_result?;
if line.trim().eq("---") {
return Ok(Some(yaml_lines.join(
"
",
)));
}
yaml_lines.push(line);
if yaml_lines.len() > 30 {
return Ok(None);
}
}
Ok(None)
}
pub(super) fn extract_and_parse_metadata<I, E>(lines: I) -> Result<Metadata, E>
where
I: Iterator<Item = Result<String, E>>,
{
let yaml_content = extract_yaml_from_lines(lines)?;
Ok(yaml_content
.and_then(|content| parse_yaml_content(&content))
.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_yaml_content() {
let yaml_content = "title: Test Recipe
servings: 4";
let metadata = parse_yaml_content(yaml_content);
assert!(metadata.is_some());
let metadata = metadata.unwrap();
assert_eq!(metadata.title().unwrap(), "Test Recipe");
assert_eq!(metadata.servings().unwrap(), 4);
let yaml_content = "invalid: yaml: content:";
let metadata = parse_yaml_content(yaml_content);
assert!(metadata.is_none());
let yaml_content = "";
let metadata = parse_yaml_content(yaml_content);
assert!(metadata.is_none());
}
}