Skip to main content

photon_runtime/
builder.rs

1//! [`Photon`] builder — storage port + backend assembly.
2//!
3//! See the crate [Getting started](https://docs.rs/uf-photon/latest/photon/#getting-started)
4//! for Embedded and Brokered walkthroughs.
5
6use std::sync::Arc;
7
8use photon_telemetry::{install_ops_log, OpsLog};
9
10use photon_backend::{
11    instrumentation, BackendContext, EmbeddedBackend, ExecutorServices, GenericPhotonBackend,
12    InProcStoragePort, PhotonBackend, PhotonError, Result, RetentionHook, RetentionPolicy,
13    StoragePort, TopicRegistry, TransportCrypto,
14};
15
16use crate::executor::ExecutorController;
17use crate::{Photon, PhotonRuntimeState};
18
19type BackendInstallFn = Box<dyn FnOnce(BackendContext) -> Result<Arc<dyn PhotonBackend>> + Send>;
20
21/// Builder for constructing [`Photon`] runtimes.
22///
23/// Wire a [`StoragePort`] (or accept the default in-process port), optionally install inventory
24/// discovery and ops telemetry, then [`build`](Self::build). Keep the returned [`Photon`] handle
25/// for `publish_on` / `subscribe_on`.
26///
27/// | Topology | Typical wiring |
28/// |----------|----------------|
29/// | **Embedded** | Default builder, or [`storage_port`](Self::storage_port) with `SQLite` |
30/// | **Brokered** | [`storage_port`](Self::storage_port) with NATS/Kafka/Fluvio on **every** binary |
31///
32/// Getting started: [Embedded](https://docs.rs/uf-photon/latest/photon/#embedded-one-binary),
33/// [Brokered](https://docs.rs/uf-photon/latest/photon/#brokered-publisher--worker-binaries).
34///
35/// # Examples
36///
37/// Default mem path (loads `PHOTON_TRANSPORT_KEY` via [`TransportCrypto::from_env`]):
38///
39/// ```rust,no_run
40/// use photon_runtime::Photon;
41///
42/// # fn main() -> photon_backend::Result<()> {
43/// let _photon = Photon::builder().auto_registry().build()?;
44/// # Ok(())
45/// # }
46/// ```
47///
48/// Explicit storage port:
49///
50/// ```rust,no_run
51/// use std::sync::Arc;
52///
53/// use photon_backend::{InProcStoragePort, StoragePort, TransportCrypto};
54/// use photon_runtime::Photon;
55///
56/// # fn main() -> photon_backend::Result<()> {
57/// let port: Arc<dyn StoragePort> = Arc::new(InProcStoragePort::new(
58///     TransportCrypto::from_env()?,
59/// ));
60/// let _photon = Photon::builder().storage_port(port).auto_registry().build()?;
61/// # Ok(())
62/// # }
63/// ```
64#[derive(Default)]
65pub struct PhotonBuilder {
66    storage_port: Option<Arc<dyn StoragePort>>,
67    backend: Option<Arc<dyn PhotonBackend>>,
68    backend_install: Option<BackendInstallFn>,
69    use_auto_registry: bool,
70    ops_log: Option<Arc<dyn OpsLog>>,
71    retention_policy: Option<RetentionPolicy>,
72    retention_hook: Option<Arc<dyn RetentionHook>>,
73}
74
75impl PhotonBuilder {
76    /// Explicit storage port (defaults to in-process `mem` via [`InProcStoragePort`]).
77    ///
78    /// Use this for `SQLite` (Embedded durable) and for broker adapters (Brokered — same port config
79    /// on publisher and worker). See
80    /// [Getting started → Brokered](https://docs.rs/uf-photon/latest/photon/#brokered-publisher--worker-binaries).
81    ///
82    /// # Example
83    ///
84    /// ```rust,no_run
85    /// use std::sync::Arc;
86    ///
87    /// use photon_backend::{InProcStoragePort, StoragePort, TransportCrypto};
88    /// use photon_runtime::Photon;
89    ///
90    /// # fn main() -> photon_backend::Result<()> {
91    /// let port: Arc<dyn StoragePort> = Arc::new(InProcStoragePort::new(
92    ///     TransportCrypto::from_env()?,
93    /// ));
94    /// let _photon = Photon::builder().storage_port(port).auto_registry().build()?;
95    /// # Ok(())
96    /// # }
97    /// ```
98    #[must_use]
99    pub fn storage_port(mut self, port: Arc<dyn StoragePort>) -> Self {
100        self.storage_port = Some(port);
101        self
102    }
103
104    /// Pre-built backend instance.
105    ///
106    /// Prefer [`storage_port`](Self::storage_port) for normal hosts. Use this when you already
107    /// constructed a [`PhotonBackend`] (tests, custom delivery stacks).
108    ///
109    /// # Errors
110    ///
111    /// [`build`](Self::build) returns an error if both this and [`backend_with_context`](Self::backend_with_context)
112    /// are set.
113    #[must_use]
114    pub fn backend(mut self, backend: Arc<dyn PhotonBackend>) -> Self {
115        self.backend = Some(backend);
116        self.backend_install = None;
117        self
118    }
119
120    /// Build backend from shared [`BackendContext`] (custom install closures).
121    ///
122    /// Prefer [`storage_port`](Self::storage_port) for adapter wiring. This is for hosts that
123    /// install a custom [`PhotonBackend`] from the registry context.
124    ///
125    /// # Example
126    ///
127    /// ```rust,no_run
128    /// use std::sync::Arc;
129    ///
130    /// use photon_backend::{BackendContext, EmbeddedBackend};
131    /// use photon_runtime::Photon;
132    ///
133    /// # fn main() -> photon_backend::Result<()> {
134    /// let _photon = Photon::builder()
135    ///     .backend_with_context(|ctx: BackendContext| EmbeddedBackend::install_mem(ctx))
136    ///     .auto_registry()
137    ///     .build()?;
138    /// # Ok(())
139    /// # }
140    /// ```
141    #[must_use]
142    pub fn backend_with_context<F>(mut self, install: F) -> Self
143    where
144        F: FnOnce(BackendContext) -> Result<Arc<dyn PhotonBackend>> + Send + 'static,
145    {
146        self.backend = None;
147        self.backend_install = Some(Box::new(install));
148        self
149    }
150
151    /// Shorthand for [`backend_with_context`](Self::backend_with_context) with
152    /// [`EmbeddedBackend::install_mem`](photon_backend::GenericPhotonBackend::install_mem).
153    ///
154    /// Equivalent to the default path when you also omit [`storage_port`](Self::storage_port)
155    /// (in-process mem). Prefer the default [`build`](Self::build) unless you need an explicit
156    /// install fn.
157    ///
158    /// # Example
159    ///
160    /// ```rust,no_run
161    /// use photon_runtime::Photon;
162    ///
163    /// # fn main() -> photon_backend::Result<()> {
164    /// let _photon = Photon::builder().mem_backend().auto_registry().build()?;
165    /// # Ok(())
166    /// # }
167    /// ```
168    #[must_use]
169    pub fn mem_backend(mut self) -> Self {
170        self.backend = None;
171        self.backend_install = Some(Box::new(EmbeddedBackend::install_mem));
172        self
173    }
174
175    /// Install a concrete [`OpsLog`] adapter before build.
176    ///
177    /// Runnable: `cargo run -p uf-photon --example telemetry_ops_log --features runtime,mem`.
178    ///
179    /// # Example
180    ///
181    /// ```rust,no_run
182    /// use photon_runtime::Photon;
183    /// use photon_telemetry::ConsoleOpsLog;
184    ///
185    /// # fn main() -> photon_backend::Result<()> {
186    /// let _photon = Photon::builder()
187    ///     .ops_log(ConsoleOpsLog)
188    ///     .auto_registry()
189    ///     .build()?;
190    /// # Ok(())
191    /// # }
192    /// ```
193    #[must_use]
194    pub fn ops_log(mut self, log: impl OpsLog + 'static) -> Self {
195        self.ops_log = Some(Arc::new(log));
196        self
197    }
198
199    /// Install a shared [`OpsLog`] trait object before build.
200    #[must_use]
201    pub fn ops_log_arc(mut self, log: Arc<dyn OpsLog>) -> Self {
202        self.ops_log = Some(log);
203        self
204    }
205
206    /// Discover `#[photon::topic]` / `#[photon::subscribe]` descriptors via Quark inventory.
207    ///
208    /// Required when using the macros in the same crate graph as the host. Without this, the
209    /// topic registry stays empty and the executor has nothing to dispatch.
210    ///
211    /// Runnable: `cargo run -p uf-photon --example embedded_mem --features runtime,mem`.
212    #[must_use]
213    pub const fn auto_registry(mut self) -> Self {
214        self.use_auto_registry = true;
215        self
216    }
217
218    /// Override default retention policy (env fallbacks apply for unset fields).
219    #[must_use]
220    pub fn retention_policy(mut self, policy: RetentionPolicy) -> Self {
221        self.retention_policy = Some(policy);
222        self
223    }
224
225    /// Host hook for extra subscriptions and legal-hold floors.
226    #[must_use]
227    pub fn retention_hook(mut self, hook: Arc<dyn RetentionHook>) -> Self {
228        self.retention_hook = Some(hook);
229        self
230    }
231
232    /// Assemble the [`Photon`] runtime.
233    ///
234    /// # Defaults
235    ///
236    /// - **Storage port:** [`InProcStoragePort`] with [`TransportCrypto::from_env`] when
237    ///   [`storage_port`](Self::storage_port) was not set — requires `PHOTON_TRANSPORT_KEY`
238    /// - **Backend:** generic backend over that port when no custom [`backend`](Self::backend) /
239    ///   [`backend_with_context`](Self::backend_with_context) is set
240    /// - **Registry:** empty unless [`auto_registry`](Self::auto_registry) was called
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if transport crypto cannot load from the environment, a custom install
245    /// fn fails, or both `backend` and `backend_with_context` were set.
246    pub fn build(self) -> Result<Photon> {
247        if let Some(log) = self.ops_log {
248            install_ops_log(log);
249        }
250
251        let registry = if self.use_auto_registry {
252            TopicRegistry::auto_discover()
253        } else {
254            TopicRegistry::new()
255        };
256
257        let port = match self.storage_port {
258            Some(port) => port,
259            None => Arc::new(InProcStoragePort::new(TransportCrypto::from_env()?)),
260        };
261
262        let ctx = BackendContext {
263            registry: registry.clone(),
264        };
265
266        let backend = match (self.backend, self.backend_install) {
267            (Some(b), None) => b,
268            (None, Some(install)) => install(ctx)?,
269            (None, None) => GenericPhotonBackend::install_with_port(
270                BackendContext { registry },
271                Arc::clone(&port),
272            )?,
273            (Some(_), Some(_)) => {
274                return Err(PhotonError::Internal(
275                    "PhotonBuilder: set backend() or backend_with_context(), not both".into(),
276                ));
277            }
278        };
279
280        let retention_policy = self.retention_policy.unwrap_or_default();
281        let runtime = PhotonRuntimeState {
282            storage_port: Arc::clone(&port),
283            executor_services: Arc::new(ExecutorServices::new(
284                port,
285                retention_policy,
286                self.retention_hook,
287            )),
288            executor: Arc::new(ExecutorController::default()),
289        };
290
291        let backend = instrumentation::wrap_backend(backend);
292        Ok(Photon::new(backend, runtime))
293    }
294}