Skip to main content

ucm_core/
block.rs

1//! Block - the fundamental unit of content in UCM.
2
3use crate::content::Content;
4use crate::edge::Edge;
5use crate::id::{compute_content_hash, generate_block_id, BlockId};
6use crate::metadata::BlockMetadata;
7use crate::version::Version;
8use serde::{Deserialize, Serialize};
9
10/// A block is the fundamental unit of content in UCM.
11///
12/// Blocks are immutable, content-addressed units identified by deterministic IDs
13/// derived from their content. They contain typed content, metadata for search
14/// and display, and edges representing relationships to other blocks.
15///
16/// # Example
17/// ```
18/// use ucm_core::{Block, Content};
19///
20/// let block = Block::new(Content::text("Hello, UCM!"), Some("intro"));
21/// println!("Block ID: {}", block.id);
22/// ```
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24pub struct Block {
25    /// Unique, content-derived identifier
26    pub id: BlockId,
27
28    /// The actual content
29    pub content: Content,
30
31    /// Block metadata
32    pub metadata: BlockMetadata,
33
34    /// Explicit relationships to other blocks
35    #[serde(default, skip_serializing_if = "Vec::is_empty")]
36    pub edges: Vec<Edge>,
37
38    /// Version for optimistic concurrency control
39    pub version: Version,
40}
41
42impl Block {
43    /// Create a new block with generated ID
44    pub fn new(content: Content, semantic_role: Option<&str>) -> Self {
45        let id = generate_block_id(&content, semantic_role, None);
46        let content_hash = compute_content_hash(&content);
47        let mut metadata = BlockMetadata::new(content_hash);
48
49        if let Some(role) = semantic_role {
50            if let Some(parsed_role) = crate::metadata::SemanticRole::parse(role) {
51                metadata.semantic_role = Some(parsed_role);
52            }
53        }
54
55        Self {
56            id,
57            content,
58            metadata,
59            edges: Vec::new(),
60            version: Version::initial(),
61        }
62    }
63
64    /// Create a new block with a specific ID (for deserialization or testing)
65    pub fn with_id(id: BlockId, content: Content) -> Self {
66        let content_hash = compute_content_hash(&content);
67        Self {
68            id,
69            content,
70            metadata: BlockMetadata::new(content_hash),
71            edges: Vec::new(),
72            version: Version::initial(),
73        }
74    }
75
76    /// Create a root block
77    pub fn root() -> Self {
78        Self {
79            id: BlockId::root(),
80            content: Content::text(""),
81            metadata: BlockMetadata::default(),
82            edges: Vec::new(),
83            version: Version::initial(),
84        }
85    }
86
87    /// Set metadata
88    pub fn with_metadata(mut self, metadata: BlockMetadata) -> Self {
89        self.metadata = metadata;
90        self
91    }
92
93    /// Set label
94    pub fn with_label(mut self, label: impl Into<String>) -> Self {
95        self.metadata.label = Some(label.into());
96        self
97    }
98
99    /// Add a tag
100    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
101        self.metadata.tags.push(tag.into());
102        self
103    }
104
105    /// Add an edge
106    pub fn with_edge(mut self, edge: Edge) -> Self {
107        self.edges.push(edge);
108        self
109    }
110
111    /// Add multiple edges
112    pub fn with_edges(mut self, edges: impl IntoIterator<Item = Edge>) -> Self {
113        self.edges.extend(edges);
114        self
115    }
116
117    /// Get the content type tag
118    pub fn content_type(&self) -> &'static str {
119        self.content.type_tag()
120    }
121
122    /// Check if the block is a root block
123    pub fn is_root(&self) -> bool {
124        self.id.is_root()
125    }
126
127    /// Get the estimated token count
128    pub fn token_estimate(&self) -> crate::metadata::TokenEstimate {
129        self.metadata
130            .token_estimate
131            .unwrap_or_else(|| crate::metadata::TokenEstimate::compute(&self.content))
132    }
133
134    /// Get content size in bytes
135    pub fn size_bytes(&self) -> usize {
136        self.content.size_bytes()
137    }
138
139    /// Update the content and regenerate ID
140    pub fn update_content(&mut self, content: Content, semantic_role: Option<&str>) {
141        self.content = content;
142        self.id = generate_block_id(&self.content, semantic_role, None);
143        self.metadata.content_hash = compute_content_hash(&self.content);
144        self.metadata.touch();
145        self.version.increment();
146    }
147
148    /// Add an edge to this block
149    pub fn add_edge(&mut self, edge: Edge) {
150        self.edges.push(edge);
151        self.version.increment();
152    }
153
154    /// Remove an edge by target and type
155    pub fn remove_edge(&mut self, target: &BlockId, edge_type: &crate::edge::EdgeType) -> bool {
156        let len_before = self.edges.len();
157        self.edges
158            .retain(|e| !(&e.target == target && &e.edge_type == edge_type));
159        let removed = self.edges.len() < len_before;
160        if removed {
161            self.version.increment();
162        }
163        removed
164    }
165
166    /// Get edges of a specific type
167    pub fn edges_of_type(&self, edge_type: &crate::edge::EdgeType) -> Vec<&Edge> {
168        self.edges
169            .iter()
170            .filter(|e| &e.edge_type == edge_type)
171            .collect()
172    }
173
174    /// Check if block has a specific tag
175    pub fn has_tag(&self, tag: &str) -> bool {
176        self.metadata.has_tag(tag)
177    }
178}
179
180/// Block lifecycle state
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum BlockState {
183    /// Reachable from document root
184    Live,
185    /// Not reachable from root but not deleted
186    Orphaned,
187    /// Marked for garbage collection
188    Deleted,
189}
190
191impl std::fmt::Display for BlockState {
192    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193        match self {
194            BlockState::Live => write!(f, "live"),
195            BlockState::Orphaned => write!(f, "orphaned"),
196            BlockState::Deleted => write!(f, "deleted"),
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::edge::EdgeType;
205
206    #[test]
207    fn test_block_creation() {
208        let block = Block::new(Content::text("Hello, world!"), Some("intro"));
209
210        assert!(!block.id.is_root());
211        assert_eq!(block.content_type(), "text");
212        assert!(block.edges.is_empty());
213    }
214
215    #[test]
216    fn test_deterministic_id() {
217        let block1 = Block::new(Content::text("Hello"), Some("intro"));
218        let block2 = Block::new(Content::text("Hello"), Some("intro"));
219
220        assert_eq!(block1.id, block2.id);
221    }
222
223    #[test]
224    fn test_different_role_different_id() {
225        let block1 = Block::new(Content::text("Hello"), Some("intro"));
226        let block2 = Block::new(Content::text("Hello"), Some("conclusion"));
227
228        assert_ne!(block1.id, block2.id);
229    }
230
231    #[test]
232    fn test_root_block() {
233        let root = Block::root();
234        assert!(root.is_root());
235    }
236
237    #[test]
238    fn test_block_builder() {
239        let block = Block::new(Content::text("Test"), None)
240            .with_label("Test Block")
241            .with_tag("important")
242            .with_tag("draft");
243
244        assert_eq!(block.metadata.label, Some("Test Block".to_string()));
245        assert!(block.has_tag("important"));
246        assert!(block.has_tag("draft"));
247    }
248
249    #[test]
250    fn test_block_edges() {
251        let target_id = BlockId::from_bytes([1u8; 12]);
252        let edge = Edge::new(EdgeType::References, target_id);
253
254        let mut block = Block::new(Content::text("Test"), None);
255        block.add_edge(edge);
256
257        assert_eq!(block.edges.len(), 1);
258        assert_eq!(block.edges_of_type(&EdgeType::References).len(), 1);
259
260        block.remove_edge(&target_id, &EdgeType::References);
261        assert!(block.edges.is_empty());
262    }
263
264    #[test]
265    fn test_update_content() {
266        let mut block = Block::new(Content::text("Original"), Some("intro"));
267        let original_id = block.id;
268        let original_version = block.version.counter;
269
270        block.update_content(Content::text("Updated"), Some("intro"));
271
272        assert_ne!(block.id, original_id);
273        assert!(block.version.counter > original_version);
274    }
275}