Skip to main content

kas_core/config/
format.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Configuration formats and read/write support
7
8#[cfg(feature = "serde")]
9use serde::{Serialize, de::DeserializeOwned};
10use std::path::Path;
11use thiserror::Error;
12
13/// Configuration read/write/format errors
14#[derive(Error, Debug)]
15pub enum Error {
16    #[cfg(feature = "yaml")]
17    #[error("config deserialisation failed")]
18    De(#[from] serde::de::value::Error),
19
20    #[cfg(feature = "yaml")]
21    #[error("config serialisation to YAML failed")]
22    YamlSer(#[from] serde_yaml2::ser::Errors),
23
24    #[cfg(feature = "json")]
25    #[error("config (de)serialisation to JSON failed")]
26    Json(#[from] serde_json::Error),
27
28    #[cfg(feature = "ron")]
29    #[error("config serialisation to RON failed")]
30    Ron(#[from] ron::Error),
31
32    #[cfg(feature = "ron")]
33    #[error("config deserialisation from RON failed")]
34    RonSpanned(#[from] ron::error::SpannedError),
35
36    #[cfg(feature = "toml")]
37    #[error("config deserialisation from TOML failed")]
38    TomlDe(#[from] toml::de::Error),
39
40    #[cfg(feature = "toml")]
41    #[error("config serialisation to TOML failed")]
42    TomlSer(#[from] toml::ser::Error),
43
44    #[error("error reading / writing config file")]
45    IoError(#[from] std::io::Error),
46
47    #[error("format not supported: {0}")]
48    UnsupportedFormat(Format),
49}
50
51/// Configuration serialisation formats
52#[non_exhaustive]
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Error)]
54pub enum Format {
55    /// Not specified: guess from the path
56    #[default]
57    #[error("no format")]
58    None,
59
60    /// JavaScript Object Notation
61    #[error("JSON")]
62    Json,
63
64    /// Tom's Obvious Minimal Language
65    #[error("TOML")]
66    Toml,
67
68    /// YAML Ain't Markup Language
69    #[error("YAML")]
70    Yaml,
71
72    /// Rusty Object Notation
73    #[error("RON")]
74    Ron,
75
76    /// Error: unable to guess format
77    #[error("(unknown format)")]
78    Unknown,
79}
80
81impl Format {
82    /// Guess format from the path name
83    ///
84    /// This does not open the file.
85    ///
86    /// Potentially fallible: on error, returns [`Format::Unknown`].
87    /// This may be due to unrecognised file extension or due to the required
88    /// feature not being enabled.
89    pub fn guess_from_path(path: &Path) -> Format {
90        // use == since there is no OsStr literal
91        if let Some(ext) = path.extension() {
92            if ext == "json" {
93                Format::Json
94            } else if ext == "toml" {
95                Format::Toml
96            } else if ext == "yaml" {
97                Format::Yaml
98            } else if ext == "ron" {
99                Format::Ron
100            } else {
101                Format::Unknown
102            }
103        } else {
104            Format::Unknown
105        }
106    }
107
108    /// Read from a path
109    #[cfg(feature = "serde")]
110    pub fn read_path<T: DeserializeOwned>(self, path: &Path) -> Result<T, Error> {
111        log::info!("read_path: path={}, format={:?}", path.display(), self);
112        match self {
113            #[cfg(feature = "json")]
114            Format::Json => {
115                let r = std::io::BufReader::new(std::fs::File::open(path)?);
116                Ok(serde_json::from_reader(r)?)
117            }
118            #[cfg(feature = "yaml")]
119            Format::Yaml => {
120                let contents = std::fs::read_to_string(path)?;
121                Ok(serde_yaml2::from_str(&contents)?)
122            }
123            #[cfg(feature = "ron")]
124            Format::Ron => {
125                let r = std::io::BufReader::new(std::fs::File::open(path)?);
126                Ok(ron::de::from_reader(r)?)
127            }
128            #[cfg(feature = "toml")]
129            Format::Toml => {
130                let contents = std::fs::read_to_string(path)?;
131                Ok(toml::from_str(&contents)?)
132            }
133            _ => {
134                let _ = path; // squelch unused warning
135                Err(Error::UnsupportedFormat(self))
136            }
137        }
138    }
139
140    /// Write to a path
141    #[cfg(feature = "serde")]
142    pub fn write_path<T: Serialize>(self, path: &Path, value: &T) -> Result<(), Error> {
143        log::info!("write_path: path={}, format={:?}", path.display(), self);
144        // Note: we use to_string*, not to_writer*, since the latter may
145        // generate incomplete documents on failure.
146        match self {
147            #[cfg(feature = "json")]
148            Format::Json => {
149                let text = serde_json::to_string_pretty(value)?;
150                std::fs::write(path, &text)?;
151                Ok(())
152            }
153            #[cfg(feature = "yaml")]
154            Format::Yaml => {
155                let text = serde_yaml2::to_string(value)?;
156                std::fs::write(path, text)?;
157                Ok(())
158            }
159            #[cfg(feature = "ron")]
160            Format::Ron => {
161                let pretty = ron::ser::PrettyConfig::default();
162                let text = ron::ser::to_string_pretty(value, pretty)?;
163                std::fs::write(path, &text)?;
164                Ok(())
165            }
166            #[cfg(feature = "toml")]
167            Format::Toml => {
168                let content = toml::to_string(value)?;
169                std::fs::write(path, &content)?;
170                Ok(())
171            }
172            _ => {
173                let _ = (path, value); // squelch unused warnings
174                Err(Error::UnsupportedFormat(self))
175            }
176        }
177    }
178
179    /// Guess format and load from a path
180    #[cfg(feature = "serde")]
181    #[inline]
182    pub fn guess_and_read_path<T: DeserializeOwned>(path: &Path) -> Result<T, Error> {
183        let format = Self::guess_from_path(path);
184        format.read_path(path)
185    }
186
187    /// Guess format and write to a path
188    #[cfg(feature = "serde")]
189    #[inline]
190    pub fn guess_and_write_path<T: Serialize>(path: &Path, value: &T) -> Result<(), Error> {
191        let format = Self::guess_from_path(path);
192        format.write_path(path, value)
193    }
194}