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}