plexor-core 0.1.0-alpha.2

Core library for the rust implementation of the Plexo distributed system architecture, providing the fundamental Plexus, Neuron, Codec, and Axon abstractions.
Documentation
// Copyright 2025 Alecks Gates
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use crate::codec::{Codec, CodecError, CodecName};
use crate::namespace::Namespace;
use crate::utils::struct_name_of_type;
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
use std::marker::PhantomData;
use std::sync::Arc;
use thiserror::Error;

#[derive(Debug, Clone, PartialEq, Error)]
pub enum NeuronError {
    #[error("Neuron encode error for {neuron_name}: {message}")]
    Encode {
        neuron_name: String,
        message: String,
    },
    #[error("Neuron decode error for {neuron_name}: {message}")]
    Decode {
        neuron_name: String,
        message: String,
    },
}

pub trait Neuron<T, C>: Debug + Send + Sync + 'static
where
    C: Codec<T> + CodecName + Send + Sync + 'static,
    T: Send + Sync + 'static,
{
    fn encode(&self, data: &T) -> Result<Vec<u8>, NeuronError>;
    fn decode(&self, data: &[u8]) -> Result<T, NeuronError>;
    fn name(&self) -> String;
    fn name_without_codec(&self) -> String;
    fn schema(&self) -> String;
    fn clone_to_box(&self) -> Box<dyn Neuron<T, C> + Send + Sync + 'static>;
    fn clone_to_arc(&self) -> Arc<dyn Neuron<T, C> + Send + Sync + 'static>;
}

pub struct NeuronImpl<T, C> {
    pub namespace: Arc<dyn Namespace>,
    pub schema: Option<String>,
    _codec_marker: PhantomData<fn() -> &'static ()>,
    _type: PhantomData<T>,
    _phantom_codec: PhantomData<C>,
}

impl<T, C> Clone for NeuronImpl<T, C>
where
    C: Codec<T> + CodecName,
{
    fn clone(&self) -> Self {
        NeuronImpl {
            namespace: self.namespace.clone(),
            schema: self.schema.clone(),
            _codec_marker: PhantomData,
            _type: PhantomData,
            _phantom_codec: PhantomData,
        }
    }
}

impl<T, C> NeuronImpl<T, C>
where
    C: Codec<T> + CodecName + Send + Sync + 'static,
    T: Send + Sync + 'static,
{
    /// Create a new NeuronImpl with the given namespace.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use std::sync::Arc;
    /// # use plexor_core::neuron::{Neuron, NeuronImpl};
    /// # use plexor_core::namespace::NamespaceImpl;
    /// # use plexor_core::codec::{Codec, CodecError, CodecName};
    /// #
    /// # #[derive(Debug, Clone)]
    /// # struct Dummy;
    /// # impl CodecName for Dummy { fn name() -> &'static str { "dummy" } }
    /// # impl Codec<Dummy> for Dummy {
    /// #     fn encode(_: &Dummy) -> Result<Vec<u8>, CodecError> { Ok(vec![]) }
    /// #     fn decode(_: &[u8]) -> Result<Dummy, CodecError> { Ok(Dummy) }
    /// # }
    /// #
    /// let ns = Arc::new(NamespaceImpl { delimiter: ".", parts: vec!["test"] });
    /// let neuron = NeuronImpl::<Dummy, Dummy>::new(ns);
    /// assert_eq!(neuron.name(), "test.Dummy.dummy");
    /// ```
    #[must_use]
    pub fn new(ns: Arc<dyn Namespace>) -> NeuronImpl<T, C> {
        NeuronImpl {
            namespace: ns,
            schema: None,
            _codec_marker: PhantomData,
            _type: PhantomData,
            _phantom_codec: PhantomData,
        }
    }

    /// Helper to create an Arc<NeuronImpl<T, C>>
    #[must_use]
    pub fn new_arc(ns: Arc<dyn Namespace>) -> Arc<Self> {
        Arc::new(Self::new(ns))
    }

    #[must_use]
    pub fn with_schema(mut self, schema: impl Into<String>) -> Self {
        self.schema = Some(schema.into());
        self
    }
}

