proplate_core/template/
config.rs

1use serde::{Deserialize, Serialize};
2use std::{
3  fs,
4  path::{Path, PathBuf},
5};
6
7use proplate_errors::{ProplateError, ProplateErrorKind, TemplateErrorKind};
8
9use crate::fs::walk::{walk_dir, walk_dir_skip};
10
11use super::{
12  op::{AdditionalOperation, Operation},
13  META_CONF,
14};
15
16#[derive(Serialize, Deserialize, Debug)]
17pub enum ArgType {
18  Text,
19  Select,
20}
21
22#[derive(Serialize, Deserialize, Debug)]
23pub struct Arg {
24  pub key: String,
25  pub q_type: ArgType,
26  pub label: String,
27  pub default_value: Option<String>,
28  /// Only used when "key" equals "Select."
29  pub options: Option<Vec<String>>,
30}
31
32#[derive(Serialize, Deserialize, Debug)]
33pub struct TemplateConf {
34  /// Template id
35  pub id: String,
36  /// Auxiliary proplate utils
37  /// for example, a "License" file that is only copied if the "License" arg is set to "MIT"
38  #[serde(default = "Vec::new")]
39  pub exclude: Vec<String>,
40  /// Arguments that Proplate will ask when a project is created using the associated template
41  pub args: Vec<Arg>,
42  /// List of files containing dynamic variables
43  /// used by Proplate to prevent having to go through every template file
44  #[serde(default = "Vec::new")]
45  pub dynamic_files: Vec<String>,
46  #[serde(default = "Vec::new")]
47  pub additional_operations: Vec<AdditionalOperation>,
48
49  #[serde(default = "TemplateConf::default_keep_meta")]
50  pub keep_meta: bool,
51
52  /// Prevent examining dyn files repeatedly.
53  #[serde(skip)]
54  pub require_dyn_file_analysis: bool,
55}
56
57impl TemplateConf {
58  pub fn new(path: &Path) -> TemplateConf {
59    let conf = path.join(META_CONF);
60    let meta_json = fs::read_to_string(conf).unwrap();
61    let mut config = parse_config(&meta_json, path.display().to_string().as_str());
62
63    normalize(&mut config, path);
64
65    config
66  }
67
68  fn default_keep_meta() -> bool {
69    false
70  }
71}
72
73fn parse_config(meta_json: &str, location: &str) -> TemplateConf {
74  serde_json::from_str(meta_json)
75    .map_err(|e| {
76      ProplateError::create(ProplateErrorKind::Template {
77        kind: TemplateErrorKind::Invalid,
78        location: location.into(),
79      })
80      .with_ctx("template:parse_config")
81      .with_cause(&e.to_string())
82    })
83    .unwrap()
84}
85
86fn normalize(config: &mut TemplateConf, base: &Path) {
87  set_exclude_files(config, base);
88  set_additional_ops_files(config, base);
89
90  config.require_dyn_file_analysis = true;
91  // Avoid unnecessary analysis
92  // As only additional_operationns has the power to change the state of the template files, we can
93  // analyze the dyn files here in the absence of any operations and say that no analysis is necessary prior to the dyn files' ctx binding.
94  if config.additional_operations.is_empty() {
95    analyze_dyn_files(config, base);
96    config.require_dyn_file_analysis = false;
97  }
98}
99
100fn set_exclude_files(config: &mut TemplateConf, base: &Path) {
101  let files = &mut config.exclude;
102
103  // Always exclude '.proplate_aux_utils' folder
104  files.extend([".proplate_aux_utils".into(), ".git".into()]);
105
106  if !config.keep_meta {
107    files.push(META_CONF.into());
108  }
109
110  to_relative_all(files, base);
111}
112
113fn set_additional_ops_files(config: &mut TemplateConf, base: &Path) {
114  for additional_op in &mut config.additional_operations {
115    for op in &mut additional_op.operations {
116      match op {
117        Operation::Copy { file, dest } => {
118          *file = to_relative(PathBuf::from(&file), base);
119          *dest = to_relative(PathBuf::from(&dest), base);
120        }
121        Operation::CopyDir { path, dest } => {
122          *path = to_relative(PathBuf::from(&path), base);
123          *dest = to_relative(PathBuf::from(&dest), base);
124        }
125        Operation::Remove { files } => {
126          to_relative_all(files, base);
127        }
128      }
129    }
130  }
131}
132
133pub fn analyze_dyn_files(config: &mut TemplateConf, base: &Path) {
134  if config.dynamic_files.is_empty() {
135    populate_dynamic_files(config, base);
136  } else {
137    update_dynamic_files(config, base);
138  }
139}
140
141/// Walks the template files to populate "dynamic_files".
142fn populate_dynamic_files(config: &mut TemplateConf, base: &Path) {
143  let TemplateConf {
144    dynamic_files,
145    exclude,
146    ..
147  } = config;
148  let exclude_paths = exclude.iter().map(|s| PathBuf::from(s)).collect::<Vec<_>>();
149  *dynamic_files = walk_dir_skip(base, exclude_paths)
150    .expect("Walk dir")
151    .iter()
152    .map(|(file, _)| file.display().to_string())
153    .collect::<Vec<_>>();
154}
155
156fn update_dynamic_files(config: &mut TemplateConf, base: &Path) {
157  let TemplateConf {
158    dynamic_files,
159    exclude,
160    ..
161  } = config;
162  to_relative_all(dynamic_files, base /* to */);
163
164  let mut expanded = Vec::new();
165
166  // recursively expand the dynamic files
167  for path in dynamic_files.iter() {
168    if let Ok(files) = walk_dir(Path::new(path)) {
169      let paths = files
170        .into_iter()
171        .filter_map(|(file, _)| {
172          let file = file.display().to_string();
173          if exclude.contains(&file) {
174            return None;
175          }
176          Some(file)
177        })
178        .collect::<Vec<_>>();
179      expanded.extend(paths);
180    }
181  }
182
183  dynamic_files.extend(expanded);
184}
185
186fn to_relative_all(files: &mut Vec<String>, to: &Path) {
187  for file in files.into_iter() {
188    *file = to_relative(PathBuf::from(&file), to);
189  }
190}
191
192fn to_relative(path: PathBuf, to: &Path) -> String {
193  to.join(path).display().to_string()
194}