roxy_cli 0.1.2

A command-line static site generator
use std::{
    borrow::BorrowMut,
    fs::File,
    io::{BufReader, Read},
    path::{Path, PathBuf},
};

use crate::{file_path::FilePath, iter_ext::Head};
use roxy_core::result::Result;
use tera::{to_value, Map, Value};
use toml::Table;

fn merge(a: &mut Value, b: Value) {
    match (a, b) {
        (&mut Value::Object(ref mut a), Value::Object(b)) => {
            b.into_iter().for_each(|(k, v)| {
                merge(a.entry(k).or_insert(Value::Null), v);
            });
        }
        (a, b) => {
            *a = b;
        }
    }
}

#[derive(Clone, Debug)]
pub(crate) struct Context {
    inner: tera::Value,
}

impl Context {
    pub fn new() -> Self {
        Self {
            inner: tera::Value::Null,
        }
    }

    pub fn merge(&mut self, other: tera::Context) {
        merge(self.inner.borrow_mut(), other.into_json())
    }

    fn as_formatted_path<P: AsRef<Path>>(path: &P) -> Option<PathBuf> {
        let path = path.as_ref();
        if path.with_extension("").file_name()? == "index" {
            Some(path.with_file_name(""))
        } else {
            Some(path.with_extension(""))
        }
    }

    pub fn from_files<'a, T: AsRef<Path> + std::fmt::Debug>(
        files: Vec<&PathBuf>,
        file_path: &'a FilePath<T>,
    ) -> Result<Context> {
        let mut context = Context::new();

        for path in files {
            let mut buf = Vec::new();

            let mut file = File::open(path).map(BufReader::new)?;
            file.read_to_end(&mut buf)?;
            let mut str = String::from_utf8(buf).map_err(anyhow::Error::from)?;
            let toml: Table = toml::from_str(&mut str).map_err(anyhow::Error::from)?;

            let path = file_path.strip_root(path).map_err(anyhow::Error::from)?;

            let value = tera::to_value(&toml).map_err(anyhow::Error::from)?;
            context.insert(&path, &value);
        }

        Ok(context)
    }

    pub fn build_paths<'a, T: AsRef<Path>>(
        &mut self,
        files: &Vec<&PathBuf>,
        file_path: &'a FilePath<T>,
    ) -> Result<()> {
        for path in files {
            let path = file_path.strip_root(path).map_err(anyhow::Error::from)?;
            if let Some(path) = Self::as_formatted_path(&path) {
                if let Some(slug) = file_path.as_slug(&path) {
                    let slug = PathBuf::from("/").join(slug);
                    if let Ok(slug) = to_value(slug) {
                        self.insert(&path.join("path"), &slug);
                    }
                }
            }
        }

        Ok(())
    }

    fn path_to_string<P: AsRef<Path>>(path: &P) -> String {
        path.as_ref().to_string_lossy().to_string()
    }

    fn create_path<P: AsRef<Path>>(path: &P, value: &Value) -> Option<Value> {
        let path = path.as_ref();
        let (head, tail) = path.components().head()?;

        let mut map = Map::new();

        if tail.clone().count() > 0 {
            let child = Self::create_path(&tail, value)?;
            map.insert(Self::path_to_string(&head), child);
        } else {
            let key = Self::path_to_string(&path.with_extension("").file_name()?);
            map.insert(key, value.to_owned());
        }

        Some(map.into())
    }

    fn as_key<P: AsRef<Path>>(path: &P) -> Option<&Path> {
        let path = path.as_ref();
        if path
            .with_extension("")
            .file_name()
            .map_or(false, |f| f.to_os_string() == "index")
        {
            path.parent()
        } else {
            Some(path)
        }
    }

    pub fn remove<P: AsRef<Path>>(&mut self, path: &P) {
        if let Some(value) =  self.get_mut(path) {
            *value = tera::Value::Null;
        }
    }

    pub fn set<P: AsRef<Path>>(&mut self, path: &P, value: &Value) {
        self.remove(path);
        self.insert(path, value);
    }

    pub fn insert<P: AsRef<Path>>(&mut self, path: &P, value: &Value) {
        let path = Self::as_key(path).map(|p| p.with_extension(""));

        let path = Self::create_path(&path.unwrap(), value).or(Some(value.clone()));

        if let Some(v) = path {
            let value = tera::Context::from_value(v);
            if let Ok(ctx) = value {
                self.merge(ctx);
            }
        }
    }

    pub fn get<P: AsRef<Path>>(&self, path: &P) -> Option<&Value> {
        Self::as_key(path)?
            .with_extension("")
            .components()
            .filter_map(|c| c.as_os_str().to_str())
            .try_fold(&self.inner, |acc, i| acc.get(i))
    }

    pub fn get_mut<P: AsRef<Path>>(&mut self, path: &P) -> Option<&mut Value> {
        Self::as_key(path)?
            .with_extension("")
            .components()
            .filter_map(|c| c.as_os_str().to_str())
            .try_fold(&mut self.inner, |acc, i| acc.get_mut(i))
    }
}

impl Into<tera::Value> for Context {
    fn into(self) -> tera::Value {
        self.inner
    }
}

impl TryInto<tera::Context> for Context {
    type Error = tera::Error;

    fn try_into(self) -> std::result::Result<tera::Context, Self::Error> {
        tera::Context::from_value(self.inner)
    }
}