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::{BTreeMap, 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}
16
17impl Default for GeneratorConfig {
18    fn default() -> Self {
19        Self {
20            indent: "  ",
21            comments: false,
22        }
23    }
24}
25
26/// ACL Generator
27pub struct Generator {
28    config: GeneratorConfig,
29}
30
31impl Generator {
32    pub fn new() -> Self {
33        Self {
34            config: GeneratorConfig::default(),
35        }
36    }
37
38    pub fn with_config(config: GeneratorConfig) -> Self {
39        Self { config }
40    }
41
42    /// Generate ACL text from a Document
43    pub fn generate(&self, doc: &Document) -> String {
44        let mut output = String::new();
45        self.write_document(doc, &mut output, 0);
46        output
47    }
48
49    fn write_document(&self, doc: &Document, out: &mut String, indent: usize) {
50        for (i, block) in doc.blocks.iter().enumerate() {
51            if i > 0 && !out.ends_with('\n') {
52                out.push('\n');
53            }
54            self.write_block(block, out, indent);
55        }
56    }
57
58    fn write_block(&self, block: &Block, out: &mut String, indent: usize) {
59        self.write_indent(out, indent);
60
61        // The parser represents a document-level attribute as a block whose sole attribute has the
62        // same name. Preserve that assignment shape for every value kind. A real block may also
63        // contain one attribute, but its key differs from the block name and must retain braces.
64        if block.labels.is_empty() && block.blocks.is_empty() && block.attributes.len() == 1 {
65            if let Some((key, value)) = block.attributes.iter().next() {
66                if key != &block.name {
67                    self.write_regular_block(block, out, indent);
68                    return;
69                }
70                out.push_str(&block.name);
71                out.push_str(" = ");
72                self.write_value(value, out);
73                out.push('\n');
74                return;
75            }
76        }
77
78        self.write_regular_block(block, out, indent);
79    }
80
81    fn write_regular_block(&self, block: &Block, out: &mut String, indent: usize) {
82        // Write block header
83        out.push_str(&block.name);
84
85        for label in &block.labels {
86            out.push(' ');
87            self.write_string(label, out);
88        }
89
90        if block.blocks.is_empty() && block.attributes.is_empty() {
91            out.push_str(" { }");
92            return;
93        }
94
95        out.push_str(" {\n");
96
97        // Write attributes
98        let mut attrs: Vec<_> = block.attributes.iter().collect();
99        attrs.sort_by(|a, b| a.0.cmp(b.0));
100
101        for (key, value) in attrs {
102            self.write_indent(out, indent + 1);
103            out.push_str(key);
104            out.push_str(" = ");
105            self.write_value(value, out);
106            out.push('\n');
107        }
108
109        // Write nested blocks
110        for block in &block.blocks {
111            self.write_block(block, out, indent + 1);
112            out.push('\n');
113        }
114
115        self.write_indent(out, indent);
116        out.push('}');
117    }
118
119    fn write_value(&self, value: &Value, out: &mut String) {
120        match value {
121            Value::String(s) => self.write_string(s, out),
122            Value::Number(n) => {
123                let mut buffer = ryu_js::Buffer::new();
124                out.push_str(buffer.format(*n));
125            }
126            Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
127            Value::Null => out.push_str("null"),
128            Value::List(items) => {
129                out.push('[');
130                for (i, item) in items.iter().enumerate() {
131                    if i > 0 {
132                        out.push_str(", ");
133                    }
134                    self.write_value(item, out);
135                }
136                out.push(']');
137            }
138            Value::Object(pairs) => {
139                let mut canonical_pairs = BTreeMap::new();
140                for (key, value) in pairs {
141                    canonical_pairs.insert(key, value);
142                }
143                out.push('{');
144                for (i, (key, value)) in canonical_pairs.into_iter().enumerate() {
145                    if i > 0 {
146                        out.push_str(", ");
147                    }
148                    out.push_str(key);
149                    out.push_str(" = ");
150                    self.write_value(value, out);
151                }
152                out.push('}');
153            }
154            Value::Call(name, args) => {
155                out.push_str(name);
156                out.push('(');
157                for (i, arg) in args.iter().enumerate() {
158                    if i > 0 {
159                        out.push_str(", ");
160                    }
161                    self.write_value(arg, out);
162                }
163                out.push(')');
164            }
165        }
166    }
167
168    fn write_string(&self, s: &str, out: &mut String) {
169        // Every String AST value must stay quoted. In particular, emitting an empty
170        // or numeric-looking string without quotes changes its type when reparsed.
171        out.push('"');
172        for c in s.chars() {
173            match c {
174                '"' => out.push_str("\\\""),
175                '\\' => out.push_str("\\\\"),
176                '\n' => out.push_str("\\n"),
177                '\r' => out.push_str("\\r"),
178                '\t' => out.push_str("\\t"),
179                _ => out.push(c),
180            }
181        }
182        out.push('"');
183    }
184
185    fn write_indent(&self, out: &mut String, indent: usize) {
186        for _ in 0..indent {
187            out.push_str(self.config.indent);
188        }
189    }
190
191    /// Generate ACL from a simple key-value structure
192    pub fn generate_from_map(&self, data: &HashMap<String, Value>) -> String {
193        let mut doc = Document::default();
194
195        // Convert each top-level key to a block
196        for (key, value) in data {
197            let mut block = Block {
198                name: key.clone(),
199                labels: Vec::new(),
200                blocks: Vec::new(),
201                attributes: HashMap::new(),
202            };
203
204            if let Value::Object(pairs) = value {
205                for (k, v) in pairs {
206                    block.attributes.insert(k.clone(), v.clone());
207                }
208            } else {
209                block.attributes.insert("_value".to_string(), value.clone());
210            }
211
212            doc.blocks.push(block);
213        }
214
215        self.generate(&doc)
216    }
217}
218
219impl Default for Generator {
220    fn default() -> Self {
221        Self::new()
222    }
223}
224
225/// Generate ACL from a Document
226pub fn generate(doc: &Document) -> String {
227    Generator::new().generate(doc)
228}
229
230/// Generate ACL from a HashMap
231pub fn generate_from_map(data: &HashMap<String, Value>) -> String {
232    Generator::new().generate_from_map(data)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[test]
240    fn test_generate_simple() {
241        let mut attrs = HashMap::new();
242        attrs.insert("name".to_string(), Value::String("test".to_string()));
243
244        let block = Block {
245            name: "name".to_string(),
246            labels: Vec::new(),
247            blocks: Vec::new(),
248            attributes: attrs,
249        };
250
251        let doc = Document {
252            blocks: vec![block],
253        };
254
255        let output = generate(&doc);
256        assert!(output.contains("name = \"test\""));
257    }
258
259    #[test]
260    fn test_string_escaping() {
261        let gen = Generator::new();
262        let mut out = String::new();
263        gen.write_string("hello world", &mut out);
264        assert_eq!(out, "\"hello world\"");
265
266        out.clear();
267        gen.write_string("hello\"world", &mut out);
268        assert_eq!(out, "\"hello\\\"world\"");
269
270        // Test string with tab character (actual tab, not escaped)
271        out.clear();
272        let with_tab = "line\tbreak";
273        gen.write_string(with_tab, &mut out);
274        // Tab needs quotes and escaping
275        assert!(out.contains("line"));
276        assert!(out.contains("break"));
277
278        // Test backslash escape (actual backslash character)
279        out.clear();
280        let with_backslash = "path\\file";
281        gen.write_string(with_backslash, &mut out);
282        assert!(out.contains("path"));
283        assert!(out.contains("file"));
284    }
285
286    #[test]
287    fn test_string_escape_carriage_return() {
288        let gen = Generator::new();
289        let mut out = String::new();
290        // Create a string with actual carriage return character
291        let with_cr = String::from("line") + &"\r" + "break";
292        assert!(with_cr.contains('\r')); // Verify it contains CR
293
294        gen.write_string(&with_cr, &mut out);
295        // The output should have CR escaped
296        assert!(out.contains("\\r"), "Expected \\r escape, got: {:?}", out);
297    }
298
299    #[test]
300    fn test_string_escape_newline() {
301        let gen = Generator::new();
302        let mut out = String::new();
303        let with_nl = String::from("line") + &"\n" + "break";
304        assert!(with_nl.contains('\n'));
305
306        gen.write_string(&with_nl, &mut out);
307        assert!(out.contains("\\n"), "Expected \\n escape, got: {:?}", out);
308    }
309
310    #[test]
311    fn test_string_escape_tab() {
312        let gen = Generator::new();
313        let mut out = String::new();
314        let with_tab = String::from("line") + &"\t" + "break";
315        assert!(with_tab.contains('\t'));
316
317        gen.write_string(&with_tab, &mut out);
318        assert!(out.contains("\\t"), "Expected \\t escape, got: {:?}", out);
319    }
320
321    #[test]
322    fn test_string_escape_backslash() {
323        let gen = Generator::new();
324        let mut out = String::new();
325        // Create a string with actual backslash character
326        let with_backslash = String::from("path") + &"\\" + "file";
327        assert!(with_backslash.contains('\\'));
328
329        gen.write_string(&with_backslash, &mut out);
330        // The output should have backslash escaped
331        assert!(
332            out.contains("\\\\"),
333            "Expected \\\\ backslash escape, got: {:?}",
334            out
335        );
336    }
337
338    #[test]
339    fn test_generate_block_with_object_value() {
340        let mut attrs = HashMap::new();
341        attrs.insert(
342            "obj".to_string(),
343            Value::Object(vec![("key".to_string(), Value::String("val".to_string()))]),
344        );
345
346        let block = Block {
347            name: "config".to_string(),
348            labels: vec![],
349            blocks: vec![],
350            attributes: attrs,
351        };
352
353        let doc = Document {
354            blocks: vec![block],
355        };
356        let output = generate(&doc);
357        assert!(output.contains("obj = {key = \"val\"}"));
358    }
359
360    #[test]
361    fn test_generate_nested_block_attrs_and_blocks() {
362        let inner = Block {
363            name: "inner".to_string(),
364            labels: vec![],
365            blocks: vec![],
366            attributes: vec![("key".to_string(), Value::String("val".to_string()))]
367                .into_iter()
368                .collect(),
369        };
370
371        let block = Block {
372            name: "outer".to_string(),
373            labels: vec![],
374            blocks: vec![inner],
375            attributes: vec![("attr".to_string(), Value::Number(1.0))]
376                .into_iter()
377                .collect(),
378        };
379
380        let doc = Document {
381            blocks: vec![block],
382        };
383        let output = generate(&doc);
384        assert!(output.contains("outer"));
385        assert!(output.contains("inner"));
386        assert!(output.contains("attr"));
387    }
388
389    #[test]
390    fn test_generate_block_with_braces() {
391        let mut attrs = HashMap::new();
392        attrs.insert("name".to_string(), Value::String("test".to_string()));
393        attrs.insert("count".to_string(), Value::Number(42.0));
394
395        let block = Block {
396            name: "config".to_string(),
397            labels: Vec::new(),
398            blocks: Vec::new(),
399            attributes: attrs,
400        };
401
402        let doc = Document {
403            blocks: vec![block],
404        };
405        let output = generate(&doc);
406        assert!(output.contains("config {"));
407        assert!(output.contains("name = \"test\""));
408        assert!(output.contains("count = 42"));
409    }
410
411    #[test]
412    fn test_generate_block_with_labels() {
413        let mut attrs = HashMap::new();
414        attrs.insert("api_key".to_string(), Value::String("sk-xxx".to_string()));
415
416        let block = Block {
417            name: "provider".to_string(),
418            labels: vec!["openai".to_string()],
419            blocks: Vec::new(),
420            attributes: attrs,
421        };
422
423        let doc = Document {
424            blocks: vec![block],
425        };
426        let output = generate(&doc);
427        assert!(output.contains("provider \"openai\""));
428        assert!(output.contains("api_key = \"sk-xxx\""));
429    }
430
431    #[test]
432    fn test_generate_nested_blocks() {
433        let inner_block = Block {
434            name: "inner".to_string(),
435            labels: vec![],
436            blocks: Vec::new(),
437            attributes: vec![("key".to_string(), Value::String("val".to_string()))]
438                .into_iter()
439                .collect(),
440        };
441
442        let block = Block {
443            name: "outer".to_string(),
444            labels: vec![],
445            blocks: vec![inner_block],
446            attributes: HashMap::new(),
447        };
448
449        let doc = Document {
450            blocks: vec![block],
451        };
452        let output = generate(&doc);
453        assert!(output.contains("outer"));
454        assert!(output.contains("inner"));
455    }
456
457    #[test]
458    fn test_generate_boolean_values() {
459        let mut attrs = HashMap::new();
460        attrs.insert("enabled".to_string(), Value::Bool(true));
461        attrs.insert("disabled".to_string(), Value::Bool(false));
462
463        let block = Block {
464            name: "config".to_string(),
465            labels: vec![],
466            blocks: Vec::new(),
467            attributes: attrs,
468        };
469
470        let doc = Document {
471            blocks: vec![block],
472        };
473        let output = generate(&doc);
474        assert!(output.contains("enabled = true"));
475        assert!(output.contains("disabled = false"));
476    }
477
478    #[test]
479    fn test_generate_null_value() {
480        let mut attrs = HashMap::new();
481        attrs.insert("value".to_string(), Value::Null);
482
483        let block = Block {
484            name: "config".to_string(),
485            labels: vec![],
486            blocks: Vec::new(),
487            attributes: attrs,
488        };
489
490        let doc = Document {
491            blocks: vec![block],
492        };
493        let output = generate(&doc);
494        assert!(output.contains("value = null"));
495    }
496
497    #[test]
498    fn test_generate_list_values() {
499        let mut attrs = HashMap::new();
500        attrs.insert(
501            "items".to_string(),
502            Value::List(vec![
503                Value::Number(1.0),
504                Value::Number(2.0),
505                Value::Number(3.0),
506            ]),
507        );
508
509        let block = Block {
510            name: "config".to_string(),
511            labels: vec![],
512            blocks: Vec::new(),
513            attributes: attrs,
514        };
515
516        let doc = Document {
517            blocks: vec![block],
518        };
519        let output = generate(&doc);
520        assert!(output.contains("items = [1, 2, 3]"));
521    }
522
523    #[test]
524    fn test_generate_string_list() {
525        let mut attrs = HashMap::new();
526        attrs.insert(
527            "names".to_string(),
528            Value::List(vec![
529                Value::String("a".to_string()),
530                Value::String("b".to_string()),
531            ]),
532        );
533
534        let block = Block {
535            name: "config".to_string(),
536            labels: vec![],
537            blocks: Vec::new(),
538            attributes: attrs,
539        };
540
541        let doc = Document {
542            blocks: vec![block],
543        };
544        let output = generate(&doc);
545        assert!(output.contains("names"));
546        assert!(output.contains("a"));
547        assert!(output.contains("b"));
548    }
549
550    #[test]
551    fn test_generate_empty_list() {
552        let mut attrs = HashMap::new();
553        attrs.insert("items".to_string(), Value::List(vec![]));
554
555        let block = Block {
556            name: "config".to_string(),
557            labels: vec![],
558            blocks: Vec::new(),
559            attributes: attrs,
560        };
561
562        let doc = Document {
563            blocks: vec![block],
564        };
565        let output = generate(&doc);
566        assert!(output.contains("items = []"));
567    }
568
569    #[test]
570    fn test_generate_object_value() {
571        let mut attrs = HashMap::new();
572        attrs.insert(
573            "obj".to_string(),
574            Value::Object(vec![
575                ("key1".to_string(), Value::String("val1".to_string())),
576                ("key2".to_string(), Value::Number(42.0)),
577            ]),
578        );
579
580        let block = Block {
581            name: "config".to_string(),
582            labels: vec![],
583            blocks: Vec::new(),
584            attributes: attrs,
585        };
586
587        let doc = Document {
588            blocks: vec![block],
589        };
590        let output = generate(&doc);
591        assert!(output.contains("obj"));
592        assert!(output.contains("key1"));
593        assert!(output.contains("key2"));
594    }
595
596    #[test]
597    fn test_generate_multiple_blocks() {
598        let block1 = Block {
599            name: "a".to_string(),
600            labels: vec![],
601            blocks: Vec::new(),
602            attributes: vec![("x".to_string(), Value::Number(1.0))]
603                .into_iter()
604                .collect(),
605        };
606        let block2 = Block {
607            name: "b".to_string(),
608            labels: vec![],
609            blocks: Vec::new(),
610            attributes: vec![("y".to_string(), Value::Number(2.0))]
611                .into_iter()
612                .collect(),
613        };
614
615        let doc = Document {
616            blocks: vec![block1, block2],
617        };
618        let output = generate(&doc);
619        assert!(output.contains("a {"));
620        assert!(output.contains("b {"));
621    }
622
623    #[test]
624    fn test_generate_from_map() {
625        let mut data = HashMap::new();
626        data.insert(
627            "key".to_string(),
628            Value::Object(
629                vec![("nested".to_string(), Value::String("value".to_string()))]
630                    .into_iter()
631                    .collect(),
632            ),
633        );
634
635        let output = generate_from_map(&data);
636        assert!(output.contains("key"));
637        assert!(output.contains("value"));
638    }
639
640    #[test]
641    fn test_generate_from_map_non_object() {
642        let mut data = HashMap::new();
643        // Insert a non-Object value to trigger the else branch
644        data.insert("string_key".to_string(), Value::String("test".to_string()));
645        data.insert("number_key".to_string(), Value::Number(42.0));
646
647        let output = generate_from_map(&data);
648        assert!(output.contains("string_key"));
649        assert!(output.contains("_value"));
650    }
651
652    #[test]
653    fn test_generator_with_config() {
654        let config = GeneratorConfig {
655            indent: "\t",
656            comments: true,
657        };
658        let gen = Generator::with_config(config);
659        assert_eq!(gen.config.indent, "\t");
660        assert!(gen.config.comments);
661    }
662
663    #[test]
664    fn test_generator_default() {
665        let gen = Generator::default();
666        assert_eq!(gen.config.indent, "  ");
667        assert!(!gen.config.comments);
668    }
669
670    #[test]
671    fn test_generate_string_with_special_chars() {
672        let gen = Generator::new();
673        let mut out = String::new();
674        gen.write_string("true", &mut out);
675        assert_eq!(out, "\"true\"");
676
677        out.clear();
678        gen.write_string("false", &mut out);
679        assert_eq!(out, "\"false\"");
680
681        out.clear();
682        gen.write_string("null", &mut out);
683        assert_eq!(out, "\"null\"");
684
685        out.clear();
686        gen.write_string("123", &mut out);
687        assert_eq!(out, "\"123\"");
688
689        out.clear();
690        gen.write_string(".hello", &mut out);
691        assert_eq!(out, "\".hello\"");
692
693        out.clear();
694        gen.write_string("-test", &mut out);
695        assert_eq!(out, "\"-test\"");
696    }
697
698    #[test]
699    fn test_generate_empty_block() {
700        let block = Block {
701            name: "empty".to_string(),
702            labels: vec![],
703            blocks: Vec::new(),
704            attributes: HashMap::new(),
705        };
706
707        let doc = Document {
708            blocks: vec![block],
709        };
710        let output = generate(&doc);
711        assert!(output.contains("empty"));
712    }
713
714    #[test]
715    fn test_generate_block_sorted_attrs() {
716        let mut attrs = HashMap::new();
717        attrs.insert("z".to_string(), Value::Number(1.0));
718        attrs.insert("a".to_string(), Value::Number(2.0));
719        attrs.insert("m".to_string(), Value::Number(3.0));
720
721        let block = Block {
722            name: "config".to_string(),
723            labels: vec![],
724            blocks: Vec::new(),
725            attributes: attrs,
726        };
727
728        let doc = Document {
729            blocks: vec![block],
730        };
731        let output = generate(&doc);
732        // Check that attributes are sorted alphabetically
733        let a_pos = output.find("a = 2").expect("should have a = 2");
734        let m_pos = output.find("m = 3").expect("should have m = 3");
735        let z_pos = output.find("z = 1").expect("should have z = 1");
736        assert!(a_pos < m_pos);
737        assert!(m_pos < z_pos);
738    }
739
740    #[test]
741    fn test_generate_float_numbers() {
742        let mut attrs = HashMap::new();
743        attrs.insert("pi".to_string(), Value::Number(3.14159));
744        attrs.insert("e".to_string(), Value::Number(2.71828));
745
746        let block = Block {
747            name: "config".to_string(),
748            labels: vec![],
749            blocks: Vec::new(),
750            attributes: attrs,
751        };
752
753        let doc = Document {
754            blocks: vec![block],
755        };
756        let output = generate(&doc);
757        assert!(output.contains("pi = 3.14159"));
758        assert!(output.contains("e = 2.71828"));
759    }
760
761    #[test]
762    fn test_generate_integer_as_integer() {
763        let mut attrs = HashMap::new();
764        attrs.insert("count".to_string(), Value::Number(42.0));
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        // Should output as integer without decimal
778        assert!(output.contains("count = 42"));
779        assert!(!output.contains("count = 42.0"));
780    }
781
782    #[test]
783    fn test_generate_function_call() {
784        let mut attrs = HashMap::new();
785        attrs.insert(
786            "api_key".to_string(),
787            Value::Call(
788                "env".to_string(),
789                vec![Value::String("API_KEY".to_string())],
790            ),
791        );
792
793        let block = Block {
794            name: "config".to_string(),
795            labels: vec![],
796            blocks: Vec::new(),
797            attributes: attrs,
798        };
799
800        let doc = Document {
801            blocks: vec![block],
802        };
803        let output = generate(&doc);
804        // String arguments must remain quoted ACL string values.
805        assert!(output.contains("api_key = env(\"API_KEY\")"));
806    }
807
808    #[test]
809    fn test_generate_function_call_multiple_args() {
810        let mut attrs = HashMap::new();
811        attrs.insert(
812            "path".to_string(),
813            Value::Call(
814                "concat".to_string(),
815                vec![
816                    Value::String("hello".to_string()),
817                    Value::String("world".to_string()),
818                ],
819            ),
820        );
821
822        let block = Block {
823            name: "config".to_string(),
824            labels: vec![],
825            blocks: Vec::new(),
826            attributes: attrs,
827        };
828
829        let doc = Document {
830            blocks: vec![block],
831        };
832        let output = generate(&doc);
833        // String arguments must remain quoted ACL string values.
834        assert!(output.contains("path = concat(\"hello\", \"world\")"));
835    }
836
837    #[test]
838    fn test_generate_function_call_empty_args() {
839        let mut attrs = HashMap::new();
840        attrs.insert("val".to_string(), Value::Call("getenv".to_string(), vec![]));
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("val = getenv()"));
854    }
855}