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 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 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());
}
}