dog-core 0.1.7

Core traits and utilities for the DogRS ecosystem - a modular Rust framework for building scalable applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
//! # Hooks: Dependency Injection (DogRS style)
//!
//! DogRS is **DI-first**: hooks should be small, portable, testable,
//! and not depend on hidden global state.
//!
//! In FeathersJS, hooks often reach for `context.app` to access
//! config/services. In DogRS, the **default** approach is:
//! **inject what you need at construction time**.
//!
//! However, DogRS also supports an **optional, Feathers-like** runtime access
//! pattern via `ctx.config` and `ctx.services` for cases where DI is awkward.
//!
//! ---
//!
//! ## The two supported styles
//!
//! ### A) Preferred: Dependency Injection (most hooks should do this)
//! ✅ Best for: validation, auth policy checks (if cheap), input shaping,
//! audit stamping, pagination clamping, etc.
//!
//! ```rust
//! use std::sync::Arc;
//! use anyhow::Result;
//! use async_trait::async_trait;
//! use dog_core::{DogBeforeHook, HookContext};
//!
//! struct EnforceMaxPage {
//!     max: usize,
//! }
//!
//! #[async_trait]
//! impl<R, P> DogBeforeHook<R, P> for EnforceMaxPage
//! where
//!     R: Send + 'static,
//!     P: Send + Clone + 'static,
//! {
//!     async fn run(&self, _ctx: &mut HookContext<R, P>) -> Result<()> {
//!         // clamp pagination, etc...
//!         Ok(())
//!     }
//! }
//!
//! // Registration:
//! // let max = app.config_snapshot().get_usize("paginate.max").unwrap_or(50);
//! // app.hooks(|h| { h.before_all(Arc::new(EnforceMaxPage { max })); });
//! ```
//!
//! ### B) Optional: Context services/config (Feathers-like escape hatch)
//! ✅ Best for: logging, auditing, light enrichment, or policy checks that
//! genuinely need a separate service and DI is too rigid.
//!
//! DogRS may populate the hook context with:
//! - `ctx.config`: a snapshot of app config at call time
//! - `ctx.services`: a runtime service caller (typed downcast)
//!
//! ```rust
//! use std::sync::Arc;
//! use anyhow::Result;
//! use async_trait::async_trait;
//! use dog_core::{DogBeforeHook, HookContext};
//!
//! // Example types
//! #[derive(Clone)]
//! struct User { id: String }
//! #[derive(Clone)]
//! struct UserParams;
//!
//! struct AttachUser;
//!
//! #[async_trait]
//! impl<Message, Params> DogBeforeHook<Message, Params> for AttachUser
//! where
//!     Message: Send + 'static,
//!     Params: Send + Clone + 'static,
//! {
//!     async fn run(&self, ctx: &mut HookContext<Message, Params>) -> Result<()> {
//!         // Read config snapshot (if provided by the app pipeline):
//!         let _max = ctx.config.get_usize("paginate.max").unwrap_or(50);
//!
//!         // Runtime lookup of another service (typed):
//!         let users = ctx.services.service::<User, UserParams>("users")?;
//!
//!         // NOTE: calling other services from hooks is powerful but risky.
//!         // users.get(...).await?;
//!         Ok(())
//!     }
//! }
//! ```
//!
//! ---
//!
//! ## Why `ctx.services` (and not `ctx.service("...")`)?
//!
//! We keep the surface explicit:
//! - `ctx` remains a *pure* per-call context
//! - service lookup is grouped under `ctx.services` so it’s obvious when you’re
//!   reaching outside the hook into the service graph.
//!
//! This mirrors the Feathers mental model (`context.app.service(...)`) without
//! putting the whole `app` onto the hook context.
//!
//! ---
//!
//! ## Important warnings (read this if you use `ctx.services`)
//!
//! Service-to-service calls **inside hooks** can be dangerous because they can:
//! - create hidden coupling (harder to reason about the dependency graph)
//! - accidentally trigger nested hook pipelines (surprising behavior)
//! - form cycles (A hook calls B which triggers a hook that calls A…)
//! - cause performance cliffs (N+1 calls in hooks)
//!
//! Prefer service-to-service calls inside the **service implementation**
//! (domain logic) rather than inside hooks.
//!
//! Use `ctx.services` inside hooks only for:
//! - logging/auditing
//! - lightweight enrichment that cannot live in the service
//! - authorization checks that must query a separate policy service
//!
//! If you do it:
//! - keep it fast and side-effect safe
//! - avoid calling the *same* service you’re currently executing
//! - avoid cascading calls (hook calls service which calls service which…)
//!
//! ---
//!
//! ## Type safety and mismatches
//!
//! `ctx.services.service::<R2, P2>("name")` performs a typed downcast.
//! If you request a different `<R2, P2>` than what was registered,
//! it returns a clear **type mismatch** error.
//!
//! This is deliberate: DogRS remains strongly typed even when providing
//! a Feathers-like runtime lookup experience.
//!



