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" => {
44 let mut parser = properties_parser::PropertiesParser::new(source.to_string());
45 parser.parse()
46 }
47 "ini" => ini_parser::parse_ini(source),
48 #[cfg(feature = "json")]
49 "json" => json_parser::parse(source),
50 #[cfg(feature = "xml")]
51 "xml" => xml_parser::parse_xml(source),
52 #[cfg(feature = "hcl")]
53 "hcl" => hcl_parser::parse_hcl(source),
54 _ => {
60 #[cfg(not(feature = "json"))]
61 if detected_format == "json" {
62 return Err(Error::feature_not_enabled("json"));
63 }
64
65 #[cfg(not(feature = "xml"))]
66 if detected_format == "xml" {
67 return Err(Error::feature_not_enabled("xml"));
68 }
69
70 #[cfg(not(feature = "hcl"))]
71 if detected_format == "hcl" {
72 return Err(Error::feature_not_enabled("hcl"));
73 }
74
75 #[cfg(feature = "noml")]
77 if detected_format == "noml" {
78 return noml_parser::parse(source);
79 }
80 #[cfg(not(feature = "noml"))]
81 if detected_format == "noml" {
82 return Err(Error::feature_not_enabled("noml"));
83 }
84
85 #[cfg(feature = "toml")]
87 if detected_format == "toml" {
88 return toml_parser::parse(source);
89 }
90 #[cfg(not(feature = "toml"))]
91 if detected_format == "toml" {
92 return Err(Error::feature_not_enabled("toml"));
93 }
94
95 conf::parse(source)
97 }
98 }
99}
100
101pub fn parse_file<P: AsRef<Path>>(path: P) -> Result<Value> {
103 let path = path.as_ref();
104 let content =
105 std::fs::read_to_string(path).map_err(|e| Error::io(path.display().to_string(), e))?;
106
107 let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
108
109 parse_string(&content, format)
110}
111
112#[cfg(feature = "async")]
114pub async fn parse_file_async<P: AsRef<Path>>(path: P) -> Result<Value> {
115 let path = path.as_ref();
116 let content = tokio::fs::read_to_string(path)
117 .await
118 .map_err(|e| Error::io(path.display().to_string(), e))?;
119
120 let format = detect_format_from_path(path).or_else(|| Some(detect_format(&content)));
121
122 parse_string(&content, format)
123}
124
125pub fn detect_format_from_path(path: &Path) -> Option<&'static str> {
127 path.extension()
128 .and_then(|ext| ext.to_str())
129 .map(|ext| match ext.to_lowercase().as_str() {
130 "conf" | "config" | "cfg" => "conf",
131 "properties" => "properties",
132 "ini" => "ini",
133 "toml" => "toml",
134 "json" => "json",
135 "noml" => "noml",
136 "xml" => "xml",
137 "hcl" | "tf" => "hcl", _ => "conf", })
140}
141
142pub fn detect_format(content: &str) -> &'static str {
144 let trimmed = content.trim();
145
146 if trimmed.starts_with('<') && contains_xml_features(content) {
148 return "xml";
149 }
150
151 if trimmed.starts_with('{') || trimmed.starts_with('[') {
153 return "json";
154 }
155
156 if contains_hcl_features(content) {
158 return "hcl";
159 }
160
161 if contains_noml_features(content) {
163 return "noml";
164 }
165
166 if contains_ini_features(content) {
168 return "ini";
169 }
170
171 if contains_properties_features(content) {
173 return "properties";
174 }
175
176 if contains_toml_features(content) {
178 return "toml";
179 }
180
181 "conf"
183}
184
185fn contains_noml_features(content: &str) -> bool {
187 content.contains("env(")
189 || content.contains("include ")
190 || content.contains("${")
191 || content.contains("@size(")
192 || content.contains("@duration(")
193 || content.contains("@url(")
194 || content.contains("@ip(")
195}
196
197fn contains_properties_features(content: &str) -> bool {
199 content.lines().any(|line| {
200 let trimmed = line.trim();
201 if trimmed.starts_with('!') {
203 return true;
204 }
205 if trimmed.contains("\\n") || trimmed.contains("\\t") || trimmed.contains("\\u") {
207 return true;
208 }
209 if trimmed.contains(':') && !trimmed.contains('=') && !trimmed.starts_with('#') {
211 return true;
212 }
213 false
214 })
215}
216
217fn contains_ini_features(content: &str) -> bool {
219 let mut has_section = false;
220 let mut has_ini_comment = false;
221 let mut has_key_value_in_section = false;
222 let mut in_section = false;
223
224 for line in content.lines() {
225 let trimmed = line.trim();
226
227 if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
229 let section_content = &trimmed[1..trimmed.len() - 1];
231 if !section_content.contains('[') && !section_content.contains(']') {
232 has_section = true;
233 in_section = true;
234 continue;
235 }
236 }
237
238 if trimmed.starts_with(';') {
240 has_ini_comment = true;
241 }
242
243 if in_section
245 && (trimmed.contains('=') || trimmed.contains(':'))
246 && !trimmed.starts_with('#')
247 && !trimmed.starts_with(';')
248 {
249 has_key_value_in_section = true;
250 }
251 }
252
253 has_section && has_key_value_in_section || has_ini_comment
255}
256
257fn contains_toml_features(content: &str) -> bool {
259 content.lines().any(|line| {
261 let trimmed = line.trim();
262 if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
264 return true;
265 }
266 if trimmed.contains("T") && trimmed.contains("Z") {
268 return true;
269 }
270 false
271 })
272}
273
274fn contains_xml_features(content: &str) -> bool {
276 let trimmed = content.trim();
277
278 if trimmed.starts_with("<?xml") {
280 return true;
281 }
282
283 if trimmed.contains("</") {
285 return true;
286 }
287
288 if trimmed.contains("xmlns") {
290 return true;
291 }
292
293 if trimmed.contains("/>") {
295 return true;
296 }
297
298 let open_tags = trimmed.matches('<').count();
300 let close_tags = trimmed.matches('>').count();
301
302 open_tags > 0 && close_tags > 0 && open_tags <= close_tags
304}
305
306fn contains_hcl_features(content: &str) -> bool {
308 for line in content.lines() {
310 let trimmed = line.trim();
311
312 if trimmed.contains(" \"") && trimmed.contains("\" {") {
314 return true;
315 }
316
317 if trimmed.starts_with("variable ") || trimmed.starts_with("output ") {
319 return true;
320 }
321
322 if trimmed.starts_with("resource ") || trimmed.starts_with("data ") {
324 return true;
325 }
326
327 if trimmed.starts_with("provider ") {
329 return true;
330 }
331
332 if trimmed.starts_with("terraform ") {
334 return true;
335 }
336
337 if trimmed.starts_with("module ") {
339 return true;
340 }
341
342 if trimmed.contains("${") && trimmed.contains("}") {
344 return true;
345 }
346 }
347
348 false
349}