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}