config_lib/parsers/
mod.rs1pub mod conf;
7
8pub mod properties_parser;
10
11pub mod ini_parser;
13
14#[cfg(feature = "json")]
15pub mod json_parser;
16
17#[cfg(feature = "xml")]
19pub mod xml_parser;
20
21#[cfg(feature = "hcl")]
23pub mod hcl_parser;
24
25#[cfg(feature = "toml")]
27pub mod toml_parser;
28
29#[cfg(feature = "noml")]
30pub mod noml_parser;
31
32use crate::error::{Error, Result};
33use crate::value::Value;
34use std::path::Path;
35
36pub fn parse_string(source: &str, format: Option<&str>) -> Result<Value> {
39 let detected_format = format.unwrap_or_else(|| detect_format(source));
40
41 match detected_format {
42 "conf" => conf::parse(source),
43 "properties" => properties_parser::parse(source),
44 "ini" => ini_parser::parse(source),
45 #[cfg(feature = "json")]
46 "json" => json_parser::parse(source),
47 #[cfg(feature = "xml")]
48 "xml" => xml_parser::parse(source),
49 #[cfg(feature = "hcl")]
50 "hcl" => hcl_parser::parse(source),
51 #[cfg(feature = "noml")]
52 "noml" => noml_parser::parse(source),
53 #[cfg(feature = "toml")]
54 "toml" => toml_parser::parse(source),
55 _ => {
56 #[cfg(not(feature = "json"))]
57 if detected_format == "json" {
58 return Err(Error::feature_not_enabled("json"));
59 }
60
61 #[cfg(not(feature = "xml"))]
62 if detected_format == "xml" {
63 return Err(Error::feature_not_enabled("xml"));
64 }
65
66 #[cfg(not(feature = "hcl"))]
67 if detected_format == "hcl" {
68 return Err(Error::feature_not_enabled("hcl"));
69 }
70
71 #[cfg(not(feature = "noml"))]
72 if detected_format == "noml" {
73 return Err(Error::feature_not_enabled("noml"));
74 }
75
76 #[cfg(not(feature = "toml"))]
77 if detected_format == "toml" {
78 return Err(Error::feature_not_enabled("toml"));
79 }
80
81 conf::parse(source)
83 }
84 }
85}
86
87pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
89 let path = path.as_ref();
90 let content =
91 std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
92
93 let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
94
95 parse_string(&content, format)
96}
97
98#[cfg(feature = "async")]
100pub async fn parse_file_async<P: AsRef<Path>>(path: P) -> Result<Value> {
101 let path = path.as_ref();
102 let content = tokio::fs::read_to_string(path)
103 .await
104 .map_err(|e| Error::io(path.display().to_string(), e))?;
105
106 let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
107
108 parse_string(&content, format)
109}
110
111pub fn detect_format_from_path(path: &Path) -> Option<&'static str> {
113 path.extension()
114 .and_then(|ext| ext.to_str())
115 .map(|ext| match ext.to_lowercase().as_str() {
116 "conf" | "config" | "cfg" => "conf",
117 "properties" => "properties",
118 "ini" => "ini",
119 "toml" => "toml",
120 "json" => "json",
121 "noml" => "noml",
122 "xml" => "xml",
123 "hcl" | "tf" => "hcl", _ => "conf", })
126}
127
128pub fn detect_format(content: &str) -> &'static str {
130 let trimmed = content.trim();
131
132 if trimmed.starts_with('<') && contains_xml_features(content) {
134 return "xml";
135 }
136
137 if trimmed.starts_with('{') || trimmed.starts_with('[') {
139 return "json";
140 }
141
142 if contains_hcl_features(content) {
144 return "hcl";
145 }
146
147 if contains_noml_features(content) {
149 return "noml";
150 }
151
152 if contains_ini_features(content) {
154 return "ini";
155 }
156
157 if contains_properties_features(content) {
159 return "properties";
160 }
161
162 if contains_toml_features(content) {
164 return "toml";
165 }
166
167 "conf"
169}
170
171fn contains_noml_features(content: &str) -> bool {
173 content.contains("env(")
175 || content.contains("include ")
176 || content.contains("${")
177 || content.contains("@size(")
178 || content.contains("@duration(")
179 || content.contains("@url(")
180 || content.contains("@ip(")
181}
182
183fn contains_properties_features(content: &str) -> bool {
185 content.lines().any(|line| {
186 let trimmed = line.trim();
187 if trimmed.starts_with('!') {
189 return true;
190 }
191 if trimmed.contains("\\n") || trimmed.contains("\\t") || trimmed.contains("\\u") {
193 return true;
194 }
195 if trimmed.contains(':') && !trimmed.contains('=') && !trimmed.starts_with('#') {
197 return true;
198 }
199 false
200 })
201}
202
203fn contains_ini_features(content: &str) -> bool {
205 let mut has_section = false;
206 let mut has_ini_comment = false;
207 let mut has_key_value_in_section = false;
208 let mut in_section = false;
209
210 for line in content.lines() {
211 let trimmed = line.trim();
212
213 if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
215 let section_content = &trimmed[1..trimmed.len() - 1];
217 if !section_content.contains('[') && !section_content.contains(']') {
218 has_section = true;
219 in_section = true;
220 continue;
221 }
222 }
223
224 if trimmed.starts_with(';') {
226 has_ini_comment = true;
227 }
228
229 if in_section
231 && (trimmed.contains('=') || trimmed.contains(':'))
232 && !trimmed.starts_with('#')
233 && !trimmed.starts_with(';')
234 {
235 has_key_value_in_section = true;
236 }
237 }
238
239 has_section && has_key_value_in_section || has_ini_comment
241}
242
243fn contains_toml_features(content: &str) -> bool {
245 content.lines().any(|line| {
247 let trimmed = line.trim();
248 if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
250 return true;
251 }
252 if trimmed.contains("T") && trimmed.contains("Z") {
254 return true;
255 }
256 false
257 })
258}
259
260fn contains_xml_features(content: &str) -> bool {
262 let trimmed = content.trim();
263
264 if trimmed.starts_with("<?xml") {
266 return true;
267 }
268
269 if trimmed.contains("</") {
271 return true;
272 }
273
274 if trimmed.contains("xmlns") {
276 return true;
277 }
278
279 if trimmed.contains("/>") {
281 return true;
282 }
283
284 let open_tags = trimmed.matches('<').count();
286 let close_tags = trimmed.matches('>').count();
287
288 open_tags > 0 && close_tags > 0 && open_tags <= close_tags
290}
291
292fn contains_hcl_features(content: &str) -> bool {
294 for line in content.lines() {
296 let trimmed = line.trim();
297
298 if trimmed.contains(" \"") && trimmed.contains("\" {") {
300 return true;
301 }
302
303 if trimmed.starts_with("variable ") || trimmed.starts_with("output ") {
305 return true;
306 }
307
308 if trimmed.starts_with("resource ") || trimmed.starts_with("data ") {
310 return true;
311 }
312
313 if trimmed.starts_with("provider ") {
315 return true;
316 }
317
318 if trimmed.starts_with("terraform ") {
320 return true;
321 }
322
323 if trimmed.starts_with("module ") {
325 return true;
326 }
327
328 if trimmed.contains("${") && trimmed.contains("}") {
330 return true;
331 }
332 }
333
334 false
335}