cobalt/cobalt_model/
site.rs

1use 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    /// The time at which the `cobalt` binary built the site
28    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    // now find the nested map it is supposed to be in
102    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}