use crate::aaml::AAML;
use crate::commands::Command;
use crate::error::AamlError;
use std::collections::HashMap;
use std::collections::HashSet;
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SchemaDef {
pub fields: HashMap<String, String>,
pub optional_fields: HashSet<String>,
}
impl SchemaDef {
pub fn is_optional(&self, field: &str) -> bool {
self.optional_fields.contains(field)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SchemaCommand;
impl SchemaCommand {
fn parse_header(args: &str) -> Result<(&str, &str), AamlError> {
let (name_part, body_part) =
args.split_once('{')
.ok_or_else(|| AamlError::DirectiveError {
directive: "schema".to_string(),
message: "Expected '{' after schema name".to_string(),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Invalid @schema syntax",
"Schema definition must start with '{' after the name",
"Use format: @schema Name { field: type, ... }",
)),
})?;
let name = name_part.trim();
if name.is_empty() {
return Err(AamlError::DirectiveError {
directive: "schema".to_string(),
message: "Schema name is empty".to_string(),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Missing schema name",
"Schema directive must have a name before '{'",
"Use format: @schema SchemaName { ... }",
)),
});
}
let body = body_part
.rsplit_once('}')
.ok_or_else(|| AamlError::DirectiveError {
directive: "schema".to_string(),
message: "Expected '}' to close schema body".to_string(),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Unclosed schema definition",
"Schema body must be closed with '}'",
"Ensure your @schema has matching braces: { ... }",
)),
})?
.0;
Ok((name, body))
}
fn parse_field<'a>(
token: &'a str,
tokens: &mut impl Iterator<Item = &'a str>,
) -> Result<(String, String, bool), AamlError> {
let (field_raw, ty) = token
.split_once(':')
.ok_or_else(|| AamlError::DirectiveError {
directive: "schema".to_string(),
message: format!("Bad field: '{}' — missing ':' separator", token),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Invalid field definition",
format!("Field '{}' must use format: field:type", token),
"Use ':' to separate field name from type, e.g., x:f64",
)),
})?;
let ty = if ty.is_empty() {
tokens.next().ok_or_else(|| AamlError::DirectiveError {
directive: "schema".to_string(),
message: format!("Field '{}:' has no type specified", field_raw),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Missing field type",
format!("Field '{}' must have a type after ':'", field_raw),
"Provide a type: field:i32, field:string, etc.",
)),
})?
} else {
ty
};
let is_optional = field_raw.ends_with('*');
let field = if is_optional {
field_raw.trim_end_matches('*')
} else {
field_raw
};
if field.is_empty() || ty.is_empty() {
return Err(AamlError::DirectiveError {
directive: "schema".to_string(),
message: format!(
"Bad field definition: '{}:{}' — empty name or type",
field, ty
),
diagnostics: Some(crate::error::ErrorDiagnostics::new(
"Empty field or type",
"Field name and type cannot be empty",
"Use format: fieldName:typeName",
)),
});
}
Ok((field.to_string(), ty.to_string(), is_optional))
}
fn parse(args: &str) -> Result<(String, SchemaDef), AamlError> {
let (name, body) = Self::parse_header(args.trim())?;
let normalized = body.replace(',', " ");
let mut tokens = normalized.split_whitespace();
let mut fields = HashMap::new();
let mut optional_fields = HashSet::new();
while let Some(token) = tokens.next() {
let (field, ty, is_optional) = Self::parse_field(token, &mut tokens)?;
if is_optional {
optional_fields.insert(field.clone());
}
fields.insert(field, ty);
}
Ok((
name.to_string(),
SchemaDef {
fields,
optional_fields,
},
))
}
}
impl Command for SchemaCommand {
fn name(&self) -> &str {
"schema"
}
fn execute(&self, aaml: &mut AAML, args: &str) -> Result<(), AamlError> {
let (name, schema) = Self::parse(args)?;
aaml.get_schemas_mut().insert(name, schema);
Ok(())
}
}