willbe/tool/
template.rs

1/// Define a private namespace for all its items.
2mod private
3{
4  #[ allow( unused_imports, clippy::wildcard_imports ) ]
5  use crate::tool::*;
6
7  use std::
8  {
9    fs,
10    path::
11    {
12      Path,
13      PathBuf
14    },
15  };
16  use error::untyped::Context;
17  // Explicit import for Result and its variants for pattern matching
18  use core::result::Result::Ok;
19
20  /// Container for templates.
21  ///
22  /// Includes files to create, parameters that those templates accept,
23  /// and values for those parameters.
24  #[ derive( Debug ) ]
25  pub struct TemplateHolder
26  {
27    /// Files of the template.
28    pub files : Vec< TemplateFileDescriptor >,
29    /// Parameters definitions.
30    pub parameters : TemplateParameters,
31    /// The values associated with the template.
32    pub values : TemplateValues,
33    /// Path to the parameter storage for recovering values
34    /// for already generated templated files.
35    pub parameter_storage : &'static Path,
36    /// Name of the template to generate
37    pub template_name : &'static str,
38  }
39
40  impl TemplateFiles for Vec< TemplateFileDescriptor > {}
41
42
43  impl TemplateHolder
44  {
45    /// Creates all files in the specified path using the template values.
46    ///
47    /// # Parameters
48    ///
49    /// - `path`: A reference to the path where the files will be created.
50    ///
51    /// # Returns
52    ///
53    /// A `Result` which is `Ok` if the files are created successfully, or an `Err` otherwise.
54    ///
55    /// # Errors
56    /// qqq: doc
57    pub fn create_all( self, path : &path::Path ) -> error::untyped::Result< () > // qqq : use typed error
58    {
59      self.files.create_all( path, &self.values )
60    }
61
62    /// Returns a reference to the template parameters.
63    ///
64    /// # Returns
65    ///
66    /// A reference to `TemplateParameters`.
67    #[ must_use ]
68    pub fn parameters( &self ) -> &TemplateParameters
69    {
70      &self.parameters
71    }
72
73    /// Sets the template values.
74    ///
75    /// # Parameters
76    ///
77    /// - `values`: The new `TemplateValues` to be set.
78    pub fn set_values( &mut self, values : TemplateValues )
79    {
80      self.values = values;
81    }
82
83    /// Returns a reference to the template values.
84    ///
85    /// # Returns
86    ///
87    /// A reference to `TemplateValues`.
88    #[ must_use ]
89    pub fn get_values( &self ) -> &TemplateValues
90    {
91      &self.values
92    }
93
94    /// Returns a mutable reference to the template values.
95    ///
96    /// # Returns
97    ///
98    /// A mutable reference to `TemplateValues`.
99    pub fn get_values_mut( &mut self ) -> &mut TemplateValues
100    {
101      &mut self.values
102    }
103
104    /// Loads existing parameters from the specified path and updates the template values.
105    ///
106    /// # Parameters
107    ///
108    /// - `path`: A reference to the path where the parameter file is located.
109    ///
110    /// # Returns
111    ///
112    /// An `Option` which is `Some(())` if the parameters are loaded successfully, or `None` otherwise.
113    pub fn load_existing_params( &mut self, path : &Path ) -> Option< () >
114    {
115      let data = fs::read_to_string( path.join( self.parameter_storage ) ).ok()?;
116      let document = data.parse::< toml_edit::Document >().ok()?;
117      let parameters : Vec< _ > = self.parameters().descriptors.iter().map( | d | &d.parameter ).cloned().collect();
118      let template_table = document.get( self.template_name )?;
119      for parameter in parameters
120      {
121        let value = template_table.get( &parameter )
122        .and_then
123        (
124          | item |
125          match item
126          {
127            toml_edit::Item::Value( toml_edit::Value::String( val ) ) => Some( val.value() ),
128            _ => None
129          }
130        );
131        if let Some( value ) = value
132        {
133          self.get_values_mut().insert_if_empty( &parameter, wca::Value::String( value.into() ) );
134        }
135      }
136      Some( () )
137    }
138
139    /// Fetches mandatory parameters that are not set yet.
140    #[ must_use ]
141    pub fn get_missing_mandatory( &self ) -> Vec< &str >
142    {
143      let values = self.get_values();
144      self
145      .parameters()
146      .list_mandatory()
147      .into_iter()
148      .filter( | key | values.0.get( *key ).and_then( | val | val.as_ref() ).is_none() )
149      .collect()
150    }
151  }
152
153  /// Files stored in a template.
154  ///
155  /// Can be iterated over, consuming the owner of the files.
156  pub trait TemplateFiles : IntoIterator< Item = TemplateFileDescriptor > + Sized
157  {
158    /// Creates all files in provided path with values for required parameters.
159    ///
160    /// Consumes owner of the files.
161    ///
162    /// # Errors
163    /// qqq: doc
164    fn create_all( self, path : &Path, values : &TemplateValues ) -> error::untyped::Result< () > // qqq : use typed error
165    {
166      let fsw = FileSystem;
167      for file in self
168      {
169        file.create_file( &fsw, path, values )?;
170      }
171      Ok( () )
172    }
173  }
174
175  /// Parameters required for the template.
176  #[ derive( Debug, Default, former::Former ) ]
177  pub struct TemplateParameters
178  {
179    #[ subform_entry( setter = false ) ]
180    descriptors : Vec< TemplateParameterDescriptor >
181  }
182
183  impl TemplateParameters
184  {
185    /// Extracts template values from props for parameters required for this template.
186    #[ must_use ]
187    pub fn values_from_props( &self, props : &wca::executor::Props ) -> TemplateValues
188    {
189      let values = self.descriptors
190      .iter()
191      .map( | d | &d.parameter )
192      .map( | param | ( param.clone(), props.get( param ).cloned() ) )
193      .collect();
194      TemplateValues( values )
195    }
196
197    /// Get a list of all mandatory parameters.
198    #[ must_use ]
199    pub fn list_mandatory( &self ) -> Vec< &str >
200    {
201      self.descriptors.iter().filter( | d | d.is_mandatory ).map( | d | d.parameter.as_str() ).collect()
202    }
203  }
204
205  /// Parameter description.
206  #[ derive( Debug, Default, former::Former ) ]
207  pub struct TemplateParameterDescriptor
208  {
209    parameter : String,
210    is_mandatory : bool,
211  }
212
213  impl< Definition > TemplateParametersFormer< Definition >
214  where
215    Definition : former::FormerDefinition< Storage = < TemplateParameters as former::EntityToStorage >::Storage >,
216  {
217    #[ inline( always ) ]
218    pub fn parameter( self, name : &str ) ->
219    TemplateParameterDescriptorAsSubformer< Self, impl TemplateParameterDescriptorAsSubformerEnd< Self > >
220    {
221      self._descriptors_subform_entry::< TemplateParameterDescriptorFormer< _ >, _ >()
222      .parameter( name )
223    }
224  }
225
226  /// Holds a map of parameters and their values.
227  #[ derive( Debug, Default ) ]
228  pub struct TemplateValues( collection::HashMap< String, Option< wca::Value > > );
229
230  impl TemplateValues
231  {
232    /// Converts values to a serializable object.
233    ///
234    /// Currently only `String`, `Number`, and `Bool` are supported.
235    #[ must_use ]
236    pub fn to_serializable( &self ) -> collection::BTreeMap< String, String >
237    {
238      self.0.iter().map
239      (
240        | ( key, value ) |
241        {
242          let value = value.as_ref().map_or
243          (
244            "___UNSPECIFIED___".to_string(),
245            | value |
246            {
247              match value
248              {
249                wca::Value::String( val ) => val.to_string(),
250                wca::Value::Number( val ) => val.to_string(),
251                wca::Value::Bool( val ) => val.to_string(),
252                wca::Value::Path( _ ) |
253                wca::Value::List( _ ) => "unsupported".to_string(),
254              }
255            }
256          );
257          ( key.to_owned(), value )
258        }
259      )
260      .collect()
261    }
262
263    /// Inserts new value if parameter wasn't initialized before.
264    pub fn insert_if_empty( &mut self, key : &str, value : wca::Value )
265    {
266      if self.0.get( key ).and_then( | v | v.as_ref() ).is_none()
267      {
268        self.0.insert( key.into() , Some( value ) );
269      }
270    }
271
272    /// Interactively asks user to provide value for a parameter.
273    #[allow(clippy::missing_panics_doc)]
274    pub fn interactive_if_empty( &mut self, key : &str )
275    {
276      if self.0.get( key ).and_then( | v | v.as_ref() ).is_none()
277      {
278        println! ("Parameter `{key}` is not set" );
279        print!( "Enter value: " );
280        use std::io::{ self, Write };
281        io::stdout().flush().unwrap();
282        let mut answer = String::new();
283        io::stdin().read_line( &mut answer ).unwrap();
284        let answer = answer.trim().to_string();
285        self.0.insert( key.into(), Some( wca::Value::String( answer ) ) );
286      }
287    }
288  }
289
290  /// File descriptor for the template.
291  ///
292  /// Holds raw template data, relative path for the file, and a flag that
293  /// specifies whether the raw data should be treated as a template.
294  #[ derive( Debug, former::Former ) ]
295  pub struct TemplateFileDescriptor
296  {
297    path : PathBuf,
298    data : &'static str,
299    is_template : bool,
300    mode : WriteMode
301  }
302
303  impl TemplateFileDescriptor
304  {
305    fn contents< FS : FileSystemPort >( &self, fs : &FS, path : &PathBuf, values : &TemplateValues )
306    -> error::untyped::Result< String >
307    {
308      let contents = if self.is_template
309      {
310        self.build_template( values )?
311      }
312      else
313      {
314        self.data.to_owned()
315      };
316      match self.mode
317      {
318        WriteMode::Rewrite => Ok( contents ),
319        WriteMode::TomlExtend =>
320        {
321          let instruction = FileReadInstruction { path : path.into() };
322          if let Ok( existing_contents ) = fs.read( &instruction )
323          {
324            let document = contents.parse::< toml_edit::Document >().context( "Failed to parse template toml file" )?;
325            let template_items = document.iter();
326            let existing_toml_contents = String::from_utf8( existing_contents ).context( "Failed to read existing toml file as a UTF-8 String" )?;
327            let mut existing_document = existing_toml_contents.parse::< toml_edit::Document >().context( "Failed to parse existing toml file" )?;
328            for ( template_key, template_item ) in template_items
329            {
330              match existing_document.get_mut( template_key )
331              {
332                Some( item ) => template_item.clone_into( item ),
333                None => template_item.clone_into( &mut existing_document[ template_key ] ),
334              }
335            }
336            return Ok( existing_document.to_string() );
337          }
338
339          Ok( contents )
340        }
341      }
342    }
343
344    // qqq : use typed error
345    fn build_template( &self, values : &TemplateValues ) -> error::untyped::Result< String >
346    {
347      let mut handlebars = handlebars::Handlebars::new();
348      handlebars.register_escape_fn( handlebars::no_escape );
349      handlebars.register_template_string( "templated_file", self.data )?;
350      handlebars.render( "templated_file", &values.to_serializable() ).context( "Failed creating a templated file" )
351    }
352
353    fn create_file< FS : FileSystemPort >( &self, fs : &FS, path : &Path, values : &TemplateValues ) -> error::untyped::Result< () > // qqq : use typed error
354    {
355      let path = path.join( &self.path );
356      let data = self.contents( fs, &path, values )?.as_bytes().to_vec();
357      let instruction = FileWriteInstruction { path, data };
358      fs.write( &instruction )?;
359      Ok( () )
360    }
361
362  }
363
364  /// Determines how the template file should be written.
365  #[ derive( Debug, Default ) ]
366  pub enum WriteMode
367  {
368    /// Overwrites existing files.
369    #[default]
370    Rewrite,
371    /// Attempts to extend existing toml files.
372    ///
373    /// If files exists it searches for the same top-level items (tables, values)
374    /// and replaces them with template defined ones.
375    /// If file does not exist it creates a new one with contents provided by the template.
376    TomlExtend
377  }
378
379  /// Helper builder for full template file list.
380  #[ derive( Debug, former::Former ) ]
381  pub struct TemplateFilesBuilder
382  {
383    /// Stores all file descriptors for current template.
384    #[ subform_entry( setter = true ) ]
385    #[ scalar( setter = false ) ]
386    pub files : Vec< TemplateFileDescriptor >,
387  }
388
389  impl< Description > TemplateFilesBuilderFormer< Description >
390  where
391    Description : former::FormerDefinition< Storage = < TemplateFilesBuilder as former::EntityToStorage >::Storage >,
392  {
393    #[ inline( always ) ]
394    pub fn file( self ) -> TemplateFileDescriptorAsSubformer< Self, impl TemplateFileDescriptorAsSubformerEnd< Self > >
395    {
396      self._files_subform_entry()
397    }
398  }
399
400  /// Instruction for writing a file.
401  #[ derive( Debug ) ]
402  pub struct FileWriteInstruction
403  {
404    path : PathBuf,
405    data : Vec< u8 >,
406  }
407
408  /// Instruction for reading from a file.
409  #[ derive( Debug ) ]
410  pub struct FileReadInstruction
411  {
412    path : PathBuf,
413  }
414
415  /// Describes how template file creation should be handled.
416  pub trait FileSystemPort
417  {
418    /// Writing to file implementation.
419    /// # Errors
420    /// qqq: doc
421    fn write( &self, instruction : &FileWriteInstruction ) -> error::untyped::Result< () >; // qqq : use typed error
422
423    /// Reading from a file implementation.
424    /// # Errors
425    /// qqq: doc
426    fn read( &self, instruction : &FileReadInstruction ) -> error::untyped::Result< Vec< u8 > >; // qqq : use typed error
427  }
428
429  // zzz : why not public?
430  struct FileSystem;
431  impl FileSystemPort for FileSystem
432  {
433    fn write( &self, instruction : &FileWriteInstruction ) -> error::untyped::Result< () > // qqq : use typed error
434    {
435      let FileWriteInstruction { path, data } = instruction;
436      let dir = path.parent().context( "Invalid file path provided" )?;
437      if !dir.exists()
438      {
439        fs::create_dir_all( dir )?;
440      }
441      fs::write( path, data ).context( "Failed creating and writing to file" )
442    }
443
444  // qqq : use typed error
445    fn read( &self, instruction : &FileReadInstruction ) -> error::untyped::Result< Vec< u8 > >
446    {
447      let FileReadInstruction { path } = instruction;
448      fs::read( path ).context( "Failed reading a file" )
449    }
450
451  }
452
453}
454
455//
456
457crate::mod_interface!
458{
459  //orphan use Template;
460  orphan use TemplateHolder;
461  orphan use TemplateFiles;
462  orphan use TemplateFileDescriptor;
463  orphan use TemplateParameters;
464  orphan use TemplateParameterDescriptor;
465  orphan use TemplateValues;
466  orphan use TemplateFilesBuilder;
467  orphan use FileSystemPort;
468  orphan use FileWriteInstruction;
469  orphan use FileReadInstruction;
470  orphan use WriteMode;
471}