Skip to main content

liminal_server/services/
actions.rs

1//! The production [`ServiceAction`] for alarms and launches (ACCESS-003 R1).
2//!
3//! An alarm or a launch request is a message: once the caller's grant on
4//! `alarms` or `launches` allowed it, its payload is published on that
5//! subject through the server's own publish path, once per operation id, for
6//! whoever serves it. `context-watch` is not acted on by a publish: a granted
7//! request on it is a context reading, reported to Lys through the
8//! [`ContextReporter`](super::context_watch::ContextReporter), and Lys
9//! decides what a crossed budget does.
10
11use super::context_watch::ContextReporter;
12use super::keys::{ServicePublisher, service_envelope};
13use super::{ServiceAction, ServiceEffectError, ServiceKind};
14use std::sync::Arc;
15
16/// The schema bytes an alarm is published under.
17pub const ALARMS_SCHEMA: &[u8] = b"liminal.services.alarms/1";
18/// The schema bytes a launch request is published under.
19pub const LAUNCHES_SCHEMA: &[u8] = b"liminal.services.launches/1";
20
21/// Alarms and launches published, context readings reported to Lys.
22#[derive(Debug)]
23pub struct PublishingActions {
24    publisher: Arc<dyn ServicePublisher>,
25    context: Arc<ContextReporter>,
26}
27
28impl PublishingActions {
29    /// Publishes through `publisher`; reports context readings through
30    /// `context`.
31    #[must_use]
32    pub const fn new(publisher: Arc<dyn ServicePublisher>, context: Arc<ContextReporter>) -> Self {
33        Self { publisher, context }
34    }
35}
36
37impl ServiceAction for PublishingActions {
38    fn act(
39        &self,
40        service: ServiceKind,
41        payload: &[u8],
42        operation: &str,
43    ) -> Result<(), ServiceEffectError> {
44        let schema = match service {
45            ServiceKind::Alarms => ALARMS_SCHEMA,
46            ServiceKind::Launches => LAUNCHES_SCHEMA,
47            ServiceKind::ContextWatch => return self.context.report(payload, operation),
48            ServiceKind::Seats | ServiceKind::Budgets | ServiceKind::SessionKeys => {
49                return Err(ServiceEffectError(format!(
50                    "{} is not acted on as a bare service",
51                    service.name()
52                )));
53            }
54        };
55        if payload.is_empty() {
56            return Err(ServiceEffectError(format!(
57                "an empty {} request carries nothing to publish",
58                service.name()
59            )));
60        }
61        self.publisher.publish(
62            service.subject_shape(),
63            &service_envelope(schema, payload),
64            operation,
65        )
66    }
67}