Skip to main content

reliakit_json/
form.rs

1//! Optional integration with [`reliakit-validate`].
2//!
3//! Available with the `validate` feature (which also enables `primitives`).
4//! [`JsonForm`] pulls several fields out of a JSON object and collects *every*
5//! failure into a single [`ValidationError`], rather than stopping at the first
6//! one — the usual shape for validating an untrusted request body.
7//!
8//! ```
9//! use reliakit_json::{JsonForm, parse_str};
10//! use reliakit_primitives::{Email, Hostname};
11//!
12//! let doc = parse_str(r#"{ "email": "nope", "host": 42 }"#).unwrap();
13//! let obj = doc.as_object().unwrap();
14//!
15//! let mut form = JsonForm::new(obj);
16//! let _email: Option<Email> = form.str_field("email");
17//! let _host: Option<Hostname> = form.str_field("host");
18//!
19//! // Both fields failed, and both are reported together.
20//! let errors = form.finish().unwrap_err();
21//! assert_eq!(errors.violations().len(), 2);
22//! ```
23//!
24//! Because [`Violation`] messages are `&'static str`, the per-field message is a
25//! fixed summary (`is required` / `must be a string` / `is invalid`). For the
26//! full [`PrimitiveError`](reliakit_primitives::PrimitiveError) detail of a
27//! single field, use [`JsonObject::get_str_as`](crate::JsonObject::get_str_as).
28
29use reliakit_primitives::PrimitiveError;
30use reliakit_validate::{ValidateResult, ValidationError, Violation};
31
32use crate::primitives::JsonExtractErrorKind;
33use crate::JsonObject;
34
35/// Accumulates field-extraction failures from a [`JsonObject`] into a single
36/// [`ValidationError`].
37///
38/// Create one with [`new`](Self::new), pull each field with
39/// [`str_field`](Self::str_field), then call [`finish`](Self::finish) to get
40/// `Ok(())` or every collected [`Violation`] at once.
41pub struct JsonForm<'a> {
42    object: &'a JsonObject,
43    errors: ValidationError,
44}
45
46impl<'a> JsonForm<'a> {
47    /// Starts a form over `object` with no recorded violations.
48    pub fn new(object: &'a JsonObject) -> Self {
49        Self {
50            object,
51            errors: ValidationError::empty(),
52        }
53    }
54
55    /// Extracts `field` as a string-backed primitive `T`.
56    ///
57    /// On success returns `Some(value)`. On failure records a [`Violation`] named
58    /// `field` (with a fixed summary message) and returns `None`, so validation
59    /// continues and every bad field is reported by [`finish`](Self::finish).
60    pub fn str_field<T>(&mut self, field: &'static str) -> Option<T>
61    where
62        T: TryFrom<&'a str, Error = PrimitiveError>,
63    {
64        let object = self.object;
65        match object.get_str_as::<T>(field) {
66            Ok(value) => Some(value),
67            Err(error) => {
68                let message = match error.kind() {
69                    JsonExtractErrorKind::Missing => "is required",
70                    JsonExtractErrorKind::WrongType { .. } => "must be a string",
71                    _ => "is invalid",
72                };
73                self.errors.push(Violation::with_field(field, message));
74                None
75            }
76        }
77    }
78
79    /// Returns `true` if no violations have been recorded yet.
80    pub fn is_valid(&self) -> bool {
81        self.errors.is_empty()
82    }
83
84    /// Borrows the violations recorded so far.
85    pub fn errors(&self) -> &ValidationError {
86        &self.errors
87    }
88
89    /// Consumes the form: `Ok(())` if every field validated, otherwise `Err`
90    /// with all collected violations.
91    pub fn finish(self) -> ValidateResult {
92        self.errors.finish()
93    }
94}
95
96#[cfg(all(test, feature = "validate"))]
97mod tests {
98    use super::JsonForm;
99    use crate::parse_str;
100    use reliakit_primitives::{Email, Hostname};
101
102    fn obj(input: &str) -> crate::JsonObject {
103        parse_str(input).unwrap().as_object().unwrap().clone()
104    }
105
106    #[test]
107    fn all_fields_valid_finishes_ok() {
108        let o = obj(r#"{ "email": "ops@example.com", "host": "api.example.com" }"#);
109        let mut form = JsonForm::new(&o);
110        let email: Option<Email> = form.str_field("email");
111        let host: Option<Hostname> = form.str_field("host");
112        assert_eq!(email.unwrap().as_str(), "ops@example.com");
113        assert_eq!(host.unwrap().as_str(), "api.example.com");
114        assert!(form.is_valid());
115        assert!(form.finish().is_ok());
116    }
117
118    #[test]
119    fn collects_every_failure_together() {
120        let o = obj(r#"{ "email": "nope", "host": 42 }"#);
121        let mut form = JsonForm::new(&o);
122        assert!(form.str_field::<Email>("email").is_none());
123        assert!(form.str_field::<Hostname>("host").is_none());
124        assert!(form.str_field::<Email>("missing").is_none());
125        assert!(!form.is_valid());
126
127        let errors = form.finish().unwrap_err();
128        let v = errors.violations();
129        assert_eq!(v.len(), 3);
130        assert_eq!(v[0].field, Some("email"));
131        assert_eq!(v[0].message, "is invalid");
132        assert_eq!(v[1].field, Some("host"));
133        assert_eq!(v[1].message, "must be a string");
134        assert_eq!(v[2].field, Some("missing"));
135        assert_eq!(v[2].message, "is required");
136    }
137
138    #[test]
139    fn errors_accessor_tracks_progress() {
140        let o = obj(r#"{ "email": "ops@example.com" }"#);
141        let mut form = JsonForm::new(&o);
142        let _: Option<Email> = form.str_field("email");
143        assert!(form.errors().is_empty());
144        let _: Option<Hostname> = form.str_field("host"); // missing
145        assert_eq!(form.errors().len(), 1);
146    }
147}