Skip to main content

ruststream_sea_file/
stream.rs

1//! [`FileStream`]: the subscription descriptor for the file transport.
2
3use std::future::{Future, ready};
4
5#[cfg(feature = "asyncapi")]
6use ruststream::asyncapi::{Binding, Bindings};
7use ruststream::runtime::IntoSource;
8use ruststream::{AddressedCopies, RedeliveryAddress, RedeliveryAddressed, SubscriptionSource};
9#[cfg(feature = "asyncapi")]
10use serde::Serialize;
11
12use crate::error::SeaFileError;
13use crate::file::ConnectedFileBroker;
14use crate::subscriber::FileSubscriber;
15
16/// A subscription descriptor for one stream key in the file.
17///
18/// A plain descriptor follows the live tail; where reading begins is the framework's
19/// `start_at(..)` clause with a [`FilePosition`](crate::FilePosition) (or a live seek through
20/// the [`SeekHandle`](crate::SeekHandle) context key). [`replay`](Self::replay) is the one
21/// reading mode the position API cannot express: it reads the finished file and completes the
22/// stream at its end instead of following live writes.
23///
24/// Implements [`SubscriptionSource`], so it can sit inline in the `#[subscriber(..)]`
25/// decorator, and [`IntoSource`], so the manual path's `subscriber(..)` constructor takes it
26/// the way it takes a subject string. The
27/// [FileStream](https://powersemmi.github.io/ruststream-sea-file/latest/components/subscriptions/file-stream/)
28/// page shows it in a service.
29///
30/// # Examples
31///
32/// A replay of a recorded file:
33///
34/// ```
35/// use ruststream_sea_file::file::prelude::*;
36/// use serde::Deserialize;
37///
38/// #[derive(Debug, Deserialize)]
39/// struct Order {
40///     id: u64,
41/// }
42///
43/// #[subscriber(FileStream::new("orders").replay())]
44/// async fn audit(order: &Order) -> HandlerOutcome {
45///     tracing::info!(order.id, "audited");
46///     HandlerOutcome::ack()
47/// }
48///
49/// #[ruststream::app]
50/// fn app() -> impl App {
51///     RustStream::new(AppInfo::new("audit", "0.1.0"))
52///         .with_broker(FileBroker::new("/var/lib/orders.ss").existing_only(), |b| {
53///             b.include(audit);
54///         })
55/// }
56/// ```
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[must_use]
59pub struct FileStream {
60    stream: String,
61    replay: bool,
62}
63
64impl FileStream {
65    /// Names the stream key.
66    pub fn new(stream: impl Into<String>) -> Self {
67        Self {
68            stream: stream.into(),
69            replay: false,
70        }
71    }
72
73    /// Replays the retained file from the beginning instead of following live writes; the
74    /// subscription completes at the end of the file.
75    ///
76    /// Every message the file holds is delivered before the subscription completes. The end is
77    /// the end-of-stream mark a writer left with
78    /// [`end_with_eos`](crate::FileBroker::end_with_eos), or the end of the file when there is
79    /// none.
80    pub fn replay(mut self) -> Self {
81        self.replay = true;
82        self
83    }
84
85    /// The stream key this descriptor resolves.
86    #[must_use]
87    pub fn stream(&self) -> &str {
88        &self.stream
89    }
90
91    pub(crate) fn replay_value(&self) -> bool {
92        self.replay
93    }
94
95    /// Where a deferred copy of a delayed message reaches this subscription again: the stream
96    /// key, on both reading modes.
97    ///
98    /// A publisher on this broker appends under that key and the subscription reads the append.
99    /// A replay reads the region the file already held and completes, so the copy reaches it
100    /// while it is still short of that point and is left unread once it has passed it; the
101    /// address is the same either way, and the delay is what decides.
102    fn redelivery_address_value(&self) -> RedeliveryAddress {
103        RedeliveryAddress::new(self.stream.clone())
104    }
105
106    /// What this subscription adds to its channel in the generated `AsyncAPI` document.
107    #[cfg(feature = "asyncapi")]
108    fn channel_extension(&self) -> Bindings {
109        channel_extension(self.stream())
110    }
111
112    /// Rejects descriptors that cannot form a subscription, before any I/O.
113    pub(crate) fn validate(&self) -> Result<(), SeaFileError> {
114        if self.stream.is_empty() {
115            return Err(SeaFileError::Invalid("stream key must be non-empty".into()));
116        }
117        Ok(())
118    }
119}
120
121/// The extension key the file transport's channel description sits under.
122#[cfg(feature = "asyncapi")]
123const FILE_EXTENSION: &str = "x-ruststream-file";
124
125/// What the document reports about a file channel: the stream key the channel resolves to.
126#[cfg(feature = "asyncapi")]
127#[derive(Debug, Serialize)]
128#[serde(rename_all = "camelCase")]
129struct FileChannel<'a> {
130    stream_key: &'a str,
131}
132
133/// What a file channel adds to the generated `AsyncAPI` document: the stream key it resolves to.
134///
135/// The specification lists no binding for a file transport and its protocol keys are a closed
136/// list, so an `x-` extension is the only lawful place for what a stream file knows. The path of
137/// the file is the server's own description and is not repeated here.
138///
139/// Both ends of the transport answer through this: a subscription passes the key its descriptor
140/// names, a publish policy passes the destination the mount site resolved. One stream key is both
141/// ends of the file, so the two descriptions agree by construction.
142#[cfg(feature = "asyncapi")]
143pub(crate) fn channel_extension(stream_key: &str) -> Bindings {
144    let body = FileChannel { stream_key };
145    // A binding that fails to build is a binding the document goes without: a broker never holds
146    // up a service over a description of itself.
147    Binding::extension(FILE_EXTENSION, &body)
148        .map(|binding| Bindings::new().with(binding))
149        .unwrap_or_default()
150}
151
152impl IntoSource for FileStream {
153    type Source = Self;
154
155    fn into_source(self) -> Self {
156        self
157    }
158}
159
160impl SubscriptionSource<ConnectedFileBroker> for FileStream {
161    type Subscriber = FileSubscriber;
162    // One stream key is both ends of the file: a publisher appends under it and this
163    // subscription reads the append, so the runtime's deferred copies have an address.
164    type Copies = AddressedCopies;
165
166    fn name(&self) -> &str {
167        self.stream()
168    }
169
170    async fn subscribe(
171        self,
172        connected: &ConnectedFileBroker,
173    ) -> Result<FileSubscriber, SeaFileError> {
174        connected.subscribe_stream(self).await
175    }
176
177    #[cfg(feature = "asyncapi")]
178    fn channel_bindings(&self) -> Bindings {
179        self.channel_extension()
180    }
181}
182
183impl RedeliveryAddressed<ConnectedFileBroker> for FileStream {
184    fn redelivery_address(
185        &self,
186        _connected: &ConnectedFileBroker,
187    ) -> impl Future<Output = Result<RedeliveryAddress, SeaFileError>> {
188        // The descriptor already knows the answer; nothing is asked of the connection.
189        ready(Ok(self.redelivery_address_value()))
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn empty_stream_keys_are_rejected_before_io() {
199        assert!(FileStream::new("").validate().is_err());
200    }
201
202    #[test]
203    fn replay_reads_the_retained_file() {
204        assert!(FileStream::new("orders").replay().replay_value());
205        assert!(!FileStream::new("orders").replay_value());
206    }
207}