use std::any::TypeId;
use async_trait::async_trait;
use crate::{Executable, Job, JobStatus};
#[async_trait]
pub trait JobPlugin {
async fn change_status(&self, _job_id: &str, _status: JobStatus) {}
async fn before_run(&self, _job_id: &str) {}
async fn after_run(&self, _job_id: &str) {}
}
pub struct JobPluginWrapper {
pub(crate) hook: Box<dyn JobPlugin + Send + Sync + 'static>,
#[allow(dead_code)]
job_type_ids: Vec<TypeId>,
}
#[allow(dead_code)]
impl JobPluginWrapper {
pub(crate) fn new(
plugin: impl JobPlugin + Send + Sync + 'static,
job_type_ids: Vec<TypeId>,
) -> Self {
let hook = Box::new(plugin);
Self { hook, job_type_ids }
}
pub(crate) async fn change_status<M: Executable + Clone + 'static>(
&self,
job_id: &str,
status: JobStatus,
) {
let type_id = TypeId::of::<Job<M>>();
if self.should_run(type_id) {
self.hook.change_status(job_id, status).await;
}
}
pub(crate) async fn before_run<M: Executable + Clone + 'static>(&self, job_id: &str) {
let type_id = TypeId::of::<Job<M>>();
if self.should_run(type_id) {
self.hook.before_run(job_id).await;
}
}
pub(crate) async fn after_run<M: Executable + Clone + 'static>(&self, job_id: &str) {
let type_id = TypeId::of::<Job<M>>();
if self.should_run(type_id) {
self.hook.after_run(job_id).await;
}
}
pub(crate) fn should_run(&self, job_type_id: TypeId) -> bool {
if self.job_type_ids.is_empty() {
return true;
}
self.job_type_ids.contains(&job_type_id)
}
}
#[cfg(test)]
mod tests {
use std::{
any::TypeId,
sync::{Arc, Mutex},
};
use async_trait::async_trait;
use super::{JobPlugin, JobPluginWrapper};
use crate::{Executable, Job, JobContext, JobStatus};
#[derive(Clone, Default)]
pub struct SimplePlugin {
last_job_id: Arc<Mutex<String>>,
}
impl SimplePlugin {
fn record(&self, job_id: &str) {
*self.last_job_id.lock().expect("lock poisoned") = job_id.to_string();
}
fn last_job_id(&self) -> String {
self.last_job_id.lock().expect("lock poisoned").clone()
}
}
#[async_trait]
impl JobPlugin for SimplePlugin {
async fn change_status(&self, job_id: &str, _status: JobStatus) {
self.record(job_id);
}
async fn before_run(&self, job_id: &str) {
self.record(job_id);
}
async fn after_run(&self, job_id: &str) {
self.record(job_id);
}
}
#[derive(Clone)]
pub struct JobA;
#[async_trait]
impl Executable for JobA {
type Output = ();
async fn execute(&mut self, _: &JobContext) {}
}
#[test]
fn test_should_run() {
pub struct B;
let plugin = JobPluginWrapper::new(SimplePlugin::default(), vec![]);
assert!(plugin.should_run(TypeId::of::<JobA>()));
assert!(plugin.should_run(TypeId::of::<B>()));
let plugin_2 = JobPluginWrapper::new(SimplePlugin::default(), vec![TypeId::of::<JobA>()]);
assert!(plugin_2.should_run(TypeId::of::<JobA>()));
assert!(!plugin_2.should_run(TypeId::of::<B>()));
}
#[tokio::test]
async fn test_change_status_hook() {
let plugin = SimplePlugin::default();
let wrapper = JobPluginWrapper::new(plugin.clone(), vec![]);
wrapper
.change_status::<JobA>("job_status", JobStatus::Failed)
.await;
assert_eq!(plugin.last_job_id(), "job_status");
}
#[tokio::test]
async fn test_change_before_run() {
let plugin = SimplePlugin::default();
let wrapper = JobPluginWrapper::new(plugin.clone(), vec![]);
wrapper.before_run::<JobA>("job_before").await;
assert_eq!(plugin.last_job_id(), "job_before");
}
#[tokio::test]
async fn test_change_after_run() {
let plugin = SimplePlugin::default();
let wrapper = JobPluginWrapper::new(plugin.clone(), vec![]);
wrapper.after_run::<JobA>("job_after").await;
assert_eq!(plugin.last_job_id(), "job_after");
}
#[tokio::test]
async fn test_hooks_skipped_for_unregistered_job_type() {
#[derive(Clone)]
struct JobB;
#[async_trait]
impl Executable for JobB {
type Output = ();
async fn execute(&mut self, _: &JobContext) {}
}
let plugin = SimplePlugin::default();
let wrapper = JobPluginWrapper::new(plugin.clone(), vec![TypeId::of::<Job<JobA>>()]);
wrapper.before_run::<JobB>("job_b").await;
assert_eq!(plugin.last_job_id(), "");
wrapper.before_run::<JobA>("job_a").await;
assert_eq!(plugin.last_job_id(), "job_a");
}
}