1use std::sync::Arc;
11
12use anyhow::Result;
13use async_trait::async_trait;
14
15use crate::{DogBeforeHook, HookContext, ServiceHooks, ServiceMethodKind};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum WriteMethods {
20 Create,
21 Patch,
22 Update,
23 AllWrites,
24}
25
26impl WriteMethods {
27 #[inline]
28 pub fn matches(&self, method: &ServiceMethodKind) -> bool {
29 match self {
30 WriteMethods::AllWrites => matches!(
31 method,
32 ServiceMethodKind::Create | ServiceMethodKind::Patch | ServiceMethodKind::Update
33 ),
34 WriteMethods::Create => matches!(method, ServiceMethodKind::Create),
35 WriteMethods::Patch => matches!(method, ServiceMethodKind::Patch),
36 WriteMethods::Update => matches!(method, ServiceMethodKind::Update),
37 }
38 }
39}
40
41#[derive(Clone)]
43pub struct HookMeta<R, P>
44where
45 R: Send + 'static,
46 P: Send + Clone + 'static,
47{
48 pub tenant: crate::TenantContext,
49 pub method: crate::ServiceMethodKind,
50 pub params: P,
51 pub services: crate::ServiceCaller<R, P>,
52 pub config: crate::DogConfigSnapshot,
53}
54
55impl<R, P> HookMeta<R, P>
56where
57 R: Send + 'static,
58 P: Send + Clone + 'static,
59{
60 pub fn from_ctx(ctx: &crate::HookContext<R, P>) -> Self {
61 Self {
62 tenant: ctx.tenant.clone(),
63 method: ctx.method.clone(),
64 params: ctx.params.clone(),
65 services: ctx.services.clone(),
66 config: ctx.config.clone(),
67 }
68 }
69}
70
71pub type ValidateFn<R, P> =
72 Arc<dyn Fn(&R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static>;
73
74pub type ResolveFn<R, P> =
75 Arc<dyn Fn(&mut R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static>;
76
77pub struct ValidateData<R, P>
79where
80 R: Send + 'static,
81 P: Send + Clone + 'static,
82{
83 methods: WriteMethods,
84 validator: ValidateFn<R, P>,
85}
86
87impl<R, P> ValidateData<R, P>
88where
89 R: Send + 'static,
90 P: Send + Clone + 'static,
91{
92 pub fn new(
93 validator: impl Fn(&R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static,
94 ) -> Self {
95 Self {
96 methods: WriteMethods::AllWrites,
97 validator: Arc::new(validator),
98 }
99 }
100
101 pub fn with_methods(mut self, methods: WriteMethods) -> Self {
102 self.methods = methods;
103 self
104 }
105}
106
107#[async_trait]
108impl<R, P> DogBeforeHook<R, P> for ValidateData<R, P>
109where
110 R: Send + 'static,
111 P: Send + Clone + 'static,
112{
113 async fn run(&self, ctx: &mut HookContext<R, P>) -> Result<()> {
114 if !self.methods.matches(&ctx.method) {
115 return Ok(());
116 }
117
118 let meta = HookMeta::from_ctx(ctx);
119
120 let data = ctx
121 .data
122 .as_ref()
123 .ok_or_else(|| anyhow::anyhow!("ValidateData requires ctx.data on write methods"))?;
124
125 (self.validator)(data, &meta)
126 }
127}
128
129pub struct ResolveData<R, P>
131where
132 R: Send + 'static,
133 P: Send + Clone + 'static,
134{
135 methods: WriteMethods,
136 resolver: ResolveFn<R, P>,
137}
138
139impl<R, P> ResolveData<R, P>
140where
141 R: Send + 'static,
142 P: Send + Clone + 'static,
143{
144 pub fn new(
145 resolver: impl Fn(&mut R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static,
146 ) -> Self {
147 Self {
148 methods: WriteMethods::AllWrites,
149 resolver: Arc::new(resolver),
150 }
151 }
152
153 pub fn with_methods(mut self, methods: WriteMethods) -> Self {
154 self.methods = methods;
155 self
156 }
157}
158
159#[async_trait]
160impl<R, P> DogBeforeHook<R, P> for ResolveData<R, P>
161where
162 R: Send + 'static,
163 P: Send + Clone + 'static,
164{
165 async fn run(&self, ctx: &mut HookContext<R, P>) -> Result<()> {
166 if !self.methods.matches(&ctx.method) {
167 return Ok(());
168 }
169
170 let meta = HookMeta::from_ctx(ctx);
172
173 let data = ctx
175 .data
176 .as_mut()
177 .ok_or_else(|| anyhow::anyhow!("ResolveData requires ctx.data on write methods"))?;
178
179 (self.resolver)(data, &meta)
180 }
181}
182
183#[derive(Default)]
185pub struct Rules {
186 errors: Vec<anyhow::Error>,
187}
188
189impl Rules {
190 pub fn new() -> Self {
191 Self { errors: Vec::new() }
192 }
193
194 pub fn non_empty(mut self, field: &str, v: &str) -> Self {
195 if v.trim().is_empty() {
196 self.errors
197 .push(anyhow::anyhow!("'{field}' must not be empty"));
198 }
199 self
200 }
201
202 pub fn min_len(mut self, field: &str, v: &str, n: usize) -> Self {
203 if v.chars().count() < n {
204 self.errors
205 .push(anyhow::anyhow!("'{field}' must be at least {n} chars"));
206 }
207 self
208 }
209
210 pub fn check(self) -> Result<()> {
211 if self.errors.is_empty() {
212 Ok(())
213 } else if self.errors.len() == 1 {
214 Err(self.errors.into_iter().next().unwrap())
215 } else {
216 let msg = self
217 .errors
218 .iter()
219 .map(|e| format!("- {e}"))
220 .collect::<Vec<_>>()
221 .join("\n");
222 Err(anyhow::anyhow!("Schema validation failed:\n{msg}"))
223 }
224 }
225}
226
227pub struct SchemaBuilder<'a, R, P>
229where
230 R: Send + 'static,
231 P: Send + Clone + 'static,
232{
233 hooks: &'a mut ServiceHooks<R, P>,
234 current_methods: WriteMethods,
235}
236
237impl<'a, R, P> SchemaBuilder<'a, R, P>
238where
239 R: Send + 'static,
240 P: Send + Clone + 'static,
241{
242 fn new(hooks: &'a mut ServiceHooks<R, P>) -> Self {
243 Self {
244 hooks,
245 current_methods: WriteMethods::AllWrites,
246 }
247 }
248
249 pub fn on_create(&mut self) -> &mut Self {
250 self.current_methods = WriteMethods::Create;
251 self
252 }
253
254 pub fn on_patch(&mut self) -> &mut Self {
255 self.current_methods = WriteMethods::Patch;
256 self
257 }
258
259 pub fn on_update(&mut self) -> &mut Self {
260 self.current_methods = WriteMethods::Update;
261 self
262 }
263
264 pub fn on_writes(&mut self) -> &mut Self {
265 self.current_methods = WriteMethods::AllWrites;
266 self
267 }
268
269 pub fn resolve(
270 &mut self,
271 f: impl Fn(&mut R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static,
272 ) -> &mut Self {
273 let hook = ResolveData::<R, P>::new(f).with_methods(self.current_methods);
274 self.hooks.before_all(Arc::new(hook));
275 self
276 }
277
278 pub fn validate(
279 &mut self,
280 f: impl Fn(&R, &HookMeta<R, P>) -> Result<()> + Send + Sync + 'static,
281 ) -> &mut Self {
282 let hook = ValidateData::<R, P>::new(f).with_methods(self.current_methods);
283 self.hooks.before_all(Arc::new(hook));
284 self
285 }
286}
287
288pub trait SchemaHooksExt<R, P>
290where
291 R: Send + 'static,
292 P: Send + Clone + 'static,
293{
294 fn schema<F>(&mut self, f: F) -> &mut Self
295 where
296 F: FnOnce(&mut SchemaBuilder<'_, R, P>);
297}
298
299impl<R, P> SchemaHooksExt<R, P> for ServiceHooks<R, P>
300where
301 R: Send + 'static,
302 P: Send + Clone + 'static,
303{
304 fn schema<F>(&mut self, f: F) -> &mut Self
305 where
306 F: FnOnce(&mut SchemaBuilder<'_, R, P>),
307 {
308 let mut b = SchemaBuilder::new(self);
309 f(&mut b);
310 self
311 }
312}