Skip to main content

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}