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