libscilo/config/root_dirs.rs
1use std::path::PathBuf;
2
3use crate::{config::file::ConfigFileRootDirs, project_root};
4
5/// Expected top-level directories to organize the project folder.
6#[derive(Clone, Debug)]
7pub struct RootDirs {
8 /// Top-level path for code.
9 ///
10 /// This folder contains all the code used to process files from [`data`][Self::data],
11 /// perform statistical analyses, create visualizations, and save the outputs
12 /// in [`results`][Self::results].
13 /// Generally, this directory contains a series of subdirectories, each of
14 /// which contain a set of related scripts for a single research question.
15 /// For example, each subdirectory may contain:
16 /// - a workflow file (e.g. [`Snakefile`](https://snakemake.github.io/) or [`Nextflow`](https://www.nextflow.io/))
17 /// - a data pre-processing script
18 /// - a statistical analysis and calculation script
19 /// - a plotting script
20 pub(crate) code: Option<PathBuf>,
21
22 /// Top-level path for data.
23 ///
24 /// All raw and processed data should be found within this directory.
25 /// The datasets located here will then be sourced by code within the [`code`][Self::code]
26 /// directory and used to generate outputs in the [`results`][Self::results]
27 /// directory.
28 pub(crate) data: Option<PathBuf>,
29
30 /// Top-level path for results.
31 ///
32 /// Code and scripts in the [`code`][Self::code] directory should create outputs
33 /// in this directory to ensure a separation of inputs, code, and outputs.
34 /// Generally, this directory contains a series of subdirectories, each of
35 /// which will contain all the outputs originating from the code in the
36 /// corresponding [`code`][Self::code] subdirectory.
37 pub(crate) results: Option<PathBuf>,
38
39 /// Top-level path for vendored external code.
40 ///
41 /// In many research projects you will need a copy of some external code,
42 /// such as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules),
43 /// or a package manifest of some kind, like a [Nix derivation](https://nixos.org/)
44 /// or an [Anaconda recipe](https://anaconda.org/).
45 /// This folder can be used to house all of the relevant code so as to not
46 /// populate the custom code present in [`code`][Self::code].
47 pub(crate) external: Option<PathBuf>,
48
49 /// Top-level path for documentation.
50 ///
51 /// Documentation such as a manuscript, experimental descriptions from contract
52 /// research organizations, or interactive HTML notebooks displaying the data
53 /// and its results can go here.
54 /// Data that belongs in [`data`][Self::data] or notebooks which process data
55 /// that belong in [`code`][Self::code] should not be placed in here.
56 pub(crate) docs: Option<PathBuf>,
57}
58
59impl Default for RootDirs {
60 fn default() -> Self {
61 let root = project_root();
62
63 Self {
64 code: Some(root.join("code")),
65 data: Some(root.join("data")),
66 results: Some(root.join("results")),
67 external: Some(root.join("pkgs")),
68 docs: Some(root.join("docs")),
69 }
70 }
71}
72
73impl From<Option<ConfigFileRootDirs>> for RootDirs {
74 fn from(value: Option<ConfigFileRootDirs>) -> Self {
75 let defaults = Self::default();
76
77 match value {
78 Some(dirs) => {
79 let code = parse_root_dir_name(dirs.code.as_deref(), defaults.code);
80 let data = parse_root_dir_name(dirs.data.as_deref(), defaults.data);
81 let docs = parse_root_dir_name(dirs.docs.as_deref(), defaults.docs);
82 let external = parse_root_dir_name(dirs.external.as_deref(), defaults.external);
83 let results = parse_root_dir_name(dirs.results.as_deref(), defaults.results);
84
85 Self {
86 code,
87 data,
88 docs,
89 external,
90 results,
91 }
92 }
93 None => RootDirs::default(),
94 }
95 }
96}
97
98impl IntoIterator for RootDirs {
99 type Item = Option<PathBuf>;
100
101 type IntoIter = std::array::IntoIter<Option<PathBuf>, 5>;
102
103 fn into_iter(self) -> Self::IntoIter {
104 [self.code, self.data, self.docs, self.external, self.results].into_iter()
105 }
106}
107
108/// A helper function to parse the directory name for a root directory.
109///
110/// If the directory specified in the configuration file is an empty string,
111/// then it will be considered as `None`.
112fn parse_root_dir_name(s: Option<&str>, default: Option<PathBuf>) -> Option<PathBuf> {
113 match s {
114 Some("") => None,
115 Some(dir_name) => Some(PathBuf::from(dir_name)),
116 None => default,
117 }
118}