Skip to main content

atom_engine/
components.rs

1use std::collections::HashMap;
2use std::hash::{Hash, Hasher};
3use std::sync::Arc;
4
5use indexmap::IndexMap;
6use serde_json::Value;
7
8use crate::error::Result;
9
10pub fn compute_props_hash(props: &Value) -> u64 {
11    let mut hasher = std::collections::hash_map::DefaultHasher::new();
12    let json_str = serde_json::to_string(props).unwrap_or_default();
13    json_str.hash(&mut hasher);
14    hasher.finish()
15}
16
17pub fn compute_cache_key(path: &str, props_hash: u64) -> u64 {
18    let mut hasher = std::collections::hash_map::DefaultHasher::new();
19    path.hash(&mut hasher);
20    props_hash.hash(&mut hasher);
21    hasher.finish()
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub enum PropType {
26    String,
27    Number,
28    Boolean,
29    Array,
30    Object,
31    Any,
32}
33
34impl PropType {
35    #[allow(clippy::should_implement_trait)]
36    pub fn from_str(s: &str) -> Self {
37        match s.to_lowercase().as_str() {
38            "string" => PropType::String,
39            "number" => PropType::Number,
40            "boolean" => PropType::Boolean,
41            "array" => PropType::Array,
42            "object" => PropType::Object,
43            _ => PropType::Any,
44        }
45    }
46
47    pub fn matches(&self, value: &Value) -> bool {
48        match self {
49            PropType::Any => true,
50            PropType::String => value.is_string(),
51            PropType::Number => value.is_number(),
52            PropType::Boolean => value.is_boolean(),
53            PropType::Array => value.is_array(),
54            PropType::Object => value.is_object(),
55        }
56    }
57}
58
59#[derive(Debug, Clone)]
60pub struct PropDef {
61    pub name: String,
62    pub prop_type: PropType,
63    pub required: bool,
64    pub default: Option<Value>,
65}
66
67impl PropDef {
68    pub fn validate(&self, value: &Value) -> std::result::Result<(), String> {
69        if value.is_null() && self.required {
70            return Err(format!("Required prop '{}' is null", self.name));
71        }
72        if !self.prop_type.matches(value) {
73            return Err(format!(
74                "Prop '{}' type mismatch: expected {:?}, got {}",
75                self.name,
76                self.prop_type,
77                match value {
78                    Value::Null => "null",
79                    Value::Bool(_) => "boolean",
80                    Value::Number(_) => "number",
81                    Value::String(_) => "string",
82                    Value::Array(_) => "array",
83                    Value::Object(_) => "object",
84                }
85            ));
86        }
87        Ok(())
88    }
89}
90
91#[derive(Debug, Clone)]
92pub struct Component {
93    pub path: String,
94    pub props: Vec<PropDef>,
95    pub template: String,
96    pub slots: Vec<String>,
97    pub optional_slots: Vec<String>,
98    pub scoped_slots: Vec<ScopedSlotDef>,
99}
100
101#[derive(Debug, Clone)]
102pub struct ScopedSlotDef {
103    pub name: String,
104    pub props: Vec<String>,
105}
106
107#[derive(Debug, Clone, Default)]
108pub struct SlotData {
109    pub fills: IndexMap<String, String>,
110    pub default: Option<String>,
111    pub scoped_data: HashMap<String, Value>,
112}
113
114#[derive(Clone, Default)]
115pub struct ComponentRenderer {
116    stack_buffers: HashMap<String, Vec<String>>,
117    slot_data: HashMap<String, SlotData>,
118    once_rendered: std::collections::HashSet<u64>,
119}
120
121impl ComponentRenderer {
122    pub fn new() -> Self {
123        ComponentRenderer {
124            stack_buffers: HashMap::new(),
125            slot_data: HashMap::new(),
126            once_rendered: std::collections::HashSet::new(),
127        }
128    }
129
130    pub fn push(&mut self, name: &str, content: String) {
131        self.stack_buffers
132            .entry(name.to_string())
133            .or_default()
134            .push(content);
135    }
136
137    pub fn prepend(&mut self, name: &str, content: String) {
138        self.stack_buffers
139            .entry(name.to_string())
140            .or_default()
141            .insert(0, content);
142    }
143
144    pub fn drain(&mut self, name: &str) -> String {
145        self.stack_buffers
146            .remove(name)
147            .map(|v| v.join("\n"))
148            .unwrap_or_default()
149    }
150
151    pub fn peek(&self, name: &str) -> Option<String> {
152        self.stack_buffers.get(name).map(|v| v.join("\n"))
153    }
154
155    pub fn set_slot_fill(&mut self, slot_name: &str, content: String) {
156        let slot_name = slot_name.trim_start_matches('$').to_string();
157        let slot = self.slot_data.entry(slot_name.clone()).or_default();
158        if slot_name == "default" || slot_name.is_empty() {
159            slot.default = Some(content);
160        } else {
161            slot.fills.insert(slot_name, content);
162        }
163    }
164
165    pub fn get_slot(&self, name: &str) -> Option<String> {
166        let name = name.trim_start_matches('$').to_string();
167        if name == "default" || name.is_empty() {
168            self.slot_data
169                .get("default")
170                .and_then(|s| s.default.clone())
171        } else {
172            self.slot_data
173                .get(&name)
174                .and_then(|s| s.fills.get(&name).cloned())
175        }
176    }
177
178    pub fn has_slot(&self, name: &str) -> bool {
179        let name = name.trim_start_matches('$').to_string();
180        if name == "default" || name.is_empty() {
181            self.slot_data
182                .get("default")
183                .map(|s| s.default.is_some())
184                .unwrap_or(false)
185        } else {
186            self.slot_data
187                .get(&name)
188                .map(|s| s.fills.contains_key(&name))
189                .unwrap_or(false)
190        }
191    }
192
193    pub fn set_scoped_data(&mut self, slot_name: &str, key: &str, value: Value) {
194        let slot_name = slot_name.trim_start_matches('$').to_string();
195        let slot = self.slot_data.entry(slot_name).or_default();
196        slot.scoped_data.insert(key.to_string(), value);
197    }
198
199    pub fn get_scoped_data(&self, slot_name: &str) -> Option<HashMap<String, Value>> {
200        let name = slot_name.trim_start_matches('$').to_string();
201        self.slot_data.get(&name).map(|s| s.scoped_data.clone())
202    }
203
204    pub fn once(&mut self, key: u64) -> bool {
205        if self.once_rendered.contains(&key) {
206            return false;
207        }
208        self.once_rendered.insert(key);
209        true
210    }
211
212    pub fn reset(&mut self) {
213        self.stack_buffers.clear();
214        self.slot_data.clear();
215    }
216}
217
218#[derive(Clone, Default)]
219pub struct ComponentRegistry {
220    components: HashMap<String, Component>,
221    cache: Arc<std::sync::RwLock<ComponentCache>>,
222    cache_enabled: bool,
223}
224
225#[derive(Default)]
226pub struct ComponentCache {
227    entries: HashMap<u64, CachedRender>,
228}
229
230#[derive(Clone)]
231pub struct CachedRender {
232    pub html: String,
233    #[allow(dead_code)]
234    pub props_hash: u64,
235}
236
237impl ComponentCache {
238    pub fn new() -> Self {
239        ComponentCache {
240            entries: HashMap::new(),
241        }
242    }
243
244    pub fn get(&self, key: &u64) -> Option<String> {
245        self.entries.get(key).map(|c| c.html.clone())
246    }
247
248    pub fn insert(&mut self, key: u64, html: String, props_hash: u64) {
249        self.entries.insert(key, CachedRender { html, props_hash });
250    }
251
252    pub fn clear(&mut self) {
253        self.entries.clear();
254    }
255
256    pub fn len(&self) -> usize {
257        self.entries.len()
258    }
259
260    pub fn is_empty(&self) -> bool {
261        self.entries.is_empty()
262    }
263}
264
265impl ComponentRegistry {
266    pub fn new() -> Self {
267        ComponentRegistry {
268            components: HashMap::new(),
269            cache: Arc::new(std::sync::RwLock::new(ComponentCache::new())),
270            cache_enabled: false,
271        }
272    }
273
274    pub fn register(&mut self, path: &str, template: &str) -> Result<()> {
275        let props = Self::parse_props(template);
276        let (slots, optional_slots) = Self::parse_slots(template);
277        let scoped_slots = Self::parse_scoped_slots(template);
278
279        self.components.insert(
280            path.to_string(),
281            Component {
282                path: path.to_string(),
283                props,
284                template: template.to_string(),
285                slots,
286                optional_slots,
287                scoped_slots,
288            },
289        );
290
291        Ok(())
292    }
293
294    pub fn get(&self, path: &str) -> Option<&Component> {
295        self.components.get(path)
296    }
297
298    pub fn resolve_tag(&self, tag: &str) -> Option<String> {
299        if self.components.contains_key(tag) {
300            return Some(tag.to_string());
301        }
302
303        let with_prefix = format!("components/{}", tag);
304        if self.components.contains_key(&with_prefix) {
305            return Some(with_prefix);
306        }
307
308        let with_ext = format!("{}.html", tag);
309        if self.components.contains_key(&with_ext) {
310            return Some(with_ext);
311        }
312
313        let parts: Vec<&str> = tag.split('.').collect();
314        if parts.len() >= 2 {
315            let nested = format!("components/{}.html", parts.join("/"));
316            if self.components.contains_key(&nested) {
317                return Some(nested);
318            }
319        }
320
321        None
322    }
323
324    pub fn list_components(&self) -> Vec<String> {
325        self.components.keys().cloned().collect()
326    }
327
328    pub fn validate_props(
329        &self,
330        path: &str,
331        provided: &Value,
332    ) -> std::result::Result<HashMap<String, Value>, String> {
333        let component = self
334            .components
335            .get(path)
336            .ok_or_else(|| format!("Component '{}' not found", path))?;
337
338        let mut result = HashMap::new();
339        let provided_obj = provided.as_object().ok_or("Props must be an object")?;
340
341        for prop_def in &component.props {
342            if let Some(value) = provided_obj.get(&prop_def.name) {
343                prop_def.validate(value)?;
344                result.insert(prop_def.name.clone(), value.clone());
345            } else if let Some(default) = &prop_def.default {
346                result.insert(prop_def.name.clone(), default.clone());
347            } else if prop_def.required {
348                return Err(format!("Required prop '{}' not provided", prop_def.name));
349            }
350        }
351
352        Ok(result)
353    }
354
355    pub fn enable_cache(&mut self, enabled: bool) {
356        self.cache_enabled = enabled;
357    }
358
359    pub fn is_cache_enabled(&self) -> bool {
360        self.cache_enabled
361    }
362
363    pub fn get_cached(&self, key: u64) -> Option<String> {
364        if !self.cache_enabled {
365            return None;
366        }
367        self.cache.read().ok()?.get(&key)
368    }
369
370    pub fn set_cached(&self, key: u64, html: String, props_hash: u64) {
371        if !self.cache_enabled {
372            return;
373        }
374        if let Ok(mut cache) = self.cache.write() {
375            cache.insert(key, html, props_hash);
376        }
377    }
378
379    pub fn clear_cache(&self) {
380        if let Ok(mut cache) = self.cache.write() {
381            cache.clear();
382        }
383    }
384
385    pub fn cache_len(&self) -> usize {
386        self.cache.read().map(|c| c.len()).unwrap_or(0)
387    }
388
389    fn parse_scoped_slots(template: &str) -> Vec<ScopedSlotDef> {
390        let mut scoped_slots = Vec::new();
391
392        if let Some(start) = template.find("{%-- atom:") {
393            if let Some(end) = template[start..].find("--%}") {
394                let atom_block = &template[start + 11..start + end];
395                if let Some(slots_start) = atom_block.find("@scoped_slots(") {
396                    let slots_end = atom_block[slots_start..]
397                        .find(')')
398                        .map(|i| slots_start + i + 1);
399                    if let Some(end_idx) = slots_end {
400                        let slots_str = &atom_block[slots_start + 14..end_idx];
401                        for part in slots_str.split(',') {
402                            let part = part.trim();
403                            if part.is_empty() {
404                                continue;
405                            }
406                            let parts: Vec<&str> = part.split(':').collect();
407                            let name = parts[0].trim().to_string();
408                            let props = if parts.len() > 1 {
409                                parts[1]
410                                    .trim()
411                                    .split(',')
412                                    .map(|s| s.trim().to_string())
413                                    .collect()
414                            } else {
415                                vec![]
416                            };
417                            scoped_slots.push(ScopedSlotDef { name, props });
418                        }
419                    }
420                }
421            }
422        }
423
424        scoped_slots
425    }
426
427    fn parse_props(template: &str) -> Vec<PropDef> {
428        let mut props = Vec::new();
429
430        if let Some(start) = template.find("{%-- atom:") {
431            if let Some(end) = template[start..].find("--%}") {
432                let atom_block = &template[start + 11..start + end];
433                if let Some(props_start) = atom_block.find("@props(") {
434                    let props_end = atom_block[props_start..]
435                        .find(')')
436                        .map(|i| props_start + i + 1);
437                    if let Some(end_idx) = props_end {
438                        let props_str = &atom_block[props_start + 7..end_idx];
439                        props = Self::parse_props_string(props_str);
440                    }
441                }
442            }
443        }
444
445        props
446    }
447
448    fn parse_props_string(s: &str) -> Vec<PropDef> {
449        let mut props = Vec::new();
450        let s = s.trim();
451        let s = s.trim_start_matches('{').trim_end_matches('}');
452
453        for part in s.split(',') {
454            let part = part.trim();
455            if part.is_empty() {
456                continue;
457            }
458
459            let parts: Vec<&str> = part.split(':').collect();
460            if parts.is_empty() {
461                continue;
462            }
463
464            let mut name = parts[0].trim().to_string();
465            let prop_type = if parts.len() > 1 {
466                PropType::from_str(parts[1].trim())
467            } else {
468                PropType::Any
469            };
470
471            let optional = name.ends_with('?');
472            if optional {
473                name = name.trim_end_matches('?').to_string();
474            }
475
476            let (required, default) = if let Some(eq_pos) = name.find('=') {
477                let default_str = &name[eq_pos + 1..];
478                let default = serde_json::Value::from(default_str.trim());
479                (false, Some(default))
480            } else {
481                (true, None)
482            };
483
484            name = name.split('=').next().unwrap_or(&name).trim().to_string();
485
486            let required = required && !optional;
487
488            props.push(PropDef {
489                name,
490                prop_type,
491                required,
492                default,
493            });
494        }
495
496        props
497    }
498
499    fn parse_slots(template: &str) -> (Vec<String>, Vec<String>) {
500        let mut slots = Vec::new();
501        let mut optional_slots = Vec::new();
502
503        let mut search = template;
504        while let Some(start) = search.find("slot_") {
505            search = &search[start + 5..];
506            if let Some(end) = search.find("()") {
507                let raw_name = &search[..end];
508                if raw_name.is_empty() {
509                    continue;
510                }
511                let is_optional = raw_name.ends_with('?');
512                let name = if is_optional {
513                    raw_name.trim_end_matches('?').to_string()
514                } else {
515                    raw_name.to_string()
516                };
517
518                if is_optional {
519                    if !optional_slots.contains(&name) {
520                        optional_slots.push(name);
521                    }
522                } else if !slots.contains(&name) {
523                    slots.push(name);
524                }
525            }
526        }
527
528        (slots, optional_slots)
529    }
530}