mini_builder_rs/builder/
mod.rs

1//! Combines the full pipeline of tokenizing, parsing, building nodes with IO.
2//!
3//! Each of the pipeline's components are available individually. However this
4//! crate's goal is to provide a simple struct that users can use by only
5//! providing sources, templates, variables and function and let the crate
6//! handle the rest. [Builder] is this struct.
7//!
8//! # IO
9//! The [Target] enum specifies where text is read and written to.
10//! - [Target::Local] corresponds to reading and writing to a string.
11//! - [Target::External] corresponds to reading and writing to a file.
12//! When a builder is constructed, a path to two directories `sources` and
13//! templates `templates` can be provided. Files under these directories will
14//! automatically be loaded with a [Target::External] read.
15//! [Target::Local] read files can be added with `add_local_*` functions.
16//!
17//! # Watching
18//! The builder has a `watch` function which will automatically re-read any
19//! files under the `sources`, `templates` directories and the `variables` file.
20//!
21//! # Variables file
22//! If a variables files is provided to the builder, it will load its contents
23//! as global variables that will be available to all sources and templates.
24//! Additionally, under `watch`, the builder will listen to changes in that file
25//! and automatically reload its variables and rebuild any sources that depend
26//! on these variables.
27//!
28//! # Examples
29//! See the examples directory
30
31pub 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
61/// Recursively gets all files from a directory, returning their path and
62/// relative name to the root folder.
63/// Example: 'root/dir1/file1.txt' -> ['dir1', 'file1.txt']
64fn 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			// ensure entry is valid
78			let entry = match entry {
79				Ok(entry) => entry,
80				_ => continue,
81			};
82
83			// ensure the name is valid (either file or directory name)
84			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			// ensure that the name is ascii
99			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			// ensure entry has metadata
107			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			// handle directories - recurse
118			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			// handle files
132			if metadata.is_file() {
133				let path = entry.path();
134
135				// get file name, choosing if to include or
136				// exclude file extensions
137				let extracted_name = if ignore_extensions {
138					path.file_stem()
139				} else {
140					path.file_name()
141				};
142
143				// process the chosen name, if valid add to the
144				// name stack and push to the output
145				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/// Specifies under which circumstances the builder should log a warning.
178#[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	// paths
218	sources_dir: Option<PathBuf>,
219	templates_dir: Option<PathBuf>,
220	generated_dir: Option<PathBuf>,
221	variables_file: Option<PathBuf>,
222	// builders
223	sources: HashMap<String, Partial<'p>>,
224	templates: HashMap<String, Partial<'p>>,
225	// globals
226	variables: Variables,
227	functions: HashMap<String, ValueFunction>,
228	// options
229	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		// helper functions that creates directories and transforms relative
241		// paths to global
242		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		// map paths
264		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		// optional - clear generated dir
270		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		// helper function, returns true if either a or b is under target
277		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		// check that none of the directories contain each other
302		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			// directories
311			sources_dir,
312			templates_dir,
313			generated_dir,
314			variables_file,
315			// builders
316			sources: HashMap::new(),
317			templates: HashMap::new(),
318			// global variables
319			variables: HashMap::new(),
320			functions: HashMap::new(),
321			// options
322			options,
323		})
324	}
325
326	/// Helper function for adding either local sources or local templates.
327	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	/// Adds a local source.
340	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	// Adds a local template.
350	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	/// Helper function for adding either several local sources or templates
360	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	/// Adds multiple local sources.
379	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	/// Adds multiple local templates.
388	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	/// Adds a variable.
397	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	/// Adds multiple variables.
403	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	/// Adds a function.
411	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	/// Adds functions.
417	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	/// Recursively checks if a partial or its dependents depend on a variable.
425	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	/// Recursively checks if a partial or its dependents depend on a partial.
474	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	/// Updates the global variables, and rebuilds all the sources that rely on
527	/// them.
528	pub fn update_variables<'a>(&mut self, variables: Vec<(String, impl Borrow<Expression<'a>>)>) {
529		// create an empty evaluation context
530		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		// calculate the value of each variable and extract the names
545		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		// list sources to update
553		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		// update sources
561		for name in sources_to_update {
562			self.write_source(&name);
563		}
564	}
565
566	/// If there exists a circular dependency between templates, it will be
567	/// returned.
568	fn find_cycle(&mut self) -> Option<Vec<String>> {
569		/// Runs dfs cycle search on the templates
570		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			// when a cycle is found, this if statement is triggered
579			if seen.contains(partial_name) {
580				// indicate to the following templates to add their names to
581				// the cycle vec
582				*add_to_cycle = true;
583
584				cycle.push(partial_name.to_string());
585				return true;
586			}
587
588			// the current partial was already checked
589			if previously_seen.contains(partial_name) {
590				return false;
591			}
592
593			// push current partial name to the dfs stack
594			seen.insert(partial_name.to_string());
595
596			// globally store visited templates
597			previously_seen.insert(partial_name.to_string());
598
599			// iterate over the templates dependent upon and continue
600			// searching for cycles
601			if let Some(partial) = templates.get(partial_name) {
602				for dep in partial.get_dependencies().iter_builder_dependencies() {
603					// a cycle was found somewhere inside the call stack
604					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 the current name equals to the first name,
609							// then the cycle's beginning was found
610							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			// pop current partial name
620			seen.remove(partial_name);
621
622			false
623		}
624
625		// starting at each source, run dfs over the templates and try to find a cycle
626		let mut previously_seen = HashSet::new(); // global dfs helper
627		for source in self.sources.values_mut() {
628			for partial_name in source.get_dependencies().iter_builder_dependencies() {
629				// local dfs helpers
630				let mut seen = HashSet::new();
631				let mut cycle = Vec::new();
632				let mut add_to_cycle = true;
633
634				// run dfs
635				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		// false
649		None
650	}
651
652	/// If specified by the options calls `find_cycle` and warns in case of a
653	/// cycle
654	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	/// Evaluates a source and writes it to its target.
666	pub fn write_source(&mut self, name: &str) {
667		// create the evaluation context
668		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		// get the source
679		let source = self.sources.get_mut(name).unwrap();
680
681		// evaluate the source, storing its output to its target
682		source.evaluate_and_store(&mut context);
683	}
684
685	/// Finds and returns a source by its name.
686	pub fn get_source(&self, name: &str) -> Option<&Partial> {
687		self.sources.get(name)
688	}
689
690	/// Rescans and rebuilds the entire project.
691	pub fn rebuild(&mut self) {
692		// clear
693		// self.sources.clear();
694		// self.templates.clear();
695		// self.variables.clear();
696
697		// rescan
698		self.rescan_templates();
699		self.rescan_sources();
700		self.rescan_variables();
701
702		self.check_cycle();
703
704		// iterate over the sources and generate output
705		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	/// Helper function, scans a directory, adding missing partials to the
712	/// specified map
713	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				// join the vec into a string to create a name
723				let name = name_stack.clone().join("/");
724
725				// if the name does not exist in the map, create a new partial and add it
726				if !map.contains_key(&name) {
727					// calculate the write target
728					let write = if let Some(write_path) = write_path {
729						// join the vec into a path to create the target's path
730						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						// if a write path is not give, the partial is assumed to have a local write target
737						Target::Local(String::new())
738					};
739
740					// TODO: what to do about this error?
741					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	/// Rescans the templates directory, parsing and adding all the missing
755	/// files.
756	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	/// Rescans the sources directory, parsing and adding all the missing files.
767	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			// iterate over each line and evaluate it
787			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						// tokenize
794						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						// parse
809						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						// ensure there is only one node
820						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						// and it is an expression
828						if let NodeContent::Expression(expression) = &nodes[0].content {
829							// and its an expression
830							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	/// Notify the system that a partial has been either changed or added,
856	/// returns the name of the partial if it was found.
857	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			// the path exists, so the partial needs to be updated
863			map.get_mut(&name).unwrap().reload();
864
865			Some(name.to_string())
866		} else {
867			None
868		}
869	}
870
871	/// Notify the system that either a source or a partial has been either
872	/// changed or added.
873	fn notify<P: AsRef<Path>>(&mut self, path: P) {
874		let path = std::fs::canonicalize(path).unwrap();
875
876		// sources
877		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		// partials
888		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		// variables
904		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	/// Notify the system that either a source or a partial has been deleted.
914	fn remove<P: AsRef<Path>>(&mut self, path: P, rebuild: bool) {
915		// remove from templates
916		Self::find_by_path(&self.templates, path.as_ref())
917			.and_then(|name| self.templates.remove(&name));
918
919		// remove from sources
920		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	/// Notify the system that either a source or a partial has been renamed.
929	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		// check if the variables file was renamed
934		self.variables_file.as_mut().map(|path| {
935			if path == p_from {
936				*path = p_to.to_path_buf();
937			}
938		});
939
940		// check if the sources or templates were renamed
941		self.remove(p_from, false);
942		self.rescan_templates();
943		self.rescan_sources();
944		self.rebuild();
945	}
946
947	// watching the files and updating them live
948	pub fn watch(&mut self) -> notify::Result<()> {
949		self.rebuild();
950
951		// create a file watcher and a channel for notifying for events
952		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		// watch the templates and the sources directory
959		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		// watch the variables file
967		if let Some(path) = self.variables_file.as_ref() {
968			watcher.watch(path, RecursiveMode::NonRecursive)?;
969		}
970
971		// main loop - listen for events and regenerate files as neccessary
972		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}