pub trait Resolvable {
fn id_matches(&self, query: &str) -> bool;
fn resolve_name(&self) -> &str;
}
pub fn resolve_target<'a, T: Resolvable>(items: &'a [T], query: &str) -> Option<&'a T> {
if query.is_empty() {
return None;
}
if let Some(item) = items.iter().find(|i| i.id_matches(query)) {
return Some(item);
}
if let Some(item) = items.iter().find(|i| i.resolve_name() == query) {
return Some(item);
}
let mut prefix_matches = items
.iter()
.filter(|i| !i.resolve_name().is_empty() && i.resolve_name().starts_with(query));
match (prefix_matches.next(), prefix_matches.next()) {
(Some(only), None) => Some(only),
_ => None,
}
}
impl Resolvable for crate::client::http_client::SessionRow {
fn id_matches(&self, query: &str) -> bool {
self.id.0.to_string() == query
}
fn resolve_name(&self) -> &str {
&self.tmux_name
}
}
impl Resolvable for crate::client::http_client::ManagedSessionSummary {
fn id_matches(&self, query: &str) -> bool {
self.id == query
}
fn resolve_name(&self) -> &str {
&self.name
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Stub {
id: String,
name: String,
}
impl Resolvable for Stub {
fn id_matches(&self, query: &str) -> bool {
self.id == query
}
fn resolve_name(&self) -> &str {
&self.name
}
}
fn stub(id: &str, name: &str) -> Stub {
Stub {
id: id.to_string(),
name: name.to_string(),
}
}
#[test]
fn resolve_target_precedence() {
let items = vec![stub("uuid-1", "frontend"), stub("uuid-2", "backend")];
assert_eq!(
resolve_target(&items, "uuid-1").map(|s| s.id.as_str()),
Some("uuid-1")
);
assert_eq!(
resolve_target(&items, "backend").map(|s| s.id.as_str()),
Some("uuid-2")
);
}
#[test]
fn resolve_target_prefix() {
let items = vec![stub("uuid-1", "tmpm-blue-fox"), stub("uuid-2", "backend")];
assert_eq!(
resolve_target(&items, "tmpm-blue").map(|s| s.name.as_str()),
Some("tmpm-blue-fox")
);
}
#[test]
fn resolve_target_prefix_is_unambiguous() {
let items = vec![stub("uuid-1", "frontend-a"), stub("uuid-2", "frontend-b")];
assert!(resolve_target(&items, "frontend-").is_none());
let items2 = vec![stub("uuid-1", "front"), stub("uuid-2", "frontend")];
assert_eq!(
resolve_target(&items2, "front").map(|s| s.id.as_str()),
Some("uuid-1"),
"exact name match beats the ambiguous-prefix rule"
);
}
#[test]
fn resolve_target_no_match() {
let items = vec![stub("uuid-1", "frontend")];
assert!(resolve_target(&items, "no-such").is_none());
assert!(resolve_target(&items, "").is_none());
}
#[test]
fn resolve_target_managed_precedence() {
use crate::client::http_client::ManagedSessionSummary;
let summary = |id: &str, name: &str| ManagedSessionSummary {
id: id.to_string(),
name: name.to_string(),
state: "Running".to_string(),
persisted_state: None,
workspace_path: None,
repo_url: None,
branch: None,
created_at: None,
last_activity_at: None,
pending_decision: None,
proposed_default: None,
source_id: None,
task: None,
cwd: None,
claude_session_id: None,
deliverable_id: None,
pane_id: None,
injection_status: None,
unresumable: false,
stale_assets: false,
stale_assets_unchecked: false,
attached: false,
slot: 0,
deleted: false,
auto_resume_parked: None,
kind: None,
};
let items = vec![summary("m-1", "tmpm-red-owl"), summary("m-2", "api")];
assert_eq!(
resolve_target(&items, "m-2").map(|s| s.id.as_str()),
Some("m-2")
);
assert_eq!(
resolve_target(&items, "tmpm-red").map(|s| s.id.as_str()),
Some("m-1")
);
}
}