1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3
4use smix_screen::A11yNode;
16use smix_selector::Selector;
17
18uniffi::include_scaffolding!("smix");
20
21#[derive(Debug, thiserror::Error)]
23pub enum FfiError {
24 #[error("invalid tree JSON: {0}")]
26 InvalidTreeJson(String),
27 #[error("invalid selector JSON: {0}")]
29 InvalidSelectorJson(String),
30}
31
32pub fn resolve_selector(tree_json: String, selector_json: String) -> Result<Vec<String>, FfiError> {
40 let tree: A11yNode =
41 serde_json::from_str(&tree_json).map_err(|e| FfiError::InvalidTreeJson(e.to_string()))?;
42 let selector: Selector = serde_json::from_str(&selector_json)
43 .map_err(|e| FfiError::InvalidSelectorJson(e.to_string()))?;
44
45 if has_index_modifier(&selector) {
52 let result = smix_selector_resolver::resolve_selector(&tree, &selector);
53 Ok(result
54 .map(|node| vec![node.identifier.clone().unwrap_or_default()])
55 .unwrap_or_default())
56 } else {
57 let matches = smix_selector_resolver::resolve_selector_all(&tree, &selector);
58 Ok(matches
59 .iter()
60 .map(|node| node.identifier.clone().unwrap_or_default())
61 .collect())
62 }
63}
64
65fn has_index_modifier(selector: &Selector) -> bool {
69 use smix_selector::Selector as S;
70 match selector {
71 S::Text { modifiers, .. }
72 | S::Id { modifiers, .. }
73 | S::Label { modifiers, .. }
74 | S::Role { modifiers, .. }
75 | S::LocalizedText { modifiers, .. } => modifiers_has_index(modifiers),
76 S::Anchor { index, .. } => {
77 index.nth.is_some() || index.first.is_some() || index.last.is_some()
78 }
79 _ => false,
84 }
85}
86
87fn modifiers_has_index(m: &smix_selector::Modifiers) -> bool {
88 m.nth.is_some() || m.first.is_some() || m.last.is_some()
89}
90
91pub fn resolve_selector_count(tree_json: String, selector_json: String) -> Result<u32, FfiError> {
99 let tree: A11yNode =
100 serde_json::from_str(&tree_json).map_err(|e| FfiError::InvalidTreeJson(e.to_string()))?;
101 let selector: Selector = serde_json::from_str(&selector_json)
102 .map_err(|e| FfiError::InvalidSelectorJson(e.to_string()))?;
103 let matches = smix_selector_resolver::resolve_selector_all(&tree, &selector);
104 Ok(u32::try_from(matches.len()).unwrap_or(u32::MAX))
105}
106
107pub fn resolve_selector_labels(
115 tree_json: String,
116 selector_json: String,
117) -> Result<Vec<String>, FfiError> {
118 let tree: A11yNode =
119 serde_json::from_str(&tree_json).map_err(|e| FfiError::InvalidTreeJson(e.to_string()))?;
120 let selector: Selector = serde_json::from_str(&selector_json)
121 .map_err(|e| FfiError::InvalidSelectorJson(e.to_string()))?;
122 let matches = smix_selector_resolver::resolve_selector_all(&tree, &selector);
123 Ok(matches
124 .iter()
125 .map(|node| node.label.clone().unwrap_or_default())
126 .collect())
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
136 fn spike_001_empty_tree_id_miss() {
137 let tree_json = r#"{"rawType":"other","bounds":{"x":0,"y":0,"w":393,"h":852},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]}"#;
138 let selector_json = r#"{"id":"nope"}"#;
139 let result = resolve_selector(tree_json.into(), selector_json.into()).unwrap();
140 assert!(
141 result.is_empty(),
142 "expected empty match on empty tree, got {result:?}"
143 );
144 }
145
146 #[test]
147 fn invalid_tree_json_returns_err() {
148 let result = resolve_selector("not json".into(), "{}".into());
149 assert!(matches!(result, Err(FfiError::InvalidTreeJson(_))));
150 }
151
152 #[test]
153 fn invalid_selector_json_returns_err() {
154 let tree_json = r#"{"rawType":"other","bounds":{"x":0,"y":0,"w":393,"h":852},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]}"#;
155 let result = resolve_selector(tree_json.into(), "not json".into());
156 assert!(matches!(result, Err(FfiError::InvalidSelectorJson(_))));
157 }
158
159 #[test]
160 fn resolve_selector_count_returns_zero_on_empty_tree() {
161 let tree_json = r#"{"rawType":"other","bounds":{"x":0,"y":0,"w":393,"h":852},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]}"#;
162 let selector_json = r#"{"id":"nope"}"#;
163 let n = resolve_selector_count(tree_json.into(), selector_json.into()).unwrap();
164 assert_eq!(n, 0);
165 }
166
167 #[test]
168 fn resolve_selector_count_matches_multiple() {
169 let tree_json = r#"{"rawType":"other","bounds":{"x":0,"y":0,"w":393,"h":852},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[{"rawType":"button","role":"button","identifier":"a","label":"Item","bounds":{"x":0,"y":0,"w":10,"h":10},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]},{"rawType":"button","role":"button","identifier":"b","label":"Item","bounds":{"x":0,"y":20,"w":10,"h":10},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]}]}"#;
170 let selector_json = r#"{"label":"Item"}"#;
171 let n = resolve_selector_count(tree_json.into(), selector_json.into()).unwrap();
172 assert_eq!(n, 2);
173 }
174
175 #[test]
176 fn resolve_selector_labels_returns_each_match_label() {
177 let tree_json = r#"{"rawType":"other","bounds":{"x":0,"y":0,"w":393,"h":852},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[{"rawType":"button","role":"button","identifier":"a","label":"First","bounds":{"x":0,"y":0,"w":10,"h":10},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]},{"rawType":"button","role":"button","identifier":"b","label":"Second","bounds":{"x":0,"y":20,"w":10,"h":10},"enabled":true,"selected":false,"hasFocus":false,"visible":true,"children":[]}]}"#;
178 let selector_json = r#"{"role":"button"}"#;
179 let labels = resolve_selector_labels(tree_json.into(), selector_json.into()).unwrap();
180 assert_eq!(labels, vec!["First".to_string(), "Second".to_string()]);
181 }
182}