1use crate::application::Application;
2use crate::module::{ApplicationModule, ModuleRegistry};
3use crate::platform::Platform;
4use origin_accounts::{AccountService, AccountStore};
5use origin_auth::TokenStore;
6use origin_connector::{Connector, ConnectorRegistry};
7use origin_domain::{AppError, Clock, SystemClock};
8use origin_events::EventBus;
9use origin_http::HttpClient;
10use origin_jobs::Jobs;
11use origin_platform::{NoopNotificationService, NotificationService, Opener};
12use origin_secrets::{MemorySecretStore, SecretStore};
13use origin_settings::{Settings, StorageSettingsStore};
14use origin_storage::{Cache, MemoryStorage, Storage};
15use origin_sync::SyncEngine;
16use std::sync::Arc;
17
18#[derive(Debug, thiserror::Error)]
19pub enum BuildError {
20 #[error("no {component} configured — call `.{component}(...)` on the builder")]
24 MissingComponent { component: &'static str },
25
26 #[error("module `{module}` failed to register: {source}")]
27 ModuleRegistration {
28 module: &'static str,
29 #[source]
30 source: AppError,
31 },
32}
33
34#[derive(Debug)]
40pub struct ApplicationBuilder {
41 clock: Arc<dyn Clock>,
42 events: EventBus,
43 storage: Option<Arc<dyn Storage>>,
44 secrets: Option<Arc<dyn SecretStore>>,
45 notifications: Option<Arc<dyn NotificationService>>,
46 opener: Option<Arc<dyn Opener>>,
47 http: Option<Arc<dyn HttpClient>>,
48 connectors: ConnectorRegistry,
49 modules: Vec<Box<dyn ApplicationModule>>,
50}
51
52impl Default for ApplicationBuilder {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58impl ApplicationBuilder {
59 pub fn new() -> Self {
60 Self {
61 clock: Arc::new(SystemClock),
62 events: EventBus::new(),
63 storage: None,
64 secrets: None,
65 notifications: None,
66 opener: None,
67 http: None,
68 connectors: ConnectorRegistry::new(),
69 modules: Vec::new(),
70 }
71 }
72
73 pub fn in_memory() -> Self {
78 Self::new()
79 .storage(Arc::new(MemoryStorage::new()))
80 .secret_store(Arc::new(MemorySecretStore::new()))
81 .notifications(Arc::new(NoopNotificationService))
82 }
83
84 pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
85 self.clock = clock;
86 self
87 }
88
89 pub fn event_bus(mut self, events: EventBus) -> Self {
91 self.events = events;
92 self
93 }
94
95 pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self {
96 self.storage = Some(storage);
97 self
98 }
99
100 pub fn secret_store(mut self, secrets: Arc<dyn SecretStore>) -> Self {
101 self.secrets = Some(secrets);
102 self
103 }
104
105 pub fn notifications(mut self, notifications: Arc<dyn NotificationService>) -> Self {
106 self.notifications = Some(notifications);
107 self
108 }
109
110 pub fn opener(mut self, opener: Arc<dyn Opener>) -> Self {
113 self.opener = Some(opener);
114 self
115 }
116
117 pub fn http_client(mut self, http: Arc<dyn HttpClient>) -> Self {
122 self.http = Some(http);
123 self
124 }
125
126 pub fn connector(mut self, connector: impl Connector) -> Self {
131 self.connectors.insert(Arc::new(connector));
132 self
133 }
134
135 pub fn module(mut self, module: impl ApplicationModule) -> Self {
136 self.modules.push(Box::new(module));
137 self
138 }
139
140 pub fn build(self) -> Result<Application, BuildError> {
141 let storage = self.storage.ok_or(BuildError::MissingComponent {
142 component: "storage",
143 })?;
144 let secrets = self.secrets.ok_or(BuildError::MissingComponent {
145 component: "secret_store",
146 })?;
147 let notifications = self.notifications.ok_or(BuildError::MissingComponent {
148 component: "notifications",
149 })?;
150
151 let cache = Cache::new(storage.clone(), self.clock.clone());
152 let settings = Settings::new(Arc::new(StorageSettingsStore::new(
153 storage.clone(),
154 self.clock.clone(),
155 )));
156
157 let accounts = AccountService::new(
158 AccountStore::new(storage.clone(), self.clock.clone()),
159 TokenStore::new(secrets.clone()),
160 self.events.clone(),
161 storage.clone(),
162 self.clock.clone(),
163 );
164
165 let jobs = Jobs::new(self.events.clone(), self.clock.clone());
166 let sync = SyncEngine::new(storage.clone(), self.clock.clone(), self.events.clone());
167
168 let platform = Platform {
169 clock: self.clock,
170 events: self.events,
171 jobs,
172 sync,
173 storage,
174 cache,
175 secrets,
176 settings,
177 notifications,
178 accounts,
179 connectors: self.connectors,
180 opener: self.opener,
181 http: self.http,
182 };
183
184 let mut registry = ModuleRegistry::new(platform.clone());
185 for module in &self.modules {
186 let id = module.id();
187 tracing::debug!(module = id, "registering module");
188 module
189 .register(&mut registry)
190 .map_err(|source| BuildError::ModuleRegistration { module: id, source })?;
191 registry.record_module(id);
192 }
193
194 tracing::info!(modules = ?registry.module_ids(), "application built");
195 Ok(Application::new(platform, registry))
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::ModuleRegistry;
203 use origin_domain::Result;
204
205 #[derive(Debug)]
206 struct Counter(u32);
207
208 #[derive(Debug)]
209 struct CountingModule;
210
211 impl ApplicationModule for CountingModule {
212 fn id(&self) -> &'static str {
213 "counting"
214 }
215
216 fn register(&self, registry: &mut ModuleRegistry) -> Result<()> {
217 registry.provide(Arc::new(Counter(7)));
218 Ok(())
219 }
220 }
221
222 #[derive(Debug)]
223 struct FailingModule;
224
225 impl ApplicationModule for FailingModule {
226 fn id(&self) -> &'static str {
227 "failing"
228 }
229
230 fn register(&self, _registry: &mut ModuleRegistry) -> Result<()> {
231 Err(AppError::configuration("missing api key"))
232 }
233 }
234
235 #[test]
236 fn a_module_service_is_resolvable_by_type() {
237 let app = ApplicationBuilder::in_memory()
238 .module(CountingModule)
239 .build()
240 .unwrap();
241
242 assert_eq!(app.modules(), &["counting"]);
243 assert_eq!(app.require::<Counter>().unwrap().0, 7);
244 }
245
246 #[test]
247 fn resolving_an_unregistered_service_names_the_type() {
248 let app = ApplicationBuilder::in_memory().build().unwrap();
249
250 let error = app.require::<Counter>().unwrap_err();
251 assert!(error.to_string().contains("Counter"), "got: {error}");
252 }
253
254 #[test]
255 fn missing_storage_is_a_build_error_not_a_silent_default() {
256 let error = ApplicationBuilder::new().build().unwrap_err();
257 assert!(matches!(
258 error,
259 BuildError::MissingComponent {
260 component: "storage"
261 }
262 ));
263 }
264
265 #[test]
266 fn a_build_without_an_http_client_says_what_is_missing() {
267 let app = ApplicationBuilder::in_memory().build().unwrap();
268
269 let error = app.platform().http().unwrap_err();
270
271 assert_eq!(error.kind(), origin_domain::ErrorKind::Configuration);
272 assert!(error.to_string().contains("http_client"), "got: {error}");
273 }
274
275 #[test]
276 fn a_build_without_an_opener_reports_a_permission_error() {
277 let app = ApplicationBuilder::in_memory().build().unwrap();
278
279 assert_eq!(
281 app.platform().opener().unwrap_err().kind(),
282 origin_domain::ErrorKind::Permission
283 );
284 }
285
286 #[test]
287 fn registered_connectors_are_resolvable_and_unknown_ones_are_not() {
288 use origin_connector::{AuthKind, Connector, ConnectorDescriptor};
289 use origin_domain::{AccountId, ConnectorId};
290
291 #[derive(Debug)]
292 struct TestConnector;
293
294 #[async_trait::async_trait]
295 impl Connector for TestConnector {
296 fn id(&self) -> ConnectorId {
297 ConnectorId::new("test")
298 }
299
300 fn descriptor(&self) -> ConnectorDescriptor {
301 ConnectorDescriptor::new(self.id(), "Test", AuthKind::None)
302 }
303
304 async fn verify(
305 &self,
306 _account: &AccountId,
307 ) -> Result<origin_connector::AccountIdentity> {
308 unimplemented!("not needed for this test")
309 }
310 }
311
312 let app = ApplicationBuilder::in_memory()
313 .connector(TestConnector)
314 .build()
315 .unwrap();
316
317 assert_eq!(
318 app.platform().connectors.ids(),
319 vec![ConnectorId::new("test")]
320 );
321 assert!(
322 app.platform()
323 .connectors
324 .require(&ConnectorId::new("absent"))
325 .is_err()
326 );
327 }
328
329 #[test]
330 fn a_failing_module_names_itself() {
331 let error = ApplicationBuilder::in_memory()
332 .module(FailingModule)
333 .build()
334 .unwrap_err();
335
336 assert!(error.to_string().contains("failing"), "got: {error}");
337 }
338}