cratefield_tables/validate.rs
1//! Row validation: one `serde_json` object against one [`TableDef`].
2//!
3//! The answer is a structured list, and the list renders into the
4//! harness's existing RFC 9457 problem+json error rather than a new error
5//! style: [`RowErrors`] converts to a
6//! [`cratefield_core::Problem`] carrying the `validation-failed` slug
7//! core already publishes, so a declared table's 400 looks exactly like a
8//! module's.
9//!
10//! # The rules
11//!
12//! - **A row is a JSON object.** An array, a string or a number is one
13//! error against the row itself, reported with an empty field name,
14//! which is the RFC 6901 pointer to the whole document.
15//! - **`null` is absence.** `{"note": null}` and `{}` are the same
16//! verdict, always. A field that must be present is missing in both;
17//! an optional field is satisfied by both. Nothing else in the crate
18//! distinguishes them, and a second implementation must not either.
19//! - **A default satisfies `required`.** A write only has to carry a
20//! field that is required *and* has no default, because otherwise the
21//! database supplies the value.
22//! - **An unknown key is an error.** A misspelled column that is silently
23//! dropped is a write that looks like it worked.
24//! - **Nothing is coerced, nothing is trimmed, and length is counted in
25//! Unicode characters.** The full value rules are in
26//! [`check_value`](crate::check_value).
27//! - **Uniqueness is not checked here.** It reads other rows, so it is
28//! the database's job. Foreign keys are the same.
29//!
30//! Errors come out in a fixed order: declared fields in declaration
31//! order, then unknown keys sorted by name. A caller can compare two runs
32//! and a second implementation can compare against the corpus.
33
34use cratefield_core::Problem;
35use serde_json::Value;
36use std::fmt::Write as _;
37
38use crate::schema::{FieldKind, TableDef, TextFormat};
39use crate::value::{ErrorCode, check_value};
40
41/// The largest number of errors a problem `detail` spells out before it
42/// summarises the rest. The structured list is never truncated.
43pub const MAX_DETAIL_ERRORS: usize = 10;
44
45/// The longest an *unknown* key is echoed back at the caller, in
46/// characters. Declared field names are bounded by the schema's own
47/// identifier rules; an unknown key is whatever the caller sent, so a
48/// megabyte key name came straight back in the 400 body, ten of them per
49/// request. Truncated keys keep the error useful — a misspelling is
50/// visible in the first few characters — without turning the validator
51/// into an echo service.
52pub const MAX_UNKNOWN_KEY_CHARS: usize = 64;
53
54/// `key`, cut to [`MAX_UNKNOWN_KEY_CHARS`] characters with a marker. Cut
55/// on a character boundary, never a byte one, so a multi-byte key cannot
56/// produce invalid UTF-8 or a lone surrogate in the response.
57fn bounded_key(key: &str) -> String {
58 let mut cut: String = key.chars().take(MAX_UNKNOWN_KEY_CHARS).collect();
59 if key.chars().nth(MAX_UNKNOWN_KEY_CHARS).is_some() {
60 cut.push('…');
61 }
62 cut
63}
64
65/// One rejected field.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct RowError {
68 /// The declared field name, or an empty string for the row itself.
69 pub field: String,
70 /// The stable code a conformance case is matched on.
71 pub code: ErrorCode,
72 /// A line for a person, phrased to follow the field name.
73 pub message: String,
74}
75
76impl RowError {
77 fn new(field: impl Into<String>, code: ErrorCode, message: impl Into<String>) -> Self {
78 Self {
79 field: field.into(),
80 code,
81 message: message.into(),
82 }
83 }
84
85 /// `field: message`, or just the message for the row itself.
86 #[must_use]
87 pub fn line(&self) -> String {
88 if self.field.is_empty() {
89 self.message.clone()
90 } else {
91 format!("{} {}", self.field, self.message)
92 }
93 }
94}
95
96/// Every reason a row was rejected, in a fixed order. Never empty.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct RowErrors {
99 errors: Vec<RowError>,
100}
101
102impl RowErrors {
103 /// One rejection, for a caller that found a single problem outside
104 /// [`validate_row`] — a primary key whose value is not the kind its
105 /// column is declared as, for instance.
106 pub(crate) fn one(field: &str, code: ErrorCode, message: impl Into<String>) -> Self {
107 Self {
108 errors: vec![RowError::new(field, code, message)],
109 }
110 }
111
112 /// The rejections, declared fields in declaration order and then
113 /// unknown keys sorted by name.
114 #[must_use]
115 pub fn errors(&self) -> &[RowError] {
116 &self.errors
117 }
118
119 /// The problem `detail`: at most [`MAX_DETAIL_ERRORS`] lines joined
120 /// with `; `, then a count of what was left out. The structured list
121 /// keeps everything.
122 #[must_use]
123 pub fn detail(&self) -> String {
124 let shown: Vec<String> = self
125 .errors
126 .iter()
127 .take(MAX_DETAIL_ERRORS)
128 .map(RowError::line)
129 .collect();
130 let mut detail = shown.join("; ");
131 if self.errors.len() > MAX_DETAIL_ERRORS {
132 let hidden = self.errors.len() - MAX_DETAIL_ERRORS;
133 let _ = write!(detail, " (and {hidden} more)");
134 }
135 detail
136 }
137
138 /// The `400 validation-failed` problem core already publishes. No new
139 /// slug: a declared table's rejection reads like a module's.
140 #[must_use]
141 pub fn problem(&self) -> Problem {
142 Problem::validation_failed(self.detail())
143 }
144}
145
146impl From<RowErrors> for Problem {
147 fn from(errors: RowErrors) -> Self {
148 errors.problem()
149 }
150}
151
152impl std::fmt::Display for RowErrors {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 f.write_str(&self.detail())
155 }
156}
157
158impl std::error::Error for RowErrors {}
159
160/// Rewrites every declared `format = "email"` field of `row` into its
161/// canonical form, and returns the row.
162///
163/// **Run this before [`validate_row`], never inside it.** The validator
164/// coerces nothing and trims nothing — that rule is what lets the
165/// conformance corpus mean one thing in two languages — so normalisation
166/// has to be a separate, visible step rather than a quiet rewrite hidden
167/// in a checker.
168///
169/// It is not optional, though. `unique` on an email column is enforced by
170/// the database on the bytes it is given, so without this
171/// `Alice@Example.COM` and `alice@example.com` are two rows, while every
172/// module in the harness treats them as one address
173/// (`cratefield_core::email::normalize`, issue #10). A declared table
174/// that skipped this would mean something different by `unique` than the
175/// rest of the harness does.
176///
177/// Trim, Unicode NFC, lowercase — the same three steps core applies, and
178/// idempotent for the same reason. A non-string value is left alone: it
179/// is the validator's business to reject it, not this function's.
180#[must_use]
181pub fn normalize_row(table: &TableDef, row: Value) -> Value {
182 let Value::Object(mut object) = row else {
183 return row;
184 };
185 for field in &table.fields {
186 if !matches!(
187 field.kind,
188 FieldKind::Text {
189 format: Some(TextFormat::Email),
190 ..
191 }
192 ) {
193 continue;
194 }
195 if let Some(Value::String(address)) = object.get(&field.name) {
196 let normalized = cratefield_core::normalize_email(address);
197 object.insert(field.name.clone(), Value::String(normalized));
198 }
199 }
200 Value::Object(object)
201}
202
203/// Checks `row` against `table`.
204///
205/// # Errors
206///
207/// Every rejection, in the fixed order described on this module.
208pub fn validate_row(table: &TableDef, row: &Value) -> Result<(), RowErrors> {
209 let Some(object) = row.as_object() else {
210 return Err(RowErrors {
211 errors: vec![RowError::new(
212 "",
213 ErrorCode::NotAnObject,
214 format!("the row must be a JSON object, not {}", type_name(row)),
215 )],
216 });
217 };
218
219 let mut errors = Vec::new();
220
221 for field in &table.fields {
222 // A missing key and a JSON null are the same thing: no value.
223 match object.get(&field.name) {
224 None | Some(Value::Null) => {
225 if field.must_be_present() {
226 errors.push(RowError::new(
227 &field.name,
228 ErrorCode::Required,
229 "is required",
230 ));
231 }
232 }
233 Some(value) => {
234 if let Err(error) = check_value(&field.kind, value) {
235 errors.push(RowError::new(&field.name, error.code, error.message));
236 }
237 }
238 }
239 }
240
241 let mut unknown: Vec<&String> = object
242 .keys()
243 .filter(|key| table.field(key).is_none())
244 .collect();
245 unknown.sort();
246 for key in unknown {
247 errors.push(RowError::new(
248 bounded_key(key),
249 ErrorCode::UnknownField,
250 format!("is not a declared field of `{}`", table.name),
251 ));
252 }
253
254 if errors.is_empty() {
255 Ok(())
256 } else {
257 Err(RowErrors { errors })
258 }
259}
260
261fn type_name(value: &Value) -> &'static str {
262 match value {
263 Value::Null => "null",
264 Value::Bool(_) => "a boolean",
265 Value::Number(_) => "a number",
266 Value::String(_) => "a string",
267 Value::Array(_) => "an array",
268 Value::Object(_) => "an object",
269 }
270}