1use serde::{Deserialize, Serialize};
7
8use crate::serve::content::ContentTypeId;
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12pub struct ContentSchema {
13 pub version: String,
15
16 pub content_type: ContentTypeId,
18
19 #[serde(default)]
21 pub fields: Vec<FieldDefinition>,
22
23 #[serde(default)]
25 pub required: Vec<String>,
26
27 #[serde(default)]
29 pub validators: Vec<ValidatorDefinition>,
30}
31
32impl ContentSchema {
33 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 pub fn with_field(mut self, field: FieldDefinition) -> Self {
46 self.fields.push(field);
47 self
48 }
49
50 pub fn with_fields(mut self, fields: Vec<FieldDefinition>) -> Self {
52 self.fields.extend(fields);
53 self
54 }
55
56 pub fn with_required(mut self, field_name: impl Into<String>) -> Self {
58 self.required.push(field_name.into());
59 self
60 }
61
62 pub fn with_validator(mut self, validator: ValidatorDefinition) -> Self {
64 self.validators.push(validator);
65 self
66 }
67
68 pub fn get_field(&self, name: &str) -> Option<&FieldDefinition> {
70 self.fields.iter().find(|f| f.name == name)
71 }
72
73 pub fn is_required(&self, name: &str) -> bool {
75 self.required.contains(&name.to_string())
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct FieldDefinition {
82 pub name: String,
84
85 pub field_type: FieldType,
87
88 #[serde(default)]
90 pub description: Option<String>,
91
92 #[serde(default)]
94 pub default: Option<serde_json::Value>,
95
96 #[serde(default)]
98 pub constraints: Vec<Constraint>,
99
100 #[serde(default)]
102 pub nullable: bool,
103}
104
105impl FieldDefinition {
106 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
120 self.description = Some(description.into());
121 self
122 }
123
124 pub fn with_default(mut self, default: serde_json::Value) -> Self {
126 self.default = Some(default);
127 self
128 }
129
130 pub fn with_constraint(mut self, constraint: Constraint) -> Self {
132 self.constraints.push(constraint);
133 self
134 }
135
136 pub fn nullable(mut self) -> Self {
138 self.nullable = true;
139 self
140 }
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145#[serde(tag = "type", rename_all = "lowercase")]
146pub enum FieldType {
147 String,
149 Integer,
151 Float,
153 Boolean,
155 DateTime,
157 Binary,
159 Array {
161 item_type: Box<Self>,
163 },
164 Object {
166 schema: Box<ContentSchema>,
168 },
169 Reference {
171 content_type: ContentTypeId,
173 },
174}
175
176impl FieldType {
177 pub fn string() -> Self {
179 Self::String
180 }
181
182 pub fn integer() -> Self {
184 Self::Integer
185 }
186
187 pub fn float() -> Self {
189 Self::Float
190 }
191
192 pub fn boolean() -> Self {
194 Self::Boolean
195 }
196
197 pub fn datetime() -> Self {
199 Self::DateTime
200 }
201
202 pub fn binary() -> Self {
204 Self::Binary
205 }
206
207 pub fn array(item_type: Self) -> Self {
209 Self::Array {
210 item_type: Box::new(item_type),
211 }
212 }
213
214 pub fn object(schema: ContentSchema) -> Self {
216 Self::Object {
217 schema: Box::new(schema),
218 }
219 }
220
221 pub fn reference(content_type: ContentTypeId) -> Self {
223 Self::Reference { content_type }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
229#[serde(tag = "kind", rename_all = "snake_case")]
230pub enum Constraint {
231 Min {
233 value: f64,
235 },
236 Max {
238 value: f64,
240 },
241 MinLength {
243 value: usize,
245 },
246 MaxLength {
248 value: usize,
250 },
251 Pattern {
253 pattern: String,
255 },
256 Enum {
258 values: Vec<serde_json::Value>,
260 },
261 Custom {
263 name: String,
265 params: serde_json::Value,
267 },
268}
269
270impl Constraint {
271 pub fn min(value: f64) -> Self {
273 Self::Min { value }
274 }
275
276 pub fn max(value: f64) -> Self {
278 Self::Max { value }
279 }
280
281 pub fn min_length(value: usize) -> Self {
283 Self::MinLength { value }
284 }
285
286 pub fn max_length(value: usize) -> Self {
288 Self::MaxLength { value }
289 }
290
291 pub fn pattern(pattern: impl Into<String>) -> Self {
293 Self::Pattern {
294 pattern: pattern.into(),
295 }
296 }
297
298 pub fn enum_values(values: Vec<serde_json::Value>) -> Self {
300 Self::Enum { values }
301 }
302
303 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
314pub struct ValidatorDefinition {
315 pub validator_type: String,
317
318 pub name: String,
320
321 pub message: String,
323
324 pub check: String,
326}
327
328impl ValidatorDefinition {
329 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 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}