ricecoder_generation/templates/
resolver.rs

1//! Placeholder resolution and case transformation
2
3use crate::templates::error::TemplateError;
4use std::collections::{HashMap, HashSet};
5
6/// Represents a case transformation for placeholder values
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum CaseTransform {
9    /// PascalCase (e.g., MyProject)
10    PascalCase,
11    /// camelCase (e.g., myProject)
12    CamelCase,
13    /// snake_case (e.g., my_project)
14    SnakeCase,
15    /// kebab-case (e.g., my-project)
16    KebabCase,
17    /// UPPERCASE (e.g., MY_PROJECT)
18    UpperCase,
19    /// lowercase (e.g., myproject)
20    LowerCase,
21}
22
23impl CaseTransform {
24    /// Apply case transformation to a string
25    pub fn apply(&self, input: &str) -> String {
26        use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToSnakeCase};
27
28        match self {
29            CaseTransform::PascalCase => input.to_pascal_case(),
30            CaseTransform::CamelCase => input.to_lower_camel_case(),
31            CaseTransform::SnakeCase => input.to_snake_case(),
32            CaseTransform::KebabCase => input.to_kebab_case(),
33            CaseTransform::UpperCase => input.to_uppercase(),
34            CaseTransform::LowerCase => input.to_lowercase(),
35        }
36    }
37}
38
39/// Represents a placeholder in a template
40#[derive(Debug, Clone)]
41pub struct Placeholder {
42    /// The base name of the placeholder (without case suffix)
43    pub name: String,
44    /// The case transformation to apply
45    pub case_transform: CaseTransform,
46    /// Whether this placeholder is required
47    pub required: bool,
48    /// Default value if not provided
49    pub default: Option<String>,
50}
51
52impl Placeholder {
53    /// Create a new placeholder with the given name and case transform
54    pub fn new(name: String, case_transform: CaseTransform) -> Self {
55        Self {
56            name,
57            case_transform,
58            required: true,
59            default: None,
60        }
61    }
62
63    /// Set whether this placeholder is required
64    pub fn with_required(mut self, required: bool) -> Self {
65        self.required = required;
66        self
67    }
68
69    /// Set a default value for this placeholder
70    pub fn with_default(mut self, default: String) -> Self {
71        self.default = Some(default);
72        self.required = false;
73        self
74    }
75}
76
77/// Resolves placeholders in templates with case transformations
78pub struct PlaceholderResolver {
79    /// Mapping of placeholder names to their values
80    values: HashMap<String, String>,
81    /// Set of required placeholders
82    required: HashSet<String>,
83    /// Maximum depth for nested placeholder resolution
84    max_depth: usize,
85}
86
87impl PlaceholderResolver {
88    /// Create a new placeholder resolver
89    pub fn new() -> Self {
90        Self {
91            values: HashMap::new(),
92            required: HashSet::new(),
93            max_depth: 10,
94        }
95    }
96
97    /// Set the maximum depth for nested placeholder resolution
98    pub fn with_max_depth(mut self, max_depth: usize) -> Self {
99        self.max_depth = max_depth;
100        self
101    }
102
103    /// Add a value for a placeholder
104    pub fn add_value(&mut self, name: impl Into<String>, value: impl Into<String>) {
105        self.values.insert(name.into(), value.into());
106    }
107
108    /// Add multiple values at once
109    pub fn add_values(&mut self, values: HashMap<String, String>) {
110        self.values.extend(values);
111    }
112
113    /// Mark a placeholder as required
114    pub fn require(&mut self, name: impl Into<String>) {
115        self.required.insert(name.into());
116    }
117
118    /// Resolve a placeholder with the given case transformation
119    pub fn resolve(
120        &self,
121        name: &str,
122        case_transform: CaseTransform,
123    ) -> Result<String, TemplateError> {
124        let value = self
125            .values
126            .get(name)
127            .ok_or_else(|| TemplateError::MissingPlaceholder(name.to_string()))?;
128
129        Ok(case_transform.apply(value))
130    }
131
132    /// Resolve a placeholder with optional default value
133    pub fn resolve_with_default(
134        &self,
135        name: &str,
136        case_transform: CaseTransform,
137        default: Option<&str>,
138    ) -> Result<String, TemplateError> {
139        match self.values.get(name) {
140            Some(value) => Ok(case_transform.apply(value)),
141            None => {
142                if let Some(default_val) = default {
143                    Ok(case_transform.apply(default_val))
144                } else {
145                    Err(TemplateError::MissingPlaceholder(name.to_string()))
146                }
147            }
148        }
149    }
150
151    /// Validate that all required placeholders are provided
152    pub fn validate(&self) -> Result<(), TemplateError> {
153        for required_name in &self.required {
154            if !self.values.contains_key(required_name) {
155                return Err(TemplateError::MissingPlaceholder(required_name.clone()));
156            }
157        }
158        Ok(())
159    }
160
161    /// Get all placeholder names that have been provided
162    pub fn provided_names(&self) -> Vec<String> {
163        self.values.keys().cloned().collect()
164    }
165
166    /// Check if a placeholder value is provided
167    pub fn has_value(&self, name: &str) -> bool {
168        self.values.contains_key(name)
169    }
170
171    /// Extract placeholder names from a template string
172    /// Returns a vector of placeholder names found in the template
173    pub fn extract_placeholder_names(&self, template: &str) -> Vec<String> {
174        let mut names = Vec::new();
175        let mut start = 0;
176
177        while let Some(pos) = template[start..].find("{{") {
178            let start_pos = start + pos;
179            if let Some(end_pos) = template[start_pos..].find("}}") {
180                let end_pos = start_pos + end_pos;
181                let content = &template[start_pos + 2..end_pos];
182
183                // Parse the placeholder to extract the name
184                if let Ok((name, _)) = self.parse_placeholder_syntax(content) {
185                    names.push(name);
186                }
187
188                start = end_pos + 2;
189            } else {
190                break;
191            }
192        }
193
194        names
195    }
196
197    /// Resolve a placeholder with nested resolution support
198    /// Supports values that reference other placeholders (e.g., "{{other_name}}")
199    pub fn resolve_nested(
200        &self,
201        name: &str,
202        case_transform: CaseTransform,
203    ) -> Result<String, TemplateError> {
204        self.resolve_nested_internal(name, case_transform, 0, &mut HashSet::new())
205    }
206
207    /// Internal recursive implementation for nested resolution with circular reference detection
208    fn resolve_nested_internal(
209        &self,
210        name: &str,
211        case_transform: CaseTransform,
212        depth: usize,
213        visited: &mut HashSet<String>,
214    ) -> Result<String, TemplateError> {
215        // Check depth limit first
216        if depth >= self.max_depth {
217            return Err(TemplateError::RenderError(format!(
218                "Maximum nesting depth ({}) exceeded for placeholder: {}",
219                self.max_depth, name
220            )));
221        }
222
223        // Check for circular references
224        if visited.contains(name) {
225            return Err(TemplateError::RenderError(format!(
226                "Circular reference detected for placeholder: {}",
227                name
228            )));
229        }
230
231        let value = self
232            .values
233            .get(name)
234            .ok_or_else(|| TemplateError::MissingPlaceholder(name.to_string()))?;
235
236        // Check if value contains nested placeholders
237        if value.contains("{{") && value.contains("}}") {
238            visited.insert(name.to_string());
239            let resolved = self.resolve_nested_value(value, depth + 1, visited)?;
240            visited.remove(name);
241            Ok(case_transform.apply(&resolved))
242        } else {
243            Ok(case_transform.apply(value))
244        }
245    }
246
247    /// Resolve nested placeholders within a value string
248    fn resolve_nested_value(
249        &self,
250        value: &str,
251        depth: usize,
252        visited: &mut HashSet<String>,
253    ) -> Result<String, TemplateError> {
254        let mut result = value.to_string();
255        let mut changed = true;
256
257        // Keep resolving until no more placeholders are found
258        while changed && depth <= self.max_depth {
259            changed = false;
260
261            // Find and replace placeholders
262            if let Some(start) = result.find("{{") {
263                if let Some(end) = result[start..].find("}}") {
264                    let end = start + end;
265                    let placeholder_content = &result[start + 2..end];
266
267                    // Extract placeholder name and case transform
268                    let (placeholder_name, case_transform) =
269                        self.parse_placeholder_syntax(placeholder_content)?;
270
271                    // Resolve the nested placeholder
272                    let resolved = self.resolve_nested_internal(
273                        &placeholder_name,
274                        case_transform,
275                        depth + 1,
276                        visited,
277                    )?;
278
279                    // Replace the placeholder with its resolved value
280                    result.replace_range(start..=end, &resolved);
281                    changed = true;
282                }
283            }
284        }
285
286        Ok(result)
287    }
288
289    /// Parse placeholder syntax to extract name and case transform
290    /// Supports formats like: "name", "Name", "NAME", "name_snake", "name-kebab", "nameCamel"
291    fn parse_placeholder_syntax(
292        &self,
293        content: &str,
294    ) -> Result<(String, CaseTransform), TemplateError> {
295        let content = content.trim();
296
297        // Determine case transform based on suffix
298        if content.ends_with("_snake") {
299            let name = content.trim_end_matches("_snake").to_string();
300            Ok((name, CaseTransform::SnakeCase))
301        } else if content.ends_with("-kebab") {
302            let name = content.trim_end_matches("-kebab").to_string();
303            Ok((name, CaseTransform::KebabCase))
304        } else if content.ends_with("Camel") {
305            let name = content.trim_end_matches("Camel").to_string();
306            Ok((name, CaseTransform::CamelCase))
307        } else if content.chars().all(|c| c.is_uppercase() || c == '_') && content.len() > 1 {
308            // All uppercase = UPPERCASE transform
309            Ok((content.to_string(), CaseTransform::UpperCase))
310        } else if content.chars().next().is_some_and(|c| c.is_uppercase()) {
311            // Starts with uppercase = PascalCase
312            Ok((content.to_string(), CaseTransform::PascalCase))
313        } else {
314            // Default to lowercase
315            Ok((content.to_string(), CaseTransform::LowerCase))
316        }
317    }
318}
319
320impl Default for PlaceholderResolver {
321    fn default() -> Self {
322        Self::new()
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_case_transform_pascal_case() {
332        let result = CaseTransform::PascalCase.apply("my_project");
333        assert_eq!(result, "MyProject");
334    }
335
336    #[test]
337    fn test_case_transform_camel_case() {
338        let result = CaseTransform::CamelCase.apply("my_project");
339        assert_eq!(result, "myProject");
340    }
341
342    #[test]
343    fn test_case_transform_snake_case() {
344        let result = CaseTransform::SnakeCase.apply("MyProject");
345        assert_eq!(result, "my_project");
346    }
347
348    #[test]
349    fn test_case_transform_kebab_case() {
350        let result = CaseTransform::KebabCase.apply("MyProject");
351        assert_eq!(result, "my-project");
352    }
353
354    #[test]
355    fn test_case_transform_upper_case() {
356        let result = CaseTransform::UpperCase.apply("my_project");
357        assert_eq!(result, "MY_PROJECT");
358    }
359
360    #[test]
361    fn test_case_transform_lower_case() {
362        let result = CaseTransform::LowerCase.apply("MyProject");
363        assert_eq!(result, "myproject");
364    }
365
366    #[test]
367    fn test_placeholder_resolver_add_value() {
368        let mut resolver = PlaceholderResolver::new();
369        resolver.add_value("name", "my_project");
370        assert!(resolver.has_value("name"));
371    }
372
373    #[test]
374    fn test_placeholder_resolver_resolve() {
375        let mut resolver = PlaceholderResolver::new();
376        resolver.add_value("name", "my_project");
377        let result = resolver.resolve("name", CaseTransform::PascalCase);
378        assert_eq!(result.unwrap(), "MyProject");
379    }
380
381    #[test]
382    fn test_placeholder_resolver_missing_placeholder() {
383        let resolver = PlaceholderResolver::new();
384        let result = resolver.resolve("name", CaseTransform::PascalCase);
385        assert!(result.is_err());
386    }
387
388    #[test]
389    fn test_placeholder_resolver_validate_required() {
390        let mut resolver = PlaceholderResolver::new();
391        resolver.require("name");
392        let result = resolver.validate();
393        assert!(result.is_err());
394    }
395
396    #[test]
397    fn test_placeholder_resolver_validate_success() {
398        let mut resolver = PlaceholderResolver::new();
399        resolver.add_value("name", "my_project");
400        resolver.require("name");
401        let result = resolver.validate();
402        assert!(result.is_ok());
403    }
404
405    #[test]
406    fn test_placeholder_resolver_with_default() {
407        let resolver = PlaceholderResolver::new();
408        let result =
409            resolver.resolve_with_default("name", CaseTransform::PascalCase, Some("default_value"));
410        assert_eq!(result.unwrap(), "DefaultValue");
411    }
412
413    #[test]
414    fn test_placeholder_resolver_add_multiple_values() {
415        let mut resolver = PlaceholderResolver::new();
416        let mut values = HashMap::new();
417        values.insert("name".to_string(), "my_project".to_string());
418        values.insert("author".to_string(), "john_doe".to_string());
419        resolver.add_values(values);
420        assert!(resolver.has_value("name"));
421        assert!(resolver.has_value("author"));
422    }
423
424    #[test]
425    fn test_case_transform_with_numbers() {
426        let result = CaseTransform::PascalCase.apply("my_project_2");
427        assert_eq!(result, "MyProject2");
428    }
429
430    #[test]
431    fn test_case_transform_with_acronyms() {
432        let result = CaseTransform::SnakeCase.apply("HTTPServer");
433        assert_eq!(result, "http_server");
434    }
435
436    #[test]
437    fn test_case_transform_single_word() {
438        let result = CaseTransform::PascalCase.apply("project");
439        assert_eq!(result, "Project");
440    }
441
442    #[test]
443    fn test_case_transform_already_pascal() {
444        let result = CaseTransform::PascalCase.apply("MyProject");
445        assert_eq!(result, "MyProject");
446    }
447
448    #[test]
449    fn test_case_transform_kebab_to_pascal() {
450        let result = CaseTransform::PascalCase.apply("my-project");
451        assert_eq!(result, "MyProject");
452    }
453
454    #[test]
455    fn test_case_transform_empty_string() {
456        let result = CaseTransform::PascalCase.apply("");
457        assert_eq!(result, "");
458    }
459
460    #[test]
461    fn test_nested_placeholder_resolution() {
462        let mut resolver = PlaceholderResolver::new();
463        resolver.add_value("base", "my_project");
464        resolver.add_value("full_name", "{{base}}_extended");
465        let result = resolver.resolve_nested("full_name", CaseTransform::SnakeCase);
466        assert_eq!(result.unwrap(), "my_project_extended");
467    }
468
469    #[test]
470    fn test_nested_placeholder_with_case_transform() {
471        let mut resolver = PlaceholderResolver::new();
472        resolver.add_value("base", "my_project");
473        resolver.add_value("full_name", "{{base}}_extended");
474        let result = resolver.resolve_nested("full_name", CaseTransform::PascalCase);
475        assert_eq!(result.unwrap(), "MyProjectExtended");
476    }
477
478    #[test]
479    fn test_circular_reference_detection() {
480        let mut resolver = PlaceholderResolver::new();
481        resolver.add_value("a", "{{b}}");
482        resolver.add_value("b", "{{a}}");
483        let result = resolver.resolve_nested("a", CaseTransform::LowerCase);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_self_reference_detection() {
489        let mut resolver = PlaceholderResolver::new();
490        resolver.add_value("a", "{{a}}");
491        let result = resolver.resolve_nested("a", CaseTransform::LowerCase);
492        assert!(result.is_err());
493    }
494
495    #[test]
496    fn test_max_depth_exceeded() {
497        let mut resolver = PlaceholderResolver::new().with_max_depth(2);
498        resolver.add_value("a", "{{b}}");
499        resolver.add_value("b", "{{c}}");
500        resolver.add_value("c", "{{d}}");
501        resolver.add_value("d", "value");
502        let result = resolver.resolve_nested("a", CaseTransform::LowerCase);
503        assert!(result.is_err());
504    }
505
506    #[test]
507    fn test_multiple_nested_placeholders() {
508        let mut resolver = PlaceholderResolver::new();
509        resolver.add_value("first", "hello");
510        resolver.add_value("second", "world");
511        resolver.add_value("combined", "{{first}}_{{second}}");
512        let result = resolver.resolve_nested("combined", CaseTransform::SnakeCase);
513        assert_eq!(result.unwrap(), "hello_world");
514    }
515
516    #[test]
517    fn test_parse_placeholder_syntax_pascal_case() {
518        let resolver = PlaceholderResolver::new();
519        let (name, transform) = resolver.parse_placeholder_syntax("Name").unwrap();
520        assert_eq!(name, "Name");
521        assert_eq!(transform, CaseTransform::PascalCase);
522    }
523
524    #[test]
525    fn test_parse_placeholder_syntax_snake_case() {
526        let resolver = PlaceholderResolver::new();
527        let (name, transform) = resolver.parse_placeholder_syntax("name_snake").unwrap();
528        assert_eq!(name, "name");
529        assert_eq!(transform, CaseTransform::SnakeCase);
530    }
531
532    #[test]
533    fn test_parse_placeholder_syntax_kebab_case() {
534        let resolver = PlaceholderResolver::new();
535        let (name, transform) = resolver.parse_placeholder_syntax("name-kebab").unwrap();
536        assert_eq!(name, "name");
537        assert_eq!(transform, CaseTransform::KebabCase);
538    }
539
540    #[test]
541    fn test_parse_placeholder_syntax_camel_case() {
542        let resolver = PlaceholderResolver::new();
543        let (name, transform) = resolver.parse_placeholder_syntax("nameCamel").unwrap();
544        assert_eq!(name, "name");
545        assert_eq!(transform, CaseTransform::CamelCase);
546    }
547
548    #[test]
549    fn test_parse_placeholder_syntax_uppercase() {
550        let resolver = PlaceholderResolver::new();
551        let (name, transform) = resolver.parse_placeholder_syntax("NAME").unwrap();
552        assert_eq!(name, "NAME");
553        assert_eq!(transform, CaseTransform::UpperCase);
554    }
555
556    #[test]
557    fn test_parse_placeholder_syntax_lowercase() {
558        let resolver = PlaceholderResolver::new();
559        let (name, transform) = resolver.parse_placeholder_syntax("name").unwrap();
560        assert_eq!(name, "name");
561        assert_eq!(transform, CaseTransform::LowerCase);
562    }
563}