use std::fs::File;
use std::path::{Path, PathBuf};
use chrono::FixedOffset;
use crate::error::SourceError;
use crate::model::index::Syndicate;
use super::index::{self, Index};
use super::yaml::{self, Yaml};
use super::{DateTime, Meta};
#[derive(Debug, Clone)]
pub struct Entry<EntryMeta>
where
EntryMeta: Meta,
{
pub title: String,
pub description: Option<String>,
pub date: Option<DateTime>,
pub updated: DateTime,
pub index: Option<Index>,
pub cc: Vec<String>,
pub meta: EntryMeta,
pub name: String,
pub content: String,
pub format: String, }
fn parse_datetime(date: &str) -> Result<DateTime, SourceError> {
Ok(if date.contains([' ', 't', 'T']) {
chrono::DateTime::<FixedOffset>::parse_from_rfc3339(date)
.map_err(|e| format!("invalid date: {e}"))?
.to_utc()
} else {
let date = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
.map_err(|e| format!("invalid date: {e}"))?;
let datetime = chrono::NaiveDateTime::new(date, chrono::NaiveTime::default());
chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc)
})
}
impl<EntryMeta> Entry<EntryMeta>
where
EntryMeta: Meta,
{
pub fn from_file<P>(full_path: P, name: &str) -> Result<Self, SourceError>
where
P: AsRef<Path>,
{
Entry::_from_file(full_path.as_ref(), name)
}
fn _from_file(full_path: &Path, name: &str) -> Result<Self, SourceError> {
const U32_MAX_AS_I64: i64 = u32::MAX as i64;
fn resolve_path(base: &str, path: &str) -> Result<PathBuf, SourceError> {
use std::path::Component::*;
let path: &Path = path.as_ref();
let resolved = match path.components().next() {
Some(Prefix(_) | RootDir) => {
return Err(SourceError::Config(
format!("index for {base} specifies an absolute directory").into(),
));
}
None => {
return Err(SourceError::Config(
format!("index for {base} specifies an empty directory").into(),
));
}
Some(CurDir | ParentDir) => Path::new(base).join(path),
_ => path.into(),
};
let mut normalized = PathBuf::new();
for component in resolved.components() {
match component {
Prefix(_) | RootDir => {
return Err(SourceError::Config(
format!("index for {base} specifies an absolute directory").into(),
));
}
CurDir => (),
ParentDir => {
if !normalized.pop() {
return Err(SourceError::Config(
format!(
"index for {base} specifies an invalid relative path: {}",
resolved.display(),
)
.into(),
));
}
}
Normal(c) => normalized.push(c),
}
}
if normalized.components().next().is_none() {
return Err(SourceError::Config(
format!("index for {base} specifies an empty directory").into(),
));
}
Ok(normalized)
}
let entry_file = File::open(full_path)?;
let (mut meta, content) = yaml::load_front(&entry_file)?;
let format = full_path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_owned();
let title = match meta.remove(&yaml::KEYS.title) {
Some(Yaml::String(title)) => title,
Some(..) => return Err("titles must be strings".into()),
None => return Err("entries must have titles".into()),
};
let description = match meta.remove(&yaml::KEYS.description) {
Some(Yaml::String(desc)) => Some(desc),
None => None,
Some(..) => return Err("invalid description type".into()),
};
let date = match meta.remove(&yaml::KEYS.date) {
Some(Yaml::String(date)) => Some(parse_datetime(&date)?),
Some(..) => return Err("date must be a string".into()),
None => None,
};
let updated = match meta.remove(&yaml::KEYS.updated) {
Some(Yaml::String(date)) => parse_datetime(&date)?,
Some(..) => return Err("date must be a string".into()),
None => match date {
Some(d) => d,
None => entry_file.metadata()?.modified()?.into(),
},
};
let index = match meta.remove(&yaml::KEYS.index) {
Some(Yaml::Boolean(b)) => b.then(|| Index {
paginate: None,
max: None,
compact: false,
sort: index::Sort::default(),
directories: vec![name.into()],
syndicate: None,
}),
Some(Yaml::String(dir)) => Some(Index {
paginate: None,
max: None,
compact: false,
sort: index::Sort::default(),
directories: vec![resolve_path(name, &dir)?],
syndicate: None,
}),
Some(Yaml::Array(array)) => Some(Index {
paginate: None,
max: None,
compact: false,
sort: index::Sort::default(),
directories: array
.into_iter()
.map(|i| match i {
Yaml::String(dir) => resolve_path(name, &dir),
_ => Err(SourceError::from("index directories must be strings")),
})
.collect::<Result<_, _>>()?,
syndicate: None,
}),
Some(Yaml::Hash(mut index)) => Some(Index {
paginate: match index.remove(&yaml::KEYS.paginate) {
Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(i as u32),
Some(Yaml::Boolean(false)) | None => None,
Some(..) => return Err("invalid pagination setting".into()),
},
max: match index.remove(&yaml::KEYS.max) {
Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(i as u32),
Some(Yaml::Boolean(false)) | None => None,
Some(..) => return Err("invalid max setting".into()),
},
syndicate: match index.remove(&yaml::KEYS.syndicate) {
Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(Syndicate {
max: Some(i as u32),
}),
Some(Yaml::Boolean(true)) => Some(Syndicate { max: None }),
Some(Yaml::Boolean(false)) | None => None,
Some(..) => return Err("invalid pagination setting".into()),
},
compact: match index.remove(&yaml::KEYS.compact) {
Some(Yaml::Boolean(b)) => b,
None => false,
Some(..) => return Err("invalid compact setting".into()),
},
sort: match index.remove(&yaml::KEYS.sort) {
Some(Yaml::String(key)) => key.parse()?,
Some(..) => return Err("invalid sort value".into()),
None => index::Sort::default(),
},
directories: match index.remove(&yaml::KEYS.directories) {
Some(Yaml::Array(array)) => array
.into_iter()
.map(|i| match i {
Yaml::String(dir) => resolve_path(name, &dir),
_ => Err(SourceError::from(
"index directories must be \
strings",
)),
})
.collect::<Result<_, _>>()?,
Some(Yaml::String(dir)) => vec![resolve_path(name, &dir)?],
Some(..) => return Err("invalid directory list in index".into()),
None => vec![name.into()],
},
}),
Some(..) => return Err("invalid index value".into()),
None => None,
};
let cc = match meta.remove(&yaml::KEYS.cc) {
Some(Yaml::String(cc)) => vec![cc],
Some(Yaml::Array(cc)) => cc
.into_iter()
.map(|v| match v {
Yaml::String(ci) => Ok(ci),
_ => Err(SourceError::from("invlaid cc value")),
})
.collect::<Result<_, _>>()?,
Some(..) => return Err("invalid cc value".into()),
None => Vec::new(),
};
let meta = EntryMeta::from_yaml(meta)?;
let name = name.to_owned();
Ok(Entry {
title,
description,
date,
updated,
index,
cc,
meta,
name,
content,
format,
})
}
}
#[derive(Debug, Clone)]
pub struct StaticEntry {
pub name: String,
pub source: PathBuf,
}