1use std::collections::BTreeMap;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::time::SystemTime;
13
14use serde_json::Value;
15
16use crate::__codegen::EncodedCreate;
17use crate::Result;
18use crate::error::Error;
19
20pub type HookFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum CrudOperation {
26 Insert,
28 Update,
30 Delete,
32 Put,
34}
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub enum ModelKind {
39 Entity,
41 Relation,
43}
44
45#[derive(Clone, Debug, Eq, PartialEq)]
47pub enum PreHookResult {
48 Continue,
50 Reject {
52 reason: String,
54 },
55}
56
57#[derive(Debug, thiserror::Error)]
59pub enum HookError {
60 #[error("hook '{hook_name}' rejected {operation:?}: {reason}")]
62 Rejected {
63 hook_name: String,
65 operation: CrudOperation,
67 reason: String,
69 },
70 #[error("hook '{hook_name}' failed: {source}")]
72 Internal {
73 hook_name: String,
75 #[source]
77 source: Box<dyn std::error::Error + Send + Sync + 'static>,
78 },
79}
80
81pub struct HookContext<'a> {
88 type_id_json: &'static str,
89 type_name: &'a str,
90 model_kind: ModelKind,
91 operation: CrudOperation,
92 iid: Option<&'a str>,
93 input: Option<&'a EncodedCreate>,
94 timestamp: &'a SystemTime,
95 metadata: &'a mut BTreeMap<String, Value>,
96}
97
98impl<'a> HookContext<'a> {
99 #[must_use]
101 pub const fn type_id_json(&self) -> &'static str {
102 self.type_id_json
103 }
104
105 #[must_use]
107 pub const fn type_name(&self) -> &str {
108 self.type_name
109 }
110
111 #[must_use]
113 pub const fn model_kind(&self) -> ModelKind {
114 self.model_kind
115 }
116
117 #[must_use]
119 pub const fn operation(&self) -> CrudOperation {
120 self.operation
121 }
122
123 #[must_use]
125 pub const fn iid(&self) -> Option<&str> {
126 self.iid
127 }
128
129 #[must_use]
131 pub const fn input(&self) -> Option<&EncodedCreate> {
132 self.input
133 }
134
135 #[must_use]
137 pub const fn timestamp(&self) -> &SystemTime {
138 self.timestamp
139 }
140
141 #[must_use]
143 pub fn metadata(&self) -> &BTreeMap<String, Value> {
144 self.metadata
145 }
146
147 pub fn metadata_mut(&mut self) -> &mut BTreeMap<String, Value> {
149 self.metadata
150 }
151
152 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<Value>) {
155 self.metadata.insert(key.into(), value.into());
156 }
157}
158
159pub trait LifecycleHook: Send + Sync {
161 fn name(&self) -> &str;
163
164 fn before_operation<'a>(
166 &'a self,
167 context: &'a mut HookContext<'_>,
168 ) -> HookFuture<'a, std::result::Result<PreHookResult, HookError>>;
169
170 fn after_operation<'a>(
172 &'a self,
173 context: &'a HookContext<'_>,
174 ) -> HookFuture<'a, std::result::Result<(), HookError>>;
175
176 fn should_run(&self, context: &HookContext<'_>) -> bool {
178 let _ = context;
179 true
180 }
181}
182
183#[derive(Default)]
184pub(crate) struct HookRunner {
185 hooks: Vec<Arc<dyn LifecycleHook>>,
186}
187
188pub(crate) struct HookState {
189 metadata: BTreeMap<String, Value>,
190 timestamp: SystemTime,
191}
192
193impl Clone for HookRunner {
194 fn clone(&self) -> Self {
195 Self {
196 hooks: self.hooks.clone(),
197 }
198 }
199}
200
201impl HookRunner {
202 pub(crate) fn add(&mut self, hook: Arc<dyn LifecycleHook>) {
203 self.hooks.push(hook);
204 }
205
206 pub(crate) fn has_hooks(&self) -> bool {
207 !self.hooks.is_empty()
208 }
209
210 pub(crate) async fn run_pre(
211 &self,
212 type_id_json: &'static str,
213 model_kind: ModelKind,
214 operation: CrudOperation,
215 iid: Option<&str>,
216 input: Option<&EncodedCreate>,
217 ) -> Result<HookState> {
218 let type_name = type_name(type_id_json);
219 let mut metadata = BTreeMap::new();
220 let timestamp = SystemTime::now();
221 for hook in &self.hooks {
222 let mut context = HookContext {
223 type_id_json,
224 type_name: &type_name,
225 model_kind,
226 operation,
227 iid,
228 input,
229 timestamp: ×tamp,
230 metadata: &mut metadata,
231 };
232 if !hook.should_run(&context) {
233 continue;
234 }
235 match hook.before_operation(&mut context).await {
236 Ok(PreHookResult::Continue) => {}
237 Ok(PreHookResult::Reject { reason }) => {
238 return Err(Error::from_hook(HookError::Rejected {
239 hook_name: hook.name().to_owned(),
240 operation,
241 reason,
242 }));
243 }
244 Err(error) => return Err(Error::from_hook(error)),
245 }
246 }
247 Ok(HookState {
248 metadata,
249 timestamp,
250 })
251 }
252
253 pub(crate) async fn run_post(
254 &self,
255 type_id_json: &'static str,
256 model_kind: ModelKind,
257 operation: CrudOperation,
258 iid: Option<&str>,
259 input: Option<&EncodedCreate>,
260 mut state: HookState,
261 ) {
262 let type_name = type_name(type_id_json);
263 let context = HookContext {
264 type_id_json,
265 type_name: &type_name,
266 model_kind,
267 operation,
268 iid,
269 input,
270 timestamp: &state.timestamp,
271 metadata: &mut state.metadata,
272 };
273 for hook in self.hooks.iter().rev() {
274 if !hook.should_run(&context) {
275 continue;
276 }
277 if let Err(error) = hook.after_operation(&context).await {
278 tracing::warn!(hook = hook.name(), error = %error, "generated post-hook error");
279 }
280 }
281 }
282}
283
284fn type_name(type_id_json: &str) -> String {
285 serde_json::from_str::<Value>(type_id_json)
286 .ok()
287 .and_then(|value| value.get("label")?.as_str().map(str::to_owned))
288 .unwrap_or_else(|| type_id_json.to_owned())
289}