hypen_engine/reactive/
binding.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
5pub struct Binding {
6 pub path: Vec<String>,
8}
9
10impl Binding {
11 pub fn new(path: Vec<String>) -> Self {
12 Self { path }
13 }
14
15 pub fn root_key(&self) -> Option<&str> {
17 self.path.first().map(|s| s.as_str())
18 }
19
20 pub fn full_path(&self) -> String {
22 self.path.join(".")
23 }
24}
25
26pub fn parse_binding(s: &str) -> Option<Binding> {
28 let trimmed = s.trim();
29
30 if !trimmed.starts_with("${") || !trimmed.ends_with('}') {
32 return None;
33 }
34
35 let content = &trimmed[2..trimmed.len() - 1];
37
38 if !content.starts_with("state.") {
40 return None;
41 }
42
43 let path: Vec<String> = content
45 .split('.')
46 .skip(1)
47 .map(|s| s.to_string())
48 .collect();
49
50 if path.is_empty() {
51 return None;
52 }
53
54 Some(Binding::new(path))
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn test_parse_simple_binding() {
63 let binding = parse_binding("${state.user}").unwrap();
64 assert_eq!(binding.path, vec!["user"]);
65 assert_eq!(binding.root_key(), Some("user"));
66 }
67
68 #[test]
69 fn test_parse_nested_binding() {
70 let binding = parse_binding("${state.user.name}").unwrap();
71 assert_eq!(binding.path, vec!["user", "name"]);
72 assert_eq!(binding.root_key(), Some("user"));
73 assert_eq!(binding.full_path(), "user.name");
74 }
75
76 #[test]
77 fn test_parse_invalid_binding() {
78 assert!(parse_binding("state.user").is_none());
79 assert!(parse_binding("${user}").is_none());
80 assert!(parse_binding("${state}").is_none());
81 assert!(parse_binding("${props.name}").is_none());
82 }
83}