1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! ## guidon
//!
//! guidon performs templating based on [handlebars](https://handlebarsjs.com/) templating system.
//!
//! ## Usage
//! Files to be handles needs to have an `.hbs` extension. Folders can be templatized too : `{{folder/subfolder}}`
//! The entry point is the [Guidon](struct.Guidon.html) structure.
//!
//! ```rust, no_run
//! use guidon::Guidon;
//!
//!  let mut guidon = Guidon::with_conf("path/to/template.toml").unwrap();
//!  guidon.apply_template("path/to/template/dir", "path/to/destination").unwrap();
//!```
//!
//! ## Configuration
//! The configuration file is structured as follows :
//! ```toml
//!# Optional
//! # By default will pick the template to apply in a folder named template in the provided folder
//! use_template_dir = true
//! # Optional
//! # By default any missing substitution key will raise an error.
//! # If false, it will silently initialize the value withe an empty string.
//! use_strict_mode = true
//!
//! # Key value pairs for template substitution
//! # Each occurrence of
//! [variables]
//! test1 = "test 1"
//! test2 = "test 2"
//! ```
//!
//! ## Logs
//! guidon uses the log facade.
//!
//! ## Callbacks
//! TODO

use crate::errors::{ErrorKind, GuidonError, Result};
use handlebars::Handlebars;
use log::{debug, error, info};
use serde_derive::Deserialize;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::{copy, create_dir, create_dir_all, read_to_string, File};
use std::path::Path;

pub mod errors;

pub type VariablesCallback<'a> = dyn Fn(&mut HashMap<String, String>) + 'a;
pub type RenderCallback<'a> = dyn Fn(String) -> String + 'a;

/// The Guidon structure
#[derive(Deserialize)]
pub struct Guidon<'a> {
    #[serde(default = "default_use_template_dir")]
    use_template_dir: bool,
    #[serde(default = "default_use_strict_mode")]
    use_strict_mode: bool,
    variables: HashMap<String, String>,
    #[serde(skip_deserializing)]
    variables_callback: Option<Box<VariablesCallback<'a>>>,
    #[serde(skip_deserializing)]
    render_callback: Option<Box<RenderCallback<'a>>>,
}

fn default_use_template_dir() -> bool {
    true
}

fn default_use_strict_mode() -> bool {
    true
}

impl<'a> Guidon<'a> {
    /// Initializes guidon with default settings. The template file path is set to `template.toml`
    pub fn new() -> Result<Self> {
        Guidon::with_conf("template.toml")
    }

    /// Initializes guidon with the provided configuration file
    ///
    /// # Arguments
    /// * `conf_file`: the given configuration file, as path reference
    ///
    /// # Example
    /// ```rust, no_run
    ///   use guidon::Guidon;
    ///
    ///   let guidon = Guidon::with_conf("my/config.toml").unwrap();
    /// ```
    ///
    pub fn with_conf<P: AsRef<Path>>(conf_file: P) -> Result<Self> {
        debug!(
            "Reading configuration from {}",
            conf_file.as_ref().display()
        );
        match read_to_string(conf_file.as_ref()) {
            Ok(conf) => Ok(toml::from_str::<Guidon>(&conf)?),
            Err(e) => Err(e.into()),
        }
    }

    /// Provides a callback to perform an operation on the variables map.
    /// Can be used to change default variables values.
    ///
    /// # Arguments
    /// * `cb`: callback. A closure with takes a `Hashmap<String, String>` as parameter and returns
    /// a `HashMap<String, String>`.
    ///
    /// # Example
    /// ```rust, no_run
    ///   use guidon::Guidon;
    ///   use std::collections::HashMap;
    ///
    ///   let cb = |h: &mut HashMap<String, String>| {
    ///             h.iter_mut().for_each(|(_, v)|  *v +=" cb");
    ///    };
    ///   let mut guidon = Guidon::new().unwrap();
    ///   guidon.set_variables_callback(cb);
    /// ```
    pub fn set_variables_callback<F>(&mut self, cb: F)
    where
        F: Fn(&mut HashMap<String, String>) + 'a,
    {
        self.variables_callback = Some(Box::new(cb) as Box<VariablesCallback>);
    }

