use std::ffi::OsStr;
use std::fs;
use std::path;
use anyhow::Context as _;
use cobalt_config::DateTime;
use liquid;
use log::debug;
use serde::{Deserialize, Serialize};
use serde_json;
use serde_yaml;
use toml;
use crate::error::*;
use super::files;
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields, default)]
pub struct Site {
pub title: Option<liquid::model::KString>,
pub description: Option<liquid::model::KString>,
pub base_url: Option<liquid::model::KString>,
pub sitemap: Option<cobalt_config::RelPath>,
pub data: Option<liquid::Object>,
pub data_dir: &'static str,
pub time: DateTime,
}
impl Site {
pub fn from_config(config: cobalt_config::Site) -> Self {
let cobalt_config::Site {
title,
description,
base_url,
sitemap,
data,
data_dir,
} = config;
let base_url = base_url.map(|mut l| {
if l.ends_with('/') {
let mut other = String::from(l.as_str());
other.pop();
l = liquid::model::KString::from_string(other);
}
l
});
Self {
title,
description,
base_url,
sitemap,
data,
data_dir,
time: DateTime::now(),
}
}
pub fn load(&self, source: &std::path::Path) -> Result<liquid::Object> {
let mut attributes = liquid::Object::new();
if let Some(title) = self.title.as_ref() {
attributes.insert(
"title".into(),
liquid::model::Value::scalar(liquid::model::KString::from_ref(title)),
);
}
if let Some(description) = self.description.as_ref() {
attributes.insert(
"description".into(),
liquid::model::Value::scalar(liquid::model::KString::from_ref(description)),
);
}
if let Some(base_url) = self.base_url.as_ref() {
attributes.insert(
"base_url".into(),
liquid::model::Value::scalar(liquid::model::KString::from_ref(base_url)),
);
}
attributes.insert("time".into(), liquid::model::Value::scalar(self.time));
let mut data = self.data.clone().unwrap_or_default();
let data_path = source.join(&self.data_dir);
insert_data_dir(&mut data, &data_path)?;
if !data.is_empty() {
attributes.insert("data".into(), liquid::model::Value::Object(data));
}
Ok(attributes)
}
}
fn deep_insert(
data_map: &mut liquid::Object,
file_path: &path::Path,
target_key: String,
data: liquid::model::Value,
) -> Result<()> {
let target_map = if let Some(path) = file_path.parent() {
let mut map = data_map;
for part in path.iter() {
let key = part.to_str().ok_or_else(|| {
anyhow::format_err!(
"The data from {:?} can't be loaded as it contains non utf-8 characters",
path
)
})?;
let cur_map = map;
let key = liquid::model::KString::from_ref(key);
map = cur_map
.entry(key)
.or_insert_with(|| liquid::model::Value::Object(liquid::Object::new()))
.as_object_mut()
.ok_or_else(|| {
anyhow::format_err!(
"Aborting: Duplicate in data tree. Would overwrite {:?} ",
path
)
})?;
}
map
} else {
data_map
};
match target_map.insert(target_key.into(), data) {
None => Ok(()),
_ => Err(anyhow::format_err!(
"The data from {:?} can't be loaded: the key already exists",
file_path
)),
}
}
fn load_data(data_path: &path::Path) -> Result<liquid::model::Value> {
let ext = data_path.extension().unwrap_or_else(|| OsStr::new(""));
let data: liquid::model::Value;
if ext == OsStr::new("yml") || ext == OsStr::new("yaml") {
let reader = fs::File::open(data_path)?;
data = serde_yaml::from_reader(reader)?;
} else if ext == OsStr::new("json") {
let reader = fs::File::open(data_path)?;
data = serde_json::from_reader(reader)?;
} else if ext == OsStr::new("toml") {
let text = files::read_file(data_path)?;
data = toml::from_str(&text)?;
} else {
anyhow::bail!(
"Failed to load of data {:?}: unknown file type '{:?}'.\n\
Supported data files extensions are: yml, yaml, json and toml.",
data_path,
ext
);
}
Ok(data)
}
fn insert_data_dir(data: &mut liquid::Object, data_root: &path::Path) -> Result<()> {
debug!("Loading data from `{}`", data_root.display());
let data_files_builder = files::FilesBuilder::new(data_root)?;
let data_files = data_files_builder.build()?;
for full_path in data_files.files() {
let rel_path = full_path
.strip_prefix(data_root)
.expect("file was found under the root");
let file_stem = full_path
.file_stem()
.expect("Files will always return with a stem");
let file_stem = String::from(file_stem.to_str().unwrap());
let data_fragment = load_data(&full_path)
.with_context(|| format!("Loading data from `{}` failed", full_path.display()))?;
deep_insert(data, rel_path, file_stem, data_fragment)
.with_context(|| format!("Merging data into `{}` failed", rel_path.display()))?;
}
Ok(())
}