1use serde_json;
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11use toml;
12
13use super::ConfigError;
14use super::ConfigProvider;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum FileFormat {
19 Json,
21 Toml,
23 Yaml,
25}
26
27impl FileFormat {
28 pub fn from_extension(path: &Path) -> Option<Self> {
30 path.extension().and_then(|ext| {
31 let ext_str = ext.to_string_lossy().to_lowercase();
32 match ext_str.as_str() {
33 "json" => Some(FileFormat::Json),
34 "toml" => Some(FileFormat::Toml),
35 "yaml" | "yml" => Some(FileFormat::Yaml),
36 _ => None,
37 }
38 })
39 }
40}
41
42#[derive(Debug)]
44pub struct FileConfigProvider {
45 #[allow(dead_code)]
46 path: PathBuf,
47 #[allow(dead_code)]
48 format: FileFormat,
49 data: HashMap<String, serde_json::Value>,
50}
51
52impl FileConfigProvider {
53 pub fn new(path: &str) -> Result<Self, ConfigError> {
55 let path_buf = PathBuf::from(path);
56 let format = FileFormat::from_extension(&path_buf)
57 .ok_or_else(|| ConfigError::provider_error("file", "unsupported file format"))?;
58
59 let data = Self::read_file(&path_buf, format)?;
60
61 Ok(Self {
62 path: path_buf,
63 format,
64 data,
65 })
66 }
67
68 fn read_file(
70 path: &Path,
71 format: FileFormat,
72 ) -> Result<HashMap<String, serde_json::Value>, ConfigError> {
73 let content = fs::read_to_string(path).map_err(|e| {
74 ConfigError::provider_error("file", format!("failed to read file: {e}"))
75 })?;
76
77 match format {
78 FileFormat::Json => serde_json::from_str(&content)
79 .map_err(|e| ConfigError::provider_error("file", format!("invalid JSON: {e}"))),
80 FileFormat::Toml => {
81 let toml_value: toml::Value = toml::from_str(&content).map_err(|e| {
82 ConfigError::provider_error("file", format!("invalid TOML: {e}"))
83 })?;
84
85 let json_value = serde_json::to_value(toml_value).map_err(|e| {
87 ConfigError::provider_error("file", format!("failed to convert TOML: {e}"))
88 })?;
89
90 match json_value {
91 serde_json::Value::Object(map) => {
92 Ok(map.into_iter().collect())
94 }
95 _ => Err(ConfigError::provider_error(
96 "file",
97 "root configuration must be an object",
98 )),
99 }
100 }
101 FileFormat::Yaml => {
102 let yaml_value: serde_yaml::Value =
103 serde_yaml::from_str(&content).map_err(|e| {
104 ConfigError::provider_error("file", format!("invalid YAML: {e}"))
105 })?;
106
107 let json_value = serde_json::to_value(yaml_value).map_err(|e| {
108 ConfigError::provider_error("file", format!("failed to convert YAML: {e}"))
109 })?;
110
111 match json_value {
112 serde_json::Value::Object(map) => Ok(map.into_iter().collect()),
113 _ => Err(ConfigError::provider_error(
114 "file",
115 "root configuration must be an object",
116 )),
117 }
118 }
119 }
120 }
121
122 fn get_nested_value(&self, key_path: &str) -> Option<&serde_json::Value> {
124 let parts: Vec<&str> = key_path.split('.').collect();
125
126 let mut current = self.data.get(parts[0])?;
127
128 for part in parts.iter().skip(1) {
129 current = current.get(part)?;
130 }
131
132 Some(current)
133 }
134}
135
136impl ConfigProvider for FileConfigProvider {
137 fn has(&self, key: &str) -> bool {
138 self.get_nested_value(key).is_some()
139 }
140
141 fn provider_name(&self) -> &str {
142 "file"
143 }
144
145 fn get_raw(&self, key: &str) -> Result<Option<serde_json::Value>, ConfigError> {
146 match self.get_nested_value(key) {
147 Some(value) => Ok(Some(value.clone())),
148 None => Ok(None),
149 }
150 }
151}