1pub mod builder_error;
32mod partial;
33pub mod targets;
34
35use std::{
36 borrow::Borrow,
37 collections::{HashMap, HashSet},
38 path::{Path, PathBuf},
39 sync::mpsc,
40};
41
42use lazy_static::lazy_static;
43use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
44use regex::Regex;
45
46use crate::{
47 builder::targets::Target,
48 evaluator::{
49 evaluate_expression,
50 evaluation_context::{ContextLocation, EvaluationContext, EvaluationContextWarnings},
51 evaluator_builder_error::EvaluatorBuilderError,
52 Evaluate, ValueFunction, Variables,
53 },
54 parser::{expression::Expression, node::NodeContent, Parser},
55 tokenizer::{Tokenizer, TokenizerOptions},
56 value::Value,
57};
58
59use self::{builder_error::BuilderError, partial::Partial, targets::ReadWriteTarget};
60
61fn list_files<P: AsRef<Path>>(
65 path: P,
66 ignore_extensions: bool,
67 warn_on_skip: bool,
68) -> Vec<(PathBuf, Vec<String>)> {
69 fn list_files_rec<P: AsRef<Path>>(
70 path: P,
71 name_stack: &mut Vec<String>,
72 out: &mut Vec<(PathBuf, Vec<String>)>,
73 ignore_extensions: bool,
74 warn_on_skip: bool,
75 ) {
76 for entry in std::fs::read_dir(path).unwrap() {
77 let entry = match entry {
79 Ok(entry) => entry,
80 _ => continue,
81 };
82
83 let name = entry.file_name();
85 let name = match name.to_str() {
86 Some(name) => name,
87 _ => {
88 if warn_on_skip {
89 log::warn!(
90 "Skipping entry {:?} - name could not be converted to string.",
91 name
92 );
93 }
94 continue;
95 }
96 };
97
98 if !name.is_ascii() {
100 if warn_on_skip {
101 log::warn!("Skipping entry {name} - name is not ascii.");
102 }
103 continue;
104 }
105
106 let metadata = match entry.metadata() {
108 Ok(metadata) => metadata,
109 _ => {
110 if warn_on_skip {
111 log::warn!("Skipping entry {name} - could not extract metadata.");
112 }
113 continue;
114 }
115 };
116
117 if metadata.is_dir() {
119 name_stack.push(name.to_string());
120 list_files_rec(
121 entry.path(),
122 name_stack,
123 out,
124 ignore_extensions,
125 warn_on_skip,
126 );
127 name_stack.pop();
128 continue;
129 }
130
131 if metadata.is_file() {
133 let path = entry.path();
134
135 let extracted_name = if ignore_extensions {
138 path.file_stem()
139 } else {
140 path.file_name()
141 };
142
143 if let Some(extracted_name) = extracted_name {
146 if let Some(name) = extracted_name.to_str() {
147 let mut name_stack = name_stack.clone();
148 name_stack.push(name.to_string());
149 out.push((entry.path().to_owned(), name_stack.clone()));
150 continue;
151 }
152 }
153
154 log::error!("Skipping entry {name} - unexpected error");
155 continue;
156 }
157
158 if warn_on_skip {
159 log::warn!("Skipping entry {name} - entry is neither a file or a directory.")
160 }
161 }
162 }
163
164 let mut out = Vec::new();
165 let mut name_stack = Vec::new();
166 list_files_rec(
167 path,
168 &mut name_stack,
169 &mut out,
170 ignore_extensions,
171 warn_on_skip,
172 );
173
174 out
175}
176
177#[derive(Clone, Copy)]
179pub struct BuilderWarnings {
180 pub found_cycle: bool,
181 pub invalid_variable_file_line: bool,
182 pub file_skip: bool,
183}
184
185impl std::default::Default for BuilderWarnings {
186 fn default() -> Self {
187 Self {
188 found_cycle: false,
189 invalid_variable_file_line: true,
190 file_skip: true,
191 }
192 }
193}
194
195#[derive(Clone)]
196pub struct BuilderOptions {
197 pub max_depth: usize,
198 pub remove_generated_dir_content_on_create: bool,
199 pub watcher_delays_millis: u64,
200 pub builder_warnings: BuilderWarnings,
201 pub evaluation_context_warnings: EvaluationContextWarnings,
202}
203
204impl std::default::Default for BuilderOptions {
205 fn default() -> Self {
206 Self {
207 max_depth: 50,
208 remove_generated_dir_content_on_create: false,
209 watcher_delays_millis: 10,
210 builder_warnings: BuilderWarnings::default(),
211 evaluation_context_warnings: EvaluationContextWarnings::default(),
212 }
213 }
214}
215
216pub struct Builder<'p> {
217 sources_dir: Option<PathBuf>,
219 templates_dir: Option<PathBuf>,
220 generated_dir: Option<PathBuf>,
221 variables_file: Option<PathBuf>,
222 sources: HashMap<String, Partial<'p>>,
224 templates: HashMap<String, Partial<'p>>,
225 variables: Variables,
227 functions: HashMap<String, ValueFunction>,
228 options: BuilderOptions,
230}
231
232impl<'p> Builder<'p> {
233 pub fn new(
234 sources_dir: Option<PathBuf>,
235 templates_dir: Option<PathBuf>,
236 generated_dir: Option<PathBuf>,
237 variables_file: Option<PathBuf>,
238 options: BuilderOptions,
239 ) -> Result<Self, BuilderError> {
240 fn map_path_dir(path: Option<PathBuf>) -> std::io::Result<Option<PathBuf>> {
243 Ok(if let Some(path_buf) = path {
244 std::fs::create_dir_all(&path_buf)?;
245 Some(std::fs::canonicalize(&path_buf)?)
246 } else {
247 None
248 })
249 }
250
251 fn map_path_file(path: Option<PathBuf>) -> std::io::Result<Option<PathBuf>> {
252 Ok(if let Some(path_buf) = path {
253 let path_buf = std::fs::canonicalize(&path_buf)?;
254 if !path_buf.as_path().exists() {
255 std::fs::File::create(&path_buf)?;
256 }
257 Some(path_buf)
258 } else {
259 None
260 })
261 }
262
263 let sources_dir = map_path_dir(sources_dir)?;
265 let templates_dir = map_path_dir(templates_dir)?;
266 let generated_dir = map_path_dir(generated_dir)?;
267 let variables_file = map_path_file(variables_file)?;
268
269 if let Some(path) = generated_dir.as_ref() {
271 if options.remove_generated_dir_content_on_create {
272 std::fs::remove_dir_all(&path)?;
273 }
274 }
275
276 fn contains_dir(
278 target: &Option<PathBuf>,
279 a: &Option<PathBuf>,
280 b: &Option<PathBuf>,
281 ) -> bool {
282 if let Some(target) = target {
283 if let Some(a) = a {
284 if a.starts_with(&target) {
285 return true;
286 }
287 }
288
289 if let Some(b) = b {
290 if b.starts_with(&target) {
291 return true;
292 }
293 }
294
295 false
296 } else {
297 false
298 }
299 }
300
301 if contains_dir(&sources_dir, &templates_dir, &generated_dir)
303 || contains_dir(&templates_dir, &sources_dir, &generated_dir)
304 || contains_dir(&generated_dir, &templates_dir, &sources_dir)
305 {
306 return Err(BuilderError::InvalidDirectories);
307 }
308
309 Ok(Self {
310 sources_dir,
312 templates_dir,
313 generated_dir,
314 variables_file,
315 sources: HashMap::new(),
317 templates: HashMap::new(),
318 variables: HashMap::new(),
320 functions: HashMap::new(),
321 options,
323 })
324 }
325
326 fn add_local(
328 partials: &mut HashMap<String, Partial>,
329 name: impl ToString,
330 content: impl ToString,
331 ) -> Result<(), EvaluatorBuilderError> {
332 partials.insert(
333 name.to_string(),
334 Partial::from_string(content.to_string(), Target::Local(String::new()))?,
335 );
336 Ok(())
337 }
338
339 pub fn add_local_source(
341 &mut self,
342 name: impl ToString,
343 content: impl ToString,
344 ) -> Result<&mut Self, EvaluatorBuilderError> {
345 Self::add_local(&mut self.sources, name, content)?;
346 Ok(self)
347 }
348
349 pub fn add_local_template(
351 &mut self,
352 name: impl ToString,
353 content: impl ToString,
354 ) -> Result<&mut Self, EvaluatorBuilderError> {
355 Self::add_local(&mut self.templates, name, content)?;
356 Ok(self)
357 }
358
359 fn add_locals(
361 partials: &mut HashMap<String, Partial>,
362 items: &[(impl ToString, impl ToString)],
363 ) -> Result<(), Vec<(String, EvaluatorBuilderError)>> {
364 let mut errors = Vec::new();
365 for (name, content) in items {
366 if let Err(err) = Self::add_local(partials, name.to_string(), content.to_string()) {
367 errors.push((name.to_string(), err));
368 }
369 }
370
371 if errors.len() > 0 {
372 Err(errors)
373 } else {
374 Ok(())
375 }
376 }
377
378 pub fn add_local_sources(
380 &mut self,
381 items: &[(impl ToString, impl ToString)],
382 ) -> Result<&mut Self, Vec<(String, EvaluatorBuilderError)>> {
383 Self::add_locals(&mut self.sources, items)?;
384 Ok(self)
385 }
386
387 pub fn add_local_templates(
389 &mut self,
390 items: &[(impl ToString, impl ToString)],
391 ) -> Result<&mut Self, Vec<(String, EvaluatorBuilderError)>> {
392 Self::add_locals(&mut self.templates, items)?;
393 Ok(self)
394 }
395
396 pub fn add_variable(&mut self, name: impl ToString, value: Value) -> &mut Self {
398 self.variables.insert(name.to_string(), value);
399 self
400 }
401
402 pub fn add_variables(&mut self, variables: &[(String, Value)]) -> &mut Self {
404 for (name, value) in variables.into_iter().cloned() {
405 self.variables.insert(name, value);
406 }
407 self
408 }
409
410 pub fn add_function(&mut self, name: impl ToString, function: ValueFunction) -> &mut Self {
412 self.functions.insert(name.to_string(), function);
413 self
414 }
415
416 pub fn add_functions(&mut self, functions: Vec<(String, ValueFunction)>) -> &mut Self {
418 for (name, value) in functions.into_iter() {
419 self.functions.insert(name, value);
420 }
421 self
422 }
423
424 fn is_source_dependent_on_variables(&self, source_name: &str, variables: &[String]) -> bool {
426 fn dfs<'a>(
427 variables: &'a [String],
428 partials: &'a HashMap<String, Partial>,
429 partial_name: &'a str,
430 seen: &mut HashSet<&'a str>,
431 ) -> bool {
432 if seen.contains(partial_name) {
433 return false;
434 }
435
436 seen.insert(partial_name);
437
438 if let Some(partial) = partials.get(partial_name) {
439 for variable in variables {
440 if partial.get_dependencies().contains_variable(&variable) {
441 return true;
442 }
443 }
444
445 for dep in partial.get_dependencies().iter_builder_dependencies() {
446 if dfs(variables, partials, dep, seen) {
447 return true;
448 }
449 }
450 }
451
452 false
453 }
454
455 if let Some(source) = self.sources.get(source_name) {
456 for variable in variables {
457 if source.get_dependencies().contains_variable(&variable) {
458 return true;
459 }
460 }
461
462 for partial in source.get_dependencies().iter_builder_dependencies() {
463 let mut seen = HashSet::new();
464 if dfs(variables, &self.templates, partial, &mut seen) {
465 return true;
466 }
467 }
468 }
469
470 false
471 }
472
473 fn is_source_dependent_on_partial(&self, source_name: &str, partial_name: &str) -> bool {
475 fn dfs<'a>(
476 partials: &'a HashMap<String, Partial>,
477 current_partial_name: &'a str,
478 target_partial_name: &'a str,
479 seen: &mut HashSet<&'a str>,
480 ) -> bool {
481 if seen.contains(current_partial_name) {
482 return false;
483 }
484
485 seen.insert(current_partial_name);
486
487 if current_partial_name == target_partial_name {
488 return true;
489 }
490
491 if let Some(partial) = partials.get(current_partial_name) {
492 if partial
493 .get_dependencies()
494 .contains_builder(target_partial_name)
495 {
496 return true;
497 }
498
499 for dep in partial.get_dependencies().iter_builder_dependencies() {
500 if dfs(partials, dep, target_partial_name, seen) {
501 return true;
502 }
503 }
504 }
505
506 false
507 }
508
509 for root_partial_name in self
510 .sources
511 .get(source_name)
512 .unwrap()
513 .get_dependencies()
514 .builder_dependencies
515 .iter()
516 {
517 let mut seen = HashSet::new();
518 if dfs(&self.templates, root_partial_name, partial_name, &mut seen) {
519 return true;
520 }
521 }
522
523 false
524 }
525
526 pub fn update_variables<'a>(&mut self, variables: Vec<(String, impl Borrow<Expression<'a>>)>) {
529 let global_variables = HashMap::new();
531 let builders = HashMap::<_, Partial>::new();
532 let functions = HashMap::new();
533 let local_variables = vec![];
534 let mut context = EvaluationContext::new(
535 &global_variables,
536 local_variables,
537 &builders,
538 &functions,
539 1,
540 self.options.evaluation_context_warnings,
541 ContextLocation::Variables,
542 );
543
544 let mut names = Vec::new();
546 for (name, expression) in variables.into_iter() {
547 let value = evaluate_expression(&mut context, expression.borrow());
548 names.push(name.clone());
549 self.variables.insert(name, value);
550 }
551
552 let mut sources_to_update = Vec::new();
554 for name in self.sources.keys().clone().into_iter() {
555 if self.is_source_dependent_on_variables(name, names.as_slice()) {
556 sources_to_update.push(name.clone());
557 }
558 }
559
560 for name in sources_to_update {
562 self.write_source(&name);
563 }
564 }
565
566 fn find_cycle(&mut self) -> Option<Vec<String>> {
569 fn dfs(
571 templates: &HashMap<String, Partial>,
572 partial_name: &str,
573 seen: &mut HashSet<String>,
574 previously_seen: &mut HashSet<String>,
575 cycle: &mut Vec<String>,
576 add_to_cycle: &mut bool,
577 ) -> bool {
578 if seen.contains(partial_name) {
580 *add_to_cycle = true;
583
584 cycle.push(partial_name.to_string());
585 return true;
586 }
587
588 if previously_seen.contains(partial_name) {
590 return false;
591 }
592
593 seen.insert(partial_name.to_string());
595
596 previously_seen.insert(partial_name.to_string());
598
599 if let Some(partial) = templates.get(partial_name) {
602 for dep in partial.get_dependencies().iter_builder_dependencies() {
603 if dfs(templates, dep, seen, previously_seen, cycle, add_to_cycle) {
605 if *add_to_cycle {
606 cycle.push(partial_name.to_string());
607
608 if partial_name.cmp(cycle[0].as_str()) == std::cmp::Ordering::Equal {
611 *add_to_cycle = false;
612 }
613 }
614 return true;
615 }
616 }
617 }
618
619 seen.remove(partial_name);
621
622 false
623 }
624
625 let mut previously_seen = HashSet::new(); for source in self.sources.values_mut() {
628 for partial_name in source.get_dependencies().iter_builder_dependencies() {
629 let mut seen = HashSet::new();
631 let mut cycle = Vec::new();
632 let mut add_to_cycle = true;
633
634 if dfs(
636 &self.templates,
637 partial_name,
638 &mut seen,
639 &mut previously_seen,
640 &mut cycle,
641 &mut add_to_cycle,
642 ) {
643 return Some(cycle);
644 }
645 }
646 }
647
648 None
650 }
651
652 fn check_cycle(&mut self) {
655 if self.options.builder_warnings.found_cycle {
656 if let Some(cycle) = self.find_cycle() {
657 log::warn!(
658 "found a circular dependency between the following files: {:?}",
659 cycle
660 );
661 }
662 }
663 }
664
665 pub fn write_source(&mut self, name: &str) {
667 let mut context = EvaluationContext::new(
669 &self.variables,
670 vec![],
671 &self.templates,
672 &self.functions,
673 self.options.max_depth,
674 self.options.evaluation_context_warnings,
675 ContextLocation::source(name),
676 );
677
678 let source = self.sources.get_mut(name).unwrap();
680
681 source.evaluate_and_store(&mut context);
683 }
684
685 pub fn get_source(&self, name: &str) -> Option<&Partial> {
687 self.sources.get(name)
688 }
689
690 pub fn rebuild(&mut self) {
692 self.rescan_templates();
699 self.rescan_sources();
700 self.rescan_variables();
701
702 self.check_cycle();
703
704 let sources: Vec<String> = self.sources.keys().cloned().collect();
706 for name in sources {
707 self.write_source(name.as_str());
708 }
709 }
710
711 fn rescan(
714 scan_path: &Option<PathBuf>,
715 write_path: &Option<PathBuf>,
716 map: &mut HashMap<String, Partial>,
717 ignore_extensions: bool,
718 warn_on_skip: bool,
719 ) {
720 if let Some(path) = scan_path.as_ref() {
721 for (path, name_stack) in list_files(path, ignore_extensions, warn_on_skip) {
722 let name = name_stack.clone().join("/");
724
725 if !map.contains_key(&name) {
727 let write = if let Some(write_path) = write_path {
729 let mut target_path = write_path.clone();
731 for name in name_stack.into_iter() {
732 target_path = target_path.join(name);
733 }
734 Target::External(target_path)
735 } else {
736 Target::Local(String::new())
738 };
739
740 map.insert(
742 name,
743 Partial::new(ReadWriteTarget {
744 read: Target::External(path),
745 write,
746 })
747 .unwrap(),
748 );
749 }
750 }
751 }
752 }
753
754 fn rescan_templates(&mut self) {
757 Self::rescan(
758 &self.templates_dir,
759 &None,
760 &mut self.templates,
761 true,
762 self.options.builder_warnings.file_skip,
763 )
764 }
765
766 fn rescan_sources(&mut self) {
768 Self::rescan(
769 &self.sources_dir,
770 &self.generated_dir,
771 &mut self.sources,
772 false,
773 self.options.builder_warnings.file_skip,
774 )
775 }
776
777 fn rescan_variables(&mut self) {
778 lazy_static! {
779 static ref FILE_PATH_RE: Regex =
780 Regex::new(r"^([a-zA-Z_][0-9a-zA-Z_]+)\s*=\s*(.*)\s*").unwrap();
781 }
782
783 if let Some(path) = &self.variables_file {
784 let contents = std::fs::read_to_string(path).unwrap();
785
786 for (index, line) in contents.lines().enumerate() {
788 if let Some(captures) = FILE_PATH_RE.captures(line) {
789 if let (Some(name), Some(expression)) = (captures.get(1), captures.get(2)) {
790 let name = name.as_str().to_string();
791 let expression = expression.as_str();
792
793 let tokens = match Tokenizer::new(expression, TokenizerOptions::default())
795 .tokenize()
796 {
797 Ok(tokens) => tokens,
798 Err(err) => {
799 if self.options.builder_warnings.invalid_variable_file_line {
800 log::warn!(
801 "Could not parse variable {name} on line {index}. Tokenization error: {err}."
802 )
803 }
804 continue;
805 }
806 };
807
808 let nodes = match Parser::new(&tokens).parse() {
810 Ok(nodes) => nodes,
811 Err(err) => {
812 if self.options.builder_warnings.invalid_variable_file_line {
813 log::warn!("Could not parse variable {name} on line {index}. Parser error: {err}.")
814 }
815 continue;
816 }
817 };
818
819 if nodes.len() != 1 {
821 if self.options.builder_warnings.invalid_variable_file_line {
822 log::warn!("Could not parse variable {name} on line {index}. Expected to have exactly one expression.")
823 }
824 continue;
825 }
826
827 if let NodeContent::Expression(expression) = &nodes[0].content {
829 self.update_variables(vec![(name, expression)]);
831 } else {
832 if self.options.builder_warnings.invalid_variable_file_line {
833 log::warn!("Could not parse variable {name} on line {index}. Parsed node is not an expression.")
834 }
835 }
836 }
837 } else {
838 log::warn!("Could not parse on line {index}: invalid syntax.")
839 }
840 }
841 }
842 }
843
844 fn find_by_path<P: AsRef<Path>>(map: &HashMap<String, Partial>, path: P) -> Option<String> {
845 let path = path.as_ref();
846 map.iter().find_map(|(name, p)| {
847 if p.read_write_target.read.path_eq(path) {
848 Some(name.clone())
849 } else {
850 None
851 }
852 })
853 }
854
855 fn notify_partial<P: AsRef<Path>>(
858 map: &mut HashMap<String, Partial>,
859 path: P,
860 ) -> Option<String> {
861 if let Some(name) = Self::find_by_path(&map, path.as_ref()) {
862 map.get_mut(&name).unwrap().reload();
864
865 Some(name.to_string())
866 } else {
867 None
868 }
869 }
870
871 fn notify<P: AsRef<Path>>(&mut self, path: P) {
874 let path = std::fs::canonicalize(path).unwrap();
875
876 if let Some(sources_dir) = self.sources_dir.as_ref() {
878 if path.starts_with(sources_dir.as_path()) {
879 if let Some(name) = Self::notify_partial(&mut self.sources, &path) {
880 self.write_source(&name);
881 } else {
882 self.rescan_sources();
883 }
884 }
885 }
886
887 if let Some(templates_dir) = self.templates_dir.as_ref() {
889 if path.starts_with(templates_dir.as_path()) {
890 if let Some(name) = Self::notify_partial(&mut self.templates, &path) {
891 let sources_names = self.sources.keys().cloned().collect::<Vec<_>>();
892 for source in sources_names {
893 if self.is_source_dependent_on_partial(&source, &name) {
894 self.write_source(&source);
895 }
896 }
897 } else {
898 self.rescan_templates();
899 }
900 }
901 }
902
903 if let Some(variables_file) = self.variables_file.as_ref() {
905 if path.as_path() == variables_file {
906 self.rescan_variables();
907 }
908 }
909
910 self.check_cycle();
911 }
912
913 fn remove<P: AsRef<Path>>(&mut self, path: P, rebuild: bool) {
915 Self::find_by_path(&self.templates, path.as_ref())
917 .and_then(|name| self.templates.remove(&name));
918
919 Self::find_by_path(&self.sources, path.as_ref())
921 .and_then(|name| self.sources.remove(&name));
922
923 if rebuild {
924 self.rebuild();
925 }
926 }
927
928 fn rename<P: AsRef<Path>, P2: AsRef<Path>>(&mut self, p_from: P, p_to: P2) {
930 let p_from = p_from.as_ref();
931 let p_to = p_to.as_ref();
932
933 self.variables_file.as_mut().map(|path| {
935 if path == p_from {
936 *path = p_to.to_path_buf();
937 }
938 });
939
940 self.remove(p_from, false);
942 self.rescan_templates();
943 self.rescan_sources();
944 self.rebuild();
945 }
946
947 pub fn watch(&mut self) -> notify::Result<()> {
949 self.rebuild();
950
951 let (tx, rx) = mpsc::channel();
953 let mut watcher: RecommendedWatcher = Watcher::new(
954 tx,
955 std::time::Duration::from_millis(self.options.watcher_delays_millis),
956 )?;
957
958 if let Some(path) = self.templates_dir.as_ref() {
960 watcher.watch(path, RecursiveMode::Recursive)?;
961 }
962 if let Some(path) = self.sources_dir.as_ref() {
963 watcher.watch(path, RecursiveMode::Recursive)?;
964 }
965
966 if let Some(path) = self.variables_file.as_ref() {
968 watcher.watch(path, RecursiveMode::NonRecursive)?;
969 }
970
971 loop {
973 match rx.recv() {
974 Ok(event) => self.handle_event(event),
975 Err(e) => println!("watch error: {:?}", e),
976 }
977 }
978 }
979
980 fn handle_event(&mut self, event: DebouncedEvent) {
981 match event {
982 DebouncedEvent::NoticeWrite(..) | DebouncedEvent::NoticeRemove(..) => {}
983 DebouncedEvent::Create(path_buf) => self.notify(path_buf),
984 DebouncedEvent::Write(path_buf) => self.notify(path_buf),
985 DebouncedEvent::Chmod(path_buf) => self.notify(path_buf),
986 DebouncedEvent::Remove(path_buf) => self.notify(path_buf),
987 DebouncedEvent::Rename(path_buf_from, path_buf_to) => {
988 self.rename(path_buf_from, path_buf_to)
989 }
990 DebouncedEvent::Rescan => {}
991 DebouncedEvent::Error(..) => {}
992 }
993 }
994}