archetect-core 0.7.3

Generates Content from Archetype Template Directories and Git Repositories.
Documentation
use std::collections::HashSet;
use std::fs;
use std::fs::File;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::rc::Rc;

use clap::crate_version;
use log::{debug, trace};
use semver::Version;

use crate::config::RuleAction;
use crate::rules::RulesContext;
use crate::system::{dot_home_layout, LayoutType, NativeSystemLayout, SystemLayout};
use crate::system::SystemError;
use crate::source::Source;
use crate::vendor::tera::{Context, Tera};
use crate::{ArchetectError, Archetype, ArchetypeError, RenderError};

pub struct Archetect {
    tera: Tera,
    paths: Rc<Box<dyn SystemLayout>>,
    offline: bool,
    headless: bool,
    switches: HashSet<String>,
}

impl Archetect {
    pub fn layout(&self) -> Rc<Box<dyn SystemLayout>> {
        self.paths.clone()
    }

    pub fn offline(&self) -> bool {
        self.offline
    }

    pub fn headless(&self) -> bool {
        self.headless
    }

    pub fn builder() -> ArchetectBuilder {
        ArchetectBuilder::new()
    }

    pub fn build() -> Result<Archetect, ArchetectError> {
        ArchetectBuilder::new().build()
    }

    pub fn template_engine(&self) -> &Tera {
        &self.tera
    }

    pub fn enable_switch<S: Into<String>>(&mut self, switch: S) {
        self.switches.insert(switch.into());
    }

    pub fn switches(&self) -> &HashSet<String> {
        &self.switches
    }

    pub fn load_archetype(&self, source: &str, relative_to: Option<Source>) -> Result<Archetype, ArchetypeError> {
        let source = Source::detect(self, source, relative_to)?;
        let archetype = Archetype::from_source(&source)?;
        Ok(archetype)
    }

    pub fn render_string(&mut self, template: &str, context: &Context) -> Result<String, RenderError> {
        match self.tera.render_str(template, &context.clone()) {
            Ok(result) => Ok(result),
            Err(err) => {
                Err(RenderError::StringRenderError {
                    string: template.to_owned(),
                    source: err,
                })
            }
        }
    }

    pub fn render_contents<P: AsRef<Path>>(&mut self, path: P, context: &Context) -> Result<String, RenderError> {
        let path = path.as_ref();
        let template = match fs::read_to_string(path) {
            Ok(template) => template,
            Err(error) => {
                return Err(RenderError::FileRenderIOError {
                    path: path.to_owned(),
                    source: error,
                });
            }
        };
        match self.tera.render_str(&template, &context.clone()) {
            Ok(result) => Ok(result),
            Err(error) => {
                Err(RenderError::FileRenderError {
                    path: path.into(),
                    source: error,
                })
            }
        }
    }

    pub fn render_directory<SRC: Into<PathBuf>, DEST: Into<PathBuf>>(
        &mut self,
        context: &Context,
        source: SRC,
        destination: DEST,
        rules_context: &mut RulesContext,
    ) -> Result<(), RenderError> {
        let source = source.into();
        let destination = destination.into();

        for entry in fs::read_dir(&source)? {
            let entry = entry?;
            let path = entry.path();

            let action = rules_context.get_source_action(path.as_path());

            if path.is_dir() {
                let destination = self.render_destination(&destination, &path, &context)?;
                debug!("Rendering   {:?}", &destination);
                fs::create_dir_all(destination.as_path())?;
                self.render_directory(context, path, destination, rules_context)?;
            } else if path.is_file() {
                let destination = self.render_destination(&destination, &path, &context)?;
                match action {
                    RuleAction::RENDER => {
                        if !destination.exists() {
                            debug!("Rendering   {:?}", destination);
                            let contents = self.render_contents(&path, &context)?;
                            self.write_contents(destination, &contents)?;
                        } else if rules_context.overwrite() {
                            debug!("Overwriting {:?}", destination);
                            let contents = self.render_contents(&path, &context)?;
                            self.write_contents(destination, &contents)?;
                        } else {
                            trace!("Preserving  {:?}", destination);
                        }
                    }
                    RuleAction::COPY => {
                        debug!("Copying     {:?}", destination);
                        self.copy_contents(&path, &destination)?;
                    }
                    RuleAction::SKIP => {
                        trace!("Skipping    {:?}", destination);
                    }
                }
            }
        }

        Ok(())
    }

