figment/providers/
data.rs

1use std::marker::PhantomData;
2use std::path::{Path, PathBuf};
3
4use serde::de::{self, DeserializeOwned};
5
6use crate::value::{Map, Dict};
7use crate::{Error, Profile, Provider, Metadata};
8
9#[derive(Debug, Clone)]
10enum Source {
11    File(Option<PathBuf>),
12    String(String)
13}
14
15/// A `Provider` that sources values from a file or string in a given
16/// [`Format`].
17///
18/// # Constructing
19///
20/// A `Data` provider is typically constructed indirectly via a type that
21/// implements the [`Format`] trait via the [`Format::file()`] and
22/// [`Format::string()`] methods which in-turn defer to [`Data::file()`] and
23/// [`Data::string()`] by default:
24///
25/// ```rust
26/// // The `Format` trait must be in-scope to use its methods.
27/// use figment::providers::{Format, Data, Json};
28///
29/// // These two are equivalent, except the former requires the explicit type.
30/// let json = Data::<Json>::file("foo.json");
31/// let json = Json::file("foo.json");
32/// ```
33///
34/// # Provider Details
35///
36///   * **Profile**
37///
38///     This provider does not set a profile.
39///
40///   * **Metadata**
41///
42///     This provider is named `${NAME} file` (when constructed via
43///     [`Data::file()`]) or `${NAME} source string` (when constructed via
44///     [`Data::string()`]), where `${NAME}` is [`Format::NAME`]. When
45///     constructed from a file, the file's path is specified as file
46///     [`Source`](crate::Source). Path interpolation is unchanged from the
47///     default.
48///
49///   * **Data (Unnested, _default_)**
50///
51///     When nesting is _not_ specified, the source file or string is read and
52///     parsed, and the parsed dictionary is emitted into the profile
53///     configurable via [`Data::profile()`], which defaults to
54///     [`Profile::Default`]. If the source is a file and the file is not
55///     present, an empty dictionary is emitted.
56///
57///   * **Data (Nested)**
58///
59///     When nesting is specified, the source value is expected to be a
60///     dictionary. It's top-level keys are emitted as profiles, and the value
61///     corresponding to each key as the profile data.
62#[derive(Debug, Clone)]
63pub struct Data<F: Format> {
64    source: Source,
65    /// The profile data will be emitted to if nesting is disabled. Defaults to
66    /// [`Profile::Default`].
67    pub profile: Option<Profile>,
68    _format: PhantomData<F>,
69}
70
71impl<F: Format> Data<F> {
72    fn new(source: Source, profile: Option<Profile>) -> Self {
73        Data { source, profile, _format: PhantomData }
74    }
75
76    /// Returns a `Data` provider that sources its values by parsing the file at
77    /// `path` as format `F`. If `path` is relative, the file is searched for in
78    /// the current working directory and all parent directories until the root,
79    /// and the first hit is used. If you don't want parent directories to be
80    /// searched, use [`Data::file_exact()`] instead.
81    ///
82    /// Nesting is disabled by default. Use [`Data::nested()`] to enable it.
83    ///
84    /// ```rust
85    /// use serde::Deserialize;
86    /// use figment::{Figment, Jail, providers::{Format, Toml}, value::Map};
87    ///
88    /// #[derive(Debug, PartialEq, Deserialize)]
89    /// struct Config {
90    ///     numbers: Vec<usize>,
91    ///     untyped: Map<String, usize>,
92    /// }
93    ///
94    /// Jail::expect_with(|jail| {
95    ///     jail.create_file("Config.toml", r#"
96    ///         numbers = [1, 2, 3]
97    ///
98    ///         [untyped]
99    ///         key = 1
100    ///         other = 4
101    ///     "#)?;
102    ///
103    ///     let config: Config = Figment::from(Toml::file("Config.toml")).extract()?;
104    ///     assert_eq!(config, Config {
105    ///         numbers: vec![1, 2, 3],
106    ///         untyped: figment::util::map!["key".into() => 1, "other".into() => 4],
107    ///     });
108    ///
109    ///     Ok(())
110    /// });
111    /// ```
112    pub fn file<P: AsRef<Path>>(path: P) -> Self {
113        fn find(path: &Path) -> Option<PathBuf> {
114            if path.is_absolute() {
115                match path.is_file() {
116                    true => return Some(path.to_path_buf()),
117                    false => return None
118                }
119            }
120
121            let cwd = std::env::current_dir().ok()?;
122            let mut cwd = cwd.as_path();
123            loop {
124                let file_path = cwd.join(path);
125                if file_path.is_file() {
126                    return Some(file_path);
127                }
128
129                cwd = cwd.parent()?;
130            }
131        }
132
133        Data::new(Source::File(find(path.as_ref())), Some(Profile::Default))
134    }
135
136    /// Returns a `Data` provider that sources its values by parsing the file at
137    /// `path` as format `F`. If `path` is relative, it is located relative to
138    /// the current working directory. No other directories are searched.
139    ///
140    /// If you want to search parent directories for `path`, use
141    /// [`Data::file()`] instead.
142    ///
143    /// Nesting is disabled by default. Use [`Data::nested()`] to enable it.
144    ///
145    /// ```rust
146    /// use serde::Deserialize;
147    /// use figment::{Figment, Jail, providers::{Format, Toml}};
148    ///
149    /// #[derive(Debug, PartialEq, Deserialize)]
150    /// struct Config {
151    ///     foo: usize,
152    /// }
153    ///
154    /// Jail::expect_with(|jail| {
155    ///     // Create 'subdir/config.toml' and set `cwd = subdir`.
156    ///     jail.create_file("config.toml", "foo = 123")?;
157    ///     jail.change_dir(jail.create_dir("subdir")?)?;
158    ///
159    ///     // We are in `subdir`. `config.toml` is in `../`. `file()` finds it.
160    ///     let config = Figment::from(Toml::file("config.toml")).extract::<Config>()?;
161    ///     assert_eq!(config.foo, 123);
162    ///
163    ///     // `file_exact()` doesn't search, so it doesn't find it.
164    ///     let config = Figment::from(Toml::file_exact("config.toml")).extract::<Config>();
165    ///     assert!(config.is_err());
166    ///     Ok(())
167    /// });
168    /// ```
169    pub fn file_exact<P: AsRef<Path>>(path: P) -> Self {
170        Data::new(Source::File(Some(path.as_ref().to_owned())), Some(Profile::Default))
171    }
172
173    /// Returns a `Data` provider that sources its values by parsing the string
174    /// `string` as format `F`. Nesting is not enabled by default; use
175    /// [`Data::nested()`] to enable nesting.
176    ///
177    /// ```rust
178    /// use serde::Deserialize;
179    /// use figment::{Figment, Jail, providers::{Format, Toml}, value::Map};
180    ///
181    /// #[derive(Debug, PartialEq, Deserialize)]
182    /// struct Config {
183    ///     numbers: Vec<usize>,
184    ///     untyped: Map<String, usize>,
185    /// }
186    ///
187    /// Jail::expect_with(|jail| {
188    ///     let source = r#"
189    ///         numbers = [1, 2, 3]
190    ///         untyped = { key = 1, other = 4 }
191    ///     "#;
192    ///
193    ///     let config: Config = Figment::from(Toml::string(source)).extract()?;
194    ///     assert_eq!(config, Config {
195    ///         numbers: vec![1, 2, 3],
196    ///         untyped: figment::util::map!["key".into() => 1, "other".into() => 4],
197    ///     });
198    ///
199    ///     Ok(())
200    /// });
201    /// ```
202    pub fn string(string: &str) -> Self {
203        Data::new(Source::String(string.into()), Some(Profile::Default))
204    }
205
206    /// Enables nesting on `self`, which results in top-level keys of the
207    /// sourced data being treated as profiles.
208    ///
209    /// ```rust
210    /// use serde::Deserialize;
211    /// use figment::{Figment, Jail, providers::{Format, Toml}, value::Map};
212    ///
213    /// #[derive(Debug, PartialEq, Deserialize)]
214    /// struct Config {
215    ///     numbers: Vec<usize>,
216    ///     untyped: Map<String, usize>,
217    /// }
218    ///
219    /// Jail::expect_with(|jail| {
220    ///     jail.create_file("Config.toml", r#"
221    ///         [global.untyped]
222    ///         global = 0
223    ///         hi = 7
224    ///
225    ///         [staging]
226    ///         numbers = [1, 2, 3]
227    ///
228    ///         [release]
229    ///         numbers = [6, 7, 8]
230    ///     "#)?;
231    ///
232    ///     // Enable nesting via `nested()`.
233    ///     let figment = Figment::from(Toml::file("Config.toml").nested());
234    ///
235    ///     let figment = figment.select("staging");
236    ///     let config: Config = figment.extract()?;
237    ///     assert_eq!(config, Config {
238    ///         numbers: vec![1, 2, 3],
239    ///         untyped: figment::util::map!["global".into() => 0, "hi".into() => 7],
240    ///     });
241    ///
242    ///     let config: Config = figment.select("release").extract()?;
243    ///     assert_eq!(config, Config {
244    ///         numbers: vec![6, 7, 8],
245    ///         untyped: figment::util::map!["global".into() => 0, "hi".into() => 7],
246    ///     });
247    ///
248    ///     Ok(())
249    /// });
250    /// ```
251    pub fn nested(mut self) -> Self {
252        self.profile = None;
253        self
254    }
255
256    /// Set the profile to emit data to when nesting is disabled.
257    ///
258    /// ```rust
259    /// use serde::Deserialize;
260    /// use figment::{Figment, Jail, providers::{Format, Toml}, value::Map};
261    ///
262    /// #[derive(Debug, PartialEq, Deserialize)]
263    /// struct Config { value: u8 }
264    ///
265    /// Jail::expect_with(|jail| {
266    ///     let provider = Toml::string("value = 123").profile("debug");
267    ///     let config: Config = Figment::from(provider).select("debug").extract()?;
268    ///     assert_eq!(config, Config { value: 123 });
269    ///
270    ///     Ok(())
271    /// });
272    /// ```
273    pub fn profile<P: Into<Profile>>(mut self, profile: P) -> Self {
274        self.profile = Some(profile.into());
275        self
276    }
277}
278
279impl<F: Format> Provider for Data<F> {
280    fn metadata(&self) -> Metadata {
281        use Source::*;
282        match &self.source {
283            String(_) => Metadata::named(format!("{} source string", F::NAME)),
284            File(None) => Metadata::named(format!("{} file", F::NAME)),
285            File(Some(p)) => Metadata::from(format!("{} file", F::NAME), &**p)
286        }
287    }
288
289    fn data(&self) -> Result<Map<Profile, Dict>, Error> {
290        use Source::*;
291        let map: Result<Map<Profile, Dict>, _> = match (&self.source, &self.profile) {
292            (File(None), _) => return Ok(Map::new()),
293            (File(Some(path)), None) => F::from_path(&path),
294            (String(s), None) => F::from_str(&s),
295            (File(Some(path)), Some(prof)) => F::from_path(&path).map(|v| prof.collect(v)),
296            (String(s), Some(prof)) => F::from_str(&s).map(|v| prof.collect(v)),
297        };
298
299        Ok(map.map_err(|e| e.to_string())?)
300    }
301}
302
303/// Trait implementable by text-based [`Data`] format providers.
304///
305/// Instead of implementing [`Provider`] directly, types that refer to data
306/// formats, such as [`Json`] and [`Toml`], implement this trait. By
307/// implementing [`Format`], they become [`Provider`]s indirectly via the
308/// [`Data`] type, which serves as a provider for all `T: Format`.
309///
310/// ```rust
311/// use figment::providers::Format;
312///
313/// # use serde::de::DeserializeOwned;
314/// # struct T;
315/// # impl Format for T {
316/// #     type Error = serde::de::value::Error;
317/// #     const NAME: &'static str = "T";
318/// #     fn from_str<'de, T: DeserializeOwned>(_: &'de str) -> Result<T, Self::Error> { todo!() }
319/// # }
320/// # fn is_provider<T: figment::Provider>(_: T) {}
321/// // If `T` implements `Format`, `T` is a `Provider`.
322/// // Initialize it with `T::file()` or `T::string()`.
323/// let provider = T::file("foo.fmt");
324/// # is_provider(provider);
325/// let provider = T::string("some -- format");
326/// # is_provider(provider);
327/// ```
328///
329/// [`Data<T>`]: Data
330///
331/// # Implementing
332///
333/// There are two primary implementation items:
334///
335///   1. [`Format::NAME`]: This should be the name of the data format: `"JSON"`
336///      or `"TOML"`. The string is used in the [metadata for `Data`].
337///
338///   2. [`Format::from_str()`]: This is the core string deserialization method.
339///      A typical implementation will simply call an existing method like
340///      [`toml::from_str`]. For writing a custom data format, see [serde's
341///      writing a data format guide].
342///
343/// The default implementations for [`Format::from_path()`], [`Format::file()`],
344/// and [`Format::string()`] methods should likely not be overwritten.
345///
346/// [`NAME`]: Format::NAME
347/// [serde's writing a data format guide]: https://serde.rs/data-format.html
348pub trait Format: Sized {
349    /// The data format's error type.
350    type Error: de::Error;
351
352    /// The name of the data format, for instance `"JSON"` or `"TOML"`.
353    const NAME: &'static str;
354
355    /// Returns a `Data` provider that sources its values by parsing the file at
356    /// `path` as format `Self`. See [`Data::file()`] for more details. The
357    /// default implementation calls `Data::file(path)`.
358    fn file<P: AsRef<Path>>(path: P) -> Data<Self> {
359        Data::file(path)
360    }
361
362    /// Returns a `Data` provider that sources its values by parsing the file at
363    /// `path` as format `Self`. See [`Data::file_exact()`] for more details. The
364    /// default implementation calls `Data::file_exact(path)`.
365    fn file_exact<P: AsRef<Path>>(path: P) -> Data<Self> {
366        Data::file_exact(path)
367    }
368
369    /// Returns a `Data` provider that sources its values by parsing `string` as
370    /// format `Self`. See [`Data::string()`] for more details. The default
371    /// implementation calls `Data::string(string)`.
372    fn string(string: &str) -> Data<Self> {
373        Data::string(string)
374    }
375
376    /// Parses `string` as the data format `Self` as a `T` or returns an error
377    /// if the `string` is an invalid `T`. **_Note:_** This method is _not_
378    /// intended to be called directly. Instead, it is intended to be
379    /// _implemented_ and then used indirectly via the [`Data::file()`] or
380    /// [`Data::string()`] methods.
381    fn from_str<'de, T: DeserializeOwned>(string: &'de str) -> Result<T, Self::Error>;
382
383    /// Parses the file at `path` as the data format `Self` as a `T` or returns
384    /// an error if the `string` is an invalid `T`. The default implementation
385    /// calls [`Format::from_str()`] with the contents of the file. **_Note:_**
386    /// This method is _not_ intended to be called directly. Instead, it is
387    /// intended to be _implemented on special occasions_ and then used
388    /// indirectly via the [`Data::file()`] or [`Data::string()`] methods.
389    fn from_path<T: DeserializeOwned>(path: &Path) -> Result<T, Self::Error> {
390        let source = std::fs::read_to_string(path).map_err(de::Error::custom)?;
391        Self::from_str(&source)
392    }
393}
394
395#[allow(unused_macros)]
396macro_rules! impl_format {
397    ($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty, $doc:expr) => (
398        #[cfg(feature = $string)]
399        #[cfg_attr(nightly, doc(cfg(feature = $string)))]
400        #[doc = $doc]
401        pub struct $name;
402
403        #[cfg(feature = $string)]
404        impl Format for $name {
405            type Error = $E;
406
407            const NAME: &'static str = $NAME;
408
409            fn from_str<'de, T: DeserializeOwned>(s: &'de str) -> Result<T, $E> {
410                $func(s)
411            }
412        }
413    );
414
415    ($name:ident $NAME:literal/$string:literal: $func:expr, $E:ty) => (
416        impl_format!($name $NAME/$string: $func, $E, concat!(
417            "A ", $NAME, " [`Format`] [`Data`] provider.",
418            "\n\n",
419            "Static constructor methods on `", stringify!($name), "` return a
420            [`Data`] value with a generic marker of [`", stringify!($name), "`].
421            Thus, further use occurs via methods on [`Data`].",
422            "\n```\n",
423            "use figment::providers::{Format, ", stringify!($name), "};",
424            "\n\n// Source directly from a source string...",
425            "\nlet provider = ", stringify!($name), r#"::string("source-string");"#,
426            "\n\n// Or read from a file on disk.",
427            "\nlet provider = ", stringify!($name), r#"::file("path-to-file");"#,
428            "\n\n// Or configured as nested (via Data::nested()):",
429            "\nlet provider = ", stringify!($name), r#"::file("path-to-file").nested();"#,
430            "\n```",
431            "\n\nSee also [`", stringify!($func), "`] for parsing details."
432        ));
433    )
434}
435
436#[cfg(feature = "yaml")]
437#[cfg_attr(nightly, doc(cfg(feature = "yaml")))]
438impl YamlExtended {
439    /// This "YAML Extended" format parser implements the draft ["Merge Key
440    /// Language-Independent Type for YAML™ Version
441    /// 1.1"](https://yaml.org/type/merge.html) spec via
442    /// [`serde_yaml::Value::apply_merge()`]. This method is _not_ intended to
443    /// be used directly but rather indirectly by making use of `YamlExtended`
444    /// as a provider. The extension is not part of any officially supported
445    /// YAML release and is deprecated entirely since YAML 1.2. Using
446    /// `YamlExtended` instead of [`Yaml`] enables merge keys, allowing YAML
447    /// like the following to parse with key merges applied:
448    ///
449    /// ```yaml
450    /// tasks:
451    ///   build: &webpack_shared
452    ///     command: webpack
453    ///     args: build
454    ///     inputs:
455    ///       - 'src/**/*'
456    ///   start:
457    ///     <<: *webpack_shared
458    ///     args: start
459    /// ```
460    ///
461    /// # Example
462    ///
463    /// ```rust
464    /// use serde::Deserialize;
465    /// use figment::{Figment, Jail, providers::{Format, Yaml, YamlExtended}};
466    ///
467    /// #[derive(Debug, PartialEq, Deserialize)]
468    /// struct Circle {
469    ///     x: usize,
470    ///     y: usize,
471    ///     r: usize,
472    /// }
473    ///
474    /// #[derive(Debug, PartialEq, Deserialize)]
475    /// struct Config {
476    ///     circle1: Circle,
477    ///     circle2: Circle,
478    ///     circle3: Circle,
479    /// }
480    ///
481    /// Jail::expect_with(|jail| {
482    ///     jail.create_file("Config.yaml", r#"
483    ///         point: &POINT { x: 1, y: 2 }
484    ///         radius: &RADIUS
485    ///           r: 10
486    ///
487    ///         circle1:
488    ///           <<: *POINT
489    ///           r: 3
490    ///
491    ///         circle2:
492    ///           <<: [ *POINT, *RADIUS ]
493    ///
494    ///         circle3:
495    ///           <<: [ *POINT, *RADIUS ]
496    ///           y: 14
497    ///           r: 20
498    ///     "#)?;
499    ///
500    ///     let config: Config = Figment::from(YamlExtended::file("Config.yaml")).extract()?;
501    ///     assert_eq!(config, Config {
502    ///         circle1: Circle { x: 1, y: 2, r: 3 },
503    ///         circle2: Circle { x: 1, y: 2, r: 10 },
504    ///         circle3: Circle { x: 1, y: 14, r: 20 },
505    ///     });
506    ///
507    ///     // Note that just `Yaml` would fail, since it doesn't support merge.
508    ///     let config = Figment::from(Yaml::file("Config.yaml"));
509    ///     assert!(config.extract::<Config>().is_err());
510    ///
511    ///     Ok(())
512    /// });
513    /// ```
514    pub fn from_str<'de, T: DeserializeOwned>(s: &'de str) -> serde_yaml::Result<T> {
515        let mut value: serde_yaml::Value = serde_yaml::from_str(s)?;
516        value.apply_merge()?;
517        T::deserialize(value)
518    }
519}
520
521impl_format!(Toml "TOML"/"toml": toml::from_str, toml::de::Error);
522impl_format!(Yaml "YAML"/"yaml": serde_yaml::from_str, serde_yaml::Error);
523impl_format!(Json "JSON"/"json": serde_json::from_str, serde_json::error::Error);
524impl_format!(YamlExtended "YAML Extended"/"yaml": YamlExtended::from_str, serde_yaml::Error);