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 stored values, each with its label.
5///
6/// Derive it on a unit-variant enum with
7/// [`#[derive(Options)]`](crate::Options): each variant stores its
8/// `snake_case` name and reads as its name in sentence case, and
9/// `#[option(value = "..", label = "..")]` overrides either.
10///
11/// ```rust
12/// # #[derive(Debug, Clone, toasty::Model)]
13/// # struct Post { #[key] #[auto] id: uuid::Uuid, status: String }
14/// # use tablo_core::{Field, SelectFilter, TextColumn, lens, Options};
15/// #[derive(Debug, Clone, Copy, PartialEq, Eq, tablo_core::Options)]
16/// enum Status {
17///     Draft,
18///     #[option(label = "Live")]
19///     Published,
20/// }
21///
22/// Field::choice(Post::fields().status()).options(Status::options());
23/// SelectFilter::new(Post::fields().status(), Status::options());
24/// TextColumn::new(lens!(Post.status)).format(|status| Status::label_of(status));
25/// ```
26///
27/// The derive also gives the enum `value()`, `label()` and `from_value()`, so
28/// code that writes the column names a variant rather than a string.
29pub trait Options {
30    /// Every option as `(value, label)`, in declaration order.
31    fn options() -> Vec<(String, String)>;
32
33    /// The label of a stored `value`, or `value` itself when no option
34    /// stores it: a row written before an option was renamed still reads.
35    fn label_of(value: &str) -> String {
36        Self::options()
37            .into_iter()
38            .find(|(stored, _)| stored == value)
39            .map_or_else(|| value.to_string(), |(_, label)| label)
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::Options;
46    use crate::Column as _;
47
48    /// One list shared by the form, the filter and the column.
49    #[derive(Debug, Clone, Copy, PartialEq, Eq, crate::Options)]
50    enum Status {
51        Draft,
52        #[option(label = "Live")]
53        Published,
54        #[option(value = "arch", label = "Archive")]
55        Archived,
56    }
57
58    #[test]
59    fn an_options_enum_shares_one_value_label_list() {
60        assert_eq!(
61            Status::options(),
62            vec![
63                ("draft".to_string(), "Draft".to_string()),
64                ("published".to_string(), "Live".to_string()),
65                ("arch".to_string(), "Archive".to_string()),
66            ]
67        );
68        assert_eq!(Status::Draft.value(), "draft");
69        assert_eq!(Status::Published.label(), "Live");
70        assert_eq!(Status::from_value("draft"), Some(Status::Draft));
71        assert_eq!(Status::from_value("arch"), Some(Status::Archived));
72        assert_eq!(Status::from_value("gone"), None);
73        assert_eq!(Status::label_of("published"), "Live");
74        assert_eq!(
75            Status::label_of("renamed"),
76            "renamed",
77            "a value no option stores reads as itself"
78        );
79    }
80
81    #[tokio::test]
82    async fn the_shared_list_feeds_a_choice_a_select_filter_and_a_column() {
83        let options = Status::options();
84        let schema = crate::Schema::new(
85            crate::Field::choice(StatusField::fields().status()).options(options.clone()),
86        );
87        let mut valid = std::collections::HashMap::new();
88        valid.insert("status".to_string(), "draft".to_string());
89        let cx = topcoat::context::CxTestBuilder::new().build();
90        let errors = schema.validate_async(&cx, &valid).await;
91        assert!(errors.is_empty(), "a shared option validates: {errors:?}");
92        let mut bogus = std::collections::HashMap::new();
93        bogus.insert("status".to_string(), "gone".to_string());
94        assert!(
95            !schema.validate_async(&cx, &bogus).await.is_empty(),
96            "an unlisted value fails"
97        );
98        let filter = crate::SelectFilter::new(StatusField::fields().status(), options.clone());
99        assert_eq!(filter.options(), options.as_slice());
100        let column = crate::TextColumn::new(crate::lens!(StatusField.status))
101            .format(|status| Status::label_of(status));
102        let row = StatusField {
103            id: uuid::Uuid::nil(),
104            status: "published".to_string(),
105        };
106        assert_eq!(column.text(&row), "Live");
107    }
108
109    #[derive(Debug, Clone, toasty::Model)]
110    struct StatusField {
111        #[key]
112        #[auto]
113        id: uuid::Uuid,
114        status: String,
115    }
116}