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}