diskplan_config/
lib.rs

1//! Configuration for the system
2//!
3//! Example config file:
4//! ```
5//! # use diskplan_config::ConfigFile;
6//! # let config_text = r#"
7#![doc = include_str!("../examples/quickstart/diskplan.toml")]
8//! # "#;
9//! # let config: ConfigFile = config_text.try_into().unwrap();
10//! # let stem = config.stems.get("main").expect("no main stem");
11//! # assert_eq!(stem.root().path().as_str(), "/tmp/diskplan-root");
12//! # assert_eq!(stem.schema().as_str(), "simple-schema.diskplan");
13//! ```
14#![warn(missing_docs)]
15
16use std::{collections::HashMap, fmt::Write as _, ops::Deref};
17
18use anyhow::{anyhow, Context as _, Result};
19use camino::{Utf8Path, Utf8PathBuf};
20
21use diskplan_filesystem::Root;
22use diskplan_schema::SchemaNode;
23
24mod cache;
25mod file;
26pub use self::{
27    cache::SchemaCache,
28    file::{ConfigFile, ConfigStem},
29};
30
31/// Application configuration
32pub struct Config<'t> {
33    /// The directory to produce. This must be absolute and begin with one of the configured roots
34    target: Utf8PathBuf,
35
36    /// Whether to apply the changes (otherwise, only simulate and print)
37    apply: bool,
38
39    /// Directory to search for schemas
40    schema_directory: Utf8PathBuf,
41
42    /// Map user names, for example "root:admin,janine:jfu"
43    usermap: HashMap<String, String>,
44
45    /// Map groups names
46    groupmap: HashMap<String, String>,
47
48    stems: Stems<'t>,
49}
50
51impl<'t> Config<'t> {
52    /// Constructs a new application configuration
53    ///
54    /// The `target` path defines a directory into which we will begin
55    ///
56    /// The `apply` flag controls whether changes should be applied to
57    /// the filesystem or just reported
58    pub fn new(target: impl AsRef<Utf8Path>, apply: bool) -> Self {
59        Config {
60            target: target.as_ref().to_owned(),
61            apply,
62            schema_directory: Utf8PathBuf::from("/"),
63            usermap: Default::default(),
64            groupmap: Default::default(),
65            stems: Default::default(),
66        }
67    }
68
69    /// Loads configuation options from the given `path`
70    pub fn load(&mut self, path: impl AsRef<Utf8Path>) -> Result<()> {
71        let ConfigFile {
72            stems,
73            schema_directory,
74        } = ConfigFile::load(path.as_ref())?;
75        self.schema_directory = schema_directory.unwrap_or_else(|| {
76            path.as_ref()
77                .parent()
78                .expect("No parent directory for config file")
79                .to_owned()
80        });
81        for (_, stem) in stems.into_iter() {
82            let schema_path = self.schema_directory.join(stem.schema());
83            self.stems.add(stem.root().to_owned(), schema_path)
84        }
85        Ok(())
86    }
87
88    /// Updates this configuration's user name map with the one provided
89    pub fn apply_user_map(&mut self, usermap: HashMap<String, String>) {
90        self.usermap.extend(usermap.into_iter())
91    }
92
93    /// Updates this configuration's group name map with the one provided
94    pub fn apply_group_map(&mut self, groupmap: HashMap<String, String>) {
95        self.groupmap.extend(groupmap.into_iter())
96    }
97
98    /// The path intended to be constructed
99    pub fn target_path(&self) -> &Utf8Path {
100        self.target.as_ref()
101    }
102
103    /// Whether to apply the changes to disk
104    pub fn will_apply(&self) -> bool {
105        self.apply
106    }
107
108    /// Add a root and schema definition file path pair
109    pub fn add_stem(&mut self, root: Root, schema_path: impl AsRef<Utf8Path>) {
110        self.stems.add(root, schema_path)
111    }
112
113    /// Add a root and schema definition file path pair, adding its already parsed schema to the cache
114    ///
115    /// The file path will not be read; this can be used for testing
116    pub fn add_precached_stem(
117        &mut self,
118        root: Root,
119        schema_path: impl AsRef<Utf8Path>,
120        schema: SchemaNode<'t>,
121    ) {
122        self.stems.add_precached(root, schema_path, schema)
123    }
124
125    /// Returns an iterator over the configured [`Root`]s
126    pub fn stem_roots(&self) -> impl Iterator<Item = &Root> {
127        self.stems.roots()
128    }
129
130    /// Returns the schema for a given path, loaded on demand, or an error if the schema cannot be
131    /// found, has a syntax error, or otherwise fails to load
132    pub fn schema_for<'s, 'p>(&'s self, path: &'p Utf8Path) -> Result<(&SchemaNode<'t>, &Root)>
133    where
134        's: 't,
135    {
136        self.stems.schema_for(path)
137    }
138
139    /// Applies the user map to the given user name, returning itself if no mapping exists for
140    /// this name
141    pub fn map_user<'a>(&'a self, name: &'a str) -> &'a str {
142        self.usermap.get(name).map(|s| s.deref()).unwrap_or(name)
143    }
144
145    /// Applies the group map to the given group name, returning itself if no mapping exists for
146    /// this name
147    pub fn map_group<'a>(&'a self, name: &'a str) -> &'a str {
148        self.groupmap.get(name).map(|s| s.deref()).unwrap_or(name)
149    }
150}
151
152/// Collection of rooted schemas; a map of each [`Root`] to the [`SchemaNode`] configured for this root
153#[derive(Default)]
154pub struct Stems<'t> {
155    /// Maps root path to the schema definition's file path
156    path_map: HashMap<Root, Utf8PathBuf>,
157
158    /// A cache of loaded schemas from their definition files
159    cache: SchemaCache<'t>,
160}
161
162impl<'t> Stems<'t> {
163    /// Constructs an empty mapping
164    pub fn new() -> Self {
165        Default::default()
166    }
167
168    /// Configures the given `root` path with the path where a schema for this root may be found
169    pub fn add(&mut self, root: Root, schema_path: impl AsRef<Utf8Path>) {
170        self.path_map.insert(root, schema_path.as_ref().to_owned());
171    }
172
173    /// Configures the given `root` path with the path where a schema for this root may be found
174    /// but then populates the internal cache with the schema data itself, avoiding any disk access
175    ///
176    /// This is primarily used for tests
177    pub fn add_precached(
178        &mut self,
179        root: Root,
180        schema_path: impl AsRef<Utf8Path>,
181        schema: SchemaNode<'t>,
182    ) {
183        let schema_path = schema_path.as_ref();
184        self.cache.inject(schema_path, schema);
185        self.add(root, schema_path);
186    }
187
188    /// Returns an iterator over the roots configures in this map
189    pub fn roots(&self) -> impl Iterator<Item = &Root> {
190        self.path_map.keys()
191    }
192
193    /// Looks up the schema associated with the root of a given `path` within this root
194    pub fn schema_for<'s, 'p>(&'s self, path: &'p Utf8Path) -> Result<(&SchemaNode<'t>, &Root)>
195    where
196        's: 't,
197    {
198        let mut longest_candidate = None;
199        for (root, schema_path) in self.path_map.iter() {
200            if path.starts_with(root.path()) {
201                match longest_candidate {
202                    None => longest_candidate = Some((root, schema_path)),
203                    Some(prev) => {
204                        if root.path().as_str().len() > prev.0.path().as_str().len() {
205                            longest_candidate = Some((root, schema_path))
206                        }
207                    }
208                }
209            }
210        }
211
212        if let Some((root, schema_path)) = longest_candidate {
213            tracing::trace!(
214                r#"Schema for path "{}", found root "{}", schema "{}""#,
215                path,
216                root.path(),
217                schema_path
218            );
219            let schema = self.cache.load(schema_path).with_context(|| {
220                format!(
221                    "Failed to load schema {} for configured root {} (for target path {})",
222                    schema_path,
223                    root.path(),
224                    path
225                )
226            })?;
227            Ok((schema, root))
228        } else {
229            let mut roots = String::new();
230            for root in self.roots() {
231                write!(roots, "\n - {}", root.path())?;
232            }
233            Err(anyhow!(
234                "No root/schema for path {}\nConfigured roots:{}",
235                path,
236                roots
237            ))
238        }
239    }
240}