tui_dispatch_shared/
lib.rs1pub const ACTION_VERBS: &[&str] = &[
11 "Start", "End", "Open", "Close", "Submit", "Confirm", "Cancel", "Next", "Prev", "Up", "Down",
12 "Left", "Right", "Enter", "Exit", "Escape", "Add", "Remove", "Clear", "Update", "Set", "Get",
13 "Load", "Save", "Delete", "Create", "Fetch", "Change", "Resize", "Error", "Show", "Hide",
14 "Enable", "Disable", "Toggle", "Focus", "Blur", "Select", "Move", "Copy", "Cycle", "Reset",
15 "Scroll",
16];
17
18pub fn split_pascal_case(value: &str) -> Vec<String> {
22 let chars: Vec<char> = value.chars().collect();
23 if chars.is_empty() {
24 return Vec::new();
25 }
26
27 let mut parts = Vec::new();
28 let mut start = 0usize;
29 for idx in 1..chars.len() {
30 let prev = chars[idx - 1];
31 let curr = chars[idx];
32 let next = chars.get(idx + 1).copied();
33
34 let lower_to_upper = prev.is_ascii_lowercase() && curr.is_ascii_uppercase();
35 let acronym_to_word = prev.is_ascii_uppercase()
36 && curr.is_ascii_uppercase()
37 && next.is_some_and(|ch| ch.is_ascii_lowercase());
38
39 if lower_to_upper || acronym_to_word {
40 parts.push(chars[start..idx].iter().collect());
41 start = idx;
42 }
43 }
44 parts.push(chars[start..].iter().collect());
45 parts
46}
47
48pub fn pascal_to_snake_case(value: &str) -> String {
50 split_pascal_case(value)
51 .into_iter()
52 .map(|part| part.to_ascii_lowercase())
53 .collect::<Vec<_>>()
54 .join("_")
55}
56
57pub fn infer_action_category(action_name: &str) -> Option<String> {
64 let parts = split_pascal_case(action_name);
65 if parts.is_empty() {
66 return None;
67 }
68
69 if parts[0] == "Did" {
70 return Some("async_result".to_string());
71 }
72
73 if parts.len() < 2 {
74 return None;
75 }
76
77 if ACTION_VERBS.contains(&parts[0].as_str()) {
79 return None;
80 }
81
82 let mut prefix_end = parts.len();
83 let mut found_verb = false;
84 for (idx, part) in parts.iter().enumerate().skip(1) {
85 if part == "Did" || ACTION_VERBS.contains(&part.as_str()) {
86 prefix_end = idx;
87 found_verb = true;
88 break;
89 }
90 }
91
92 if !found_verb || prefix_end == 0 {
93 return None;
94 }
95
96 Some(
97 parts[..prefix_end]
98 .iter()
99 .map(|part| part.to_ascii_lowercase())
100 .collect::<Vec<_>>()
101 .join("_"),
102 )
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_split_pascal_case_handles_acronyms() {
111 assert_eq!(
112 split_pascal_case("APIFetchStart"),
113 vec!["API".to_string(), "Fetch".to_string(), "Start".to_string()]
114 );
115 assert_eq!(
116 split_pascal_case("SearchHTTPResult"),
117 vec![
118 "Search".to_string(),
119 "HTTP".to_string(),
120 "Result".to_string()
121 ]
122 );
123 }
124
125 #[test]
126 fn test_pascal_to_snake_case_handles_acronyms() {
127 assert_eq!(pascal_to_snake_case("APIFetch"), "api_fetch");
128 assert_eq!(pascal_to_snake_case("HTTPResult"), "http_result");
129 }
130
131 #[test]
132 fn test_infer_action_category() {
133 assert_eq!(
134 infer_action_category("SearchStart"),
135 Some("search".to_string())
136 );
137 assert_eq!(
138 infer_action_category("SearchQuerySubmit"),
139 Some("search_query".to_string())
140 );
141 assert_eq!(
142 infer_action_category("WeatherDidLoad"),
143 Some("weather".to_string())
144 );
145 assert_eq!(
146 infer_action_category("DidLoad"),
147 Some("async_result".to_string())
148 );
149 assert_eq!(infer_action_category("Tick"), None);
150 assert_eq!(infer_action_category("OpenConnectionForm"), None);
151 assert_eq!(
152 infer_action_category("APIFetchStart"),
153 Some("api".to_string())
154 );
155 }
156}