1use std::fs::File;
18use std::path::{Path, PathBuf};
19
20use chrono::FixedOffset;
21
22use crate::error::SourceError;
23use crate::model::index::Syndicate;
24
25use super::index::{self, Index};
26use super::yaml::{self, Yaml};
27use super::{DateTime, Meta};
28
29#[derive(Debug, Clone)]
33pub struct Entry<EntryMeta>
34where
35 EntryMeta: Meta,
36{
37 pub title: String,
42
43 pub description: Option<String>,
47
48 pub date: Option<DateTime>,
52
53 pub updated: DateTime,
58
59 pub index: Option<Index>,
61
62 pub cc: Vec<String>,
64
65 pub meta: EntryMeta,
67
68 pub name: String,
70
71 pub content: String,
73
74 pub format: String, }
77
78fn parse_datetime(date: &str) -> Result<DateTime, SourceError> {
79 Ok(if date.contains([' ', 't', 'T']) {
80 chrono::DateTime::<FixedOffset>::parse_from_rfc3339(date)
81 .map_err(|e| format!("invalid date: {e}"))?
82 .to_utc()
83 } else {
84 let date = chrono::NaiveDate::parse_from_str(date, "%Y-%m-%d")
85 .map_err(|e| format!("invalid date: {e}"))?;
86 let datetime = chrono::NaiveDateTime::new(date, chrono::NaiveTime::default());
87 chrono::DateTime::from_naive_utc_and_offset(datetime, chrono::Utc)
88 })
89}
90
91impl<EntryMeta> Entry<EntryMeta>
92where
93 EntryMeta: Meta,
94{
95 pub fn from_file<P>(full_path: P, name: &str) -> Result<Self, SourceError>
96 where
97 P: AsRef<Path>,
98 {
99 Entry::_from_file(full_path.as_ref(), name)
100 }
101
102 fn _from_file(full_path: &Path, name: &str) -> Result<Self, SourceError> {
103 const U32_MAX_AS_I64: i64 = u32::MAX as i64;
105
106 fn resolve_path(base: &str, path: &str) -> Result<PathBuf, SourceError> {
107 use std::path::Component::*;
108 let path: &Path = path.as_ref();
109 let resolved = match path.components().next() {
110 Some(Prefix(_) | RootDir) => {
111 return Err(SourceError::Config(
112 format!("index for {base} specifies an absolute directory").into(),
113 ));
114 }
115 None => {
116 return Err(SourceError::Config(
117 format!("index for {base} specifies an empty directory").into(),
118 ));
119 }
120 Some(CurDir | ParentDir) => Path::new(base).join(path),
121 _ => path.into(),
122 };
123 let mut normalized = PathBuf::new();
124 for component in resolved.components() {
125 match component {
126 Prefix(_) | RootDir => {
127 return Err(SourceError::Config(
128 format!("index for {base} specifies an absolute directory").into(),
129 ));
130 }
131 CurDir => (),
132 ParentDir => {
133 if !normalized.pop() {
134 return Err(SourceError::Config(
135 format!(
136 "index for {base} specifies an invalid relative path: {}",
137 resolved.display(),
138 )
139 .into(),
140 ));
141 }
142 }
143 Normal(c) => normalized.push(c),
144 }
145 }
146 if normalized.components().next().is_none() {
147 return Err(SourceError::Config(
148 format!("index for {base} specifies an empty directory").into(),
149 ));
150 }
151 Ok(normalized)
152 }
153
154 let entry_file = File::open(full_path)?;
157 let (mut meta, content) = yaml::load_front(&entry_file)?;
158
159 let format = full_path
160 .extension()
161 .and_then(|e| e.to_str())
162 .unwrap_or("")
163 .to_owned();
164 let title = match meta.remove(&yaml::KEYS.title) {
165 Some(Yaml::String(title)) => title,
166 Some(..) => return Err("titles must be strings".into()),
167 None => return Err("entries must have titles".into()),
168 };
169 let description = match meta.remove(&yaml::KEYS.description) {
170 Some(Yaml::String(desc)) => Some(desc),
171 None => None,
172 Some(..) => return Err("invalid description type".into()),
173 };
174 let date = match meta.remove(&yaml::KEYS.date) {
175 Some(Yaml::String(date)) => Some(parse_datetime(&date)?),
176 Some(..) => return Err("date must be a string".into()),
177 None => None,
178 };
179 let updated = match meta.remove(&yaml::KEYS.updated) {
180 Some(Yaml::String(date)) => parse_datetime(&date)?,
181 Some(..) => return Err("date must be a string".into()),
182 None => match date {
183 Some(d) => d,
184 None => entry_file.metadata()?.modified()?.into(),
185 },
186 };
187 let index = match meta.remove(&yaml::KEYS.index) {
188 Some(Yaml::Boolean(b)) => b.then(|| Index {
189 paginate: None,
190 max: None,
191 compact: false,
192 sort: index::Sort::default(),
193 directories: vec![name.into()],
194 syndicate: None,
195 }),
196 Some(Yaml::String(dir)) => Some(Index {
197 paginate: None,
198 max: None,
199 compact: false,
200 sort: index::Sort::default(),
201 directories: vec![resolve_path(name, &dir)?],
202 syndicate: None,
203 }),
204 Some(Yaml::Array(array)) => Some(Index {
205 paginate: None,
206 max: None,
207 compact: false,
208 sort: index::Sort::default(),
209 directories: array
210 .into_iter()
211 .map(|i| match i {
212 Yaml::String(dir) => resolve_path(name, &dir),
213 _ => Err(SourceError::from("index directories must be strings")),
214 })
215 .collect::<Result<_, _>>()?,
216 syndicate: None,
217 }),
218 Some(Yaml::Hash(mut index)) => Some(Index {
219 paginate: match index.remove(&yaml::KEYS.paginate) {
220 Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(i as u32),
221 Some(Yaml::Boolean(false)) | None => None,
222 Some(..) => return Err("invalid pagination setting".into()),
223 },
224 max: match index.remove(&yaml::KEYS.max) {
225 Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(i as u32),
226 Some(Yaml::Boolean(false)) | None => None,
227 Some(..) => return Err("invalid max setting".into()),
228 },
229 syndicate: match index.remove(&yaml::KEYS.syndicate) {
230 Some(Yaml::Integer(i @ 1..=U32_MAX_AS_I64)) => Some(Syndicate {
231 max: Some(i as u32),
232 }),
233 Some(Yaml::Boolean(true)) => Some(Syndicate { max: None }),
234 Some(Yaml::Boolean(false)) | None => None,
235 Some(..) => return Err("invalid pagination setting".into()),
236 },
237 compact: match index.remove(&yaml::KEYS.compact) {
238 Some(Yaml::Boolean(b)) => b,
239 None => false,
240 Some(..) => return Err("invalid compact setting".into()),
241 },
242 sort: match index.remove(&yaml::KEYS.sort) {
243 Some(Yaml::String(key)) => key.parse()?,
244 Some(..) => return Err("invalid sort value".into()),
245 None => index::Sort::default(),
246 },
247 directories: match index.remove(&yaml::KEYS.directories) {
248 Some(Yaml::Array(array)) => array
249 .into_iter()
250 .map(|i| match i {
251 Yaml::String(dir) => resolve_path(name, &dir),
252 _ => Err(SourceError::from(
253 "index directories must be \
254 strings",
255 )),
256 })
257 .collect::<Result<_, _>>()?,
258 Some(Yaml::String(dir)) => vec![resolve_path(name, &dir)?],
259 Some(..) => return Err("invalid directory list in index".into()),
260 None => vec![name.into()],
261 },
262 }),
263 Some(..) => return Err("invalid index value".into()),
264 None => None,
265 };
266 let cc = match meta.remove(&yaml::KEYS.cc) {
267 Some(Yaml::String(cc)) => vec![cc],
268 Some(Yaml::Array(cc)) => cc
269 .into_iter()
270 .map(|v| match v {
271 Yaml::String(ci) => Ok(ci),
272 _ => Err(SourceError::from("invlaid cc value")),
273 })
274 .collect::<Result<_, _>>()?,
275 Some(..) => return Err("invalid cc value".into()),
276 None => Vec::new(),
277 };
278 let meta = EntryMeta::from_yaml(meta)?;
279 let name = name.to_owned();
280
281 Ok(Entry {
282 title,
283 description,
284 date,
285 updated,
286 index,
287 cc,
288 meta,
289 name,
290 content,
291 format,
292 })
293 }
294}
295
296#[derive(Debug, Clone)]
300pub struct StaticEntry {
301 pub name: String,
302 pub source: PathBuf,
303}