1use crate::prelude::*;
4use cloudillo_core::app::AppState;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use super::dsl::types::Operation;
12
13pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
15
16pub type HookFunction = Arc<
19 dyn Fn(Arc<AppState>, HookContext) -> BoxFuture<'static, ClResult<HookResult>> + Send + Sync,
20>;
21
22#[derive(Clone, Default)]
24pub enum HookImplementation {
25 Dsl(Vec<Operation>),
27
28 Native(HookFunction),
30
31 Hybrid { dsl: Vec<Operation>, native: HookFunction },
33
34 #[default]
36 None,
37}
38
39impl Serialize for HookImplementation {
42 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43 where
44 S: serde::Serializer,
45 {
46 match self {
47 HookImplementation::Dsl(ops) => ops.serialize(serializer),
48 HookImplementation::None => None::<Vec<Operation>>.serialize(serializer),
49 HookImplementation::Native(_) => {
50 None::<Vec<Operation>>.serialize(serializer)
52 }
53 HookImplementation::Hybrid { dsl, .. } => {
54 dsl.serialize(serializer)
56 }
57 }
58 }
59}
60
61impl<'de> Deserialize<'de> for HookImplementation {
63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64 where
65 D: serde::Deserializer<'de>,
66 {
67 let ops: Option<Vec<Operation>> = Option::deserialize(deserializer)?;
68 match ops {
69 None => Ok(HookImplementation::None),
70 Some(ops) => Ok(HookImplementation::Dsl(ops)),
71 }
72 }
73}
74
75impl std::fmt::Debug for HookImplementation {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::Dsl(ops) => f.debug_tuple("Dsl").field(ops).finish(),
79 Self::Native(_) => f.debug_tuple("Native").field(&"<function>").finish(),
80 Self::Hybrid { dsl, .. } => f
81 .debug_struct("Hybrid")
82 .field("dsl", dsl)
83 .field("native", &"<function>")
84 .finish(),
85 Self::None => write!(f, "None"),
86 }
87 }
88}
89
90impl HookImplementation {
91 pub fn is_some(&self) -> bool {
93 !matches!(self, HookImplementation::None)
94 }
95
96 pub fn is_none(&self) -> bool {
98 matches!(self, HookImplementation::None)
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct HookResult {
105 pub vars: HashMap<String, serde_json::Value>,
107
108 pub continue_processing: bool,
110
111 pub return_value: Option<serde_json::Value>,
113}
114
115impl Default for HookResult {
116 fn default() -> Self {
117 Self { vars: HashMap::new(), continue_processing: true, return_value: None }
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum HookType {
124 OnCreate,
125 OnReceive,
126 OnAccept,
127 OnReject,
128}
129
130impl HookType {
131 pub fn as_str(&self) -> &'static str {
133 match self {
134 HookType::OnCreate => "on_create",
135 HookType::OnReceive => "on_receive",
136 HookType::OnAccept => "on_accept",
137 HookType::OnReject => "on_reject",
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
144pub struct HookContext {
145 pub action_id: String,
147 pub r#type: String,
148 pub subtype: Option<String>,
149 pub issuer: String,
150 pub audience: Option<String>,
151 pub parent: Option<String>,
152 pub subject: Option<String>,
153 pub content: Option<serde_json::Value>,
154 pub attachments: Option<Vec<String>>,
155
156 pub created_at: String,
158 pub expires_at: Option<String>,
159
160 pub tenant_id: i64,
162 pub tenant_tag: String,
163 pub tenant_type: String,
164
165 pub is_inbound: bool,
167 pub is_outbound: bool,
168
169 pub client_address: Option<String>,
172
173 pub vars: HashMap<String, serde_json::Value>,
175}
176
177impl HookContext {
178 pub fn builder() -> HookContextBuilder {
180 HookContextBuilder::default()
181 }
182}
183
184#[derive(Default)]
186pub struct HookContextBuilder {
187 action_id: String,
188 r#type: String,
189 subtype: Option<String>,
190 issuer: String,
191 audience: Option<String>,
192 parent: Option<String>,
193 subject: Option<String>,
194 content: Option<serde_json::Value>,
195 attachments: Option<Vec<String>>,
196 created_at: String,
197 expires_at: Option<String>,
198 tenant_id: i64,
199 tenant_tag: String,
200 tenant_type: String,
201 is_inbound: bool,
202 is_outbound: bool,
203 client_address: Option<String>,
204 vars: HashMap<String, serde_json::Value>,
205}
206
207impl HookContextBuilder {
208 pub fn action_id(mut self, id: impl Into<String>) -> Self {
210 self.action_id = id.into();
211 self
212 }
213
214 pub fn action_type(mut self, t: impl Into<String>) -> Self {
216 self.r#type = t.into();
217 self
218 }
219
220 pub fn subtype(mut self, st: Option<String>) -> Self {
222 self.subtype = st;
223 self
224 }
225
226 pub fn issuer(mut self, i: impl Into<String>) -> Self {
228 self.issuer = i.into();
229 self
230 }
231
232 pub fn audience(mut self, a: Option<String>) -> Self {
234 self.audience = a;
235 self
236 }
237
238 pub fn parent(mut self, p: Option<String>) -> Self {
240 self.parent = p;
241 self
242 }
243
244 pub fn subject(mut self, s: Option<String>) -> Self {
246 self.subject = s;
247 self
248 }
249
250 pub fn content(mut self, c: Option<serde_json::Value>) -> Self {
252 self.content = c;
253 self
254 }
255
256 pub fn attachments(mut self, a: Option<Vec<String>>) -> Self {
258 self.attachments = a;
259 self
260 }
261
262 pub fn created_at(mut self, ts: impl Into<String>) -> Self {
264 self.created_at = ts.into();
265 self
266 }
267
268 pub fn expires_at(mut self, ts: Option<String>) -> Self {
270 self.expires_at = ts;
271 self
272 }
273
274 pub fn tenant(mut self, id: i64, tag: impl Into<String>, typ: impl Into<String>) -> Self {
276 self.tenant_id = id;
277 self.tenant_tag = tag.into();
278 self.tenant_type = typ.into();
279 self
280 }
281
282 pub fn inbound(mut self) -> Self {
284 self.is_inbound = true;
285 self.is_outbound = false;
286 self
287 }
288
289 pub fn outbound(mut self) -> Self {
291 self.is_inbound = false;
292 self.is_outbound = true;
293 self
294 }
295
296 pub fn client_address(mut self, addr: Option<String>) -> Self {
298 self.client_address = addr;
299 self
300 }
301
302 pub fn vars(mut self, vars: HashMap<String, serde_json::Value>) -> Self {
304 self.vars = vars;
305 self
306 }
307
308 pub fn build(self) -> HookContext {
310 HookContext {
311 action_id: self.action_id,
312 r#type: self.r#type,
313 subtype: self.subtype,
314 issuer: self.issuer,
315 audience: self.audience,
316 parent: self.parent,
317 subject: self.subject,
318 content: self.content,
319 attachments: self.attachments,
320 created_at: self.created_at,
321 expires_at: self.expires_at,
322 tenant_id: self.tenant_id,
323 tenant_tag: self.tenant_tag,
324 tenant_type: self.tenant_type,
325 is_inbound: self.is_inbound,
326 is_outbound: self.is_outbound,
327 client_address: self.client_address,
328 vars: self.vars,
329 }
330 }
331}
332
333pub struct ActionTypeHooks {
335 pub on_create: Option<HookFunction>,
336 pub on_receive: Option<HookFunction>,
337 pub on_accept: Option<HookFunction>,
338 pub on_reject: Option<HookFunction>,
339}
340
341pub struct HookRegistry {
343 hooks: HashMap<String, ActionTypeHooks>,
344}
345
346impl HookRegistry {
347 pub fn new() -> Self {
349 Self { hooks: HashMap::new() }
350 }
351
352 pub fn register_type(&mut self, type_name: &str, hooks: ActionTypeHooks) {
354 self.hooks.insert(type_name.to_string(), hooks);
355 }
356
357 pub fn register_hook(&mut self, type_name: &str, hook_type: HookType, function: HookFunction) {
359 let entry = self.hooks.entry(type_name.to_string()).or_insert_with(|| ActionTypeHooks {
360 on_create: None,
361 on_receive: None,
362 on_accept: None,
363 on_reject: None,
364 });
365
366 match hook_type {
367 HookType::OnCreate => entry.on_create = Some(function),
368 HookType::OnReceive => entry.on_receive = Some(function),
369 HookType::OnAccept => entry.on_accept = Some(function),
370 HookType::OnReject => entry.on_reject = Some(function),
371 }
372 }
373
374 pub fn get_hook(&self, type_name: &str, hook_type: HookType) -> Option<&HookFunction> {
376 self.hooks.get(type_name).and_then(|hooks| match hook_type {
377 HookType::OnCreate => hooks.on_create.as_ref(),
378 HookType::OnReceive => hooks.on_receive.as_ref(),
379 HookType::OnAccept => hooks.on_accept.as_ref(),
380 HookType::OnReject => hooks.on_reject.as_ref(),
381 })
382 }
383
384 pub fn has_hook(&self, type_name: &str, hook_type: HookType) -> bool {
386 self.get_hook(type_name, hook_type).is_some()
387 }
388
389 pub fn registered_types(&self) -> Vec<&str> {
391 self.hooks.keys().map(|s| s.as_str()).collect()
392 }
393}
394
395impl Default for HookRegistry {
396 fn default() -> Self {
397 Self::new()
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_hook_type_str_conversion() {
407 assert_eq!(HookType::OnCreate.as_str(), "on_create");
408 assert_eq!(HookType::OnReceive.as_str(), "on_receive");
409 assert_eq!(HookType::OnAccept.as_str(), "on_accept");
410 assert_eq!(HookType::OnReject.as_str(), "on_reject");
411 }
412
413 #[test]
414 fn test_hook_result_default() {
415 let result = HookResult::default();
416 assert!(result.vars.is_empty());
417 assert!(result.continue_processing);
418 assert!(result.return_value.is_none());
419 }
420
421 #[test]
422 fn test_hook_registry_create() {
423 let registry = HookRegistry::new();
424 assert!(registry.registered_types().is_empty());
425 }
426
427 #[test]
428 fn test_hook_registry_register_hook() {
429 let mut registry = HookRegistry::new();
430
431 let hook: HookFunction = Arc::new(|_, _| Box::pin(async { Ok(HookResult::default()) }));
433
434 registry.register_hook("TEST", HookType::OnCreate, hook.clone());
435 assert!(registry.has_hook("TEST", HookType::OnCreate));
436 assert!(!registry.has_hook("TEST", HookType::OnReceive));
437 }
438
439 #[test]
440 fn test_hook_implementation_default() {
441 let impl_hook = HookImplementation::default();
442 assert!(matches!(impl_hook, HookImplementation::None));
443 }
444
445 #[test]
446 fn test_hook_context_creation() {
447 let context = HookContext {
448 action_id: "a1~test".to_string(),
449 r#type: "POST".to_string(),
450 subtype: None,
451 issuer: "user1".to_string(),
452 audience: None,
453 parent: None,
454 subject: None,
455 content: None,
456 attachments: None,
457 created_at: "2025-11-09T00:00:00Z".to_string(),
458 expires_at: None,
459 tenant_id: 1,
460 tenant_tag: "dev".to_string(),
461 tenant_type: "user".to_string(),
462 is_inbound: false,
463 is_outbound: true,
464 client_address: None,
465 vars: HashMap::new(),
466 };
467
468 assert_eq!(context.action_id, "a1~test");
469 assert_eq!(context.r#type, "POST");
470 assert_eq!(context.issuer, "user1");
471 }
472}
473
474