use crate::hurl::HurlEntry;
pub fn entry_path(title: &str) -> Vec<String> {
let segs: Vec<String> = title
.split('/')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if segs.is_empty() {
vec![String::new()]
} else {
segs
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Row {
Up,
Folder(String),
Entry(usize),
}
pub fn rows_for(entries: &[HurlEntry], folder: &[String]) -> Vec<Row> {
let mut folders: Vec<String> = Vec::new();
let mut leaves: Vec<usize> = Vec::new();
for (i, e) in entries.iter().enumerate() {
let path = entry_path(&e.title);
if path.len() <= folder.len() || path[..folder.len()] != *folder {
continue;
}
if path.len() == folder.len() + 1 {
leaves.push(i);
} else {
let name = &path[folder.len()];
if !folders.contains(name) {
folders.push(name.clone());
}
}
}
folders.sort();
let mut rows = Vec::with_capacity(folders.len() + leaves.len() + 1);
if !folder.is_empty() {
rows.push(Row::Up);
}
rows.extend(folders.into_iter().map(Row::Folder));
rows.extend(leaves.into_iter().map(Row::Entry));
rows
}
pub fn rows_matching(entries: &[HurlEntry], query: &str) -> Vec<Row> {
let needle = query.trim().to_lowercase();
entries
.iter()
.enumerate()
.filter(|(_, e)| e.title.to_lowercase().contains(&needle))
.map(|(i, _)| Row::Entry(i))
.collect()
}
pub fn folder_of(entries: &[HurlEntry], idx: usize) -> Vec<String> {
let Some(e) = entries.get(idx) else {
return Vec::new();
};
let mut path = entry_path(&e.title);
path.pop();
path
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(title: &str) -> HurlEntry {
HurlEntry {
title: title.to_string(),
method: "GET".to_string(),
url: "http://x".to_string(),
..Default::default()
}
}
#[test]
fn entry_path_splits_on_slash_and_trims_segments() {
assert_eq!(entry_path("Auth / Login"), vec!["Auth", "Login"]);
assert_eq!(
entry_path("Auth/Tokens/Refresh"),
vec!["Auth", "Tokens", "Refresh"]
);
assert_eq!(entry_path("plain"), vec!["plain"]);
}
#[test]
fn entry_path_of_an_untitled_or_slash_only_title_is_one_empty_segment() {
assert_eq!(entry_path(""), vec![""]);
assert_eq!(entry_path("///"), vec![""]);
}
#[test]
fn root_rows_show_top_level_requests_and_folders_without_an_up_row() {
let entries = vec![
entry("plain"),
entry("Auth/Login"),
entry("Auth/Logout"),
entry("Files/Upload/Big"),
];
let rows = rows_for(&entries, &[]);
assert_eq!(
rows,
vec![
Row::Folder("Auth".into()),
Row::Folder("Files".into()),
Row::Entry(0)
]
);
}
#[test]
fn descending_a_folder_shows_its_direct_children_with_an_up_row() {
let entries = vec![
entry("plain"),
entry("Auth/Login"),
entry("Auth/Logout"),
entry("Auth/Tokens/Refresh"),
];
let rows = rows_for(&entries, &["Auth".to_string()]);
assert_eq!(
rows,
vec![
Row::Up,
Row::Folder("Tokens".into()),
Row::Entry(1),
Row::Entry(2)
]
);
}
#[test]
fn nested_folders_stay_flat_at_each_level() {
let entries = vec![entry("Files/Upload/Big"), entry("Files/Upload/Small")];
let rows = rows_for(&entries, &["Files".to_string(), "Upload".to_string()]);
assert_eq!(rows, vec![Row::Up, Row::Entry(0), Row::Entry(1)]);
}
#[test]
fn a_filtered_list_is_flat_and_matches_on_the_whole_title() {
let entries = vec![
entry("plain"),
entry("Auth/Login"),
entry("Auth/Logout"),
entry("Files/Upload/Big"),
];
assert_eq!(
rows_matching(&entries, "log"),
vec![Row::Entry(1), Row::Entry(2)]
);
assert_eq!(
rows_matching(&entries, "auth/"),
vec![Row::Entry(1), Row::Entry(2)]
);
assert_eq!(rows_matching(&entries, "BIG"), vec![Row::Entry(3)]);
assert!(rows_matching(&entries, "nothing").is_empty());
}
#[test]
fn a_filter_reaches_requests_the_current_folder_would_hide() {
let entries = vec![entry("Auth/Login"), entry("Files/Upload/Big")];
let browsing = rows_for(&entries, &["Auth".to_string()]);
assert_eq!(browsing, vec![Row::Up, Row::Entry(0)]);
assert_eq!(rows_matching(&entries, "upload"), vec![Row::Entry(1)]);
}
#[test]
fn an_all_whitespace_query_matches_everything_rather_than_nothing() {
let entries = vec![entry("Auth/Login"), entry("plain")];
assert_eq!(
rows_matching(&entries, " "),
vec![Row::Entry(0), Row::Entry(1)]
);
}
#[test]
fn folder_of_returns_the_parent_path_of_an_entry() {
let entries = vec![entry("plain"), entry("Auth/Tokens/Refresh")];
assert_eq!(folder_of(&entries, 0), Vec::<String>::new());
assert_eq!(
folder_of(&entries, 1),
vec!["Auth".to_string(), "Tokens".to_string()]
);
assert_eq!(folder_of(&entries, 99), Vec::<String>::new());
}
}