archetect_core/
core.rs

1use std::collections::HashSet;
2use std::fs;
3use std::fs::File;
4use std::io::Write;
5use std::path::{Path, PathBuf};
6use std::rc::Rc;
7
8use clap::crate_version;
9use log::{debug, trace};
10use semver::Version;
11
12use crate::config::RuleAction;
13use crate::rules::RulesContext;
14use crate::system::{dot_home_layout, LayoutType, NativeSystemLayout, SystemLayout};
15use crate::system::SystemError;
16use crate::source::Source;
17use crate::vendor::tera::{Context, Tera};
18use crate::{ArchetectError, Archetype, ArchetypeError, RenderError};
19
20pub struct Archetect {
21    tera: Tera,
22    paths: Rc<Box<dyn SystemLayout>>,
23    offline: bool,
24    headless: bool,
25    switches: HashSet<String>,
26}
27
28impl Archetect {
29    pub fn layout(&self) -> Rc<Box<dyn SystemLayout>> {
30        self.paths.clone()
31    }
32
33    pub fn offline(&self) -> bool {
34        self.offline
35    }
36
37    pub fn headless(&self) -> bool {
38        self.headless
39    }
40
41    pub fn builder() -> ArchetectBuilder {
42        ArchetectBuilder::new()
43    }
44
45    pub fn build() -> Result<Archetect, ArchetectError> {
46        ArchetectBuilder::new().build()
47    }
48
49    pub fn template_engine(&self) -> &Tera {
50        &self.tera
51    }
52
53    pub fn enable_switch<S: Into<String>>(&mut self, switch: S) {
54        self.switches.insert(switch.into());
55    }
56
57    pub fn switches(&self) -> &HashSet<String> {
58        &self.switches
59    }
60
61    pub fn load_archetype(&self, source: &str, relative_to: Option<Source>) -> Result<Archetype, ArchetypeError> {
62        let source = Source::detect(self, source, relative_to)?;
63        let archetype = Archetype::from_source(&source)?;
64        Ok(archetype)
65    }
66
67    pub fn render_string(&mut self, template: &str, context: &Context) -> Result<String, RenderError> {
68        match self.tera.render_str(template, &context.clone()) {
69            Ok(result) => Ok(result),
70            Err(err) => {
71                Err(RenderError::StringRenderError {
72                    string: template.to_owned(),
73                    source: err,
74                })
75            }
76        }
77    }
78
79    pub fn render_contents<P: AsRef<Path>>(&mut self, path: P, context: &Context) -> Result<String, RenderError> {
80        let path = path.as_ref();
81        let template = match fs::read_to_string(path) {
82            Ok(template) => template,
83            Err(error) => {
84                return Err(RenderError::FileRenderIOError {
85                    path: path.to_owned(),
86                    source: error,
87                });
88            }
89        };
90        match self.tera.render_str(&template, &context.clone()) {
91            Ok(result) => Ok(result),
92            Err(error) => {
93                Err(RenderError::FileRenderError {
94                    path: path.into(),
95                    source: error,
96                })
97            }
98        }
99    }
100
101    pub fn render_directory<SRC: Into<PathBuf>, DEST: Into<PathBuf>>(
102        &mut self,
103        context: &Context,
104        source: SRC,
105        destination: DEST,
106        rules_context: &mut RulesContext,
107    ) -> Result<(), RenderError> {
108        let source = source.into();
109        let destination = destination.into();
110
111        for entry in fs::read_dir(&source)? {
112            let entry = entry?;
113            let path = entry.path();
114
115            let action = rules_context.get_source_action(path.as_path());
116
117            if path.is_dir() {
118                let destination = self.render_destination(&destination, &path, &context)?;
119                debug!("Rendering   {:?}", &destination);
120                fs::create_dir_all(destination.as_path())?;
121                self.render_directory(context, path, destination, rules_context)?;
122            } else if path.is_file() {
123                let destination = self.render_destination(&destination, &path, &context)?;
124                match action {
125                    RuleAction::RENDER => {
126                        if !destination.exists() {
127                            debug!("Rendering   {:?}", destination);
128                            let contents = self.render_contents(&path, &context)?;
129                            self.write_contents(destination, &contents)?;
130                        } else if rules_context.overwrite() {
131                            debug!("Overwriting {:?}", destination);
132                            let contents = self.render_contents(&path, &context)?;
133                            self.write_contents(destination, &contents)?;
134                        } else {
135                            trace!("Preserving  {:?}", destination);
136                        }
137                    }
138                    RuleAction::COPY => {
139                        debug!("Copying     {:?}", destination);
140                        self.copy_contents(&path, &destination)?;
141                    }
142                    RuleAction::SKIP => {
143                        trace!("Skipping    {:?}", destination);
144                    }
145                }
146            }
147        }
148
149        Ok(())
150    }
151
152    fn render_destination<P: AsRef<Path>, C: AsRef<Path>>(
153        &mut self,
154        parent: P,
155        child: C,
156        context: &Context,
157    ) -> Result<PathBuf, RenderError> {
158        let mut destination = parent.as_ref().to_owned();
159        let child = child.as_ref();
160        let name = self.render_path(&child, &context)?;
161        destination.push(name);
162        Ok(destination)
163    }
164
165    fn render_path<P: AsRef<Path>>(&mut self, path: P, context: &Context) -> Result<String, RenderError> {
166        let path = path.as_ref();
167        let filename = path.file_name().unwrap_or(path.as_os_str()).to_str().unwrap();
168        match self.tera.render_str(filename, &context.clone()) {
169            Ok(result) => Ok(result),
170            Err(error) => {
171                Err(RenderError::PathRenderError {
172                    path: path.into(),
173                    source: error,
174                })
175            }
176        }
177    }
178
179    pub fn write_contents<P: AsRef<Path>>(&self, destination: P, contents: &str) -> Result<(), RenderError> {
180        let destination = destination.as_ref();
181        let mut output = File::create(&destination)?;
182        output.write(contents.as_bytes())?;
183        Ok(())
184    }
185
186    pub fn copy_contents<S: AsRef<Path>, D: AsRef<Path>>(&self, source: S, destination: D) -> Result<(), RenderError> {
187        let source = source.as_ref();
188        let destination = destination.as_ref();
189        fs::copy(source, destination)?;
190        Ok(())
191    }
192
193    pub fn version(&self) -> Version {
194        Version::parse(crate_version!()).unwrap()
195    }
196}
197
198pub struct ArchetectBuilder {
199    layout: Option<Box<dyn SystemLayout>>,
200    offline: bool,
201    headless: bool,
202    switches: HashSet<String>,
203}
204
205impl ArchetectBuilder {
206    fn new() -> ArchetectBuilder {
207        ArchetectBuilder {
208            layout: None,
209            offline: false,
210            headless: false,
211            switches: HashSet::new(),
212        }
213    }
214
215    pub fn build(self) -> Result<Archetect, ArchetectError> {
216        let layout = dot_home_layout()?;
217        let paths = self.layout.unwrap_or_else(|| Box::new(layout));
218        let paths = Rc::new(paths);
219
220        Ok(Archetect {
221            tera: crate::vendor::tera::extensions::create_tera(),
222            paths,
223            offline: self.offline,
224            headless: self.headless,
225            switches: self.switches,
226        })
227    }
228
229    pub fn with_layout<P: SystemLayout + 'static>(mut self, layout: P) -> ArchetectBuilder {
230        self.layout = Some(Box::new(layout));
231        self
232    }
233
234    pub fn with_layout_type(self, layout: LayoutType) -> Result<ArchetectBuilder, SystemError> {
235        let builder = match layout {
236            LayoutType::Native => self.with_layout(NativeSystemLayout::new()?),
237            LayoutType::DotHome => self.with_layout(dot_home_layout()?),
238            LayoutType::Temp => self.with_layout(crate::system::temp_layout()?),
239        };
240        Ok(builder)
241    }
242
243    pub fn with_offline(mut self, offline: bool) -> ArchetectBuilder {
244        self.offline = offline;
245        self
246    }
247
248    pub fn with_headless(mut self, headless: bool) -> ArchetectBuilder {
249        self.headless = headless;
250        self
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use crate::system::{NativeSystemLayout, RootedSystemLayout};
257
258    use super::*;
259
260    #[test]
261    fn test_explicit_native_paths() {
262        let archetect = Archetect::builder()
263            .with_layout(NativeSystemLayout::new().unwrap())
264            .build()
265            .unwrap();
266
267        println!("{}", archetect.layout().catalog_cache_dir().display());
268    }
269
270    #[test]
271    fn test_explicit_directory_paths() {
272        let paths = RootedSystemLayout::new("~/.archetect/").unwrap();
273        let archetect = Archetect::builder().with_layout(paths).build().unwrap();
274
275        println!("{}", archetect.layout().catalog_cache_dir().display());
276    }
277
278    #[test]
279    fn test_implicit() {
280        let archetect = Archetect::build().unwrap();
281
282        println!("{}", archetect.layout().catalog_cache_dir().display());
283
284        std::fs::create_dir_all(archetect.layout().configs_dir()).expect("Error creating directory");
285        std::fs::create_dir_all(archetect.layout().git_cache_dir()).expect("Error creating directory");
286    }
287
288    mod templating {
289        use crate::Archetect;
290        use crate::vendor::tera::Context;
291
292        #[test]
293        fn test_truncate_filter() {
294            let mut archetect = Archetect::build().unwrap();
295            let template = "{{ 'Jimmie' | truncate(length=1, end='') }}";
296            let result = archetect.render_string(template, &Context::new()).unwrap();
297            assert_eq!(&result, "J");
298        }
299    }
300}