Skip to main content

photon_core/
identity.rs

1//! Identity reconstruction port (injected at integration boundary).
2//!
3//! Publish captures `actor_json` on each event. When `#[photon::subscribe]` handlers run, the
4//! executor calls [`IdentityFactory::reconstruct`] so handlers execute with the same actor
5//! identity that triggered the publish — mirroring Chronon/Boson-style permission boundaries.
6
7use std::any::Any;
8
9use crate::error::IdentityError;
10
11/// Opaque actor handle for handler execution.
12///
13/// Bound as [`Send`] + [`Sync`] so handlers may take `Arc<dyn Actor>` across await
14/// points in the subscribe invoker future.
15///
16/// Implementors must provide the [`Any`] downcast helpers (standard bodies):
17///
18/// ```ignore
19/// fn as_any(&self) -> &dyn Any { self }
20/// fn as_any_mut(&mut self) -> &mut dyn Any { self }
21/// fn into_any(self: Box<Self>) -> Box<dyn Any> { self }
22/// ```
23///
24/// Or expand `actor_downcast_methods!` inside the `impl Actor` block.
25pub trait Actor: Send + Sync + Any {
26    /// Debug label for logs/tests.
27    fn label(&self) -> &str;
28
29    /// Downcast to [`Any`] (immutable).
30    fn as_any(&self) -> &dyn Any;
31
32    /// Downcast to [`Any`] (mutable).
33    fn as_any_mut(&mut self) -> &mut dyn Any;
34
35    /// Consume into a type-erased [`Any`] box for concrete downcasts in subscribe v2.
36    fn into_any(self: Box<Self>) -> Box<dyn Any>;
37}
38
39/// Expands the standard [`Actor`] [`Any`] downcast method bodies.
40///
41/// Place inside an `impl Actor for T { ... }` block alongside [`Actor::label`].
42#[macro_export]
43macro_rules! actor_downcast_methods {
44    () => {
45        fn as_any(&self) -> &dyn ::std::any::Any {
46            self
47        }
48        fn as_any_mut(&mut self) -> &mut dyn ::std::any::Any {
49            self
50        }
51        fn into_any(self: ::std::boxed::Box<Self>) -> ::std::boxed::Box<dyn ::std::any::Any> {
52            self
53        }
54    };
55}
56
57/// Reconstruct handler identity from captured actor JSON (handler boundary only).
58pub trait IdentityFactory: Send + Sync + 'static {
59    /// Build an actor for `#[photon::subscribe]` dispatch.
60    ///
61    /// # Errors
62    ///
63    /// Returns an error if the operation fails.
64    fn reconstruct(&self, actor_json: &str) -> Result<Box<dyn Actor>, IdentityError>;
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    struct TestActor {
72        label: String,
73    }
74
75    impl Actor for TestActor {
76        fn label(&self) -> &str {
77            &self.label
78        }
79        actor_downcast_methods!();
80    }
81
82    struct TestFactory;
83
84    impl IdentityFactory for TestFactory {
85        fn reconstruct(&self, actor_json: &str) -> Result<Box<dyn Actor>, IdentityError> {
86            if actor_json.contains("System") {
87                Ok(Box::new(TestActor {
88                    label: "ok".into(),
89                }))
90            } else {
91                Err(IdentityError::InvalidActor("missing System".into()))
92            }
93        }
94    }
95
96    #[test]
97    fn identity_factory_reconstructs() {
98        let factory = TestFactory;
99        let actor = factory
100            .reconstruct(r#"{"System":{"operation":"t"}}"#)
101            .expect("ok");
102        assert_eq!(actor.label(), "ok");
103    }
104
105    #[test]
106    fn actor_downcast_to_concrete() {
107        let factory = TestFactory;
108        let actor = factory
109            .reconstruct(r#"{"System":{"operation":"t"}}"#)
110            .expect("ok");
111        let concrete = actor
112            .into_any()
113            .downcast::<TestActor>()
114            .expect("TestActor");
115        assert_eq!(concrete.label(), "ok");
116    }
117}