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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
//! Data path representations.
//!
//! When resolving data from a collection of files, we iterate
//! down from top-level, and descend a [DataPath] until exhaustion,
//! merging values as we go.  That way, more specific data paths
//! override fields as necessary, and we can be flexible about how
//! the data for a yaml mapping is broken up into smaller files.
//!
//! At a high-level, given a data address of ["a", "b", "c"], we can iterate like so:
//!
//! - look for `a.b.c` in `index.yml`
//! - look for `b.c` in `a.yml`
//! - look for `b.c` in `a/index.yml`
//! - look for `c` in `a/b.yml`
//! - look for `c` in `a/b/index.yml`
//! - merge all data from `a/b/c.yml`
//! - merge all data from `a/b/c/index.yml`
//!
//! [DataPath] provides a simple means for performing this process.
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};

use super::values::{take_sub_value_at_address, value_from_file};
use super::DataResolverError;

enum Level {
    Dir,
    File,
}

/// Represents a position in the data directory when resolving data.
pub struct DataPath<'a> {
    level: Level,
    path: PathBuf,
    address: &'a [&'a str],
}

impl<'a> DataPath<'a> {
    /// Takes self by value, and steps to the next logical data path (mutating self).  Returns None
    /// if there's nowhere to go.
    pub fn descend(mut self) -> Option<Self> {
        use Level::{Dir, File};
        match &self.level {
            File => {
                if !self.path.is_dir() {
                    return None;
                }
                self.level = Dir;
            }
            Dir => {
                if let Some((head, tail)) = self.address.split_first() {
                    self.path.push(head);
                    self.address = tail;
                    self.level = File;
                }
            }
        }
        Some(self)
    }
    /// Returns whether or not this instance is exhausted, i.e. when [descend](DataPath::descend()) would
    /// be a no-op
    pub fn done(&self) -> bool {
        use Level::{Dir, File};
        match &self.level {
            File => false,
            Dir => self.address.is_empty(),
        }
    }
    fn file(&self) -> PathBuf {
        self.path.with_extension("yml")
    }
    /// Returns the current path file stem (i.e. basename without file extension)
    pub fn file_stem(&self) -> Option<&OsStr> {
        self.path.file_stem()
    }
    fn get_value(&self, path: &Path) -> Result<serde_yaml::Value, DataResolverError> {
        let mut value = value_from_file(path)?;
        take_sub_value_at_address(&mut value, self.address)
    }
    fn index(&self) -> PathBuf {
        self.path.join("index.yml")
    }
    /// Spawns a new instance with a given path suffix appended, and same data address.
    pub fn join<P: AsRef<Path>>(&self, tail: P) -> Self {
        Self {
            level: Level::File,
            path: self.path.join(tail),
            address: self.address,
        }
    }
    /// Creates a new instance from a path and data address.
    pub fn new<P: Into<PathBuf>>(path: P, address: &'a [&'a str]) -> Self {
        Self {
            address,
            level: Level::Dir,
            path: path.into(),
        }
    }
    /// Creates a vector of new instances, one for each file/directory at the current path.
    pub fn sub_paths(&self) -> Vec<Self> {
        fs::read_dir(&self.path).map_or_else(
            |_| vec![],
            |reader| {
                reader
                    .filter_map(|dir_entry| dir_entry.ok())
                    .map(|dir_entry| dir_entry.file_name())
                    .map(|p| self.join(p))
                    .collect()
            },
        )
    }
    /// Tries to convert the current position to a [serde_yaml::Value].
    pub fn value(&self) -> Result<serde_yaml::Value, DataResolverError> {
        match &self.level {
            Level::Dir => self.get_value(&self.index()),
            Level::File => self.get_value(&self.file()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use color_eyre::Result;
    use indoc::indoc;
    use test_files::TestFiles;
    use test_utils::yaml;

    trait GetDataPath<'a> {
        fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a>;
    }

    impl<'a> GetDataPath<'a> for TestFiles {
        fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a> {
            DataPath::new(self.path(), address)
        }
    }

    #[test]
    fn resolves_num_at_root() -> Result<()> {
        let mocks = TestFiles::new();
        mocks.file(
            "index.yml",
            indoc! {"
                ---
                3
            "},
        );
        let v = mocks.data_path(&[]).value()?;
        assert_eq!(v, yaml! {"3"});
        Ok(())
    }

    #[test]
    fn resolves_num_deeper() -> Result<()> {
        let mocks = TestFiles::new();
        mocks.file(
            "index.yml",
            indoc! {"
	            ---
	            a:
	                b:
	                    c: 3
	        "},
        );
        let v = mocks.data_path(&["a", "b", "c"]).value()?;
        assert_eq!(v, yaml! {"3"});
        Ok(())
    }

    #[test]
    fn resolves_list_num_at_index() -> Result<()> {
        let mocks = TestFiles::new();
        mocks.file(
            "index.yml",
            indoc! {"
	            ---
	            a:
	            - 4
	            - 5
	            - 6
	        "},
        );
        let v = mocks.data_path(&["a"]).value()?;
        assert_eq!(v, yaml! {"[4, 5, 6]"});
        Ok(())
    }
}