1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4pub mod ffi;
5
6pub type JSON = serde_json::Value;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum ConfigLevel {
12 Global,
13 Model { name: String },
14 Field { model: String, field: String },
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PathSegment {
20 pub kind: String,
21 pub name: String,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum Severity {
28 Error,
29 Warning,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ValidationError {
35 pub path: Vec<PathSegment>,
36 pub message: String,
37 pub severity: Severity,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct OutputFile {
43 pub path: String,
44 pub content: String,
45}
46
47#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50pub enum CaseFormat {
51 Snake,
52 Camel,
53 Pascal,
54 Kebab,
55 Constant,
56 Title,
57}
58
59pub struct Utils;
61
62impl Utils {
63 pub fn change_case(&self, input: &str, format: CaseFormat) -> String {
64 match format {
65 CaseFormat::Snake => to_snake_case(input),
66 CaseFormat::Camel => to_camel_case(input),
67 CaseFormat::Pascal => to_pascal_case(input),
68 CaseFormat::Kebab => to_kebab_case(input),
69 CaseFormat::Constant => to_constant_case(input),
70 CaseFormat::Title => to_title_case(input),
71 }
72 }
73}
74
75fn to_snake_case(s: &str) -> String {
77 let mut result = String::new();
78 let mut prev_is_upper = false;
79
80 for (i, ch) in s.chars().enumerate() {
81 if ch.is_uppercase() {
82 if i > 0 && !prev_is_upper {
83 result.push('_');
84 }
85 result.push(ch.to_lowercase().next().unwrap());
86 prev_is_upper = true;
87 } else {
88 result.push(ch);
89 prev_is_upper = false;
90 }
91 }
92
93 result
94}
95
96fn to_camel_case(s: &str) -> String {
97 let mut result = String::new();
98 let mut capitalize_next = false;
99
100 for (i, ch) in s.chars().enumerate() {
101 if ch == '_' || ch == '-' || ch == ' ' {
102 capitalize_next = true;
103 } else if i == 0 {
104 result.push(ch.to_lowercase().next().unwrap());
105 } else if capitalize_next {
106 result.push(ch.to_uppercase().next().unwrap());
107 capitalize_next = false;
108 } else {
109 result.push(ch);
110 }
111 }
112
113 result
114}
115
116fn to_pascal_case(s: &str) -> String {
117 let mut result = String::new();
118 let mut capitalize_next = true;
119
120 for ch in s.chars() {
121 if ch == '_' || ch == '-' || ch == ' ' {
122 capitalize_next = true;
123 } else if capitalize_next {
124 result.push(ch.to_uppercase().next().unwrap());
125 capitalize_next = false;
126 } else {
127 result.push(ch);
128 }
129 }
130
131 result
132}
133
134fn to_kebab_case(s: &str) -> String {
135 to_snake_case(s).replace('_', "-")
136}
137
138fn to_constant_case(s: &str) -> String {
139 to_snake_case(s).to_uppercase()
140}
141
142fn to_title_case(s: &str) -> String {
143 s.split('_')
144 .map(|word| {
145 let mut chars = word.chars();
146 match chars.next() {
147 None => String::new(),
148 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
149 }
150 })
151 .collect::<Vec<_>>()
152 .join(" ")
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct Schema {
159 pub models: HashMap<String, ModelDefinition>,
160 pub type_aliases: HashMap<String, TypeAliasDefinition>,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct ModelDefinition {
165 pub name: String,
166 pub parents: Vec<String>,
167 pub fields: Vec<FieldDefinition>,
168 pub config: JSON,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub entity_id: Option<u64>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct FieldDefinition {
175 pub name: String,
176 pub field_type: TypeExpression,
177 pub optional: bool,
178 pub default: Option<Value>,
179 pub config: JSON,
180 #[serde(skip_serializing_if = "Option::is_none")]
181 pub entity_id: Option<u64>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct TypeAliasDefinition {
186 pub name: String,
187 pub alias_type: TypeExpression,
188 pub config: JSON,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub entity_id: Option<u64>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(tag = "type", rename_all = "snake_case")]
195pub enum TypeExpression {
196 Identifier { name: String },
197 Array { element_type: Box<TypeExpression> },
198 Union { types: Vec<TypeExpression> },
199 StringLiteral { value: String },
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(untagged)]
204pub enum Value {
205 String(String),
206 Number(f64),
207 Boolean(bool),
208 Null,
209}
210
211impl From<&serde_json::Value> for Value {
212 fn from(json: &serde_json::Value) -> Self {
213 match json {
214 serde_json::Value::String(s) => Value::String(s.clone()),
215 serde_json::Value::Number(n) => Value::Number(n.as_f64().unwrap_or(0.0)),
216 serde_json::Value::Bool(b) => Value::Boolean(*b),
217 serde_json::Value::Null => Value::Null,
218 _ => Value::Null,
220 }
221 }
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(tag = "type", rename_all = "snake_case")]
228pub enum Delta {
229 ModelAdded {
231 name: String,
232 after: ModelDefinition,
233 },
234 ModelRemoved {
235 name: String,
236 before: ModelDefinition,
237 },
238 ModelRenamed {
239 old_name: String,
240 new_name: String,
241 #[serde(skip_serializing_if = "Option::is_none")]
242 id: Option<u64>,
243 before: ModelDefinition,
244 after: ModelDefinition,
245 },
246
247 FieldAdded {
249 model: String,
250 field: String,
251 after: FieldDefinition,
252 },
253 FieldRemoved {
254 model: String,
255 field: String,
256 before: FieldDefinition,
257 },
258 FieldRenamed {
259 model: String,
260 old_name: String,
261 new_name: String,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 id: Option<u64>,
264 before: FieldDefinition,
265 after: FieldDefinition,
266 },
267 FieldTypeChanged {
268 model: String,
269 field: String,
270 before: TypeExpression,
271 after: TypeExpression,
272 },
273 FieldOptionalityChanged {
274 model: String,
275 field: String,
276 before: bool,
277 after: bool,
278 },
279 FieldDefaultChanged {
280 model: String,
281 field: String,
282 before: Option<Value>,
283 after: Option<Value>,
284 },
285
286 TypeAliasAdded {
288 name: String,
289 after: TypeAliasDefinition,
290 },
291 TypeAliasRemoved {
292 name: String,
293 before: TypeAliasDefinition,
294 },
295 TypeAliasRenamed {
296 old_name: String,
297 new_name: String,
298 #[serde(skip_serializing_if = "Option::is_none")]
299 id: Option<u64>,
300 before: TypeAliasDefinition,
301 after: TypeAliasDefinition,
302 },
303 TypeAliasTypeChanged {
304 name: String,
305 before: TypeExpression,
306 after: TypeExpression,
307 },
308
309 InheritanceAdded {
311 model: String,
312 parent: String,
313 },
314 InheritanceRemoved {
315 model: String,
316 parent: String,
317 },
318
319 GlobalConfigChanged {
321 before: JSON,
322 after: JSON,
323 },
324 ModelConfigChanged {
325 model: String,
326 before: JSON,
327 after: JSON,
328 },
329 FieldConfigChanged {
330 model: String,
331 field: String,
332 before: JSON,
333 after: JSON,
334 },
335}
336
337pub use export_plugin_impl as export_plugin;
340
341pub fn export_plugin_impl(_attr: &str, _item: &str) -> String {
344 String::new()
346}
347
348#[macro_export]
375macro_rules! schema_from_file {
376 ($path:expr) => {
377 pub fn __cdm_schema_content() -> String {
378 include_str!($path).to_string()
379 }
380 $crate::export_schema!(__cdm_schema_content);
381 };
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387
388 #[test]
390 fn test_to_snake_case() {
391 assert_eq!(to_snake_case("HelloWorld"), "hello_world");
392 assert_eq!(to_snake_case("helloWorld"), "hello_world");
393 assert_eq!(to_snake_case("hello"), "hello");
394 assert_eq!(to_snake_case("HELLO"), "hello"); assert_eq!(to_snake_case(""), "");
396 assert_eq!(to_snake_case("ID"), "id"); assert_eq!(to_snake_case("MyHTTPServer"), "my_httpserver"); }
399
400 #[test]
401 fn test_to_camel_case() {
402 assert_eq!(to_camel_case("hello_world"), "helloWorld");
403 assert_eq!(to_camel_case("hello-world"), "helloWorld");
404 assert_eq!(to_camel_case("hello world"), "helloWorld");
405 assert_eq!(to_camel_case("hello"), "hello");
406 assert_eq!(to_camel_case("HelloWorld"), "helloWorld");
407 assert_eq!(to_camel_case(""), "");
408 assert_eq!(to_camel_case("one_two_three"), "oneTwoThree");
409 }
410
411 #[test]
412 fn test_to_pascal_case() {
413 assert_eq!(to_pascal_case("hello_world"), "HelloWorld");
414 assert_eq!(to_pascal_case("hello-world"), "HelloWorld");
415 assert_eq!(to_pascal_case("hello world"), "HelloWorld");
416 assert_eq!(to_pascal_case("hello"), "Hello");
417 assert_eq!(to_pascal_case("HelloWorld"), "HelloWorld");
418 assert_eq!(to_pascal_case(""), "");
419 assert_eq!(to_pascal_case("one_two_three"), "OneTwoThree");
420 }
421
422 #[test]
423 fn test_to_kebab_case() {
424 assert_eq!(to_kebab_case("HelloWorld"), "hello-world");
425 assert_eq!(to_kebab_case("helloWorld"), "hello-world");
426 assert_eq!(to_kebab_case("hello"), "hello");
427 assert_eq!(to_kebab_case(""), "");
428 }
429
430 #[test]
431 fn test_to_constant_case() {
432 assert_eq!(to_constant_case("HelloWorld"), "HELLO_WORLD");
433 assert_eq!(to_constant_case("helloWorld"), "HELLO_WORLD");
434 assert_eq!(to_constant_case("hello"), "HELLO");
435 assert_eq!(to_constant_case(""), "");
436 }
437
438 #[test]
439 fn test_to_title_case() {
440 assert_eq!(to_title_case("hello_world"), "Hello World");
441 assert_eq!(to_title_case("hello"), "Hello");
442 assert_eq!(to_title_case("one_two_three"), "One Two Three");
443 assert_eq!(to_title_case(""), "");
444 }
445
446 #[test]
447 fn test_utils_change_case() {
448 let utils = Utils;
449
450 assert_eq!(utils.change_case("HelloWorld", CaseFormat::Snake), "hello_world");
451 assert_eq!(utils.change_case("hello_world", CaseFormat::Camel), "helloWorld");
452 assert_eq!(utils.change_case("hello_world", CaseFormat::Pascal), "HelloWorld");
453 assert_eq!(utils.change_case("HelloWorld", CaseFormat::Kebab), "hello-world");
454 assert_eq!(utils.change_case("HelloWorld", CaseFormat::Constant), "HELLO_WORLD");
455 assert_eq!(utils.change_case("hello_world", CaseFormat::Title), "Hello World");
456 }
457
458 #[test]
460 fn test_config_level_serialization() {
461 let global = ConfigLevel::Global;
463 let json = serde_json::to_string(&global).unwrap();
464 assert!(json.contains("\"type\":\"global\""));
465
466 let deserialized: ConfigLevel = serde_json::from_str(&json).unwrap();
467 assert!(matches!(deserialized, ConfigLevel::Global));
468
469 let model = ConfigLevel::Model { name: "User".to_string() };
471 let json = serde_json::to_string(&model).unwrap();
472 assert!(json.contains("\"type\":\"model\""));
473 assert!(json.contains("\"name\":\"User\""));
474
475 let field = ConfigLevel::Field {
477 model: "User".to_string(),
478 field: "id".to_string()
479 };
480 let json = serde_json::to_string(&field).unwrap();
481 assert!(json.contains("\"type\":\"field\""));
482 assert!(json.contains("\"model\":\"User\""));
483 assert!(json.contains("\"field\":\"id\""));
484 }
485
486 #[test]
487 fn test_severity_serialization() {
488 let error = Severity::Error;
489 let json = serde_json::to_string(&error).unwrap();
490 assert_eq!(json, "\"error\"");
491
492 let warning = Severity::Warning;
493 let json = serde_json::to_string(&warning).unwrap();
494 assert_eq!(json, "\"warning\"");
495 }
496
497 #[test]
498 fn test_validation_error_serialization() {
499 let error = ValidationError {
500 path: vec![
501 PathSegment {
502 kind: "field".to_string(),
503 name: "email".to_string(),
504 },
505 ],
506 message: "Invalid email format".to_string(),
507 severity: Severity::Error,
508 };
509
510 let json = serde_json::to_string(&error).unwrap();
511 let deserialized: ValidationError = serde_json::from_str(&json).unwrap();
512
513 assert_eq!(deserialized.path.len(), 1);
514 assert_eq!(deserialized.path[0].kind, "field");
515 assert_eq!(deserialized.path[0].name, "email");
516 assert_eq!(deserialized.message, "Invalid email format");
517 assert_eq!(deserialized.severity, Severity::Error);
518 }
519
520 #[test]
521 fn test_output_file_serialization() {
522 let file = OutputFile {
523 path: "output.txt".to_string(),
524 content: "Hello, world!".to_string(),
525 };
526
527 let json = serde_json::to_string(&file).unwrap();
528 let deserialized: OutputFile = serde_json::from_str(&json).unwrap();
529
530 assert_eq!(deserialized.path, "output.txt");
531 assert_eq!(deserialized.content, "Hello, world!");
532 }
533
534 #[test]
535 fn test_type_expression_serialization() {
536 let identifier = TypeExpression::Identifier {
538 name: "string".to_string()
539 };
540 let json = serde_json::to_string(&identifier).unwrap();
541 assert!(json.contains("\"type\":\"identifier\""));
542 assert!(json.contains("\"name\":\"string\""));
543
544 let array = TypeExpression::Array {
546 element_type: Box::new(TypeExpression::Identifier {
547 name: "number".to_string()
548 })
549 };
550 let json = serde_json::to_string(&array).unwrap();
551 assert!(json.contains("\"type\":\"array\""));
552
553 let union = TypeExpression::Union {
555 types: vec![
556 TypeExpression::Identifier { name: "string".to_string() },
557 TypeExpression::Identifier { name: "number".to_string() },
558 ]
559 };
560 let json = serde_json::to_string(&union).unwrap();
561 assert!(json.contains("\"type\":\"union\""));
562
563 let literal = TypeExpression::StringLiteral {
565 value: "active".to_string()
566 };
567 let json = serde_json::to_string(&literal).unwrap();
568 assert!(json.contains("\"type\":\"string_literal\""));
569 }
570
571 #[test]
572 fn test_value_serialization() {
573 let string_val = Value::String("test".to_string());
575 let json = serde_json::to_string(&string_val).unwrap();
576 assert_eq!(json, "\"test\"");
577
578 let number_val = Value::Number(42.5);
580 let json = serde_json::to_string(&number_val).unwrap();
581 assert_eq!(json, "42.5");
582
583 let bool_val = Value::Boolean(true);
585 let json = serde_json::to_string(&bool_val).unwrap();
586 assert_eq!(json, "true");
587
588 let null_val = Value::Null;
590 let json = serde_json::to_string(&null_val).unwrap();
591 assert_eq!(json, "null");
592 }
593
594 #[test]
595 fn test_delta_model_added_serialization() {
596 let delta = Delta::ModelAdded {
597 name: "User".to_string(),
598 after: ModelDefinition {
599 name: "User".to_string(),
600 parents: vec![],
601 fields: vec![],
602 config: serde_json::json!({}),
603 entity_id: None,
604 },
605 };
606
607 let json = serde_json::to_string(&delta).unwrap();
608 assert!(json.contains("\"type\":\"model_added\""));
609 assert!(json.contains("\"name\":\"User\""));
610
611 let deserialized: Delta = serde_json::from_str(&json).unwrap();
612 assert!(matches!(deserialized, Delta::ModelAdded { .. }));
613 }
614
615 #[test]
616 fn test_delta_field_added_serialization() {
617 let delta = Delta::FieldAdded {
618 model: "User".to_string(),
619 field: "email".to_string(),
620 after: FieldDefinition {
621 name: "email".to_string(),
622 field_type: TypeExpression::Identifier {
623 name: "string".to_string(),
624 },
625 optional: false,
626 default: None,
627 config: serde_json::json!({}),
628 entity_id: None,
629 },
630 };
631
632 let json = serde_json::to_string(&delta).unwrap();
633 assert!(json.contains("\"type\":\"field_added\""));
634 assert!(json.contains("\"model\":\"User\""));
635 assert!(json.contains("\"field\":\"email\""));
636
637 let deserialized: Delta = serde_json::from_str(&json).unwrap();
638 assert!(matches!(deserialized, Delta::FieldAdded { .. }));
639 }
640
641 #[test]
642 fn test_schema_serialization() {
643 let mut models = HashMap::new();
644 models.insert(
645 "User".to_string(),
646 ModelDefinition {
647 name: "User".to_string(),
648 parents: vec![],
649 fields: vec![
650 FieldDefinition {
651 name: "id".to_string(),
652 field_type: TypeExpression::Identifier {
653 name: "number".to_string(),
654 },
655 optional: false,
656 default: None,
657 config: serde_json::json!({}),
658 entity_id: None,
659 },
660 ],
661 config: serde_json::json!({}),
662 entity_id: None,
663 },
664 );
665
666 let schema = Schema {
667 models,
668 type_aliases: HashMap::new(),
669 };
670
671 let json = serde_json::to_string(&schema).unwrap();
672 let deserialized: Schema = serde_json::from_str(&json).unwrap();
673
674 assert_eq!(deserialized.models.len(), 1);
675 assert!(deserialized.models.contains_key("User"));
676 assert_eq!(deserialized.type_aliases.len(), 0);
677 }
678
679 #[test]
680 fn test_case_format_serialization() {
681 assert_eq!(
682 serde_json::to_string(&CaseFormat::Snake).unwrap(),
683 "\"snake\""
684 );
685 assert_eq!(
686 serde_json::to_string(&CaseFormat::Camel).unwrap(),
687 "\"camel\""
688 );
689 assert_eq!(
690 serde_json::to_string(&CaseFormat::Pascal).unwrap(),
691 "\"pascal\""
692 );
693 assert_eq!(
694 serde_json::to_string(&CaseFormat::Kebab).unwrap(),
695 "\"kebab\""
696 );
697 assert_eq!(
698 serde_json::to_string(&CaseFormat::Constant).unwrap(),
699 "\"constant\""
700 );
701 assert_eq!(
702 serde_json::to_string(&CaseFormat::Title).unwrap(),
703 "\"title\""
704 );
705 }
706}