config_lib/parsers/
mod.rs

1//! # Configuration Format Parsers
2//!
3//! Multi-format configuration parsing with automatic format detection
4//! and high-performance implementations.
5
6pub mod conf;
7
8/// Java Properties format parser
9pub mod properties_parser;
10
11/// INI format parser
12pub mod ini_parser;
13
14#[cfg(feature = "json")]
15pub mod json_parser;
16
17/// XML format parser (enterprise feature)
18#[cfg(feature = "xml")]
19pub mod xml_parser;
20
21/// HCL format parser (HashiCorp Configuration Language)
22#[cfg(feature = "hcl")]
23pub mod hcl_parser;
24
25// TOML and NOML parsers now enabled with published crate
26#[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
36/// Parse configuration from a string with optional format hint
37/// Uses zero-copy AST parser for enterprise performance
38pub 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        // NOML and TOML disabled for CI/CD
55        // #[cfg(feature = "noml")]
56        // "noml" => noml_parser::parse(source),
57        // #[cfg(feature = "toml")]
58        // "toml" => toml_parser::parse(source),
59        _ => {
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            // NOML format parsing
76            #[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            // TOML format parsing (via NOML)
86            #[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            // For now, treat everything else as conf format
96            conf::parse(source)
97        }
98    }
99}
100
101/// Parse configuration from a file with automatic format detection
102pub 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/// Async version of parse_file
113#[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
125/// Detect configuration format from file path
126pub 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", // .tf files are Terraform HCL
138            _ => "conf",           // Default to conf for unknown extensions
139        })
140}
141
142/// Detect configuration format from content
143pub fn detect_format(content: &str) -> &'static str {
144    let trimmed = content.trim();
145
146    // XML detection - starts with < and contains XML tags
147    if trimmed.starts_with('<') && contains_xml_features(content) {
148        return "xml";
149    }
150
151    // JSON detection - starts with { or [
152    if trimmed.starts_with('{') || trimmed.starts_with('[') {
153        return "json";
154    }
155
156    // HCL detection - look for HCL-specific features
157    if contains_hcl_features(content) {
158        return "hcl";
159    }
160
161    // NOML detection - look for NOML-specific features
162    if contains_noml_features(content) {
163        return "noml";
164    }
165
166    // INI detection - look for section headers (before properties since INI can use colons)
167    if contains_ini_features(content) {
168        return "ini";
169    }
170
171    // Properties detection - look for properties-specific features
172    if contains_properties_features(content) {
173        return "properties";
174    }
175
176    // TOML detection - look for TOML-specific syntax
177    if contains_toml_features(content) {
178        return "toml";
179    }
180
181    // Default to conf format
182    "conf"
183}
184
185/// Check if content contains NOML-specific features
186fn contains_noml_features(content: &str) -> bool {
187    // Look for NOML-specific syntax
188    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
197/// Check if content contains Properties-specific features
198fn contains_properties_features(content: &str) -> bool {
199    content.lines().any(|line| {
200        let trimmed = line.trim();
201        // Properties comments with ! (specific to Java Properties)
202        if trimmed.starts_with('!') {
203            return true;
204        }
205        // Properties escape sequences (more specific than CONF)
206        if trimmed.contains("\\n") || trimmed.contains("\\t") || trimmed.contains("\\u") {
207            return true;
208        }
209        // Properties use : separator more commonly than CONF
210        if trimmed.contains(':') && !trimmed.contains('=') && !trimmed.starts_with('#') {
211            return true;
212        }
213        false
214    })
215}
216
217/// Check if content contains INI-specific features
218fn 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        // INI section headers [section] but not TOML arrays
228        if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
229            // Make sure it's not a TOML array of tables
230            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        // INI comments with ; (TOML uses #)
239        if trimmed.starts_with(';') {
240            has_ini_comment = true;
241        }
242
243        // Key-value pairs in sections
244        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    // INI is likely if we have sections with key-value pairs OR semicolon comments
254    has_section && has_key_value_in_section || has_ini_comment
255}
256
257/// Check if content contains TOML-specific features
258fn contains_toml_features(content: &str) -> bool {
259    // Look for TOML-specific syntax patterns
260    content.lines().any(|line| {
261        let trimmed = line.trim();
262        // TOML section headers
263        if trimmed.starts_with('[') && trimmed.ends_with(']') && !trimmed.contains('=') {
264            return true;
265        }
266        // TOML datetime format
267        if trimmed.contains("T") && trimmed.contains("Z") {
268            return true;
269        }
270        false
271    })
272}
273
274/// Check if content contains XML-specific features
275fn contains_xml_features(content: &str) -> bool {
276    let trimmed = content.trim();
277
278    // Look for XML declaration
279    if trimmed.starts_with("<?xml") {
280        return true;
281    }
282
283    // Look for closing XML tags
284    if trimmed.contains("</") {
285        return true;
286    }
287
288    // Look for XML namespaces
289    if trimmed.contains("xmlns") {
290        return true;
291    }
292
293    // Look for self-closing tags
294    if trimmed.contains("/>") {
295        return true;
296    }
297
298    // Check for balanced XML structure
299    let open_tags = trimmed.matches('<').count();
300    let close_tags = trimmed.matches('>').count();
301
302    // Basic XML structure validation
303    open_tags > 0 && close_tags > 0 && open_tags <= close_tags
304}
305
306/// Check if content contains HCL-specific features
307fn contains_hcl_features(content: &str) -> bool {
308    // Look for HCL-specific syntax patterns
309    for line in content.lines() {
310        let trimmed = line.trim();
311
312        // HCL block syntax: resource "type" "name" {
313        if trimmed.contains(" \"") && trimmed.contains("\" {") {
314            return true;
315        }
316
317        // HCL variable/output blocks
318        if trimmed.starts_with("variable ") || trimmed.starts_with("output ") {
319            return true;
320        }
321
322        // HCL resource/data blocks
323        if trimmed.starts_with("resource ") || trimmed.starts_with("data ") {
324            return true;
325        }
326
327        // HCL provider blocks
328        if trimmed.starts_with("provider ") {
329            return true;
330        }
331
332        // HCL terraform blocks
333        if trimmed.starts_with("terraform ") {
334            return true;
335        }
336
337        // HCL module blocks
338        if trimmed.starts_with("module ") {
339            return true;
340        }
341
342        // HCL functions and interpolation
343        if trimmed.contains("${") && trimmed.contains("}") {
344            return true;
345        }
346    }
347
348    false
349}