Skip to content

Commit 8f9377e

Browse files
authored
Move publishing to a module (#26)
This change puts all the publishing machinery into a `publish` module, and includes this module if the corresponding feature is enabled (which it is by default). This will help to organize the crate better, especially as the consumer half of the API will be added in the coming weeks. Along with the module move, the `Message` type was renamed to `EncodableMessage`, to make it clear that this is for encoding. A corresponding DecodableMessage trait will be included with the consumer API, using a separate trait to enable encoding borrowed types and decoding into owned types.
1 parent 17ea5ff commit 8f9377e

File tree

13 files changed

+263
-236
lines changed

13 files changed

+263
-236
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ maintenance = { status = "actively-developed" }
1919

2020
[features]
2121
default = ["sink"]
22+
23+
# Whether publishing is enabled
24+
publish = []
25+
2226
# Publishers
2327
google = ["base64", "yup-oauth2", "hyper", "http", "serde_json", "serde", "serde/derive", "uuid/serde"]
28+
2429
# Validators
2530
json-schema = ["valico", "serde_json", "serde"]
2631
protobuf = ["prost"]
2732

28-
sink = ["futures-util/sink", "either"]
33+
sink = ["futures-util/sink", "either", "publish"]
2934

3035
[[example]]
3136
name = "publish"

examples/publish.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
use futures_util::stream::StreamExt;
2-
use hedwig::{publishers::GooglePubSubPublisher, Headers, Message, Publisher};
2+
use hedwig::{
3+
publish::{EncodableMessage, GooglePubSubPublisher, Publisher},
4+
Headers,
5+
};
36
use std::{env, sync::Arc, time::SystemTime};
47

58
#[derive(serde::Serialize)]
@@ -9,7 +12,7 @@ struct UserCreatedMessage {
912
user_id: String,
1013
}
1114

12-
impl<'a> Message for &'a UserCreatedMessage {
15+
impl<'a> EncodableMessage for &'a UserCreatedMessage {
1316
type Error = hedwig::validators::JsonSchemaValidatorError;
1417
type Validator = hedwig::validators::JsonSchemaValidator;
1518
fn topic(&self) -> &'static str {
@@ -91,7 +94,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error + 'static>> {
9194
uuid: uuid::Uuid::new_v4(),
9295
user_id: "U_123".into(),
9396
};
94-
let topic = Message::topic(&&message);
97+
let topic = EncodableMessage::topic(&&message);
9598
let validated = message.encode(&validator).unwrap();
9699
let mut publish = publisher.publish(topic, [validated].iter());
97100
while let Some(r) = publish.next().await {

src/lib.rs

Lines changed: 9 additions & 195 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
//! user_id: String,
5656
//! }
5757
//!
58-
//! impl<'a> hedwig::Message for &'a UserCreatedMessage {
58+
//! impl<'a> hedwig::publish::EncodableMessage for &'a UserCreatedMessage {
5959
//! type Error = hedwig::validators::JsonSchemaValidatorError;
6060
//! type Validator = hedwig::validators::JsonSchemaValidator;
6161
//! fn topic(&self) -> hedwig::Topic { "user.created" }
@@ -72,12 +72,12 @@
7272
//! }
7373
//!
7474
//! let publisher = /* Some publisher */
75-
//! # hedwig::publishers::NullPublisher;
75+
//! # hedwig::publish::NullPublisher;
7676
//! let validator = hedwig::validators::JsonSchemaValidator::new(schema)?;
77-
//! let mut batch = hedwig::PublishBatch::new();
77+
//! let mut batch = hedwig::publish::PublishBatch::new();
7878
//! batch.message(&validator, &UserCreatedMessage { user_id: String::from("U_123") });
7979
//! let mut result_stream = batch.publish(&publisher);
80-
//! let mut next_batch = hedwig::PublishBatch::new();
80+
//! let mut next_batch = hedwig::publish::PublishBatch::new();
8181
//! async {
8282
//! while let Some(result) = result_stream.next().await {
8383
//! match result {
@@ -105,29 +105,18 @@
105105
#![cfg_attr(not(test), deny(unused))]
106106
#![cfg_attr(docsrs, feature(doc_cfg))]
107107

108-
use std::{
109-
collections::BTreeMap,
110-
pin::Pin,
111-
task::{Context, Poll},
112-
time::SystemTime,
113-
};
108+
use std::{collections::BTreeMap, time::SystemTime};
114109

115-
use futures_util::{
116-
ready,
117-
stream::{self, Stream},
118-
};
119-
use pin_project::pin_project;
120110
use uuid::Uuid;
121111

122-
pub mod publishers;
112+
#[cfg(feature = "publish")]
113+
#[cfg_attr(docsrs, doc(cfg(feature = "publish")))]
114+
pub mod publish;
115+
123116
#[cfg(test)]
124117
mod tests;
125118
pub mod validators;
126119

127-
#[cfg(feature = "sink")]
128-
#[cfg_attr(docsrs, doc(cfg(feature = "sink")))]
129-
pub mod sink;
130-
131120
/// A message queue topic name to which messages can be published
132121
pub type Topic = &'static str;
133122

@@ -140,46 +129,6 @@ pub enum Error {
140129
EncodeMessage(#[source] Box<dyn std::error::Error + Send + Sync>),
141130
}
142131

143-
/// Message publishers.
144-
///
145-
/// Message publishers deliver a validated message to an endpoint, possibly a remote one. Message
146-
/// publishers may also additionally validate a message for publisher-specific requirements (e.g.
147-
/// size).
148-
pub trait Publisher {
149-
/// The identifier for a successfully published message.
150-
type MessageId: 'static;
151-
152-
/// The error that this publisher returns when publishing of a message fails.
153-
type MessageError: std::error::Error + Send + Sync + 'static;
154-
155-
/// The stream of results that the `publish` method returns.
156-
type PublishStream: Stream<Item = Result<Self::MessageId, Self::MessageError>>;
157-
158-
/// Publish a batch of messages.
159-
///
160-
/// The output stream shall return a result for each message in `messages` slice in order.
161-
fn publish<'a, I>(&self, topic: Topic, messages: I) -> Self::PublishStream
162-
where
163-
I: Iterator<Item = &'a ValidatedMessage> + DoubleEndedIterator + ExactSizeIterator;
164-
}
165-
166-
/// Types that can be encoded and published.
167-
pub trait Message {
168-
/// The errors that can occur when calling the [`Message::encode`] method.
169-
///
170-
/// Will typically match the errors returned by the [`Message::Validator`].
171-
type Error: std::error::Error + Send + Sync + 'static;
172-
173-
/// The validator to use for this message.
174-
type Validator;
175-
176-
/// Topic into which this message shall be published.
177-
fn topic(&self) -> Topic;
178-
179-
/// Encode the message payload.
180-
fn encode(self, validator: &Self::Validator) -> Result<ValidatedMessage, Self::Error>;
181-
}
182-
183132
/// Custom headers associated with a message.
184133
pub type Headers = BTreeMap<String, String>;
185134

@@ -236,138 +185,3 @@ impl ValidatedMessage {
236185
&self.data
237186
}
238187
}
239-
240-
/// A convenience builder for publishing in batches.
241-
#[derive(Default, Debug)]
242-
pub struct PublishBatch {
243-
messages: BTreeMap<Topic, Vec<ValidatedMessage>>,
244-
}
245-
246-
impl PublishBatch {
247-
/// Construct a new batch.
248-
pub fn new() -> Self {
249-
Self::default()
250-
}
251-
252-
/// Number of messages currently queued.
253-
pub fn len(&self) -> usize {
254-
self.messages.iter().fold(0, |acc, (_, v)| acc + v.len())
255-
}
256-
257-
/// Whether the batch is empty.
258-
pub fn is_empty(&self) -> bool {
259-
self.messages.iter().all(|(_, v)| v.is_empty())
260-
}
261-
262-
/// Add an already validated message to be published in this batch.
263-
pub fn push(&mut self, topic: Topic, validated: ValidatedMessage) -> &mut Self {
264-
self.messages.entry(topic).or_default().push(validated);
265-
self
266-
}
267-
268-
/// Validate and add a message to be published in this batch.
269-
pub fn message<M: Message>(
270-
&mut self,
271-
validator: &M::Validator,
272-
msg: M,
273-
) -> Result<&mut Self, Error> {
274-
let topic = msg.topic();
275-
let validated = msg
276-
.encode(validator)
277-
.map_err(|e| Error::EncodeMessage(e.into()))?;
278-
Ok(self.push(topic, validated))
279-
}
280-
281-
/// Publish all the enqueued messages, batching them for high efficiency.
282-
///
283-
/// The order in which messages were added to the batch and the order of messages as seen by
284-
/// the publisher is not strictly preserved. As thus, the output stream will not preserve the
285-
/// message ordering either.
286-
///
287-
/// Some kinds of errors that occur during publishing may not be transient. An example of such
288-
/// an error is attempting to publish a too large message with the [`GooglePubSubPublisher`].
289-
/// For
290-
/// errors like these retrying is most likely incorrect as they would just fail again.
291-
/// Publisher-specific error types may have methods to make a decision easier.
292-
///
293-
/// [`GooglePubSubPublisher`]: publishers::GooglePubSubPublisher
294-
pub fn publish<P>(self, publisher: &P) -> PublishBatchStream<P::PublishStream>
295-
where
296-
P: Publisher,
297-
P::PublishStream: Unpin,
298-
{
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-
})
372-
}
373-
}

0 commit comments

Comments
 (0)