use std::collections::HashMap;
use std::str::FromStr;
use linked_hash_map::LinkedHashMap;
use thiserror::Error;
use unic_langid::LanguageIdentifier;
use url::Url;
use yaml_rust::{Yaml, YamlEmitter, YamlLoader};
#[cfg(feature = "biblatex")]
use biblatex::{Bibliography, TypeError};
use crate::lang::{Case, SentenceCase};
use crate::types::{
parse_range, Date, DateError, Duration, DurationError, EntryType, FmtString,
NumOrStr, Person, PersonError, PersonRole, QualifiedUrl, Title,
};
use crate::{Entry, Value};
#[derive(Clone, Error, Debug)]
pub enum YamlBibliographyError {
#[error("string could not be read as yaml")]
Scan(#[from] yaml_rust::ScanError),
#[error("file has no top-level hash map")]
Structure,
#[error("the entry with key `{key}` does not contain a hash map")]
EntryStructure {
key: String,
},
#[error("a field name in the entry with key `{key}` cannot be read as a string")]
FieldNameNoStr {
key: String,
},
#[error("a entry key cannot be parsed as a string")]
KeyNoStr,
#[error(
"wrong data type for field `{field}` in entry `{key}` (expected {expected:?})"
)]
DataTypeMismatch {
key: String,
field: String,
expected: String,
},
#[error("error when parsing data for field `{field}` in entry `{key}` ({source})")]
DataType {
key: String,
field: String,
#[source]
source: YamlDataTypeError,
},
}
#[derive(Clone, Error, Debug)]
pub enum YamlFmtStringError {
#[error("key cannot be parsed as a string")]
KeyNoString,
#[error("no value was found")]
NoValue,
#[error("value cannot be parsed as a string")]
ValueNoString,
#[error("the `verbatim` property must be boolean")]
VerbatimNoBool,
}
#[derive(Clone, Error, Debug)]
pub enum YamlDataTypeError {
#[error("formattable string structurally malformed")]
FmtString(#[from] YamlFmtStringError),
#[error("date string structurally malformed")]
Date(#[from] DateError),
#[error("person string structurally malformed")]
Person(#[from] PersonError),
#[error("duration string structurally malformed")]
Duration(#[from] DurationError),
#[error("invalid url")]
Url(#[from] url::ParseError),
#[error("string is not a range")]
Range,
#[error("array element empty")]
EmptyArrayElement,
#[error("missing required field in details hash map")]
MissingRequiredField,
#[error("mismatched primitive type")]
MismatchedPrimitive,
}
impl YamlBibliographyError {
fn new_data_type_error(key: &str, field: &str, expected: &str) -> Self {
Self::DataTypeMismatch {
key: key.to_string(),
field: field.to_string(),
expected: expected.to_string(),
}
}
fn new_data_type_src_error(
key: &str,
field: &str,
dtype_err: YamlDataTypeError,
) -> Self {
Self::DataType {
key: key.to_string(),
field: field.to_string(),
source: dtype_err,
}
}
}
pub fn from_yaml_str(string: &str) -> Result<Vec<Entry>, YamlBibliographyError> {
let yaml = YamlLoader::load_from_str(string)?
.into_iter()
.next()
.ok_or(YamlBibliographyError::Structure)?;
from_yaml(yaml)
}
pub fn from_yaml(yaml: Yaml) -> Result<Vec<Entry>, YamlBibliographyError> {
let doc = yaml.into_hash().ok_or(YamlBibliographyError::Structure)?;
let mut entries = vec![];
for (key, fields) in doc.into_iter() {
let key = key.into_string().ok_or(YamlBibliographyError::KeyNoStr)?;
entries.push(entry_from_yaml(key, fields, EntryType::Misc)?);
}
Ok(entries)
}
#[cfg(feature = "biblatex")]
#[derive(Clone, Debug)]
pub enum BibLaTeXError {
Parse(biblatex::ParseError),
Type(biblatex::TypeError),
}
#[cfg(feature = "biblatex")]
impl std::fmt::Display for BibLaTeXError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(err) => write!(f, "biblatex parse error: {}", err),
Self::Type(err) => write!(f, "biblatex type error: {}", err),
}
}
}
#[cfg(feature = "biblatex")]
pub fn from_biblatex_str(biblatex: &str) -> Result<Vec<Entry>, Vec<BibLaTeXError>> {
let bibliography =
Bibliography::parse(biblatex).map_err(|e| vec![BibLaTeXError::Parse(e)])?;
from_biblatex(&bibliography)
.map_err(|e| e.into_iter().map(BibLaTeXError::Type).collect())
}
#[cfg(feature = "biblatex")]
pub fn from_biblatex(bibliography: &Bibliography) -> Result<Vec<Entry>, Vec<TypeError>> {
use std::convert::TryInto;
let res: Vec<Result<Entry, TypeError>> =
bibliography.iter().map(TryInto::try_into).collect();
let errors: Vec<TypeError> = res
.iter()
.filter_map(|item| match item {
Ok(_) => None,
Err(err) => Some(err.clone()),
})
.collect();
if !errors.is_empty() {
Err(errors)
} else {
Ok(res.into_iter().map(|item| item.unwrap()).collect())
}
}
fn yaml_hash_map_with_string_keys(
map: LinkedHashMap<Yaml, Yaml>,
) -> LinkedHashMap<String, Yaml> {
map.into_iter()
.filter_map(|(k, v)| k.into_string().map(|k| (k, v)))
.collect()
}
fn title_from_hash_map(
map: LinkedHashMap<Yaml, Yaml>,
) -> Result<Title, YamlFmtStringError> {
let canonical = fmt_str_from_hash_map(map.clone())?;
let map = yaml_hash_map_with_string_keys(map);
let shorthand = if let Some(sh) = map.get("shorthand") {
let sh = if let Some(s) = sh.as_str() {
FmtString::new(s)
} else if let Some(hm) = sh.clone().into_hash() {
fmt_str_from_hash_map(hm)?
} else {
return Err(YamlFmtStringError::NoValue);
};
Some(sh)
} else {
None
};
let translated = if let Some(tl) = map.get("translation") {
let tl = if let Some(s) = tl.as_str() {
FmtString::new(s)
} else if let Some(hm) = tl.clone().into_hash() {
fmt_str_from_hash_map(hm)?
} else {
return Err(YamlFmtStringError::NoValue);
};
Some(tl)
} else {
None
};
Ok(Title { canonical, shorthand, translated })
}
fn fmt_str_from_hash_map(
map: LinkedHashMap<Yaml, Yaml>,
) -> Result<FmtString, YamlFmtStringError> {
let map = yaml_hash_map_with_string_keys(map);
let fields = ["value", "sentence-case", "title-case"];
let mut fields: Vec<String> = fields
.iter()
.filter_map(|&f| map.get(f).and_then(|v| v.clone().into_string()))
.collect();
if fields.is_empty() {
return Err(YamlFmtStringError::NoValue);
}
let value = fields.remove(0);
let mut fmt = FmtString::new(value);
if let Some(verbatim) = map.get("verbatim") {
fmt = fmt.verbatim(verbatim.as_bool().ok_or(YamlFmtStringError::VerbatimNoBool)?);
}
if let Some(sentence_case) = map.get("sentence-case") {
fmt = fmt.sentence_case(
sentence_case
.clone()
.into_string()
.ok_or(YamlFmtStringError::ValueNoString)?,
);
}
if let Some(title_case) = map.get("title-case") {
fmt = fmt.title_case(
title_case
.clone()
.into_string()
.ok_or(YamlFmtStringError::ValueNoString)?,
);
}
Ok(fmt)
}
fn person_from_yaml(
item: Yaml,
key: &str,
field_name: &str,
) -> Result<Person, YamlBibliographyError> {
if let Some(map) = item.clone().into_hash() {
let mut map = yaml_hash_map_with_string_keys(map);
let name = map.remove("name").and_then(|v| v.into_string()).ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
key,
field_name,
YamlDataTypeError::MissingRequiredField,
)
})?;
let optionals = ["given-name", "prefix", "suffix", "alias"];
let mut values = vec![];
for &field in optionals.iter() {
values.push(map.remove(field).and_then(|v| v.into_string()));
}
Ok(Person {
name,
alias: values.pop().unwrap(),
suffix: values.pop().unwrap(),
prefix: values.pop().unwrap(),
given_name: values.pop().unwrap(),
})
} else if let Some(s) = item.into_string() {
Ok(Person::from_strings(&s.split(',').collect::<Vec<&str>>()).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
key,
field_name,
YamlDataTypeError::Person(e),
)
})?)
} else {
Err(YamlBibliographyError::new_data_type_error(key, field_name, "person"))
}
}
fn persons_from_yaml(
value: Yaml,
key: &str,
field_name: &str,
) -> Result<Vec<Person>, YamlBibliographyError> {
let mut persons = vec![];
if value.is_array() {
for item in value {
persons.push(person_from_yaml(item, key, field_name)?);
}
} else {
persons.push(person_from_yaml(value, key, field_name)?);
}
Ok(persons)
}
fn affiliated_from_yaml(
item: Yaml,
key: &str,
field_name: &str,
) -> Result<(Vec<Person>, PersonRole), YamlBibliographyError> {
let mut map = yaml_hash_map_with_string_keys(item.into_hash().ok_or_else(|| {
YamlBibliographyError::new_data_type_error(key, field_name, "affiliated person")
})?);
let persons = map
.remove("names")
.ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
key,
field_name,
YamlDataTypeError::MissingRequiredField,
)
})
.and_then(|value| persons_from_yaml(value, key, field_name))?;
let role = map
.remove("role")
.ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
key,
field_name,
YamlDataTypeError::MissingRequiredField,
)
})
.and_then(|t| {
t.into_string().ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
key,
field_name,
YamlDataTypeError::MismatchedPrimitive,
)
})
})?;
let role =
PersonRole::from_str(&role.to_lowercase()).unwrap_or(PersonRole::Unknown(role));
Ok((persons, role))
}
fn entry_from_yaml(
key: String,
yaml: Yaml,
default_type: EntryType,
) -> Result<Entry, YamlBibliographyError> {
let mut entry = Entry {
key: key.clone(),
content: HashMap::new(),
entry_type: default_type,
};
for (field_name, yaml) in yaml
.into_hash()
.ok_or_else(|| YamlBibliographyError::EntryStructure { key: key.clone() })?
.into_iter()
{
let field_name = field_name
.into_string()
.ok_or_else(|| YamlBibliographyError::FieldNameNoStr { key: key.clone() })?;
let fname_str = field_name.as_str();
if fname_str == "type" {
let val = yaml.into_string().ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::MismatchedPrimitive,
)
})?;
if let Ok(tp) = EntryType::from_str(&val.to_lowercase()) {
entry.entry_type = tp;
}
continue;
}
let value = match fname_str {
"title" => match yaml {
Yaml::Hash(map) => {
Value::Title(title_from_hash_map(map).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::FmtString(e),
)
})?)
}
Yaml::String(t) => Value::Title(Title {
canonical: FmtString::new(t),
shorthand: None,
translated: None,
}),
_ => {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"text or formattable string or title",
));
}
},
"publisher" | "location" | "archive" | "archive-location" => match yaml {
Yaml::Hash(map) => {
Value::FmtString(fmt_str_from_hash_map(map).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::FmtString(e),
)
})?)
}
Yaml::String(t) => Value::FmtString(FmtString::new(t)),
_ => {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"text or formattable string",
));
}
},
"author" | "editor" => {
Value::Persons(persons_from_yaml(yaml, &key, &field_name)?)
}
"affiliated" => {
let mut res = vec![];
if yaml.is_array() {
for item in yaml {
res.push(affiliated_from_yaml(item, &key, &field_name)?);
}
} else if let Yaml::Hash(item) = yaml {
res.push(affiliated_from_yaml(Yaml::Hash(item), &key, &field_name)?);
} else {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"affiliated person",
));
}
Value::PersonsWithRoles(res)
}
"date" => Value::Date(match yaml {
Yaml::Integer(value) => Date::from_year(value as i32),
Yaml::String(value) => Date::from_str(&value).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Date(e),
)
})?,
_ => {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"date",
));
}
}),
"issue" | "edition" => {
let as_int = yaml.as_i64();
let as_str = if as_int.is_none() { yaml.into_string() } else { None };
if let Some(i) = as_int {
Value::IntegerOrText(NumOrStr::Number(i))
} else if let Some(t) = as_str {
Value::IntegerOrText(NumOrStr::Str(t))
} else {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"integer or text",
));
}
}
"volume-total" | "page-total" => {
Value::Integer(yaml.into_i64().ok_or_else(|| {
YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"integer",
)
})?)
}
"volume" | "page-range" => Value::Range(match yaml {
Yaml::Integer(value) => value..value,
Yaml::String(value) => parse_range(&value).ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Range,
)
})?,
_ => {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"integer range",
));
}
}),
"runtime" => {
let v = yaml
.into_string()
.ok_or_else(|| {
YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"duration",
)
})
.and_then(|s| {
Duration::from_str(&s).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Duration(e),
)
})
})?;
Value::Duration(v)
}
"time-range" => {
let v = yaml
.into_string()
.ok_or_else(|| {
YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"duration",
)
})
.and_then(|s| {
Duration::range_from_str(&s).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Duration(e),
)
})
})?;
Value::TimeRange(v)
}
"url" => {
let (url, date) = match yaml {
Yaml::String(s) => (
Url::parse(&s).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Url(e),
)
})?,
None,
),
Yaml::Hash(map) => {
let mut map = yaml_hash_map_with_string_keys(map);
let url = map
.remove("value")
.ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::MissingRequiredField,
)
})
.and_then(|value| {
value
.into_string()
.ok_or_else(|| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::MismatchedPrimitive,
)
})
.and_then(|s| {
Url::parse(&s).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Url(e),
)
})
})
})?;
let date = if let Some(date) = map.remove("date") {
if let Some(year) = date.as_i64() {
Some(Date::from_year(year as i32))
} else if let Some(s) = date.into_string() {
Some(Date::from_str(&s).map_err(|e| {
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::Date(e),
)
})?)
} else {
return Err(
YamlBibliographyError::new_data_type_src_error(
&key,
&field_name,
YamlDataTypeError::MismatchedPrimitive,
),
);
}
} else {
None
};
(url, date)
}
_ => {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"qualified url",
));
}
};
Value::Url(QualifiedUrl { value: url, visit_date: date })
}
"language" => Value::Language(
yaml.into_string().and_then(|f| f.parse().ok()).ok_or_else(|| {
YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"unicode language identifier",
)
})?,
),
"parent" => {
if yaml.is_array() {
let mut entries = vec![];
for item in yaml {
entries.push(entry_from_yaml(
key.clone(),
item,
entry.entry_type.default_parent(),
)?)
}
Value::Entries(entries)
} else {
Value::Entries(vec![entry_from_yaml(
key.clone(),
yaml,
entry.entry_type.default_parent(),
)?])
}
}
_ => {
if let Some(t) = yaml.clone().into_string() {
Value::Text(t)
} else if let Some(i) = yaml.as_i64() {
Value::Text(i.to_string())
} else if let Some(i) = yaml.as_f64() {
Value::Text(i.to_string())
} else {
return Err(YamlBibliographyError::new_data_type_error(
&key,
&field_name,
"text",
));
}
}
};
entry.content.insert(field_name, value);
}
Ok(entry)
}
impl From<Date> for Yaml {
fn from(date: Date) -> Self {
let s = match (date.month.is_some(), date.day.is_some()) {
(true, true) => format!(
"{:04}-{:02}-{:02}",
date.year,
date.month.unwrap() + 1,
date.day.unwrap() + 1,
),
(true, false) => format!("{:04}-{:02}", date.year, date.month.unwrap() + 1),
(false, _) => return Yaml::Integer(date.year as i64),
};
Yaml::String(s)
}
}
impl From<Duration> for Yaml {
fn from(d: Duration) -> Self {
if d.days == 0 {
if d.hours == 0 {
if d.milliseconds == 0.0 {
Yaml::String(format!("{:02}:{:02}", d.minutes, d.seconds))
} else {
Yaml::String(format!(
"{:02}:{:02},{:03}",
d.minutes, d.seconds, d.milliseconds
))
}
} else {
Yaml::String(format!(
"{:02}:{:02}:{:02},{:03}",
d.hours, d.minutes, d.seconds, d.milliseconds
))
}
} else if d.milliseconds == 0.0 {
Yaml::String(format!(
"{}:{:02}:{:02}:{:02}",
d.days, d.hours, d.minutes, d.seconds
))
} else {
Yaml::String(format!(
"{}:{:02}:{:02}:{:02},{:03}",
d.days, d.hours, d.minutes, d.seconds, d.milliseconds
))
}
}
}
impl From<EntryType> for Yaml {
fn from(entry_type: EntryType) -> Self {
Yaml::String(entry_type.to_string())
}
}
impl From<&FmtString> for Yaml {
fn from(fmt: &FmtString) -> Self {
if !fmt.verbatim && fmt.title_case.is_none() && fmt.sentence_case.is_none() {
Yaml::String(fmt.value.clone())
} else {
let mut hm = LinkedHashMap::new();
hm.insert(Yaml::String("value".into()), Yaml::String(fmt.value.clone()));
if fmt.verbatim {
hm.insert(Yaml::String("verbatim".into()), Yaml::Boolean(true));
} else {
if let Some(title) = &fmt.title_case {
if fmt.value != *title {
hm.insert(
Yaml::String("title-case".into()),
Yaml::String(title.clone()),
);
}
}
if let Some(sentence) = &fmt.sentence_case {
if SentenceCase::new().apply(&fmt.value) != *sentence {
hm.insert(
Yaml::String("sentence-case".into()),
Yaml::String(sentence.clone()),
);
}
}
}
if hm.len() == 1 {
hm.remove(&Yaml::String("value".into())).unwrap()
} else {
Yaml::Hash(hm)
}
}
}
}
impl From<&NumOrStr> for Yaml {
fn from(num_or_str: &NumOrStr) -> Self {
match num_or_str {
NumOrStr::Number(i) => Yaml::Integer(*i),
NumOrStr::Str(s) => Yaml::String(s.clone()),
}
}
}
impl From<&Person> for Yaml {
fn from(person: &Person) -> Self {
if let Some(alias) = &person.alias {
let mut hm = LinkedHashMap::new();
hm.insert(Yaml::String("name".into()), Yaml::String(person.name.clone()));
if let Some(given_name) = &person.given_name {
hm.insert(
Yaml::String("given-name".into()),
Yaml::String(given_name.clone()),
);
}
if let Some(prefix) = &person.prefix {
hm.insert(Yaml::String("prefix".into()), Yaml::String(prefix.clone()));
}
if let Some(suffix) = &person.suffix {
hm.insert(Yaml::String("suffix".into()), Yaml::String(suffix.clone()));
}
hm.insert(Yaml::String("alias".into()), Yaml::String(alias.clone()));
Yaml::Hash(hm)
} else {
let mut res = if let Some(prefix) = &person.prefix {
format!("{} {}", prefix, person.name)
} else {
person.name.clone()
};
if let Some(gn) = &person.given_name {
res += ", ";
res += gn;
if let Some(suffix) = &person.suffix {
res += ", ";
res += suffix;
}
}
Yaml::String(res)
}
}
}
impl From<&PersonRole> for Yaml {
fn from(role: &PersonRole) -> Self {
Yaml::String(role.to_string())
}
}
impl From<&QualifiedUrl> for Yaml {
fn from(url: &QualifiedUrl) -> Self {
if let Some(date) = url.visit_date {
let mut hm = LinkedHashMap::new();
hm.insert(Yaml::String("value".into()), Yaml::String(url.value.to_string()));
hm.insert(Yaml::String("date".into()), date.into());
Yaml::Hash(hm)
} else {
Yaml::String(url.value.to_string())
}
}
}
impl From<&Title> for Yaml {
fn from(title: &Title) -> Self {
if title.translated.is_none() && title.shorthand.is_none() {
(&title.canonical).into()
} else {
let mut map = match (&title.canonical).into() {
Yaml::Hash(map) => map,
Yaml::String(s) => {
let mut map = LinkedHashMap::new();
map.insert(Yaml::String("value".into()), Yaml::String(s));
map
}
_ => unreachable!(),
};
if let Some(translated) = &title.translated {
map.insert(Yaml::String("translation".into()), translated.into());
}
if let Some(shorthand) = &title.shorthand {
map.insert(Yaml::String("shorthand".into()), shorthand.into());
}
Yaml::Hash(map)
}
}
}
fn range_into_yaml(range: &std::ops::Range<i64>) -> Yaml {
if range.start != range.end {
Yaml::String(format!("{}-{}", range.start, range.end))
} else {
Yaml::Integer(range.start)
}
}
fn affiliated_into_yaml(affiliated: &(Vec<Person>, PersonRole)) -> Yaml {
let persons = Yaml::Array(affiliated.0.iter().map(Into::into).collect());
let mut hm = LinkedHashMap::new();
hm.insert(Yaml::String("names".into()), persons);
hm.insert(Yaml::String("role".into()), (&affiliated.1).into());
Yaml::Hash(hm)
}
fn time_range_into_yaml(range: std::ops::Range<Duration>) -> Yaml {
if range.start != range.end {
let start =
if let Yaml::String(s) = range.start.into() { s } else { unreachable!() };
let end = if let Yaml::String(s) = range.end.into() { s } else { unreachable!() };
Yaml::String(format!("{}-{}", start, end))
} else {
range.start.into()
}
}
fn language_into_yaml(lang: &LanguageIdentifier) -> Yaml {
Yaml::String(lang.to_string())
}
fn persons_into_yaml(pers: &[Person]) -> Yaml {
let mut persons: Vec<_> = pers.iter().map(Into::into).collect();
match persons.len() {
1 => persons.pop().unwrap(),
_ => Yaml::Array(persons),
}
}
fn affiliateds_into_yaml(pers: &[(Vec<Person>, PersonRole)]) -> Yaml {
let mut persons: Vec<_> = pers.iter().map(affiliated_into_yaml).collect();
match persons.len() {
1 => persons.pop().unwrap(),
_ => Yaml::Array(persons),
}
}
impl From<&Entry> for Yaml {
fn from(entry: &Entry) -> Self {
let mut hm = LinkedHashMap::new();
hm.insert(Yaml::String("type".into()), entry.kind().into());
for (s, item) in &entry.content {
let content = match item {
Value::Title(i) => i.into(),
Value::FmtString(i) => i.into(),
Value::Text(i) => Yaml::String(i.clone()),
Value::Integer(i) => Yaml::Integer(*i),
Value::Date(i) => (*i).into(),
Value::Persons(i) => persons_into_yaml(i),
Value::PersonsWithRoles(i) => affiliateds_into_yaml(i),
Value::IntegerOrText(i) => i.into(),
Value::Range(i) => range_into_yaml(i),
Value::Duration(i) => (*i).into(),
Value::TimeRange(i) => time_range_into_yaml(i.clone()),
Value::Url(i) => i.into(),
Value::Language(i) => language_into_yaml(i),
Value::Entries(i) => Yaml::Array(i.iter().map(Into::into).collect()),
};
hm.insert(Yaml::String(s.clone()), content);
}
Yaml::Hash(hm)
}
}
pub fn to_yaml<'a>(entries: impl IntoIterator<Item = &'a Entry>) -> Yaml {
let mut items = LinkedHashMap::new();
for entry in entries {
items.insert(Yaml::String(entry.key().into()), entry.into());
}
Yaml::Hash(items)
}
pub fn to_yaml_str<'a>(entries: impl IntoIterator<Item = &'a Entry>) -> Option<String> {
let mut out_str = String::new();
let mut emitter = YamlEmitter::new(&mut out_str);
emitter.dump(&to_yaml(entries)).ok()?;
Some(out_str)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
#[test]
fn person_initials() {
let contents = fs::read_to_string("tests/basic.yml").unwrap();
let entries = from_yaml_str(&contents).unwrap();
let yaml = to_yaml_str(&entries).unwrap();
let reconstructed = from_yaml_str(&yaml).unwrap();
assert_eq!(entries.len(), reconstructed.len());
for entry in entries {
let match_e = reconstructed.iter().find(|x| x.key() == entry.key()).unwrap();
assert_eq!(match_e, &entry);
}
}
}