1use super::AAML;
4use crate::aaml::parsing;
5use crate::error::AamlError;
6use crate::types::resolve_builtin;
7use std::collections::HashMap;
8
9impl AAML {
10 pub(super) fn validate_against_schemas(
14 &self,
15 field: &str,
16 value: &str,
17 ) -> Result<(), AamlError> {
18 for (schema_name, schema_def) in &self.schemas {
19 if let Some(type_name) = schema_def.fields.get(field) {
20 return self.validate_typed_field(type_name, value, schema_name, field);
21 }
22 }
23 Ok(())
24 }
25
26 pub(crate) fn validate_typed_field(
34 &self,
35 type_name: &str,
36 value: &str,
37 schema_name: &str,
38 field: &str,
39 ) -> Result<(), AamlError> {
40 let make_err = |details: String| AamlError::SchemaValidationError {
41 schema: schema_name.to_string(),
42 field: field.to_string(),
43 type_name: type_name.to_string(),
44 details,
45 diagnostics: None,
46 };
47
48 if let Some(type_def) = self.types.get(type_name) {
50 return type_def
51 .validate(value, self)
52 .map_err(|e| make_err(e.to_string()));
53 }
54
55 if let Some(nested_schema) = self.schemas.get(type_name) {
57 let fields = nested_schema.fields.clone();
58 return self
59 .validate_inline_object_against_schema(value, type_name, &fields)
60 .map_err(|e| make_err(e.to_string()));
61 }
62
63 resolve_builtin(type_name).map_or_else(
65 |_| Err(make_err(format!("Unknown type '{type_name}'"))),
66 |type_def| {
67 type_def
68 .validate(value, self)
69 .map_err(|e| make_err(e.to_string()))
70 },
71 )
72 }
73
74 fn validate_inline_object_against_schema(
81 &self,
82 value: &str,
83 schema_name: &str,
84 schema_fields: &HashMap<String, String>,
85 ) -> Result<(), AamlError> {
86 if !parsing::is_inline_object(value) {
87 return Err(AamlError::InvalidValue {
88 details: format!(
89 "Field typed as schema '{schema_name}' must be an inline object '{{ k = v, ... }}'"
90 ),
91 expected: "inline object format: { key = value, ... }".to_string(),
92 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
93 "Schema field must be an object",
94 format!("Expected inline object for schema '{schema_name}', got: '{value}'"),
95 "Use format: { key1 = value1, key2 = value2 }",
96 ))),
97 });
98 }
99
100 let pairs = parsing::parse_inline_object(value)?;
101
102 let pair_map: HashMap<&str, &str> = pairs
103 .iter()
104 .map(|(k, v)| (k.as_str(), v.as_str()))
105 .collect();
106
107 let optional_fields = self
109 .schemas
110 .get(schema_name)
111 .map(|s| s.optional_fields.clone())
112 .unwrap_or_default();
113
114 for (field, type_name) in schema_fields {
115 match pair_map.get(field.as_str()) {
116 None => {
117 if !optional_fields.contains(field.as_str()) {
119 return Err(AamlError::SchemaValidationError {
120 schema: schema_name.to_string(),
121 field: field.clone(),
122 type_name: type_name.clone(),
123 details: format!(
124 "Missing field '{field}' in inline object for schema '{schema_name}'"
125 ),
126 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
127 "Missing required field",
128 format!(
129 "Field '{field}' is required in schema '{schema_name}' but not provided"
130 ),
131 format!("Add field: {field} = <value>"),
132 ))),
133 });
134 }
135 }
136 Some(field_value) => {
137 self.validate_typed_field(type_name, field_value, schema_name, field)?;
138 }
139 }
140 }
141
142 Ok(())
143 }
144
145 pub fn validate_schemas_completeness(&self) -> Result<(), AamlError> {
148 let names: Vec<&str> = self
149 .schemas
150 .keys()
151 .map(std::string::String::as_str)
152 .collect();
153 self.validate_schemas_completeness_for(&names)
154 }
155
156 pub fn validate_schemas_completeness_for(
160 &self,
161 schema_names: &[&str],
162 ) -> Result<(), AamlError> {
163 for name in schema_names {
164 let Some(schema_def) = self.schemas.get(*name) else {
165 continue;
166 };
167 for (field, type_name) in &schema_def.fields {
168 if schema_def.is_optional(field) {
169 continue;
170 }
171 if !self.map.contains_key(field.as_str()) {
172 return Err(AamlError::SchemaValidationError {
173 schema: name.to_string(),
174 field: field.clone(),
175 type_name: type_name.clone(),
176 details: format!("Missing required field '{field}'"),
177 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
178 "Missing required field in schema",
179 format!(
180 "Schema '{name}' requires field '{field}' of type '{type_name}'"
181 ),
182 format!("Define the field in your configuration: {field} = <value>"),
183 ))),
184 });
185 }
186 }
187 }
188 Ok(())
189 }
190
191 pub fn apply_schema(
200 &self,
201 schema_name: &str,
202 data: &HashMap<String, String>,
203 ) -> Result<(), AamlError> {
204 let schema = self
205 .schemas
206 .get(schema_name)
207 .ok_or_else(|| AamlError::NotFound {
208 key: schema_name.to_string(),
209 context: "schema registry".to_string(),
210 diagnostics: Some(Box::new(crate::error::ErrorDiagnostics::new(
211 "Schema not found",
212 format!("Schema '{schema_name}' does not exist"),
213 "Check your @schema definitions",
214 ))),
215 })?;
216
217 for (field, type_name) in &schema.fields {
218 match data.get(field) {
219 None => {
220 if !schema.optional_fields.contains(field.as_str()) {
221 return Err(AamlError::SchemaValidationError {
222 schema: schema_name.to_string(),
223 field: field.clone(),
224 type_name: type_name.clone(),
225 details: format!("Missing required field '{field}'"),
226 diagnostics: None,
227 });
228 }
229 }
230 Some(value) => {
231 self.validate_typed_field(type_name, value, schema_name, field)?;
232 }
233 }
234 }
235
236 Ok(())
237 }
238}