use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::SystemTime;
use serde_json::Value;
use crate::__codegen::EncodedCreate;
use crate::Result;
use crate::error::Error;
pub type HookFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CrudOperation {
Insert,
Update,
Delete,
Put,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ModelKind {
Entity,
Relation,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreHookResult {
Continue,
Reject {
reason: String,
},
}
#[derive(Debug, thiserror::Error)]
pub enum HookError {
#[error("hook '{hook_name}' rejected {operation:?}: {reason}")]
Rejected {
hook_name: String,
operation: CrudOperation,
reason: String,
},
#[error("hook '{hook_name}' failed: {source}")]
Internal {
hook_name: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
}
pub struct HookContext<'a> {
type_id_json: &'static str,
type_name: &'a str,
model_kind: ModelKind,
operation: CrudOperation,
iid: Option<&'a str>,
input: Option<&'a EncodedCreate>,
timestamp: &'a SystemTime,
metadata: &'a mut BTreeMap<String, Value>,
}
impl<'a> HookContext<'a> {
#[must_use]
pub const fn type_id_json(&self) -> &'static str {
self.type_id_json
}
#[must_use]
pub const fn type_name(&self) -> &str {
self.type_name
}
#[must_use]
pub const fn model_kind(&self) -> ModelKind {
self.model_kind
}
#[must_use]
pub const fn operation(&self) -> CrudOperation {
self.operation
}
#[must_use]
pub const fn iid(&self) -> Option<&str> {
self.iid
}
#[must_use]
pub const fn input(&self) -> Option<&EncodedCreate> {
self.input
}
#[must_use]
pub const fn timestamp(&self) -> &SystemTime {
self.timestamp
}
#[must_use]
pub fn metadata(&self) -> &BTreeMap<String, Value> {
self.metadata
}
pub fn metadata_mut(&mut self) -> &mut BTreeMap<String, Value> {
self.metadata
}
pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<Value>) {
self.metadata.insert(key.into(), value.into());
}
}
pub trait LifecycleHook: Send + Sync {
fn name(&self) -> &str;
fn before_operation<'a>(
&'a self,
context: &'a mut HookContext<'_>,
) -> HookFuture<'a, std::result::Result<PreHookResult, HookError>>;
fn after_operation<'a>(
&'a self,
context: &'a HookContext<'_>,
) -> HookFuture<'a, std::result::Result<(), HookError>>;
fn should_run(&self, context: &HookContext<'_>) -> bool {
let _ = context;
true
}
}
#[derive(Default)]
pub(crate) struct HookRunner {
hooks: Vec<Arc<dyn LifecycleHook>>,
}
pub(crate) struct HookState {
metadata: BTreeMap<String, Value>,
timestamp: SystemTime,
}
impl Clone for HookRunner {
fn clone(&self) -> Self {
Self {
hooks: self.hooks.clone(),
}
}
}
impl HookRunner {
pub(crate) fn add(&mut self, hook: Arc<dyn LifecycleHook>) {
self.hooks.push(hook);
}
pub(crate) fn has_hooks(&self) -> bool {
!self.hooks.is_empty()
}
pub(crate) async fn run_pre(
&self,
type_id_json: &'static str,
model_kind: ModelKind,
operation: CrudOperation,
iid: Option<&str>,
input: Option<&EncodedCreate>,
) -> Result<HookState> {
let type_name = type_name(type_id_json);
let mut metadata = BTreeMap::new();
let timestamp = SystemTime::now();
for hook in &self.hooks {
let mut context = HookContext {
type_id_json,
type_name: &type_name,
model_kind,
operation,
iid,
input,
timestamp: ×tamp,
metadata: &mut metadata,
};
if !hook.should_run(&context) {
continue;
}
match hook.before_operation(&mut context).await {
Ok(PreHookResult::Continue) => {}
Ok(PreHookResult::Reject { reason }) => {
return Err(Error::from_hook(HookError::Rejected {
hook_name: hook.name().to_owned(),
operation,
reason,
}));
}
Err(error) => return Err(Error::from_hook(error)),
}
}
Ok(HookState {
metadata,
timestamp,
})
}
pub(crate) async fn run_post(
&self,
type_id_json: &'static str,
model_kind: ModelKind,
operation: CrudOperation,
iid: Option<&str>,
input: Option<&EncodedCreate>,
mut state: HookState,
) {
let type_name = type_name(type_id_json);
let context = HookContext {
type_id_json,
type_name: &type_name,
model_kind,
operation,
iid,
input,
timestamp: &state.timestamp,
metadata: &mut state.metadata,
};
for hook in self.hooks.iter().rev() {
if !hook.should_run(&context) {
continue;
}
if let Err(error) = hook.after_operation(&context).await {
tracing::warn!(hook = hook.name(), error = %error, "generated post-hook error");
}
}
}
}
fn type_name(type_id_json: &str) -> String {
serde_json::from_str::<Value>(type_id_json)
.ok()
.and_then(|value| value.get("label")?.as_str().map(str::to_owned))
.unwrap_or_else(|| type_id_json.to_owned())
}