iftree 1.0.0

Include many files in your Rust code for self-contained binaries.
Documentation
use super::sanitize_name;
use crate::model;
use std::array;
use std::iter;
use std::path;

pub fn main(paths: Vec<model::Path>) -> model::Result<model::Forest> {
    let mut forest = model::Forest::new();

    for path in paths.into_iter() {
        add_path(&mut forest, path)?;
    }

    let mut index = 0;
    overwrite_indices_in_order(&mut forest, &mut index);

    Ok(forest)
}

fn add_path(forest: &mut model::Forest, path: model::Path) -> model::Result<()> {
    match path.relative.last() {
        None => Err(model::Error::UnexpectedEmptyRelativePath {
            absolute_path: path::PathBuf::from(path.absolute),
        }),

        Some(filename) => {
            let file = model::File {
                identifier: sanitize_name::main(
                    filename,
                    sanitize_name::Convention::ScreamingSnakeCase,
                ),
                index: 0,
                relative_path: path.relative.join(NORMALIZED_FOLDER_SEPARATOR),
                absolute_path: path.absolute,
            };

            let mut reverse_path = path.relative;
            reverse_path.reverse();

            add_file(forest, reverse_path, file)
        }
    }
}

const NORMALIZED_FOLDER_SEPARATOR: &str = "/";

fn add_file(
    parent: &mut model::Forest,
    mut reverse_path: Vec<String>,
    file: model::File,
) -> model::Result<()> {
    match reverse_path.pop() {
        None => Err(model::Error::UnexpectedPathCollision(path::PathBuf::from(
            file.relative_path,
        ))),

        Some(name) => match parent.get_mut(&name) {
            None => {
                let child = get_singleton_tree(reverse_path, file, &name);
                parent.insert(name, child);
                Ok(())
            }

            Some(model::Tree::File(_)) => Err(model::Error::UnexpectedPathCollision(
                path::PathBuf::from(file.relative_path),
            )),

            Some(model::Tree::Folder(model::Folder { forest, .. })) => {
                add_file(forest, reverse_path, file)
            }
        },
    }
}

fn get_singleton_tree(reverse_path: Vec<String>, file: model::File, root: &str) -> model::Tree {
    let parents = get_folder_identifiers(
        &reverse_path
            .iter()
            .skip(1)
            .map(|name| name.as_ref())
            .chain(iter::once(root))
            .collect::<Vec<_>>(),
    );

    let mut tree = model::Tree::File(file);

    for (child, parent) in reverse_path.into_iter().zip(parents.into_iter()) {
        let forest = array::IntoIter::new([(child, tree)]).collect();
        tree = model::Tree::Folder(model::Folder {
            identifier: parent,
            forest,
        });
    }

    tree
}

fn get_folder_identifiers(names: &[&str]) -> Vec<syn::Ident> {
    names
        .iter()
        .map(|name| sanitize_name::main(name, sanitize_name::Convention::SnakeCase))
        .collect()
}

fn overwrite_indices_in_order(forest: &mut model::Forest, index: &mut usize) {
    for tree in forest.values_mut() {
        match tree {
            model::Tree::File(file) => {
                file.index = *index;
                *index += 1;
            }

            model::Tree::Folder(model::Folder { forest, .. }) => {
                overwrite_indices_in_order(forest, index)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn handles_empty_set() {
        let actual = main(vec![]);

        let actual = actual.unwrap();
        let expected = model::Forest::new();
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_files() {
        let actual = main(vec![
            model::Path {
                relative: vec![String::from('B')],
                absolute: String::from("/a/B"),
            },
            model::Path {
                relative: vec![String::from('c')],
                absolute: String::from("/a/c"),
            },
        ]);

        let actual = actual.unwrap();
        let expected = array::IntoIter::new([
            (
                String::from('B'),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#B"),
                    index: 0,
                    relative_path: String::from('B'),
                    absolute_path: String::from("/a/B"),
                }),
            ),
            (
                String::from('c'),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#C"),
                    index: 1,
                    relative_path: String::from('c'),
                    absolute_path: String::from("/a/c"),
                }),
            ),
        ])
        .collect();
        assert_eq!(actual, expected);
    }

    #[test]
    fn handles_folders() {
        let actual = main(vec![
            model::Path {
                relative: vec![String::from('a')],
                absolute: String::from("/a"),
            },
            model::Path {
                relative: vec![String::from('b'), String::from('a'), String::from('b')],
                absolute: String::from("/b/a/b"),
            },
            model::Path {
                relative: vec![String::from('b'), String::from('c')],
                absolute: String::from("/b/c"),
            },
        ]);

        let actual = actual.unwrap();
        let expected = array::IntoIter::new([
            (
                String::from('a'),
                model::Tree::File(model::File {
                    identifier: quote::format_ident!("r#A"),
                    index: 0,
                    relative_path: String::from('a'),
                    absolute_path: String::from("/a"),
                }),
            ),
            (
                String::from('b'),
                model::Tree::Folder(model::Folder {
                    identifier: quote::format_ident!("r#b"),
                    forest: array::IntoIter::new([
                        (
                            String::from('a'),
                            model::Tree::Folder(model::Folder {
                                identifier: quote::format_ident!("r#a"),
                                forest: array::IntoIter::new([(
                                    String::from('b'),
                                    model::Tree::File(model::File {
                                        identifier: quote::format_ident!("r#B"),
                                        index: 1,
                                        relative_path: String::from("b/a/b"),
                                        absolute_path: String::from("/b/a/b"),
                                    }),
                                )])
                                .collect(),
                            }),
                        ),
                        (
                            String::from('c'),
                            model::Tree::File(model::File {
                                identifier: quote::format_ident!("r#C"),
                                index: 2,
                                relative_path: String::from("b/c"),
                                absolute_path: String::from("/b/c"),
                            }),
                        ),
                    ])
                    .collect(),
                }),
            ),
        ])
        .collect();
        assert_eq!(actual, expected);
    }

    #[test]
    fn given_empty_relative_path_it_errs() {
        let actual = main(vec![model::Path {
            relative: vec![],
            absolute: String::from("/a/b"),
        }]);

        let actual = actual.unwrap_err();
        let expected = model::Error::UnexpectedEmptyRelativePath {
            absolute_path: path::PathBuf::from("/a/b"),
        };
        assert_eq!(actual, expected);
    }

    #[test]
    fn given_path_collision_it_errs() {
        let actual = main(vec![
            model::Path {
                relative: vec![String::from('a'), String::from('b')],
                ..model::stubs::path()
            },
            model::Path {
                relative: vec![String::from('a'), String::from('b')],
                ..model::stubs::path()
            },
        ]);

        let actual = actual.unwrap_err();
        let expected = model::Error::UnexpectedPathCollision(path::PathBuf::from("a/b"));
        assert_eq!(actual, expected);
    }
}