helix/dna/out/
hlx_config_format.rs

1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4use serde::{Deserialize, Serialize};
5pub use crate::dna::hel::error::HlxError;
6
7/// Enhanced HLX Config Parser with proper AST-based parsing
8/// Supports full HLX syntax including nested structures, arrays, and complex expressions
9
10/// HLX Config Format (.hlx files) - Text-based configuration
11/// This handles human-readable configuration files with .hlx extension
12
13/// HLX Config structure
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HlxConfig {
16    /// Configuration sections
17    pub sections: HashMap<String, HlxSection>,
18    /// Global metadata
19    pub metadata: HashMap<String, serde_json::Value>,
20}
21
22/// Configuration section
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct HlxSection {
25    /// Section properties
26    pub properties: HashMap<String, serde_json::Value>,
27    /// Section metadata
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub metadata: Option<HashMap<String, serde_json::Value>>,
30}
31
32/// Enhanced Parser Structures for AST-based parsing
33
34/// Source location for error reporting
35#[derive(Debug, Clone, PartialEq)]
36pub struct SourceLocation {
37    pub line: usize,
38    pub column: usize,
39    pub position: usize,
40}
41
42/// Parse error with location information
43#[derive(Debug, Clone)]
44pub struct ConfigParseError {
45    pub message: String,
46    pub location: Option<SourceLocation>,
47    pub context: String,
48}
49
50impl std::fmt::Display for ConfigParseError {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        if let Some(loc) = &self.location {
53            write!(f, "{} at line {}, column {}", self.message, loc.line, loc.column)?;
54            if !self.context.is_empty() {
55                write!(f, " (context: {})", self.context)?;
56            }
57        } else {
58            write!(f, "{}", self.message)?;
59        }
60        Ok(())
61    }
62}
63
64impl std::error::Error for ConfigParseError {}
65
66/// Tokens for config parsing
67#[derive(Debug, Clone, PartialEq)]
68pub enum ConfigToken {
69    Identifier(String),
70    String(String),
71    Number(f64),
72    Bool(bool),
73    LeftBrace,
74    RightBrace,
75    LeftBracket,
76    RightBracket,
77    Equals,
78    Comma,
79    Comment(String),
80    Newline,
81    Eof,
82}
83
84impl ConfigToken {
85    pub fn as_string(&self) -> Option<&str> {
86        match self {
87            ConfigToken::String(s) => Some(s),
88            ConfigToken::Identifier(s) => Some(s),
89            _ => None,
90        }
91    }
92}
93
94/// Token with location information
95#[derive(Debug, Clone)]
96pub struct TokenWithLocation {
97    pub token: ConfigToken,
98    pub location: SourceLocation,
99}
100
101/// AST nodes for config parsing
102#[derive(Debug, Clone)]
103pub enum ConfigValue {
104    String(String),
105    Number(f64),
106    Bool(bool),
107    Array(Vec<ConfigValue>),
108    Object(HashMap<String, ConfigValue>),
109    Null,
110}
111
112impl ConfigValue {
113    pub fn to_json_value(&self) -> serde_json::Value {
114        match self {
115            ConfigValue::String(s) => serde_json::Value::String(s.clone()),
116            ConfigValue::Number(n) => serde_json::Value::Number(serde_json::Number::from_f64(*n).unwrap_or(serde_json::Number::from(0))),
117            ConfigValue::Bool(b) => serde_json::Value::Bool(*b),
118            ConfigValue::Array(arr) => serde_json::Value::Array(arr.iter().map(|v| v.to_json_value()).collect()),
119            ConfigValue::Object(obj) => {
120                let mut map = serde_json::Map::new();
121                for (k, v) in obj {
122                    map.insert(k.clone(), v.to_json_value());
123                }
124                serde_json::Value::Object(map)
125            }
126            ConfigValue::Null => serde_json::Value::Null,
127        }
128    }
129}
130
131/// Configuration block AST
132#[derive(Debug, Clone)]
133pub struct ConfigBlock {
134    pub name: String,
135    pub properties: HashMap<String, ConfigValue>,
136    pub location: SourceLocation,
137}
138
139/// Enhanced config parser with lexer and AST
140pub struct EnhancedConfigParser {
141    tokens: Vec<TokenWithLocation>,
142    current: usize,
143}
144
145impl EnhancedConfigParser {
146    /// Create a new parser from source text
147    pub fn new(source: &str) -> Result<Self, ConfigParseError> {
148        let tokens = Self::tokenize(source)?;
149        Ok(Self { tokens, current: 0 })
150    }
151
152    /// Tokenize the source text
153    fn tokenize(source: &str) -> Result<Vec<TokenWithLocation>, ConfigParseError> {
154        let mut tokens = Vec::new();
155        let mut chars = source.chars().peekable();
156        let mut line = 1;
157        let mut column = 1;
158        let mut position = 0;
159
160        while let Some(&ch) = chars.peek() {
161            let start_location = SourceLocation { line, column, position };
162
163            match ch {
164                '#' => {
165                    // Comment - consume until end of line
166                    let mut comment = String::new();
167                    chars.next(); // consume '#'
168                    column += 1;
169                    position += 1;
170                    while let Some(&c) = chars.peek() {
171                        if c == '\n' {
172                            break;
173                        }
174                        comment.push(c);
175                        chars.next();
176                        column += 1;
177                        position += 1;
178                    }
179                    tokens.push(TokenWithLocation {
180                        token: ConfigToken::Comment(comment),
181                        location: start_location,
182                    });
183                }
184                '"' => {
185                    // String literal
186                    chars.next(); // consume opening quote
187                    column += 1;
188                    position += 1;
189                    let mut string = String::new();
190                    let mut escaped = false;
191                    while let Some(c) = chars.next() {
192                        column += 1;
193                        position += 1;
194                        if escaped {
195                            match c {
196                                'n' => string.push('\n'),
197                                't' => string.push('\t'),
198                                'r' => string.push('\r'),
199                                '"' => string.push('"'),
200                                '\\' => string.push('\\'),
201                                _ => string.push(c),
202                            }
203                            escaped = false;
204                        } else if c == '\\' {
205                            escaped = true;
206                        } else if c == '"' {
207                            break;
208                        } else {
209                            string.push(c);
210                        }
211                    }
212                    tokens.push(TokenWithLocation {
213                        token: ConfigToken::String(string),
214                        location: start_location,
215                    });
216                }
217                '0'..='9' | '-' => {
218                    // Number
219                    let mut num_str = String::new();
220                    let mut has_dot = false;
221                    while let Some(&c) = chars.peek() {
222                        if c.is_ascii_digit() || c == '.' || c == '-' {
223                            if c == '.' {
224                                if has_dot {
225                                    break;
226                                }
227                                has_dot = true;
228                            }
229                            num_str.push(c);
230                            chars.next();
231                            column += 1;
232                            position += 1;
233                        } else {
234                            break;
235                        }
236                    }
237                    match num_str.parse::<f64>() {
238                        Ok(num) => tokens.push(TokenWithLocation {
239                            token: ConfigToken::Number(num),
240                            location: start_location,
241                        }),
242                        Err(_) => {
243                            return Err(ConfigParseError {
244                                message: format!("Invalid number: {}", num_str),
245                                location: Some(start_location),
246                                context: "expected valid numeric value".to_string(),
247                            });
248                        }
249                    }
250                }
251                'a'..='z' | 'A'..='Z' | '_' => {
252                    // Identifier or keyword
253                    let mut ident = String::new();
254                    while let Some(&c) = chars.peek() {
255                        if c.is_alphanumeric() || c == '_' {
256                            ident.push(c);
257                            chars.next();
258                            column += 1;
259                            position += 1;
260                        } else {
261                            break;
262                        }
263                    }
264                    let token = match ident.as_str() {
265                        "true" => ConfigToken::Bool(true),
266                        "false" => ConfigToken::Bool(false),
267                        "null" => ConfigToken::Identifier("null".to_string()),
268                        _ => ConfigToken::Identifier(ident),
269                    };
270                    tokens.push(TokenWithLocation {
271                        token,
272                        location: start_location,
273                    });
274                }
275                '=' => {
276                    tokens.push(TokenWithLocation {
277                        token: ConfigToken::Equals,
278                        location: start_location,
279                    });
280                    chars.next();
281                    column += 1;
282                    position += 1;
283                }
284                '{' => {
285                    tokens.push(TokenWithLocation {
286                        token: ConfigToken::LeftBrace,
287                        location: start_location,
288                    });
289                    chars.next();
290                    column += 1;
291                    position += 1;
292                }
293                '}' => {
294                    tokens.push(TokenWithLocation {
295                        token: ConfigToken::RightBrace,
296                        location: start_location,
297                    });
298                    chars.next();
299                    column += 1;
300                    position += 1;
301                }
302                '[' => {
303                    tokens.push(TokenWithLocation {
304                        token: ConfigToken::LeftBracket,
305                        location: start_location,
306                    });
307                    chars.next();
308                    column += 1;
309                    position += 1;
310                }
311                ']' => {
312                    tokens.push(TokenWithLocation {
313                        token: ConfigToken::RightBracket,
314                        location: start_location,
315                    });
316                    chars.next();
317                    column += 1;
318                    position += 1;
319                }
320                ',' => {
321                    tokens.push(TokenWithLocation {
322                        token: ConfigToken::Comma,
323                        location: start_location,
324                    });
325                    chars.next();
326                    column += 1;
327                    position += 1;
328                }
329                '\n' => {
330                    tokens.push(TokenWithLocation {
331                        token: ConfigToken::Newline,
332                        location: start_location,
333                    });
334                    chars.next();
335                    line += 1;
336                    column = 1;
337                    position += 1;
338                }
339                ' ' | '\t' | '\r' => {
340                    // Skip whitespace
341                    chars.next();
342                    column += 1;
343                    position += 1;
344                }
345                _ => {
346                    return Err(ConfigParseError {
347                        message: format!("Unexpected character: {}", ch),
348                        location: Some(start_location),
349                        context: "expected valid token".to_string(),
350                    });
351                }
352            }
353        }
354
355        tokens.push(TokenWithLocation {
356            token: ConfigToken::Eof,
357            location: SourceLocation { line, column, position },
358        });
359
360        Ok(tokens)
361    }
362
363    /// Parse the configuration
364    pub fn parse(&mut self) -> Result<HlxConfig, ConfigParseError> {
365        let mut config = HlxConfig {
366            sections: HashMap::new(),
367            metadata: HashMap::new(),
368        };
369
370        while !self.is_at_end() {
371            match self.current_token().token.clone() {
372                ConfigToken::Comment(_) => {
373                    self.advance();
374                }
375                ConfigToken::Newline => {
376                    self.advance();
377                }
378                ConfigToken::Identifier(ref ident) => {
379                    if self.peek_next().map(|t| t.token == ConfigToken::LeftBrace).unwrap_or(false) {
380                        // Block syntax: identifier { ... }
381                        let block = self.parse_block()?;
382                        let section = HlxSection {
383                            properties: block.properties.into_iter()
384                                .map(|(k, v)| (k, v.to_json_value()))
385                                .collect(),
386                            metadata: None,
387                        };
388                        config.sections.insert(block.name, section);
389                    } else if self.peek_next().map(|t| t.token == ConfigToken::Equals).unwrap_or(false) {
390                        // Property syntax: key = value
391                        let key = ident.clone();
392                        self.advance(); // consume identifier
393                        self.advance(); // consume equals
394                        let value = self.parse_value()?;
395                        config.metadata.insert(key, value.to_json_value());
396                    } else {
397                        self.advance();
398                    }
399                }
400                ConfigToken::String(ref s) => {
401                    if self.peek_next().map(|t| t.token == ConfigToken::LeftBrace).unwrap_or(false) {
402                        // Block syntax with quoted name: "name" { ... }
403                        let block = self.parse_block()?;
404                        let section = HlxSection {
405                            properties: block.properties.into_iter()
406                                .map(|(k, v)| (k, v.to_json_value()))
407                                .collect(),
408                            metadata: None,
409                        };
410                        config.sections.insert(block.name, section);
411                    } else if self.peek_next().map(|t| t.token == ConfigToken::Equals).unwrap_or(false) {
412                        // Property syntax with quoted key: "key" = value
413                        let key = s.clone();
414                        self.advance(); // consume string
415                        self.advance(); // consume equals
416                        let value = self.parse_value()?;
417                        config.metadata.insert(key, value.to_json_value());
418                    } else {
419                        self.advance();
420                    }
421                }
422                _ => {
423                    self.advance();
424                }
425            }
426        }
427
428        Ok(config)
429    }
430
431    /// Parse a block: identifier { properties }
432    fn parse_block(&mut self) -> Result<ConfigBlock, ConfigParseError> {
433        let start_location = self.current_token().location.clone();
434        let name = match &self.current_token().token {
435            ConfigToken::Identifier(s) => s.clone(),
436            ConfigToken::String(s) => s.clone(),
437            _ => {
438                return Err(ConfigParseError {
439                    message: "Expected identifier or string for block name".to_string(),
440                    location: Some(self.current_token().location.clone()),
441                    context: "block syntax: name { ... }".to_string(),
442                });
443            }
444        };
445
446        self.advance(); // consume name
447        self.expect_token(ConfigToken::LeftBrace)?;
448
449        let mut properties = HashMap::new();
450
451        while !self.is_at_end() && self.current_token().token != ConfigToken::RightBrace {
452            match &self.current_token().token {
453                ConfigToken::Comment(_) | ConfigToken::Newline => {
454                    self.advance();
455                    continue;
456                }
457                ConfigToken::Identifier(key) | ConfigToken::String(key) => {
458                    let prop_key = key.clone();
459                    self.advance(); // consume key
460
461                    if self.current_token().token == ConfigToken::LeftBrace {
462                        // Nested block
463                        let nested_block = self.parse_block()?;
464                        let mut nested_props = HashMap::new();
465                        for (k, v) in nested_block.properties {
466                            nested_props.insert(k, v);
467                        }
468                        properties.insert(prop_key, ConfigValue::Object(nested_props));
469                    } else {
470                        self.expect_token(ConfigToken::Equals)?;
471                        let value = self.parse_value()?;
472                        properties.insert(prop_key, value);
473                    }
474                }
475                _ => {
476                    return Err(ConfigParseError {
477                        message: "Expected property key or closing brace".to_string(),
478                        location: Some(self.current_token().location.clone()),
479                        context: "property syntax: key = value".to_string(),
480                    });
481                }
482            }
483        }
484
485        self.expect_token(ConfigToken::RightBrace)?;
486
487        Ok(ConfigBlock {
488            name,
489            properties,
490            location: start_location,
491        })
492    }
493
494    /// Parse a value (string, number, bool, array, object)
495    fn parse_value(&mut self) -> Result<ConfigValue, ConfigParseError> {
496        match &self.current_token().token {
497            ConfigToken::String(s) => {
498                let value = ConfigValue::String(s.clone());
499                self.advance();
500                Ok(value)
501            }
502            ConfigToken::Number(n) => {
503                let value = ConfigValue::Number(*n);
504                self.advance();
505                Ok(value)
506            }
507            ConfigToken::Bool(b) => {
508                let value = ConfigValue::Bool(*b);
509                self.advance();
510                Ok(value)
511            }
512            ConfigToken::Identifier(s) if s == "null" => {
513                self.advance();
514                Ok(ConfigValue::Null)
515            }
516            ConfigToken::LeftBracket => {
517                self.parse_array()
518            }
519            ConfigToken::LeftBrace => {
520                self.parse_object()
521            }
522            _ => {
523                Err(ConfigParseError {
524                    message: "Expected value (string, number, bool, array, or object)".to_string(),
525                    location: Some(self.current_token().location.clone()),
526                    context: "supported value types: strings, numbers, booleans, arrays [...], objects {...}".to_string(),
527                })
528            }
529        }
530    }
531
532    /// Parse an array: [ value1, value2, ... ]
533    fn parse_array(&mut self) -> Result<ConfigValue, ConfigParseError> {
534        self.expect_token(ConfigToken::LeftBracket)?;
535        let mut values = Vec::new();
536
537        while !self.is_at_end() && self.current_token().token != ConfigToken::RightBracket {
538            if matches!(self.current_token().token, ConfigToken::Comment(_) | ConfigToken::Newline | ConfigToken::Comma) {
539                self.advance();
540                continue;
541            }
542
543            values.push(self.parse_value()?);
544        }
545
546        self.expect_token(ConfigToken::RightBracket)?;
547        Ok(ConfigValue::Array(values))
548    }
549
550    /// Parse an object: { key1 = value1, key2 = value2, ... }
551    fn parse_object(&mut self) -> Result<ConfigValue, ConfigParseError> {
552        self.expect_token(ConfigToken::LeftBrace)?;
553        let mut properties = HashMap::new();
554
555        while !self.is_at_end() && self.current_token().token != ConfigToken::RightBrace {
556            if matches!(self.current_token().token, ConfigToken::Comment(_) | ConfigToken::Newline | ConfigToken::Comma) {
557                self.advance();
558                continue;
559            }
560
561            let key = match &self.current_token().token {
562                ConfigToken::Identifier(s) | ConfigToken::String(s) => s.clone(),
563                _ => {
564                    return Err(ConfigParseError {
565                        message: "Expected property key".to_string(),
566                        location: Some(self.current_token().location.clone()),
567                        context: "object property syntax: key = value".to_string(),
568                    });
569                }
570            };
571
572            self.advance(); // consume key
573            self.expect_token(ConfigToken::Equals)?;
574            let value = self.parse_value()?;
575            properties.insert(key, value);
576        }
577
578        self.expect_token(ConfigToken::RightBrace)?;
579        Ok(ConfigValue::Object(properties))
580    }
581
582    /// Get current token
583    fn current_token(&self) -> &TokenWithLocation {
584        &self.tokens[self.current]
585    }
586
587    /// Check if at end of tokens
588    fn is_at_end(&self) -> bool {
589        self.current >= self.tokens.len() || matches!(self.current_token().token, ConfigToken::Eof)
590    }
591
592    /// Advance to next token
593    fn advance(&mut self) {
594        if !self.is_at_end() {
595            self.current += 1;
596        }
597    }
598
599    /// Peek at next token without advancing
600    fn peek_next(&self) -> Option<&TokenWithLocation> {
601        if self.current + 1 < self.tokens.len() {
602            Some(&self.tokens[self.current + 1])
603        } else {
604            None
605        }
606    }
607
608    /// Expect a specific token, advance if found
609    fn expect_token(&mut self, expected: ConfigToken) -> Result<(), ConfigParseError> {
610        if self.current_token().token == expected {
611            self.advance();
612            Ok(())
613        } else {
614            Err(ConfigParseError {
615                message: format!("Expected {:?}, found {:?}", expected, self.current_token().token),
616                location: Some(self.current_token().location.clone()),
617                context: format!("expected token: {:?}", expected),
618            })
619        }
620    }
621}
622
623/// HLX Config Reader/Writer
624pub struct HlxConfigHandler;
625
626impl HlxConfigHandler {
627    /// Read HLX config from file
628    pub fn read_from_file<P: AsRef<Path>>(path: P) -> Result<HlxConfig, HlxError> {
629        let content = fs::read_to_string(&path)
630            .map_err(|e| HlxError::io_error(
631                format!("Failed to read HLX config file: {}", e),
632                format!("Check if file exists and is readable: {}", path.as_ref().display())
633            ))?;
634
635        Self::parse_content(&content)
636    }
637
638    /// Write HLX config to file
639    pub fn write_to_file<P: AsRef<Path>>(config: &HlxConfig, path: P) -> Result<(), HlxError> {
640        let content = Self::serialize_config(config)?;
641
642        fs::write(&path, content)
643            .map_err(|e| HlxError::io_error(
644                format!("Failed to write HLX config file: {}", e),
645                format!("Check write permissions: {}", path.as_ref().display())
646            ))
647    }
648
649    /// Parse HLX config content with enhanced AST-based parsing
650    pub fn parse_content(content: &str) -> Result<HlxConfig, HlxError> {
651        // Try enhanced parser first (full HLX syntax)
652        match EnhancedConfigParser::new(content) {
653            Ok(mut parser) => {
654                match parser.parse() {
655                    Ok(config) => return Ok(config),
656                    Err(parse_err) => {
657                        // Fall back to legacy parser for backward compatibility
658                        eprintln!("Enhanced parser failed: {}, falling back to legacy parser", parse_err);
659                        return Self::parse_content_legacy(content);
660                    }
661                }
662            }
663            Err(lex_err) => {
664                // Fall back to legacy parser for backward compatibility
665                eprintln!("Enhanced parser lexer failed: {}, falling back to legacy parser", lex_err);
666                return Self::parse_content_legacy(content);
667            }
668        }
669    }
670
671    /// Legacy TOML-like parsing for backward compatibility
672    fn parse_content_legacy(content: &str) -> Result<HlxConfig, HlxError> {
673        let mut sections = HashMap::new();
674        let mut metadata = HashMap::new();
675        let mut current_section = None;
676
677        for line in content.lines() {
678            let line = line.trim();
679
680            // Skip comments and empty lines
681            if line.is_empty() || line.starts_with('#') {
682                continue;
683            }
684
685            if line.starts_with('[') && line.ends_with(']') {
686                // Section header
687                let section_name = &line[1..line.len() - 1];
688                current_section = Some(section_name.to_string());
689                sections.insert(section_name.to_string(), HlxSection {
690                    properties: HashMap::new(),
691                    metadata: None,
692                });
693            } else if let Some(section_name) = &current_section {
694                // Property in section
695                if let Some((key, value)) = Self::parse_property(line) {
696                    if let Some(section) = sections.get_mut(section_name) {
697                        section.properties.insert(key, value);
698                    }
699                }
700            } else {
701                // Global property
702                if let Some((key, value)) = Self::parse_property(line) {
703                    metadata.insert(key, value);
704                }
705            }
706        }
707
708        Ok(HlxConfig { sections, metadata })
709    }
710
711    /// Serialize config to string
712    pub fn serialize_config(config: &HlxConfig) -> Result<String, HlxError> {
713        let mut output = String::new();
714
715        // Write metadata
716        for (key, value) in &config.metadata {
717            output.push_str(&format!("{} = {}\n", key, Self::serialize_value(value)));
718        }
719
720        // Write sections
721        for (section_name, section) in &config.sections {
722            output.push_str(&format!("\n[{}]\n", section_name));
723
724            for (key, value) in &section.properties {
725                output.push_str(&format!("{} = {}\n", key, Self::serialize_value(value)));
726            }
727        }
728
729        Ok(output)
730    }
731
732    /// Parse a property line (key = value)
733    fn parse_property(line: &str) -> Option<(String, serde_json::Value)> {
734        let parts: Vec<&str> = line.splitn(2, '=').map(|s| s.trim()).collect();
735        if parts.len() == 2 {
736            let key = parts[0].to_string();
737            let value_str = parts[1];
738
739            // Try to parse as JSON, fallback to string
740            if let Ok(value) = serde_json::from_str(value_str) {
741                Some((key, value))
742            } else {
743                Some((key, serde_json::Value::String(value_str.to_string())))
744            }
745        } else {
746            None
747        }
748    }
749
750    /// Serialize a value to string
751    fn serialize_value(value: &serde_json::Value) -> String {
752        match value {
753            serde_json::Value::String(s) => format!("\"{}\"", s),
754            serde_json::Value::Number(n) => n.to_string(),
755            serde_json::Value::Bool(b) => b.to_string(),
756            _ => value.to_string(),
757        }
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn test_parse_simple_config() {
767        let content = r#"
768# Global metadata
769version = "1.0"
770name = "test"
771
772[database]
773host = "localhost"
774port = 5432
775
776[logging]
777level = "info"
778file = "/var/log/app.log"
779"#;
780
781        let config = HlxConfigHandler::parse_content(content).unwrap();
782
783        assert_eq!(config.metadata.get("version").unwrap().as_str().unwrap(), "1.0");
784        assert_eq!(config.metadata.get("name").unwrap().as_str().unwrap(), "test");
785
786        assert!(config.sections.contains_key("database"));
787        assert!(config.sections.contains_key("logging"));
788
789        let db_section = &config.sections["database"];
790        assert_eq!(db_section.properties.get("host").unwrap().as_str().unwrap(), "localhost");
791        assert_eq!(db_section.properties.get("port").unwrap().as_i64().unwrap(), 5432);
792    }
793
794    #[test]
795    fn test_parse_enhanced_hlx_syntax() {
796        let content = r#"
797# Enhanced HLX syntax test
798version = "2.0"
799enabled = true
800
801project "test-project" {
802    name = "Test Project"
803    version = "1.0.0"
804    description = "A test project"
805
806    database {
807        host = "localhost"
808        port = 5432
809        credentials = {
810            username = "admin"
811            password = "secret"
812        }
813        features = ["ssl", "pooling", "metrics"]
814    }
815
816    agents = [
817        "agent1",
818        "agent2",
819        "agent3"
820    ]
821
822    settings {
823        debug = true
824        timeout = 30.5
825        retries = 3
826    }
827}
828
829logging {
830    level = "info"
831    file = "/var/log/app.log"
832    format = "json"
833}
834"#;
835
836        let config = HlxConfigHandler::parse_content(content).unwrap();
837
838        // Test global metadata
839        assert_eq!(config.metadata.get("version").unwrap().as_str().unwrap(), "2.0");
840        assert_eq!(config.metadata.get("enabled").unwrap().as_bool().unwrap(), true);
841
842        // Test project section
843        assert!(config.sections.contains_key("test-project"));
844        let project = &config.sections["test-project"];
845        assert_eq!(project.properties.get("name").unwrap().as_str().unwrap(), "Test Project");
846        assert_eq!(project.properties.get("version").unwrap().as_str().unwrap(), "1.0.0");
847
848        // Test nested database object
849        let db_obj = project.properties.get("database").unwrap().as_object().unwrap();
850        assert_eq!(db_obj.get("host").unwrap().as_str().unwrap(), "localhost");
851        assert_eq!(db_obj.get("port").unwrap().as_i64().unwrap(), 5432);
852
853        // Test nested credentials object
854        let creds = db_obj.get("credentials").unwrap().as_object().unwrap();
855        assert_eq!(creds.get("username").unwrap().as_str().unwrap(), "admin");
856        assert_eq!(creds.get("password").unwrap().as_str().unwrap(), "secret");
857
858        // Test array
859        let features = db_obj.get("features").unwrap().as_array().unwrap();
860        assert_eq!(features.len(), 3);
861        assert_eq!(features[0].as_str().unwrap(), "ssl");
862        assert_eq!(features[1].as_str().unwrap(), "pooling");
863        assert_eq!(features[2].as_str().unwrap(), "metrics");
864
865        // Test agents array
866        let agents = project.properties.get("agents").unwrap().as_array().unwrap();
867        assert_eq!(agents.len(), 3);
868        assert_eq!(agents[0].as_str().unwrap(), "agent1");
869        assert_eq!(agents[1].as_str().unwrap(), "agent2");
870        assert_eq!(agents[2].as_str().unwrap(), "agent3");
871
872        // Test settings object
873        let settings = project.properties.get("settings").unwrap().as_object().unwrap();
874        assert_eq!(settings.get("debug").unwrap().as_bool().unwrap(), true);
875        assert_eq!(settings.get("timeout").unwrap().as_f64().unwrap(), 30.5);
876        assert_eq!(settings.get("retries").unwrap().as_i64().unwrap(), 3);
877
878        // Test logging section
879        assert!(config.sections.contains_key("logging"));
880        let logging = &config.sections["logging"];
881        assert_eq!(logging.properties.get("level").unwrap().as_str().unwrap(), "info");
882        assert_eq!(logging.properties.get("file").unwrap().as_str().unwrap(), "/var/log/app.log");
883        assert_eq!(logging.properties.get("format").unwrap().as_str().unwrap(), "json");
884    }
885
886    #[test]
887    fn test_enhanced_parser_error_reporting() {
888        // Test invalid syntax
889        let content = r#"
890project "test" {
891    name = "test"
892    invalid_syntax = [
893"#;
894
895        let result = EnhancedConfigParser::new(content);
896        assert!(result.is_err());
897        if let Err(err) = result {
898            assert!(err.message.contains("Unexpected character") || err.message.contains("Expected"));
899        }
900    }
901
902    #[test]
903    fn test_backward_compatibility() {
904        // Test that legacy TOML-like syntax still works
905        let content = r#"
906# Legacy format
907version = "1.0"
908name = "legacy"
909
910[database]
911host = "localhost"
912port = 5432
913
914[logging]
915level = "info"
916"#;
917
918        let config = HlxConfigHandler::parse_content(content).unwrap();
919
920        // Should parse correctly with legacy parser fallback
921        assert_eq!(config.metadata.get("version").unwrap().as_str().unwrap(), "1.0");
922        assert_eq!(config.metadata.get("name").unwrap().as_str().unwrap(), "legacy");
923        assert!(config.sections.contains_key("database"));
924        assert!(config.sections.contains_key("logging"));
925    }
926
927    #[test]
928    fn test_serialize_config() {
929        let mut config = HlxConfig::default();
930        config.metadata.insert("version".to_string(), serde_json::Value::String("1.0".to_string()));
931
932        let mut db_section = HlxSection::default();
933        db_section.properties.insert("host".to_string(), serde_json::Value::String("localhost".to_string()));
934
935        config.sections.insert("database".to_string(), db_section);
936
937        let output = HlxConfigHandler::serialize_config(&config).unwrap();
938        assert!(output.contains("version = \"1.0\""));
939        assert!(output.contains("[database]"));
940        assert!(output.contains("host = \"localhost\""));
941    }
942
943    #[test]
944    fn test_complex_hlx_config() {
945        let content = r#"
946# Complex HLX configuration
947app_name = "ComplexApp"
948version = "3.0.0"
949
950server "web-server" {
951    host = "0.0.0.0"
952    port = 8080
953    ssl = true
954
955    endpoints = [
956        "/api/v1",
957        "/api/v2",
958        "/health"
959    ]
960
961    middleware {
962        cors = {
963            enabled = true
964            origins = ["*"]
965            methods = ["GET", "POST", "PUT", "DELETE"]
966        }
967
968        rate_limit = {
969            requests_per_minute = 1000
970            burst_limit = 100
971        }
972    }
973
974    database {
975        primary = {
976            host = "db1.example.com"
977            port = 5432
978            name = "app_db"
979        }
980
981        replicas = [
982            {
983                host = "db2.example.com"
984                port = 5432
985            },
986            {
987                host = "db3.example.com"
988                port = 5432
989            }
990        ]
991    }
992}
993
994workers = [
995    {
996        name = "queue-worker"
997        type = "async"
998        concurrency = 10
999    },
1000    {
1001        name = "cache-worker"
1002        type = "sync"
1003        concurrency = 5
1004    }
1005]
1006"#;
1007
1008        let config = HlxConfigHandler::parse_content(content).unwrap();
1009
1010        // Test complex nested structures
1011        assert!(config.sections.contains_key("web-server"));
1012        let server = &config.sections["web-server"];
1013
1014        // Test nested middleware.cors
1015        let middleware = server.properties.get("middleware").unwrap().as_object().unwrap();
1016        let cors = middleware.get("cors").unwrap().as_object().unwrap();
1017        assert_eq!(cors.get("enabled").unwrap().as_bool().unwrap(), true);
1018        let origins = cors.get("origins").unwrap().as_array().unwrap();
1019        assert_eq!(origins[0].as_str().unwrap(), "*");
1020
1021        // Test nested database.primary
1022        let db = server.properties.get("database").unwrap().as_object().unwrap();
1023        let primary = db.get("primary").unwrap().as_object().unwrap();
1024        assert_eq!(primary.get("host").unwrap().as_str().unwrap(), "db1.example.com");
1025
1026        // Test array of objects (replicas)
1027        let replicas = db.get("replicas").unwrap().as_array().unwrap();
1028        assert_eq!(replicas.len(), 2);
1029        let replica1 = replicas[0].as_object().unwrap();
1030        assert_eq!(replica1.get("host").unwrap().as_str().unwrap(), "db2.example.com");
1031
1032        // Test global workers array
1033        let workers = config.metadata.get("workers").unwrap().as_array().unwrap();
1034        assert_eq!(workers.len(), 2);
1035        let worker1 = workers[0].as_object().unwrap();
1036        assert_eq!(worker1.get("name").unwrap().as_str().unwrap(), "queue-worker");
1037        assert_eq!(worker1.get("concurrency").unwrap().as_i64().unwrap(), 10);
1038    }
1039}
1040
1041impl Default for HlxConfig {
1042    fn default() -> Self {
1043        Self {
1044            sections: HashMap::new(),
1045            metadata: HashMap::new(),
1046        }
1047    }
1048}
1049
1050impl Default for HlxSection {
1051    fn default() -> Self {
1052        Self {
1053            properties: HashMap::new(),
1054            metadata: None,
1055        }
1056    }
1057}