Skip to main content

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