use crate::commands::{CmdMessage, CmdResult, DisplayPad};
use crate::error::{PadzError, Result};
use crate::index::{DisplayIndex, PadSelector};
use crate::model::Scope;
use crate::store::{Bucket, DataStore};
use chrono::Utc;
use uuid::Uuid;
use super::helpers::{fmt_path, resolve_selectors};
pub fn run<S: DataStore>(
store: &mut S,
scope: Scope,
selectors: &[PadSelector],
destination_selector: Option<&PadSelector>,
) -> Result<CmdResult> {
let resolved_sources = resolve_selectors(store, scope, selectors, false)?;
if resolved_sources.is_empty() {
return Ok(CmdResult::default());
}
let destination_data = if let Some(dest_sel) = destination_selector {
let resolved_dests =
resolve_selectors(store, scope, std::slice::from_ref(dest_sel), false)?;
if resolved_dests.is_empty() {
return Err(PadzError::Api(format!(
"Destination not found: {:?}",
dest_sel
)));
}
if resolved_dests.len() > 1 {
return Err(PadzError::Api(
"Destination selector must resolve to a single pad".to_string(),
));
}
let (_dest_idx, dest_id) = resolved_dests.into_iter().next().unwrap();
let dest_pad = store.get_pad(&dest_id, scope, Bucket::Active)?;
Some((dest_id, dest_pad.metadata.title))
} else {
None
};
let dest_uuid = destination_data.map(|(uuid, _)| uuid);
let mut result = CmdResult::default();
let mut processed_ids = std::collections::HashSet::new();
for (display_index, source_uuid) in resolved_sources {
if !processed_ids.insert(source_uuid) {
continue;
}
if Some(source_uuid) == dest_uuid {
return Err(PadzError::Api(format!(
"Cannot move pad '{}' into itself",
fmt_path(&display_index)
)));
}
if let Some(target_id) = dest_uuid {
if is_descendant_of(store, scope, target_id, source_uuid)? {
return Err(PadzError::Api(format!(
"Cannot move pad '{}' into its own descendant",
fmt_path(&display_index)
)));
}
}
let mut pad = store.get_pad(&source_uuid, scope, Bucket::Active)?;
if pad.metadata.parent_id == dest_uuid {
result.add_message(CmdMessage::info(format!(
"Pad '{}' is already at destination",
fmt_path(&display_index)
)));
continue;
}
pad.metadata.parent_id = dest_uuid;
pad.metadata.updated_at = Utc::now();
store.save_pad(&pad, scope, Bucket::Active)?;
result.affected_pads.push(DisplayPad {
pad,
index: display_index
.last()
.cloned()
.unwrap_or(DisplayIndex::Regular(0)), matches: None,
children: Vec::new(),
});
}
Ok(result)
}
fn is_descendant_of<S: DataStore>(
store: &S,
scope: Scope,
child_id: Uuid,
potential_ancestor_id: Uuid,
) -> Result<bool> {
let mut current_id = child_id;
let mut depth = 0;
const MAX_DEPTH: u32 = 1000;
while depth < MAX_DEPTH {
let pad = store.get_pad(¤t_id, scope, Bucket::Active)?;
if let Some(parent_id) = pad.metadata.parent_id {
if parent_id == potential_ancestor_id {
return Ok(true);
}
current_id = parent_id;
depth += 1;
} else {
return Ok(false);
}
}
Ok(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::create;
use crate::index::DisplayIndex;
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
fn setup_store() -> (BucketedStore<MemBackend>, Uuid, Uuid) {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
let root_res =
create::run(&mut store, Scope::Project, "Root".into(), "".into(), None).unwrap();
let root_id = root_res.affected_pads[0].pad.metadata.id;
let child_res = create::run(
&mut store,
Scope::Project,
"Child".into(),
"".into(),
Some(PadSelector::Path(vec![DisplayIndex::Regular(1)])),
)
.unwrap();
let child_id = child_res.affected_pads[0].pad.metadata.id;
(store, root_id, child_id)
}
#[test]
fn test_move_child_to_root() {
let (mut store, _root_id, child_id) = setup_store();
let child = store
.get_pad(&child_id, Scope::Project, Bucket::Active)
.unwrap();
assert!(child.metadata.parent_id.is_some());
run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])],
None,
)
.unwrap();
let child_after = store
.get_pad(&child_id, Scope::Project, Bucket::Active)
.unwrap();
assert!(child_after.metadata.parent_id.is_none());
}
#[test]
fn test_move_root_to_another_pad() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
create::run(&mut store, Scope::Project, "B".into(), "".into(), None).unwrap();
run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
Some(&PadSelector::Path(vec![DisplayIndex::Regular(2)])),
)
.unwrap();
let pads = store.list_pads(Scope::Project, Bucket::Active).unwrap();
let pad_a = pads.iter().find(|p| p.metadata.title == "A").unwrap();
let pad_b = pads.iter().find(|p| p.metadata.title == "B").unwrap();
assert_eq!(pad_b.metadata.parent_id, Some(pad_a.metadata.id));
}
#[test]
fn test_prevent_move_to_self() {
let mut store = BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
);
create::run(&mut store, Scope::Project, "A".into(), "".into(), None).unwrap();
let res = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])],
Some(&PadSelector::Path(vec![DisplayIndex::Regular(1)])),
);
assert!(res.is_err());
assert!(res.unwrap_err().to_string().contains("into itself"));
}
#[test]
fn test_prevent_cycle_move_to_descendant() {
let (mut store, _root_id, _child_id) = setup_store();
let res = run(
&mut store,
Scope::Project,
&[PadSelector::Path(vec![DisplayIndex::Regular(1)])], Some(&PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(1),
])), );
assert!(res.is_err());
assert!(res.unwrap_err().to_string().contains("descendant"));
}
}