cobalt/cobalt_model/
site.rs1use std::ffi::OsStr;
2use std::fs;
3use std::path;
4
5use anyhow::Context as _;
6use cobalt_config::DateTime;
7use liquid;
8use log::debug;
9use serde::{Deserialize, Serialize};
10use serde_json;
11use serde_yaml;
12use toml;
13
14use crate::error::Result;
15
16use super::files;
17
18#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
19#[serde(deny_unknown_fields, default)]
20pub struct Site {
21 pub title: Option<liquid::model::KString>,
22 pub description: Option<liquid::model::KString>,
23 pub base_url: Option<liquid::model::KString>,
24 pub sitemap: Option<cobalt_config::RelPath>,
25 pub data: Option<liquid::Object>,
26 pub data_dir: &'static str,
27 pub time: DateTime,
29}
30
31impl Site {
32 pub fn from_config(config: cobalt_config::Site) -> Self {
33 let cobalt_config::Site {
34 title,
35 description,
36 base_url,
37 sitemap,
38 data,
39 data_dir,
40 } = config;
41
42 let base_url = base_url.map(|mut l| {
43 if l.ends_with('/') {
44 let mut other = String::from(l.as_str());
45 other.pop();
46 l = liquid::model::KString::from_string(other);
47 }
48 l
49 });
50
51 Self {
52 title,
53 description,
54 base_url,
55 sitemap,
56 data,
57 data_dir,
58 time: DateTime::now(),
59 }
60 }
61
62 pub fn load(&self, source: &path::Path) -> Result<liquid::Object> {
63 let mut attributes = liquid::Object::new();
64 if let Some(title) = self.title.as_ref() {
65 attributes.insert(
66 "title".into(),
67 liquid::model::Value::scalar(liquid::model::KString::from_ref(title)),
68 );
69 }
70 if let Some(description) = self.description.as_ref() {
71 attributes.insert(
72 "description".into(),
73 liquid::model::Value::scalar(liquid::model::KString::from_ref(description)),
74 );
75 }
76 if let Some(base_url) = self.base_url.as_ref() {
77 attributes.insert(
78 "base_url".into(),
79 liquid::model::Value::scalar(liquid::model::KString::from_ref(base_url)),
80 );
81 }
82 attributes.insert("time".into(), liquid::model::Value::scalar(self.time));
83
84 let mut data = self.data.clone().unwrap_or_default();
85 let data_path = source.join(self.data_dir);
86 insert_data_dir(&mut data, &data_path)?;
87 if !data.is_empty() {
88 attributes.insert("data".into(), liquid::model::Value::Object(data));
89 }
90
91 Ok(attributes)
92 }
93}
94
95fn deep_insert(
96 data_map: &mut liquid::Object,
97 file_path: &path::Path,
98 target_key: String,
99 data: liquid::model::Value,
100) -> Result<()> {
101 let target_map = if let Some(path) = file_path.parent() {
103 let mut map = data_map;
104 for part in path.iter() {
105 let key = part.to_str().ok_or_else(|| {
106 anyhow::format_err!(
107 "The data from `{}` can't be loaded as it contains non utf-8 characters",
108 path.display()
109 )
110 })?;
111 let cur_map = map;
112 let key = liquid::model::KString::from_ref(key);
113 map = cur_map
114 .entry(key)
115 .or_insert_with(|| liquid::model::Value::Object(liquid::Object::new()))
116 .as_object_mut()
117 .ok_or_else(|| {
118 anyhow::format_err!(
119 "Aborting: Duplicate in data tree. Would overwrite {} ",
120 path.display()
121 )
122 })?;
123 }
124 map
125 } else {
126 data_map
127 };
128
129 match target_map.insert(target_key.into(), data) {
130 None => Ok(()),
131 _ => Err(anyhow::format_err!(
132 "The data from {} can't be loaded: the key already exists",
133 file_path.display()
134 )),
135 }
136}
137
138fn load_data(data_path: &path::Path) -> Result<liquid::model::Value> {
139 let ext = data_path.extension().unwrap_or_else(|| OsStr::new(""));
140
141 let data: liquid::model::Value;
142
143 if ext == OsStr::new("yml") || ext == OsStr::new("yaml") {
144 let reader = fs::File::open(data_path)?;
145 data = serde_yaml::from_reader(reader)?;
146 } else if ext == OsStr::new("json") {
147 let reader = fs::File::open(data_path)?;
148 data = serde_json::from_reader(reader)?;
149 } else if ext == OsStr::new("toml") {
150 let text = files::read_file(data_path)?;
151 data = toml::from_str(&text)?;
152 } else {
153 anyhow::bail!(
154 "Failed to load of data `{}`: unknown file type '{:?}'.\n\
155 Supported data files extensions are: yml, yaml, json and toml.",
156 data_path.display(),
157 ext
158 );
159 }
160
161 Ok(data)
162}
163
164fn insert_data_dir(data: &mut liquid::Object, data_root: &path::Path) -> Result<()> {
165 debug!("Loading data from `{}`", data_root.display());
166
167 let data_files_builder = files::FilesBuilder::new(data_root)?;
168 let data_files = data_files_builder.build()?;
169 for full_path in data_files.files() {
170 let rel_path = full_path
171 .strip_prefix(data_root)
172 .expect("file was found under the root");
173
174 let file_stem = full_path
175 .file_stem()
176 .expect("Files will always return with a stem");
177 let file_stem = String::from(file_stem.to_str().unwrap());
178 let data_fragment = load_data(&full_path)
179 .with_context(|| format!("Loading data from `{}` failed", full_path.display()))?;
180
181 deep_insert(data, rel_path, file_stem, data_fragment)
182 .with_context(|| format!("Merging data into `{}` failed", rel_path.display()))?;
183 }
184
185 Ok(())
186}