    fn render_destination<P: AsRef<Path>, C: AsRef<Path>>(
        &mut self,
        parent: P,
        child: C,
        context: &Context,
    ) -> Result<PathBuf, RenderError> {
        let mut destination = parent.as_ref().to_owned();
        let child = child.as_ref();
        let name = self.render_path(&child, &context)?;
        destination.push(name);
        Ok(destination)
    }

    fn render_path<P: AsRef<Path>>(&mut self, path: P, context: &Context) -> Result<String, RenderError> {
        let path = path.as_ref();
        let filename = path.file_name().unwrap_or(path.as_os_str()).to_str().unwrap();
        match self.tera.render_str(filename, &context.clone()) {
            Ok(result) => Ok(result),
            Err(error) => {
                Err(RenderError::PathRenderError {
                    path: path.into(),
                    source: error,
                })
            }
        }
    }

    pub fn write_contents<P: AsRef<Path>>(&self, destination: P, contents: &str) -> Result<(), RenderError> {
        let destination = destination.as_ref();
        let mut output = File::create(&destination)?;
        output.write(contents.as_bytes())?;
        Ok(())
    }

    pub fn copy_contents<S: AsRef<Path>, D: AsRef<Path>>(&self, source: S, destination: D) -> Result<(), RenderError> {
        let source = source.as_ref();
        let destination = destination.as_ref();
        fs::copy(source, destination)?;
        Ok(())
    }

    pub fn version(&self) -> Version {
        Version::parse(crate_version!()).unwrap()
    }
}

pub struct ArchetectBuilder {
    layout: Option<Box<dyn SystemLayout>>,
    offline: bool,
    headless: bool,
    switches: HashSet<String>,
}

impl ArchetectBuilder {
    fn new() -> ArchetectBuilder {
        ArchetectBuilder {
            layout: None,
            offline: false,
            headless: false,
            switches: HashSet::new(),
        }
    }

    pub fn build(self) -> Result<Archetect, ArchetectError> {
        let layout = dot_home_layout()?;
        let paths = self.layout.unwrap_or_else(|| Box::new(layout));
        let paths = Rc::new(paths);

        Ok(Archetect {
            tera: crate::vendor::tera::extensions::create_tera(),
            paths,
            offline: self.offline,
            headless: self.headless,
            switches: self.switches,
        })
    }

    pub fn with_layout<P: SystemLayout + 'static>(mut self, layout: P) -> ArchetectBuilder {
        self.layout = Some(Box::new(layout));
        self
    }

    pub fn with_layout_type(self, layout: LayoutType) -> Result<ArchetectBuilder, SystemError> {
        let builder = match layout {
            LayoutType::Native => self.with_layout(NativeSystemLayout::new()?),
            LayoutType::DotHome => self.with_layout(dot_home_layout()?),
            LayoutType::Temp => self.with_layout(crate::system::temp_layout()?),
        };
        Ok(builder)
    }

    pub fn with_offline(mut self, offline: bool) -> ArchetectBuilder {
        self.offline = offline;
        self
    }

    pub fn with_headless(mut self, headless: bool) -> ArchetectBuilder {
        self.headless = headless;
        self
    }
}

#[cfg(test)]
mod tests {
    use crate::system::{NativeSystemLayout, RootedSystemLayout};

    use super::*;

    #[test]
    fn test_explicit_native_paths() {
        let archetect = Archetect::builder()
            .with_layout(NativeSystemLayout::new().unwrap())
            .build()
            .unwrap();

        println!("{}", archetect.layout().catalog_cache_dir().display());
    }

    #[test]
    fn test_explicit_directory_paths() {
        let paths = RootedSystemLayout::new("~/.archetect/").unwrap();
        let archetect = Archetect::builder().with_layout(paths).build().unwrap();

        println!("{}", archetect.layout().catalog_cache_dir().display());
    }

    #[test]
    fn test_implicit() {
        let archetect = Archetect::build().unwrap();

        println!("{}", archetect.layout().catalog_cache_dir().display());

        std::fs::create_dir_all(archetect.layout().configs_dir()).expect("Error creating directory");
        std::fs::create_dir_all(archetect.layout().git_cache_dir()).expect("Error creating directory");
    }

    mod templating {
        use crate::Archetect;
        use crate::vendor::tera::Context;

        #[test]
        fn test_truncate_filter() {
            let mut archetect = Archetect::build().unwrap();
            let template = "{{ 'Jimmie' | truncate(length=1, end='') }}";
            let result = archetect.render_string(template, &Context::new()).unwrap();
            assert_eq!(&result, "J");
        }
    }
}