1use crate::scalar::{ScalarType, ScalarValue};
7use crate::yaml::{Document, TaggedNode};
8use rowan::ast::AstNode;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum ValidationErrorKind {
13 TypeNotAllowed {
15 found_type: ScalarType,
17 allowed_types: Vec<ScalarType>,
19 },
20 CustomConstraintFailed {
22 constraint_name: String,
24 actual_value: String,
26 },
27 CoercionFailed {
29 from_type: ScalarType,
31 to_types: Vec<ScalarType>,
33 },
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ValidationError {
39 pub kind: ValidationErrorKind,
41 pub path: String,
43 pub schema_name: String,
45}
46
47impl ValidationError {
48 pub fn type_not_allowed(
50 path: impl Into<String>,
51 schema_name: impl Into<String>,
52 found_type: ScalarType,
53 allowed_types: Vec<ScalarType>,
54 ) -> Self {
55 Self {
56 kind: ValidationErrorKind::TypeNotAllowed {
57 found_type,
58 allowed_types,
59 },
60 path: path.into(),
61 schema_name: schema_name.into(),
62 }
63 }
64
65 pub fn custom_constraint_failed(
67 path: impl Into<String>,
68 schema_name: impl Into<String>,
69 constraint_name: impl Into<String>,
70 actual_value: impl Into<String>,
71 ) -> Self {
72 Self {
73 kind: ValidationErrorKind::CustomConstraintFailed {
74 constraint_name: constraint_name.into(),
75 actual_value: actual_value.into(),
76 },
77 path: path.into(),
78 schema_name: schema_name.into(),
79 }
80 }
81
82 pub fn coercion_failed(
84 path: impl Into<String>,
85 schema_name: impl Into<String>,
86 from_type: ScalarType,
87 to_types: Vec<ScalarType>,
88 ) -> Self {
89 Self {
90 kind: ValidationErrorKind::CoercionFailed {
91 from_type,
92 to_types,
93 },
94 path: path.into(),
95 schema_name: schema_name.into(),
96 }
97 }
98
99 pub fn message(&self) -> String {
101 match &self.kind {
102 ValidationErrorKind::TypeNotAllowed {
103 found_type,
104 allowed_types,
105 } => {
106 format!(
107 "type {:?} not allowed in {} schema, expected one of {:?}",
108 found_type, self.schema_name, allowed_types
109 )
110 }
111 ValidationErrorKind::CustomConstraintFailed {
112 constraint_name,
113 actual_value,
114 } => {
115 format!("custom constraint '{constraint_name}' failed for value '{actual_value}'")
116 }
117 ValidationErrorKind::CoercionFailed {
118 from_type,
119 to_types,
120 } => {
121 format!(
122 "cannot coerce {:?} to any of {:?} in {} schema",
123 from_type, to_types, self.schema_name
124 )
125 }
126 }
127 }
128}
129
130impl std::fmt::Display for ValidationError {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 write!(f, "Validation error at {}: {}", self.path, self.message())
133 }
134}
135
136impl std::error::Error for ValidationError {}
137
138pub type ValidationResult<T> = Result<T, Vec<ValidationError>>;
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub enum CustomValidationResult {
144 Valid,
146 Invalid {
148 constraint: String,
150 reason: String,
152 },
153}
154
155impl CustomValidationResult {
156 pub fn invalid(constraint: impl Into<String>, reason: impl Into<String>) -> Self {
158 Self::Invalid {
159 constraint: constraint.into(),
160 reason: reason.into(),
161 }
162 }
163
164 pub fn is_valid(&self) -> bool {
166 matches!(self, Self::Valid)
167 }
168
169 pub fn is_invalid(&self) -> bool {
171 matches!(self, Self::Invalid { .. })
172 }
173}
174
175pub type CustomValidator = Box<dyn Fn(&str, &str) -> CustomValidationResult + Send + Sync>;
179
180pub struct CustomSchema {
182 pub name: String,
184 pub allowed_types: Vec<ScalarType>,
186 pub custom_validators: std::collections::HashMap<ScalarType, CustomValidator>,
188 pub allow_coercion: bool,
190}
191
192impl std::fmt::Debug for CustomSchema {
193 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194 f.debug_struct("CustomSchema")
195 .field("name", &self.name)
196 .field("allowed_types", &self.allowed_types)
197 .field("allow_coercion", &self.allow_coercion)
198 .field(
199 "validators",
200 &format!("<{} validators>", self.custom_validators.len()),
201 )
202 .finish()
203 }
204}
205
206impl CustomSchema {
207 pub fn new(name: impl Into<String>) -> Self {
209 Self {
210 name: name.into(),
211 allowed_types: Vec::new(),
212 custom_validators: std::collections::HashMap::new(),
213 allow_coercion: true,
214 }
215 }
216
217 pub fn allow_type(mut self, scalar_type: ScalarType) -> Self {
219 if !self.allowed_types.contains(&scalar_type) {
220 self.allowed_types.push(scalar_type);
221 }
222 self
223 }
224
225 pub fn allow_types(mut self, types: &[ScalarType]) -> Self {
227 for &scalar_type in types {
228 if !self.allowed_types.contains(&scalar_type) {
229 self.allowed_types.push(scalar_type);
230 }
231 }
232 self
233 }
234
235 pub fn with_validator<F>(mut self, scalar_type: ScalarType, validator: F) -> Self
237 where
238 F: Fn(&str, &str) -> CustomValidationResult + Send + Sync + 'static,
239 {
240 self.custom_validators
241 .insert(scalar_type, Box::new(validator));
242 self
243 }
244
245 pub fn strict(mut self) -> Self {
247 self.allow_coercion = false;
248 self
249 }
250
251 pub fn allows_type(&self, scalar_type: ScalarType) -> bool {
253 self.allowed_types.contains(&scalar_type)
254 }
255
256 pub fn validate_scalar(&self, content: &str, path: &str) -> Result<(), ValidationError> {
258 self.validate_scalar_value(&ScalarValue::parse(content.trim()), path)
259 }
260
261 fn validate_scalar_value(
262 &self,
263 scalar_value: &ScalarValue,
264 path: &str,
265 ) -> Result<(), ValidationError> {
266 let content = scalar_value.value();
267 let scalar_type = scalar_value.scalar_type();
268
269 check_type(
270 scalar_value,
271 &self.allowed_types,
272 self.allow_coercion,
273 path,
274 &self.name,
275 )?;
276
277 if let Some(validator) = self.custom_validators.get(&scalar_type) {
278 let result = validator(content, path);
279 if let CustomValidationResult::Invalid { constraint, reason } = result {
280 return Err(ValidationError::custom_constraint_failed(
281 path,
282 &self.name,
283 format!("{constraint}: {reason}"),
284 content,
285 ));
286 }
287 }
288
289 Ok(())
290 }
291}
292
293fn check_type(
296 value: &ScalarValue,
297 allowed: &[ScalarType],
298 coerce: bool,
299 path: &str,
300 schema_name: &str,
301) -> Result<(), ValidationError> {
302 let scalar_type = value.scalar_type();
303 if allowed.contains(&scalar_type) {
304 return Ok(());
305 }
306 if !coerce {
307 return Err(ValidationError::type_not_allowed(
308 path,
309 schema_name,
310 scalar_type,
311 allowed.to_vec(),
312 ));
313 }
314 if allowed.iter().any(|&t| value.coerce_to_type(t).is_some()) {
315 Ok(())
316 } else {
317 Err(ValidationError::coercion_failed(
318 path,
319 schema_name,
320 scalar_type,
321 allowed.to_vec(),
322 ))
323 }
324}
325
326#[derive(Debug)]
328pub enum Schema {
329 Failsafe,
331 Json,
333 Core,
335 Custom(CustomSchema),
337}
338
339impl PartialEq for Schema {
340 fn eq(&self, other: &Self) -> bool {
341 match (self, other) {
342 (Schema::Failsafe, Schema::Failsafe)
343 | (Schema::Json, Schema::Json)
344 | (Schema::Core, Schema::Core) => true,
345 (Schema::Custom(a), Schema::Custom(b)) => a.name == b.name,
346 _ => false,
347 }
348 }
349}
350
351impl Schema {
352 pub fn name(&self) -> &str {
354 match self {
355 Schema::Failsafe => "failsafe",
356 Schema::Json => "json",
357 Schema::Core => "core",
358 Schema::Custom(custom) => &custom.name,
359 }
360 }
361
362 pub fn allows_scalar_type(&self, scalar_type: ScalarType) -> bool {
364 self.allowed_scalar_types().contains(&scalar_type)
365 }
366
367 pub fn allowed_scalar_types(&self) -> Vec<ScalarType> {
369 match self {
370 Schema::Failsafe => vec![ScalarType::String],
371 Schema::Json => vec![
372 ScalarType::String,
373 ScalarType::Integer,
374 ScalarType::Float,
375 ScalarType::Boolean,
376 ScalarType::Null,
377 ],
378 Schema::Core => vec![
379 ScalarType::String,
380 ScalarType::Integer,
381 ScalarType::Float,
382 ScalarType::Boolean,
383 ScalarType::Null,
384 #[cfg(feature = "base64")]
385 ScalarType::Binary,
386 ScalarType::Timestamp,
387 ScalarType::Regex,
388 ],
389 Schema::Custom(custom) => custom.allowed_types.clone(),
390 }
391 }
392}
393
394#[derive(Debug)]
396pub struct SchemaValidator {
397 schema: Schema,
398 strict: bool,
399}
400
401impl SchemaValidator {
402 pub fn new(schema: Schema) -> Self {
404 Self {
405 schema,
406 strict: false,
407 }
408 }
409
410 pub fn failsafe() -> Self {
412 Self::new(Schema::Failsafe)
413 }
414
415 pub fn json() -> Self {
417 Self::new(Schema::Json)
418 }
419
420 pub fn core() -> Self {
422 Self::new(Schema::Core)
423 }
424
425 pub fn custom(schema: CustomSchema) -> Self {
427 Self::new(Schema::Custom(schema))
428 }
429
430 pub fn strict(mut self) -> Self {
432 self.strict = true;
433 self
434 }
435
436 pub fn schema(&self) -> &Schema {
438 &self.schema
439 }
440
441 pub fn validate(&self, document: &Document) -> ValidationResult<()> {
443 self.check_document(document, false)
444 }
445
446 pub fn can_coerce(&self, document: &Document) -> ValidationResult<()> {
450 self.check_document(document, !self.strict)
451 }
452
453 fn check_document(&self, document: &Document, coerce_only: bool) -> ValidationResult<()> {
454 let mut errors = Vec::new();
455 if let Some(root) = document.root_node() {
456 self.check_node(&root, "root", coerce_only, &mut errors);
457 }
458 if errors.is_empty() {
459 Ok(())
460 } else {
461 Err(errors)
462 }
463 }
464
465 fn check_node(
466 &self,
467 node: &rowan::SyntaxNode<crate::yaml::Lang>,
468 path: &str,
469 coerce_only: bool,
470 errors: &mut Vec<ValidationError>,
471 ) {
472 use crate::yaml::{extract, Mapping, Scalar, Sequence};
473
474 if let Some(scalar) = extract::<Scalar>(node) {
475 let value = ScalarValue::from_scalar(&scalar);
476 self.check_scalar(&value, false, path, coerce_only, errors);
477 } else if let Some(tagged_node) = extract::<TaggedNode>(node) {
478 if let Some(content) = tagged_node
479 .as_sequence()
480 .map(|s| s.syntax().clone())
481 .or_else(|| tagged_node.as_mapping().map(|m| m.syntax().clone()))
482 {
483 self.check_node(&content, path, coerce_only, errors);
484 } else if let Some(value) = tagged_scalar_value(&tagged_node) {
485 self.check_scalar(&value, true, path, coerce_only, errors);
486 }
487 } else if let Some(sequence) = extract::<Sequence>(node) {
488 for (i, item) in sequence.items().enumerate() {
489 self.check_node(&item, &format!("{path}[{i}]"), coerce_only, errors);
490 }
491 } else if let Some(mapping) = extract::<Mapping>(node) {
492 for (key_node, value_node) in mapping.pairs() {
494 let key_name = key_node.text().to_string().trim().to_string();
495 self.check_node(
496 &value_node,
497 &format!("{path}.{key_name}"),
498 coerce_only,
499 errors,
500 );
501 }
502 }
503 }
504
505 fn check_scalar(
506 &self,
507 value: &ScalarValue,
508 tagged: bool,
509 path: &str,
510 coerce_only: bool,
511 errors: &mut Vec<ValidationError>,
512 ) {
513 let result = match &self.schema {
514 Schema::Custom(custom_schema) if !coerce_only => {
515 custom_schema.validate_scalar_value(value, path)
516 }
517 _ => check_type(
519 value,
520 &self.schema.allowed_scalar_types(),
521 !tagged && !self.strict,
522 path,
523 self.schema.name(),
524 ),
525 };
526 errors.extend(result.err());
527 }
528}
529
530fn tagged_scalar_value(tagged_node: &TaggedNode) -> Option<ScalarValue> {
532 let value = ScalarValue::from_tagged(tagged_node)?;
533 let scalar_type = match tagged_node.normalized_tag().as_deref() {
534 Some(crate::tag::TIMESTAMP) => ScalarType::Timestamp,
535 Some(crate::tag::REGEX) => ScalarType::Regex,
536 #[cfg(feature = "base64")]
537 Some(crate::tag::BINARY) => ScalarType::Binary,
538 _ => return Some(value),
539 };
540 Some(value.with_type(scalar_type))
541}
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_validator_creation() {
549 let failsafe = SchemaValidator::failsafe();
550 assert_eq!(*failsafe.schema(), Schema::Failsafe);
551 assert!(!failsafe.strict);
552
553 let json = SchemaValidator::json();
554 assert_eq!(*json.schema(), Schema::Json);
555
556 let core = SchemaValidator::core();
557 assert_eq!(*core.schema(), Schema::Core);
558
559 let strict_validator = SchemaValidator::json().strict();
560 assert!(strict_validator.strict);
561 }
562}