1use crate::context::ValidationContext;
24use crate::dump::{DumpError, DumpOptions};
25use crate::schema::SchemaRegistry;
26use serde_json::{Map, Value};
27use std::marker::PhantomData;
28
29pub trait ModelHooks {
31 fn before_model(_input: &mut Value, _ctx: &mut ValidationContext) {}
33
34 fn before_field(_field: &str, _input: &mut Value, _ctx: &mut ValidationContext) {}
37
38 fn after_fields(&self, _ctx: &mut ValidationContext) {}
40
41 fn after_model(&self, _ctx: &mut ValidationContext) {}
43
44 fn computed_fields(
49 &self,
50 _out: &mut Map<String, Value>,
51 _opts: &DumpOptions,
52 ) -> Result<(), DumpError> {
53 Ok(())
54 }
55
56 fn computed_schema(_properties: &mut Map<String, Value>, _registry: &mut SchemaRegistry) {}
58
59 fn serialize_field(&self, _field: &str, value: Value) -> Result<Value, DumpError> {
64 Ok(value)
65 }
66
67 fn serialize_model(&self, value: Value) -> Result<Value, DumpError> {
72 Ok(value)
73 }
74}
75
76#[doc(hidden)]
78pub struct Probe<T: ?Sized>(PhantomData<fn() -> PhantomData<T>>);
79
80impl<T: ?Sized> Probe<T> {
81 pub const fn new() -> Self {
83 Self(PhantomData)
84 }
85}
86
87impl<T: ?Sized> Default for Probe<T> {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93macro_rules! probe_traits {
94 ($($(#[$doc:meta])* fn $name:ident($($arg:ident: $ty:ty),*) $(-> $ret:ty)? => |$($d:ident),*| $default:expr;)*) => {
95 #[doc(hidden)]
97 pub trait ViaHooks<T: ?Sized> {
98 $($(#[$doc])* fn $name(&self, $($arg: $ty),*) $(-> $ret)?;)*
99 }
100
101 #[doc(hidden)]
103 pub trait ViaDefault<T: ?Sized> {
104 $($(#[$doc])* fn $name(&self, $($arg: $ty),*) $(-> $ret)?;)*
105 }
106
107 impl<T: ?Sized> ViaDefault<T> for Probe<T> {
108 $(
109 #[allow(unused_variables)]
110 fn $name(&self, $($arg: $ty),*) $(-> $ret)? {
111 let ($($d,)*) = ($($d,)*);
112 $default
113 }
114 )*
115 }
116 };
117}
118
119probe_traits! {
120 fn has_hooks(model: &T) -> bool => |model| false;
122 fn before_model(input: &mut Value, ctx: &mut ValidationContext) => |input, ctx| ();
124 fn before_field(field: &str, input: &mut Value, ctx: &mut ValidationContext) => |field, input, ctx| ();
126 fn after_fields(model: &T, ctx: &mut ValidationContext) => |model, ctx| ();
128 fn after_model(model: &T, ctx: &mut ValidationContext) => |model, ctx| ();
130 fn computed_fields(model: &T, out: &mut Map<String, Value>, opts: &DumpOptions) -> Result<(), DumpError> => |model, out, opts| Ok(());
132 fn computed_schema(properties: &mut Map<String, Value>, registry: &mut SchemaRegistry) => |properties, registry| ();
134 fn serialize_field(model: &T, field: &str, value: Value) -> Result<Value, DumpError> => |model, field, value| Ok(value);
136 fn serialize_model(model: &T, value: Value) -> Result<Value, DumpError> => |model, value| Ok(value);
138}
139
140impl<T: ModelHooks> ViaHooks<T> for &Probe<T> {
141 fn has_hooks(&self, _: &T) -> bool {
142 true
143 }
144
145 fn before_model(&self, input: &mut Value, ctx: &mut ValidationContext) {
146 T::before_model(input, ctx);
147 }
148 fn before_field(&self, field: &str, input: &mut Value, ctx: &mut ValidationContext) {
149 T::before_field(field, input, ctx);
150 }
151 fn after_fields(&self, model: &T, ctx: &mut ValidationContext) {
152 model.after_fields(ctx);
153 }
154 fn after_model(&self, model: &T, ctx: &mut ValidationContext) {
155 model.after_model(ctx);
156 }
157 fn computed_fields(
158 &self,
159 model: &T,
160 out: &mut Map<String, Value>,
161 opts: &DumpOptions,
162 ) -> Result<(), DumpError> {
163 model.computed_fields(out, opts)
164 }
165 fn computed_schema(&self, properties: &mut Map<String, Value>, registry: &mut SchemaRegistry) {
166 T::computed_schema(properties, registry);
167 }
168 fn serialize_field(&self, model: &T, field: &str, value: Value) -> Result<Value, DumpError> {
169 model.serialize_field(field, value)
170 }
171 fn serialize_model(&self, model: &T, value: Value) -> Result<Value, DumpError> {
172 model.serialize_model(value)
173 }
174}
175
176#[cfg(test)]
177#[allow(clippy::needless_borrow)]
179mod tests {
180 use super::*;
181 use serde_json::json;
182
183 struct WithHooks;
184 impl ModelHooks for WithHooks {
185 fn after_model(&self, ctx: &mut ValidationContext) {
186 ctx.error("custom", "from hook");
187 }
188 fn serialize_model(&self, _value: Value) -> Result<Value, DumpError> {
189 Ok(json!("hooked"))
190 }
191 }
192
193 struct WithoutHooks;
194
195 #[test]
196 fn probe_selects_hooks_when_implemented() {
197 let mut ctx = ValidationContext::new();
198 (&&Probe::<WithHooks>::new()).after_model(&WithHooks, &mut ctx);
199 assert_eq!(ctx.error_count(), 1);
200 let out = (&&Probe::<WithHooks>::new())
201 .serialize_model(&WithHooks, json!(1))
202 .unwrap();
203 assert_eq!(out, json!("hooked"));
204 }
205
206 #[test]
207 fn probe_falls_back_to_no_op() {
208 let mut ctx = ValidationContext::new();
209 (&&Probe::<WithoutHooks>::new()).after_model(&WithoutHooks, &mut ctx);
210 assert_eq!(ctx.error_count(), 0);
211 let out = (&&Probe::<WithoutHooks>::new())
212 .serialize_model(&WithoutHooks, json!(1))
213 .unwrap();
214 assert_eq!(out, json!(1));
215 }
216}