Skip to main content

rightkit_daemon/
service.rs

1//! `Service::builder()`: one [`Handler`] and one [`MethodTable`], hosted on the local transport
2//! and, with the `remote` feature, offered to signed devices as `POST /v1/call/{method}`.
3//!
4//! ```text
5//! let service = Service::builder()
6//!     .methods(MethodTable::new().method("echo", MethodPolicy::local(["app"]).and_remote(["venture"])))
7//!     .handler(Arc::new(MyHandler))
8//!     .local(&suite, ServiceConfig::new("svc", "role", 5, "1.0", Allowlist::new()), HostOptions::new())
9//!     .remote(DaemonConfig::new(state_dir))
10//!     .start()?;
11//! // Local callers use `Client`; serve `service.daemon()` over TLS with `remote::serve_blocking`.
12//! ```
13//!
14//! The method table decides which transport each method is on, so a `transport: both` method is
15//! reachable by [`Client::call`](crate::Client::call) and by a signed device with the same params
16//! and the handler's own error codes. Methods that are remote-only by nature (`upload.chunk`,
17//! `oauth.callback`, `webhook.inbound`) and the existing v1 routes keep their own routes.
18
19use std::sync::Arc;
20
21use crate::contract::{Handler, MethodTable, TableError};
22use crate::registration::{Registration, RegistrationError, TransportProfile};
23use crate::server::{host_with_options, HostError, HostOptions, Hosted, ServiceConfig};
24use crate::suite::Suite;
25
26#[cfg(feature = "remote")]
27use crate::remote::{Clock, Daemon, DaemonConfig, DeviceCalls, OAuthCallbackHandler, SystemClock};
28
29/// Why [`ServiceBuilder::start`] failed.
30#[derive(Debug)]
31pub enum BuildError {
32    /// A required part was not given (`methods`, `handler`).
33    Missing(&'static str),
34    /// Neither `local` nor `remote` was configured.
35    NoTransport,
36    /// The method table was refused.
37    Table(TableError),
38    /// The local host did not start.
39    Host(Box<HostError>),
40    /// The remote daemon did not open its state.
41    Io(std::io::Error),
42    /// A [`Registration`] was refused (see [`RegistrationError`]).
43    Registration(RegistrationError),
44}
45
46impl std::fmt::Display for BuildError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            BuildError::Missing(what) => write!(f, "service builder: {what} not set"),
50            BuildError::NoTransport => f.write_str("service builder: no transport configured"),
51            BuildError::Table(e) => write!(f, "service builder: {e}"),
52            BuildError::Host(e) => write!(f, "service builder: local host: {e}"),
53            BuildError::Io(e) => write!(f, "service builder: remote state: {e}"),
54            BuildError::Registration(e) => write!(f, "service builder: {e}"),
55        }
56    }
57}
58
59impl std::error::Error for BuildError {}
60
61struct LocalSpec {
62    suite: Suite,
63    cfg: ServiceConfig,
64    opts: HostOptions,
65}
66
67#[cfg(feature = "remote")]
68struct RemoteSpec {
69    cfg: DaemonConfig,
70    clock: Arc<dyn Clock>,
71    oauth: Option<Arc<dyn OAuthCallbackHandler>>,
72}
73
74/// A running service: the local host, the remote daemon, or both, over one handler.
75pub struct Service {
76    table: MethodTable,
77    hosted: Option<Hosted>,
78    #[cfg(feature = "remote")]
79    daemon: Option<Arc<Daemon>>,
80}
81
82impl Service {
83    pub fn builder() -> ServiceBuilder {
84        ServiceBuilder::default()
85    }
86
87    pub fn methods(&self) -> &MethodTable {
88        &self.table
89    }
90
91    /// The local transport host, when configured. Dropping the service releases it.
92    pub fn local(&self) -> Option<&Hosted> {
93        self.hosted.as_ref()
94    }
95
96    /// The remote daemon, when configured. Serve it over TLS with
97    /// [`remote::serve`](crate::remote::serve) or [`remote::serve_blocking`](crate::remote::serve_blocking).
98    #[cfg(feature = "remote")]
99    pub fn daemon(&self) -> Option<&Arc<Daemon>> {
100        self.daemon.as_ref()
101    }
102
103    /// Release the local host. The remote listener stops with its own `StopHandle`.
104    pub fn shutdown(&self) {
105        if let Some(h) = &self.hosted {
106            h.shutdown();
107        }
108    }
109}
110
111#[derive(Default)]
112pub struct ServiceBuilder {
113    table: Option<MethodTable>,
114    handler: Option<Arc<dyn Handler>>,
115    local: Option<LocalSpec>,
116    profile: Option<TransportProfile>,
117    #[cfg(feature = "remote")]
118    remote: Option<RemoteSpec>,
119}
120
121impl ServiceBuilder {
122    /// Declare the whole service as one [`Registration`] (identity, method descriptors and a
123    /// handler per method) and host it on the local transport. Replaces `methods`, `handler` and
124    /// `local`; the `remote` calls stay available only for a registration whose profile is
125    /// [`TransportProfile::LocalAndRemote`] (`start` refuses them otherwise). Opens no listener
126    /// beyond the current-user-only local endpoint.
127    pub fn register(
128        mut self,
129        suite: &Suite,
130        registration: Registration,
131        opts: HostOptions,
132    ) -> Result<Self, BuildError> {
133        let (identity, profile, table, handler) = registration
134            .into_parts()
135            .map_err(BuildError::Registration)?;
136        self.table = Some(table);
137        self.handler = Some(handler);
138        self.local = Some(LocalSpec {
139            suite: suite.clone(),
140            cfg: identity.to_config(),
141            opts,
142        });
143        self.profile = Some(profile);
144        Ok(self)
145    }
146
147    /// The one source of method policy for every transport.
148    pub fn methods(mut self, table: MethodTable) -> Self {
149        self.table = Some(table);
150        self
151    }
152
153    /// The one handler both transports call.
154    pub fn handler(mut self, handler: Arc<dyn Handler>) -> Self {
155        self.handler = Some(handler);
156        self
157    }
158
159    /// Host on the local transport. The allowlist in `cfg` is replaced by the one the method
160    /// table implies.
161    pub fn local(mut self, suite: &Suite, cfg: ServiceConfig, opts: HostOptions) -> Self {
162        self.local = Some(LocalSpec {
163            suite: suite.clone(),
164            cfg,
165            opts,
166        });
167        self
168    }
169
170    /// Open the remote daemon with the system clock and no OAuth handler.
171    #[cfg(feature = "remote")]
172    pub fn remote(self, cfg: DaemonConfig) -> Self {
173        self.remote_with(cfg, Arc::new(SystemClock), None)
174    }
175
176    /// [`remote`](Self::remote) with an injected clock and an OAuth callback handler.
177    #[cfg(feature = "remote")]
178    pub fn remote_with(
179        mut self,
180        cfg: DaemonConfig,
181        clock: Arc<dyn Clock>,
182        oauth: Option<Arc<dyn OAuthCallbackHandler>>,
183    ) -> Self {
184        self.remote = Some(RemoteSpec { cfg, clock, oauth });
185        self
186    }
187
188    /// Open the configured transports. The remote state opens first (it fails without side
189    /// effects); the local host publishes `service.json` last.
190    pub fn start(self) -> Result<Service, BuildError> {
191        let table = self.table.ok_or(BuildError::Missing("methods"))?;
192        let handler = self.handler.ok_or(BuildError::Missing("handler"))?;
193        #[cfg(feature = "remote")]
194        let has_remote = self.remote.is_some();
195        #[cfg(not(feature = "remote"))]
196        let has_remote = false;
197        // A local-only registration never gets a remote daemon, whichever order the calls came in.
198        if has_remote && self.profile == Some(TransportProfile::Local) {
199            return Err(BuildError::Registration(
200                RegistrationError::RemoteNotDeclared,
201            ));
202        }
203        if self.local.is_none() && !has_remote {
204            return Err(BuildError::NoTransport);
205        }
206
207        #[cfg(feature = "remote")]
208        let daemon = match self.remote {
209            Some(r) => {
210                let d = Daemon::open(r.cfg, r.clock, r.oauth)
211                    .map_err(BuildError::Io)?
212                    .with_calls(DeviceCalls::new(table.clone(), handler.clone()));
213                Some(Arc::new(d))
214            }
215            None => None,
216        };
217
218        let hosted = match self.local {
219            Some(l) => {
220                let cfg = l.cfg.with_methods(&table);
221                Some(
222                    host_with_options(&l.suite, cfg, handler, l.opts)
223                        .map_err(|e| BuildError::Host(Box::new(e)))?,
224                )
225            }
226            None => None,
227        };
228        Ok(Service {
229            table,
230            hosted,
231            #[cfg(feature = "remote")]
232            daemon,
233        })
234    }
235}