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);