confql_data_resolver/
data_path.rs

1//! Data path representations.
2//!
3//! When resolving data from a collection of files, we iterate
4//! down from top-level, and descend a [DataPath] until exhaustion,
5//! merging values as we go.  That way, more specific data paths
6//! override fields as necessary, and we can be flexible about how
7//! the data for a yaml mapping is broken up into smaller files.
8//!
9//! At a high-level, given a data address of ["a", "b", "c"], we can iterate like so:
10//!
11//! - look for `a.b.c` in `index.yml`
12//! - look for `b.c` in `a.yml`
13//! - look for `b.c` in `a/index.yml`
14//! - look for `c` in `a/b.yml`
15//! - look for `c` in `a/b/index.yml`
16//! - merge all data from `a/b/c.yml`
17//! - merge all data from `a/b/c/index.yml`
18//!
19//! [DataPath] provides a simple means for performing this process.
20use std::ffi::OsStr;
21use std::fs;
22use std::path::{Path, PathBuf};
23
24use super::values::{take_sub_value_at_address, value_from_file};
25use super::DataResolverError;
26
27enum Level {
28    Dir,
29    File,
30}
31
32/// Represents a position in the data directory when resolving data.
33pub struct DataPath<'a> {
34    level: Level,
35    path: PathBuf,
36    address: &'a [&'a str],
37}
38
39impl<'a> DataPath<'a> {
40    /// Takes self by value, and steps to the next logical data path (mutating self).  Returns None
41    /// if there's nowhere to go.
42    pub fn descend(mut self) -> Option<Self> {
43        use Level::{Dir, File};
44        match &self.level {
45            File => {
46                if !self.path.is_dir() {
47                    return None;
48                }
49                self.level = Dir;
50            }
51            Dir => {
52                if let Some((head, tail)) = self.address.split_first() {
53                    self.path.push(head);
54                    self.address = tail;
55                    self.level = File;
56                }
57            }
58        }
59        Some(self)
60    }
61    /// Returns whether or not this instance is exhausted, i.e. when [descend](DataPath::descend()) would
62    /// be a no-op
63    pub fn done(&self) -> bool {
64        use Level::{Dir, File};
65        match &self.level {
66            File => false,
67            Dir => self.address.is_empty(),
68        }
69    }
70    fn file(&self) -> PathBuf {
71        self.path.with_extension("yml")
72    }
73    /// Returns the current path file stem (i.e. basename without file extension)
74    pub fn file_stem(&self) -> Option<&OsStr> {
75        self.path.file_stem()
76    }
77    fn get_value(&self, path: &Path) -> Result<serde_yaml::Value, DataResolverError> {
78        let mut value = value_from_file(path)?;
79        take_sub_value_at_address(&mut value, self.address)
80    }
81    fn index(&self) -> PathBuf {
82        self.path.join("index.yml")
83    }
84    /// Spawns a new instance with a given path suffix appended, and same data address.
85    pub fn join<P: AsRef<Path>>(&self, tail: P) -> Self {
86        Self {
87            level: Level::File,
88            path: self.path.join(tail),
89            address: self.address,
90        }
91    }
92    /// Creates a new instance from a path and data address.
93    pub fn new<P: Into<PathBuf>>(path: P, address: &'a [&'a str]) -> Self {
94        Self {
95            address,
96            level: Level::Dir,
97            path: path.into(),
98        }
99    }
100    /// Creates a vector of new instances, one for each file/directory at the current path.
101    pub fn sub_paths(&self) -> Vec<Self> {
102        fs::read_dir(&self.path).map_or_else(
103            |_| vec![],
104            |reader| {
105                reader
106                    .filter_map(|dir_entry| dir_entry.ok())
107                    .map(|dir_entry| dir_entry.file_name())
108                    .map(|p| self.join(p))
109                    .collect()
110            },
111        )
112    }
113    /// Tries to convert the current position to a [serde_yaml::Value].
114    pub fn value(&self) -> Result<serde_yaml::Value, DataResolverError> {
115        match &self.level {
116            Level::Dir => self.get_value(&self.index()),
117            Level::File => self.get_value(&self.file()),
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use color_eyre::Result;
126    use indoc::indoc;
127    use test_files::TestFiles;
128    use test_utils::yaml;
129
130    trait GetDataPath<'a> {
131        fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a>;
132    }
133
134    impl<'a> GetDataPath<'a> for TestFiles {
135        fn data_path(&self, address: &'a [&'a str]) -> DataPath<'a> {
136            DataPath::new(self.path(), address)
137        }
138    }
139
140    #[test]
141    fn resolves_num_at_root() -> Result<()> {
142        let mocks = TestFiles::new();
143        mocks.file(
144            "index.yml",
145            indoc! {"
146                ---
147                3
148            "},
149        );
150        let v = mocks.data_path(&[]).value()?;
151        assert_eq!(v, yaml! {"3"});
152        Ok(())
153    }
154
155    #[test]
156    fn resolves_num_deeper() -> Result<()> {
157        let mocks = TestFiles::new();
158        mocks.file(
159            "index.yml",
160            indoc! {"
161	            ---
162	            a:
163	                b:
164	                    c: 3
165	        "},
166        );
167        let v = mocks.data_path(&["a", "b", "c"]).value()?;
168        assert_eq!(v, yaml! {"3"});
169        Ok(())
170    }
171
172    #[test]
173    fn resolves_list_num_at_index() -> Result<()> {
174        let mocks = TestFiles::new();
175        mocks.file(
176            "index.yml",
177            indoc! {"
178	            ---
179	            a:
180	            - 4
181	            - 5
182	            - 6
183	        "},
184        );
185        let v = mocks.data_path(&["a"]).value()?;
186        assert_eq!(v, yaml! {"[4, 5, 6]"});
187        Ok(())
188    }
189}