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}