1use serde::{Deserialize, Serialize};
7use smallvec::SmallVec;
8use std::collections::HashMap;
9
10use crate::DomainError;
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct SchemaId(String);
15
16impl SchemaId {
17 pub fn new(id: impl Into<String>) -> Self {
31 Self(id.into())
32 }
33
34 pub fn as_str(&self) -> &str {
36 &self.0
37 }
38}
39
40impl std::fmt::Display for SchemaId {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{}", self.0)
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub enum Schema {
76 String {
78 min_length: Option<usize>,
80 max_length: Option<usize>,
82 pattern: Option<String>,
84 allowed_values: Option<SmallVec<[String; 8]>>,
86 },
87
88 Integer {
90 minimum: Option<i64>,
92 maximum: Option<i64>,
94 },
95
96 Number {
98 minimum: Option<f64>,
100 maximum: Option<f64>,
102 },
103
104 Boolean,
106
107 Null,
109
110 Array {
112 items: Option<Box<Schema>>,
114 min_items: Option<usize>,
116 max_items: Option<usize>,
118 unique_items: bool,
120 },
121
122 Object {
124 properties: HashMap<String, Schema>,
126 required: Vec<String>,
128 additional_properties: bool,
130 },
131
132 OneOf {
134 schemas: SmallVec<[Box<Schema>; 4]>,
136 },
137
138 AllOf {
140 schemas: SmallVec<[Box<Schema>; 4]>,
142 },
143
144 Any,
146}
147
148pub type SchemaValidationResult<T> = Result<T, SchemaValidationError>;
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]
161pub enum SchemaValidationError {
162 #[error("Type mismatch at '{path}': expected {expected}, got {actual}")]
164 TypeMismatch {
165 path: String,
167 expected: String,
169 actual: String,
171 },
172
173 #[error("Missing required field at '{path}': {field}")]
175 MissingRequired {
176 path: String,
178 field: String,
180 },
181
182 #[error("Value out of range at '{path}': {value} not in [{min}, {max}]")]
184 OutOfRange {
185 path: String,
187 value: String,
189 min: String,
191 max: String,
193 },
194
195 #[error("String length constraint at '{path}': length {actual} not in [{min}, {max}]")]
197 StringLengthConstraint {
198 path: String,
200 actual: usize,
202 min: usize,
204 max: usize,
206 },
207
208 #[error("Pattern mismatch at '{path}': value '{value}' does not match pattern '{pattern}'")]
210 PatternMismatch {
211 path: String,
213 value: String,
215 pattern: String,
217 },
218
219 #[error("Array size constraint at '{path}': size {actual} not in [{min}, {max}]")]
221 ArraySizeConstraint {
222 path: String,
224 actual: usize,
226 min: usize,
228 max: usize,
230 },
231
232 #[error("Unique items constraint at '{path}': duplicate items found")]
234 DuplicateItems {
235 path: String,
237 },
238
239 #[error("Invalid enum value at '{path}': '{value}' not in allowed values")]
241 InvalidEnumValue {
242 path: String,
244 value: String,
246 },
247
248 #[error("Additional property not allowed at '{path}': '{property}'")]
250 AdditionalPropertyNotAllowed {
251 path: String,
253 property: String,
255 },
256
257 #[error("No matching schema in OneOf at '{path}'")]
259 NoMatchingOneOf {
260 path: String,
262 },
263
264 #[error("Not all schemas match in AllOf at '{path}': {failures}")]
266 AllOfFailure {
267 path: String,
269 failures: String,
271 },
272}
273
274impl Schema {
275 pub fn allows_type(&self, schema_type: SchemaType) -> bool {
285 match (self, schema_type) {
286 (Self::String { .. }, SchemaType::String) => true,
287 (Self::Integer { .. }, SchemaType::Integer) => true,
288 (Self::Number { .. }, SchemaType::Number) => true,
289 (Self::Boolean, SchemaType::Boolean) => true,
290 (Self::Null, SchemaType::Null) => true,
291 (Self::Array { .. }, SchemaType::Array) => true,
292 (Self::Object { .. }, SchemaType::Object) => true,
293 (Self::Any, _) => true,
294 (Self::OneOf { schemas }, schema_type) => {
295 schemas.iter().any(|s| s.allows_type(schema_type))
296 }
297 (Self::AllOf { schemas }, schema_type) => {
298 schemas.iter().all(|s| s.allows_type(schema_type))
299 }
300 _ => false,
301 }
302 }
303
304 pub fn validation_cost(&self) -> usize {
312 match self {
313 Self::Null | Self::Boolean | Self::Any => 1,
314 Self::Integer { .. } | Self::Number { .. } => 5,
315 Self::String {
316 pattern: Some(_), ..
317 } => 50, Self::String { .. } => 10,
319 Self::Array { items, .. } => {
320 let item_cost = items.as_ref().map_or(1, |s| s.validation_cost());
321 10 + item_cost
322 }
323 Self::Object { properties, .. } => {
324 let prop_cost: usize = properties.values().map(|s| s.validation_cost()).sum();
325 20 + prop_cost
326 }
327 Self::OneOf { schemas } => {
328 let max_cost = schemas
329 .iter()
330 .map(|s| s.validation_cost())
331 .max()
332 .unwrap_or(0);
333 30 + max_cost * schemas.len()
334 }
335 Self::AllOf { schemas } => {
336 let total_cost: usize = schemas.iter().map(|s| s.validation_cost()).sum();
337 20 + total_cost
338 }
339 }
340 }
341
342 pub fn string(min_length: Option<usize>, max_length: Option<usize>) -> Self {
344 Self::String {
345 min_length,
346 max_length,
347 pattern: None,
348 allowed_values: None,
349 }
350 }
351
352 pub fn integer(minimum: Option<i64>, maximum: Option<i64>) -> Self {
354 Self::Integer { minimum, maximum }
355 }
356
357 pub fn number(minimum: Option<f64>, maximum: Option<f64>) -> Self {
359 Self::Number { minimum, maximum }
360 }
361
362 pub fn array(items: Option<Schema>) -> Self {
364 Self::Array {
365 items: items.map(Box::new),
366 min_items: None,
367 max_items: None,
368 unique_items: false,
369 }
370 }
371
372 pub fn object(properties: HashMap<String, Schema>, required: Vec<String>) -> Self {
374 Self::Object {
375 properties,
376 required,
377 additional_properties: true,
378 }
379 }
380}
381
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
384pub enum SchemaType {
385 String,
387 Integer,
389 Number,
391 Boolean,
393 Null,
395 Array,
397 Object,
399}
400
401impl From<&Schema> for SchemaType {
402 fn from(schema: &Schema) -> Self {
403 match schema {
404 Schema::String { .. } => Self::String,
405 Schema::Integer { .. } => Self::Integer,
406 Schema::Number { .. } => Self::Number,
407 Schema::Boolean => Self::Boolean,
408 Schema::Null => Self::Null,
409 Schema::Array { .. } => Self::Array,
410 Schema::Object { .. } => Self::Object,
411 Schema::Any => Self::Object, Schema::OneOf { .. } | Schema::AllOf { .. } => Self::Object,
413 }
414 }
415}
416
417impl From<DomainError> for SchemaValidationError {
418 fn from(error: DomainError) -> Self {
419 match error {
420 DomainError::ValidationError(msg) => Self::TypeMismatch {
421 path: "/".to_string(),
422 expected: "valid".to_string(),
423 actual: msg,
424 },
425 _ => Self::TypeMismatch {
426 path: "/".to_string(),
427 expected: "valid".to_string(),
428 actual: error.to_string(),
429 },
430 }
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_schema_id_creation() {
440 let id = SchemaId::new("test-schema-v1");
441 assert_eq!(id.as_str(), "test-schema-v1");
442 assert_eq!(id.to_string(), "test-schema-v1");
443 }
444
445 #[test]
446 fn test_schema_allows_type() {
447 let string_schema = Schema::string(Some(1), Some(100));
448 assert!(string_schema.allows_type(SchemaType::String));
449 assert!(!string_schema.allows_type(SchemaType::Integer));
450
451 let any_schema = Schema::Any;
452 assert!(any_schema.allows_type(SchemaType::String));
453 assert!(any_schema.allows_type(SchemaType::Integer));
454 }
455
456 #[test]
457 fn test_validation_cost() {
458 let simple = Schema::Boolean;
459 assert_eq!(simple.validation_cost(), 1);
460
461 let complex = Schema::Object {
462 properties: [
463 ("id".to_string(), Schema::integer(None, None)),
464 ("name".to_string(), Schema::string(Some(1), Some(100))),
465 ]
466 .into_iter()
467 .collect(),
468 required: vec!["id".to_string()],
469 additional_properties: false,
470 };
471 assert!(complex.validation_cost() > 20);
472 }
473
474 #[test]
475 fn test_schema_builders() {
476 let str_schema = Schema::string(Some(1), Some(100));
477 assert!(matches!(str_schema, Schema::String { .. }));
478
479 let int_schema = Schema::integer(Some(0), Some(100));
480 assert!(matches!(int_schema, Schema::Integer { .. }));
481
482 let arr_schema = Schema::array(Some(Schema::integer(None, None)));
483 assert!(matches!(arr_schema, Schema::Array { .. }));
484 }
485}