tablo_core/schema/options.rs
1//! Typed options: one list of `(value, label)` pairs a choice field, a select
2//! filter and a column share.
3
4use crate::Text;
5
6/// A closed set of values, each with its label.
7///
8/// Derive it on a unit-variant enum with
9/// [`#[derive(Options)]`](crate::Options): each variant's value is its
10/// `snake_case` name and its label the message `option-{enum}.{variant}`, which reads the name
11/// in sentence case when no catalog holds it, and `#[option(value = "..", label = "..")]`
12/// overrides either, the label as plain text.
13///
14/// The derive also makes the enum a [`FormScalar`](crate::FormScalar) that a
15/// form posts as its value and a column reads as its label, so a model field
16/// of the enum's type, derived `toasty::Embed`, binds without a string in
17/// between:
18///
19/// ```rust
20/// # use tablo_core::{Field, SelectFilter, Table, TextColumn, lens, Options};
21/// #[derive(Debug, Clone, Copy, PartialEq, Eq, toasty::Embed, tablo_core::Options)]
22/// enum Status {
23/// Draft,
24/// #[option(label = "Live")]
25/// Published,
26/// }
27///
28/// #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
29/// struct Post {
30/// #[key]
31/// #[auto]
32/// id: uuid::Uuid,
33/// status: Status,
34/// }
35///
36/// Field::choice(lens!(Post.status)).options(Status::options());
37/// SelectFilter::of(lens!(Post.status));
38/// Table::new(TextColumn::new(lens!(Post.status))).group_by(lens!(Post.status));
39/// Post::filter(Post::fields().status().eq(Status::Published));
40/// ```
41///
42/// Toasty stores the variant under its own discriminant, `snake_case` by
43/// default; the option's value is only the form's spelling. A `String` field
44/// takes the same list, storing the value itself:
45/// `SelectFilter::new(lens!(Post.kind), Kind::options())`.
46///
47/// The derive also gives the enum `value()`, `label()` and `from_value()`.
48pub trait Options {
49 /// Every option as `(value, label)`, in declaration order.
50 fn options() -> Vec<(String, Text)>;
51
52 /// The label of a `value`, or `value` itself when no option has it: a
53 /// `String` row written before an option was renamed still reads.
54 fn label_of(value: &str) -> Text {
55 Self::options()
56 .into_iter()
57 .find(|(stored, _)| stored == value)
58 .map_or_else(|| Text::from(value), |(_, label)| label)
59 }
60}
61
62/// An optional field offers its type's options; an empty submission reads as `None`.
63impl<T: Options> Options for Option<T> {
64 fn options() -> Vec<(String, Text)> {
65 T::options()
66 }
67}
68
69#[cfg(test)]
70mod tests;