1use crate::ast::{Block, Document, Value};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct GeneratorConfig {
11 pub indent: &'static str,
13 pub comments: bool,
15 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
30pub 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 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 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 out.push_str(&block.name);
79
80 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 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 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 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 let needs_quotes = !s.is_empty();
183
184 if needs_quotes {
185 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 pub fn generate_from_map(&self, data: &HashMap<String, Value>) -> String {
211 let mut doc = Document::default();
212
213 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
243pub fn generate(doc: &Document) -> String {
245 Generator::new().generate(doc)
246}
247
248pub 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 out.clear();
290 let with_tab = "line\tbreak";
291 gen.write_string(with_tab, &mut out);
292 assert!(out.contains("line"));
294 assert!(out.contains("break"));
295
296 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 let with_cr = String::from("line") + &"\r" + "break";
310 assert!(with_cr.contains('\r')); gen.write_string(&with_cr, &mut out);
313 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 let with_backslash = String::from("path") + &"\\" + "file";
345 assert!(with_backslash.contains('\\'));
346
347 gen.write_string(&with_backslash, &mut out);
348 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 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 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 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 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 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 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 let output = generate(&doc);
904 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 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 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 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 assert!(output.contains("providers {"));
988 assert!(output.contains("models {"));
989
990 assert!(output.contains("name = \"openai\""));
992 assert!(output.contains("name = \"kimi-k2.5\""));
993 }
994}