use matchsorter::{
Key, MatchSorterOptions, Ranking, get_highest_ranking, get_item_values, rank_item,
};
struct User {
name: String,
email: String,
tags: Vec<String>,
}
fn sample_user() -> User {
User {
name: "Alice".to_owned(),
email: "alice@example.com".to_owned(),
tags: vec!["admin".to_owned(), "staff".to_owned()],
}
}
fn default_opts() -> MatchSorterOptions<User> {
MatchSorterOptions::default()
}
#[test]
fn ac01_key_new_accepts_closure_returning_vec_string() {
let user = sample_user();
let key = Key::new(|u: &User| vec![u.name.clone()]);
let values = get_item_values(&user, &key);
assert_eq!(values, vec!["Alice"]);
}
#[test]
fn ac02_builder_methods_set_fields_correctly() {
let user = sample_user();
let keys = vec![
Key::new(|u: &User| vec![u.name.clone()])
.threshold(Ranking::Acronym)
.max_ranking(Ranking::Contains)
.min_ranking(Ranking::Acronym),
];
let info = get_highest_ranking(&user, &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
assert_eq!(info.key_threshold, Some(Ranking::Acronym));
}
#[test]
fn ac03_max_ranking_clamps_starts_with_to_contains() {
let user = sample_user();
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).max_ranking(Ranking::Contains)];
let info = get_highest_ranking(&user, &keys, "ali", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn ac04_min_ranking_promotes_fuzzy_match_to_contains() {
let user = User {
name: "playground".to_owned(),
email: String::new(),
tags: vec![],
};
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).min_ranking(Ranking::Contains)];
let info = get_highest_ranking(&user, &keys, "plgnd", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn ac05_min_ranking_does_not_promote_no_match() {
let user = User {
name: "abc".to_owned(),
email: String::new(),
tags: vec![],
};
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).min_ranking(Ranking::Contains)];
let info = get_highest_ranking(&user, &keys, "xyz", &default_opts());
assert_eq!(info.rank, Ranking::NoMatch);
}
#[test]
fn ac06_multiple_keys_best_ranking_wins() {
let user = sample_user();
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.email.clone()]),
Key::new(|u: &User| vec![u.name.clone()]),
];
let info = get_highest_ranking(&user, &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.ranked_value, "Alice");
assert_eq!(info.key_index, 1);
}
#[test]
fn ac07_equal_rank_tiebreak_first_key_wins() {
let user = sample_user();
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.name.clone()]),
Key::new(|u: &User| vec![u.name.clone()]),
];
let info = get_highest_ranking(&user, &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.key_index, 0);
}
#[test]
fn ac08_multi_value_key_best_tag_wins() {
let user = sample_user();
let keys = vec![Key::new(|u: &User| u.tags.clone())];
let info = get_highest_ranking(&user, &keys, "admin", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.ranked_value, "admin");
}
#[test]
fn ac09a_no_keys_mode_vec_string() {
let items: Vec<String> = vec!["Green".to_owned(), "Greenland".to_owned(), "abc".to_owned()];
let rankings: Vec<Ranking> = items
.iter()
.map(|item| rank_item(item, "Green", false))
.collect();
assert_eq!(rankings[0], Ranking::CaseSensitiveEqual);
assert_eq!(rankings[1], Ranking::StartsWith);
assert_eq!(rankings[2], Ranking::NoMatch);
}
#[test]
fn ac09b_no_keys_mode_vec_str() {
let items: Vec<&str> = vec!["Green", "Greenland", "abc"];
let rankings: Vec<Ranking> = items
.iter()
.map(|item| rank_item(item, "Green", false))
.collect();
assert_eq!(rankings[0], Ranking::CaseSensitiveEqual);
assert_eq!(rankings[1], Ranking::StartsWith);
assert_eq!(rankings[2], Ranking::NoMatch);
}
#[test]
fn ac10_zero_unsafe_blocks() {
let src_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let rs_files = collect_rs_files(&src_dir);
assert!(!rs_files.is_empty(), "should find at least one .rs file");
for path in &rs_files {
let contents = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
for (line_num, line) in contents.lines().enumerate() {
let trimmed = line.trim();
if trimmed.starts_with("//") {
continue;
}
let code_part = match trimmed.find("//") {
Some(pos) => &trimmed[..pos],
None => trimmed,
};
assert!(
!code_part.contains("unsafe"),
"found `unsafe` in {}:{}: {}",
path.display(),
line_num + 1,
line,
);
}
}
}
fn collect_rs_files(dir: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut files = Vec::new();
if dir.is_dir() {
for entry in std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("failed to read dir {}: {e}", dir.display()))
{
let entry =
entry.unwrap_or_else(|e| panic!("failed to read entry in {}: {e}", dir.display()));
let path = entry.path();
if path.is_dir() {
files.extend(collect_rs_files(&path));
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
}
files
}