use crate::commands;
use crate::error::{PadzError, Result};
use crate::index::{parse_index_or_range, PadSelector};
use crate::model::{Pad, Scope};
use crate::store::DataStore;
pub struct PadzApi<S: DataStore> {
store: S,
paths: commands::PadzPaths,
}
impl<S: DataStore> PadzApi<S> {
pub fn new(store: S, paths: commands::PadzPaths) -> Self {
Self { store, paths }
}
pub fn create_pad(
&mut self,
scope: Scope,
title: String,
content: String,
parent: Option<&str>,
) -> Result<commands::CmdResult> {
let parent_selector = if let Some(p) = parent {
Some(parse_index_or_range(p).map_err(PadzError::Api)?)
} else {
None
};
commands::create::run(&mut self.store, scope, title, content, parent_selector)
}
pub fn get_pads<I: AsRef<str>>(
&self,
scope: Scope,
filter: PadFilter,
ids: &[I],
) -> Result<commands::CmdResult> {
let selectors = if ids.is_empty() {
vec![]
} else {
parse_selectors(ids)?
};
commands::get::run(&self.store, scope, filter, &selectors)
}
pub fn view_pads<I: AsRef<str>>(
&self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::view::run(&self.store, scope, &selectors)
}
pub fn delete_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::delete::run(&mut self.store, scope, &selectors)
}
pub fn pin_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::pinning::pin(&mut self.store, scope, &selectors)
}
pub fn unpin_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::pinning::unpin(&mut self.store, scope, &selectors)
}
pub fn complete_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::status::complete(&mut self.store, scope, &selectors)
}
pub fn reopen_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::status::reopen(&mut self.store, scope, &selectors)
}
pub fn update_pads(
&mut self,
scope: Scope,
updates: &[commands::PadUpdate],
) -> Result<commands::CmdResult> {
commands::update::run(&mut self.store, scope, updates)
}
pub fn update_pads_from_content<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
raw_content: &str,
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::update::run_from_content(&mut self.store, scope, &selectors, raw_content)
}
pub fn move_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
to_parent: Option<&str>,
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
let parent_selector = if let Some(p) = to_parent {
if p.trim().is_empty() {
None } else {
Some(parse_index_or_range(p).map_err(PadzError::Api)?)
}
} else {
None
};
commands::move_pads::run(&mut self.store, scope, &selectors, parent_selector.as_ref())
}
pub fn purge_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
recursive: bool,
confirmed: bool,
include_done: bool,
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::purge::run(
&mut self.store,
scope,
&selectors,
recursive,
confirmed,
include_done,
)
}
pub fn archive_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::archive::run(&mut self.store, scope, &selectors)
}
pub fn unarchive_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors_for_archived(indexes)?;
commands::unarchive::run(&mut self.store, scope, &selectors)
}
pub fn restore_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors_for_deleted(indexes)?;
commands::restore::run(&mut self.store, scope, &selectors)
}
pub fn export_pads<I: AsRef<str>>(
&self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::export::run(&self.store, scope, &selectors)
}
pub fn export_pads_single_file<I: AsRef<str>>(
&self,
scope: Scope,
indexes: &[I],
title: &str,
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::export::run_single_file(&self.store, scope, &selectors, title)
}
pub fn import_pads(
&mut self,
scope: Scope,
paths: Vec<std::path::PathBuf>,
import_exts: &[String],
) -> Result<commands::CmdResult> {
commands::import::run(&mut self.store, scope, paths, import_exts)
}
pub fn doctor(&mut self, scope: Scope) -> Result<commands::CmdResult> {
commands::doctor::run(&mut self.store, scope)
}
pub fn pad_paths<I: AsRef<str>>(
&self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::paths::run(&self.store, scope, &selectors)
}
pub fn get_path_by_id(&self, scope: Scope, id: uuid::Uuid) -> Result<std::path::PathBuf> {
use crate::store::Bucket;
self.store.get_pad_path(&id, scope, Bucket::Active)
}
pub fn refresh_pad(&mut self, scope: Scope, id: &uuid::Uuid) -> Result<Option<Pad>> {
use crate::store::Bucket;
let pad = self.store.get_pad(id, scope, Bucket::Active)?;
if pad.content.trim().is_empty() {
self.store.delete_pad(id, scope, Bucket::Active)?;
return Ok(None);
}
let mut updated = pad;
let content = updated.content.clone();
updated.update_from_raw(&content);
self.store.save_pad(&updated, scope, Bucket::Active)?;
Ok(Some(updated))
}
pub fn remove_pad(&mut self, scope: Scope, id: uuid::Uuid) -> Result<()> {
use crate::store::Bucket;
self.store.delete_pad(&id, scope, Bucket::Active)
}
pub fn init(&self, scope: Scope) -> Result<commands::CmdResult> {
commands::init::run(&self.paths, scope)
}
pub fn init_link(
&self,
local_padz: &std::path::Path,
target: &std::path::Path,
) -> Result<commands::CmdResult> {
commands::init::link(local_padz, target)
}
pub fn init_unlink(&self, local_padz: &std::path::Path) -> Result<commands::CmdResult> {
commands::init::unlink(local_padz)
}
pub fn paths(&self) -> &commands::PadzPaths {
&self.paths
}
pub fn list_tags(&self, scope: Scope) -> Result<commands::CmdResult> {
commands::tags::list_tags(&self.store, scope)
}
pub fn list_pad_tags<I: AsRef<str>>(
&self,
scope: Scope,
indexes: &[I],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::tags::list_pad_tags(&self.store, scope, &selectors)
}
pub fn create_tag(&mut self, scope: Scope, name: &str) -> Result<commands::CmdResult> {
commands::tags::create_tag(&mut self.store, scope, name)
}
pub fn delete_tag(&mut self, scope: Scope, name: &str) -> Result<commands::CmdResult> {
commands::tags::delete_tag(&mut self.store, scope, name)
}
pub fn rename_tag(
&mut self,
scope: Scope,
old_name: &str,
new_name: &str,
) -> Result<commands::CmdResult> {
commands::tags::rename_tag(&mut self.store, scope, old_name, new_name)
}
pub fn add_tags_to_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
tags: &[String],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::tagging::add_tags(&mut self.store, scope, &selectors, tags)
}
pub fn remove_tags_from_pads<I: AsRef<str>>(
&mut self,
scope: Scope,
indexes: &[I],
tags: &[String],
) -> Result<commands::CmdResult> {
let selectors = parse_selectors(indexes)?;
commands::tagging::remove_tags(&mut self.store, scope, &selectors, tags)
}
}
fn parse_selectors<I: AsRef<str>>(inputs: &[I]) -> Result<Vec<PadSelector>> {
let mut all_selectors = Vec::new();
let mut parse_failed = false;
for input in inputs {
match parse_index_or_range(input.as_ref()) {
Ok(selector) => all_selectors.push(selector),
Err(_) => {
parse_failed = true;
break;
}
}
}
if !parse_failed {
let mut unique_selectors = Vec::new();
for s in all_selectors {
if !unique_selectors.contains(&s) {
unique_selectors.push(s);
}
}
return Ok(unique_selectors);
}
let search_term = inputs
.iter()
.map(|s| s.as_ref())
.collect::<Vec<&str>>()
.join(" ");
Ok(vec![PadSelector::Title(search_term)])
}
fn parse_selectors_for_deleted<I: AsRef<str>>(inputs: &[I]) -> Result<Vec<PadSelector>> {
let normalized: Vec<String> = inputs
.iter()
.map(|s| normalize_to_deleted_index(s.as_ref()))
.collect();
parse_selectors(&normalized)
}
fn parse_selectors_for_archived<I: AsRef<str>>(inputs: &[I]) -> Result<Vec<PadSelector>> {
let normalized: Vec<String> = inputs
.iter()
.map(|s| normalize_to_archived_index(s.as_ref()))
.collect();
parse_selectors(&normalized)
}
fn normalize_to_archived_index(s: &str) -> String {
if let Some(dash_pos) = s.find('-') {
if dash_pos > 0 {
let start_str = &s[..dash_pos];
let end_str = &s[dash_pos + 1..];
let normalized_start = normalize_path_for_archived(start_str);
let normalized_end = normalize_path_for_archived(end_str);
return format!("{}-{}", normalized_start, normalized_end);
}
}
normalize_path_for_archived(s)
}
fn normalize_path_for_archived(s: &str) -> String {
let mut parts: Vec<String> = s.split('.').map(|s| s.to_string()).collect();
if let Some(last) = parts.last_mut() {
if last.chars().all(|c| c.is_ascii_digit()) && !last.is_empty() {
*last = format!("ar{}", last);
}
}
parts.join(".")
}
fn normalize_to_deleted_index(s: &str) -> String {
if let Some(dash_pos) = s.find('-') {
if dash_pos > 0 {
let start_str = &s[..dash_pos];
let end_str = &s[dash_pos + 1..];
let normalized_start = normalize_path_for_deleted(start_str);
let normalized_end = normalize_path_for_deleted(end_str);
return format!("{}-{}", normalized_start, normalized_end);
}
}
normalize_path_for_deleted(s)
}
fn normalize_path_for_deleted(s: &str) -> String {
let mut parts: Vec<String> = s.split('.').map(|s| s.to_string()).collect();
if let Some(last) = parts.last_mut() {
*last = normalize_single_to_deleted(last);
}
parts.join(".")
}
fn normalize_single_to_deleted(s: &str) -> String {
if s.chars().all(|c| c.is_ascii_digit()) && !s.is_empty() {
format!("d{}", s)
} else {
s.to_string()
}
}
pub use crate::model::TodoStatus;
pub use commands::get::{PadFilter, PadStatusFilter};
pub use commands::{CmdMessage, CmdResult, MessageLevel, PadUpdate, PadzPaths};
#[cfg(test)]
mod tests {
use super::*;
use crate::index::{DisplayIndex, PadSelector};
use crate::store::backend::StorageBackend;
use crate::store::bucketed::BucketedStore;
use crate::store::mem_backend::MemBackend;
use std::path::PathBuf;
type TestStore = BucketedStore<MemBackend>;
fn make_store() -> TestStore {
BucketedStore::new(
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
MemBackend::new(),
)
}
fn make_api() -> PadzApi<TestStore> {
let store = make_store();
let paths = PadzPaths {
project: Some(PathBuf::from("/tmp/test")),
global: PathBuf::from("/tmp/global"),
};
PadzApi::new(store, paths)
}
#[test]
fn test_parse_selectors_single_index() {
let inputs = vec!["1"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
assert!(matches!(selectors[0], PadSelector::Path(_)));
}
#[test]
fn test_parse_selectors_multiple_indexes() {
let inputs = vec!["1", "3", "5"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 3);
}
#[test]
fn test_parse_selectors_deduplicates() {
let inputs = vec!["1", "1", "2", "1"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 2);
}
#[test]
fn test_parse_selectors_title_fallback() {
let inputs = vec!["meeting", "notes"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
match &selectors[0] {
PadSelector::Title(term) => assert_eq!(term, "meeting notes"),
_ => panic!("Expected Title selector"),
}
}
#[test]
fn test_parse_selectors_mixed_input_becomes_title() {
let inputs = vec!["1", "meeting", "2"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
match &selectors[0] {
PadSelector::Title(term) => assert_eq!(term, "1 meeting 2"),
_ => panic!("Expected Title selector"),
}
}
#[test]
fn test_parse_selectors_range() {
let inputs = vec!["1-3"];
let selectors = parse_selectors(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
assert!(matches!(selectors[0], PadSelector::Range(_, _)));
}
#[test]
fn test_api_create_pad_simple() {
let mut api = make_api();
let result = api
.create_pad(Scope::Project, "Test Title".into(), "Content".into(), None)
.unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert_eq!(result.affected_pads[0].pad.metadata.title, "Test Title");
}
#[test]
fn test_api_create_pad_with_parent_string() {
let mut api = make_api();
api.create_pad(Scope::Project, "Parent".into(), "".into(), None)
.unwrap();
let result = api
.create_pad(Scope::Project, "Child".into(), "".into(), Some("1"))
.unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert_eq!(result.affected_pads[0].pad.metadata.title, "Child");
assert!(result.affected_pads[0].pad.metadata.parent_id.is_some());
}
#[test]
fn test_api_get_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
let result = api
.get_pads(Scope::Project, PadFilter::default(), &[] as &[String])
.unwrap();
assert_eq!(result.listed_pads.len(), 1);
}
#[test]
fn test_api_view_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
let result = api.view_pads(Scope::Project, &["1"]).unwrap();
assert_eq!(result.listed_pads.len(), 1);
assert_eq!(result.listed_pads[0].pad.metadata.title, "Test");
}
#[test]
fn test_api_delete_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
let result = api.delete_pads(Scope::Project, &["1"]).unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert!(matches!(
result.affected_pads[0].index,
crate::index::DisplayIndex::Deleted(_)
));
}
#[test]
fn test_api_pin_unpin_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
let result = api.pin_pads(Scope::Project, &["1"]).unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert!(result.affected_pads[0].pad.metadata.is_pinned);
let result = api.unpin_pads(Scope::Project, &["p1"]).unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert!(!result.affected_pads[0].pad.metadata.is_pinned);
}
#[test]
fn test_api_update_pads() {
let mut api = make_api();
api.create_pad(
Scope::Project,
"Old Title".into(),
"Old Content".into(),
None,
)
.unwrap();
let updates = vec![PadUpdate::new(
DisplayIndex::Regular(1),
"New Title".into(),
"New Content".into(),
)];
let result = api.update_pads(Scope::Project, &updates).unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert_eq!(result.affected_pads[0].pad.metadata.title, "New Title");
}
#[test]
fn test_api_restore_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
api.delete_pads(Scope::Project, &["1"]).unwrap();
let result = api.restore_pads(Scope::Project, &["1"]).unwrap();
assert_eq!(result.affected_pads.len(), 1);
assert!(matches!(
result.affected_pads[0].index,
crate::index::DisplayIndex::Regular(_)
));
}
#[test]
fn test_api_purge_pads_confirmed() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
api.delete_pads(Scope::Project, &["1"]).unwrap();
let result = api.purge_pads(Scope::Project, &["d1"], false, true, false);
assert!(result.is_ok());
let list = api
.get_pads(
Scope::Project,
PadFilter {
status: PadStatusFilter::All,
search_term: None,
todo_status: None,
tags: None,
},
&[] as &[String],
)
.unwrap();
assert_eq!(list.listed_pads.len(), 0);
}
#[test]
fn test_api_purge_pads_not_confirmed() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
api.delete_pads(Scope::Project, &["1"]).unwrap();
let result = api.purge_pads(Scope::Project, &["d1"], false, false, false);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Aborted"));
let list = api
.get_pads(
Scope::Project,
PadFilter {
status: PadStatusFilter::Deleted,
search_term: None,
todo_status: None,
tags: None,
},
&[] as &[String],
)
.unwrap();
assert_eq!(list.listed_pads.len(), 1);
}
#[test]
fn test_api_doctor() {
let mut api = make_api();
let result = api.doctor(Scope::Project).unwrap();
assert!(!result.messages.is_empty());
}
#[test]
fn test_api_paths_accessor() {
let api = make_api();
let paths = api.paths();
assert_eq!(paths.project, Some(PathBuf::from("/tmp/test")));
assert_eq!(paths.global, PathBuf::from("/tmp/global"));
}
#[test]
fn test_api_import_pads() {
let mut api = make_api();
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("note.md");
std::fs::write(&file_path, "Imported Note\n\nContent").unwrap();
let result = api
.import_pads(Scope::Project, vec![file_path], &[".md".to_string()])
.unwrap();
assert!(result
.messages
.iter()
.any(|m| m.content.contains("Total imported: 1")));
}
#[test]
fn test_normalize_single_to_deleted() {
assert_eq!(normalize_single_to_deleted("1"), "d1");
assert_eq!(normalize_single_to_deleted("42"), "d42");
assert_eq!(normalize_single_to_deleted("d1"), "d1");
assert_eq!(normalize_single_to_deleted("d42"), "d42");
assert_eq!(normalize_single_to_deleted("p1"), "p1");
assert_eq!(normalize_single_to_deleted("p99"), "p99");
assert_eq!(normalize_single_to_deleted(""), "");
assert_eq!(normalize_single_to_deleted("abc"), "abc");
}
#[test]
fn test_normalize_to_deleted_index_ranges() {
assert_eq!(normalize_to_deleted_index("3-5"), "d3-d5");
assert_eq!(normalize_to_deleted_index("1-10"), "d1-d10");
assert_eq!(normalize_to_deleted_index("d3-d5"), "d3-d5");
assert_eq!(normalize_to_deleted_index("3-d5"), "d3-d5");
assert_eq!(normalize_to_deleted_index("d3-5"), "d3-d5");
assert_eq!(normalize_to_deleted_index("3"), "d3");
assert_eq!(normalize_to_deleted_index("d3"), "d3");
assert_eq!(normalize_to_deleted_index("1.2"), "1.d2");
assert_eq!(normalize_to_deleted_index("p1.2"), "p1.d2");
assert_eq!(normalize_to_deleted_index("d1.2"), "d1.d2");
assert_eq!(normalize_to_deleted_index("1.p2"), "1.p2");
assert_eq!(normalize_to_deleted_index("1.d2"), "1.d2");
assert_eq!(normalize_to_deleted_index("1.2-1.4"), "1.d2-1.d4");
assert_eq!(normalize_to_deleted_index("d1.2-d1.4"), "d1.d2-d1.d4");
}
#[test]
fn test_parse_selectors_for_deleted() {
let inputs = vec!["1", "3", "d5"];
let selectors = parse_selectors_for_deleted(&inputs).unwrap();
assert_eq!(selectors.len(), 3);
assert!(matches!(selectors[0], PadSelector::Path(_)));
if let PadSelector::Path(path) = &selectors[0] {
assert_eq!(path.len(), 1);
assert!(matches!(path[0], DisplayIndex::Deleted(1)));
}
if let PadSelector::Path(path) = &selectors[1] {
assert_eq!(path.len(), 1);
assert!(matches!(path[0], DisplayIndex::Deleted(3)));
}
if let PadSelector::Path(path) = &selectors[2] {
assert_eq!(path.len(), 1);
assert!(matches!(path[0], DisplayIndex::Deleted(5)));
}
}
#[test]
fn test_parse_selectors_for_deleted_with_range() {
let inputs = vec!["1-3"];
let selectors = parse_selectors_for_deleted(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
match &selectors[0] {
PadSelector::Range(start, end) => {
assert_eq!(start.len(), 1);
assert!(matches!(start[0], DisplayIndex::Deleted(1)));
assert_eq!(end.len(), 1);
assert!(matches!(end[0], DisplayIndex::Deleted(3)));
}
_ => panic!("Expected Range"),
}
}
#[test]
fn test_parse_selectors_for_deleted_with_hierarchical_range() {
let inputs = vec!["1.2-1.4"];
let selectors = parse_selectors_for_deleted(&inputs).unwrap();
assert_eq!(selectors.len(), 1);
match &selectors[0] {
PadSelector::Range(start_path, end_path) => {
assert_eq!(start_path.len(), 2);
assert!(matches!(start_path[0], DisplayIndex::Regular(1)));
assert!(matches!(start_path[1], DisplayIndex::Deleted(2)));
assert_eq!(end_path.len(), 2);
assert!(matches!(end_path[0], DisplayIndex::Regular(1)));
assert!(matches!(end_path[1], DisplayIndex::Deleted(4)));
}
_ => panic!("Expected PadSelector::Range"),
}
}
#[test]
fn test_api_move_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "A".into(), "".into(), None)
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(10));
api.create_pad(Scope::Project, "B".into(), "".into(), None)
.unwrap();
let result = api.move_pads(Scope::Project, &["1"], Some("2")).unwrap();
assert!(result.messages.is_empty());
assert_eq!(result.affected_pads.len(), 1);
assert_eq!(result.affected_pads[0].pad.metadata.title, "B");
let pads = api
.get_pads(Scope::Project, PadFilter::default(), &[] as &[String])
.unwrap()
.listed_pads;
let pad_a = pads.iter().find(|p| p.pad.metadata.title == "A").unwrap();
assert_eq!(pad_a.children.len(), 1);
assert_eq!(pad_a.children[0].pad.metadata.title, "B");
}
#[test]
fn test_api_move_pads_to_root() {
let mut api = make_api();
api.create_pad(Scope::Project, "Parent".into(), "".into(), None)
.unwrap();
api.create_pad(Scope::Project, "Child".into(), "".into(), Some("1"))
.unwrap();
let result = api.move_pads(Scope::Project, &["1.1"], None).unwrap();
assert!(result.messages.is_empty());
assert_eq!(result.affected_pads.len(), 1);
let pads = api
.get_pads(Scope::Project, PadFilter::default(), &[] as &[String])
.unwrap()
.listed_pads;
let child = pads
.iter()
.find(|p| p.pad.metadata.title == "Child")
.unwrap();
assert!(child.pad.metadata.parent_id.is_none());
}
#[test]
fn test_api_move_pads_cycle_error() {
let mut api = make_api();
api.create_pad(Scope::Project, "A".into(), "".into(), None)
.unwrap();
let result = api.move_pads(Scope::Project, &["1"], Some("1"));
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("into itself"));
}
#[test]
fn test_api_list_tags() {
let mut api = make_api();
api.create_tag(Scope::Project, "work").unwrap();
let result = api.list_tags(Scope::Project).unwrap();
assert!(result.messages.iter().any(|m| m.content.contains("work")));
}
#[test]
fn test_api_create_tag() {
let mut api = make_api();
let result = api.create_tag(Scope::Project, "rust").unwrap();
assert!(result.messages[0].content.contains("Created tag 'rust'"));
}
#[test]
fn test_api_delete_tag() {
let mut api = make_api();
api.create_tag(Scope::Project, "work").unwrap();
let result = api.delete_tag(Scope::Project, "work").unwrap();
assert!(result.messages[0].content.contains("Deleted tag 'work'"));
}
#[test]
fn test_api_rename_tag() {
let mut api = make_api();
api.create_tag(Scope::Project, "old-name").unwrap();
let result = api
.rename_tag(Scope::Project, "old-name", "new-name")
.unwrap();
assert!(result.messages[0]
.content
.contains("Renamed tag 'old-name' to 'new-name'"));
}
#[test]
fn test_api_add_tags_to_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
api.create_tag(Scope::Project, "work").unwrap();
let result = api
.add_tags_to_pads(Scope::Project, &["1"], &["work".to_string()])
.unwrap();
assert!(result.messages[0].content.contains("Added tag"));
assert!(result.affected_pads[0]
.pad
.metadata
.tags
.contains(&"work".to_string()));
}
#[test]
fn test_api_remove_tags_from_pads() {
let mut api = make_api();
api.create_pad(Scope::Project, "Test".into(), "".into(), None)
.unwrap();
api.create_tag(Scope::Project, "work").unwrap();
api.add_tags_to_pads(Scope::Project, &["1"], &["work".to_string()])
.unwrap();
let result = api
.remove_tags_from_pads(Scope::Project, &["1"], &["work".to_string()])
.unwrap();
assert!(result.messages[0].content.contains("Removed tag"));
assert!(result.affected_pads[0].pad.metadata.tags.is_empty());
}
#[test]
fn test_api_refresh_pad_updates_title() {
let mut api = make_api();
let result = api
.create_pad(Scope::Project, "Original Title".into(), "Body".into(), None)
.unwrap();
let pad_id = result.affected_pads[0].pad.metadata.id;
api.store
.active
.backend
.write_content(&pad_id, Scope::Project, "New Title\n\nNew body")
.unwrap();
let refreshed = api.refresh_pad(Scope::Project, &pad_id).unwrap();
assert!(refreshed.is_some());
let pad = refreshed.unwrap();
assert_eq!(pad.metadata.title, "New Title");
assert_eq!(pad.content, "New Title\n\nNew body");
}
#[test]
fn test_api_refresh_pad_empty_deletes() {
let mut api = make_api();
let result = api
.create_pad(Scope::Project, "Will Be Empty".into(), "".into(), None)
.unwrap();
let pad_id = result.affected_pads[0].pad.metadata.id;
api.store
.active
.backend
.write_content(&pad_id, Scope::Project, " \n\n ")
.unwrap();
let refreshed = api.refresh_pad(Scope::Project, &pad_id).unwrap();
assert!(refreshed.is_none());
let list = api
.get_pads(Scope::Project, PadFilter::default(), &[] as &[String])
.unwrap();
assert_eq!(list.listed_pads.len(), 0);
}
#[test]
fn test_api_remove_pad() {
let mut api = make_api();
let result = api
.create_pad(Scope::Project, "To Remove".into(), "Content".into(), None)
.unwrap();
let pad_id = result.affected_pads[0].pad.metadata.id;
api.remove_pad(Scope::Project, pad_id).unwrap();
let list = api
.get_pads(Scope::Project, PadFilter::default(), &[] as &[String])
.unwrap();
assert_eq!(list.listed_pads.len(), 0);
}
}