Skip to main content

a3s_acl/
lib.rs

1// ============================================================================
2// ACL - Agent Configuration Language
3//
4// A3S-native configuration language for defining agent configurations.
5// ============================================================================
6
7pub mod ast;
8mod canonical;
9pub mod diagnostic;
10pub mod generator;
11pub mod lexer;
12mod parse_limits;
13pub mod parser;
14pub mod schema;
15
16pub use ast::{Block, Document, Value};
17pub use canonical::{
18    canonical_bytes, canonical_bytes_with_schema, canonical_digest, canonical_digest_with_schema,
19    CanonicalError, CANONICAL_DIGEST_ALGORITHM,
20};
21pub use diagnostic::{DiagnosticCode, DiagnosticReport, ParseError};
22pub use generator::{generate, generate_from_map, Generator, GeneratorConfig};
23pub use lexer::{Lexer, Location, Span, Token, TokenWithSpan};
24pub use parse_limits::{ParseLimits, DEFAULT_PARSE_LIMITS};
25pub use parser::{collect_diagnostics, collect_diagnostics_with_limits, parse, parse_with_limits};
26pub use schema::{
27    validate_document, validate_document_with_limits, AttributeSchema, BlockSchema, CallSchema,
28    Cardinality, ObjectSchema, Schema, SchemaDefinitionError, SchemaDiagnostic,
29    SchemaDiagnosticCode, SchemaReport, ValueSchema,
30};
31
32/// Parse ACL text into a Document
33///
34/// # Example
35///
36/// ```
37/// use a3s_acl::parse;
38///
39/// let input = r#"
40///     default_model = "openai/gpt-4"
41///
42///     providers "openai" {
43///         name = "openai"
44///         api_key = "sk-test"
45///         base_url = "https://api.openai.com/v1"
46///     }
47/// "#;
48///
49/// let doc = parse(input).unwrap();
50/// for block in doc.blocks {
51///     println!("Block: {}", block.name);
52/// }
53/// ```
54pub fn parse_acl(input: &str) -> Result<Document, ParseError> {
55    parse(input)
56}
57
58/// Generate ACL text from a Document
59///
60/// # Example
61///
62/// ```
63/// use a3s_acl::{generate, Document, Block, Value};
64/// use std::collections::HashMap;
65///
66/// let mut attrs = HashMap::new();
67/// attrs.insert("name".to_string(), Value::String("test".to_string()));
68///
69/// let doc = Document {
70///     blocks: vec![Block {
71///         name: "config".to_string(),
72///         labels: vec![],
73///         blocks: vec![],
74///         attributes: attrs,
75///     }],
76/// };
77///
78/// let output = generate(&doc);
79/// println!("{}", output);
80/// ```
81pub fn generate_acl(doc: &Document) -> String {
82    generate(doc)
83}
84
85/// High-level builder API for creating ACL configurations
86pub mod builder {
87    use crate::ast::{Block, Document, Value};
88    use std::collections::HashMap;
89
90    /// Builder for ACL Documents
91    pub struct DocumentBuilder {
92        blocks: Vec<Block>,
93    }
94
95    impl DocumentBuilder {
96        pub fn new() -> Self {
97            Self { blocks: Vec::new() }
98        }
99
100        /// Add a block to the document
101        pub fn block(mut self, block: Block) -> Self {
102            self.blocks.push(block);
103            self
104        }
105
106        /// Add a simple key-value block
107        pub fn kv_block(mut self, name: &str, key: &str, value: Value) -> Self {
108            let mut attrs = HashMap::new();
109            attrs.insert(key.to_string(), value);
110            self.blocks.push(Block {
111                name: name.to_string(),
112                labels: Vec::new(),
113                blocks: Vec::new(),
114                attributes: attrs,
115            });
116            self
117        }
118
119        /// Build the document
120        pub fn build(self) -> Document {
121            Document {
122                blocks: self.blocks,
123            }
124        }
125    }
126
127    impl Default for DocumentBuilder {
128        fn default() -> Self {
129            Self::new()
130        }
131    }
132
133    /// Builder for Blocks
134    pub struct BlockBuilder {
135        name: String,
136        labels: Vec<String>,
137        blocks: Vec<Block>,
138        attributes: HashMap<String, Value>,
139    }
140
141    impl BlockBuilder {
142        pub fn new(name: &str) -> Self {
143            Self {
144                name: name.to_string(),
145                labels: Vec::new(),
146                blocks: Vec::new(),
147                attributes: HashMap::new(),
148            }
149        }
150
151        pub fn label(mut self, label: &str) -> Self {
152            self.labels.push(label.to_string());
153            self
154        }
155
156        pub fn attr(mut self, key: &str, value: Value) -> Self {
157            self.attributes.insert(key.to_string(), value);
158            self
159        }
160
161        pub fn nested_block(mut self, block: Block) -> Self {
162            self.blocks.push(block);
163            self
164        }
165
166        pub fn build(self) -> Block {
167            Block {
168                name: self.name,
169                labels: self.labels,
170                blocks: self.blocks,
171                attributes: self.attributes,
172            }
173        }
174    }
175
176    // Helper functions for creating Values
177    pub fn string(s: &str) -> Value {
178        Value::String(s.to_string())
179    }
180
181    pub fn number(n: f64) -> Value {
182        Value::Number(n)
183    }
184
185    pub fn integer(n: i64) -> Value {
186        Value::Number(n as f64)
187    }
188
189    pub fn boolean(b: bool) -> Value {
190        Value::Bool(b)
191    }
192
193    pub fn null() -> Value {
194        Value::Null
195    }
196
197    pub fn list(items: Vec<Value>) -> Value {
198        Value::List(items)
199    }
200
201    pub fn call(name: &str, args: Vec<Value>) -> Value {
202        Value::Call(name.to_string(), args)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_parse_acl_function() {
212        let input = r#"
213            name = "test"
214            count = 42
215        "#;
216        let doc = parse_acl(input).unwrap();
217        assert_eq!(doc.blocks.len(), 2);
218    }
219
220    #[test]
221    fn test_generate_acl_function() {
222        let doc = Document {
223            blocks: vec![Block {
224                name: "test".to_string(),
225                labels: vec![],
226                blocks: vec![],
227                attributes: vec![("key".to_string(), Value::String("value".to_string()))]
228                    .into_iter()
229                    .collect(),
230            }],
231        };
232        let output = generate_acl(&doc);
233        assert!(output.contains("test"));
234        assert!(output.contains("value"));
235    }
236
237    #[test]
238    fn test_builder_document_builder() {
239        let doc = builder::DocumentBuilder::new()
240            .kv_block("config", "key", builder::string("value"))
241            .build();
242
243        assert_eq!(doc.blocks.len(), 1);
244        assert_eq!(doc.blocks[0].name, "config");
245    }
246
247    #[test]
248    fn test_builder_document_builder_with_block() {
249        let block = builder::BlockBuilder::new("provider")
250            .attr("name", builder::string("openai"))
251            .build();
252
253        let doc = builder::DocumentBuilder::new().block(block).build();
254
255        assert_eq!(doc.blocks.len(), 1);
256        assert_eq!(doc.blocks[0].name, "provider");
257    }
258
259    #[test]
260    fn test_builder_document_default() {
261        let doc = builder::DocumentBuilder::default().build();
262        assert!(doc.blocks.is_empty());
263    }
264
265    #[test]
266    fn test_builder_block_builder() {
267        let block = builder::BlockBuilder::new("provider")
268            .label("openai")
269            .attr("api_key", builder::string("sk-xxx"))
270            .attr("enabled", builder::boolean(true))
271            .build();
272
273        assert_eq!(block.name, "provider");
274        assert_eq!(block.labels, vec!["openai"]);
275        assert_eq!(
276            block
277                .attributes
278                .get("api_key")
279                .map(|v| v.to_string())
280                .unwrap(),
281            "sk-xxx"
282        );
283    }
284
285    #[test]
286    fn test_builder_block_builder_nested() {
287        let inner = builder::BlockBuilder::new("model")
288            .attr("name", builder::string("gpt-4"))
289            .build();
290
291        let block = builder::BlockBuilder::new("provider")
292            .label("openai")
293            .nested_block(inner)
294            .build();
295
296        assert_eq!(block.blocks.len(), 1);
297        assert_eq!(block.blocks[0].name, "model");
298    }
299
300    #[test]
301    fn test_builder_value_helpers() {
302        assert_eq!(builder::string("test"), Value::String("test".to_string()));
303        assert_eq!(builder::number(3.14), Value::Number(3.14));
304        assert_eq!(builder::integer(42), Value::Number(42.0));
305        assert_eq!(builder::boolean(true), Value::Bool(true));
306        assert_eq!(builder::boolean(false), Value::Bool(false));
307        assert_eq!(builder::null(), Value::Null);
308        assert_eq!(
309            builder::list(vec![Value::Number(1.0)]),
310            Value::List(vec![Value::Number(1.0)])
311        );
312    }
313
314    #[test]
315    fn test_re_exports() {
316        // Verify re-exported types are accessible
317        let _ = Document::default();
318        let _ = Block {
319            name: "test".to_string(),
320            labels: vec![],
321            blocks: vec![],
322            attributes: std::collections::HashMap::new(),
323        };
324        let _ = Value::String("test".to_string());
325        let _ = Generator::new();
326        let _ = GeneratorConfig::default();
327        let _ = Lexer::new("");
328        let _ = Location {
329            line: 1,
330            column: 1,
331            offset: 0,
332        };
333        let _ = Span::default();
334        let _ = Token::Eof;
335        let _ = TokenWithSpan::new(Token::Eof, Location::default(), Location::default());
336        let _ = ParseError::new(
337            DiagnosticCode::UnexpectedToken,
338            "test",
339            Span::point(Location {
340                line: 1,
341                column: 1,
342                offset: 0,
343            }),
344        );
345    }
346
347    #[test]
348    fn test_lexer_re_exports() {
349        let mut lexer = Lexer::new("test = 42");
350        let tokens = lexer.tokenize();
351        assert!(!tokens.is_empty());
352    }
353
354    #[test]
355    fn test_token_with_span() {
356        let loc = Location {
357            line: 1,
358            column: 1,
359            offset: 0,
360        };
361        let token = TokenWithSpan::new(Token::Ident("test".to_string()), loc, loc);
362        assert!(matches!(token.token, Token::Ident(_)));
363    }
364}