Skip to main content

tablo_core/values/
error.rs

1//! [`FieldError`] and [`FieldErrors`]: the keys a submission was refused under, and why.
2
3use std::collections::HashSet;
4
5use crate::Text;
6
7/// Why a key was refused.
8#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum FieldErrorKind {
11    /// Posted empty, and the field has no blank answer. The form renders
12    /// "{label} is required" under the control.
13    Required,
14    /// Posted a value the field refuses, with the sentence the form renders.
15    Invalid(Text),
16}
17
18/// One key a submission was refused under: a form key, or a record form's
19/// field enum in [`Resource::validate_record`](crate::Resource::validate_record).
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct FieldError<K = String> {
22    /// What the error renders under.
23    pub key: K,
24    /// Why the key was refused.
25    pub kind: FieldErrorKind,
26}
27
28impl<K> FieldError<K> {
29    /// `key` was posted empty and has no blank answer.
30    pub fn required(key: impl Into<K>) -> Self {
31        Self {
32            key: key.into(),
33            kind: FieldErrorKind::Required,
34        }
35    }
36
37    /// `key` carried a value it refuses; `message` says why.
38    pub fn invalid(key: impl Into<K>, message: impl Into<Text>) -> Self {
39        Self {
40            key: key.into(),
41            kind: FieldErrorKind::Invalid(message.into()),
42        }
43    }
44
45    /// The sentence the form renders under the control labelled `label`.
46    pub fn message(&self, label: impl Into<Text>) -> Text {
47        match &self.kind {
48            FieldErrorKind::Required => crate::text!("tablo-field-required", label = label),
49            FieldErrorKind::Invalid(message) => message.clone(),
50        }
51    }
52}
53
54/// The refusal of a key holding another shape than its parse reads: a list or rows where it reads
55/// text, or text where it reads a list. Only a schema whose controls disagree with its parse
56/// holds one.
57pub(crate) fn unreadable(key: &str) -> FieldError {
58    FieldError::invalid(key, Text::message("tablo-field-unreadable"))
59}
60
61/// A refused submission.
62///
63/// [`Resource::validate_record`](crate::Resource::validate_record) keys it by the record form's
64/// field enum (`FieldErrors<UserFormField>`), so an error always names a field the form renders;
65/// one on an embedded value renders under its first control. The panel keys every error by the
66/// form key it renders under (`FieldErrors`, keyed by `String`), and [`Source::form`] reads them
67/// back when a schema renders.
68///
69/// [`Source::form`]: crate::Source::form
70#[derive(Debug)]
71pub struct FieldErrors<K = String> {
72    errors: Vec<FieldError<K>>,
73}
74
75impl<K> Default for FieldErrors<K> {
76    fn default() -> Self {
77        Self { errors: Vec::new() }
78    }
79}
80
81impl<K> FieldErrors<K> {
82    /// No errors.
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Refuse `key` with `message`.
88    pub fn add(&mut self, key: impl Into<K>, message: impl Into<Text>) {
89        self.errors.push(FieldError::invalid(key, message));
90    }
91
92    /// Refuse `error`'s key with `error`.
93    pub fn push(&mut self, error: FieldError<K>) {
94        self.errors.push(error);
95    }
96
97    /// Whether nothing was refused.
98    pub fn is_empty(&self) -> bool {
99        self.errors.is_empty()
100    }
101
102    /// Every error, in the order it was added.
103    pub fn iter(&self) -> impl Iterator<Item = &FieldError<K>> {
104        self.errors.iter()
105    }
106
107    /// Append `other`'s errors after these.
108    pub fn extend(&mut self, other: Self) {
109        self.errors.extend(other.errors);
110    }
111}
112
113impl<K> IntoIterator for FieldErrors<K> {
114    type Item = FieldError<K>;
115    type IntoIter = std::vec::IntoIter<FieldError<K>>;
116
117    fn into_iter(self) -> Self::IntoIter {
118        self.errors.into_iter()
119    }
120}
121
122impl FieldErrors {
123    /// Whether any error renders under `key`.
124    pub fn contains_key(&self, key: &str) -> bool {
125        self.errors.iter().any(|error| error.key == key)
126    }
127
128    /// The error `key` renders: the first one added.
129    pub fn first(&self, key: &str) -> Option<&FieldError> {
130        self.errors.iter().find(|error| error.key == key)
131    }
132
133    /// Take `other`'s errors, dropping this collection's own errors under every
134    /// key `other` names.
135    ///
136    /// A source that owns a key answers for it: a rejected upload replaces the
137    /// error the emptied control would otherwise report.
138    pub(crate) fn replace(&mut self, other: Self) {
139        let owned: HashSet<&str> = other
140            .errors
141            .iter()
142            .map(|error| error.key.as_str())
143            .collect();
144        self.errors
145            .retain(|kept| !owned.contains(kept.key.as_str()));
146        self.errors.extend(other.errors);
147    }
148}