aam_core/commands/
schema.rs1use crate::aaml::AAML;
17use crate::commands::Command;
18use crate::error::AamlError;
19use std::collections::HashMap;
20use std::collections::HashSet;
21
22#[derive(Clone, Debug)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct SchemaDef {
33 pub fields: HashMap<String, String>,
35 pub optional_fields: HashSet<String>,
37}
38
39impl SchemaDef {
40 #[must_use]
42 pub fn is_optional(&self, field: &str) -> bool {
43 self.optional_fields.contains(field)
44 }
45}
46
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct SchemaCommand;
50
51impl SchemaCommand {
52 fn parse_header(args: &str) -> Result<(&str, &str), AamlError> {
54 let (name_part, body_part) =
55 args.split_once('{')
56 .ok_or_else(|| AamlError::DirectiveError {
57 directive: "schema".to_string(),
58 message: "Expected '{' after schema name".to_string(),
59 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
60 "Invalid @schema syntax",
61 "Schema definition must start with '{' after the name",
62 "Use format: @schema Name { field: type, ... }",
63 ))),
64 })?;
65
66 let name = name_part.trim();
67 if name.is_empty() {
68 return Err(AamlError::DirectiveError {
69 directive: "schema".to_string(),
70 message: "Schema name is empty".to_string(),
71 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
72 "Missing schema name",
73 "Schema directive must have a name before '{'",
74 "Use format: @schema SchemaName { ... }",
75 ))),
76 });
77 }
78
79 let body = body_part
80 .rsplit_once('}')
81 .ok_or_else(|| AamlError::DirectiveError {
82 directive: "schema".to_string(),
83 message: "Expected '}' to close schema body".to_string(),
84 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
85 "Unclosed schema definition",
86 "Schema body must be closed with '}'",
87 "Ensure your @schema has matching braces: { ... }",
88 ))),
89 })?
90 .0;
91
92 Ok((name, body))
93 }
94
95 fn parse_field<'a>(
101 token: &'a str,
102 tokens: &mut impl Iterator<Item = &'a str>,
103 ) -> Result<(String, String, bool), AamlError> {
104 let (field_raw, ty) = token
105 .split_once(':')
106 .ok_or_else(|| AamlError::DirectiveError {
107 directive: "schema".to_string(),
108 message: format!("Bad field: '{token}' — missing ':' separator"),
109 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
110 "Invalid field definition",
111 format!("Field '{token}' must use format: field:type"),
112 "Use ':' to separate field name from type, e.g., x:f64",
113 ))),
114 })?;
115
116 let ty = if ty.is_empty() {
118 tokens.next().ok_or_else(|| AamlError::DirectiveError {
119 directive: "schema".to_string(),
120 message: format!("Field '{field_raw}:' has no type specified"),
121 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
122 "Missing field type",
123 format!("Field '{field_raw}' must have a type after ':'"),
124 "Provide a type: field:i32, field:string, etc.",
125 ))),
126 })?
127 } else {
128 ty
129 };
130
131 let is_optional = field_raw.ends_with('*');
132 let field = if is_optional {
133 field_raw.trim_end_matches('*')
134 } else {
135 field_raw
136 };
137
138 if field.is_empty() || ty.is_empty() {
139 return Err(AamlError::DirectiveError {
140 directive: "schema".to_string(),
141 message: format!("Bad field definition: '{field}:{ty}' — empty name or type"),
142 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
143 "Empty field or type",
144 "Field name and type cannot be empty",
145 "Use format: fieldName:typeName",
146 ))),
147 });
148 }
149
150 Ok((field.to_string(), ty.to_string(), is_optional))
151 }
152
153 fn parse(args: &str) -> Result<(String, SchemaDef), AamlError> {
157 let (name, body) = Self::parse_header(args.trim())?;
158
159 let normalized = body.replace(',', " ");
162 let mut tokens = normalized.split_whitespace();
163 let mut fields = HashMap::new();
164 let mut optional_fields = HashSet::new();
165
166 while let Some(token) = tokens.next() {
167 let (field, ty, is_optional) = Self::parse_field(token, &mut tokens)?;
168 if is_optional {
169 optional_fields.insert(field.clone());
170 }
171 fields.insert(field, ty);
172 }
173
174 Ok((
175 name.to_string(),
176 SchemaDef {
177 fields,
178 optional_fields,
179 },
180 ))
181 }
182}
183
184impl Command for SchemaCommand {
185 fn name(&self) -> &'static str {
186 "schema"
187 }
188
189 fn execute(&self, aaml: &mut AAML, args: &str) -> Result<(), AamlError> {
193 let (name, schema) = Self::parse(args)?;
194 aaml.get_schemas_mut().insert(name, schema);
195 Ok(())
196 }
197}