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
//! Per-field clean / validate hooks (features #83).
//!
//! The declarative attributes — `#[umbral(trim, lowercase, max_length, email)]` —
//! cover the rules the framework can name. This is the escape hatch for the ones
//! only your app knows: masking a banned word, normalising a phone number into
//! E.164, rejecting a username that collides with a reserved route.
//!
//! One hook shape covers both jobs, because in practice they are the same job:
//!
//! ```ignore
//! register_cleaner::<Post>("title", |v| {
//! let s = v.as_str().unwrap_or_default();
//! if s.contains("<script") {
//! return Err("HTML is not allowed in a title".into()); // reject
//! }
//! Ok(json!(s.replace("damn", "d***"))) // transform
//! });
//! ```
//!
//! `Ok(value)` rewrites the value before it is written; `Err(message)` fails the
//! write as a [`WriteError::Validator`], which every surface already knows how to
//! render — a REST 400 with a per-field error map, the `Form<T>` extractor's
//! errors, and the admin's inline field errors. You write the rule; you do not
//! wire it anywhere.
//!
//! # Where it runs
//!
//! At the same seam as `trim` / `lowercase`, which means **every** write path:
//! the typed `create` / `bulk_create` / `update_values`, and the dynamic path that
//! REST and the admin run on. A hook that only fired for *some* writers would be
//! worse than none — it would look enforced while a background job walked past it.
//!
//! The framework ships no word lists, no policy, no opinions about content. It
//! ships the hook.
use HashMap;
use ;
use Value as JsonValue;
use crateModel;
use crateWriteError;
/// A field hook: rewrite the value, or reject the write with a message.
///
/// Sync on purpose. A cleaner runs inside the write path, once per field per row,
/// and an `await` there would put a database round-trip (or an HTTP call to a
/// moderation API) in the middle of every insert. If your rule genuinely needs
/// I/O, do it in the handler and pass the result down — that keeps the cost where
/// the developer can see it.
pub type Cleaner = ;
type Registry = ;
/// Register a clean/validate hook for one field of one model.
///
/// Call it at boot — from `main`, or a plugin's `Plugin::on_ready`. Hooks run in
/// registration order, each seeing the previous one's output, so you can compose
/// a normalise step and a reject step.
///
/// # Panics
///
/// If `field` is not a column on `M`. That is a boot-time programming error, and
/// the alternative — quietly registering a hook against a field that does not
/// exist — means a moderation or sanitisation rule that *looks* installed and
/// never runs. Failing loudly at startup is the whole point.
/// Drop every registered hook. Test-only — the registry is process-global, so a
/// test that registers one would otherwise leak into the next.
/// True when *any* hook is registered — checked before the per-field work so a
/// codebase with no cleaners pays a single atomic read per write, not a hash
/// lookup per column.
pub
/// Run every hook registered for `(table, field)` against `value`.
///
/// `Ok(Some(v))` — a hook rewrote it. `Ok(None)` — no hooks, nothing to do (the
/// caller can skip a clone). `Err` — a hook rejected the write.
pub