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}