    /// Provides a callback to be called when a variables is not found in the configuration file.
    ///
    /// **This currently doesn't work**
    ///
    /// # Arguments
    /// * `cb`: callback. A closure with takes the expected key as a `String` parameter and returns
    /// the value to use as a `String`.
    ///

    pub fn set_render_callback<F>(&mut self, cb: F)
    where
        F: Fn(String) -> String + 'a,
    {
        self.render_callback = Some(Box::new(cb) as Box<RenderCallback>);
    }

    /// Apply template.
    /// The substitution will be performed with variables provided in the config file.
    ///
    /// # Arguments
    /// * `from_dir`: the directory where the template files are located. If `use_template_dir` is
    /// set to `true` (default value), the template will be searched for in `from_dir/template`.
    /// * `to_dir` : the directory where the templated file structure will be created.
    pub fn apply_template<F, T>(&mut self, from_dir: F, to_dir: T) -> Result<()>
    where
        F: AsRef<Path>,
        T: AsRef<Path>,
    {
        info!(
            "Applying template for {}",
            from_dir.as_ref().display().to_string()
        );
        // 1 - Check substition values (callback)
        // call callback

        if let Some(cb) = &self.variables_callback {
            debug!("Calling variable callback");
            cb(&mut self.variables);
        }

        // 2 - Create destination directory
        // Fails if the destination dir doesn't exists
        create_dir(to_dir.as_ref())?;

        // 3 - Parse template dirs, apply template and copy to destination
        let mut handlebars = Handlebars::new();
        handlebars.set_strict_mode(self.use_strict_mode);
        let template_dir = if self.use_template_dir {
            from_dir.as_ref().join("template")
        } else {
            from_dir.as_ref().to_path_buf()
        };
        self.parse_dir(
            &template_dir.canonicalize()?,
            &to_dir.as_ref().canonicalize()?,
            &handlebars,
        )?;

        info!("Template applied.");
        Ok(())
    }

