58
58
//! impl<'a> hedwig::Message for &'a UserCreatedMessage {
59
59
//! type Error = hedwig::validators::JsonSchemaValidatorError;
60
60
//! type Validator = hedwig::validators::JsonSchemaValidator;
61
- //! fn topic(&self) -> &'static str { "user.created" }
61
+ //! fn topic(&self) -> hedwig::Topic { "user.created" }
62
62
//! fn encode(self, validator: &Self::Validator)
63
63
//! -> Result<hedwig::ValidatedMessage, Self::Error> {
64
64
//! validator.validate(
99
99
broken_intra_doc_links,
100
100
clippy:: all,
101
101
unsafe_code,
102
- unreachable_pub,
103
- unused
102
+ unreachable_pub
104
103
) ]
104
+ #![ allow( clippy:: unknown_clippy_lints) ]
105
+ #![ cfg_attr( not( test) , deny( unused) ) ]
105
106
#![ cfg_attr( docsrs, feature( doc_cfg) ) ]
106
107
107
- use std:: { collections:: BTreeMap , time:: SystemTime } ;
108
+ use std:: {
109
+ collections:: BTreeMap ,
110
+ pin:: Pin ,
111
+ task:: { Context , Poll } ,
112
+ time:: SystemTime ,
113
+ } ;
108
114
109
- use futures_util:: stream:: { self , Stream , StreamExt } ;
115
+ use futures_util:: {
116
+ ready,
117
+ stream:: { self , Stream } ,
118
+ } ;
119
+ use pin_project:: pin_project;
110
120
use uuid:: Uuid ;
111
121
112
122
pub mod publishers;
113
123
#[ cfg( test) ]
114
124
mod tests;
115
125
pub mod validators;
116
126
127
+ #[ cfg( feature = "sink" ) ]
128
+ #[ cfg_attr( docsrs, doc( cfg( feature = "sink" ) ) ) ]
129
+ pub mod sink;
130
+
131
+ /// A message queue topic name to which messages can be published
132
+ pub type Topic = & ' static str ;
133
+
117
134
/// All errors that may be returned when operating top level APIs.
118
135
#[ derive( Debug , thiserror:: Error ) ]
119
136
#[ non_exhaustive]
@@ -141,7 +158,7 @@ pub trait Publisher {
141
158
/// Publish a batch of messages.
142
159
///
143
160
/// The output stream shall return a result for each message in `messages` slice in order.
144
- fn publish < ' a , I > ( & self , topic : & ' static str , messages : I ) -> Self :: PublishStream
161
+ fn publish < ' a , I > ( & self , topic : Topic , messages : I ) -> Self :: PublishStream
145
162
where
146
163
I : Iterator < Item = & ' a ValidatedMessage > + DoubleEndedIterator + ExactSizeIterator ;
147
164
}
@@ -157,7 +174,7 @@ pub trait Message {
157
174
type Validator ;
158
175
159
176
/// Topic into which this message shall be published.
160
- fn topic ( & self ) -> & ' static str ;
177
+ fn topic ( & self ) -> Topic ;
161
178
162
179
/// Encode the message payload.
163
180
fn encode ( self , validator : & Self :: Validator ) -> Result < ValidatedMessage , Self :: Error > ;
@@ -170,6 +187,8 @@ pub type Headers = BTreeMap<String, String>;
170
187
///
171
188
/// The only way to construct this is via a validator.
172
189
#[ derive( Debug , Clone ) ]
190
+ // derive Eq only in tests so that users can't foot-shoot an expensive == over data
191
+ #[ cfg_attr( test, derive( PartialEq , Eq ) ) ]
173
192
pub struct ValidatedMessage {
174
193
/// Unique message identifier.
175
194
id : Uuid ,
@@ -221,7 +240,7 @@ impl ValidatedMessage {
221
240
/// A convenience builder for publishing in batches.
222
241
#[ derive( Default , Debug ) ]
223
242
pub struct PublishBatch {
224
- messages : BTreeMap < & ' static str , Vec < ValidatedMessage > > ,
243
+ messages : BTreeMap < Topic , Vec < ValidatedMessage > > ,
225
244
}
226
245
227
246
impl PublishBatch {
@@ -241,7 +260,7 @@ impl PublishBatch {
241
260
}
242
261
243
262
/// Add an already validated message to be published in this batch.
244
- pub fn push ( & mut self , topic : & ' static str , validated : ValidatedMessage ) -> & mut Self {
263
+ pub fn push ( & mut self , topic : Topic , validated : ValidatedMessage ) -> & mut Self {
245
264
self . messages . entry ( topic) . or_default ( ) . push ( validated) ;
246
265
self
247
266
}
@@ -272,28 +291,83 @@ impl PublishBatch {
272
291
/// Publisher-specific error types may have methods to make a decision easier.
273
292
///
274
293
/// [`GooglePubSubPublisher`]: publishers::GooglePubSubPublisher
275
- pub fn publish < P > (
276
- self ,
277
- publisher : & P ,
278
- ) -> impl Stream <
279
- Item = (
280
- Result < P :: MessageId , P :: MessageError > ,
281
- & ' static str ,
282
- ValidatedMessage ,
283
- ) ,
284
- >
294
+ pub fn publish < P > ( self , publisher : & P ) -> PublishBatchStream < P :: PublishStream >
285
295
where
286
296
P : Publisher ,
287
297
P :: PublishStream : Unpin ,
288
298
{
289
- self . messages
290
- . into_iter ( )
291
- . map ( |( topic, msgs) | {
292
- publisher
293
- . publish ( topic, msgs. iter ( ) )
294
- . zip ( stream:: iter ( msgs. into_iter ( ) ) )
295
- . map ( move |( r, m) | ( r, topic, m) )
296
- } )
297
- . collect :: < stream:: SelectAll < _ > > ( )
299
+ PublishBatchStream (
300
+ self . messages
301
+ . into_iter ( )
302
+ . map ( |( topic, msgs) | TopicPublishStream :: new ( topic, msgs, publisher) )
303
+ . collect :: < stream:: SelectAll < _ > > ( ) ,
304
+ )
305
+ }
306
+ }
307
+
308
+ /// The stream returned by the method [`PublishBatch::publish`](PublishBatch::publish)
309
+ // This stream and TopicPublishStream are made explicit types instead of combinators like
310
+ // map/zip/etc so that callers can refer to a concrete return type instead of `impl Stream`
311
+ #[ pin_project]
312
+ #[ derive( Debug ) ]
313
+ pub struct PublishBatchStream < P > ( #[ pin] stream:: SelectAll < TopicPublishStream < P > > ) ;
314
+
315
+ impl < P > Stream for PublishBatchStream < P >
316
+ where
317
+ P : Stream + Unpin ,
318
+ {
319
+ type Item = ( P :: Item , Topic , ValidatedMessage ) ;
320
+
321
+ fn poll_next ( self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Option < Self :: Item > > {
322
+ self . project ( ) . 0 . poll_next ( cx)
323
+ }
324
+ }
325
+
326
+ #[ pin_project]
327
+ #[ derive( Debug ) ]
328
+ struct TopicPublishStream < P > {
329
+ topic : Topic ,
330
+ messages : std:: vec:: IntoIter < ValidatedMessage > ,
331
+
332
+ #[ pin]
333
+ publish_stream : P ,
334
+ }
335
+
336
+ impl < P > TopicPublishStream < P > {
337
+ fn new < Pub > ( topic : Topic , messages : Vec < ValidatedMessage > , publisher : & Pub ) -> Self
338
+ where
339
+ Pub : Publisher < PublishStream = P > ,
340
+ P : Stream < Item = Result < Pub :: MessageId , Pub :: MessageError > > ,
341
+ {
342
+ let publish_stream = publisher. publish ( topic, messages. iter ( ) ) ;
343
+ Self {
344
+ topic,
345
+ messages : messages. into_iter ( ) ,
346
+ publish_stream,
347
+ }
348
+ }
349
+ }
350
+
351
+ impl < P > Stream for TopicPublishStream < P >
352
+ where
353
+ P : Stream ,
354
+ {
355
+ type Item = ( P :: Item , Topic , ValidatedMessage ) ;
356
+
357
+ fn poll_next ( self : Pin < & mut Self > , cx : & mut Context ) -> Poll < Option < Self :: Item > > {
358
+ let this = self . project ( ) ;
359
+
360
+ // `map` has lifetime constraints that aren't nice here
361
+ #[ allow( clippy:: manual_map) ]
362
+ Poll :: Ready ( match ready ! ( this. publish_stream. poll_next( cx) ) {
363
+ None => None ,
364
+ Some ( stream_item) => Some ( (
365
+ stream_item,
366
+ this. topic ,
367
+ this. messages
368
+ . next ( )
369
+ . expect ( "should be as many messages as publishes" ) ,
370
+ ) ) ,
371
+ } )
298
372
}
299
373
}
0 commit comments