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
#![doc = include_str!("../README.md")]
#[cfg(test)]
extern crate indoc;
#[cfg(test)]
extern crate rstest;
#[cfg(test)]
extern crate temp_dir;
extern crate thiserror;

mod canonical_path;
mod dependency_path;
mod includes;
mod loader;

use loader::Loader;
use std::path::{Path, PathBuf};

/// Load the given file path and recursively follow references to other files
/// inside it, inserting the text from references.
///
/// References are either `${include("<path>")}` or `${include_indent("<path>")}`,
/// with the latter preserving local indentation for each new line in the referenced
/// file. Paths can be relative or absolute.
///
/// The function will check references for cyclic dependencies and will return a [Error::CyclicDependency] should it detect one.
///
/// # Example
///
/// Given the following files...
///
/// start.txt:
/// ```text
/// START
///   ${include_indent("mid.txt")}
/// ```
///
/// mid.txt:
/// ```text
/// MIDDLE 1
/// MIDDLE 2
/// ${include("end.txt")}
/// ```
///
/// end.txt:
/// ```text
/// END
/// ```
///
/// Then after loading the `start.txt` file, you'll get all files combined.
///
/// ```
/// use recursive_file_loader::load_file_recursively;
/// use indoc::indoc;
/// # use temp_dir::TempDir;
/// # let dir = TempDir::new().unwrap();
/// # let start = dir.child("start.txt");
/// # let mid = dir.child("mid.txt");
/// # let end = dir.child("end.txt");
/// # std::fs::write(
/// #     &start,
/// #     "START\n  ${include_indent(\"mid.txt\")}".as_bytes(),
/// # ).unwrap();
/// # std::fs::write(
/// #     &mid,
/// #     "MIDDLE 1\nMIDDLE 2\n${include(\"end.txt\")}".as_bytes(),
/// # ).unwrap();
/// # std::fs::write(
/// #     &end,
/// #     "END".as_bytes(),
/// # ).unwrap();
///
/// let path = "start.txt";
/// # let path = &start;
///
/// let result = load_file_recursively(&path).unwrap();
///
/// assert_eq!(&result, indoc!("
///     START
///       MIDDLE 1
///       MIDDLE 2
///       END")
/// );
/// ```
///
/// Note that the indentation in `start.txt` has been applied to everything `start.txt` included.
pub fn load_file_recursively<P: AsRef<Path>>(origin: P) -> Result<String, Error> {
    Loader::new().load_file_recursively(origin)
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
    #[error("file not found: '{0}'")]
    FileNotFound(PathBuf),

    #[error("cyclic dependency detected between '{0}' and '{1}'")]
    CyclicDependency(PathBuf, PathBuf),

    #[error("IO Error")]
    IOError(#[from] std::io::Error),
}