Skip to main content

nectar_primitives/chunk/
any_chunk.rs

1//! Type-erased chunk type
2//!
3//! This module provides [`AnyChunk`], an enum that can hold any chunk type
4//! for runtime polymorphism without requiring trait objects.
5
6use bytes::Bytes;
7
8use crate::bmt::DEFAULT_BODY_SIZE;
9use crate::error::Result;
10
11use super::chunk_type::ChunkType;
12use super::content::ContentChunk;
13use super::single_owner::SingleOwnerChunk;
14use super::traits::{Chunk, ChunkAddress};
15use super::type_id::ChunkTypeId;
16
17/// Type-erased chunk for runtime polymorphism with configurable body size.
18///
19/// This enum provides dynamic dispatch for chunks without requiring object-safe traits.
20/// Use this when you need to store heterogeneous chunk types in collections or pass
21/// chunks through interfaces that can't be generic.
22///
23/// # Why an enum instead of `Box<dyn Chunk>`?
24///
25/// The [`Chunk`] trait has an associated type (`type Header`) which makes it not
26/// object-safe. This enum provides the same functionality while maintaining type safety.
27///
28/// # Examples
29///
30/// ```
31/// use nectar_primitives::{AnyChunk, Chunk, ContentChunk, ChunkTypeId};
32///
33/// // Create a content chunk
34/// let content = ContentChunk::new(&b"hello world"[..]).unwrap();
35/// let any: AnyChunk = content.clone().into();
36///
37/// // Access common properties
38/// assert_eq!(any.type_id(), ChunkTypeId::CONTENT);
39///
40/// // Get the concrete type back
41/// if let Some(recovered) = any.as_content() {
42///     assert_eq!(recovered.address(), content.address());
43/// }
44/// ```
45#[derive(Debug, Clone)]
46pub enum AnyChunk<const BODY_SIZE: usize = DEFAULT_BODY_SIZE> {
47    /// A content-addressed chunk (CAC).
48    Content(ContentChunk<BODY_SIZE>),
49    /// A single-owner chunk (SOC).
50    SingleOwner(SingleOwnerChunk<BODY_SIZE>),
51    /// A custom chunk type (for extensibility).
52    ///
53    /// This variant allows storing chunks of types not known at compile time.
54    /// The raw bytes are preserved for potential later processing.
55    Custom {
56        /// The chunk type identifier.
57        type_id: ChunkTypeId,
58        /// The chunk's address.
59        address: ChunkAddress,
60        /// The raw chunk data.
61        data: Bytes,
62    },
63}
64
65impl<const BODY_SIZE: usize> AnyChunk<BODY_SIZE> {
66    /// Get the address of this chunk.
67    pub fn address(&self) -> &ChunkAddress {
68        match self {
69            Self::Content(c) => c.address(),
70            Self::SingleOwner(c) => c.address(),
71            Self::Custom { address, .. } => address,
72        }
73    }
74
75    /// Get the raw data contained in this chunk.
76    pub fn data(&self) -> &Bytes {
77        match self {
78            Self::Content(c) => c.data(),
79            Self::SingleOwner(c) => c.data(),
80            Self::Custom { data, .. } => data,
81        }
82    }
83
84    /// Get the type ID of this chunk.
85    pub const fn type_id(&self) -> ChunkTypeId {
86        match self {
87            Self::Content(_) => ChunkTypeId::CONTENT,
88            Self::SingleOwner(_) => ChunkTypeId::SINGLE_OWNER,
89            Self::Custom { type_id, .. } => *type_id,
90        }
91    }
92
93    /// Get the total size of this chunk in bytes.
94    pub fn size(&self) -> usize {
95        match self {
96            Self::Content(c) => c.size(),
97            Self::SingleOwner(c) => c.size(),
98            Self::Custom { data, .. } => data.len(),
99        }
100    }
101
102    /// Get the span (logical data length) of this chunk.
103    ///
104    /// For content chunks and single-owner chunks, this returns the BMT span.
105    /// For custom chunks, the span is not available (returns 0).
106    pub fn span(&self) -> u64 {
107        match self {
108            Self::Content(c) => super::traits::BmtChunk::span(c),
109            Self::SingleOwner(c) => super::traits::BmtChunk::span(c),
110            Self::Custom { .. } => 0, // Custom chunks don't have span info
111        }
112    }
113
114    /// Verify that this chunk's address matches an expected address.
115    pub fn verify(&self, expected: &ChunkAddress) -> Result<()> {
116        match self {
117            Self::Content(c) => c.verify(expected),
118            Self::SingleOwner(c) => c.verify(expected),
119            Self::Custom { address, .. } => {
120                if address != expected {
121                    return Err(
122                        super::error::ChunkError::verification_failed(*expected, *address).into(),
123                    );
124                }
125                Ok(())
126            }
127        }
128    }
129
130    /// Convert this chunk into its serialized bytes representation.
131    pub fn into_bytes(self) -> Bytes {
132        match self {
133            Self::Content(c) => c.into(),
134            Self::SingleOwner(c) => c.into(),
135            Self::Custom { data, .. } => data,
136        }
137    }
138
139    /// Check if this chunk is of a specific type.
140    pub fn is<T: ChunkType>(&self) -> bool {
141        self.type_id() == T::TYPE_ID
142    }
143
144    /// Check if this is a content chunk.
145    pub const fn is_content(&self) -> bool {
146        matches!(self, Self::Content(_))
147    }
148
149    /// Check if this is a single-owner chunk.
150    pub const fn is_single_owner(&self) -> bool {
151        matches!(self, Self::SingleOwner(_))
152    }
153
154    /// Check if this is a custom chunk type.
155    pub const fn is_custom(&self) -> bool {
156        matches!(self, Self::Custom { .. })
157    }
158
159    /// Get a reference to the contained ContentChunk, if this is one.
160    pub const fn as_content(&self) -> Option<&ContentChunk<BODY_SIZE>> {
161        match self {
162            Self::Content(c) => Some(c),
163            _ => None,
164        }
165    }
166
167    /// Get a reference to the contained SingleOwnerChunk, if this is one.
168    pub const fn as_single_owner(&self) -> Option<&SingleOwnerChunk<BODY_SIZE>> {
169        match self {
170            Self::SingleOwner(c) => Some(c),
171            _ => None,
172        }
173    }
174
175    /// Convert into the contained ContentChunk, if this is one.
176    pub fn into_content(self) -> Option<ContentChunk<BODY_SIZE>> {
177        match self {
178            Self::Content(c) => Some(c),
179            _ => None,
180        }
181    }
182
183    /// Convert into the contained SingleOwnerChunk, if this is one.
184    pub fn into_single_owner(self) -> Option<SingleOwnerChunk<BODY_SIZE>> {
185        match self {
186            Self::SingleOwner(c) => Some(c),
187            _ => None,
188        }
189    }
190}
191
192impl<const BODY_SIZE: usize> From<ContentChunk<BODY_SIZE>> for AnyChunk<BODY_SIZE> {
193    fn from(chunk: ContentChunk<BODY_SIZE>) -> Self {
194        Self::Content(chunk)
195    }
196}
197
198impl<const BODY_SIZE: usize> From<SingleOwnerChunk<BODY_SIZE>> for AnyChunk<BODY_SIZE> {
199    fn from(chunk: SingleOwnerChunk<BODY_SIZE>) -> Self {
200        Self::SingleOwner(chunk)
201    }
202}
203
204impl<const BODY_SIZE: usize> PartialEq for AnyChunk<BODY_SIZE> {
205    fn eq(&self, other: &Self) -> bool {
206        self.address() == other.address()
207    }
208}
209
210impl<const BODY_SIZE: usize> Eq for AnyChunk<BODY_SIZE> {}
211
212#[cfg(test)]
213mod tests {
214    use super::super::traits::Chunk;
215    use super::*;
216
217    type DefaultContentChunk = ContentChunk<DEFAULT_BODY_SIZE>;
218    type DefaultSingleOwnerChunk = SingleOwnerChunk<DEFAULT_BODY_SIZE>;
219    type DefaultAnyChunk = AnyChunk<DEFAULT_BODY_SIZE>;
220
221    #[test]
222    fn test_content_chunk_conversion() {
223        let content = DefaultContentChunk::new(&b"hello world"[..]).unwrap();
224        let address = *content.address();
225
226        let any: DefaultAnyChunk = content.into();
227
228        assert!(any.is_content());
229        assert!(!any.is_single_owner());
230        assert!(!any.is_custom());
231        assert_eq!(any.type_id(), ChunkTypeId::CONTENT);
232        assert_eq!(*any.address(), address);
233    }
234
235    #[test]
236    fn test_as_content() {
237        let content = DefaultContentChunk::new(&b"test data"[..]).unwrap();
238        let expected_addr = *content.address();
239
240        let any: DefaultAnyChunk = content.into();
241        let recovered = any.as_content().unwrap();
242
243        assert_eq!(*recovered.address(), expected_addr);
244    }
245
246    #[test]
247    fn test_into_content() {
248        let content = DefaultContentChunk::new(&b"test data"[..]).unwrap();
249        let expected_addr = *content.address();
250
251        let any: DefaultAnyChunk = content.into();
252        let recovered = any.into_content().unwrap();
253
254        assert_eq!(*recovered.address(), expected_addr);
255    }
256
257    #[test]
258    fn test_is_methods() {
259        let content: DefaultAnyChunk = DefaultContentChunk::new(&b"test"[..]).unwrap().into();
260
261        assert!(content.is::<DefaultContentChunk>());
262        assert!(!content.is::<DefaultSingleOwnerChunk>());
263    }
264
265    #[test]
266    fn test_clone() {
267        let content = DefaultContentChunk::new(&b"test"[..]).unwrap();
268        let any: DefaultAnyChunk = content.into();
269        let cloned = any.clone();
270
271        assert_eq!(any.address(), cloned.address());
272        assert_eq!(any.type_id(), cloned.type_id());
273    }
274}