ipfrs_core/
codec_registry.rs

1//! Codec registry system for pluggable encoding/decoding.
2//!
3//! This module provides a trait-based system for registering and using different
4//! codecs for IPLD data encoding/decoding. Similar to the hash registry, this allows
5//! runtime codec selection and custom codec implementations.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use ipfrs_core::{Codec, CodecRegistry, Ipld};
11//! use ipfrs_core::cid::codec;
12//!
13//! // Get the global codec registry
14//! let registry = ipfrs_core::global_codec_registry();
15//!
16//! // Encode data with DAG-CBOR
17//! let data = Ipld::String("Hello, IPLD!".to_string());
18//! let encoded = registry.encode(codec::DAG_CBOR, &data).unwrap();
19//!
20//! // Decode back
21//! let decoded = registry.decode(codec::DAG_CBOR, &encoded).unwrap();
22//! assert_eq!(data, decoded);
23//! ```
24
25use crate::{Error, Ipld, Result};
26use std::collections::HashMap;
27use std::sync::{Arc, RwLock};
28
29/// Trait for codec implementations.
30///
31/// Codecs encode and decode IPLD data to/from binary formats.
32pub trait Codec: Send + Sync {
33    /// Encode IPLD data to bytes.
34    fn encode(&self, data: &Ipld) -> Result<Vec<u8>>;
35
36    /// Decode bytes to IPLD data.
37    fn decode(&self, bytes: &[u8]) -> Result<Ipld>;
38
39    /// Get the codec code (multicodec identifier).
40    fn code(&self) -> u64;
41
42    /// Get the codec name.
43    fn name(&self) -> &str;
44}
45
46/// DAG-CBOR codec implementation.
47#[derive(Debug, Clone, Default)]
48pub struct DagCborCodec;
49
50impl Codec for DagCborCodec {
51    fn encode(&self, data: &Ipld) -> Result<Vec<u8>> {
52        data.to_dag_cbor()
53    }
54
55    fn decode(&self, bytes: &[u8]) -> Result<Ipld> {
56        Ipld::from_dag_cbor(bytes)
57    }
58
59    fn code(&self) -> u64 {
60        crate::cid::codec::DAG_CBOR
61    }
62
63    fn name(&self) -> &str {
64        "dag-cbor"
65    }
66}
67
68/// DAG-JSON codec implementation.
69#[derive(Debug, Clone, Default)]
70pub struct DagJsonCodec;
71
72impl Codec for DagJsonCodec {
73    fn encode(&self, data: &Ipld) -> Result<Vec<u8>> {
74        let json_str = data.to_dag_json()?;
75        Ok(json_str.into_bytes())
76    }
77
78    fn decode(&self, bytes: &[u8]) -> Result<Ipld> {
79        let json_str = std::str::from_utf8(bytes)
80            .map_err(|e| Error::Deserialization(format!("Invalid UTF-8: {}", e)))?;
81        Ipld::from_dag_json(json_str)
82    }
83
84    fn code(&self) -> u64 {
85        crate::cid::codec::DAG_JSON
86    }
87
88    fn name(&self) -> &str {
89        "dag-json"
90    }
91}
92
93/// RAW codec implementation (no-op, stores bytes as-is).
94#[derive(Debug, Clone, Default)]
95pub struct RawCodec;
96
97impl Codec for RawCodec {
98    fn encode(&self, data: &Ipld) -> Result<Vec<u8>> {
99        match data {
100            Ipld::Bytes(bytes) => Ok(bytes.clone()),
101            _ => Err(Error::Serialization(
102                "RAW codec requires Ipld::Bytes".to_string(),
103            )),
104        }
105    }
106
107    fn decode(&self, bytes: &[u8]) -> Result<Ipld> {
108        Ok(Ipld::Bytes(bytes.to_vec()))
109    }
110
111    fn code(&self) -> u64 {
112        crate::cid::codec::RAW
113    }
114
115    fn name(&self) -> &str {
116        "raw"
117    }
118}
119
120/// Registry for codecs.
121///
122/// Allows runtime selection of encoding/decoding algorithms and
123/// registration of custom codec implementations.
124#[derive(Clone)]
125pub struct CodecRegistry {
126    codecs: Arc<RwLock<HashMap<u64, Arc<dyn Codec>>>>,
127}
128
129impl Default for CodecRegistry {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl CodecRegistry {
136    /// Create a new codec registry with default codecs.
137    ///
138    /// Default codecs:
139    /// - RAW (0x55)
140    /// - DAG-CBOR (0x71)
141    /// - DAG-JSON (0x0129)
142    pub fn new() -> Self {
143        let registry = Self {
144            codecs: Arc::new(RwLock::new(HashMap::new())),
145        };
146
147        // Register default codecs
148        registry.register(Arc::new(RawCodec));
149        registry.register(Arc::new(DagCborCodec));
150        registry.register(Arc::new(DagJsonCodec));
151
152        registry
153    }
154
155    /// Register a codec implementation.
156    ///
157    /// If a codec with the same code already exists, it will be replaced.
158    pub fn register(&self, codec: Arc<dyn Codec>) {
159        let code = codec.code();
160        let mut codecs = self.codecs.write().unwrap();
161        codecs.insert(code, codec);
162    }
163
164    /// Get a codec by its code.
165    pub fn get(&self, code: u64) -> Option<Arc<dyn Codec>> {
166        let codecs = self.codecs.read().unwrap();
167        codecs.get(&code).cloned()
168    }
169
170    /// Check if a codec is registered.
171    pub fn has_codec(&self, code: u64) -> bool {
172        let codecs = self.codecs.read().unwrap();
173        codecs.contains_key(&code)
174    }
175
176    /// List all registered codec codes.
177    pub fn list_codecs(&self) -> Vec<u64> {
178        let codecs = self.codecs.read().unwrap();
179        codecs.keys().copied().collect()
180    }
181
182    /// Get codec name by code.
183    pub fn get_name(&self, code: u64) -> Option<String> {
184        self.get(code).map(|c| c.name().to_string())
185    }
186
187    /// Encode IPLD data using the specified codec.
188    pub fn encode(&self, codec_code: u64, data: &Ipld) -> Result<Vec<u8>> {
189        let codec = self.get(codec_code).ok_or_else(|| {
190            Error::Serialization(format!("Codec 0x{:x} not registered", codec_code))
191        })?;
192        codec.encode(data)
193    }
194
195    /// Decode bytes using the specified codec.
196    pub fn decode(&self, codec_code: u64, bytes: &[u8]) -> Result<Ipld> {
197        let codec = self.get(codec_code).ok_or_else(|| {
198            Error::Deserialization(format!("Codec 0x{:x} not registered", codec_code))
199        })?;
200        codec.decode(bytes)
201    }
202}
203
204/// Global codec registry instance.
205static GLOBAL_CODEC_REGISTRY: std::sync::OnceLock<CodecRegistry> = std::sync::OnceLock::new();
206
207/// Get the global codec registry.
208///
209/// # Examples
210///
211/// ```rust
212/// use ipfrs_core::global_codec_registry;
213/// use ipfrs_core::cid::codec;
214///
215/// let registry = global_codec_registry();
216/// assert!(registry.has_codec(codec::DAG_CBOR));
217/// assert!(registry.has_codec(codec::DAG_JSON));
218/// assert!(registry.has_codec(codec::RAW));
219/// ```
220pub fn global_codec_registry() -> &'static CodecRegistry {
221    GLOBAL_CODEC_REGISTRY.get_or_init(CodecRegistry::new)
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227    use crate::cid::codec;
228    use std::collections::BTreeMap;
229
230    #[test]
231    fn test_raw_codec() {
232        let codec = RawCodec;
233        let data = Ipld::Bytes(b"hello".to_vec());
234
235        let encoded = codec.encode(&data).unwrap();
236        assert_eq!(encoded, b"hello");
237
238        let decoded = codec.decode(&encoded).unwrap();
239        assert_eq!(decoded, data);
240    }
241
242    #[test]
243    fn test_raw_codec_requires_bytes() {
244        let codec = RawCodec;
245        let data = Ipld::String("hello".to_string());
246
247        assert!(codec.encode(&data).is_err());
248    }
249
250    #[test]
251    fn test_dag_cbor_codec() {
252        let codec = DagCborCodec;
253        let mut map = BTreeMap::new();
254        map.insert("key".to_string(), Ipld::Integer(42));
255        let data = Ipld::Map(map);
256
257        let encoded = codec.encode(&data).unwrap();
258        let decoded = codec.decode(&encoded).unwrap();
259        assert_eq!(decoded, data);
260    }
261
262    #[test]
263    fn test_dag_json_codec() {
264        let codec = DagJsonCodec;
265        let data = Ipld::String("hello".to_string());
266
267        let encoded = codec.encode(&data).unwrap();
268        let decoded = codec.decode(&encoded).unwrap();
269        assert_eq!(decoded, data);
270    }
271
272    #[test]
273    fn test_codec_registry() {
274        let registry = CodecRegistry::new();
275
276        // Default codecs should be registered
277        assert!(registry.has_codec(codec::RAW));
278        assert!(registry.has_codec(codec::DAG_CBOR));
279        assert!(registry.has_codec(codec::DAG_JSON));
280
281        // Get codec by code
282        let cbor_codec = registry.get(codec::DAG_CBOR).unwrap();
283        assert_eq!(cbor_codec.code(), codec::DAG_CBOR);
284        assert_eq!(cbor_codec.name(), "dag-cbor");
285    }
286
287    #[test]
288    fn test_registry_encode_decode() {
289        let registry = CodecRegistry::new();
290        let data = Ipld::Integer(12345);
291
292        // Encode with DAG-CBOR
293        let encoded = registry.encode(codec::DAG_CBOR, &data).unwrap();
294
295        // Decode back
296        let decoded = registry.decode(codec::DAG_CBOR, &encoded).unwrap();
297        assert_eq!(decoded, data);
298    }
299
300    #[test]
301    fn test_registry_list_codecs() {
302        let registry = CodecRegistry::new();
303        let codecs = registry.list_codecs();
304
305        assert!(codecs.contains(&codec::RAW));
306        assert!(codecs.contains(&codec::DAG_CBOR));
307        assert!(codecs.contains(&codec::DAG_JSON));
308        assert_eq!(codecs.len(), 3);
309    }
310
311    #[test]
312    fn test_registry_get_name() {
313        let registry = CodecRegistry::new();
314
315        assert_eq!(registry.get_name(codec::RAW).unwrap(), "raw");
316        assert_eq!(registry.get_name(codec::DAG_CBOR).unwrap(), "dag-cbor");
317        assert_eq!(registry.get_name(codec::DAG_JSON).unwrap(), "dag-json");
318        assert!(registry.get_name(0x9999).is_none());
319    }
320
321    #[test]
322    fn test_unregistered_codec_error() {
323        let registry = CodecRegistry::new();
324        let data = Ipld::Integer(42);
325
326        let result = registry.encode(0x9999, &data);
327        assert!(result.is_err());
328        assert!(result.unwrap_err().to_string().contains("not registered"));
329    }
330
331    #[test]
332    fn test_global_registry() {
333        let registry = global_codec_registry();
334
335        assert!(registry.has_codec(codec::RAW));
336        assert!(registry.has_codec(codec::DAG_CBOR));
337        assert!(registry.has_codec(codec::DAG_JSON));
338    }
339
340    #[test]
341    fn test_codec_replacement() {
342        let registry = CodecRegistry::new();
343
344        // Register a custom codec with same code
345        registry.register(Arc::new(RawCodec));
346
347        // Should still work
348        assert!(registry.has_codec(codec::RAW));
349    }
350
351    #[test]
352    fn test_codec_codes() {
353        assert_eq!(RawCodec.code(), codec::RAW);
354        assert_eq!(DagCborCodec.code(), codec::DAG_CBOR);
355        assert_eq!(DagJsonCodec.code(), codec::DAG_JSON);
356    }
357
358    #[test]
359    fn test_codec_names() {
360        assert_eq!(RawCodec.name(), "raw");
361        assert_eq!(DagCborCodec.name(), "dag-cbor");
362        assert_eq!(DagJsonCodec.name(), "dag-json");
363    }
364}