figment/
metadata.rs

1use std::fmt;
2use std::borrow::Cow;
3use std::path::{Path, PathBuf};
4use std::panic::Location;
5
6use crate::Profile;
7
8/// Metadata about a configuration value: its source's name and location.
9///
10/// # Overview
11///
12/// Every [`Value`] produced by a [`Figment`] is [`Tag`]ed with `Metadata`
13/// by its producing [`Provider`]. The metadata consists of:
14///
15///   * A name for the source, e.g. "TOML File".
16///   * The [`Source`] itself, if it is known.
17///   * A default or custom [interpolater](#interpolation).
18///   * A source [`Location`] where a value's provider was added to the
19///   containing figment, if it is known.
20///
21/// This information is used to produce insightful error messages as well as to
22/// generate values like [`RelativePathBuf`] that know about their configuration
23/// source.
24///
25/// [`Location`]: std::panic::Location
26///
27/// ## Errors
28///
29/// [`Error`]s produced by [`Figment`]s contain the `Metadata` for the value
30/// that caused the error. The `Display` implementation for `Error` uses the
31/// metadata's interpolater to display the path to the key for the value that
32/// caused the error.
33///
34/// ## Interpolation
35///
36/// Interpolation takes a figment profile and key path (`a.b.c`) and turns it
37/// into a source-native path. The default interpolater returns a figment key
38/// path prefixed with the profile if the profile is custom:
39///
40/// ```text
41/// ${profile}.${a}.${b}.${c}
42/// ```
43///
44/// Providers are free to implement any interpolater for their metadata. For
45/// example, the interpolater for [`Env`] uppercases each path key:
46///
47/// ```rust
48/// use figment::Metadata;
49///
50/// let metadata = Metadata::named("environment variable(s)")
51///     .interpolater(|profile, path| {
52///         let keys: Vec<_> = path.iter()
53///             .map(|k| k.to_ascii_uppercase())
54///             .collect();
55///
56///         format!("{}", keys.join("."))
57///     });
58///
59/// let profile = figment::Profile::Default;
60/// let interpolated = metadata.interpolate(&profile, &["key", "path"]);
61/// assert_eq!(interpolated, "KEY.PATH");
62/// ```
63///
64/// [`Provider`]: crate::Provider
65/// [`Error`]: crate::Error
66/// [`Figment`]: crate::Figment
67/// [`RelativePathBuf`]: crate::value::magic::RelativePathBuf
68/// [`value`]: crate::value::Value
69/// [`Tag`]: crate::value::Tag
70/// [`Env`]: crate::providers::Env
71#[derive(Debug, Clone)]
72pub struct Metadata {
73    /// The name of the configuration source for a given value.
74    pub name: Cow<'static, str>,
75    /// The source of the configuration value, if it is known.
76    pub source: Option<Source>,
77    /// The source location where this value's provider was added to the
78    /// containing figment, if it is known.
79    pub provide_location: Option<&'static Location<'static>>,
80    interpolater: Box<dyn Interpolator>,
81}
82
83impl Metadata {
84    /// Creates a new `Metadata` with the given `name` and `source`.
85    ///
86    /// # Example
87    ///
88    /// ```rust
89    /// use figment::Metadata;
90    ///
91    /// let metadata = Metadata::from("AWS Config Store", "path/to/value");
92    /// assert_eq!(metadata.name, "AWS Config Store");
93    /// assert_eq!(metadata.source.unwrap().custom(), Some("path/to/value"));
94    /// ```
95    #[inline(always)]
96    pub fn from<N, S>(name: N, source: S) -> Self
97        where N: Into<Cow<'static, str>>, S: Into<Source>
98    {
99        Metadata::named(name).source(source)
100    }
101
102    /// Creates a new `Metadata` with the given `name` and no source.
103    ///
104    /// # Example
105    ///
106    /// ```rust
107    /// use figment::Metadata;
108    ///
109    /// let metadata = Metadata::named("AWS Config Store");
110    /// assert_eq!(metadata.name, "AWS Config Store");
111    /// assert!(metadata.source.is_none());
112    /// ```
113    #[inline]
114    pub fn named<T: Into<Cow<'static, str>>>(name: T) -> Self {
115        Metadata { name: name.into(), ..Metadata::default() }
116    }
117
118    /// Sets the `source` of `self` to `Some(source)`.
119    ///
120    /// # Example
121    ///
122    /// ```rust
123    /// use figment::Metadata;
124    ///
125    /// let metadata = Metadata::named("AWS Config Store").source("config/path");
126    /// assert_eq!(metadata.name, "AWS Config Store");
127    /// assert_eq!(metadata.source.unwrap().custom(), Some("config/path"));
128    /// ```
129    #[inline(always)]
130    pub fn source<S: Into<Source>>(mut self, source: S) -> Self {
131        self.source = Some(source.into());
132        self
133    }
134
135    /// Sets the `interpolater` of `self` to the function `f`. The interpolater
136    /// can be invoked via [`Metadata::interpolate()`].
137    ///
138    /// # Example
139    ///
140    /// ```rust
141    /// use figment::Metadata;
142    ///
143    /// let metadata = Metadata::named("environment variable(s)")
144    ///     .interpolater(|profile, path| {
145    ///         let keys: Vec<_> = path.iter()
146    ///             .map(|k| k.to_ascii_uppercase())
147    ///             .collect();
148    ///
149    ///         format!("{}", keys.join("."))
150    ///     });
151    ///
152    /// let profile = figment::Profile::Default;
153    /// let interpolated = metadata.interpolate(&profile, &["key", "path"]);
154    /// assert_eq!(interpolated, "KEY.PATH");
155    /// ```
156    #[inline(always)]
157    pub fn interpolater<I: Clone + Send + Sync + 'static>(mut self, f: I) -> Self
158        where I: Fn(&Profile, &[&str]) -> String
159    {
160        self.interpolater = Box::new(f);
161        self
162    }
163
164    /// Runs the interpolater in `self` on `profile` and `keys`.
165    ///
166    /// # Example
167    ///
168    /// ```rust
169    /// use figment::{Metadata, Profile};
170    ///
171    /// let url = "ftp://config.dev";
172    /// let md = Metadata::named("Network").source(url)
173    ///     .interpolater(move |profile, keys| match profile.is_custom() {
174    ///         true => format!("{}/{}/{}", url, profile, keys.join("/")),
175    ///         false => format!("{}/{}", url, keys.join("/")),
176    ///     });
177    ///
178    /// let interpolated = md.interpolate(&Profile::Default, &["key", "path"]);
179    /// assert_eq!(interpolated, "ftp://config.dev/key/path");
180    ///
181    /// let profile = Profile::new("static");
182    /// let interpolated = md.interpolate(&profile, &["key", "path"]);
183    /// assert_eq!(interpolated, "ftp://config.dev/static/key/path");
184    /// ```
185    pub fn interpolate<K: AsRef<str>>(&self, profile: &Profile, keys: &[K]) -> String {
186        let keys: Vec<_> = keys.iter().map(|k| k.as_ref()).collect();
187        (self.interpolater)(profile, &keys)
188    }
189}
190
191impl PartialEq for Metadata {
192    fn eq(&self, other: &Self) -> bool {
193        self.name == other.name && self.source == other.source
194    }
195}
196
197impl Default for Metadata {
198    fn default() -> Self {
199        Self {
200            name: "Default".into(),
201            source: None,
202            provide_location: None,
203            interpolater: Box::new(default_interpolater),
204        }
205    }
206}
207
208/// The source for a configuration value.
209///
210/// The `Source` of a given value can be determined via that value's
211/// [`Metadata.source`](Metadata#structfield.source) retrievable via the value's
212/// [`Tag`] (via [`Value::tag()`] or via the magic value [`Tagged`]) and
213/// [`Figment::get_metadata()`].
214///
215/// [`Tag`]: crate::value::Tag
216/// [`Value::tag()`]: crate::value::Value::tag()
217/// [`Tagged`]: crate::value::magic::Tagged
218/// [`Figment::get_metadata()`]: crate::Figment::get_metadata()
219#[non_exhaustive]
220#[derive(PartialEq, Debug, Clone)]
221pub enum Source {
222    /// A file: the path to the file.
223    File(PathBuf),
224    /// Some programatic value: the source location.
225    Code(&'static Location<'static>),
226    /// A custom source all-together.
227    Custom(String),
228}
229
230impl Source {
231    /// Returns the path to the source file if `self.kind` is `Kind::File`.
232    ///
233    /// # Example
234    ///
235    /// ```rust
236    /// use std::path::Path;
237    /// use figment::Source;
238    ///
239    /// let source = Source::from(Path::new("a/b/c.txt"));
240    /// assert_eq!(source.file_path(), Some(Path::new("a/b/c.txt")));
241    /// ```
242    pub fn file_path(&self) -> Option<&Path> {
243        match self {
244            Source::File(ref p) => Some(p),
245            _ => None,
246        }
247    }
248
249    /// Returns the location to the source code if `self` is `Source::Code`.
250    ///
251    /// # Example
252    ///
253    /// ```rust
254    /// use std::panic::Location;
255    ///
256    /// use figment::Source;
257    ///
258    /// let location = Location::caller();
259    /// let source = Source::Code(location);
260    /// assert_eq!(source.code_location(), Some(location));
261    /// ```
262    pub fn code_location(&self) -> Option<&'static Location<'static>> {
263        match self {
264            Source::Code(s) => Some(s),
265            _ => None
266        }
267    }
268    /// Returns the custom source location if `self` is `Source::Custom`.
269    ///
270    /// # Example
271    ///
272    /// ```rust
273    /// use figment::Source;
274    ///
275    /// let source = Source::Custom("ftp://foo".into());
276    /// assert_eq!(source.custom(), Some("ftp://foo"));
277    /// ```
278    pub fn custom(&self) -> Option<&str> {
279        match self {
280            Source::Custom(ref c) => Some(c),
281            _ => None,
282        }
283    }
284}
285
286/// Displays the source. Location and custom sources are displayed directly.
287/// File paths are displayed relative to the current working directory if the
288/// relative path is shorter than the complete path.
289impl fmt::Display for Source {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            Source::File(p) => {
293                use {std::env::current_dir, crate::util::diff_paths};
294
295                match current_dir().ok().and_then(|cwd| diff_paths(p, &cwd)) {
296                    Some(r) if r.iter().count() < p.iter().count() => r.display().fmt(f),
297                    Some(_) | None => p.display().fmt(f)
298                }
299            }
300            Source::Code(l) => l.fmt(f),
301            Source::Custom(c) => c.fmt(f),
302        }
303    }
304}
305
306impl From<&Path> for Source {
307    fn from(path: &Path) -> Source {
308        Source::File(path.into())
309    }
310}
311
312impl From<&'static Location<'static>> for Source {
313    fn from(location: &'static Location<'static>) -> Source {
314        Source::Code(location)
315    }
316}
317
318impl From<&str> for Source {
319    fn from(string: &str) -> Source {
320        Source::Custom(string.into())
321    }
322}
323
324impl From<String> for Source {
325    fn from(string: String) -> Source {
326        Source::Custom(string)
327    }
328}
329
330crate::util::cloneable_fn_trait!(
331    Interpolator: Fn(&Profile, &[&str]) -> String + Send + Sync + 'static
332);
333
334fn default_interpolater(profile: &Profile, keys: &[&str]) -> String {
335    format!("{}.{}", profile, keys.join("."))
336}