use crate::CtlError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Target {
Ref(u32),
Name(String),
Id(String),
Role(String),
RoleAt(String, u32),
}
impl Target {
pub fn describe(&self) -> String {
match self {
Target::Ref(n) => format!("@e{n}"),
Target::Name(s) => format!("name:{s}"),
Target::Id(s) => format!("id:{s}"),
Target::Role(s) => format!("role:{s}"),
Target::RoleAt(s, n) => format!("role:{s}#{n}"),
}
}
}
pub fn parse_target(raw: &str) -> Result<Target, CtlError> {
if let Some(n) = raw.strip_prefix("@e") {
let n: u32 = n.parse().map_err(|_| bad(raw))?;
if n == 0 {
return Err(bad(raw)); }
return Ok(Target::Ref(n));
}
for (prefix, build) in [
(
"name:",
(|s: String| Target::Name(s)) as fn(String) -> Target,
),
("id:", |s: String| Target::Id(s)),
("role:", |s: String| Target::Role(s)),
] {
if let Some(rest) = raw.strip_prefix(prefix) {
if rest.is_empty() {
return Err(bad(raw));
}
if prefix == "role:" {
if let Some((role, idx)) = rest.rsplit_once('#') {
let n: u32 = idx.parse().map_err(|_| bad(raw))?;
if !role.is_empty() && n >= 1 {
return Ok(Target::RoleAt(role.to_string(), n));
}
return Err(bad(raw));
}
}
return Ok(build(rest.to_string()));
}
}
Err(bad(raw))
}
fn bad(raw: &str) -> CtlError {
CtlError::protocol(format!(
"invalid target {raw:?}: expected @eN, name:<substr>, id:<AutomationId> or role:<Type>"
))
}
pub fn fuzzy_contains(haystack: &str, needle: &str) -> bool {
let norm = |s: &str| {
s.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_lowercase()
};
let h = norm(haystack);
let n = norm(needle);
!n.is_empty() && h.contains(&n)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ErrorCode;
#[test]
fn parses_all_four_forms() {
assert_eq!(parse_target("@e3").unwrap(), Target::Ref(3));
assert_eq!(
parse_target("name:确定").unwrap(),
Target::Name("确定".into())
);
assert_eq!(
parse_target("id:num1Button").unwrap(),
Target::Id("num1Button".into())
);
assert_eq!(
parse_target("role:Button").unwrap(),
Target::Role("Button".into())
);
assert_eq!(
parse_target("role:Button#2").unwrap(),
Target::RoleAt("Button".into(), 2)
);
}
#[test]
fn role_ordinal_edges() {
for bad in ["role:Button#0", "role:#2", "role:Button#", "role:Button#x"] {
assert!(parse_target(bad).is_err(), "{bad:?} should be rejected");
}
}
#[test]
fn fuzzy_matches_case_and_whitespace_insensitive() {
assert!(fuzzy_contains("保存 设置", "保存 设置"));
assert!(fuzzy_contains("Open File", "open file"));
assert!(!fuzzy_contains("Open", "open file"));
assert!(!fuzzy_contains("anything", ""));
}
#[test]
fn rejects_bare_words_and_edge_cases() {
for bad in [
"button", "@e", "@e0", "@ex", "name:", "id:", "role:", "@e-1",
] {
let err = parse_target(bad).unwrap_err();
assert_eq!(err.code, ErrorCode::Protocol, "{bad:?} should be PROTOCOL");
}
}
#[test]
fn describe_round_trips_for_messages() {
assert_eq!(parse_target("@e7").unwrap().describe(), "@e7");
}
}