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::{
12 ConfirmationService, DenyingConfirmationService, GlobalShortcutService, MemoryProcessRunner,
13 MemoryWorkspaceFs, MemoryWorkspaceWatcher, NoopGlobalShortcutService, NoopNotificationService,
14 NotificationService, Opener, ProcessAllowlist, ProcessRunner, TrayService, WorkspaceFs,
15 WorkspaceWatcher,
16};
17use origin_secrets::{MemorySecretStore, SecretStore};
18use origin_settings::{Settings, StorageSettingsStore};
19use origin_storage::{Cache, MemoryStorage, Storage};
20use origin_sync::SyncEngine;
21use std::sync::Arc;
22
23#[derive(Debug, thiserror::Error)]
24pub enum BuildError {
25 #[error("no {component} configured — call `.{component}(...)` on the builder")]
29 MissingComponent { component: &'static str },
30
31 #[error("module `{module}` failed to register: {source}")]
32 ModuleRegistration {
33 module: &'static str,
34 #[source]
35 source: AppError,
36 },
37}
38
39#[derive(Debug)]
45pub struct ApplicationBuilder {
46 clock: Arc<dyn Clock>,
47 events: EventBus,
48 storage: Option<Arc<dyn Storage>>,
49 secrets: Option<Arc<dyn SecretStore>>,
50 notifications: Option<Arc<dyn NotificationService>>,
51 opener: Option<Arc<dyn Opener>>,
52 http: Option<Arc<dyn HttpClient>>,
53 confirmation: Arc<dyn ConfirmationService>,
56 tray: Option<Arc<dyn TrayService>>,
58 connectors: ConnectorRegistry,
59 modules: Vec<Box<dyn ApplicationModule>>,
60 workspace_fs: Option<Arc<dyn WorkspaceFs>>,
61 workspace_watcher: Option<Arc<dyn WorkspaceWatcher>>,
62 process_runner: Option<Arc<dyn ProcessRunner>>,
63 global_shortcuts: Option<Arc<dyn GlobalShortcutService>>,
64}
65
66impl Default for ApplicationBuilder {
67 fn default() -> Self {
68 Self::new()
69 }
70}
71
72impl ApplicationBuilder {
73 pub fn new() -> Self {
74 Self {
75 clock: Arc::new(SystemClock),
76 events: EventBus::new(),
77 storage: None,
78 secrets: None,
79 notifications: None,
80 opener: None,
81 http: None,
82 confirmation: Arc::new(DenyingConfirmationService),
83 tray: None,
84 connectors: ConnectorRegistry::new(),
85 modules: Vec::new(),
86 workspace_fs: None,
87 workspace_watcher: None,
88 process_runner: None,
89 global_shortcuts: None,
90 }
91 }
92
93 pub fn in_memory() -> Self {
98 Self::new()
99 .storage(Arc::new(MemoryStorage::new()))
100 .secret_store(Arc::new(MemorySecretStore::new()))
101 .notifications(Arc::new(NoopNotificationService))
102 .workspace_fs(Arc::new(MemoryWorkspaceFs::new()))
103 .workspace_watcher(Arc::new(MemoryWorkspaceWatcher::new()))
104 .process_runner(Arc::new(MemoryProcessRunner::success(
105 ProcessAllowlist::default(),
106 )))
107 .global_shortcuts(Arc::new(NoopGlobalShortcutService))
108 }
109
110 pub fn clock(mut self, clock: Arc<dyn Clock>) -> Self {
111 self.clock = clock;
112 self
113 }
114
115 pub fn event_bus(mut self, events: EventBus) -> Self {
117 self.events = events;
118 self
119 }
120
121 pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self {
122 self.storage = Some(storage);
123 self
124 }
125
126 pub fn secret_store(mut self, secrets: Arc<dyn SecretStore>) -> Self {
127 self.secrets = Some(secrets);
128 self
129 }
130
131 pub fn notifications(mut self, notifications: Arc<dyn NotificationService>) -> Self {
132 self.notifications = Some(notifications);
133 self
134 }
135
136 pub fn opener(mut self, opener: Arc<dyn Opener>) -> Self {
139 self.opener = Some(opener);
140 self
141 }
142
143 pub fn confirmation(mut self, confirmation: Arc<dyn ConfirmationService>) -> Self {
147 self.confirmation = confirmation;
148 self
149 }
150
151 pub fn tray(mut self, tray: Arc<dyn TrayService>) -> Self {
154 self.tray = Some(tray);
155 self
156 }
157
158 pub fn http_client(mut self, http: Arc<dyn HttpClient>) -> Self {
163 self.http = Some(http);
164 self
165 }
166
167 pub fn connector(mut self, connector: impl Connector) -> Self {
172 self.connectors.insert(Arc::new(connector));
173 self
174 }
175
176 pub fn workspace_fs(mut self, workspace_fs: Arc<dyn WorkspaceFs>) -> Self {
178 self.workspace_fs = Some(workspace_fs);
179 self
180 }
181
182 pub fn workspace_watcher(mut self, workspace_watcher: Arc<dyn WorkspaceWatcher>) -> Self {
184 self.workspace_watcher = Some(workspace_watcher);
185 self
186 }
187
188 pub fn process_runner(mut self, process_runner: Arc<dyn ProcessRunner>) -> Self {
190 self.process_runner = Some(process_runner);
191 self
192 }
193
194 pub fn global_shortcuts(mut self, global_shortcuts: Arc<dyn GlobalShortcutService>) -> Self {
196 self.global_shortcuts = Some(global_shortcuts);
197 self
198 }
199
200 pub fn module(mut self, module: impl ApplicationModule) -> Self {
201 self.modules.push(Box::new(module));
202 self
203 }
204
205 pub fn build(self) -> Result<Application, BuildError> {
206 let storage = self.storage.ok_or(BuildError::MissingComponent {
207 component: "storage",
208 })?;
209 let secrets = self.secrets.ok_or(BuildError::MissingComponent {
210 component: "secret_store",
211 })?;
212 let notifications = self.notifications.ok_or(BuildError::MissingComponent {
213 component: "notifications",
214 })?;
215
216 let cache = Cache::new(storage.clone(), self.clock.clone());
217 let settings = Settings::new(Arc::new(StorageSettingsStore::new(
218 storage.clone(),
219 self.clock.clone(),
220 )));
221
222 let accounts = AccountService::new(
223 AccountStore::new(storage.clone(), self.clock.clone()),
224 TokenStore::new(secrets.clone()),
225 self.events.clone(),
226 storage.clone(),
227 self.clock.clone(),
228 );
229
230 let jobs = Jobs::new(self.events.clone(), self.clock.clone());
231 let sync = SyncEngine::new(storage.clone(), self.clock.clone(), self.events.clone());
232
233 let platform = Platform {
234 clock: self.clock,
235 events: self.events,
236 jobs,
237 sync,
238 storage,
239 cache,
240 secrets,
241 settings,
242 notifications,
243 confirmation: self.confirmation,
244 tray: self.tray,
245 accounts,
246 connectors: self.connectors,
247 opener: self.opener,
248 http: self.http,
249 workspace_fs: self.workspace_fs,
250 workspace_watcher: self.workspace_watcher,
251 process_runner: self.process_runner,
252 global_shortcuts: self.global_shortcuts,
253 };
254
255 let mut registry = ModuleRegistry::new(platform.clone());
256 for module in &self.modules {
257 let id = module.id();
258 tracing::debug!(module = id, "registering module");
259 module
260 .register(&mut registry)
261 .map_err(|source| BuildError::ModuleRegistration { module: id, source })?;
262 registry.record_module(id);
263 }
264
265 tracing::info!(modules = ?registry.module_ids(), "application built");
266 Ok(Application::new(platform, registry))
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::ModuleRegistry;
274 use origin_domain::Result;
275
276 #[derive(Debug)]
277 struct Counter(u32);
278
279 #[derive(Debug)]
280 struct CountingModule;
281
282 impl ApplicationModule for CountingModule {
283 fn id(&self) -> &'static str {
284 "counting"
285 }
286
287 fn register(&self, registry: &mut ModuleRegistry) -> Result<()> {
288 registry.provide(Arc::new(Counter(7)));
289 Ok(())
290 }
291 }
292
293 #[derive(Debug)]
294 struct FailingModule;
295
296 impl ApplicationModule for FailingModule {
297 fn id(&self) -> &'static str {
298 "failing"
299 }
300
301 fn register(&self, _registry: &mut ModuleRegistry) -> Result<()> {
302 Err(AppError::configuration("missing api key"))
303 }
304 }
305
306 #[test]
307 fn a_module_service_is_resolvable_by_type() {
308 let app = ApplicationBuilder::in_memory()
309 .module(CountingModule)
310 .build()
311 .unwrap();
312
313 assert_eq!(app.modules(), &["counting"]);
314 assert_eq!(app.require::<Counter>().unwrap().0, 7);
315 }
316
317 #[test]
318 fn resolving_an_unregistered_service_names_the_type() {
319 let app = ApplicationBuilder::in_memory().build().unwrap();
320
321 let error = app.require::<Counter>().unwrap_err();
322 assert!(error.to_string().contains("Counter"), "got: {error}");
323 }
324
325 #[test]
326 fn missing_storage_is_a_build_error_not_a_silent_default() {
327 let error = ApplicationBuilder::new().build().unwrap_err();
328 assert!(matches!(
329 error,
330 BuildError::MissingComponent {
331 component: "storage"
332 }
333 ));
334 }
335
336 #[test]
337 fn a_build_without_an_http_client_says_what_is_missing() {
338 let app = ApplicationBuilder::in_memory().build().unwrap();
339
340 let error = app.platform().http().unwrap_err();
341
342 assert_eq!(error.kind(), origin_domain::ErrorKind::Configuration);
343 assert!(error.to_string().contains("http_client"), "got: {error}");
344 }
345
346 #[test]
347 fn a_build_without_an_opener_reports_a_permission_error() {
348 let app = ApplicationBuilder::in_memory().build().unwrap();
349
350 assert_eq!(
352 app.platform().opener().unwrap_err().kind(),
353 origin_domain::ErrorKind::Permission
354 );
355 }
356
357 #[test]
358 fn in_memory_wires_workspace_and_process_and_shortcuts() {
359 let app = ApplicationBuilder::in_memory().build().unwrap();
360
361 assert!(app.platform().workspace_fs().is_ok());
362 assert!(app.platform().workspace_watcher().is_ok());
363 assert!(app.platform().process_runner().is_ok());
364 assert!(app.platform().global_shortcuts().is_ok());
365 }
366
367 #[test]
368 fn clean_build_reports_missing_workspace_components() {
369 let app = ApplicationBuilder::new()
370 .storage(Arc::new(MemoryStorage::new()))
371 .secret_store(Arc::new(MemorySecretStore::new()))
372 .notifications(Arc::new(NoopNotificationService))
373 .build()
374 .unwrap();
375
376 assert!(app.platform().workspace_fs().is_err());
377 assert!(app.platform().workspace_watcher().is_err());
378 assert!(app.platform().process_runner().is_err());
379 assert!(app.platform().global_shortcuts().is_err());
380 }
381
382 #[test]
383 fn registered_connectors_are_resolvable_and_unknown_ones_are_not() {
384 use origin_connector::{AuthKind, Connector, ConnectorDescriptor};
385 use origin_domain::{AccountId, ConnectorId};
386
387 #[derive(Debug)]
388 struct TestConnector;
389
390 #[async_trait::async_trait]
391 impl Connector for TestConnector {
392 fn id(&self) -> ConnectorId {
393 ConnectorId::new("test")
394 }
395
396 fn descriptor(&self) -> ConnectorDescriptor {
397 ConnectorDescriptor::new(self.id(), "Test", AuthKind::None)
398 }
399
400 async fn verify(
401 &self,
402 _account: &AccountId,
403 ) -> Result<origin_connector::AccountIdentity> {
404 unimplemented!("not needed for this test")
405 }
406 }
407
408 let app = ApplicationBuilder::in_memory()
409 .connector(TestConnector)
410 .build()
411 .unwrap();
412
413 assert_eq!(
414 app.platform().connectors.ids(),
415 vec![ConnectorId::new("test")]
416 );
417 assert!(
418 app.platform()
419 .connectors
420 .require(&ConnectorId::new("absent"))
421 .is_err()
422 );
423 }
424
425 #[test]
426 fn a_failing_module_names_itself() {
427 let error = ApplicationBuilder::in_memory()
428 .module(FailingModule)
429 .build()
430 .unwrap_err();
431
432 assert!(error.to_string().contains("failing"), "got: {error}");
433 }
434}