Skip to main content

origin_app/
builder.rs

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    /// A required component was never provided. There is no implicit fallback for
21    /// storage or credentials — silently defaulting those would ship an application
22    /// that loses data or keeps tokens in process memory.
23    #[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/// Assembles an [`Application`] from ports and modules.
35///
36/// Clock and event bus have exactly one sensible default and are pre-filled. Storage,
37/// credentials and notifications must be chosen explicitly — or taken from
38/// [`ApplicationBuilder::in_memory`] for tests.
39#[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    /// A fully in-memory application: no files, no keychain, no notifications.
74    ///
75    /// This is the configuration from ADR-0002 — it makes the whole application
76    /// testable without starting a desktop session.
77    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    /// Share an existing bus, e.g. one the host layer already subscribed to.
90    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    /// Grant the ability to open external URLs. Omitted means the product does not
111    /// have the capability at all, not that it is disabled at runtime.
112    pub fn opener(mut self, opener: Arc<dyn Opener>) -> Self {
113        self.opener = Some(opener);
114        self
115    }
116
117    /// Give the application an HTTP client.
118    ///
119    /// One client for the whole application: it owns the connection pool, and several
120    /// would defeat keep-alive (ADR-0014).
121    pub fn http_client(mut self, http: Arc<dyn HttpClient>) -> Self {
122        self.http = Some(http);
123        self
124    }
125
126    /// Register a connector.
127    ///
128    /// The set of external services a build can reach is fixed here, at compile time,
129    /// and is therefore auditable (ADR-0006).
130    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        // Capabilities are absent, not disabled: nothing can turn this on at runtime.
280        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}