Skip to main content

a3s_acl/
generator.rs

1// ============================================================================
2// ACL Generator - Generate ACL text from structured data
3// ============================================================================
4
5use crate::ast::{Block, Document, Value};
6use std::collections::HashMap;
7
8/// Configuration for the generator
9#[derive(Debug, Clone)]
10pub struct GeneratorConfig {
11    /// Indent string (default: "  " - two spaces)
12    pub indent: &'static str,
13    /// Enable inline comments
14    pub comments: bool,
15    /// Output labels as attributes (e.g., `name = "label"`) instead of block labels (e.g., `block "label" { }`)
16    /// This generates HCL-compatible output instead of ACL-style labeled blocks
17    pub labels_as_attrs: bool,
18}
19
20impl Default for GeneratorConfig {
21    fn default() -> Self {
22        Self {
23            indent: "  ",
24            comments: false,
25            labels_as_attrs: false,
26        }
27    }
28}
29
30/// ACL Generator
31pub struct Generator {
32    config: GeneratorConfig,
33}
34
35impl Generator {
36    pub fn new() -> Self {
37        Self {
38            config: GeneratorConfig::default(),
39        }
40    }
41
42    pub fn with_config(config: GeneratorConfig) -> Self {
43        Self { config }
44    }
45
46    /// Generate ACL text from a Document
47    pub fn generate(&self, doc: &Document) -> String {
48        let mut output = String::new();
49        self.write_document(doc, &mut output, 0);
50        output
51    }
52
53    fn write_document(&self, doc: &Document, out: &mut String, indent: usize) {
54        for (i, block) in doc.blocks.iter().enumerate() {
55            if i > 0 {
56                out.push('\n');
57            }
58            self.write_block(block, out, indent);
59        }
60    }
61
62    fn write_block(&self, block: &Block, out: &mut String, indent: usize) {
63        self.write_indent(out, indent);
64
65        // Check if this is a single-value block (no nested blocks, only one attribute)
66        if block.labels.is_empty() && block.blocks.is_empty() && block.attributes.len() == 1 {
67            let (_key, value) = block.attributes.iter().next().unwrap();
68            if value.is_string() && !value.as_str().unwrap().contains(' ') {
69                out.push_str(&block.name);
70                out.push_str(" = ");
71                self.write_value(value, out);
72                out.push('\n');
73                return;
74            }
75        }
76
77        // Write block header
78        out.push_str(&block.name);
79
80        // In labels_as_attrs mode, don't output labels in block header
81        // Instead, we'll output them as attributes inside the block
82        if !self.config.labels_as_attrs {
83            for label in &block.labels {
84                out.push(' ');
85                self.write_string(label, out);
86            }
87        }
88
89        if block.blocks.is_empty()
90            && block.attributes.is_empty()
91            && (self.config.labels_as_attrs || block.labels.is_empty())
92        {
93            out.push('\n');
94            return;
95        }
96
97        out.push_str(" {\n");
98
99        // In labels_as_attrs mode, output the first label as a "name" attribute
100        if self.config.labels_as_attrs && !block.labels.is_empty() {
101            self.write_indent(out, indent + 1);
102            out.push_str("name = ");
103            self.write_string(&block.labels[0], out);
104            out.push('\n');
105        }
106
107        // Write attributes
108        let mut attrs: Vec<_> = block.attributes.iter().collect();
109        attrs.sort_by(|a, b| a.0.cmp(b.0));
110
111        for (key, value) in attrs {
112            self.write_indent(out, indent + 1);
113            out.push_str(key);
114            out.push_str(" = ");
115            self.write_value(value, out);
116            out.push('\n');
117        }
118
119        // Write nested blocks
120        for block in &block.blocks {
121            self.write_block(block, out, indent + 1);
122            out.push('\n');
123        }
124
125        self.write_indent(out, indent);
126        out.push('}');
127    }
128
129    fn write_value(&self, value: &Value, out: &mut String) {
130        match value {
131            Value::String(s) => self.write_string(s, out),
132            Value::Number(n) => {
133                if n.fract() == 0.0 {
134                    out.push_str(&format!("{}", *n as i64));
135                } else {
136                    out.push_str(&format!("{}", n));
137                }
138            }
139            Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
140            Value::Null => out.push_str("null"),
141            Value::List(items) => {
142                out.push('[');
143                for (i, item) in items.iter().enumerate() {
144                    if i > 0 {
145                        out.push_str(", ");
146                    }
147                    self.write_value(item, out);
148                }
149                out.push(']');
150            }
151            Value::Object(pairs) => {
152                out.push('{');
153                for (i, (key, value)) in pairs.iter().enumerate() {
154                    if i > 0 {
155                        out.push_str(", ");
156                    }
157                    out.push_str(key);
158                    out.push_str(" = ");
159                    self.write_value(value, out);
160                }
161                out.push('}');
162            }
163            Value::Call(name, args) => {
164                out.push_str(name);
165                out.push('(');
166                for (i, arg) in args.iter().enumerate() {
167                    if i > 0 {
168                        out.push_str(", ");
169                    }
170                    self.write_value(arg, out);
171                }
172                out.push(')');
173            }
174        }
175    }
176
177    fn write_string(&self, s: &str, out: &mut String) {
178        // Check if the string needs quotes
179        // In HCL, only the special keywords true, false, null can appear unquoted AS LITERALS,
180        // but when we have a String value (Value::String), we must quote it to preserve the string type.
181        // So we quote everything except empty strings.
182        let needs_quotes = !s.is_empty();
183
184        if needs_quotes {
185            // Use double quotes with escaping
186            out.push('"');
187            for c in s.chars() {
188                match c {
189                    '"' => out.push_str("\\\""),
190                    '\\' => out.push_str("\\\\"),
191                    '\n' => out.push_str("\\n"),
192                    '\r' => out.push_str("\\r"),
193                    '\t' => out.push_str("\\t"),
194                    _ => out.push(c),
195                }
196            }
197            out.push('"');
198        } else {
199            out.push_str(s);
200        }
201    }
202
203    fn write_indent(&self, out: &mut String, indent: usize) {
204        for _ in 0..indent {
205            out.push_str(self.config.indent);
206        }
207    }
208
209    /// Generate ACL from a simple key-value structure
210    pub fn generate_from_map(&self, data: &HashMap<String, Value>) -> String {
211        let mut doc = Document::default();
212
213        // Convert each top-level key to a block
214        for (key, value) in data {
215            let mut block = Block {
216                name: key.clone(),
217                labels: Vec::new(),
218                blocks: Vec::new(),
219                attributes: HashMap::new(),
220            };
221
222            if let Value::Object(pairs) = value {
223                for (k, v) in pairs {
224                    block.attributes.insert(k.clone(), v.clone());
225                }
226            } else {
227                block.attributes.insert("_value".to_string(), value.clone());
228            }
229
230            doc.blocks.push(block);
231        }
232
233        self.generate(&doc)
234    }
235}
236
237impl Default for Generator {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Generate ACL from a Document
244pub fn generate(doc: &Document) -> String {
245    Generator::new().generate(doc)
246}
247
248/// Generate ACL from a HashMap
249pub fn generate_from_map(data: &HashMap<String, Value>) -> String {
250    Generator::new().generate_from_map(data)
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_generate_simple() {
259        let mut attrs = HashMap::new();
260        attrs.insert("name".to_string(), Value::String("test".to_string()));
261
262        let block = Block {
263            name: "name".to_string(),
264            labels: Vec::new(),
265            blocks: Vec::new(),
266            attributes: attrs,
267        };
268
269        let doc = Document {
270            blocks: vec![block],
271        };
272
273        let output = generate(&doc);
274        assert!(output.contains("name = \"test\""));
275    }
276
277    #[test]
278    fn test_string_escaping() {
279        let gen = Generator::new();
280        let mut out = String::new();
281        gen.write_string("hello world", &mut out);
282        assert_eq!(out, "\"hello world\"");
283
284        out.clear();
285        gen.write_string("hello\"world", &mut out);
286        assert_eq!(out, "\"hello\\\"world\"");
287
288        // Test string with tab character (actual tab, not escaped)
289        out.clear();
290        let with_tab = "line\tbreak";
291        gen.write_string(with_tab, &mut out);
292        // Tab needs quotes and escaping
293        assert!(out.contains("line"));
294        assert!(out.contains("break"));
295
296        // Test backslash escape (actual backslash character)
297        out.clear();
298        let with_backslash = "path\\file";
299        gen.write_string(with_backslash, &mut out);
300        assert!(out.contains("path"));
301        assert!(out.contains("file"));
302    }
303
304    #[test]
305    fn test_string_escape_carriage_return() {
306        let gen = Generator::new();
307        let mut out = String::new();
308        // Create a string with actual carriage return character
309        let with_cr = String::from("line") + &"\r" + "break";
310        assert!(with_cr.contains('\r')); // Verify it contains CR
311
312        gen.write_string(&with_cr, &mut out);
313        // The output should have CR escaped
314        assert!(out.contains("\\r"), "Expected \\r escape, got: {:?}", out);
315    }
316
317    #[test]
318    fn test_string_escape_newline() {
319        let gen = Generator::new();
320        let mut out = String::new();
321        let with_nl = String::from("line") + &"\n" + "break";
322        assert!(with_nl.contains('\n'));
323
324        gen.write_string(&with_nl, &mut out);
325        assert!(out.contains("\\n"), "Expected \\n escape, got: {:?}", out);
326    }
327
328    #[test]
329    fn test_string_escape_tab() {
330        let gen = Generator::new();
331        let mut out = String::new();
332        let with_tab = String::from("line") + &"\t" + "break";
333        assert!(with_tab.contains('\t'));
334
335        gen.write_string(&with_tab, &mut out);
336        assert!(out.contains("\\t"), "Expected \\t escape, got: {:?}", out);
337    }
338
339    #[test]
340    fn test_string_escape_backslash() {
341        let gen = Generator::new();
342        let mut out = String::new();
343        // Create a string with actual backslash character
344        let with_backslash = String::from("path") + &"\\" + "file";
345        assert!(with_backslash.contains('\\'));
346
347        gen.write_string(&with_backslash, &mut out);
348        // The output should have backslash escaped
349        assert!(
350            out.contains("\\\\"),
351            "Expected \\\\ backslash escape, got: {:?}",
352            out
353        );
354    }
355
356    #[test]
357    fn test_generate_block_with_object_value() {
358        let mut attrs = HashMap::new();
359        attrs.insert(
360            "obj".to_string(),
361            Value::Object(vec![("key".to_string(), Value::String("val".to_string()))]),
362        );
363
364        let block = Block {
365            name: "config".to_string(),
366            labels: vec![],
367            blocks: vec![],
368            attributes: attrs,
369        };
370
371        let doc = Document {
372            blocks: vec![block],
373        };
374        let output = generate(&doc);
375        assert!(output.contains("obj = {key = \"val\"}"));
376    }
377
378    #[test]
379    fn test_generate_nested_block_attrs_and_blocks() {
380        let inner = Block {
381            name: "inner".to_string(),
382            labels: vec![],
383            blocks: vec![],
384            attributes: vec![("key".to_string(), Value::String("val".to_string()))]
385                .into_iter()
386                .collect(),
387        };
388
389        let block = Block {
390            name: "outer".to_string(),
391            labels: vec![],
392            blocks: vec![inner],
393            attributes: vec![("attr".to_string(), Value::Number(1.0))]
394                .into_iter()
395                .collect(),
396        };
397
398        let doc = Document {
399            blocks: vec![block],
400        };
401        let output = generate(&doc);
402        assert!(output.contains("outer"));
403        assert!(output.contains("inner"));
404        assert!(output.contains("attr"));
405    }
406
407    #[test]
408    fn test_generate_block_with_braces() {
409        let mut attrs = HashMap::new();
410        attrs.insert("name".to_string(), Value::String("test".to_string()));
411        attrs.insert("count".to_string(), Value::Number(42.0));
412
413        let block = Block {
414            name: "config".to_string(),
415            labels: Vec::new(),
416            blocks: Vec::new(),
417            attributes: attrs,
418        };
419
420        let doc = Document {
421            blocks: vec![block],
422        };
423        let output = generate(&doc);
424        assert!(output.contains("config {"));
425        assert!(output.contains("name = \"test\""));
426        assert!(output.contains("count = 42"));
427    }
428
429    #[test]
430    fn test_generate_block_with_labels() {
431        let mut attrs = HashMap::new();
432        attrs.insert("api_key".to_string(), Value::String("sk-xxx".to_string()));
433
434        let block = Block {
435            name: "provider".to_string(),
436            labels: vec!["openai".to_string()],
437            blocks: Vec::new(),
438            attributes: attrs,
439        };
440
441        let doc = Document {
442            blocks: vec![block],
443        };
444        let output = generate(&doc);
445        assert!(output.contains("provider \"openai\""));
446        assert!(output.contains("api_key = \"sk-xxx\""));
447    }
448
449    #[test]
450    fn test_generate_nested_blocks() {
451        let inner_block = Block {
452            name: "inner".to_string(),
453            labels: vec![],
454            blocks: Vec::new(),
455            attributes: vec![("key".to_string(), Value::String("val".to_string()))]
456                .into_iter()
457                .collect(),
458        };
459
460        let block = Block {
461            name: "outer".to_string(),
462            labels: vec![],
463            blocks: vec![inner_block],
464            attributes: HashMap::new(),
465        };
466
467        let doc = Document {
468            blocks: vec![block],
469        };
470        let output = generate(&doc);
471        assert!(output.contains("outer"));
472        assert!(output.contains("inner"));
473    }
474
475    #[test]
476    fn test_generate_boolean_values() {
477        let mut attrs = HashMap::new();
478        attrs.insert("enabled".to_string(), Value::Bool(true));
479        attrs.insert("disabled".to_string(), Value::Bool(false));
480
481        let block = Block {
482            name: "config".to_string(),
483            labels: vec![],
484            blocks: Vec::new(),
485            attributes: attrs,
486        };
487
488        let doc = Document {
489            blocks: vec![block],
490        };
491        let output = generate(&doc);
492        assert!(output.contains("enabled = true"));
493        assert!(output.contains("disabled = false"));
494    }
495
496    #[test]
497    fn test_generate_null_value() {
498        let mut attrs = HashMap::new();
499        attrs.insert("value".to_string(), Value::Null);
500
501        let block = Block {
502            name: "config".to_string(),
503            labels: vec![],
504            blocks: Vec::new(),
505            attributes: attrs,
506        };
507
508        let doc = Document {
509            blocks: vec![block],
510        };
511        let output = generate(&doc);
512        assert!(output.contains("value = null"));
513    }
514
515    #[test]
516    fn test_generate_list_values() {
517        let mut attrs = HashMap::new();
518        attrs.insert(
519            "items".to_string(),
520            Value::List(vec![
521                Value::Number(1.0),
522                Value::Number(2.0),
523                Value::Number(3.0),
524            ]),
525        );
526
527        let block = Block {
528            name: "config".to_string(),
529            labels: vec![],
530            blocks: Vec::new(),
531            attributes: attrs,
532        };
533
534        let doc = Document {
535            blocks: vec![block],
536        };
537        let output = generate(&doc);
538        assert!(output.contains("items = [1, 2, 3]"));
539    }
540
541    #[test]
542    fn test_generate_string_list() {
543        let mut attrs = HashMap::new();
544        attrs.insert(
545            "names".to_string(),
546            Value::List(vec![
547                Value::String("a".to_string()),
548                Value::String("b".to_string()),
549            ]),
550        );
551
552        let block = Block {
553            name: "config".to_string(),
554            labels: vec![],
555            blocks: Vec::new(),
556            attributes: attrs,
557        };
558
559        let doc = Document {
560            blocks: vec![block],
561        };
562        let output = generate(&doc);
563        assert!(output.contains("names"));
564        assert!(output.contains("a"));
565        assert!(output.contains("b"));
566    }
567
568    #[test]
569    fn test_generate_empty_list() {
570        let mut attrs = HashMap::new();
571        attrs.insert("items".to_string(), Value::List(vec![]));
572
573        let block = Block {
574            name: "config".to_string(),
575            labels: vec![],
576            blocks: Vec::new(),
577            attributes: attrs,
578        };
579
580        let doc = Document {
581            blocks: vec![block],
582        };
583        let output = generate(&doc);
584        assert!(output.contains("items = []"));
585    }
586
587    #[test]
588    fn test_generate_object_value() {
589        let mut attrs = HashMap::new();
590        attrs.insert(
591            "obj".to_string(),
592            Value::Object(vec![
593                ("key1".to_string(), Value::String("val1".to_string())),
594                ("key2".to_string(), Value::Number(42.0)),
595            ]),
596        );
597
598        let block = Block {
599            name: "config".to_string(),
600            labels: vec![],
601            blocks: Vec::new(),
602            attributes: attrs,
603        };
604
605        let doc = Document {
606            blocks: vec![block],
607        };
608        let output = generate(&doc);
609        assert!(output.contains("obj"));
610        assert!(output.contains("key1"));
611        assert!(output.contains("key2"));
612    }
613
614    #[test]
615    fn test_generate_multiple_blocks() {
616        let block1 = Block {
617            name: "a".to_string(),
618            labels: vec![],
619            blocks: Vec::new(),
620            attributes: vec![("x".to_string(), Value::Number(1.0))]
621                .into_iter()
622                .collect(),
623        };
624        let block2 = Block {
625            name: "b".to_string(),
626            labels: vec![],
627            blocks: Vec::new(),
628            attributes: vec![("y".to_string(), Value::Number(2.0))]
629                .into_iter()
630                .collect(),
631        };
632
633        let doc = Document {
634            blocks: vec![block1, block2],
635        };
636        let output = generate(&doc);
637        assert!(output.contains("a {"));
638        assert!(output.contains("b {"));
639    }
640
641    #[test]
642    fn test_generate_from_map() {
643        let mut data = HashMap::new();
644        data.insert(
645            "key".to_string(),
646            Value::Object(
647                vec![("nested".to_string(), Value::String("value".to_string()))]
648                    .into_iter()
649                    .collect(),
650            ),
651        );
652
653        let output = generate_from_map(&data);
654        assert!(output.contains("key"));
655        assert!(output.contains("value"));
656    }
657
658    #[test]
659    fn test_generate_from_map_non_object() {
660        let mut data = HashMap::new();
661        // Insert a non-Object value to trigger the else branch
662        data.insert("string_key".to_string(), Value::String("test".to_string()));
663        data.insert("number_key".to_string(), Value::Number(42.0));
664
665        let output = generate_from_map(&data);
666        assert!(output.contains("string_key"));
667        assert!(output.contains("_value"));
668    }
669
670    #[test]
671    fn test_generator_with_config() {
672        let config = GeneratorConfig {
673            indent: "\t",
674            comments: true,
675            labels_as_attrs: false,
676        };
677        let gen = Generator::with_config(config);
678        assert_eq!(gen.config.indent, "\t");
679        assert!(gen.config.comments);
680        assert!(!gen.config.labels_as_attrs);
681    }
682
683    #[test]
684    fn test_generator_default() {
685        let gen = Generator::default();
686        assert_eq!(gen.config.indent, "  ");
687        assert!(!gen.config.comments);
688    }
689
690    #[test]
691    fn test_generate_string_with_special_chars() {
692        let gen = Generator::new();
693        let mut out = String::new();
694        gen.write_string("true", &mut out);
695        assert_eq!(out, "\"true\"");
696
697        out.clear();
698        gen.write_string("false", &mut out);
699        assert_eq!(out, "\"false\"");
700
701        out.clear();
702        gen.write_string("null", &mut out);
703        assert_eq!(out, "\"null\"");
704
705        out.clear();
706        gen.write_string("123", &mut out);
707        assert_eq!(out, "\"123\"");
708
709        out.clear();
710        gen.write_string(".hello", &mut out);
711        assert_eq!(out, "\".hello\"");
712
713        out.clear();
714        gen.write_string("-test", &mut out);
715        assert_eq!(out, "\"-test\"");
716    }
717
718    #[test]
719    fn test_generate_empty_block() {
720        let block = Block {
721            name: "empty".to_string(),
722            labels: vec![],
723            blocks: Vec::new(),
724            attributes: HashMap::new(),
725        };
726
727        let doc = Document {
728            blocks: vec![block],
729        };
730        let output = generate(&doc);
731        assert!(output.contains("empty"));
732    }
733
734    #[test]
735    fn test_generate_block_sorted_attrs() {
736        let mut attrs = HashMap::new();
737        attrs.insert("z".to_string(), Value::Number(1.0));
738        attrs.insert("a".to_string(), Value::Number(2.0));
739        attrs.insert("m".to_string(), Value::Number(3.0));
740
741        let block = Block {
742            name: "config".to_string(),
743            labels: vec![],
744            blocks: Vec::new(),
745            attributes: attrs,
746        };
747
748        let doc = Document {
749            blocks: vec![block],
750        };
751        let output = generate(&doc);
752        // Check that attributes are sorted alphabetically
753        let a_pos = output.find("a = 2").expect("should have a = 2");
754        let m_pos = output.find("m = 3").expect("should have m = 3");
755        let z_pos = output.find("z = 1").expect("should have z = 1");
756        assert!(a_pos < m_pos);
757        assert!(m_pos < z_pos);
758    }
759
760    #[test]
761    fn test_generate_float_numbers() {
762        let mut attrs = HashMap::new();
763        attrs.insert("pi".to_string(), Value::Number(3.14159));
764        attrs.insert("e".to_string(), Value::Number(2.71828));
765
766        let block = Block {
767            name: "config".to_string(),
768            labels: vec![],
769            blocks: Vec::new(),
770            attributes: attrs,
771        };
772
773        let doc = Document {
774            blocks: vec![block],
775        };
776        let output = generate(&doc);
777        assert!(output.contains("pi = 3.14159"));
778        assert!(output.contains("e = 2.71828"));
779    }
780
781    #[test]
782    fn test_generate_integer_as_integer() {
783        let mut attrs = HashMap::new();
784        attrs.insert("count".to_string(), Value::Number(42.0));
785
786        let block = Block {
787            name: "config".to_string(),
788            labels: vec![],
789            blocks: Vec::new(),
790            attributes: attrs,
791        };
792
793        let doc = Document {
794            blocks: vec![block],
795        };
796        let output = generate(&doc);
797        // Should output as integer without decimal
798        assert!(output.contains("count = 42"));
799        assert!(!output.contains("count = 42.0"));
800    }
801
802    #[test]
803    fn test_generate_function_call() {
804        let mut attrs = HashMap::new();
805        attrs.insert(
806            "api_key".to_string(),
807            Value::Call(
808                "env".to_string(),
809                vec![Value::String("API_KEY".to_string())],
810            ),
811        );
812
813        let block = Block {
814            name: "config".to_string(),
815            labels: vec![],
816            blocks: Vec::new(),
817            attributes: attrs,
818        };
819
820        let doc = Document {
821            blocks: vec![block],
822        };
823        let output = generate(&doc);
824        // String arguments must be quoted for HCL compatibility
825        assert!(output.contains("api_key = env(\"API_KEY\")"));
826    }
827
828    #[test]
829    fn test_generate_function_call_multiple_args() {
830        let mut attrs = HashMap::new();
831        attrs.insert(
832            "path".to_string(),
833            Value::Call(
834                "concat".to_string(),
835                vec![
836                    Value::String("hello".to_string()),
837                    Value::String("world".to_string()),
838                ],
839            ),
840        );
841
842        let block = Block {
843            name: "config".to_string(),
844            labels: vec![],
845            blocks: Vec::new(),
846            attributes: attrs,
847        };
848
849        let doc = Document {
850            blocks: vec![block],
851        };
852        let output = generate(&doc);
853        // String arguments must be quoted for HCL compatibility
854        assert!(output.contains("path = concat(\"hello\", \"world\")"));
855    }
856
857    #[test]
858    fn test_generate_function_call_empty_args() {
859        let mut attrs = HashMap::new();
860        attrs.insert("val".to_string(), Value::Call("getenv".to_string(), vec![]));
861
862        let block = Block {
863            name: "config".to_string(),
864            labels: vec![],
865            blocks: Vec::new(),
866            attributes: attrs,
867        };
868
869        let doc = Document {
870            blocks: vec![block],
871        };
872        let output = generate(&doc);
873        assert!(output.contains("val = getenv()"));
874    }
875
876    #[test]
877    fn test_generate_labels_as_attrs() {
878        // Test the labels_as_attrs config option
879        let mut attrs = HashMap::new();
880        attrs.insert("api_key".to_string(), Value::String("sk-xxx".to_string()));
881
882        let inner = Block {
883            name: "models".to_string(),
884            labels: vec!["kimi-k2.5".to_string()],
885            blocks: vec![],
886            attributes: vec![("id".to_string(), Value::String("kimi-k2.5".to_string()))]
887                .into_iter()
888                .collect(),
889        };
890
891        let block = Block {
892            name: "providers".to_string(),
893            labels: vec!["openai".to_string()],
894            blocks: vec![inner],
895            attributes: attrs,
896        };
897
898        let doc = Document {
899            blocks: vec![block],
900        };
901
902        // Without labels_as_attrs (default ACL format)
903        let output = generate(&doc);
904        // HCL format quotes all non-empty string values
905        assert!(
906            output.contains("providers \"openai\""),
907            "Should have quoted label"
908        );
909        assert!(
910            output.contains("models \"kimi-k2.5\""),
911            "Should have quoted label"
912        );
913
914        // With labels_as_attrs (HCL format)
915        let config = GeneratorConfig {
916            labels_as_attrs: true,
917            ..Default::default()
918        };
919        let gen = Generator::with_config(config);
920        let output = gen.generate(&doc);
921        assert!(
922            output.contains("providers {"),
923            "Should have HCL-style block"
924        );
925        assert!(
926            output.contains("name = \"openai\""),
927            "Should output label as name attr"
928        );
929        assert!(
930            !output.contains("providers openai"),
931            "Should NOT have ACL-style label header"
932        );
933    }
934
935    #[test]
936    fn test_generate_labels_as_attrs_nested() {
937        // Test nested blocks with labels_as_attrs
938        let model_block = Block {
939            name: "models".to_string(),
940            labels: vec!["kimi-k2.5".to_string()],
941            blocks: vec![],
942            attributes: vec![
943                ("id".to_string(), Value::String("kimi-k2.5".to_string())),
944                ("name".to_string(), Value::String("Kimixxx".to_string())),
945            ]
946            .into_iter()
947            .collect(),
948        };
949
950        let provider_block = Block {
951            name: "providers".to_string(),
952            labels: vec!["openai".to_string()],
953            blocks: vec![model_block],
954            attributes: vec![
955                ("name".to_string(), Value::String("openai".to_string())),
956                (
957                    "base_url".to_string(),
958                    Value::String("https://api.openai.com/v1".to_string()),
959                ),
960            ]
961            .into_iter()
962            .collect(),
963        };
964
965        let doc = Document {
966            blocks: vec![provider_block],
967        };
968
969        let config = GeneratorConfig {
970            labels_as_attrs: true,
971            ..Default::default()
972        };
973        let gen = Generator::with_config(config);
974        let output = gen.generate(&doc);
975
976        // Should NOT have labels in block header (e.g., "providers openai")
977        assert!(
978            !output.contains("providers openai"),
979            "Should not have ACL-style label in header"
980        );
981        assert!(
982            !output.contains("models kimi-k2.5"),
983            "Should not have ACL-style label in header"
984        );
985
986        // Should have HCL-style blocks
987        assert!(output.contains("providers {"));
988        assert!(output.contains("models {"));
989
990        // Should have name = ... as attribute (quoted for HCL string values)
991        assert!(output.contains("name = \"openai\""));
992        assert!(output.contains("name = \"kimi-k2.5\""));
993    }
994}