use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;

use crate::{ServiceMethodKind, TenantContext};

pub enum HookResult<R> {
    One(R),
    Many(Vec<R>),
}

/// A typed, Feathers-inspired hook context.
///
/// This context flows through:
/// around → before → service → after → error
/// A typed, Feathers-inspired hook context.
///
/// This context flows through:
/// around → before → service → after → error
pub struct HookContext<R, P>
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    pub tenant: TenantContext,
    pub method: ServiceMethodKind,
    pub params: P,

    /// Input data (create / patch / update)
    pub data: Option<R>,

    /// Output result (after hooks)
    pub result: Option<HookResult<R>>,

    /// Error captured during execution
    pub error: Option<anyhow::Error>,

    /// Feathers-style access to other services (runtime lookup)
    pub services: crate::ServiceCaller<R, P>,

    /// Immutable snapshot of app config for this call
    pub config: crate::DogConfigSnapshot,
}

impl<R, P> HookContext<R, P>
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    pub fn new(
        tenant: TenantContext,
        method: ServiceMethodKind,
        params: P,
        services: crate::ServiceCaller<R, P>,
        config: crate::DogConfigSnapshot,
    ) -> Self {
        Self {
            tenant,
            method,
            params,
            data: None,
            result: None,
            error: None,
            services,
            config,
        }
    }
}

/// Helper used by the pipeline:
/// returns `all + method` hooks in that order.
pub(crate) fn collect_method_hooks<T>(
    all: &[T],
    by_method: &std::collections::HashMap<crate::ServiceMethodKind, Vec<T>>,
    method: &crate::ServiceMethodKind,
) -> Vec<T>
where
    T: Clone,
{
    let mut hooks = all.to_vec();
    if let Some(method_hooks) = by_method.get(method) {
        hooks.extend(method_hooks.clone());
    }
    hooks
}

pub type HookFut<'a> = Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>;

type NextCall<R, P> = dyn for<'a> FnOnce(&'a mut HookContext<R, P>) -> HookFut<'a> + Send;

/// Around hooks wrap the entire pipeline (like Feathers `around.all`)
pub struct Next<R, P>
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    pub(crate) call: Box<NextCall<R, P>>,
}

impl<R, P> Next<R, P>
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    pub async fn run(self, ctx: &mut HookContext<R, P>) -> Result<()> {
        (self.call)(ctx).await
    }
}


#[async_trait]
pub trait DogBeforeHook<R, P>: Send + Sync
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    async fn run(&self, ctx: &mut HookContext<R, P>) -> Result<()>;
}

#[async_trait]
pub trait DogAfterHook<R, P>: Send + Sync
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    async fn run(&self, ctx: &mut HookContext<R, P>) -> Result<()>;
}

#[async_trait]
pub trait DogErrorHook<R, P>: Send + Sync
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    async fn run(&self, ctx: &mut HookContext<R, P>) -> Result<()>;
}

#[async_trait]
pub trait DogAroundHook<R, P>: Send + Sync
where
    R: Send + 'static,
    P: Send + Clone + 'static,
{
    async fn run(&self, ctx: &mut HookContext<R, P>, next: Next<R, P>) -> Result<()>;
}


