configmodel/
config.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8use std::borrow::Cow;
9use std::collections::BTreeMap;
10use std::hash::Hasher;
11use std::ops::Range;
12use std::path::PathBuf;
13use std::str;
14use std::sync::Arc;
15
16use minibytes::Text;
17
18use crate::convert::FromConfig;
19use crate::Error;
20use crate::Result;
21
22#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
23pub struct ContentHash(u64);
24
25/// Readable config. This can be used as a trait object.
26#[auto_impl::auto_impl(&, Box, Arc)]
27pub trait Config: Send + Sync {
28    /// Get config names in the given section. Sorted by insertion order.
29    fn keys(&self, section: &str) -> Vec<Text>;
30
31    /// Keys with the given prefix.
32    fn keys_prefixed(&self, section: &str, prefix: &str) -> Vec<Text> {
33        self.keys(section)
34            .into_iter()
35            .filter(|k| k.starts_with(prefix))
36            .collect()
37    }
38
39    /// Get config value for a given config.
40    /// Return `None` if the config item does not exist or is unset.
41    fn get(&self, section: &str, name: &str) -> Option<Text> {
42        self.get_considering_unset(section, name)?
43    }
44
45    /// Similar to `get`, but can represent "%unset" result.
46    /// - `None`: not set or unset.
47    /// - `Some(None)`: unset.
48    /// - `Some(Some(value))`: set.
49    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>>;
50
51    /// Get a nonempty config value for a given config.
52    /// Return `None` if the config item does not exist, is unset or is empty str.
53    fn get_nonempty(&self, section: &str, name: &str) -> Option<Text> {
54        self.get(section, name).filter(|v| !v.is_empty())
55    }
56
57    /// Get config sections.
58    fn sections(&self) -> Cow<[Text]>;
59
60    /// Get the sources of a config.
61    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]>;
62
63    /// Get on-disk files loaded for this `Config`.
64    fn files(&self) -> Cow<[(PathBuf, Option<ContentHash>)]> {
65        Cow::Borrowed(&[])
66    }
67
68    /// Break the config into (immutable) layers.
69    ///
70    /// If returns an empty list, then the config object is considered atomic.
71    ///
72    /// If returns a list, then those are considered "sub"-configs that this
73    /// config will consider. The order matters. Config with a larger index
74    /// overrides configs with smaller indexes. Note the combination of all
75    /// sub-configs might not be equivalent to the "self" config, since
76    /// there might be some overrides.
77    fn layers(&self) -> Vec<Arc<dyn Config>> {
78        Vec::new()
79    }
80
81    /// The name of the current layer.
82    fn layer_name(&self) -> Text;
83
84    fn pinned(&self) -> Vec<(Text, Text, Vec<ValueSource>)> {
85        Vec::new()
86    }
87}
88
89/// Extra APIs (incompatible with trait objects) around reading config.
90pub trait ConfigExt: Config {
91    /// Get a config item. Convert to type `T`.
92    fn get_opt<T: FromConfig>(&self, section: &str, name: &str) -> Result<Option<T>> {
93        self.get(section, name)
94            .map(|bytes| T::try_from_str_with_config(&self, &bytes))
95            .transpose()
96    }
97
98    /// Get a nonempty config item. Convert to type `T`.
99    fn get_nonempty_opt<T: FromConfig>(&self, section: &str, name: &str) -> Result<Option<T>> {
100        self.get_nonempty(section, name)
101            .map(|bytes| T::try_from_str_with_config(&self, &bytes))
102            .transpose()
103    }
104
105    /// Get a config item. Convert to type `T`.
106    ///
107    /// If the config item is not set, calculate it using `default_func`.
108    fn get_or<T: FromConfig>(
109        &self,
110        section: &str,
111        name: &str,
112        default_func: impl Fn() -> T,
113    ) -> Result<T> {
114        Ok(self.get_opt(section, name)?.unwrap_or_else(default_func))
115    }
116
117    /// Get a config item. Convert to type `T`.
118    ///
119    /// If the config item is not set, return `T::default()`.
120    fn get_or_default<T: Default + FromConfig>(&self, section: &str, name: &str) -> Result<T> {
121        self.get_or(section, name, Default::default)
122    }
123
124    /// Get a config item. Convert to type `T`.
125    ///
126    /// If the config item is not set, return Error::NotSet.
127    fn must_get<T: FromConfig>(&self, section: &str, name: &str) -> Result<T> {
128        match self.get_nonempty_opt(section, name)? {
129            Some(val) => Ok(val),
130            None => Err(Error::NotSet(section.to_string(), name.to_string())),
131        }
132    }
133}
134
135impl<T: Config> ConfigExt for T {}
136
137impl Config for BTreeMap<&str, &str> {
138    fn keys(&self, section: &str) -> Vec<Text> {
139        let prefix = format!("{}.", section);
140        BTreeMap::keys(self)
141            .filter_map(|k| k.strip_prefix(&prefix).map(|k| k.to_string().into()))
142            .collect()
143    }
144
145    fn sections(&self) -> Cow<[Text]> {
146        let mut sections = Vec::new();
147        let mut last_section = None;
148        for section in BTreeMap::keys(self).filter_map(|k| k.split('.').next()) {
149            if Some(section) != last_section {
150                last_section = Some(section);
151                sections.push(Text::from(section.to_string()));
152            }
153        }
154        Cow::Owned(sections)
155    }
156
157    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
158        let key: &str = &format!("{}.{}", section, name);
159        BTreeMap::get(self, &key).map(|v| Some(v.to_string().into()))
160    }
161
162    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]> {
163        match Config::get(self, section, name) {
164            None => Cow::Borrowed(&[]),
165            Some(value) => Cow::Owned(vec![ValueSource {
166                value: Some(value),
167                source: Text::from_static("BTreeMap"),
168                location: None,
169            }]),
170        }
171    }
172
173    fn layer_name(&self) -> Text {
174        Text::from_static("BTreeMap")
175    }
176}
177
178impl Config for BTreeMap<String, String> {
179    fn keys(&self, section: &str) -> Vec<Text> {
180        let prefix = format!("{}.", section);
181        BTreeMap::keys(self)
182            .filter_map(|k| k.strip_prefix(&prefix).map(|k| k.to_string().into()))
183            .collect()
184    }
185
186    fn sections(&self) -> Cow<[Text]> {
187        let mut sections = Vec::new();
188        let mut last_section = None;
189        for section in BTreeMap::keys(self).filter_map(|k| k.split('.').next()) {
190            if Some(section) != last_section {
191                last_section = Some(section);
192                sections.push(Text::from(section.to_string()));
193            }
194        }
195        Cow::Owned(sections)
196    }
197
198    fn get_considering_unset(&self, section: &str, name: &str) -> Option<Option<Text>> {
199        BTreeMap::get(self, &format!("{}.{}", section, name)).map(|v| Some(v.clone().into()))
200    }
201
202    fn get_sources(&self, section: &str, name: &str) -> Cow<[ValueSource]> {
203        match Config::get(self, section, name) {
204            None => Cow::Borrowed(&[]),
205            Some(value) => Cow::Owned(vec![ValueSource {
206                value: Some(value),
207                source: Text::from_static("BTreeMap"),
208                location: None,
209            }]),
210        }
211    }
212
213    fn layer_name(&self) -> Text {
214        Text::from_static("BTreeMap")
215    }
216}
217
218impl ContentHash {
219    pub fn from_contents(contents: &[u8]) -> Self {
220        let mut xx = twox_hash::XxHash::default();
221        xx.write(contents);
222        Self(xx.finish())
223    }
224}
225
226/// A config value with associated metadata like where it comes from.
227#[derive(Clone, Debug)]
228pub struct ValueSource {
229    pub value: Option<Text>,
230    pub source: Text, // global, user, repo, "--config", or an extension name, etc.
231    pub location: Option<ValueLocation>,
232}
233
234/// The on-disk file name and byte offsets that provide the config value.
235/// Useful if applications want to edit config values in-place.
236#[derive(Clone, Debug)]
237pub struct ValueLocation {
238    pub path: Arc<PathBuf>,
239    pub content: Text,
240    pub location: Range<usize>,
241}
242
243impl ValueSource {
244    /// Return the actual value stored in this config value, or `None` if unset.
245    pub fn value(&self) -> &Option<Text> {
246        &self.value
247    }
248
249    /// Return the "source" information for the config value. It's usually who sets the config,
250    /// like "--config", "user_hgrc", "system_hgrc", etc.
251    pub fn source(&self) -> &Text {
252        &self.source
253    }
254
255    /// Return the file path and byte range for the exact config value,
256    /// or `None` if there is no such information.
257    ///
258    /// If the value is `None`, the byte range is for the "%unset" statement.
259    pub fn location(&self) -> Option<(PathBuf, Range<usize>)> {
260        self.location
261            .as_ref()
262            .map(|src| (src.path.as_ref().to_path_buf(), src.location.clone()))
263    }
264
265    /// Return the file content. Or `None` if there is no such information.
266    pub fn file_content(&self) -> Option<Text> {
267        self.location.as_ref().map(|src| src.content.clone())
268    }
269
270    /// Return the line number, starting from 1.
271    pub fn line_number(&self) -> Option<usize> {
272        let loc = self.location.as_ref()?;
273        let line_no = loc
274            .content
275            .slice(..loc.location.start)
276            .chars()
277            .filter(|&c| c == '\n')
278            .count();
279        Some(line_no + 1)
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::*;
286
287    fn wants_impl(_: impl Config) {}
288
289    #[test]
290    fn test_btreemap_config() {
291        let map: BTreeMap<&str, &str> = vec![("foo.bar", "baz")].into_iter().collect();
292        assert_eq!(format!("{:?}", Config::keys(&map, "foo")), "[\"bar\"]");
293        assert_eq!(
294            format!("{:?}", Config::get(&map, "foo", "bar")),
295            "Some(\"baz\")"
296        );
297        assert_eq!(format!("{:?}", Config::get(&map, "foo", "baz")), "None");
298
299        // Make sure we can pass BTreeMap config to generic func.
300        wants_impl(&map);
301    }
302
303    #[test]
304    fn test_must_get() {
305        let map: BTreeMap<&str, &str> = vec![("foo.bar", "baz")].into_iter().collect();
306        assert_eq!(
307            map.must_get::<Vec<String>>("foo", "bar").unwrap(),
308            vec!["baz".to_string()]
309        );
310        assert!(matches!(
311            map.must_get::<Vec<String>>("foo", "nope"),
312            Err(Error::NotSet(_, _))
313        ));
314    }
315}