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
use serde::{Deserialize, Serialize};
use std::{
  fs,
  path::{Path, PathBuf},
};

use proplate_errors::ProplateError;

use crate::fs::walk::{walk_dir, walk_dir_skip};

use super::{
  op::{AdditionalOperation, Operation},
  META_CONF,
};

#[derive(Serialize, Deserialize, Debug)]
pub enum ArgType {
  Text,
  Select,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct Arg {
  pub key: String,
  pub q_type: ArgType,
  pub label: String,
  pub default_value: Option<String>,
  /// Only used when "key" equals "Select."
  pub options: Option<Vec<String>>,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct TemplateConf {
  /// Template id
  pub id: String,
  /// Auxiliary proplate utils
  /// for example, a "License" file that is only copied if the "License" arg is set to "MIT"
  #[serde(default = "Vec::new")]
  pub exclude: Vec<String>,
  /// Arguments that Proplate will ask when a project is created using the associated template
  pub args: Vec<Arg>,
  /// List of files containing dynamic variables
  /// used by Proplate to prevent having to go through every template file
  #[serde(default = "Vec::new")]
  pub dynamic_files: Vec<String>,
  #[serde(default = "Vec::new")]
  pub additional_operations: Vec<AdditionalOperation>,

  #[serde(default = "TemplateConf::default_keep_meta")]
  pub keep_meta: bool,

  /// Prevent examining dyn files repeatedly.
  #[serde(skip)]
  pub require_dyn_file_analysis: bool,
}

impl TemplateConf {
  pub fn new(path: &Path) -> TemplateConf {
    let conf = path.join(META_CONF);
    let meta_json = fs::read_to_string(conf).expect("meta.json can't be located or locked");
    let mut config = parse_config(&meta_json);

    normalize(&mut config, path);

    config
  }

  fn default_keep_meta() -> bool {
    false
  }
}

fn parse_config(meta_json: &str) -> TemplateConf {
  serde_json::from_str(meta_json)
    .map_err(|e| ProplateError::invalid_template_conf(e.to_string().as_str()))
    .unwrap()
}

fn normalize(config: &mut TemplateConf, base: &Path) {
  set_exclude_files(config, base);
  set_additional_ops_files(config, base);

  config.require_dyn_file_analysis = true;
  // Avoid unnecessary analysis
  // As only additional_operationns has the power to change the state of the template files, we can
  // 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.
  if config.additional_operations.is_empty() {
    analyze_dyn_files(config, base);
    config.require_dyn_file_analysis = false;
  }
}

fn set_exclude_files(config: &mut TemplateConf, base: &Path) {
  let files = &mut config.exclude;

  // Always exclude '.proplate_aux_utils' folder
  files.extend([".proplate_aux_utils".into(), ".git".into()]);

  if !config.keep_meta {
    files.push(META_CONF.into());
  }

  to_relative_all(files, base);
}

fn set_additional_ops_files(config: &mut TemplateConf, base: &Path) {
  for additional_op in &mut config.additional_operations {
    for op in &mut additional_op.operations {
      match op {
        Operation::Copy { file, dest } => {
          *file = to_relative(PathBuf::from(&file), base);
          *dest = to_relative(PathBuf::from(&dest), base);
        }
        Operation::CopyDir { path, dest } => {
          *path = to_relative(PathBuf::from(&path), base);
          *dest = to_relative(PathBuf::from(&dest), base);
        }
        Operation::Remove { files } => {
          to_relative_all(files, base);
        }
      }
    }
  }
}

pub fn analyze_dyn_files(config: &mut TemplateConf, base: &Path) {
  if config.dynamic_files.is_empty() {
    populate_dynamic_files(config, base);
  } else {
    update_dynamic_files(config, base);
  }
}

/// Walks the template files to populate "dynamic_files".
fn populate_dynamic_files(config: &mut TemplateConf, base: &Path) {
  let TemplateConf {
    dynamic_files,
    exclude,
    ..
  } = config;
  let exclude_paths = exclude.iter().map(|s| PathBuf::from(s)).collect::<Vec<_>>();
  *dynamic_files = walk_dir_skip(base, exclude_paths)
    .expect("Walk dir")
    .iter()
    .map(|(file, _)| file.display().to_string())
    .collect::<Vec<_>>();
}

fn update_dynamic_files(config: &mut TemplateConf, base: &Path) {
  let TemplateConf {
    dynamic_files,
    exclude,
    ..
  } = config;
  to_relative_all(dynamic_files, base /* to */);

  let mut expanded = Vec::new();

  // recursively expand the dynamic files
  for path in dynamic_files.iter() {
    if let Ok(files) = walk_dir(Path::new(path)) {
      let paths = files
        .into_iter()
        .filter_map(|(file, _)| {
          let file = file.display().to_string();
          if exclude.contains(&file) {
            return None;
          }
          Some(file)
        })
        .collect::<Vec<_>>();
      expanded.extend(paths);
    }
  }

  dynamic_files.extend(expanded);
}

fn to_relative_all(files: &mut Vec<String>, to: &Path) {
  for file in files.into_iter() {
    *file = to_relative(PathBuf::from(&file), to);
  }
}

fn to_relative(path: PathBuf, to: &Path) -> String {
  to.join(path).display().to_string()
}