Skip to main content

plexor_core/
neuron.rs

1// Copyright 2025 Alecks Gates
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this
5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7use crate::codec::{Codec, CodecError, CodecName};
8use crate::namespace::Namespace;
9use crate::utils::struct_name_of_type;
10use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
11use std::marker::PhantomData;
12use std::sync::Arc;
13use thiserror::Error;
14
15#[derive(Debug, Clone, PartialEq, Error)]
16pub enum NeuronError {
17    #[error("Neuron encode error for {neuron_name}: {message}")]
18    Encode {
19        neuron_name: String,
20        message: String,
21    },
22    #[error("Neuron decode error for {neuron_name}: {message}")]
23    Decode {
24        neuron_name: String,
25        message: String,
26    },
27}
28
29pub trait Neuron<T, C>: Debug + Send + Sync + 'static
30where
31    C: Codec<T> + CodecName + Send + Sync + 'static,
32    T: Send + Sync + 'static,
33{
34    fn encode(&self, data: &T) -> Result<Vec<u8>, NeuronError>;
35    fn decode(&self, data: &[u8]) -> Result<T, NeuronError>;
36    fn name(&self) -> String;
37    fn name_without_codec(&self) -> String;
38    fn schema(&self) -> String;
39    fn clone_to_box(&self) -> Box<dyn Neuron<T, C> + Send + Sync + 'static>;
40    fn clone_to_arc(&self) -> Arc<dyn Neuron<T, C> + Send + Sync + 'static>;
41}
42
43pub struct NeuronImpl<T, C> {
44    pub namespace: Arc<dyn Namespace>,
45    pub schema: Option<String>,
46    _codec_marker: PhantomData<fn() -> &'static ()>,
47    _type: PhantomData<T>,
48    _phantom_codec: PhantomData<C>,
49}
50
51impl<T, C> Clone for NeuronImpl<T, C>
52where
53    C: Codec<T> + CodecName,
54{
55    fn clone(&self) -> Self {
56        NeuronImpl {
57            namespace: self.namespace.clone(),
58            schema: self.schema.clone(),
59            _codec_marker: PhantomData,
60            _type: PhantomData,
61            _phantom_codec: PhantomData,
62        }
63    }
64}
65
66impl<T, C> NeuronImpl<T, C>
67where
68    C: Codec<T> + CodecName + Send + Sync + 'static,
69    T: Send + Sync + 'static,
70{
71    /// Create a new NeuronImpl with the given namespace.
72    ///
73    /// # Examples
74    ///
75    /// ```rust
76    /// # use std::sync::Arc;
77    /// # use plexor_core::neuron::{Neuron, NeuronImpl};
78    /// # use plexor_core::namespace::NamespaceImpl;
79    /// # use plexor_core::codec::{Codec, CodecError, CodecName};
80    /// #
81    /// # #[derive(Debug, Clone)]
82    /// # struct Dummy;
83    /// # impl CodecName for Dummy { fn name() -> &'static str { "dummy" } }
84    /// # impl Codec<Dummy> for Dummy {
85    /// #     fn encode(_: &Dummy) -> Result<Vec<u8>, CodecError> { Ok(vec![]) }
86    /// #     fn decode(_: &[u8]) -> Result<Dummy, CodecError> { Ok(Dummy) }
87    /// # }
88    /// #
89    /// let ns = Arc::new(NamespaceImpl { delimiter: ".", parts: vec!["test"] });
90    /// let neuron = NeuronImpl::<Dummy, Dummy>::new(ns);
91    /// assert_eq!(neuron.name(), "test.Dummy.dummy");
92    /// ```
93    #[must_use]
94    pub fn new(ns: Arc<dyn Namespace>) -> NeuronImpl<T, C> {
95        NeuronImpl {
96            namespace: ns,
97            schema: None,
98            _codec_marker: PhantomData,
99            _type: PhantomData,
100            _phantom_codec: PhantomData,
101        }
102    }
103
104    /// Helper to create an Arc<NeuronImpl<T, C>>
105    #[must_use]
106    pub fn new_arc(ns: Arc<dyn Namespace>) -> Arc<Self> {
107        Arc::new(Self::new(ns))
108    }
109
110    #[must_use]
111    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
112        self.schema = Some(schema.into());
113        self
114    }
115}
116
117impl<T, C> Neuron<T, C> for NeuronImpl<T, C>
118where
119    C: Codec<T> + CodecName + Send + Sync + 'static,
120    T: Send + Sync + 'static,
121    Self: Send + Sync + 'static,
122{
123    fn encode(&self, data: &T) -> Result<Vec<u8>, NeuronError> {
124        C::encode(data).map_err(|err| match err {
125            CodecError::Encode(message) => NeuronError::Encode {
126                neuron_name: self.name(),
127                message,
128            },
129            CodecError::Decode(message) => NeuronError::Decode {
130                neuron_name: self.name(),
131                message,
132            },
133        })
134    }
135
136    fn decode(&self, data: &[u8]) -> Result<T, NeuronError> {
137        C::decode(data).map_err(|err| match err {
138            CodecError::Encode(message) => NeuronError::Encode {
139                neuron_name: self.name(),
140                message,
141            },
142            CodecError::Decode(message) => NeuronError::Decode {
143                neuron_name: self.name(),
144                message,
145            },
146        })
147    }
148
149    fn name(&self) -> String {
150        self.namespace
151            .with_suffix(vec![struct_name_of_type::<T>(), C::name()])
152    }
153
154    fn name_without_codec(&self) -> String {
155        self.namespace.with_suffix(vec![struct_name_of_type::<T>()])
156    }
157
158    fn schema(&self) -> String {
159        self.schema.clone().unwrap_or_default()
160    }
161
162    fn clone_to_box(&self) -> Box<dyn Neuron<T, C> + Send + Sync + 'static> {
163        Box::new(self.clone())
164    }
165
166    fn clone_to_arc(&self) -> Arc<dyn Neuron<T, C> + Send + Sync + 'static> {
167        Arc::new(self.clone())
168    }
169}
170
171impl<T, C> Debug for NeuronImpl<T, C>
172where
173    C: Codec<T> + CodecName,
174{
175    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
176        f.debug_struct("NeuronImpl")
177            .field("namespace", &self.namespace.path())
178            .field("codec_name", &C::name())
179            .field("type", &struct_name_of_type::<T>())
180            .finish()
181    }
182}
183
184impl<T, C> Display for NeuronImpl<T, C>
185where
186    C: Codec<T> + CodecName + Send + Sync + 'static,
187    T: Send + Sync + 'static,
188{
189    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
190        write!(f, "{}", self.name())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::test_utils::{DebugCodec, DebugStruct, test_namespace};
198
199    #[test]
200    fn test_neuron() {
201        let d = DebugStruct {
202            foo: 42,
203            bar: "spanish inquisition!".to_string(),
204        };
205
206        let ns = test_namespace();
207
208        let neuron: NeuronImpl<DebugStruct, DebugCodec> = NeuronImpl::new(ns.clone());
209        assert_eq!(neuron.name(), "dev.plexo.DebugStruct.debug");
210        assert_eq!(neuron.name_without_codec(), "dev.plexo.DebugStruct");
211
212        let encoded = neuron.encode(&d).expect("Encoding should succeed in test");
213        assert_eq!(
214            String::from_utf8_lossy(&encoded),
215            r#"DebugStruct { foo: 42, bar: "spanish inquisition!" }"#
216        );
217        let decoded = neuron
218            .decode(&encoded)
219            .expect("Decoding should succeed in test");
220        assert_eq!(decoded.foo, d.foo);
221        assert_eq!(decoded.bar, d.bar);
222
223        let boxed_neuron = neuron.clone_to_box();
224        assert_eq!(boxed_neuron.name(), "dev.plexo.DebugStruct.debug");
225
226        let arced_neuron = neuron.clone_to_arc();
227        assert_eq!(arced_neuron.name(), "dev.plexo.DebugStruct.debug");
228    }
229
230    #[test]
231    fn test_neuron_display() {
232        let ns = test_namespace();
233        let neuron = NeuronImpl::<DebugStruct, DebugCodec>::new(ns);
234        assert_eq!(format!("{neuron}"), "dev.plexo.DebugStruct.debug");
235    }
236}