use serde::{Deserialize, Serialize};
use ucm_core::{BlockId, Content, EdgeType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MoveTarget {
ToParent {
parent_id: BlockId,
index: Option<usize>,
},
Before { sibling_id: BlockId },
After { sibling_id: BlockId },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Operation {
Edit {
block_id: BlockId,
path: String,
value: serde_json::Value,
operator: EditOperator,
},
Move {
block_id: BlockId,
new_parent: BlockId,
index: Option<usize>,
},
MoveToTarget {
block_id: BlockId,
target: MoveTarget,
},
Append {
parent_id: BlockId,
content: Content,
label: Option<String>,
tags: Vec<String>,
semantic_role: Option<String>,
index: Option<usize>,
},
Delete {
block_id: BlockId,
cascade: bool,
preserve_children: bool,
},
Prune { condition: Option<PruneCondition> },
Link {
source: BlockId,
edge_type: EdgeType,
target: BlockId,
metadata: Option<serde_json::Value>,
},
Unlink {
source: BlockId,
edge_type: EdgeType,
target: BlockId,
},
CreateSnapshot {
name: String,
description: Option<String>,
},
RestoreSnapshot { name: String },
WriteSection {
section_id: BlockId,
markdown: String,
base_heading_level: Option<usize>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditOperator {
Set,
Append,
Remove,
Increment,
Decrement,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum PruneCondition {
Unreachable,
TagContains(String),
Custom(String),
}
#[derive(Debug, Clone)]
pub struct OperationResult {
pub success: bool,
pub affected_blocks: Vec<BlockId>,
pub warnings: Vec<String>,
pub error: Option<String>,
}
impl OperationResult {
pub fn success(affected: Vec<BlockId>) -> Self {
Self {
success: true,
affected_blocks: affected,
warnings: Vec::new(),
error: None,
}
}
pub fn failure(error: impl Into<String>) -> Self {
Self {
success: false,
affected_blocks: Vec::new(),
warnings: Vec::new(),
error: Some(error.into()),
}
}
pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
self.warnings.push(warning.into());
self
}
}
impl Operation {
pub fn description(&self) -> String {
match self {
Operation::Edit { block_id, path, .. } => {
format!("EDIT {} SET {}", block_id, path)
}
Operation::Move {
block_id,
new_parent,
..
} => {
format!("MOVE {} TO {}", block_id, new_parent)
}
Operation::MoveToTarget { block_id, target } => match target {
MoveTarget::ToParent { parent_id, index } => {
if let Some(idx) = index {
format!("MOVE {} TO {} AT {}", block_id, parent_id, idx)
} else {
format!("MOVE {} TO {}", block_id, parent_id)
}
}
MoveTarget::Before { sibling_id } => {
format!("MOVE {} BEFORE {}", block_id, sibling_id)
}
MoveTarget::After { sibling_id } => {
format!("MOVE {} AFTER {}", block_id, sibling_id)
}
},
Operation::Append { parent_id, .. } => {
format!("APPEND to {}", parent_id)
}
Operation::Delete {
block_id, cascade, ..
} => {
if *cascade {
format!("DELETE {} CASCADE", block_id)
} else {
format!("DELETE {}", block_id)
}
}
Operation::Prune { condition } => match condition {
Some(PruneCondition::Unreachable) | None => "PRUNE UNREACHABLE".to_string(),
Some(PruneCondition::TagContains(tag)) => format!("PRUNE WHERE tag={}", tag),
Some(PruneCondition::Custom(c)) => format!("PRUNE WHERE {}", c),
},
Operation::Link {
source,
edge_type,
target,
..
} => {
format!("LINK {} {} {}", source, edge_type.as_str(), target)
}
Operation::Unlink {
source,
edge_type,
target,
} => {
format!("UNLINK {} {} {}", source, edge_type.as_str(), target)
}
Operation::CreateSnapshot { name, .. } => {
format!("SNAPSHOT CREATE {}", name)
}
Operation::RestoreSnapshot { name } => {
format!("SNAPSHOT RESTORE {}", name)
}
Operation::WriteSection {
section_id,
base_heading_level,
..
} => {
if let Some(level) = base_heading_level {
format!("WRITE_SECTION {} BASE_LEVEL {}", section_id, level)
} else {
format!("WRITE_SECTION {}", section_id)
}
}
}
}
}