use crate::error::{PadzError, Result};
use crate::index::{current_ordering_key, index_pads, DisplayIndex, DisplayPad};
use crate::model::Scope;
use crate::store::Bucket;
use crate::store::DataStore;
use uuid::Uuid;
pub fn indexed_pads<S: DataStore>(store: &S, scope: Scope) -> Result<Vec<DisplayPad>> {
let active_pads = store.list_pads(scope, Bucket::Active)?;
let archived_pads = store.list_pads(scope, Bucket::Archived)?;
let deleted_pads = store.list_pads(scope, Bucket::Deleted)?;
Ok(index_pads(
active_pads,
archived_pads,
deleted_pads,
current_ordering_key(),
))
}
use crate::index::PadSelector;
pub fn resolve_selectors<S: DataStore>(
store: &S,
scope: Scope,
selectors: &[PadSelector],
check_delete_protection: bool,
) -> Result<Vec<(Vec<DisplayIndex>, Uuid)>> {
let root_pads = indexed_pads(store, scope)?;
let linearized = linearize_tree(&root_pads);
let mut results = Vec::new();
for selector in selectors {
match selector {
PadSelector::Path(path) => {
if let Some((_, pad)) = find_in_linearized(&linearized, path) {
if check_delete_protection && pad.pad.metadata.delete_protected {
return Err(PadzError::Api(
"Pinned pads are delete protected, unpin then delete it".to_string(),
));
}
results.push((path.clone(), pad.pad.metadata.id));
} else {
let s: Vec<String> = path.iter().map(|idx| idx.to_string()).collect();
return Err(PadzError::Api(format!(
"Index {} not found in current scope",
s.join(".")
)));
}
}
PadSelector::Range(start_path, end_path) => {
let start_idx = linearized
.iter()
.position(|(p, _)| p == start_path)
.ok_or_else(|| {
PadzError::Api(format!("Range start {} not found", fmt_path(start_path)))
})?;
let end_idx = linearized
.iter()
.position(|(p, _)| p == end_path)
.ok_or_else(|| {
PadzError::Api(format!("Range end {} not found", fmt_path(end_path)))
})?;
if start_idx > end_idx {
return Err(PadzError::Api(format!(
"Invalid range: {} appears after {} in the list",
fmt_path(start_path),
fmt_path(end_path)
)));
}
for (path, pad) in linearized.iter().take(end_idx + 1).skip(start_idx) {
if check_delete_protection && pad.pad.metadata.delete_protected {
return Err(PadzError::Api(
"Pinned pads are delete protected, unpin then delete it".to_string(),
));
}
results.push((path.clone(), pad.pad.metadata.id));
}
}
PadSelector::Uuid(uuid) => {
let found = linearized
.iter()
.find(|(_, dp)| dp.pad.metadata.id == *uuid);
match found {
Some((path, dp)) => {
if check_delete_protection && dp.pad.metadata.delete_protected {
return Err(PadzError::Api(
"Pinned pads are delete protected, unpin then delete it"
.to_string(),
));
}
results.push((path.clone(), dp.pad.metadata.id));
}
None => {
return Err(PadzError::Api(format!("No pad found with UUID {}", uuid)));
}
}
}
PadSelector::ShortUuid(hex) => {
let matches: Vec<&(Vec<DisplayIndex>, &DisplayPad)> = linearized
.iter()
.filter(|(_, dp)| {
dp.pad
.metadata
.id
.to_string()
.replace('-', "")
.starts_with(hex.as_str())
})
.collect();
match matches.len() {
0 => {
return Err(PadzError::Api(format!(
"No pad found with UUID prefix {}",
hex
)));
}
1 => {
let (path, dp) = matches[0];
if check_delete_protection && dp.pad.metadata.delete_protected {
return Err(PadzError::Api(
"Pinned pads are delete protected, unpin then delete it"
.to_string(),
));
}
results.push((path.clone(), dp.pad.metadata.id));
}
n => {
return Err(PadzError::Api(format!(
"UUID prefix \"{}\" matches {} pads. Use more characters to be unique.",
hex, n
)));
}
}
}
PadSelector::Title(term) => {
let term_lower = term.to_lowercase();
let matches: Vec<&(Vec<DisplayIndex>, &DisplayPad)> = linearized
.iter()
.filter(|(_, dp)| dp.pad.metadata.title.to_lowercase().contains(&term_lower))
.collect();
match matches.len() {
0 => return Err(PadzError::Api(format!("No pad found matching \"{}\"", term))),
1 => {
let (path, dp) = matches[0];
if check_delete_protection && dp.pad.metadata.delete_protected {
return Err(PadzError::Api("Pinned pads are delete protected, unpin then delete it".to_string()));
}
results.push((path.clone(), dp.pad.metadata.id));
},
n => return Err(PadzError::Api(format!(
"Term \"{}\" matches multiple paths, add more to make it unique(matched {} pads). Please be more specific.",
term, n
))),
}
}
}
}
Ok(results)
}
fn linearize_tree(roots: &[DisplayPad]) -> Vec<(Vec<DisplayIndex>, &DisplayPad)> {
let mut result = Vec::new();
for pad in roots {
linearize_recursive(pad, Vec::new(), &mut result);
}
result
}
fn linearize_recursive<'a>(
pad: &'a DisplayPad,
parent_path: Vec<DisplayIndex>,
result: &mut Vec<(Vec<DisplayIndex>, &'a DisplayPad)>,
) {
let mut current_path = parent_path;
current_path.push(pad.index.clone());
result.push((current_path.clone(), pad));
for child in &pad.children {
linearize_recursive(child, current_path.clone(), result);
}
}
fn find_in_linearized<'a>(
linearized: &'a [(Vec<DisplayIndex>, &'a DisplayPad)],
path: &[DisplayIndex],
) -> Option<&'a (Vec<DisplayIndex>, &'a DisplayPad)> {
linearized.iter().find(|(p, _)| p == path)
}
pub fn fmt_path(path: &[DisplayIndex]) -> String {
let s: Vec<String> = path.iter().map(|idx| idx.to_string()).collect();
s.join(".")
}
pub fn bucket_for_index(index: &DisplayIndex) -> Bucket {
match index {
DisplayIndex::Pinned(_) | DisplayIndex::Regular(_) => Bucket::Active,
DisplayIndex::Archived(_) => Bucket::Archived,
DisplayIndex::Deleted(_) => Bucket::Deleted,
}
}
pub fn pads_by_selectors<S: DataStore>(
store: &S,
scope: Scope,
selectors: &[PadSelector],
check_delete_protection: bool,
) -> Result<Vec<DisplayPad>> {
let resolved = resolve_selectors(store, scope, selectors, check_delete_protection)?;
let mut pads = Vec::with_capacity(resolved.len());
for (path, id) in resolved {
let local_index = path.last().cloned().unwrap_or(DisplayIndex::Regular(0));
let bucket = bucket_for_index(&local_index);
let pad = store.get_pad(&id, scope, bucket)?;
pads.push(DisplayPad {
pad,
index: local_index,
matches: None,
children: Vec::new(),
});
}
Ok(pads)
}
#[derive(Debug, Clone)]
pub struct NestedPad {
pub pad: DisplayPad,
pub depth: usize,
}
pub fn collect_nested_pads<S: DataStore>(
store: &S,
scope: Scope,
root_pads: &[DisplayPad],
) -> Result<Vec<NestedPad>> {
let indexed = indexed_pads(store, scope)?;
let mut result = Vec::new();
for dp in root_pads {
if let Some(tree_node) = find_node_by_id(&indexed, dp.pad.metadata.id) {
flatten_tree(tree_node, 0, &mut result);
} else {
result.push(NestedPad {
pad: dp.clone(),
depth: 0,
});
}
}
Ok(result)
}
fn flatten_tree(dp: &DisplayPad, depth: usize, result: &mut Vec<NestedPad>) {
result.push(NestedPad {
pad: DisplayPad {
pad: dp.pad.clone(),
index: dp.index.clone(),
matches: dp.matches.clone(),
children: Vec::new(), },
depth,
});
for child in &dp.children {
if !matches!(child.index, DisplayIndex::Deleted(_)) {
flatten_tree(child, depth + 1, result);
}
}
}
pub fn get_descendant_ids<S: DataStore>(
store: &S,
scope: Scope,
target_ids: &[Uuid],
) -> Result<Vec<Uuid>> {
let roots = indexed_pads(store, scope)?;
let mut seen = std::collections::HashSet::new();
let mut descendants = Vec::new();
for target in target_ids {
if let Some(node) = find_node_by_id(&roots, *target) {
collect_subtree_ids(node, &mut descendants, &mut seen);
}
}
Ok(descendants)
}
fn find_node_by_id(pads: &[DisplayPad], id: Uuid) -> Option<&DisplayPad> {
for dp in pads {
if dp.pad.metadata.id == id {
return Some(dp);
}
if let Some(found) = find_node_by_id(&dp.children, id) {
return Some(found);
}
}
None
}
fn collect_subtree_ids(
dp: &DisplayPad,
ids: &mut Vec<Uuid>,
seen: &mut std::collections::HashSet<Uuid>,
) {
for child in &dp.children {
if seen.insert(child.pad.metadata.id) {
ids.push(child.pad.metadata.id);
}
collect_subtree_ids(child, ids, seen);
}
}
pub fn find_pad_by_uuid<F>(pads: &[DisplayPad], uuid: Uuid, index_filter: F) -> Option<&DisplayPad>
where
F: Fn(&DisplayIndex) -> bool + Copy,
{
for dp in pads {
if dp.pad.metadata.id == uuid && index_filter(&dp.index) {
return Some(dp);
}
if let Some(found) = find_pad_by_uuid(&dp.children, uuid, index_filter) {
return Some(found);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::create;
use crate::index::DisplayIndex;
use crate::model::Scope;
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
#[test]
fn test_range_selection_within_siblings() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child A".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Child B".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Child C".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(3)],
)],
false,
)
.unwrap();
assert_eq!(result.len(), 3);
}
#[test]
fn test_range_selection_cross_parent() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Parent 1".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Child 1".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Parent 2".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Child 2".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(2), DisplayIndex::Regular(1)],
)],
false,
)
.unwrap();
assert_eq!(result.len(), 3);
}
#[test]
fn test_range_selection_root_only() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Root 1".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child 1".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(&mut store, Scope::Project, "Root 2".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(2)],
)],
false,
)
.unwrap();
assert_eq!(result.len(), 2);
}
#[test]
fn test_range_includes_children_of_intermediate_nodes() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Root 1".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child 1".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(&mut store, Scope::Project, "Root 2".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Root 3".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(3), DisplayIndex::Regular(1)],
)],
false,
)
.unwrap();
assert_eq!(result.len(), 4);
}
#[test]
fn test_pinned_child_addressable_by_path() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
crate::commands::pinning::pin(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Pinned(1),
])],
false,
)
.unwrap();
assert_eq!(result.len(), 1);
let pad = store
.get_pad(&result[0].1, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(pad.metadata.title, "Child");
}
#[test]
fn test_title_search_no_match_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Alpha".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Beta".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("Gamma".to_string())],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("No pad found matching"));
assert!(err.to_string().contains("Gamma"));
}
#[test]
fn test_title_search_multiple_matches_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Meeting Monday".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Meeting Tuesday".into(),
"".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Meeting Wednesday".into(),
"".into(),
None,
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("Meeting".to_string())],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("multiple"));
assert!(err.to_string().contains("3")); }
#[test]
fn test_title_search_single_match_succeeds() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Alpha".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Beta".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Gamma".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("Beta".to_string())],
false,
)
.unwrap();
assert_eq!(result.len(), 1);
let pad = store
.get_pad(&result[0].1, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(pad.metadata.title, "Beta");
}
#[test]
fn test_title_search_matches_title_only_not_content() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Shopping List".into(),
"Buy apples and oranges".into(),
None,
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Todo List".into(),
"Call dentist".into(),
None,
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("apples".to_string())],
false,
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("No pad found"));
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("Shopping".to_string())],
false,
)
.unwrap();
assert_eq!(result.len(), 1);
let pad = store
.get_pad(&result[0].1, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(pad.metadata.title, "Shopping List");
}
#[test]
fn test_title_search_is_case_insensitive() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"UPPERCASE TITLE".into(),
"".into(),
None,
)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("uppercase".to_string())],
false,
)
.unwrap();
assert_eq!(result.len(), 1);
let pad = store
.get_pad(&result[0].1, Scope::Project, Bucket::Active)
.unwrap();
assert_eq!(pad.metadata.title, "UPPERCASE TITLE");
}
#[test]
fn test_title_search_delete_protection_check() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"ProtectedPad".into(),
"".into(),
None,
)
.unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad = pads[0].clone();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("ProtectedPad".to_string())],
true, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("delete protected"));
}
#[test]
fn test_title_search_delete_protection_disabled() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"ProtectedPad".into(),
"".into(),
None,
)
.unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad = pads[0].clone();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Title("ProtectedPad".to_string())],
false, )
.unwrap();
assert_eq!(result.len(), 1);
}
#[test]
fn test_uuid_resolution() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let result =
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let pad_uuid = result.affected_pads[0].pad.metadata.id;
let resolved = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Uuid(pad_uuid)],
false,
)
.unwrap();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].1, pad_uuid);
}
#[test]
fn test_uuid_not_found_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let fake_uuid = uuid::Uuid::new_v4();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Uuid(fake_uuid)],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("No pad found with UUID"));
}
#[test]
fn test_uuid_delete_protection() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let result =
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let pad_uuid = result.affected_pads[0].pad.metadata.id;
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad = pads[0].clone();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Uuid(pad_uuid)],
true, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("delete protected"));
}
#[test]
fn test_range_invalid_order_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Pad B".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Pad C".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(3)],
vec![DisplayIndex::Regular(1)],
)],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("appears after"));
}
#[test]
fn test_range_start_not_found_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(99)],
vec![DisplayIndex::Regular(1)],
)],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Range start"));
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_range_end_not_found_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(99)],
)],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Range end"));
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_range_delete_protection_check() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Pad B".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Pad C".into(), "".into(), None).unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad_b = pads
.iter()
.find(|p| p.metadata.title == "Pad B")
.unwrap()
.clone();
pad_b.metadata.delete_protected = true;
store
.save_pad(&pad_b, Scope::Project, Bucket::Active)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Range(
vec![DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(3)],
)],
true, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("delete protected"));
}
#[test]
fn test_path_selector_not_found_returns_error() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(99)])],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_path_selector_delete_protection_check() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad = pads[0].clone();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
true, );
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("delete protected"));
}
#[test]
fn collect_nested_returns_parent_then_children_with_depths() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child A".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Child B".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let flat = pads_by_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false,
)
.unwrap();
assert_eq!(flat.len(), 1);
let nested = collect_nested_pads(&store, Scope::Project, &flat).unwrap();
assert_eq!(nested.len(), 3);
assert_eq!(nested[0].pad.pad.metadata.title, "Parent");
assert_eq!(nested[0].depth, 0);
assert_eq!(nested[1].depth, 1);
assert_eq!(nested[2].depth, 1);
}
#[test]
fn collect_nested_deep_tree_tracks_depth() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Root".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Level 1".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Level 2".into(),
"".into(),
Some(PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])),
)
.unwrap();
let flat = pads_by_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false,
)
.unwrap();
let nested = collect_nested_pads(&store, Scope::Project, &flat).unwrap();
let depths: Vec<usize> = nested.iter().map(|np| np.depth).collect();
assert_eq!(depths, vec![0, 1, 2]);
}
#[test]
fn collect_nested_skips_deleted_children() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Parent".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Keep".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
create::run(
&mut store,
Scope::Project,
"Delete Me".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
crate::commands::delete::run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
)
.unwrap();
let flat = pads_by_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false,
)
.unwrap();
let nested = collect_nested_pads(&store, Scope::Project, &flat).unwrap();
assert_eq!(nested.len(), 2);
assert_eq!(nested[0].pad.pad.metadata.title, "Parent");
assert_eq!(nested[1].pad.pad.metadata.title, "Keep");
}
#[test]
fn collect_nested_leaf_pad_returns_single() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(
&mut store,
Scope::Project,
"Leaf".into(),
"body".into(),
None,
)
.unwrap();
let flat = pads_by_selectors(
&store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
false,
)
.unwrap();
let nested = collect_nested_pads(&store, Scope::Project, &flat).unwrap();
assert_eq!(nested.len(), 1);
assert_eq!(nested[0].depth, 0);
assert_eq!(nested[0].pad.pad.metadata.title, "Leaf");
}
#[test]
fn collect_nested_multiple_roots_each_expand() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Root A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "Root B".into(), "".into(), None).unwrap();
create::run(
&mut store,
Scope::Project,
"Child of A".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(2)])),
)
.unwrap();
let flat = pads_by_selectors(
&store,
Scope::Project,
&[
PadSelector::Path(vec![DisplayIndex::Regular(1)]),
PadSelector::Path(vec![DisplayIndex::Regular(2)]),
],
false,
)
.unwrap();
let nested = collect_nested_pads(&store, Scope::Project, &flat).unwrap();
assert_eq!(nested.len(), 3);
assert_eq!(nested[0].pad.pad.metadata.title, "Root B");
assert_eq!(nested[0].depth, 0);
assert_eq!(nested[1].pad.pad.metadata.title, "Root A");
assert_eq!(nested[1].depth, 0);
assert_eq!(nested[2].pad.pad.metadata.title, "Child of A");
assert_eq!(nested[2].depth, 1);
}
#[test]
fn test_short_uuid_resolves_to_pad() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let result =
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let pad_uuid = result.affected_pads[0].pad.metadata.id;
let hex = pad_uuid.to_string().replace('-', "");
let short = &hex[..8];
let resolved = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::ShortUuid(short.to_string())],
false,
)
.unwrap();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].1, pad_uuid);
}
#[test]
fn test_short_uuid_not_found() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::ShortUuid("00000000".to_string())],
false,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("No pad found with UUID prefix"));
}
#[test]
fn test_short_uuid_delete_protection() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let result =
create::run(&mut store, Scope::Project, "Pad A".into(), "".into(), None).unwrap();
let pad_uuid = result.affected_pads[0].pad.metadata.id;
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let mut pad = pads[0].clone();
pad.metadata.delete_protected = true;
store
.save_pad(&pad, Scope::Project, Bucket::Active)
.unwrap();
let hex = pad_uuid.to_string().replace('-', "");
let short = &hex[..8];
let result = resolve_selectors(
&store,
Scope::Project,
&[PadSelector::ShortUuid(short.to_string())],
true,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("delete protected"));
}
}