Skip to main content

alimentar/serve/
schema.rs

1//! Content Schema - Schema definitions for content validation and UI generation
2//!
3//! Provides a flexible schema system for defining content structure,
4//! validation rules, and UI hints.
5
6use serde::{Deserialize, Serialize};
7
8use crate::serve::content::ContentTypeId;
9
10/// Schema definition for content validation and UI generation
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ContentSchema {
13    /// Schema version for compatibility checking
14    pub version: String,
15
16    /// Content type identifier
17    pub content_type: ContentTypeId,
18
19    /// Field definitions with types and constraints
20    #[serde(default)]
21    pub fields: Vec<FieldDefinition>,
22
23    /// Required fields for validation
24    #[serde(default)]
25    pub required: Vec<String>,
26
27    /// Custom validators (regex patterns, range checks, etc.)
28    #[serde(default)]
29    pub validators: Vec<ValidatorDefinition>,
30}
31
32impl ContentSchema {
33    /// Create a new schema
34    pub fn new(content_type: ContentTypeId, version: impl Into<String>) -> Self {
35        Self {
36            version: version.into(),
37            content_type,
38            fields: Vec::new(),
39            required: Vec::new(),
40            validators: Vec::new(),
41        }
42    }
43
44    /// Add a field definition
45    pub fn with_field(mut self, field: FieldDefinition) -> Self {
46        self.fields.push(field);
47        self
48    }
49
50    /// Add multiple field definitions
51    pub fn with_fields(mut self, fields: Vec<FieldDefinition>) -> Self {
52        self.fields.extend(fields);
53        self
54    }
55
56    /// Mark a field as required
57    pub fn with_required(mut self, field_name: impl Into<String>) -> Self {
58        self.required.push(field_name.into());
59        self
60    }
61
62    /// Add a validator
63    pub fn with_validator(mut self, validator: ValidatorDefinition) -> Self {
64        self.validators.push(validator);
65        self
66    }
67
68    /// Get a field by name
69    pub fn get_field(&self, name: &str) -> Option<&FieldDefinition> {
70        self.fields.iter().find(|f| f.name == name)
71    }
72
73    /// Check if a field is required
74    pub fn is_required(&self, name: &str) -> bool {
75        self.required.contains(&name.to_string())
76    }
77}
78
79/// Field definition with type and constraints
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct FieldDefinition {
82    /// Field name
83    pub name: String,
84
85    /// Field type
86    pub field_type: FieldType,
87
88    /// Human-readable description
89    #[serde(default)]
90    pub description: Option<String>,
91
92    /// Default value (as JSON)
93    #[serde(default)]
94    pub default: Option<serde_json::Value>,
95
96    /// Validation constraints
97    #[serde(default)]
98    pub constraints: Vec<Constraint>,
99
100    /// Whether this field is nullable
101    #[serde(default)]
102    pub nullable: bool,
103}
104
105impl FieldDefinition {
106    /// Create a new field definition
107    pub fn new(name: impl Into<String>, field_type: FieldType) -> Self {
108        Self {
109            name: name.into(),
110            field_type,
111            description: None,
112            default: None,
113            constraints: Vec::new(),
114            nullable: false,
115        }
116    }
117
118    /// Add a description
119    pub fn with_description(mut self, description: impl Into<String>) -> Self {
120        self.description = Some(description.into());
121        self
122    }
123
124    /// Set default value
125    pub fn with_default(mut self, default: serde_json::Value) -> Self {
126        self.default = Some(default);
127        self
128    }
129
130    /// Add a constraint
131    pub fn with_constraint(mut self, constraint: Constraint) -> Self {
132        self.constraints.push(constraint);
133        self
134    }
135
136    /// Set nullable
137    pub fn nullable(mut self) -> Self {
138        self.nullable = true;
139        self
140    }
141}
142
143/// Field type enumeration
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145#[serde(tag = "type", rename_all = "lowercase")]
146pub enum FieldType {
147    /// String type
148    String,
149    /// Integer type
150    Integer,
151    /// Floating point type
152    Float,
153    /// Boolean type
154    Boolean,
155    /// Date/time type
156    DateTime,
157    /// Binary data type
158    Binary,
159    /// Array type with item type
160    Array {
161        /// Type of array items
162        item_type: Box<Self>,
163    },
164    /// Object type with nested schema
165    Object {
166        /// Nested schema
167        schema: Box<ContentSchema>,
168    },
169    /// Reference to another content type
170    Reference {
171        /// Referenced content type
172        content_type: ContentTypeId,
173    },
174}
175
176impl FieldType {
177    /// Create a string type
178    pub fn string() -> Self {
179        Self::String
180    }
181
182    /// Create an integer type
183    pub fn integer() -> Self {
184        Self::Integer
185    }
186
187    /// Create a float type
188    pub fn float() -> Self {
189        Self::Float
190    }
191
192    /// Create a boolean type
193    pub fn boolean() -> Self {
194        Self::Boolean
195    }
196
197    /// Create a datetime type
198    pub fn datetime() -> Self {
199        Self::DateTime
200    }
201
202    /// Create a binary type
203    pub fn binary() -> Self {
204        Self::Binary
205    }
206
207    /// Create an array type
208    pub fn array(item_type: Self) -> Self {
209        Self::Array {
210            item_type: Box::new(item_type),
211        }
212    }
213
214    /// Create an object type
215    pub fn object(schema: ContentSchema) -> Self {
216        Self::Object {
217            schema: Box::new(schema),
218        }
219    }
220
221    /// Create a reference type
222    pub fn reference(content_type: ContentTypeId) -> Self {
223        Self::Reference { content_type }
224    }
225}
226
227/// Constraint definition for field validation
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229#[serde(tag = "kind", rename_all = "snake_case")]
230pub enum Constraint {
231    /// Minimum value (for numbers)
232    Min {
233        /// Minimum value
234        value: f64,
235    },
236    /// Maximum value (for numbers)
237    Max {
238        /// Maximum value
239        value: f64,
240    },
241    /// Minimum length (for strings/arrays)
242    MinLength {
243        /// Minimum length
244        value: usize,
245    },
246    /// Maximum length (for strings/arrays)
247    MaxLength {
248        /// Maximum length
249        value: usize,
250    },
251    /// Regex pattern (for strings)
252    Pattern {
253        /// Regex pattern
254        pattern: String,
255    },
256    /// Enum of allowed values
257    Enum {
258        /// Allowed values
259        values: Vec<serde_json::Value>,
260    },
261    /// Custom constraint
262    Custom {
263        /// Constraint name
264        name: String,
265        /// Constraint parameters
266        params: serde_json::Value,
267    },
268}
269
270impl Constraint {
271    /// Create a minimum value constraint
272    pub fn min(value: f64) -> Self {
273        Self::Min { value }
274    }
275
276    /// Create a maximum value constraint
277    pub fn max(value: f64) -> Self {
278        Self::Max { value }
279    }
280
281    /// Create a minimum length constraint
282    pub fn min_length(value: usize) -> Self {
283        Self::MinLength { value }
284    }
285
286    /// Create a maximum length constraint
287    pub fn max_length(value: usize) -> Self {
288        Self::MaxLength { value }
289    }
290
291    /// Create a pattern constraint
292    pub fn pattern(pattern: impl Into<String>) -> Self {
293        Self::Pattern {
294            pattern: pattern.into(),
295        }
296    }
297
298    /// Create an enum constraint
299    pub fn enum_values(values: Vec<serde_json::Value>) -> Self {
300        Self::Enum { values }
301    }
302
303    /// Create a custom constraint
304    pub fn custom(name: impl Into<String>, params: serde_json::Value) -> Self {
305        Self::Custom {
306            name: name.into(),
307            params,
308        }
309    }
310}
311
312/// Validator definition for custom validation logic
313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314pub struct ValidatorDefinition {
315    /// Validator type (e.g., "custom", "regex", "range")
316    pub validator_type: String,
317
318    /// Validator name for identification
319    pub name: String,
320
321    /// Error message on validation failure
322    pub message: String,
323
324    /// Validation expression or configuration
325    pub check: String,
326}
327
328impl ValidatorDefinition {
329    /// Create a new validator definition
330    pub fn new(
331        validator_type: impl Into<String>,
332        name: impl Into<String>,
333        message: impl Into<String>,
334        check: impl Into<String>,
335    ) -> Self {
336        Self {
337            validator_type: validator_type.into(),
338            name: name.into(),
339            message: message.into(),
340            check: check.into(),
341        }
342    }
343
344    /// Create a custom validator
345    pub fn custom(
346        name: impl Into<String>,
347        message: impl Into<String>,
348        check: impl Into<String>,
349    ) -> Self {
350        Self::new("custom", name, message, check)
351    }
352}
353
354#[cfg(test)]
355#[allow(clippy::float_cmp, clippy::unwrap_used)]
356mod tests {
357    use super::*;
358
359    #[test]
360    fn test_schema_creation() {
361        let schema = ContentSchema::new(ContentTypeId::dataset(), "1.0")
362            .with_field(FieldDefinition::new("name", FieldType::String))
363            .with_field(FieldDefinition::new("age", FieldType::Integer))
364            .with_required("name");
365
366        assert_eq!(schema.version, "1.0");
367        assert_eq!(schema.fields.len(), 2);
368        assert!(schema.is_required("name"));
369        assert!(!schema.is_required("age"));
370    }
371
372    #[test]
373    fn test_field_definition() {
374        let field = FieldDefinition::new("email", FieldType::String)
375            .with_description("User email address")
376            .with_constraint(Constraint::pattern(r"^[\w.-]+@[\w.-]+\.\w+$"))
377            .with_constraint(Constraint::max_length(255));
378
379        assert_eq!(field.name, "email");
380        assert_eq!(field.constraints.len(), 2);
381    }
382
383    #[test]
384    fn test_field_types() {
385        assert_eq!(FieldType::string(), FieldType::String);
386        assert_eq!(FieldType::integer(), FieldType::Integer);
387
388        let array_type = FieldType::array(FieldType::String);
389        match array_type {
390            FieldType::Array { item_type } => {
391                assert_eq!(*item_type, FieldType::String);
392            }
393            _ => panic!("Expected Array type"),
394        }
395    }
396
397    #[test]
398    fn test_constraints() {
399        let min = Constraint::min(0.0);
400        let max = Constraint::max(100.0);
401        let pattern = Constraint::pattern(r"^\d+$");
402        let enum_vals =
403            Constraint::enum_values(vec![serde_json::json!("a"), serde_json::json!("b")]);
404
405        assert!(matches!(min, Constraint::Min { value } if value == 0.0));
406        assert!(matches!(max, Constraint::Max { value } if value == 100.0));
407        assert!(matches!(pattern, Constraint::Pattern { .. }));
408        assert!(matches!(enum_vals, Constraint::Enum { .. }));
409    }
410
411    #[test]
412    fn test_validator_definition() {
413        let validator = ValidatorDefinition::custom(
414            "valid_email",
415            "Email must be valid",
416            "email matches /^[\\w.-]+@[\\w.-]+\\.\\w+$/",
417        );
418
419        assert_eq!(validator.validator_type, "custom");
420        assert_eq!(validator.name, "valid_email");
421    }
422
423    #[test]
424    fn test_nested_schema() {
425        let address_schema = ContentSchema::new(ContentTypeId::new("address"), "1.0")
426            .with_field(FieldDefinition::new("street", FieldType::String))
427            .with_field(FieldDefinition::new("city", FieldType::String));
428
429        let user_schema = ContentSchema::new(ContentTypeId::new("user"), "1.0")
430            .with_field(FieldDefinition::new("name", FieldType::String))
431            .with_field(FieldDefinition::new(
432                "address",
433                FieldType::object(address_schema),
434            ));
435
436        assert_eq!(user_schema.fields.len(), 2);
437        match &user_schema.fields[1].field_type {
438            FieldType::Object { schema } => {
439                assert_eq!(schema.fields.len(), 2);
440            }
441            _ => panic!("Expected Object type"),
442        }
443    }
444
445    #[test]
446    fn test_get_field() {
447        let schema = ContentSchema::new(ContentTypeId::dataset(), "1.0")
448            .with_field(FieldDefinition::new("id", FieldType::Integer))
449            .with_field(FieldDefinition::new("name", FieldType::String));
450
451        assert!(schema.get_field("id").is_some());
452        assert!(schema.get_field("name").is_some());
453        assert!(schema.get_field("nonexistent").is_none());
454    }
455
456    #[test]
457    fn test_schema_with_fields() {
458        let fields = vec![
459            FieldDefinition::new("a", FieldType::String),
460            FieldDefinition::new("b", FieldType::Integer),
461        ];
462        let schema = ContentSchema::new(ContentTypeId::dataset(), "1.0").with_fields(fields);
463        assert_eq!(schema.fields.len(), 2);
464    }
465
466    #[test]
467    fn test_schema_with_validator() {
468        let validator = ValidatorDefinition::new("custom", "test", "must be valid", "true");
469        let schema = ContentSchema::new(ContentTypeId::dataset(), "1.0").with_validator(validator);
470        assert_eq!(schema.validators.len(), 1);
471    }
472
473    #[test]
474    fn test_field_with_default() {
475        let field =
476            FieldDefinition::new("count", FieldType::Integer).with_default(serde_json::json!(0));
477        assert_eq!(field.default, Some(serde_json::json!(0)));
478    }
479
480    #[test]
481    fn test_field_nullable() {
482        let field = FieldDefinition::new("optional", FieldType::String).nullable();
483        assert!(field.nullable);
484    }
485
486    #[test]
487    fn test_field_type_boolean() {
488        assert_eq!(FieldType::boolean(), FieldType::Boolean);
489    }
490
491    #[test]
492    fn test_field_type_float() {
493        assert_eq!(FieldType::float(), FieldType::Float);
494    }
495
496    #[test]
497    fn test_field_type_datetime() {
498        assert_eq!(FieldType::datetime(), FieldType::DateTime);
499    }
500
501    #[test]
502    fn test_field_type_binary() {
503        assert_eq!(FieldType::binary(), FieldType::Binary);
504    }
505
506    #[test]
507    fn test_constraint_min_length() {
508        let c = Constraint::min_length(5);
509        assert!(matches!(c, Constraint::MinLength { value } if value == 5));
510    }
511
512    #[test]
513    fn test_validator_new() {
514        let v = ValidatorDefinition::new("regex", "email_check", "invalid email", r"^.+@.+$");
515        assert_eq!(v.validator_type, "regex");
516        assert_eq!(v.name, "email_check");
517        assert_eq!(v.message, "invalid email");
518    }
519
520    #[test]
521    fn test_field_type_reference() {
522        let ref_type = FieldType::reference(ContentTypeId::dataset());
523        match ref_type {
524            FieldType::Reference { content_type } => {
525                assert_eq!(content_type.as_str(), "alimentar.dataset");
526            }
527            _ => panic!("Expected Reference type"),
528        }
529    }
530
531    #[test]
532    fn test_constraint_custom() {
533        let c = Constraint::custom("unique", serde_json::json!({"scope": "global"}));
534        match c {
535            Constraint::Custom { name, params } => {
536                assert_eq!(name, "unique");
537                assert!(params.get("scope").is_some());
538            }
539            _ => panic!("Expected Custom constraint"),
540        }
541    }
542
543    #[test]
544    fn test_schema_serialization() {
545        let schema = ContentSchema::new(ContentTypeId::dataset(), "1.0")
546            .with_field(FieldDefinition::new("id", FieldType::Integer));
547
548        let json = serde_json::to_string(&schema);
549        assert!(json.is_ok());
550
551        let parsed: Result<ContentSchema, _> =
552            serde_json::from_str(&json.ok().unwrap_or_else(|| panic!("Should serialize")));
553        assert!(parsed.is_ok());
554    }
555}