recursive-file-loader 1.0.2

Library to recursively load files via references in the files
Documentation
use crate::Error;
use std::{ops::Range, path::{PathBuf, Path}};

#[derive(Debug)]
pub struct Include {
    path: PathBuf,
    backslashes: Range<usize>,
    range: Range<usize>,
    indentation: Option<String>,
}

impl Include {
    pub fn new<P: AsRef<Path>>(
        range: Range<usize>,
        path: P,
        backslashes: Range<usize>,
        indentation: Option<String>,
    ) -> Self {
        Include {
            path: path.as_ref().to_owned(),
            backslashes,
            range,
            indentation,
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    fn indentation(&self) -> Option<&String> {
        self.indentation
            .as_ref()
            .filter(|it| !it.is_empty())
    }

    pub fn replace<S: Into<String>, F: FnOnce() -> Result<S, Error>>(
        &self,
        target: &mut String,
        producer: F,
    ) -> Result<(), Error> {
        let is_escaped = self.backslashes.len() % 2 == 1;
        if !is_escaped {
            let text = producer()?.into();
            let text = match self.indentation() {
                None => text,
                Some(indentation) => text.lines().enumerate()
                    .map(|(index, line)| match index {
                        0 => line.to_owned(),
                        _ => format!("{}{}", indentation, line),
                    })
                    .collect::<Vec<_>>()
                    .join("\n"),
            };

            let end_index = match text.chars().last() {
                Some('\n') => text.len() - 1,
                _ => text.len(),
            };
            target.replace_range(self.range.clone(), &text[0..end_index]);
        }
        escape_backslashes(target, &self.backslashes);

        Ok(())
    }
}

fn escape_backslashes(target: &mut String, backslashes: &Range<usize>) {
    if backslashes.is_empty() {
        return;
    }

    let num_backslaches_to_remove = backslashes.len() / 2;
    let new_end = backslashes.end - num_backslaches_to_remove;

    target.replace_range(backslashes.start..new_end, "");
}

#[cfg(test)]
mod test_replace {
    use rstest::rstest;
    use crate::{canonical_path::CanonicalPath, Error};
    use std::ops::Range;
    use super::Include;

    #[rstest]
    #[case("12345", 0..0, 0..4, "XXX5")]
    #[case("/1234", 0..1, 1..3, "1234")]
    #[case("//123", 0..2, 2..4, "/XXX3")]
    #[case("///12", 0..3, 3..4, "/12")]
    #[case("////1", 0..4, 4..5, "//XXX")]
    fn should_replace_text_correctly(
        #[case] input: &str,
        #[case] backslashes: Range<usize>,
        #[case] range: Range<usize>,
        #[case] expectation: &str,
    ) -> Result<(), Error>{
        let include = Include::new(
            range,
            CanonicalPath::_new("/source", "/source"),
            backslashes,
            None,
        );
        let mut input = input.to_owned();
        include.replace(&mut input, || Ok("XXX"))?;

        assert_eq!(&input, expectation);

        Ok(())
    }

    #[rstest]
    #[case("12345", 0..0, 0..4, "XXX", "  ", "XXX5")]
    #[case("12345", 0..0, 0..4, "X\nX", "  ", "X\n  X5")]
    fn should_correctly_handle_indentation(
        #[case] input: &str,
        #[case] backslashes: Range<usize>,
        #[case] range: Range<usize>,
        #[case] replacement: &str,
        #[case] indentation: &str,
        #[case] expectation: &str,
    ) -> Result<(), Error>{
        let include = Include::new(
            range,
            CanonicalPath::_new("/source", "canonical"),
            backslashes,
            Some(indentation.to_owned()),
        );
        let mut input = input.to_owned();
        include.replace(&mut input, || Ok(replacement))?;

        assert_eq!(&input, expectation);

        Ok(())
    }
}

#[cfg(test)]
mod test_escape_backslashes {
    use super::escape_backslashes;
    use rstest::rstest;
    use std::ops::Range;

    #[rstest]
    #[case("//", 0..2, "/")]
    #[case("////", 0..2, "///")]
    #[case("xx//xx", 2..4, "xx/xx")]
    fn should_escape_backslashes_correctly(
        #[case] input: &str,
        #[case] range: Range<usize>,
        #[case] expectation: &str,
    ) {
        let mut input = input.to_owned();
        escape_backslashes(&mut input, &range);
        assert_eq!(&input, expectation);
    }
}