impl<T, C> Neuron<T, C> for NeuronImpl<T, C>
where
    C: Codec<T> + CodecName + Send + Sync + 'static,
    T: Send + Sync + 'static,
    Self: Send + Sync + 'static,
{
    fn encode(&self, data: &T) -> Result<Vec<u8>, NeuronError> {
        C::encode(data).map_err(|err| match err {
            CodecError::Encode(message) => NeuronError::Encode {
                neuron_name: self.name(),
                message,
            },
            CodecError::Decode(message) => NeuronError::Decode {
                neuron_name: self.name(),
                message,
            },
        })
    }

    fn decode(&self, data: &[u8]) -> Result<T, NeuronError> {
        C::decode(data).map_err(|err| match err {
            CodecError::Encode(message) => NeuronError::Encode {
                neuron_name: self.name(),
                message,
            },
            CodecError::Decode(message) => NeuronError::Decode {
                neuron_name: self.name(),
                message,
            },
        })
    }

    fn name(&self) -> String {
        self.namespace
            .with_suffix(vec![struct_name_of_type::<T>(), C::name()])
    }

    fn name_without_codec(&self) -> String {
        self.namespace.with_suffix(vec![struct_name_of_type::<T>()])
    }

    fn schema(&self) -> String {
        self.schema.clone().unwrap_or_default()
    }

    fn clone_to_box(&self) -> Box<dyn Neuron<T, C> + Send + Sync + 'static> {
        Box::new(self.clone())
    }

    fn clone_to_arc(&self) -> Arc<dyn Neuron<T, C> + Send + Sync + 'static> {
        Arc::new(self.clone())
    }
}

impl<T, C> Debug for NeuronImpl<T, C>
where
    C: Codec<T> + CodecName,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_struct("NeuronImpl")
            .field("namespace", &self.namespace.path())
            .field("codec_name", &C::name())
            .field("type", &struct_name_of_type::<T>())
            .finish()
    }
}

impl<T, C> Display for NeuronImpl<T, C>
where
    C: Codec<T> + CodecName + Send + Sync + 'static,
    T: Send + Sync + 'static,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        write!(f, "{}", self.name())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::{DebugCodec, DebugStruct, test_namespace};

    #[test]
    fn test_neuron() {
        let d = DebugStruct {
            foo: 42,
            bar: "spanish inquisition!".to_string(),
        };

        let ns = test_namespace();

        let neuron: NeuronImpl<DebugStruct, DebugCodec> = NeuronImpl::new(ns.clone());
        assert_eq!(neuron.name(), "dev.plexo.DebugStruct.debug");
        assert_eq!(neuron.name_without_codec(), "dev.plexo.DebugStruct");

        let encoded = neuron.encode(&d).expect("Encoding should succeed in test");
        assert_eq!(
            String::from_utf8_lossy(&encoded),
            r#"DebugStruct { foo: 42, bar: "spanish inquisition!" }"#
        );
        let decoded = neuron
            .decode(&encoded)
            .expect("Decoding should succeed in test");
        assert_eq!(decoded.foo, d.foo);
        assert_eq!(decoded.bar, d.bar);

        let boxed_neuron = neuron.clone_to_box();
        assert_eq!(boxed_neuron.name(), "dev.plexo.DebugStruct.debug");

        let arced_neuron = neuron.clone_to_arc();
        assert_eq!(arced_neuron.name(), "dev.plexo.DebugStruct.debug");
    }

    #[test]
    fn test_neuron_display() {
        let ns = test_namespace();
        let neuron = NeuronImpl::<DebugStruct, DebugCodec>::new(ns);
        assert_eq!(format!("{neuron}"), "dev.plexo.DebugStruct.debug");
    }
}