cfgfifo/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2//! `cfgfifo` is a Rust library for serializing & deserializing various common
3//! configuration file formats ([JSON][], [JSON5][], [RON][], [TOML][], and
4//! [YAML][]), including autodetecting the format of a file based on its file
5//! extension.  It's good for application authors who want to support multiple
6//! configuration file formats but don't want to write out a bunch of
7//! boilerplate.  `cfgfifo` has already written that boilerplate for you, so
8//! let it (de)serialize your files!
9//!
10//! [JSON]: https://www.json.org
11//! [JSON5]: https://json5.org
12//! [RON]: https://github.com/ron-rs/ron
13//! [TOML]: https://toml.io
14//! [YAML]: https://yaml.org
15//!
16//! Overview
17//! ========
18//!
19//! - Call [`load()`] on a file path to deserialize its contents as a
20//!   [`serde::de::DeserializeOwned`] type.  The file's format will be
21//!   determined based on its file extension.
22//!
23//! - Call [`dump()`] on a file path to serialize a [`serde::Serialize`] value
24//!   to it.  The file's format will be determined based on its file extension.
25//!
26//! - For finer control over how file formats are identified, configure a
27//!   [`Cfgfifo`] struct and use its [`load()`][Cfgfifo::load] and
28//!   [`dump()`][Cfgfifo::dump] methods.
29//!
30//! - For per-format operations, including (de)serializing to & from strings,
31//!   readers, and writers, use the [`Format`] enum.
32//!
33//! Features
34//! ========
35//!
36//! Support for each configuration file format is controlled by a Cargo
37//! feature; the features for all formats are enabled by default.  These
38//! features are:
39//!
40//! - `json` — Support for JSON via the [`serde_json`] crate
41//! - `json5` — Support for JSON5 via the [`json5`] crate
42//! - `ron` — Support for RON via the [`ron`] crate
43//! - `toml` — Support for TOML via the [`toml`] crate
44//! - `yaml` — Support for YAML via the [`serde_yaml`] crate
45//!
46//! Format Limitations
47//! ==================
48//!
49//! If you wish to (de)serialize a type in multiple formats using this crate,
50//! you must first ensure that all of the formats you're using support the type
51//! and its (de)serialization options, as not all formats are equal in this
52//! regard.
53//!
54//! The following format-specific limitations are currently known:
55//!
56//! - RON has limited support for internally tagged enums with fields, untagged
57//!   enums with fields, and the `serde(flatten)` attribute.
58//!
59//! - TOML does not support the unit tuple `()`, unit (fieldless) structs, maps
60//!   with non-string keys, or top-level types that do not serialize to tables.
61//!
62//! - YAML does not support bytes or nested enums (e.g.,
63//!   `Enum::Variant(AnotherEnum)`, where `AnotherEnum` is "fat").
64//!
65//! Example
66//! =======
67//!
68//! ```compile_fail
69//! use serde::Deserialize;
70//!
71//! #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
72//! struct AppConfig {
73//!     #[serde(default)]
74//!     enable_foo: bool,
75//!     #[serde(default)]
76//!     bar_type: BarType,
77//!     #[serde(default)]
78//!     flavor: Option<String>,
79//! }
80//!
81//! #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
82//! enum BarType {
83//!     #[default]
84//!     Open,
85//!     Closed,
86//!     Clopen,
87//! }
88//!
89//! fn main() -> anyhow::Result<()> {
90//!     let Some(cfgpath) = std::env::args().nth(1) else {
91//!         anyhow::bail!("No configuration file specified");
92//!     };
93//!     // cfgfifo identifies the format used by the file `cfgpath` based on its
94//!     // file extension and deserializes it appropriately:
95//!     let cfg: AppConfig = cfgfifo::load(cfgpath)?;
96//!     println!("You specified the following configuration:");
97//!     println!("{cfg:#?}");
98//!     Ok(())
99//! }
100//! ```
101
102use serde::{de::DeserializeOwned, Serialize};
103#[allow(unused_imports)]
104use serde_path_to_error::{deserialize as depath, serialize as serpath, Error as PathError};
105use std::fs::File;
106use std::io::{self, Write};
107use std::path::Path;
108use strum::{Display, EnumIter, EnumString};
109use thiserror::Error;
110
111#[cfg(feature = "ron")]
112use ron::ser::PrettyConfig;
113
114/// An enum of file formats supported by this build of `cfgfifo`.
115///
116/// Each variant is only present if the corresponding Cargo feature of
117/// `cfgfifo` was enabled at compile time.
118///
119/// A `Format` can be [displayed][std::fmt::Display] as a string containing its
120/// name in all-uppercase, and a `Format` can be [parsed][std::str::FromStr]
121/// from its name in any case.
122#[derive(
123    Clone, Copy, Debug, Display, EnumIter, EnumString, Eq, Hash, Ord, PartialEq, PartialOrd,
124)]
125#[strum(ascii_case_insensitive, serialize_all = "UPPERCASE")]
126#[non_exhaustive]
127pub enum Format {
128    /// The [JSON](https://www.json.org) format, (de)serialized with the
129    /// [`serde_json`] crate.
130    ///
131    /// Serialization uses multiline/"pretty" format.
132    #[cfg(feature = "json")]
133    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
134    Json,
135
136    /// The [JSON5](https://json5.org) format, deserialized with the [`json5`]
137    /// crate.
138    ///
139    /// Serialization uses multiline/"pretty" format, performed via
140    /// `serde_json`, as json5's serialization (which also uses `serde_json`)
141    /// is single-line/"non-pretty."
142    #[cfg(feature = "json5")]
143    #[cfg_attr(docsrs, doc(cfg(feature = "json5")))]
144    Json5,
145
146    /// The [RON](https://github.com/ron-rs/ron) format, (de)serialized with
147    /// the [`ron`] crate.
148    ///
149    /// Serialization uses multiline/"pretty" format.
150    #[cfg(feature = "ron")]
151    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
152    Ron,
153
154    /// The [TOML](https://toml.io) format, (de)serialized with the [`toml`]
155    /// crate.
156    ///
157    /// Serialization uses "pretty" format, in which arrays are serialized on
158    /// multiple lines.
159    #[cfg(feature = "toml")]
160    #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
161    Toml,
162
163    /// The [YAML](https://yaml.org) format, (de)serialized with the
164    /// [`serde_yaml`] crate.
165    #[cfg(feature = "yaml")]
166    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
167    Yaml,
168}
169
170impl Format {
171    /// Returns an iterator over all [`Format`] variants
172    pub fn iter() -> FormatIter {
173        // To avoid the need for users to import the trait
174        <Format as strum::IntoEnumIterator>::iter()
175    }
176
177    /// Returns an array of the recognized file extensions for the file format.
178    ///
179    /// Each returned file extension is lowercase and does not start with a
180    /// period.  The array of file extensions for a given format is sorted in
181    /// lexicographical order.
182    #[cfg_attr(all(feature = "json", feature = "yaml"), doc = concat!(
183        "# Example\n",
184        "\n",
185        "```\n",
186        "use cfgfifo::Format;\n",
187        "\n",
188        "assert_eq!(Format::Json.extensions(), &[\"json\"]);\n",
189        "assert_eq!(Format::Yaml.extensions(), &[\"yaml\", \"yml\"]);\n",
190        "```\n",
191    ))]
192    pub fn extensions(&self) -> &'static [&'static str] {
193        match self {
194            #[cfg(feature = "json")]
195            Format::Json => &["json"],
196            #[cfg(feature = "json5")]
197            Format::Json5 => &["json5"],
198            #[cfg(feature = "ron")]
199            Format::Ron => &["ron"],
200            #[cfg(feature = "toml")]
201            Format::Toml => &["toml"],
202            #[cfg(feature = "yaml")]
203            Format::Yaml => &["yaml", "yml"],
204            #[allow(unreachable_patterns)]
205            _ => unreachable!(),
206        }
207    }
208
209    /// Test whether a given file extension is associated with the format
210    ///
211    /// The file extension is matched case-insensitively and may optionally
212    /// start with a period.
213    #[cfg_attr(feature = "json", doc = concat!(
214        "# Example\n",
215        "\n",
216        "```\n",
217        "use cfgfifo::Format;\n",
218        "\n",
219        "assert!(Format::Json.has_extension(\".json\"));\n",
220        "assert!(Format::Json.has_extension(\"JSON\"));\n",
221        "assert!(!Format::Json.has_extension(\"cfg\"));\n",
222        "```\n",
223    ))]
224    pub fn has_extension(&self, ext: &str) -> bool {
225        let ext = ext.strip_prefix('.').unwrap_or(ext);
226        self.extensions()
227            .iter()
228            .any(|x| x.eq_ignore_ascii_case(ext))
229    }
230
231    /// Converts a file extension to the corresponding [`Format`]
232    ///
233    /// File extensions are matched case-insensitively and may optionally start
234    /// with a period.  If the given file extension does not correspond to a
235    /// known file format, `None` is returned.
236    #[cfg_attr(all(feature = "json", feature = "yaml"), doc = concat!(
237        "# Example\n",
238        "\n",
239        "```\n",
240        "use cfgfifo::Format;\n",
241        "\n",
242        "assert_eq!(Format::from_extension(\".json\"), Some(Format::Json));\n",
243        "assert_eq!(Format::from_extension(\"YML\"), Some(Format::Yaml));\n",
244        "assert_eq!(Format::from_extension(\"cfg\"), None);\n",
245        "```\n",
246    ))]
247    pub fn from_extension(ext: &str) -> Option<Format> {
248        Format::iter().find(|f| f.has_extension(ext))
249    }
250
251    /// Determine the [`Format`] of a file path based on its file extension.
252    #[cfg_attr(all(feature = "json", feature = "ron"), doc = concat!(
253        "# Example\n",
254        "\n",
255        "```\n",
256        "use cfgfifo::Format;\n",
257        "\n",
258        "assert_eq!(Format::identify(\"path/to/file.json\").unwrap(), Format::Json);\n",
259        "assert_eq!(Format::identify(\"path/to/file.RON\").unwrap(), Format::Ron);\n",
260        "assert!(Format::identify(\"path/to/file.cfg\").is_err());\n",
261        "assert!(Format::identify(\"path/to/file\").is_err());\n",
262        "```\n",
263    ))]
264    /// # Errors
265    ///
266    /// Returns an error if the given file path does not have an extension, the
267    /// extension is not valid Unicode, or the extension is unknown to this
268    /// build.
269    pub fn identify<P: AsRef<Path>>(path: P) -> Result<Format, IdentifyError> {
270        let ext = get_ext(path.as_ref())?;
271        Format::from_extension(ext).ok_or_else(|| IdentifyError::Unknown(ext.to_owned()))
272    }
273
274    /// Serialize a value to a string in this format
275    #[cfg_attr(feature = "json", doc = concat!(
276        "# Example\n",
277        "\n",
278        "```\n",
279        "use cfgfifo::Format;\n",
280        "use serde::Serialize;\n",
281        "\n",
282        "#[derive(Clone, Debug, Eq, PartialEq, Serialize)]\n",
283        "struct Data {\n",
284        "    name: String,\n",
285        "    size: u32,\n",
286        "    enabled: bool,\n",
287        "}\n",
288        "\n",
289        "let datum = Data {\n",
290        "    name: String::from(\"Example\"),\n",
291        "    size: 42,\n",
292        "    enabled: true,\n",
293        "};\n",
294        "\n",
295        "let s = Format::Json.dump_to_string(&datum).unwrap();\n",
296        "\n",
297        "assert_eq!(\n",
298        "    s,\n",
299        "    concat!(\n",
300        "        \"{\\n\",\n",
301        "        \"  \\\"name\\\": \\\"Example\\\",\\n\",\n",
302        "        \"  \\\"size\\\": 42,\\n\",\n",
303        "        \"  \\\"enabled\\\": true\\n\",\n",
304        "        \"}\"\n",
305        "    )\n",
306        ");\n",
307        "```\n",
308    ))]
309    /// # Errors
310    ///
311    /// Returns an error if the underlying serializer returns an error.
312    #[allow(unused_variables)]
313    pub fn dump_to_string<T: Serialize>(&self, value: &T) -> Result<String, SerializeError> {
314        match self {
315            #[cfg(feature = "json")]
316            Format::Json => {
317                let mut buffer = Vec::new();
318                let mut ser = serde_json::Serializer::pretty(&mut buffer);
319                serpath(value, &mut ser)?;
320                let Ok(s) = String::from_utf8(buffer) else {
321                    unreachable!("serialized JSON should be valid UTF-8");
322                };
323                Ok(s)
324            }
325            #[cfg(feature = "json5")]
326            Format::Json5 => {
327                // json5::to_string() just serializes as JSON, but non-prettily
328                let mut buffer = Vec::new();
329                let mut ser = serde_json::Serializer::pretty(&mut buffer);
330                serpath(value, &mut ser)?;
331                let Ok(s) = String::from_utf8(buffer) else {
332                    unreachable!("serialized JSON should be valid UTF-8");
333                };
334                Ok(s)
335            }
336            #[cfg(feature = "ron")]
337            Format::Ron => {
338                let mut buffer = Vec::new();
339                let mut ser = ron::Serializer::new(&mut buffer, Some(ron_config()))
340                    .map_err(SerializeError::RonStart)?;
341                serpath(value, &mut ser)?;
342                let Ok(s) = String::from_utf8(buffer) else {
343                    unreachable!("serialized RON should be valid UTF-8");
344                };
345                Ok(s)
346            }
347            #[cfg(feature = "toml")]
348            Format::Toml => {
349                let mut s = String::new();
350                let ser = toml::Serializer::pretty(&mut s);
351                serpath(value, ser)?;
352                Ok(s)
353            }
354            #[cfg(feature = "yaml")]
355            Format::Yaml => {
356                let mut buffer = Vec::new();
357                self.dump_to_writer(&mut buffer, value)?;
358                let Ok(s) = String::from_utf8(buffer) else {
359                    unreachable!("serialized YAML should be valid UTF-8");
360                };
361                Ok(s)
362            }
363            #[allow(unreachable_patterns)]
364            _ => unreachable!(),
365        }
366    }
367
368    /// Deserialize a string in this format
369    #[cfg_attr(feature = "yaml", doc = concat!(
370        "# Example\n",
371        "\n",
372        "```\n",
373        "use cfgfifo::Format;\n",
374        "use serde::Deserialize;\n",
375        "\n",
376        "#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]\n",
377        "struct Data {\n",
378        "    name: String,\n",
379        "    size: u32,\n",
380        "    enabled: bool,\n",
381        "}\n",
382        "\n",
383        "let s = concat!(\n",
384        "    \"name: Example\\n\",\n",
385        "    \"size: 42\\n\",\n",
386        "    \"enabled: true\\n\",\n",
387        ");\n",
388        "\n",
389        "let datum: Data = Format::Yaml.load_from_str(s).unwrap();\n",
390        "\n",
391        "assert_eq!(\n",
392        "    datum,\n",
393        "    Data {\n",
394        "        name: String::from(\"Example\"),\n",
395        "        size: 42,\n",
396        "        enabled: true,\n",
397        "    }\n",
398        ");\n",
399        "```\n",
400    ))]
401    /// # Errors
402    ///
403    /// Returns an error if the underlying deserializer returns an error.
404    #[allow(unused_variables)]
405    pub fn load_from_str<T: DeserializeOwned>(&self, s: &str) -> Result<T, DeserializeError> {
406        match self {
407            #[cfg(feature = "json")]
408            Format::Json => {
409                let mut de = serde_json::Deserializer::from_str(s);
410                let value = depath(&mut de)?;
411                de.end().map_err(DeserializeError::JsonEnd)?;
412                Ok(value)
413            }
414            #[cfg(feature = "json5")]
415            Format::Json5 => {
416                let mut de =
417                    json5::Deserializer::from_str(s).map_err(DeserializeError::Json5Syntax)?;
418                depath(&mut de).map_err(Into::into)
419            }
420            #[cfg(feature = "ron")]
421            Format::Ron => {
422                let mut de = ron::Deserializer::from_str(s).map_err(DeserializeError::RonStart)?;
423                let value = match depath(&mut de) {
424                    Ok(value) => value,
425                    Err(e) => {
426                        let path = e.path().clone();
427                        let inner = e.into_inner();
428                        let ron_e = de.span_error(inner);
429                        return Err(DeserializeError::Ron(PathError::new(path, ron_e)));
430                    }
431                };
432                de.end()
433                    .map_err(|e| DeserializeError::RonEnd(de.span_error(e)))?;
434                Ok(value)
435            }
436            #[cfg(feature = "toml")]
437            Format::Toml => {
438                let de = toml::Deserializer::new(s);
439                depath(de).map_err(Into::into)
440            }
441            #[cfg(feature = "yaml")]
442            Format::Yaml => {
443                let de = serde_yaml::Deserializer::from_str(s);
444                depath(de).map_err(Into::into)
445            }
446            #[allow(unreachable_patterns)]
447            _ => unreachable!(),
448        }
449    }
450
451    /// Serialize a value to a [writer][std::io::Write] in this format.
452    ///
453    /// If the format's serializer does not normally end its output with a
454    /// newline, one is appended so that the written text always ends in a
455    /// newline.
456    ///
457    /// # Errors
458    ///
459    /// Returns an error if an I/O error occurs or if the underlying serializer
460    /// returns an error.
461    #[allow(unused_mut, unused_variables)]
462    pub fn dump_to_writer<W: Write, T: Serialize>(
463        &self,
464        mut writer: W,
465        value: &T,
466    ) -> Result<(), SerializeError> {
467        match self {
468            #[cfg(feature = "json")]
469            Format::Json => {
470                let mut ser = serde_json::Serializer::pretty(&mut writer);
471                serpath(value, &mut ser)?;
472                writer.write_all(b"\n")?;
473                Ok(())
474            }
475            #[cfg(feature = "json5")]
476            Format::Json5 => {
477                // Serialize as JSON, as that's what json5 does, except the
478                // latter doesn't support serializing to a writer.
479                let mut ser = serde_json::Serializer::pretty(&mut writer);
480                serpath(value, &mut ser)?;
481                writer.write_all(b"\n")?;
482                Ok(())
483            }
484            #[cfg(feature = "ron")]
485            Format::Ron => {
486                let mut ser = ron::Serializer::new(&mut writer, Some(ron_config()))
487                    .map_err(SerializeError::RonStart)?;
488                serpath(value, &mut ser)?;
489                writer.write_all(b"\n")?;
490                Ok(())
491            }
492            #[cfg(feature = "toml")]
493            Format::Toml => {
494                let s = self.dump_to_string(value)?;
495                writer.write_all(s.as_bytes())?;
496                Ok(())
497            }
498            #[cfg(feature = "yaml")]
499            Format::Yaml => {
500                let mut ser = serde_yaml::Serializer::new(writer);
501                serpath(value, &mut ser).map_err(Into::into)
502            }
503            #[allow(unreachable_patterns)]
504            _ => unreachable!(),
505        }
506    }
507
508    /// Deserialize a value in this format from a [reader][std::io::Read].
509    ///
510    /// # Errors
511    ///
512    /// Returns an error if an I/O error occurs or if the underlying
513    /// deserializer returns an error.
514    #[allow(unused_variables)]
515    pub fn load_from_reader<R: io::Read, T: DeserializeOwned>(
516        &self,
517        reader: R,
518    ) -> Result<T, DeserializeError> {
519        match self {
520            #[cfg(feature = "json")]
521            Format::Json => {
522                let mut de = serde_json::Deserializer::from_reader(reader);
523                let value = depath(&mut de)?;
524                de.end().map_err(DeserializeError::JsonEnd)?;
525                Ok(value)
526            }
527            #[cfg(feature = "json5")]
528            Format::Json5 => {
529                let s = io::read_to_string(reader)?;
530                self.load_from_str(&s)
531            }
532            #[cfg(feature = "ron")]
533            Format::Ron => {
534                let s = io::read_to_string(reader)?;
535                self.load_from_str(&s)
536            }
537            #[cfg(feature = "toml")]
538            Format::Toml => {
539                let s = io::read_to_string(reader)?;
540                self.load_from_str(&s)
541            }
542            #[cfg(feature = "yaml")]
543            Format::Yaml => {
544                let de = serde_yaml::Deserializer::from_reader(reader);
545                depath(de).map_err(Into::into)
546            }
547            #[allow(unreachable_patterns)]
548            _ => unreachable!(),
549        }
550    }
551}
552
553/// Deserialize the contents of the given file, with the format automatically
554/// determined based on the file's extension.
555///
556/// # Errors
557///
558/// Returns an error if the format cannot be determined from the file
559/// extension, if an I/O error occurs, or if the underlying deserializer
560/// returns an error.
561pub fn load<T: DeserializeOwned, P: AsRef<Path>>(path: P) -> Result<T, LoadError> {
562    Cfgfifo::default().load(path)
563}
564
565/// Serialize a value to the given file, with the format automatically
566/// determined based on the file's extension.
567///
568/// # Errors
569///
570/// Returns an error if the format cannot be determined from the file
571/// extension, if an I/O error occurs, or if the underlying serializer returns
572/// an error.
573pub fn dump<P: AsRef<Path>, T: Serialize>(path: P, value: &T) -> Result<(), DumpError> {
574    Cfgfifo::default().dump(path, value)
575}
576
577/// A configurable loader & dumper of serialized data in files.
578///
579/// By default, a `Cfgfifo` instance's [`identify()`][Cfgfifo::identify],
580/// [`load()`][Cfgfifo::load], and [`dump()`][Cfgfifo::dump] methods act the
581/// same as [`Format::identify()`], [`load()`], and [`dump()`], but the
582/// instance can be customized to only support a subset of enabled [`Format`]s
583/// and/or to use a given fallback [`Format`] if identifying a file's format
584/// fails.
585#[derive(Clone, Debug, Eq, PartialEq)]
586pub struct Cfgfifo {
587    formats: Vec<Format>,
588    fallback: Option<Format>,
589}
590
591impl Cfgfifo {
592    /// Create a new Cfgfifo instance
593    pub fn new() -> Cfgfifo {
594        Cfgfifo {
595            formats: Format::iter().collect(),
596            fallback: None,
597        }
598    }
599
600    /// Set the [`Format`]s to support.
601    ///
602    /// By default, all enabled formats are selected.
603    ///
604    /// This is useful if you want to always restrict loading & dumping to a
605    /// certain set of formats even if more formats become enabled via [feature
606    /// unification].
607    ///
608    /// [feature unification]: https://doc.rust-lang.org/cargo/reference/features.html#feature-unification
609    pub fn formats<I: IntoIterator<Item = Format>>(mut self, iter: I) -> Self {
610        self.formats = iter.into_iter().collect();
611        self
612    }
613
614    /// Set a fallback [`Format`] to use if file format identification fails
615    pub fn fallback(mut self, fallback: Option<Format>) -> Self {
616        self.fallback = fallback;
617        self
618    }
619
620    /// Determine the [`Format`] of a file path based on its file extension.
621    #[cfg_attr(all(feature = "json", feature = "yaml"), doc = concat!(
622        "# Example\n",
623        "\n",
624        "```\n",
625        "use cfgfifo::{Cfgfifo, Format};\n",
626        "\n",
627        "let cfgfifo = Cfgfifo::new()\n",
628        "    .formats([Format::Json, Format::Yaml])\n",
629        "    .fallback(Some(Format::Json));\n",
630        "\n",
631        "assert_eq!(cfgfifo.identify(\"path/to/file.json\").unwrap(), Format::Json);\n",
632        "assert_eq!(cfgfifo.identify(\"path/to/file.YML\").unwrap(), Format::Yaml);\n",
633        "assert_eq!(cfgfifo.identify(\"path/to/file.ron\").unwrap(), Format::Json);\n",
634        "assert_eq!(cfgfifo.identify(\"path/to/file.cfg\").unwrap(), Format::Json);\n",
635        "assert_eq!(cfgfifo.identify(\"path/to/file\").unwrap(), Format::Json);\n",
636        "```\n",
637    ))]
638    /// # Errors
639    ///
640    /// Returns an error if the given file path does not have an extension, the
641    /// extension is not valid Unicode, or the extension does not belong to a
642    /// supported [`Format`].
643    ///
644    /// All error conditions are suppressed if a [fallback][Cfgfifo::fallback]
645    /// was set.
646    pub fn identify<P: AsRef<Path>>(&self, path: P) -> Result<Format, IdentifyError> {
647        let ext = match (get_ext(path.as_ref()), self.fallback) {
648            (Ok(ext), _) => ext,
649            #[allow(unreachable_patterns)]
650            (Err(_), Some(f)) => return Ok(f),
651            (Err(e), _) => return Err(e),
652        };
653        self.formats
654            .iter()
655            .find(|f| f.has_extension(ext))
656            .copied()
657            .or(self.fallback)
658            .ok_or_else(|| IdentifyError::Unknown(ext.to_owned()))
659    }
660
661    /// Deserialize the contents of the given file, with the format
662    /// automatically determined based on the file's extension.
663    ///
664    /// # Errors
665    ///
666    /// Returns an error if the format cannot be determined from the file
667    /// extension and no fallback format was set, if an I/O error occurs, or if
668    /// the underlying deserializer returns an error.
669    pub fn load<T: DeserializeOwned, P: AsRef<Path>>(&self, path: P) -> Result<T, LoadError> {
670        let fmt = self.identify(&path)?;
671        let fp = io::BufReader::new(File::open(path).map_err(LoadError::Open)?);
672        fmt.load_from_reader(fp).map_err(Into::into)
673    }
674
675    /// Serialize a value to the given file, with the format automatically
676    /// determined based on the file's extension.
677    ///
678    /// # Errors
679    ///
680    /// Returns an error if the format cannot be determined from the file
681    /// extension and no fallback format was set, if an I/O error occurs, or if
682    /// the underlying serializer returns an error.
683    pub fn dump<P: AsRef<Path>, T: Serialize>(&self, path: P, value: &T) -> Result<(), DumpError> {
684        let fmt = self.identify(&path)?;
685        let mut fp = io::BufWriter::new(File::create(path).map_err(DumpError::Open)?);
686        fmt.dump_to_writer(&mut fp, value)?;
687        fp.flush().map_err(DumpError::Flush)
688    }
689}
690
691impl Default for Cfgfifo {
692    /// Same as [`Cfgfifo::new()`]
693    fn default() -> Cfgfifo {
694        Cfgfifo::new()
695    }
696}
697
698/// Error type returned by [`Format::identify()`] and [`Cfgfifo::identify()`]
699#[derive(Clone, Debug, Eq, Error, PartialEq)]
700pub enum IdentifyError {
701    /// Returned if the file path's extension did not correspond to a known &
702    /// enabled file format
703    #[error("unknown file extension: {0:?}")]
704    Unknown(
705        /// The file extension (without leading period)
706        String,
707    ),
708    /// Returned if the file path's extension was not valid Unicode
709    #[error("file extension is not valid Unicode")]
710    NotUnicode,
711    /// Returned if the file path did not have a file extension
712    #[error("file does not have a file extension")]
713    NoExtension,
714}
715
716/// Error type returned by [`Format::dump_to_string()`] and
717/// [`Format::dump_to_writer()`]
718///
719/// The available variants on this enum depend on which formats were enabled at
720/// compile time.  Where possible, errors from the format serializers are
721/// wrapped in [`serde_path_to_error::Error`].
722#[derive(Debug, Error)]
723#[non_exhaustive]
724pub enum SerializeError {
725    /// Returned if an I/O error occurred while writing to a writer.
726    ///
727    /// Some serializers may catch & report such errors themselves.
728    #[error(transparent)]
729    Io(#[from] io::Error),
730
731    /// Returned if JSON or JSON5 serialization failed
732    #[cfg(any(feature = "json", feature = "json5"))]
733    #[cfg_attr(docsrs, doc(cfg(any(feature = "json", feature = "json5"))))]
734    #[error(transparent)]
735    Json(#[from] PathError<serde_json::Error>),
736
737    /// Returned if initializing RON serialization failed
738    #[cfg(feature = "ron")]
739    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
740    #[error(transparent)]
741    RonStart(ron::error::Error),
742
743    /// Returned if RON serialization failed
744    #[cfg(feature = "ron")]
745    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
746    #[error(transparent)]
747    Ron(#[from] PathError<ron::error::Error>),
748
749    /// Returned if TOML serialization failed
750    #[cfg(feature = "toml")]
751    #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
752    #[error(transparent)]
753    Toml(#[from] PathError<toml::ser::Error>),
754
755    /// Returned if YAML serialization failed
756    #[cfg(feature = "yaml")]
757    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
758    #[error(transparent)]
759    Yaml(#[from] PathError<serde_yaml::Error>),
760}
761
762/// Error type returned by [`Format::load_from_str()`] and
763/// [`Format::load_from_reader()`]
764///
765/// The available variants on this enum depend on which formats were enabled at
766/// compile time.  Where possible, errors from the format deserializers are
767/// wrapped in [`serde_path_to_error::Error`].
768#[derive(Debug, Error)]
769#[non_exhaustive]
770pub enum DeserializeError {
771    /// Returned if an I/O error occurred while reading from a reader.
772    ///
773    /// Some deserializers may catch & report such errors themselves.
774    #[error(transparent)]
775    Io(#[from] io::Error),
776
777    /// Returned if JSON deserialization failed
778    #[cfg(feature = "json")]
779    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
780    #[error(transparent)]
781    Json(#[from] PathError<serde_json::Error>),
782
783    /// Returned if JSON input had invalid trailing characters
784    #[cfg(feature = "json")]
785    #[cfg_attr(docsrs, doc(cfg(feature = "json")))]
786    #[error(transparent)]
787    JsonEnd(serde_json::Error),
788
789    /// Returned if JSON5 deserialization failed due to the input having
790    /// invalid syntax
791    #[cfg(feature = "json5")]
792    #[cfg_attr(docsrs, doc(cfg(feature = "json5")))]
793    #[error(transparent)]
794    Json5Syntax(json5::Error),
795
796    /// Returned if JSON5 deserialization failed
797    #[cfg(feature = "json5")]
798    #[cfg_attr(docsrs, doc(cfg(feature = "json5")))]
799    #[error(transparent)]
800    Json5(#[from] PathError<json5::Error>),
801
802    /// Returned if initializing RON deserialization failed
803    #[cfg(feature = "ron")]
804    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
805    #[error(transparent)]
806    RonStart(ron::error::SpannedError),
807
808    /// Returned if RON deserialization failed
809    #[cfg(feature = "ron")]
810    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
811    #[error(transparent)]
812    Ron(#[from] PathError<ron::error::SpannedError>),
813
814    /// Returned if RON input had invalid trailing characters
815    #[cfg(feature = "ron")]
816    #[cfg_attr(docsrs, doc(cfg(feature = "ron")))]
817    #[error(transparent)]
818    RonEnd(ron::error::SpannedError),
819
820    /// Returned if TOML deserialization failed
821    #[cfg(feature = "toml")]
822    #[cfg_attr(docsrs, doc(cfg(feature = "toml")))]
823    #[error(transparent)]
824    Toml(#[from] PathError<toml::de::Error>),
825
826    /// Returned if YAML deserialization failed
827    #[cfg(feature = "yaml")]
828    #[cfg_attr(docsrs, doc(cfg(feature = "yaml")))]
829    #[error(transparent)]
830    Yaml(#[from] PathError<serde_yaml::Error>),
831}
832
833/// Error type returned by [`load()`] and [`Cfgfifo::load()`]
834#[derive(Debug, Error)]
835pub enum LoadError {
836    /// Returned if the file format could not be identified from the file
837    /// extension
838    #[error("failed to identify file format")]
839    Identify(#[from] IdentifyError),
840
841    /// Returned if the file could not be opened for reading
842    #[error("failed to open file for reading")]
843    Open(#[source] io::Error),
844
845    /// Returned if deserialization failed
846    #[error("failed to deserialize file contents")]
847    Deserialize(#[from] DeserializeError),
848}
849
850/// Error type returned by [`dump()`] and [`Cfgfifo::dump()`]
851#[derive(Debug, Error)]
852pub enum DumpError {
853    /// Returned if the file format could not be identified from the file
854    /// extension
855    #[error("failed to identify file format")]
856    Identify(#[from] IdentifyError),
857
858    /// Returned if the file could not be opened for writing
859    #[error("failed to open file for writing")]
860    Open(#[source] io::Error),
861
862    /// Returned if serialization failed
863    #[error("failed to serialize structure")]
864    Serialize(#[from] SerializeError),
865
866    /// Returned if flushing the file failed after writing
867    #[error("failed to flush output file")]
868    Flush(#[source] io::Error),
869}
870
871#[cfg(feature = "ron")]
872fn ron_config() -> PrettyConfig {
873    // The default PrettyConfig sets new_line to CR LF on Windows.  Let's not
874    // do that here.
875    PrettyConfig::default().new_line(String::from("\n"))
876}
877
878fn get_ext(path: &Path) -> Result<&str, IdentifyError> {
879    path.extension()
880        .ok_or(IdentifyError::NoExtension)?
881        .to_str()
882        .ok_or(IdentifyError::NotUnicode)
883}
884
885#[cfg(test)]
886mod tests {
887    use super::*;
888    use rstest::rstest;
889
890    #[rstest]
891    #[case("file.ini", "ini")]
892    #[case("file.xml", "xml")]
893    #[case("file.cfg", "cfg")]
894    #[case("file.jsn", "jsn")]
895    #[case("file.tml", "tml")]
896    fn identify_unknown(#[case] path: &str, #[case] ext: String) {
897        assert_eq!(
898            Format::identify(path),
899            Err(IdentifyError::Unknown(ext.clone()))
900        );
901        assert_eq!(
902            Cfgfifo::default().identify(path),
903            Err(IdentifyError::Unknown(ext))
904        );
905    }
906
907    #[cfg(unix)]
908    #[test]
909    fn identify_not_unicode() {
910        use std::os::unix::ffi::OsStrExt;
911        let path = std::ffi::OsStr::from_bytes(b"file.js\xF6n");
912        assert_eq!(Format::identify(path), Err(IdentifyError::NotUnicode));
913        assert_eq!(
914            Cfgfifo::default().identify(path),
915            Err(IdentifyError::NotUnicode)
916        );
917    }
918
919    #[cfg(windows)]
920    #[test]
921    fn identify_not_unicode() {
922        use std::os::windows::ffi::OsStringExt;
923        let path = std::ffi::OsString::from_wide(&[
924            0x66, 0x69, 0x6C, 0x65, 0x2E, 0x6A, 0xDC00, 0x73, 0x6E,
925        ]);
926        assert_eq!(Format::identify(&path), Err(IdentifyError::NotUnicode));
927        assert_eq!(
928            Cfgfifo::default().identify(path),
929            Err(IdentifyError::NotUnicode)
930        );
931    }
932
933    #[test]
934    fn identify_no_ext() {
935        assert_eq!(Format::identify("file"), Err(IdentifyError::NoExtension));
936        assert_eq!(
937            Cfgfifo::default().identify("file"),
938            Err(IdentifyError::NoExtension)
939        );
940    }
941
942    #[cfg(feature = "json")]
943    mod json {
944        use super::*;
945
946        #[test]
947        fn basics() {
948            let f = Format::Json;
949            assert_eq!(f.to_string(), "JSON");
950            assert_eq!(f.extensions(), ["json"]);
951            assert_eq!("json".parse::<Format>().unwrap(), f);
952            assert_eq!("JSON".parse::<Format>().unwrap(), f);
953            assert_eq!("Json".parse::<Format>().unwrap(), f);
954            assert!(Format::iter().any(|f2| f == f2));
955        }
956
957        #[rstest]
958        #[case("json")]
959        #[case(".json")]
960        #[case("JSON")]
961        #[case(".JSON")]
962        fn from_extension(#[case] ext: &str) {
963            assert!(Format::Json.has_extension(ext));
964            assert_eq!(Format::from_extension(ext).unwrap(), Format::Json);
965        }
966
967        #[rstest]
968        #[case("file.json")]
969        #[case("dir/file.JSON")]
970        #[case("/dir/file.Json")]
971        fn identify(#[case] path: &str) {
972            assert_eq!(Format::identify(path).unwrap(), Format::Json);
973        }
974    }
975
976    #[cfg(not(feature = "json"))]
977    mod not_json {
978        use super::*;
979
980        #[test]
981        fn not_variant() {
982            assert!(!Format::iter().any(|f| f.to_string() == "JSON"));
983        }
984
985        #[test]
986        fn identify() {
987            assert_eq!(
988                Format::identify("file.json"),
989                Err(IdentifyError::Unknown(String::from("json")))
990            );
991        }
992    }
993
994    #[cfg(feature = "json5")]
995    mod json5 {
996        use super::*;
997
998        #[test]
999        fn basics() {
1000            let f = Format::Json5;
1001            assert_eq!(f.to_string(), "JSON5");
1002            assert_eq!(f.extensions(), ["json5"]);
1003            assert_eq!("json5".parse::<Format>().unwrap(), f);
1004            assert_eq!("JSON5".parse::<Format>().unwrap(), f);
1005            assert_eq!("Json5".parse::<Format>().unwrap(), f);
1006            assert!(Format::iter().any(|f2| f == f2));
1007        }
1008
1009        #[rstest]
1010        #[case("json5")]
1011        #[case(".json5")]
1012        #[case("JSON5")]
1013        #[case(".JSON5")]
1014        fn from_extension(#[case] ext: &str) {
1015            assert!(Format::Json5.has_extension(ext));
1016            assert_eq!(Format::from_extension(ext).unwrap(), Format::Json5);
1017        }
1018
1019        #[rstest]
1020        #[case("file.json5")]
1021        #[case("dir/file.JSON5")]
1022        #[case("/dir/file.Json5")]
1023        fn identify(#[case] path: &str) {
1024            assert_eq!(Format::identify(path).unwrap(), Format::Json5);
1025        }
1026    }
1027
1028    #[cfg(not(feature = "json5"))]
1029    mod not_json5 {
1030        use super::*;
1031
1032        #[test]
1033        fn not_variant() {
1034            assert!(!Format::iter().any(|f| f.to_string() == "JSON5"));
1035        }
1036
1037        #[test]
1038        fn identify() {
1039            assert_eq!(
1040                Format::identify("file.json5"),
1041                Err(IdentifyError::Unknown(String::from("json5")))
1042            );
1043        }
1044    }
1045
1046    #[cfg(feature = "ron")]
1047    mod ron {
1048        use super::*;
1049
1050        #[test]
1051        fn basics() {
1052            let f = Format::Ron;
1053            assert_eq!(f.to_string(), "RON");
1054            assert_eq!(f.extensions(), ["ron"]);
1055            assert_eq!("ron".parse::<Format>().unwrap(), f);
1056            assert_eq!("RON".parse::<Format>().unwrap(), f);
1057            assert_eq!("Ron".parse::<Format>().unwrap(), f);
1058            assert!(Format::iter().any(|f2| f == f2));
1059        }
1060
1061        #[rstest]
1062        #[case("ron")]
1063        #[case(".ron")]
1064        #[case("RON")]
1065        #[case(".RON")]
1066        fn from_extension(#[case] ext: &str) {
1067            assert!(Format::Ron.has_extension(ext));
1068            assert_eq!(Format::from_extension(ext).unwrap(), Format::Ron);
1069        }
1070
1071        #[rstest]
1072        #[case("file.ron")]
1073        #[case("dir/file.RON")]
1074        #[case("/dir/file.Ron")]
1075        fn identify(#[case] path: &str) {
1076            assert_eq!(Format::identify(path).unwrap(), Format::Ron);
1077        }
1078    }
1079
1080    #[cfg(not(feature = "ron"))]
1081    mod not_ron {
1082        use super::*;
1083
1084        #[test]
1085        fn not_variant() {
1086            assert!(!Format::iter().any(|f| f.to_string() == "RON"));
1087        }
1088
1089        #[test]
1090        fn identify() {
1091            assert_eq!(
1092                Format::identify("file.ron"),
1093                Err(IdentifyError::Unknown(String::from("ron")))
1094            );
1095        }
1096    }
1097
1098    #[cfg(feature = "toml")]
1099    mod toml {
1100        use super::*;
1101
1102        #[test]
1103        fn basics() {
1104            let f = Format::Toml;
1105            assert_eq!(f.to_string(), "TOML");
1106            assert_eq!(f.extensions(), ["toml"]);
1107            assert_eq!("toml".parse::<Format>().unwrap(), f);
1108            assert_eq!("TOML".parse::<Format>().unwrap(), f);
1109            assert_eq!("Toml".parse::<Format>().unwrap(), f);
1110            assert!(Format::iter().any(|f2| f == f2));
1111        }
1112
1113        #[rstest]
1114        #[case("toml")]
1115        #[case(".toml")]
1116        #[case("TOML")]
1117        #[case(".TOML")]
1118        fn from_extension(#[case] ext: &str) {
1119            assert!(Format::Toml.has_extension(ext));
1120            assert_eq!(Format::from_extension(ext).unwrap(), Format::Toml);
1121        }
1122
1123        #[rstest]
1124        #[case("file.toml")]
1125        #[case("dir/file.TOML")]
1126        #[case("/dir/file.Toml")]
1127        fn identify(#[case] path: &str) {
1128            assert_eq!(Format::identify(path).unwrap(), Format::Toml);
1129        }
1130    }
1131
1132    #[cfg(not(feature = "toml"))]
1133    mod not_toml {
1134        use super::*;
1135
1136        #[test]
1137        fn not_variant() {
1138            assert!(!Format::iter().any(|f| f.to_string() == "TOML"));
1139        }
1140
1141        #[test]
1142        fn identify() {
1143            assert_eq!(
1144                Format::identify("file.toml"),
1145                Err(IdentifyError::Unknown(String::from("toml")))
1146            );
1147        }
1148    }
1149
1150    #[cfg(feature = "yaml")]
1151    mod yaml {
1152        use super::*;
1153
1154        #[test]
1155        fn basics() {
1156            let f = Format::Yaml;
1157            assert_eq!(f.to_string(), "YAML");
1158            assert_eq!(f.extensions(), ["yaml", "yml"]);
1159            assert_eq!("yaml".parse::<Format>().unwrap(), f);
1160            assert_eq!("YAML".parse::<Format>().unwrap(), f);
1161            assert_eq!("Yaml".parse::<Format>().unwrap(), f);
1162            assert!(Format::iter().any(|f2| f == f2));
1163        }
1164
1165        #[rstest]
1166        #[case("yaml")]
1167        #[case(".yaml")]
1168        #[case("YAML")]
1169        #[case(".YAML")]
1170        #[case("yml")]
1171        #[case(".yml")]
1172        #[case("YML")]
1173        #[case(".YML")]
1174        fn from_extension(#[case] ext: &str) {
1175            assert!(Format::Yaml.has_extension(ext));
1176            assert_eq!(Format::from_extension(ext).unwrap(), Format::Yaml);
1177        }
1178
1179        #[rstest]
1180        #[case("file.yaml")]
1181        #[case("dir/file.YAML")]
1182        #[case("/dir/file.Yaml")]
1183        #[case("file.yml")]
1184        #[case("dir/file.YML")]
1185        #[case("/dir/file.Yml")]
1186        fn identify(#[case] path: &str) {
1187            assert_eq!(Format::identify(path).unwrap(), Format::Yaml);
1188        }
1189    }
1190
1191    #[cfg(not(feature = "yaml"))]
1192    mod not_yaml {
1193        use super::*;
1194
1195        #[test]
1196        fn not_variant() {
1197            assert!(!Format::iter().any(|f| f.to_string() == "YAML"));
1198        }
1199
1200        #[test]
1201        fn identify() {
1202            assert_eq!(
1203                Format::identify("file.yaml"),
1204                Err(IdentifyError::Unknown(String::from("yaml")))
1205            );
1206        }
1207    }
1208
1209    mod cfgfifo {
1210        #[allow(unused_imports)]
1211        use super::*;
1212
1213        #[cfg(all(
1214            feature = "json",
1215            feature = "json5",
1216            feature = "ron",
1217            feature = "toml",
1218            feature = "yaml"
1219        ))]
1220        #[test]
1221        fn default() {
1222            let cfg = Cfgfifo::default();
1223            assert_eq!(cfg.identify("file.json").unwrap(), Format::Json);
1224            assert_eq!(cfg.identify("file.json5").unwrap(), Format::Json5);
1225            assert_eq!(cfg.identify("file.Ron").unwrap(), Format::Ron);
1226            assert_eq!(cfg.identify("file.toml").unwrap(), Format::Toml);
1227            assert_eq!(cfg.identify("file.YML").unwrap(), Format::Yaml);
1228            assert!(cfg.identify("file.cfg").is_err());
1229            assert!(cfg.identify("file").is_err());
1230        }
1231
1232        #[cfg(all(
1233            feature = "json",
1234            feature = "json5",
1235            feature = "ron",
1236            feature = "toml",
1237            feature = "yaml"
1238        ))]
1239        #[test]
1240        fn fallback() {
1241            let cfg = Cfgfifo::new().fallback(Some(Format::Json));
1242            assert_eq!(cfg.identify("file.json").unwrap(), Format::Json);
1243            assert_eq!(cfg.identify("file.json5").unwrap(), Format::Json5);
1244            assert_eq!(cfg.identify("file.Ron").unwrap(), Format::Ron);
1245            assert_eq!(cfg.identify("file.toml").unwrap(), Format::Toml);
1246            assert_eq!(cfg.identify("file.YML").unwrap(), Format::Yaml);
1247            assert_eq!(cfg.identify("file.cfg").unwrap(), Format::Json);
1248            assert_eq!(cfg.identify("file").unwrap(), Format::Json);
1249        }
1250
1251        #[cfg(all(feature = "json", feature = "toml"))]
1252        #[test]
1253        fn formats() {
1254            let cfg = Cfgfifo::new().formats([Format::Json, Format::Toml]);
1255            assert_eq!(cfg.identify("file.json").unwrap(), Format::Json);
1256            assert!(cfg.identify("file.json5").is_err());
1257            assert!(cfg.identify("file.Ron").is_err());
1258            assert_eq!(cfg.identify("file.toml").unwrap(), Format::Toml);
1259            assert!(cfg.identify("file.YML").is_err());
1260            assert!(cfg.identify("file.cfg").is_err());
1261            assert!(cfg.identify("file").is_err());
1262        }
1263
1264        #[cfg(all(feature = "json", feature = "toml", feature = "yaml"))]
1265        #[test]
1266        fn formats_fallback() {
1267            let cfg = Cfgfifo::new()
1268                .formats([Format::Json, Format::Toml])
1269                .fallback(Some(Format::Yaml));
1270            assert_eq!(cfg.identify("file.json").unwrap(), Format::Json);
1271            assert_eq!(cfg.identify("file.json5").unwrap(), Format::Yaml);
1272            assert_eq!(cfg.identify("file.Ron").unwrap(), Format::Yaml);
1273            assert_eq!(cfg.identify("file.toml").unwrap(), Format::Toml);
1274            assert_eq!(cfg.identify("file.YML").unwrap(), Format::Yaml);
1275            assert_eq!(cfg.identify("file.cfg").unwrap(), Format::Yaml);
1276            assert_eq!(cfg.identify("file").unwrap(), Format::Yaml);
1277        }
1278    }
1279}