tablo_core/form.rs
1//! Record forms: the typed value a form submission parses into, and the write
2//! that stores it.
3//!
4//! Declares one `RecordForm` struct with one field per written model column.
5//! The struct is the form's one source of truth: it parses every key, and a
6//! field's blank answer decides whether its control is required. Completes
7//! unposted keys from the stored record and writes only named fields plus model
8//! defaults.
9//!
10//! ```no_run
11//! #[derive(Debug, Clone, toasty::Model)]
12//! struct User {
13//! #[key]
14//! #[auto]
15//! id: uuid::Uuid,
16//! name: String,
17//! age: i64,
18//! }
19//!
20//! #[derive(tablo_core::RecordForm)]
21//! #[form(model = User)]
22//! struct UserForm {
23//! name: String,
24//! #[form(blank = 0)]
25//! age: i64,
26//! }
27//!
28//! // A column type toasty stores but the form edge cannot spell.
29//! #[derive(Debug, Clone, toasty::Model)]
30//! struct Tagged {
31//! #[key]
32//! #[auto]
33//! id: uuid::Uuid,
34//! tags: Vec<String>,
35//! }
36//! ```
37//!
38//! # Compile-time refusals
39//!
40//! A field the model does not have:
41//!
42//! ```compile_fail
43//! # #[derive(Debug, Clone, toasty::Model)]
44//! # struct User { #[key] #[auto] id: uuid::Uuid, name: String, age: i64 }
45//! #[derive(tablo_core::RecordForm)]
46//! #[form(model = User)]
47//! struct UserForm {
48//! nickname: String,
49//! }
50//! ```
51//!
52//! A type the model's field does not have:
53//!
54//! ```compile_fail
55//! # #[derive(Debug, Clone, toasty::Model)]
56//! # struct User { #[key] #[auto] id: uuid::Uuid, name: String, age: i64 }
57//! #[derive(tablo_core::RecordForm)]
58//! #[form(model = User)]
59//! struct UserForm {
60//! age: i32,
61//! }
62//! ```
63//!
64//! A bare choice, which would offer nothing: a foreign key names its source with
65//! `#[form(relationship = ..)]`.
66//!
67//! ```compile_fail
68//! # #[derive(Debug, Clone, toasty::Model)]
69//! # struct Post { #[key] #[auto] id: uuid::Uuid, author_id: uuid::Uuid }
70//! #[derive(tablo_core::RecordForm)]
71//! #[form(model = Post)]
72//! struct PostForm {
73//! #[form(choice)]
74//! author_id: uuid::Uuid,
75//! }
76//! ```
77//!
78//! Two keys that each pick the control:
79//!
80//! ```compile_fail
81//! # #[derive(Debug, Clone, toasty::Model)]
82//! # struct Post { #[key] #[auto] id: uuid::Uuid, author_id: String }
83//! # struct Authors;
84//! #[derive(tablo_core::RecordForm)]
85//! #[form(model = Post)]
86//! struct PostForm {
87//! #[form(relationship = Authors, file)]
88//! author_id: String,
89//! }
90//! ```
91//!
92//! A scalar type the form edge cannot spell:
93//!
94//! ```compile_fail
95//! #[derive(Debug, Clone, toasty::Model)]
96//! struct Tagged {
97//! #[key]
98//! #[auto]
99//! id: uuid::Uuid,
100//! tags: Vec<String>,
101//! }
102//!
103//! #[derive(tablo_core::RecordForm)]
104//! #[form(model = Tagged)]
105//! struct TaggedForm {
106//! tags: Vec<String>,
107//! }
108//! ```
109//!
110//! A many-to-many field whose keys are not its source's primary key:
111//!
112//! ```compile_fail
113//! # #[derive(Debug, Clone, toasty::Model)]
114//! # struct Post {
115//! # #[key] #[auto] id: uuid::Uuid,
116//! # #[has_many] taggings: toasty::Deferred<Vec<Tagging>>,
117//! # #[has_many(via = taggings.tag)] tags: toasty::Deferred<Vec<Tag>>,
118//! # }
119//! # #[derive(Debug, Clone, toasty::Model)]
120//! # struct Tag { #[key] #[auto] id: uuid::Uuid }
121//! # #[derive(Debug, Clone, toasty::Model)]
122//! # struct Tagging {
123//! # #[key] #[auto] id: uuid::Uuid,
124//! # #[index] post_id: uuid::Uuid,
125//! # #[belongs_to(key = post_id, references = id)] post: toasty::Deferred<Post>,
126//! # #[index] tag_id: uuid::Uuid,
127//! # #[belongs_to(key = tag_id, references = id)] tag: toasty::Deferred<Tag>,
128//! # }
129//! # struct TagResource;
130//! # impl tablo_core::Resource for TagResource { type Model = Tag; type Form = tablo_core::NoForm<Tag>; }
131//! #[derive(tablo_core::RecordForm)]
132//! #[form(model = Post)]
133//! struct PostForm {
134//! #[form(relationship = TagResource)]
135//! tags: Vec<i64>,
136//! }
137//! ```
138//!
139//! `optional` on a type with no empty value:
140//!
141//! ```compile_fail
142//! # #[derive(Debug, Clone, toasty::Model)]
143//! # struct User { #[key] #[auto] id: uuid::Uuid, name: String, age: i64 }
144//! #[derive(tablo_core::RecordForm)]
145//! #[form(model = User)]
146//! struct UserForm {
147//! #[form(optional)]
148//! age: i64,
149//! }
150//! ```
151//!
152//! `embed` on a type that is not an [`EmbeddedForm`](crate::EmbeddedForm):
153//!
154//! ```compile_fail
155//! # #[derive(Debug, Clone, toasty::Model)]
156//! # struct User { #[key] #[auto] id: uuid::Uuid, name: String, age: i64 }
157//! #[derive(tablo_core::RecordForm)]
158//! #[form(model = User)]
159//! struct UserForm {
160//! #[form(embed)]
161//! name: String,
162//! }
163//! ```
164
165use std::{
166 collections::{HashMap, HashSet},
167 fmt::Debug,
168 hash::Hash,
169};
170
171use toasty::{Executor, schema::Model};
172use topcoat::context::Cx;
173
174use crate::{
175 detail::Detail,
176 schema::{FieldResolver, OptionSource, Schema, TypedValue},
177 table::{Table, column::Includes},
178};
179
180/// A type one form key reads and writes: `String`, every [`TypedValue`] type,
181/// every enum deriving [`Options`](crate::Options), and an `Option` of any of them.
182///
183/// The value the parse sees is trimmed and non-empty; an empty submission is the
184/// record-form field's **blank answer** instead, and a field with none is required.
185///
186/// A text field binds a path of any `FormScalar` type
187/// ([`Field::text`](crate::Field::text)), and the form derives read and write
188/// every field that is not `#[form(embed)]` through it.
189#[diagnostic::on_unimplemented(
190 message = "`{Self}` is not a form scalar",
191 label = "a form field of this type has no text spelling",
192 note = "a form scalar is `String`, a `TypedValue` type, an `Options` enum, or an `Option` of \
193 one; implement `TypedValue` for an app type, derive `Options` for a unit enum, or \
194 mark an `EmbeddedForm` value `#[form(embed)]`"
195)]
196pub trait FormScalar: Sized {
197 /// The `type` attribute of the text control that edits it.
198 const INPUT_TYPE: &'static str = "text";
199
200 /// Parse a trimmed, non-empty submission, or return the error message.
201 fn parse_form(value: &str) -> std::result::Result<Self, String>;
202
203 /// The form spelling of a stored value.
204 fn to_form(&self) -> String;
205
206 /// What a column cell and a group header read: the form spelling, or an
207 /// `Options` enum's label.
208 fn to_label(&self) -> String {
209 self.to_form()
210 }
211}
212
213impl FormScalar for String {
214 fn parse_form(value: &str) -> std::result::Result<Self, String> {
215 Ok(value.to_string())
216 }
217
218 fn to_form(&self) -> String {
219 self.clone()
220 }
221}
222
223impl<T: TypedValue> FormScalar for T {
224 const INPUT_TYPE: &'static str = T::INPUT_TYPE;
225
226 fn parse_form(value: &str) -> std::result::Result<Self, String> {
227 T::parse_input(value).ok_or_else(|| format!("`{value}` is not a valid {}", T::NOUN))
228 }
229
230 fn to_form(&self) -> String {
231 self.to_string()
232 }
233}
234
235/// A form scalar whose `Option` is one too: every [`TypedValue`] type, and every enum deriving
236/// [`Options`](crate::Options), which implements it.
237#[doc(hidden)]
238pub trait NullableScalar: FormScalar {}
239
240impl<T: TypedValue> NullableScalar for T {}
241
242impl<T: NullableScalar> FormScalar for Option<T> {
243 const INPUT_TYPE: &'static str = T::INPUT_TYPE;
244
245 fn parse_form(value: &str) -> std::result::Result<Self, String> {
246 T::parse_form(value).map(Some)
247 }
248
249 fn to_form(&self) -> String {
250 self.as_ref().map(T::to_form).unwrap_or_default()
251 }
252
253 fn to_label(&self) -> String {
254 self.as_ref().map(T::to_label).unwrap_or_default()
255 }
256}
257
258impl FormScalar for Option<String> {
259 fn parse_form(value: &str) -> std::result::Result<Self, String> {
260 Ok(Some(value.to_string()))
261 }
262
263 fn to_form(&self) -> String {
264 self.clone().unwrap_or_default()
265 }
266}
267
268/// Compiles only for a form scalar: the form derives call it, spanned on a
269/// field's type, so a field of another type fails there.
270#[doc(hidden)]
271pub fn assert_form_scalar<T: FormScalar>() {}
272
273/// Why a key was refused.
274#[derive(Debug, Clone, PartialEq, Eq)]
275#[non_exhaustive]
276pub enum FieldErrorKind {
277 /// Posted empty, and the field has no blank answer. The form renders
278 /// "{label} is required" under the control.
279 Required,
280 /// Posted a value the field refuses, with the sentence the form renders.
281 Invalid(String),
282}
283
284/// One key a submission was refused under: a form key, or a record form's
285/// field enum in [`Resource::validate_record`](crate::Resource::validate_record).
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct FieldError<K = String> {
288 /// What the error renders under.
289 pub key: K,
290 /// Why the key was refused.
291 pub kind: FieldErrorKind,
292}
293
294impl<K> FieldError<K> {
295 /// `key` was posted empty and has no blank answer.
296 pub fn required(key: impl Into<K>) -> Self {
297 Self {
298 key: key.into(),
299 kind: FieldErrorKind::Required,
300 }
301 }
302
303 /// `key` carried a value it refuses; `message` says why.
304 pub fn invalid(key: impl Into<K>, message: impl Into<String>) -> Self {
305 Self {
306 key: key.into(),
307 kind: FieldErrorKind::Invalid(message.into()),
308 }
309 }
310
311 /// The sentence the form renders under the control labelled `label`.
312 pub fn message(&self, label: &str) -> String {
313 match &self.kind {
314 FieldErrorKind::Required => format!("{label} is required"),
315 FieldErrorKind::Invalid(message) => message.clone(),
316 }
317 }
318}
319
320/// Read one scalar from a completed submission.
321///
322/// Trims the value; an empty one is `blank`, the field's blank answer, or
323/// refuses as [`FieldErrorKind::Required`] when it has none.
324pub fn parse_scalar<T: FormScalar>(
325 key: &str,
326 values: &HashMap<String, String>,
327 blank: Option<T>,
328) -> std::result::Result<T, FieldError> {
329 let raw = values.get(key).map(|value| value.trim()).unwrap_or("");
330 if raw.is_empty() {
331 return blank.ok_or_else(|| FieldError::required(key));
332 }
333 T::parse_form(raw).map_err(|message| FieldError::invalid(key, message))
334}
335
336/// Read a password from a completed submission: required as [`parse_scalar`] reads a `String`,
337/// but kept as typed, with its outer spaces, since a login compares the password untrimmed.
338pub fn parse_password(
339 key: &str,
340 values: &HashMap<String, String>,
341 blank: Option<String>,
342) -> std::result::Result<String, FieldError> {
343 parse_scalar::<String>(key, values, blank)?;
344 Ok(values.get(key).cloned().unwrap_or_default())
345}
346
347/// The values a multiple choice holds, as its one form key carries them: what a record form
348/// hydrates a many-to-many field with, and what a hand-written one writes.
349pub fn write_list<'a, T: FormScalar + 'a>(values: impl IntoIterator<Item = &'a T>) -> String {
350 encode_list(&values.into_iter().map(T::to_form).collect::<Vec<_>>())
351}
352
353/// Read the values a multiple choice posted, none when it posted nothing: what a record form's
354/// parse reads a many-to-many field with.
355///
356/// The panel folds the values the choice posts under its key into the key's one value before any
357/// parse reads it.
358///
359/// # Errors
360///
361/// A value that is not a list, or one `T` refuses, under `key`.
362pub fn parse_list<T: FormScalar>(
363 key: &str,
364 values: &HashMap<String, String>,
365) -> std::result::Result<Vec<T>, FieldError> {
366 let Some(items) = decode_list(values.get(key).map_or("", String::as_str)) else {
367 return Err(FieldError::invalid(key, "The choices could not be read"));
368 };
369 items
370 .iter()
371 .map(|item| T::parse_form(item).map_err(|message| FieldError::invalid(key, message)))
372 .collect()
373}
374
375/// `items` as one form value.
376pub(crate) fn encode_list(items: &[String]) -> String {
377 serde_json::to_string(items).expect("a list of strings serializes")
378}
379
380/// The items one form value holds: none for a blank value, `None` for one that is not a list.
381pub(crate) fn decode_list(value: &str) -> Option<Vec<String>> {
382 if value.trim().is_empty() {
383 return Some(Vec::new());
384 }
385 serde_json::from_str(value).ok()
386}
387
388/// The form key of the many-to-many field `path`, a `#[has_many(via = ..)]` list of `R`'s
389/// records, holding their keys of type `K`: the derive calls it, so a field whose type is not
390/// `Vec<K>` of the related model's key, or whose source is not that model's, fails to compile.
391#[doc(hidden)]
392pub fn links_key<M, R, K>(
393 path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
394) -> String
395where
396 M: Model,
397 R: OptionSource,
398 R::Model: Model<PrimaryKey = K>,
399 K: FormScalar,
400{
401 crate::schema::form_key(path.into())
402}
403
404/// `includes` and the include loading the many-to-many field `path`, which
405/// [`RecordForm::hydrate`] reads.
406#[doc(hidden)]
407pub fn links_include<M, R>(
408 includes: Includes<M>,
409 path: impl Into<toasty::stmt::Path<M, toasty::stmt::List<R::Model>>>,
410) -> Includes<M>
411where
412 M: Model,
413 R: OptionSource,
414{
415 includes.with(path.into())
416}
417
418/// The keys of the records a many-to-many field holds, as its form key carries them, or `None`
419/// when the record was loaded without them.
420#[doc(hidden)]
421pub fn write_links<T>(records: &toasty::Deferred<Vec<T>>) -> Option<String>
422where
423 T: Model + toasty::stmt::IntoExpr<T>,
424{
425 (!records.is_unloaded()).then(|| {
426 encode_list(
427 &records
428 .get()
429 .iter()
430 .map(crate::toasty_compat::pk::pk_text)
431 .collect::<Vec<_>>(),
432 )
433 })
434}
435
436/// One record-form field and the form keys it binds.
437#[derive(Debug, Clone)]
438pub struct FormField<K> {
439 /// The field, as the form's field enum names it.
440 pub field: K,
441 /// The Rust field name, for build-time refusals.
442 pub name: &'static str,
443 /// The keys the field binds: one for a scalar, every key of an embedded
444 /// value (an enum's discriminant first). An error on the field renders
445 /// under the first.
446 pub keys: Vec<String>,
447 /// The keys with no blank answer, which an empty submission refuses: the
448 /// panel renders their controls required.
449 pub required: Vec<String>,
450}
451
452/// The typed value a resource's form submission parses into.
453///
454/// Derive it with [`tablo_core::RecordForm`](crate::RecordForm); a
455/// hand-written impl is what the derive expands to.
456pub trait RecordForm: Sized + Send + 'static {
457 /// The model the form writes.
458 type Model: Model + toasty::stmt::IntoExpr<Self::Model> + Send + Sync + 'static;
459
460 /// One variant per form field: the key [`Posted`] uses. Its bound form keys
461 /// are [`Self::fields`]'.
462 type Field: Copy + Eq + Hash + Debug + Send + Sync + 'static;
463
464 /// Every field, in declaration order, with the keys it binds through `resolver`'s app schema.
465 fn fields(resolver: &FieldResolver) -> Vec<FormField<Self::Field>>;
466
467 /// The control the form renders `field` with when its [`form`](crate::ResourceDef::form)
468 /// does not place it: what `controls()` hands over for the field, as a one-control schema.
469 ///
470 /// A resource's form renders every field it does not place after the ones it does, in
471 /// declaration order, so a resource that declares no form renders one control per field.
472 fn control(field: Self::Field) -> Schema<Self>;
473
474 /// The form's default table: one column per field a column can show, in declaration order.
475 /// A [`ResourceDef`](crate::ResourceDef) without a [`table`](crate::ResourceDef::table) lists
476 /// it.
477 ///
478 /// The derive lists each text field in a sortable column, searchable over a `String` or
479 /// `Option<String>`, an options field by its option's label, and a toggle as yes or no (see
480 /// [`RecordForm`](derive@crate::RecordForm)). The default here lists none, so a resource
481 /// whose form lists nothing, such as one naming [`NoForm`], declares its own.
482 fn table() -> Table<Self::Model> {
483 Table::new(())
484 }
485
486 /// The form's default detail page: one column per field a column can show, in declaration
487 /// order. A [`ResourceDef`](crate::ResourceDef) without a [`view`](crate::ResourceDef::view)
488 /// shows it.
489 ///
490 /// The derive shows each field [`table`](Self::table) lists, a relationship as the key it
491 /// holds, a file field as a link, and an embedded value leaf by leaf (see
492 /// [`RecordForm`](derive@crate::RecordForm)). A relation's record shows through a
493 /// [`RelationColumn`](crate::RelationColumn), in a declared
494 /// [`view`](crate::ResourceDef::view). The default here shows none, which turns the detail page
495 /// off for a resource naming [`NoForm`].
496 fn detail() -> Detail<Self::Model> {
497 Detail::empty()
498 }
499
500 /// The relations [`hydrate`](Self::hydrate) reads off the record: each many-to-many field's,
501 /// which the edit page and the edit's write load the record with.
502 fn includes() -> Includes<Self::Model> {
503 Includes::new()
504 }
505
506 /// The stored record as the form spells it.
507 fn hydrate(cx: &Cx, record: &Self::Model) -> HashMap<String, String>;
508
509 /// The records each many-to-many field holds, as their keys' form spelling: what
510 /// [`write_create`](crate::resource::write_create) links a new record to, and what
511 /// [`write_update`](crate::resource::write_update) leaves a posted field's record linked to.
512 fn links(&self) -> Vec<(Self::Field, Vec<String>)> {
513 Vec::new()
514 }
515
516 /// Parse a completed submission: the form's only presence and type check.
517 ///
518 /// # Errors
519 ///
520 /// Every key that failed, each once, under one of [`Self::fields`]' keys.
521 fn parse(
522 cx: &Cx,
523 values: &HashMap<String, String>,
524 ) -> std::result::Result<Self, Vec<FieldError>>;
525
526 /// The create builder with every field set.
527 fn into_create(self) -> <Self::Model as Model>::Create;
528
529 /// The instance update builder with one assignment per named field, or
530 /// `None` when no field is named.
531 ///
532 /// `None` is what keeps a submission that names no field from building an
533 /// empty update, which toasty refuses with an assertion.
534 fn into_update<'a>(
535 self,
536 record: &'a mut Self::Model,
537 named: &HashSet<Self::Field>,
538 ) -> Option<<Self::Model as Model>::Update<'a>>;
539
540 /// Execute a builder [`Self::into_update`] returned.
541 ///
542 /// Toasty's instance update builder implements no trait that carries
543 /// `exec`, so the generic write reaches it through the form.
544 fn exec_update<'a>(
545 update: <Self::Model as Model>::Update<'a>,
546 ex: &'a mut dyn Executor,
547 ) -> impl Future<Output = toasty::Result<()>> + Send + 'a;
548
549 /// Whether the resource naming this form has create and edit pages.
550 ///
551 /// [`Panel::resource`](crate::Panel::resource) registers the create, edit,
552 /// and options routes only when this holds. [`NoForm`] sets it to `false`.
553 const HAS_FORM: bool = true;
554}
555
556/// The record form of a resource with no create or edit page.
557///
558/// A list-only resource names it as [`Resource::Form`](crate::Resource::Form):
559/// `type Form = NoForm<Self::Model>;`. [`fields`](RecordForm::fields) and
560/// [`hydrate`](RecordForm::hydrate) are empty and
561/// [`into_update`](RecordForm::into_update) answers `None`. No route parses or
562/// writes it, so [`parse`](RecordForm::parse) and
563/// [`into_create`](RecordForm::into_create) panic naming the model, and the
564/// future [`exec_update`](RecordForm::exec_update) returns panics when polled.
565pub struct NoForm<M>(std::marker::PhantomData<fn() -> M>);
566
567impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> NoForm<M> {
568 fn unreachable() -> ! {
569 panic!("`NoForm<{}>` has no form", std::any::type_name::<M>())
570 }
571}
572
573impl<M: Model + toasty::stmt::IntoExpr<M> + Send + Sync + 'static> RecordForm for NoForm<M> {
574 type Model = M;
575 type Field = std::convert::Infallible;
576
577 const HAS_FORM: bool = false;
578
579 fn fields(_resolver: &FieldResolver) -> Vec<FormField<Self::Field>> {
580 Vec::new()
581 }
582
583 fn control(field: Self::Field) -> Schema<Self> {
584 match field {}
585 }
586
587 fn hydrate(_cx: &Cx, _record: &M) -> HashMap<String, String> {
588 HashMap::new()
589 }
590
591 fn parse(
592 _cx: &Cx,
593 _values: &HashMap<String, String>,
594 ) -> std::result::Result<Self, Vec<FieldError>> {
595 Self::unreachable()
596 }
597
598 fn into_create(self) -> M::Create {
599 Self::unreachable()
600 }
601
602 fn into_update<'a>(
603 self,
604 _record: &'a mut M,
605 _named: &HashSet<Self::Field>,
606 ) -> Option<M::Update<'a>> {
607 None
608 }
609
610 fn exec_update<'a>(
611 update: M::Update<'a>,
612 ex: &'a mut dyn Executor,
613 ) -> impl Future<Output = toasty::Result<()>> + Send + 'a {
614 // `into_update` answers `None`, so no builder reaches here. The
615 // builder is not known to be `Send`, so the future must not hold it.
616 drop((update, ex));
617 async { Self::unreachable() }
618 }
619}
620
621/// An edit submission: the parsed form, and the fields the submission named.
622///
623/// Derefs to the form, so a record fn reads `posted.author_id` — the posted
624/// value, or the stored one when the submission did not name the field.
625pub struct Posted<F: RecordForm> {
626 form: F,
627 named: HashSet<F::Field>,
628}
629
630impl<F: RecordForm> Posted<F> {
631 /// A posted form that names `named`.
632 pub fn new(form: F, named: impl IntoIterator<Item = F::Field>) -> Self {
633 Self {
634 form,
635 named: named.into_iter().collect(),
636 }
637 }
638
639 /// Whether the submission named `field`.
640 pub fn named(&self, field: F::Field) -> bool {
641 self.named.contains(&field)
642 }
643
644 /// The update builder assigning every named field, or `None` when the
645 /// submission named none. See [`RecordForm::into_update`].
646 pub fn into_update(self, record: &mut F::Model) -> Option<<F::Model as Model>::Update<'_>> {
647 self.form.into_update(record, &self.named)
648 }
649}
650
651impl<F: RecordForm> std::ops::Deref for Posted<F> {
652 type Target = F;
653
654 fn deref(&self) -> &F {
655 &self.form
656 }
657}
658
659/// A refused submission.
660///
661/// [`Resource::validate_record`](crate::Resource::validate_record) keys it by the record form's
662/// field enum (`FieldErrors<UserFormField>`), so an error always names a field the form renders;
663/// one on an embedded value renders under its first control. The panel keys every error by the
664/// form key it renders under (`FieldErrors`, keyed by `String`), and [`Source::form`] reads them
665/// back when a schema renders.
666///
667/// [`Source::form`]: crate::Source::form
668#[derive(Debug)]
669pub struct FieldErrors<K = String> {
670 errors: Vec<FieldError<K>>,
671}
672
673impl<K> Default for FieldErrors<K> {
674 fn default() -> Self {
675 Self { errors: Vec::new() }
676 }
677}
678
679impl<K> FieldErrors<K> {
680 /// No errors.
681 pub fn new() -> Self {
682 Self::default()
683 }
684
685 /// Refuse `key` with `message`.
686 pub fn add(&mut self, key: impl Into<K>, message: impl Into<String>) {
687 self.errors.push(FieldError::invalid(key, message));
688 }
689
690 /// Refuse `error`'s key with `error`.
691 pub fn push(&mut self, error: FieldError<K>) {
692 self.errors.push(error);
693 }
694
695 /// Whether nothing was refused.
696 pub fn is_empty(&self) -> bool {
697 self.errors.is_empty()
698 }
699
700 /// Every error, in the order it was added.
701 pub fn iter(&self) -> impl Iterator<Item = &FieldError<K>> {
702 self.errors.iter()
703 }
704
705 /// Append `other`'s errors after these.
706 pub fn extend(&mut self, other: Self) {
707 self.errors.extend(other.errors);
708 }
709}
710
711impl<K> IntoIterator for FieldErrors<K> {
712 type Item = FieldError<K>;
713 type IntoIter = std::vec::IntoIter<FieldError<K>>;
714
715 fn into_iter(self) -> Self::IntoIter {
716 self.errors.into_iter()
717 }
718}
719
720impl FieldErrors {
721 /// Whether any error renders under `key`.
722 pub fn contains_key(&self, key: &str) -> bool {
723 self.errors.iter().any(|error| error.key == key)
724 }
725
726 /// The error `key` renders: the first one added.
727 pub fn first(&self, key: &str) -> Option<&FieldError> {
728 self.errors.iter().find(|error| error.key == key)
729 }
730
731 /// Take `other`'s errors, dropping this collection's own errors under every
732 /// key `other` names.
733 ///
734 /// A source that owns a key answers for it: a rejected upload replaces the
735 /// error the emptied control would otherwise report.
736 pub(crate) fn replace(&mut self, other: Self) {
737 let owned: HashSet<&str> = other
738 .errors
739 .iter()
740 .map(|error| error.key.as_str())
741 .collect();
742 self.errors
743 .retain(|kept| !owned.contains(kept.key.as_str()));
744 self.errors.extend(other.errors);
745 }
746}
747
748#[cfg(test)]
749mod tests;