1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
//! Schema validation methods for [`AAML`](AAML).
use super::AAML;
use crate::aaml::parsing;
use crate::error::AamlError;
use crate::types::resolve_builtin;
use std::collections::HashMap;
impl AAML {
/// Validates a single field value against any schema that declares it.
///
/// If the field is not declared in any schema the function succeeds silently.
pub(super) fn validate_against_schemas(
&self,
field: &str,
value: &str,
) -> Result<(), AamlError> {
for (schema_name, schema_def) in &self.schemas {
if let Some(type_name) = schema_def.fields.get(field) {
return self.validate_typed_field(type_name, value, schema_name, field);
}
}
Ok(())
}
/// Validates `value` against `type_name`, checking:
/// 1. Registered custom types.
/// 2. Nested schema types (type_name matches a registered schema name).
/// 3. `list<T>` — validates every element of a `[...]` literal against `T`.
/// 4. Built-in module types (`math::`, `time::`, `physics::`, primitives).
///
/// Returns a [`AamlError::SchemaValidationError`] on failure.
pub(crate) fn validate_typed_field(
&self,
type_name: &str,
value: &str,
schema_name: &str,
field: &str,
) -> Result<(), AamlError> {
let make_err = |details: String| AamlError::SchemaValidationError {
schema: schema_name.to_string(),
field: field.to_string(),
type_name: type_name.to_string(),
details,
};
// 1. Registered custom type alias
if let Some(type_def) = self.types.get(type_name) {
return type_def
.validate(value, self)
.map_err(|e| make_err(e.to_string()));
}
// 2. Nested schema — type_name matches a registered schema name
if let Some(nested_schema) = self.schemas.get(type_name) {
return self
.validate_inline_object_against_schema(
value,
type_name,
nested_schema.fields.clone(),
)
.map_err(|e| make_err(e.to_string()));
}
// 3. Built-in types
match resolve_builtin(type_name) {
Ok(type_def) => type_def
.validate(value, self)
.map_err(|e| make_err(e.to_string())),
Err(_) => Err(make_err(format!("Unknown type '{}'", type_name))),
}
}
/// Validates an inline object literal `{ key = val, ... }` against the
/// fields of the named nested schema.
///
/// - Required fields (not marked `*`) declared in the schema must be present.
/// - Optional fields (marked `*`) may be absent; if present they are validated.
/// - Each value is validated against its declared type (recursively).
fn validate_inline_object_against_schema(
&self,
value: &str,
schema_name: &str,
schema_fields: HashMap<String, String>,
) -> Result<(), AamlError> {
if !parsing::is_inline_object(value) {
return Err(AamlError::InvalidValue(format!(
"Field typed as schema '{}' must be an inline object '{{ k = v, ... }}', got: '{}'",
schema_name, value
)));
}
let pairs = parsing::parse_inline_object(value).map_err(|e| {
AamlError::InvalidValue(format!(
"Failed to parse inline object for schema '{}': {}",
schema_name, e
))
})?;
let pair_map: HashMap<&str, &str> = pairs
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
// Fetch optional set from the registered schema (if still available).
let optional_fields = self
.schemas
.get(schema_name)
.map(|s| s.optional_fields.clone())
.unwrap_or_default();
for (field, type_name) in &schema_fields {
match pair_map.get(field.as_str()) {
None => {
// Missing field — only an error for required fields
if !optional_fields.contains(field.as_str()) {
return Err(AamlError::SchemaValidationError {
schema: schema_name.to_string(),
field: field.clone(),
type_name: type_name.clone(),
details: format!(
"Missing field '{}' in inline object for schema '{}'",
field, schema_name
),
});
}
}
Some(field_value) => {
self.validate_typed_field(type_name, field_value, schema_name, field)?;
}
}
}
Ok(())
}
/// Checks every **required** field in every registered schema against the current map.
/// Optional fields (declared with `*`) are skipped.
pub fn validate_schemas_completeness(&self) -> Result<(), AamlError> {
let names: Vec<&str> = self.schemas.keys().map(|s| s.as_str()).collect();
self.validate_schemas_completeness_for(&names)
}
// Medium Complexity
/// Checks required fields only for the named schemas.
/// Used by `@derive` to validate only child-defined schemas, not inherited ones.
pub fn validate_schemas_completeness_for(
&self,
schema_names: &[&str],
) -> Result<(), AamlError> {
for name in schema_names {
let Some(schema_def) = self.schemas.get(*name) else {
continue;
};
for (field, type_name) in &schema_def.fields {
if schema_def.is_optional(field) {
continue;
}
if !self.map.contains_key(field.as_str()) {
return Err(AamlError::SchemaValidationError {
schema: name.to_string(),
field: field.clone(),
type_name: type_name.clone(),
details: format!("Missing required field '{field}'"),
});
}
}
}
Ok(())
}
/// Validates a complete `data` map against the named schema.
///
/// For every **required** field declared in the schema the method checks:
/// 1. The key is present in `data`.
/// 2. The value satisfies the declared type (including nested schemas and lists).
///
/// Optional fields (declared with `*`) are only validated when they are
/// present in `data`; their absence is not an error.
pub fn apply_schema(
&self,
schema_name: &str,
data: &HashMap<String, String>,
) -> Result<(), AamlError> {
let schema = self
.schemas
.get(schema_name)
.ok_or_else(|| AamlError::NotFound(format!("Schema '{}' not found", schema_name)))?;
let fields: Vec<(String, String)> = schema
.fields
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let optional = schema.optional_fields.clone();
for (field, type_name) in &fields {
match data.get(field) {
None => {
if !optional.contains(field.as_str()) {
return Err(AamlError::SchemaValidationError {
schema: schema_name.to_string(),
field: field.clone(),
type_name: type_name.clone(),
details: format!("Missing required field '{}'", field),
});
}
}
Some(value) => {
self.validate_typed_field(type_name, value, schema_name, field)?;
}
}
}
Ok(())
}
}