pub mod filters;
pub mod transforms;
use std::convert::Infallible;
use either::Either;
use transforms::async_fn::AsyncFnTransform;
use self::{
filters::{Filter, FilterAction},
transforms::{
Transform, TransformAction,
async_fn::IntoTransformedEntries,
field::{Field, TransformField, TransformFieldAdapter},
},
};
use crate::{
actres_try,
cancellation_token::CancellationToken,
entry::Entry,
error::FetcherError,
external_save::ExternalSave,
maybe_send::{MaybeSend, MaybeSendSync},
sinks::{Sink, SinkAction},
sources::Source,
task::entry_to_msg_map::EntryToMsgMap,
};
pub trait Action: MaybeSendSync {
type Err: Into<FetcherError>;
fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
context: ActionContext<'_, S, E>,
) -> impl Future<Output = ActionResult<Self::Err>> + MaybeSend
where
S: Source,
E: ExternalSave;
}
#[derive(Debug)]
pub enum ActionResult<E, T = Vec<Entry>> {
Ok(T),
Err(E),
Terminated,
}
#[derive(Debug)]
pub struct ActionContext<'a, S, E> {
pub source: Option<&'a mut S>,
pub entry_to_msg_map: Option<&'a mut EntryToMsgMap<E>>,
pub tag: Option<&'a str>,
pub cancel_token: Option<&'a CancellationToken>,
}
pub fn filter<F>(f: F) -> FilterAction<F>
where
F: Filter,
{
FilterAction(f)
}
pub fn transform<T>(t: T) -> TransformAction<T>
where
T: Transform,
{
TransformAction(t)
}
pub fn transform_field<T>(field: Field, t: T) -> TransformAction<TransformFieldAdapter<T>>
where
T: TransformField,
{
transform(TransformFieldAdapter {
field,
transformator: t,
})
}
pub fn transform_body<T>(t: T) -> TransformAction<TransformFieldAdapter<T>>
where
T: TransformField,
{
transform_field(Field::Body, t)
}
pub fn transform_fn<F, Fut, T>(f: F) -> TransformAction<AsyncFnTransform<F>>
where
F: Fn(Entry) -> Fut + MaybeSendSync,
Fut: Future<Output = T> + MaybeSend,
T: IntoTransformedEntries,
{
transform(transforms::async_fn::AsyncFnTransform(f))
}
pub fn sink<S>(s: S) -> SinkAction<S>
where
S: Sink,
{
SinkAction(s)
}
macro_rules! reborrow_ctx {
($ctx:expr) => {{
let ctx = $ctx;
ActionContext {
source: ctx.source.as_deref_mut(),
entry_to_msg_map: ctx.entry_to_msg_map.as_deref_mut(),
tag: ctx.tag.as_deref(),
cancel_token: ctx.cancel_token.as_deref(),
}
}};
}
impl Action for () {
type Err = Infallible;
async fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
_context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
ActionResult::Ok(entries)
}
}
impl<A> Action for Option<A>
where
A: Action,
{
type Err = A::Err;
async fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
let Some(act) = self else {
return ActionResult::Ok(entries);
};
act.apply(entries, context).await
}
}
impl<A1, A2> Action for Either<A1, A2>
where
A1: Action,
A2: Action,
{
type Err = FetcherError;
async fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
match self {
Either::Left(x) => x.apply(entries, context).await.map_err(Into::into),
Either::Right(x) => x.apply(entries, context).await.map_err(Into::into),
}
}
}
impl Action for Infallible {
type Err = Infallible;
async fn apply<S, E>(
&mut self,
_entries: Vec<Entry>,
_context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
match *self {}
}
}
#[cfg(feature = "nightly")]
impl Action for ! {
type Err = !;
async fn apply<S, E>(
&mut self,
_entries: Vec<Entry>,
_context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
match *self {}
}
}
impl<A> Action for &mut A
where
A: Action,
{
type Err = A::Err;
fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
context: ActionContext<'_, S, E>,
) -> impl Future<Output = ActionResult<Self::Err>> + MaybeSend
where
S: Source,
E: ExternalSave,
{
(*self).apply(entries, context)
}
}
impl<A> Action for (A,)
where
A: Action,
{
type Err = A::Err;
async fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
context: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
self.0.apply(entries, context).await
}
}
macro_rules! impl_action_for_tuples {
($($type_name:ident)+) => {
impl<$($type_name),+> Action for ($($type_name),+)
where
$($type_name: Action),+
{
type Err = FetcherError;
#[expect(non_snake_case, reason = "it's fine to re-use the names to make calling the macro easier")]
async fn apply<S, E>(
&mut self,
entries: Vec<Entry>,
mut ctx: ActionContext<'_, S, E>,
) -> ActionResult<Self::Err>
where
S: Source,
E: ExternalSave,
{
let mut action_num = 0;
let ($($type_name),+) = self;
$(
if ctx.cancel_token.as_ref().is_some_and(|tok| tok.is_cancelled()) {
tracing::debug!("Task terminated while in the middle of action pipeline execution. Not all have actions have been run to completion.");
return ActionResult::Terminated;
}
#[allow(unused_assignments, reason = "last iteration won't use it, it's fine")]
{
tracing::trace!("Running action #{action_num}");
action_num += 1;
}
let act_result = $type_name.apply(entries, reborrow_ctx!(&mut ctx)).await;
let entries = actres_try!(act_result.map_err(Into::into));
)+
ActionResult::Ok(entries)
}
}
}
}
impl_action_for_tuples!(A1 A2);
impl_action_for_tuples!(A1 A2 A3);
impl_action_for_tuples!(A1 A2 A3 A4);
impl_action_for_tuples!(A1 A2 A3 A4 A5);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7 A8);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7 A8 A9);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7 A8 A9 A10);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11);
impl_action_for_tuples!(A1 A2 A3 A4 A5 A6 A7 A8 A9 A10 A11 A12);
impl<E> ActionResult<E> {
pub fn map_err<O, F>(self, op: O) -> ActionResult<F>
where
O: FnOnce(E) -> F,
{
match self {
ActionResult::Ok(items) => ActionResult::Ok(items),
ActionResult::Err(e) => ActionResult::Err(op(e)),
ActionResult::Terminated => ActionResult::Terminated,
}
}
}
impl<T, E> From<Result<T, E>> for ActionResult<E, T> {
fn from(value: Result<T, E>) -> Self {
match value {
Ok(t) => ActionResult::Ok(t),
Err(e) => ActionResult::Err(e),
}
}
}
#[macro_export]
macro_rules! actres_try {
($res:expr $(,)?) => {
match ActionResult::from($res) {
ActionResult::Ok(items) => items,
ActionResult::Err(e) => return ActionResult::Err(From::from(e)),
ActionResult::Terminated => return ActionResult::Terminated,
}
};
}
impl Default for ActionContext<'_, (), ()> {
fn default() -> Self {
Self {
source: None,
entry_to_msg_map: None,
tag: None,
cancel_token: None,
}
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, Instant};
use tokio::join;
use crate::{Task, actions::transform_fn, cancellation_token::CancellationToken};
#[tokio::test]
async fn cancel_token_stops_task_mid_work() {
const ACTION_DELAY_MS: u64 = 200;
let (cancel_token, tx) = CancellationToken::new();
let request_stop_in_100ms = async move {
tokio::time::sleep(Duration::from_millis(100)).await;
tx.send(()).unwrap();
};
let long_noop_transform = async |entry| {
tokio::time::sleep(Duration::from_millis(ACTION_DELAY_MS)).await;
entry
};
let pipeline = (
transform_fn(long_noop_transform),
transform_fn(long_noop_transform),
transform_fn(long_noop_transform),
);
let mut task = Task::<(), _, _>::builder("test")
.action(pipeline)
.cancel_token(cancel_token)
.build_without_replies();
let now = Instant::now();
let (task_res, ()) = join!(task.run(), request_stop_in_100ms);
task_res.unwrap();
let elapsed = now.elapsed();
let delay_of_3_actions = Duration::from_millis(
ACTION_DELAY_MS * 3,
);
assert!(
elapsed < delay_of_3_actions,
"{}ms should be less than {} * 3 = {}",
elapsed.as_millis(),
ACTION_DELAY_MS,
ACTION_DELAY_MS * 3,
);
}
}