/// Feathers-style hooks container:
///
/// {
///   around: { all, create, find },
///   before: { all, create },
///   after:  { all, find },
///   error:  { all, create }
/// }
pub struct ServiceHooks<R, P> {
    pub around_all: Vec<Arc<dyn DogAroundHook<R, P>>>,
    pub before_all: Vec<Arc<dyn DogBeforeHook<R, P>>>,
    pub after_all: Vec<Arc<dyn DogAfterHook<R, P>>>,
    pub error_all: Vec<Arc<dyn DogErrorHook<R, P>>>,

    pub around_by_method: HashMap<ServiceMethodKind, Vec<Arc<dyn DogAroundHook<R, P>>>>,
    pub before_by_method: HashMap<ServiceMethodKind, Vec<Arc<dyn DogBeforeHook<R, P>>>>,
    pub after_by_method: HashMap<ServiceMethodKind, Vec<Arc<dyn DogAfterHook<R, P>>>>,
    pub error_by_method: HashMap<ServiceMethodKind, Vec<Arc<dyn DogErrorHook<R, P>>>>,
}

impl<R, P> Default for ServiceHooks<R, P> {
    fn default() -> Self {
        Self::new()
    }
}

impl<R, P> ServiceHooks<R, P> {
    pub fn new() -> Self {
        Self {
            around_all: Vec::new(),
            before_all: Vec::new(),
            after_all: Vec::new(),
            error_all: Vec::new(),
            around_by_method: HashMap::new(),
            before_by_method: HashMap::new(),
            after_by_method: HashMap::new(),
            error_by_method: HashMap::new(),
        }
    }

    pub fn is_empty(&self) -> bool {
        self.around_all.is_empty()
            && self.before_all.is_empty()
            && self.after_all.is_empty()
            && self.error_all.is_empty()
            && self.around_by_method.is_empty()
            && self.before_by_method.is_empty()
            && self.after_by_method.is_empty()
            && self.error_by_method.is_empty()
    }

    // ─────────── AROUND ───────────

    pub fn around_all(&mut self, hook: Arc<dyn DogAroundHook<R, P>>) -> &mut Self {
        self.around_all.push(hook);
        self
    }

    pub fn around(
        &mut self,
        method: ServiceMethodKind,
        hook: Arc<dyn DogAroundHook<R, P>>,
    ) -> &mut Self {
        self.around_by_method.entry(method).or_default().push(hook);
        self
    }

    // ─────────── BEFORE ───────────

    pub fn before_all(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before_all.push(hook);
        self
    }

    pub fn before(
        &mut self,
        method: ServiceMethodKind,
        hook: Arc<dyn DogBeforeHook<R, P>>,
    ) -> &mut Self {
        self.before_by_method.entry(method).or_default().push(hook);
        self
    }

    pub fn before_create(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Create, hook)
    }

    pub fn before_find(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Find, hook)
    }

    pub fn before_get(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Get, hook)
    }

    pub fn before_update(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Update, hook)
    }

    pub fn before_patch(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Patch, hook)
    }

    pub fn before_remove(&mut self, hook: Arc<dyn DogBeforeHook<R, P>>) -> &mut Self {
        self.before(ServiceMethodKind::Remove, hook)
    }

    // ─────────── AFTER ───────────

    pub fn after_all(&mut self, hook: Arc<dyn DogAfterHook<R, P>>) -> &mut Self {
        self.after_all.push(hook);
        self
    }

    pub fn after(
        &mut self,
        method: ServiceMethodKind,
        hook: Arc<dyn DogAfterHook<R, P>>,
    ) -> &mut Self {
        self.after_by_method.entry(method).or_default().push(hook);
        self
    }

    pub fn after_create(&mut self, hook: Arc<dyn DogAfterHook<R, P>>) -> &mut Self {
        self.after(ServiceMethodKind::Create, hook)
    }

    pub fn after_find(&mut self, hook: Arc<dyn DogAfterHook<R, P>>) -> &mut Self {
        self.after(ServiceMethodKind::Find, hook)
    }

    // ─────────── ERROR ───────────

    pub fn error_all(&mut self, hook: Arc<dyn DogErrorHook<R, P>>) -> &mut Self {
        self.error_all.push(hook);
        self
    }

    pub fn error(
        &mut self,
        method: ServiceMethodKind,
        hook: Arc<dyn DogErrorHook<R, P>>,
    ) -> &mut Self {
        self.error_by_method.entry(method).or_default().push(hook);
        self
    }
}