Skip to main content

cdm_plugin_interface/
lib.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4pub mod ffi;
5
6pub type JSON = serde_json::Value;
7
8/// Configuration level for validation
9#[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/// Path segment for error reporting
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PathSegment {
20    pub kind: String,
21    pub name: String,
22}
23
24/// Error severity
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26#[serde(rename_all = "lowercase")]
27pub enum Severity {
28    Error,
29    Warning,
30}
31
32/// Validation error with structured path
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ValidationError {
35    pub path: Vec<PathSegment>,
36    pub message: String,
37    pub severity: Severity,
38}
39
40/// Output file from build or migrate
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct OutputFile {
43    pub path: String,
44    pub content: String,
45}
46
47/// Case format for string conversion
48#[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
59/// Utility functions provided by CDM runtime
60pub 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
75// Simple implementations for case conversion
76fn 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/// Schema types
156
157#[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            // Arrays and objects are not supported - convert to Null
219            _ => Value::Null,
220        }
221    }
222}
223
224/// Delta types for migrations
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(tag = "type", rename_all = "snake_case")]
228pub enum Delta {
229    // Models
230    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    // Fields
248    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    // Type Aliases
287    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    // Inheritance
310    InheritanceAdded {
311        model: String,
312        parent: String,
313    },
314    InheritanceRemoved {
315        model: String,
316        parent: String,
317    },
318
319    // Config Changes
320    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
337/// Export macro placeholder - in a real implementation, this would be a proc macro
338/// For now, we'll just use it as a marker
339pub use export_plugin_impl as export_plugin;
340
341/// Placeholder for the actual proc macro
342/// In a real implementation, this would be in a separate proc-macro crate
343pub fn export_plugin_impl(_attr: &str, _item: &str) -> String {
344    // This is a placeholder - actual implementation would generate WASM exports
345    String::new()
346}
347
348/// Helper macro to embed a schema.cdm file and export it via the WASM `_schema()` function.
349///
350/// This is a Rust-specific convenience for plugin development. It uses `include_str!()`
351/// to embed the schema file at compile time, making the WASM binary self-contained.
352///
353/// # Example
354///
355/// ```rust,ignore
356/// use cdm_plugin_api::schema_from_file;
357///
358/// // Embeds ../schema.cdm and creates the _schema() export
359/// schema_from_file!("../schema.cdm");
360/// ```
361///
362/// # How it works
363///
364/// The macro expands to:
365/// - Load the file contents at compile time using `include_str!()`
366/// - Create a function that returns the embedded schema string
367/// - Use the `export_schema!` macro to wrap it with proper FFI handling
368///
369/// # Note
370///
371/// This is optional - plugins can implement `_schema()` however they want.
372/// This macro is just a convenience for Rust plugins that want to keep
373/// their schema in a separate `.cdm` file.
374#[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    // Case conversion tests
389    #[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");  // All uppercase becomes lowercase without underscores between consecutive uppers
395        assert_eq!(to_snake_case(""), "");
396        assert_eq!(to_snake_case("ID"), "id");  // Consecutive uppercase letters don't get underscores between them
397        assert_eq!(to_snake_case("MyHTTPServer"), "my_httpserver");  // Consecutive uppers treated as one block
398    }
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    // Serialization tests
459    #[test]
460    fn test_config_level_serialization() {
461        // Global level
462        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        // Model level
470        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        // Field level
476        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        // Identifier
537        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        // Array
545        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        // Union
554        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        // String literal
564        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        // String
574        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        // Number
579        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        // Boolean
584        let bool_val = Value::Boolean(true);
585        let json = serde_json::to_string(&bool_val).unwrap();
586        assert_eq!(json, "true");
587
588        // Null
589        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}