    fn parse_dir(&mut self, dir: &Path, to: &Path, hb: &Handlebars) -> Result<()> {
        debug!("Parsing dir {}", dir.display().to_string());
        if !dir.is_dir() {
            return Err(GuidonError::new(ErrorKind::NotAFolder, "Not a directory"));
        }

        for entry in dir.read_dir()? {
            match entry {
                Ok(entry) => {
                    let entry_path = entry.path();
                    debug!("Parsing entry {}", entry_path.display().to_string());
                    if entry_path.is_dir() {
                        debug!("Entry is a directory");
                        let name =
                            entry_path
                                .file_name()
                                .and_then(OsStr::to_str)
                                .ok_or_else(|| {
                                    GuidonError::new(ErrorKind::NotFound, "Can't extract dir name")
                                })?;
                        if name.starts_with("{{") && name.ends_with("}}") {
                            let mut sub = name.trim_matches(|c| c == '{' || c == '}');
                            sub = self.variables.get(sub).unwrap_or_else(|| {
                                panic!("No such value in template.hbs: {}", sub)
                            });
                            let target_path = to.join(sub);
                            debug!("Creating folder {}", target_path.display().to_string());
                            create_dir_all(&target_path)?;
                            self.parse_dir(&entry.path(), &target_path, hb)?;
                        } else {
                            let dir_name = entry_path.file_name().unwrap();
                            let target_path = to.join(dir_name);
                            debug!("Creating folder {}", target_path.display().to_string());
                            create_dir_all(&target_path)?;
                            self.parse_dir(&entry.path(), &target_path, hb)?;
                        }
                    } else {
                        debug!("Entry is a file");
                        if entry_path
                            .extension()
                            .and_then(OsStr::to_str)
                            .map_or_else(|| false, |s| s == "hbs")
                        {
                            debug!("Entry is a template source");
                            let mut source_template = File::open(entry.path())?;
                            let target_file_name = entry_path.file_stem().unwrap();
                            let target_path = to.join(target_file_name);
                            let mut output_file = File::create(&target_path)?;
                            debug!("Copying entry to {}", target_path.display().to_string());
                            if let Some(cb) = &self.render_callback {
                                unimplemented!();
                                // TODO: Make it work...
                                loop {
                                    match hb.render_template_source_to_write(
                                        &mut source_template,
                                        &self.variables,
                                        &mut output_file,
                                    ) {
                                        Err(e) => {
                                            if let Some(error) = e.as_render_error() {
                                                debug!("Render error : {}", error.desc);
                                                let variable: &str =
                                                    error.desc.split('"').nth(1).unwrap();
                                                let value = cb(variable.to_string());
                                                self.variables.insert(variable.to_string(), value);
                                            } else {
                                                debug!("Handlebars error");
                                                return Err(e.into());
                                            }
                                        }
                                        Ok(_) => {
                                            debug!(
                                                "Mapping done for {}",
                                                target_file_name.to_string_lossy()
                                            );
                                            break;
                                        }
                                    }
                                }
                            } else {
                                hb.render_template_source_to_write(
                                    &mut source_template,
                                    &self.variables,
                                    &mut output_file,
                                )?;
                            }
                        } else {
                            let target_path = to.join(entry_path.file_name().unwrap());
                            debug!("Copying entry to {}", target_path.display().to_string());
                            copy(&entry_path, to.join(target_path))?;
                        }
                    }
                }
                Err(e) => error!("Unparsable dir entry : {}", e.to_string()),
            }
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use crate::Guidon;
    use log::info;
    use log::LevelFilter;
    use pretty_assertions::{assert_eq, assert_ne};
    use serde_derive::Deserialize;
    use std::collections::HashMap;
    use std::fs::remove_dir_all;
    use std::sync::Once;

    static INIT: Once = Once::new();

    fn setup() {
        INIT.call_once(|| {
            let _ = env_logger::builder()
                .is_test(true)
                .filter_level(LevelFilter::Trace)
                .try_init();
        });
    }

    #[test]
    fn should_parse_toml() {
        setup();
        let toml_content = r#"
          use_template_dir = false

          [variables]
          key1 = "value1"
          key2 = "value2"
          "#;

        let guidon: Guidon = toml::from_str(toml_content).unwrap();
        assert_eq!(guidon.render_callback.is_none(), true);
        assert_eq!(guidon.use_template_dir, false);
        assert_eq!(guidon.variables["key1"], "value1".to_string())
    }

    #[test]
    fn should_parse_toml_with_defaults() {
        setup();
        let toml_content = r#"
          [variables]
          key1 = "value1"
          key2 = "value2"
          "#;

        let guidon: Guidon = toml::from_str(toml_content).unwrap();
        assert_eq!(guidon.render_callback.is_none(), true);
        assert_eq!(guidon.use_template_dir, true);
        assert_eq!(guidon.variables["key1"], "value1".to_string())
    }

    #[test]
    fn test_variables_callback() {
        let cb = |h: &mut HashMap<String, String>| {
            h.iter_mut().for_each(|(_, v)| *v += " cb");
        };

        setup();
        let toml_content = r#"
          [variables]
          key1 = "value1"
          key2 = "value2"
          "#;

        let mut guidon: Guidon = toml::from_str(toml_content).unwrap();
        guidon.set_variables_callback(cb);

        let mut map: HashMap<String, String> = HashMap::new();
        map.insert("toto".to_owned(), "tutu".to_string());
        map.insert("titi".to_string(), "tata".to_string());
        cb(&mut map);

        assert_eq!(map["toto"], "tutu cb".to_string());
        assert_eq!(map["titi"], "tata cb".to_string());
    }
}