dampen_core/codegen/
config.rs

1//! Configuration for code generation behavior
2//!
3//! This module provides configuration structures for controlling how
4//! Dampen generates Rust code from XML UI definitions.
5
6use std::path::PathBuf;
7
8/// Configuration for code generation behavior
9#[derive(Debug, Clone)]
10pub struct CodegenConfig {
11    /// Output directory for generated code
12    pub output_dir: PathBuf,
13
14    /// Whether to format generated code with prettyplease
15    pub format_output: bool,
16
17    /// Whether to validate generated code syntax
18    pub validate_syntax: bool,
19
20    /// Model type name (e.g., "MyModel")
21    pub model_type: String,
22
23    /// Message enum name (e.g., "Message")
24    pub message_type: String,
25
26    /// Optional persistence configuration
27    pub persistence: Option<PersistenceConfig>,
28}
29
30/// Configuration for window state persistence
31#[derive(Debug, Clone)]
32pub struct PersistenceConfig {
33    /// Application identifier for persistence (e.g., "my-app")
34    pub app_name: String,
35}
36
37impl CodegenConfig {
38    /// Create a new CodegenConfig with the given output directory
39    ///
40    /// # Arguments
41    /// * `output_dir` - Directory where generated code will be written
42    ///
43    /// # Returns
44    /// A new CodegenConfig with default settings
45    pub fn new(output_dir: PathBuf) -> Self {
46        Self {
47            output_dir,
48            format_output: true,
49            validate_syntax: true,
50            model_type: "Model".to_string(),
51            message_type: "Message".to_string(),
52            persistence: None,
53        }
54    }
55
56    /// Enable window state persistence with the given app name
57    pub fn with_persistence(mut self, app_name: impl Into<String>) -> Self {
58        self.persistence = Some(PersistenceConfig {
59            app_name: app_name.into(),
60        });
61        self
62    }
63
64    /// Set the model type name
65    pub fn with_model_type(mut self, model_type: impl Into<String>) -> Self {
66        self.model_type = model_type.into();
67        self
68    }
69
70    /// Set the message type name
71    pub fn with_message_type(mut self, message_type: impl Into<String>) -> Self {
72        self.message_type = message_type.into();
73        self
74    }
75
76    /// Enable or disable code formatting
77    pub fn with_formatting(mut self, format_output: bool) -> Self {
78        self.format_output = format_output;
79        self
80    }
81
82    /// Enable or disable syntax validation
83    pub fn with_validation(mut self, validate_syntax: bool) -> Self {
84        self.validate_syntax = validate_syntax;
85        self
86    }
87
88    /// Validate the configuration
89    ///
90    /// # Returns
91    /// Ok if configuration is valid, Err with message otherwise
92    pub fn validate(&self) -> Result<(), String> {
93        // Check if output_dir path is valid (we can't check writability at compile time)
94        if self.output_dir.as_os_str().is_empty() {
95            return Err("Output directory cannot be empty".to_string());
96        }
97
98        // Validate model type is a valid Rust identifier
99        if !is_valid_identifier(&self.model_type) {
100            return Err(format!(
101                "Model type '{}' is not a valid Rust identifier",
102                self.model_type
103            ));
104        }
105
106        // Validate message type is a valid Rust identifier
107        if !is_valid_identifier(&self.message_type) {
108            return Err(format!(
109                "Message type '{}' is not a valid Rust identifier",
110                self.message_type
111            ));
112        }
113
114        Ok(())
115    }
116}
117
118impl Default for CodegenConfig {
119    fn default() -> Self {
120        Self::new(PathBuf::from("target/generated"))
121    }
122}
123
124impl PersistenceConfig {
125    /// Create a new PersistenceConfig with the given app name
126    pub fn new(app_name: impl Into<String>) -> Self {
127        Self {
128            app_name: app_name.into(),
129        }
130    }
131}
132
133/// Check if a string is a valid Rust identifier
134///
135/// Valid identifiers must:
136/// - Start with uppercase letter (for types)
137/// - Contain only alphanumeric characters and underscores
138fn is_valid_identifier(s: &str) -> bool {
139    if s.is_empty() {
140        return false;
141    }
142
143    let mut chars = s.chars();
144
145    // First character must be uppercase letter
146    if let Some(first) = chars.next() {
147        if !first.is_uppercase() {
148            return false;
149        }
150    } else {
151        return false;
152    }
153
154    // Remaining characters must be alphanumeric or underscore
155    chars.all(|c| c.is_alphanumeric() || c == '_')
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_valid_identifiers() {
164        assert!(is_valid_identifier("Model"));
165        assert!(is_valid_identifier("MyModel"));
166        assert!(is_valid_identifier("Model123"));
167        assert!(is_valid_identifier("My_Model"));
168        assert!(is_valid_identifier("M"));
169    }
170
171    #[test]
172    fn test_invalid_identifiers() {
173        assert!(!is_valid_identifier(""));
174        assert!(!is_valid_identifier("model")); // lowercase
175        assert!(!is_valid_identifier("123Model")); // starts with number
176        assert!(!is_valid_identifier("My-Model")); // contains hyphen
177        assert!(!is_valid_identifier("My Model")); // contains space
178        assert!(!is_valid_identifier("_Model")); // starts with underscore
179    }
180
181    #[test]
182    fn test_config_validation() {
183        let config = CodegenConfig::default();
184        assert!(config.validate().is_ok());
185
186        let config = CodegenConfig::new(PathBuf::from("")).with_model_type("Model");
187        assert!(config.validate().is_err());
188
189        let config = CodegenConfig::new(PathBuf::from("target")).with_model_type("invalid");
190        assert!(config.validate().is_err());
191
192        let config = CodegenConfig::new(PathBuf::from("target")).with_message_type("invalid");
193        assert!(config.validate().is_err());
194    }
195}