gonfig/
config.rs

1use crate::{
2    error::{Error, Result},
3    source::{ConfigSource, Source},
4};
5use serde_json::Value;
6use std::any::Any;
7use std::fs;
8use std::path::{Path, PathBuf};
9
10/// Supported configuration file formats.
11///
12/// This enum represents the different file formats that gonfig can parse
13/// for configuration files. Each format has its own parsing logic and
14/// error handling.
15///
16/// # Examples
17///
18/// ```rust
19/// use gonfig::ConfigFormat;
20///
21/// // Automatic detection from file extension
22/// let format = ConfigFormat::from_extension("json").unwrap();
23/// assert!(matches!(format, ConfigFormat::Json));
24///
25/// // Manual format specification
26/// let format = ConfigFormat::Yaml;
27/// ```
28#[derive(Debug, Clone, PartialEq)]
29pub enum ConfigFormat {
30    /// JSON format (.json files)
31    Json,
32    /// YAML format (.yaml, .yml files)  
33    Yaml,
34    /// TOML format (.toml files)
35    Toml,
36}
37
38impl ConfigFormat {
39    /// Detect configuration format from file extension.
40    ///
41    /// Returns the appropriate format for common file extensions:
42    /// - `json` → [`ConfigFormat::Json`]
43    /// - `yaml`, `yml` → [`ConfigFormat::Yaml`]
44    /// - `toml` → [`ConfigFormat::Toml`]
45    ///
46    /// # Examples
47    ///
48    /// ```rust
49    /// use gonfig::ConfigFormat;
50    ///
51    /// assert!(matches!(ConfigFormat::from_extension("json"), Some(ConfigFormat::Json)));
52    /// assert!(matches!(ConfigFormat::from_extension("yml"), Some(ConfigFormat::Yaml)));
53    /// assert!(matches!(ConfigFormat::from_extension("toml"), Some(ConfigFormat::Toml)));
54    /// assert_eq!(ConfigFormat::from_extension("unknown"), None);
55    /// ```
56    pub fn from_extension(ext: &str) -> Option<Self> {
57        match ext.to_lowercase().as_str() {
58            "json" => Some(ConfigFormat::Json),
59            "yaml" | "yml" => Some(ConfigFormat::Yaml),
60            "toml" => Some(ConfigFormat::Toml),
61            _ => None,
62        }
63    }
64
65    /// Parse configuration content according to the format.
66    ///
67    /// Converts the string content into a [`serde_json::Value`] that can be
68    /// merged with other configuration sources. All formats are normalized
69    /// to JSON values internally.
70    ///
71    /// # Arguments
72    ///
73    /// * `content` - The configuration file content as a string
74    ///
75    /// # Returns
76    ///
77    /// A [`serde_json::Value`] representing the parsed configuration.
78    ///
79    /// # Errors
80    ///
81    /// Returns [`Error::Serialization`] if the content cannot be parsed
82    /// according to the format's syntax rules.
83    ///
84    /// # Examples
85    ///
86    /// ```rust
87    /// use gonfig::ConfigFormat;
88    ///
89    /// let format = ConfigFormat::Json;
90    /// let content = r#"{"name": "test", "port": 8080}"#;
91    /// let value = format.parse(content).unwrap();
92    /// ```
93    pub fn parse(&self, content: &str) -> Result<Value> {
94        match self {
95            ConfigFormat::Json => serde_json::from_str(content)
96                .map_err(|e| Error::Serialization(format!("JSON parse error: {}", e))),
97            ConfigFormat::Yaml => serde_yaml::from_str(content)
98                .map_err(|e| Error::Serialization(format!("YAML parse error: {}", e))),
99            ConfigFormat::Toml => {
100                let toml_value: toml::Value = toml::from_str(content)
101                    .map_err(|e| Error::Serialization(format!("TOML parse error: {}", e)))?;
102                serde_json::to_value(toml_value).map_err(|e| {
103                    Error::Serialization(format!("TOML to JSON conversion error: {}", e))
104                })
105            }
106        }
107    }
108}
109
110/// Configuration file source.
111///
112/// The `Config` struct represents a configuration file that can be loaded
113/// and parsed. It supports automatic format detection, optional files,
114/// and various configuration file formats (JSON, YAML, TOML).
115///
116/// # Examples
117///
118/// ```rust,no_run
119/// use gonfig::Config;
120///
121/// // Load a required configuration file
122/// let config = Config::from_file("app.json")?;
123///
124/// // Load an optional configuration file (won't fail if missing)
125/// let config = Config::from_file_optional("optional.yaml")?;
126/// # Ok::<(), gonfig::Error>(())
127/// ```
128#[derive(Debug, Clone)]
129pub struct Config {
130    path: PathBuf,
131    format: ConfigFormat,
132    required: bool,
133    data: Option<Value>,
134}
135
136impl Config {
137    /// Load a required configuration file with automatic format detection.
138    ///
139    /// The file format is detected from the file extension. If the file doesn't
140    /// exist or cannot be parsed, this method returns an error.
141    ///
142    /// # Arguments
143    ///
144    /// * `path` - Path to the configuration file
145    ///
146    /// # Examples
147    ///
148    /// ```rust,no_run
149    /// use gonfig::Config;
150    ///
151    /// let config = Config::from_file("app.json")?;
152    /// let config = Config::from_file("settings.yaml")?;
153    /// let config = Config::from_file("config.toml")?;
154    /// # Ok::<(), gonfig::Error>(())
155    /// ```
156    ///
157    /// # Errors
158    ///
159    /// - [`Error::Config`] if the file extension is not recognized
160    /// - [`Error::Io`] if the file cannot be read
161    /// - [`Error::Serialization`] if the file cannot be parsed
162    pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
163        let path = path.as_ref().to_path_buf();
164        let format = path
165            .extension()
166            .and_then(|ext| ext.to_str())
167            .and_then(ConfigFormat::from_extension)
168            .ok_or_else(|| Error::Config(format!("Unknown config format for file: {:?}", path)))?;
169
170        let mut config = Self {
171            path,
172            format,
173            required: true,
174            data: None,
175        };
176
177        config.load()?;
178        Ok(config)
179    }
180
181    /// Load an optional configuration file with automatic format detection.
182    ///
183    /// Similar to [`from_file`], but won't return an error if the file doesn't exist.
184    /// Parse errors will still cause the method to fail.
185    ///
186    /// # Arguments
187    ///
188    /// * `path` - Path to the optional configuration file
189    ///
190    /// # Examples
191    ///
192    /// ```rust,no_run
193    /// use gonfig::Config;
194    ///
195    /// // Won't fail if user.json doesn't exist
196    /// let config = Config::from_file_optional("user.json")?;
197    /// # Ok::<(), gonfig::Error>(())
198    /// ```
199    ///
200    /// [`from_file`]: Config::from_file
201    pub fn from_file_optional(path: impl AsRef<Path>) -> Result<Self> {
202        let path = path.as_ref().to_path_buf();
203        let format = path
204            .extension()
205            .and_then(|ext| ext.to_str())
206            .and_then(ConfigFormat::from_extension)
207            .ok_or_else(|| Error::Config(format!("Unknown config format for file: {:?}", path)))?;
208
209        let path_display = path.display().to_string();
210        let mut config = Self {
211            path,
212            format,
213            required: false,
214            data: None,
215        };
216
217        // For optional configs, only ignore file-not-found errors
218        match config.load() {
219            Ok(()) => {}
220            Err(Error::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => {
221                // File not found is OK for optional configs
222            }
223            Err(e) => {
224                // Log parse errors but don't fail
225                tracing::warn!(
226                    "Failed to parse optional config file {}: {}",
227                    path_display,
228                    e
229                );
230            }
231        }
232        Ok(config)
233    }
234
235    /// Load a configuration file with explicit format specification.
236    ///
237    /// Use this method when you need to override automatic format detection
238    /// or when working with files that don't have standard extensions.
239    ///
240    /// # Arguments
241    ///
242    /// * `path` - Path to the configuration file
243    /// * `format` - The format to use for parsing
244    ///
245    /// # Examples
246    ///
247    /// ```rust,no_run
248    /// use gonfig::{Config, ConfigFormat};
249    ///
250    /// // Force JSON parsing for a file without extension
251    /// let config = Config::with_format("config", ConfigFormat::Json)?;
252    /// # Ok::<(), gonfig::Error>(())
253    /// ```
254    pub fn with_format(path: impl AsRef<Path>, format: ConfigFormat) -> Result<Self> {
255        let mut config = Self {
256            path: path.as_ref().to_path_buf(),
257            format,
258            required: true,
259            data: None,
260        };
261
262        config.load()?;
263        Ok(config)
264    }
265
266    fn load(&mut self) -> Result<()> {
267        match fs::read_to_string(&self.path) {
268            Ok(content) => {
269                self.data = Some(self.format.parse(&content)?);
270                Ok(())
271            }
272            Err(e) => {
273                if self.required {
274                    Err(Error::Io(e))
275                } else {
276                    self.data = Some(Value::Object(serde_json::Map::new()));
277                    Ok(())
278                }
279            }
280        }
281    }
282
283    /// Reload the configuration from disk.
284    ///
285    /// This method re-reads the configuration file and parses it again.
286    /// Useful for applications that need to respond to configuration changes
287    /// at runtime.
288    ///
289    /// # Examples
290    ///
291    /// ```rust,no_run
292    /// use gonfig::Config;
293    ///
294    /// let mut config = Config::from_file("app.json")?;
295    /// // ... some time later ...
296    /// config.reload()?; // Re-read from disk
297    /// # Ok::<(), gonfig::Error>(())
298    /// ```
299    ///
300    /// # Errors
301    ///
302    /// Returns the same errors as the original loading method if the file
303    /// cannot be read or parsed.
304    pub fn reload(&mut self) -> Result<()> {
305        self.load()
306    }
307}
308
309impl ConfigSource for Config {
310    fn source_type(&self) -> Source {
311        Source::ConfigFile
312    }
313
314    fn collect(&self) -> Result<Value> {
315        Ok(self
316            .data
317            .clone()
318            .unwrap_or_else(|| Value::Object(serde_json::Map::new())))
319    }
320
321    fn has_value(&self, key: &str) -> bool {
322        if let Some(data) = &self.data {
323            let parts: Vec<&str> = key.split('.').collect();
324            let mut current = data;
325
326            for part in parts {
327                match current.get(part) {
328                    Some(value) => current = value,
329                    None => return false,
330                }
331            }
332            true
333        } else {
334            false
335        }
336    }
337
338    fn get_value(&self, key: &str) -> Option<Value> {
339        if let Some(data) = &self.data {
340            let parts: Vec<&str> = key.split('.').collect();
341            let mut current = data;
342
343            for part in parts {
344                match current.get(part) {
345                    Some(value) => current = value,
346                    None => return None,
347                }
348            }
349            Some(current.clone())
350        } else {
351            None
352        }
353    }
354
355    fn as_any(&self) -> &dyn Any {
356        self
357    }
358}