Skip to main content

tablo_core/values/
scalar.rs

1//! [`FormScalar`]: a type one form key reads and writes, and the blanket impls over
2//! [`TypedValue`] and `Option`.
3
4use jiff::tz::TimeZone;
5
6use super::TypedValue;
7
8/// A type one form key reads and writes: `String`, every [`TypedValue`] type,
9/// every enum deriving [`Options`](crate::Options), and an `Option` of any of them.
10///
11/// The value the parse sees is trimmed and non-empty; an empty submission is the
12/// record-form field's **blank answer** instead, and a field with none is required.
13///
14/// A text field binds a path of any `FormScalar` type
15/// ([`Field::text`](crate::Field::text)), and the form derives read and write
16/// every field that is not `#[form(embed)]` through it.
17#[diagnostic::on_unimplemented(
18    message = "`{Self}` is not a form scalar",
19    label = "a form field of this type has no text spelling",
20    note = "a form scalar is `String`, a `TypedValue` type, an `Options` enum, or an `Option` of \
21            one; implement `TypedValue` for an app type, derive `Options` for a unit enum, or \
22            mark an `EmbeddedForm` value `#[form(embed)]`"
23)]
24pub trait FormScalar: Sized {
25    /// The `type` attribute of the text control that edits it.
26    const INPUT_TYPE: &'static str = "text";
27
28    /// The control's `step` attribute, when its input type takes one.
29    const STEP: Option<&'static str> = None;
30
31    /// Whether the database orders the stored column as the values order: a [`TypedValue`]'s
32    /// [`SORTABLE`](TypedValue::SORTABLE), and `true` for any other type.
33    const SORTABLE: bool = true;
34
35    /// The control's `value` for a stored value's form spelling, shown in the request's time zone
36    /// `zone`: a [`TypedValue`]'s [`input_value`](TypedValue::input_value), and the spelling
37    /// itself for any other type.
38    fn input_value(stored: &str, zone: &TimeZone) -> String {
39        let _ = zone;
40        stored.to_string()
41    }
42
43    /// The form spelling of what the control posted, read in the request's time zone `zone`: a
44    /// [`TypedValue`]'s [`form_value`](TypedValue::form_value), and the posted text itself for
45    /// any other type.
46    fn form_value(posted: &str, zone: &TimeZone) -> String {
47        let _ = zone;
48        posted.to_string()
49    }
50
51    /// Parse a trimmed, non-empty submission, or return the error message.
52    fn parse_form(value: &str) -> std::result::Result<Self, crate::Text>;
53
54    /// The form spelling of a stored value: a [`TypedValue`]'s
55    /// [`form_spelling`](TypedValue::form_spelling).
56    fn to_form(&self) -> String;
57
58    /// What a column cell and a group header read: the form spelling, or an
59    /// `Options` enum's label.
60    fn to_label(&self) -> crate::Text {
61        self.to_form().into()
62    }
63
64    /// The color a badge shows the value in: an `Options` enum's `#[option(color = "…")]`, and
65    /// `None` for any other type.
66    fn color(&self) -> Option<crate::Color> {
67        None
68    }
69
70    /// The icon a column shows the value with: an `Options` enum's `#[option(icon = …)]`, and
71    /// `None` for any other type.
72    fn icon(&self) -> Option<topcoat::icon::IconData> {
73        None
74    }
75
76    /// How `self` orders against `other` for a text field's [`min`](crate::TextField::min) and
77    /// [`max`](crate::TextField::max): a [`TypedValue`]'s [`order`](TypedValue::order), and
78    /// `None` for any other type.
79    fn order(&self, _other: &Self) -> Option<std::cmp::Ordering> {
80        None
81    }
82
83    /// The type a text field's comparison with another field orders by: `Self`, and `T` for an
84    /// `Option<T>`, so an optional end date compares with a required start date.
85    #[doc(hidden)]
86    fn compared_as() -> &'static str {
87        std::any::type_name::<Self>()
88    }
89}
90
91impl FormScalar for String {
92    fn parse_form(value: &str) -> std::result::Result<Self, crate::Text> {
93        Ok(value.to_string())
94    }
95
96    fn to_form(&self) -> String {
97        self.clone()
98    }
99}
100
101impl<T: TypedValue> FormScalar for T {
102    const INPUT_TYPE: &'static str = T::INPUT_TYPE;
103    const STEP: Option<&'static str> = T::STEP;
104    const SORTABLE: bool = T::SORTABLE;
105
106    fn input_value(stored: &str, zone: &TimeZone) -> String {
107        T::input_value(stored, zone)
108    }
109
110    fn form_value(posted: &str, zone: &TimeZone) -> String {
111        T::form_value(posted, zone)
112    }
113
114    fn parse_form(value: &str) -> std::result::Result<Self, crate::Text> {
115        T::parse_input(value).ok_or_else(|| {
116            crate::text!(
117                "tablo-field-not-a",
118                value = value,
119                kind = crate::Text::message(T::NOUN)
120            )
121        })
122    }
123
124    fn to_form(&self) -> String {
125        self.form_spelling()
126    }
127
128    fn order(&self, other: &Self) -> Option<std::cmp::Ordering> {
129        TypedValue::order(self, other)
130    }
131}
132
133/// A form scalar whose `Option` is one too: every [`TypedValue`] type, and every enum deriving
134/// [`Options`](crate::Options), which implements it.
135#[doc(hidden)]
136pub trait NullableScalar: FormScalar {}
137
138impl<T: TypedValue> NullableScalar for T {}
139
140impl<T: NullableScalar> FormScalar for Option<T> {
141    const INPUT_TYPE: &'static str = T::INPUT_TYPE;
142    const STEP: Option<&'static str> = T::STEP;
143    const SORTABLE: bool = T::SORTABLE;
144
145    fn input_value(stored: &str, zone: &TimeZone) -> String {
146        T::input_value(stored, zone)
147    }
148
149    fn form_value(posted: &str, zone: &TimeZone) -> String {
150        T::form_value(posted, zone)
151    }
152
153    fn parse_form(value: &str) -> std::result::Result<Self, crate::Text> {
154        T::parse_form(value).map(Some)
155    }
156
157    fn to_form(&self) -> String {
158        self.as_ref().map(T::to_form).unwrap_or_default()
159    }
160
161    fn to_label(&self) -> crate::Text {
162        self.as_ref().map(T::to_label).unwrap_or_default()
163    }
164
165    fn color(&self) -> Option<crate::Color> {
166        self.as_ref().and_then(T::color)
167    }
168
169    fn icon(&self) -> Option<topcoat::icon::IconData> {
170        self.as_ref().and_then(T::icon)
171    }
172
173    fn order(&self, other: &Self) -> Option<std::cmp::Ordering> {
174        T::order(self.as_ref()?, other.as_ref()?)
175    }
176
177    fn compared_as() -> &'static str {
178        T::compared_as()
179    }
180}
181
182impl FormScalar for Option<String> {
183    fn parse_form(value: &str) -> std::result::Result<Self, crate::Text> {
184        Ok(Some(value.to_string()))
185    }
186
187    fn compared_as() -> &'static str {
188        String::compared_as()
189    }
190
191    fn to_form(&self) -> String {
192        self.clone().unwrap_or_default()
193    }
194}
195
196/// Compiles only for a form scalar: the form derives call it, spanned on a
197/// field's type, so a field of another type fails there.
198#[doc(hidden)]
199pub fn assert_form_scalar<T: FormScalar>() {}