slack_messaging/composition_objects/
option.rs

1use crate::composition_objects::{
2    Plain, Text, TextExt,
3    types::{UrlAvailable, UrlUnavailable},
4};
5use crate::validators::*;
6
7use serde::Serialize;
8use slack_messaging_derive::Builder;
9use std::marker::PhantomData;
10
11/// [Option object](https://docs.slack.dev/reference/block-kit/composition-objects/option-object)
12/// representation.
13///
14/// This is a generic struct that can represent an option object with different text object types.
15///
16/// # Type Parameters
17///
18/// * `T`: The type of text object used for the `text` and `description` fields. Defaults to
19///   `Text<Plain>`. Must implement the [`TextExt`] trait.
20/// * `P`: A phantom type used to control the availability of the `url` field. Defaults to
21///   [`UrlUnavailable`]. Use [`UrlAvailable`] to include the `url` field.
22///
23/// # Fields and Validations
24///
25/// For more details, see the [official
26/// documentation](https://docs.slack.dev/reference/block-kit/composition-objects/option-object).
27///
28/// | Field | Type | Required | Validation |
29/// |-------|------|----------|------------|
30/// | text | type parameter `T` bounds [TextExt] | Yes | Max length 75 characters |
31/// | value | String | Yes | Max length 150 characters |
32/// | description | type parameter `T` bounds [TextExt] | No | Max length 75 characters |
33/// | url | String | No (only if type parameter `P` is [UrlAvailable]) | Max length 3000 characters |
34///
35/// # Example
36///
37/// ```
38/// use slack_messaging::plain_text;
39/// use slack_messaging::composition_objects::{Opt, Plain, Text, types::UrlAvailable};
40/// # use std::error::Error;
41///
42/// # fn try_main() -> Result<(), Box<dyn Error>> {
43/// let option: Opt = Opt::builder()
44///     .text(plain_text!("Maru")?)
45///     .value("maru")
46///     .build()?;
47///
48/// let expected = serde_json::json!({
49///     "text": {
50///         "type": "plain_text",
51///         "text": "Maru"
52///     },
53///     "value": "maru"
54/// });
55///
56/// let json = serde_json::to_value(option).unwrap();
57///
58/// assert_eq!(json, expected);
59///
60/// // Using UrlAvailable to include the url field
61/// let option_with_url = Opt::<Text<Plain>, UrlAvailable>::builder()
62///    .text(plain_text!("Maru")?)
63///    .value("maru")
64///    .url("https://example.com/maru")
65///    .build()?;
66///
67/// let expected_with_url = serde_json::json!({
68///    "text": {
69///        "type": "plain_text",
70///        "text": "Maru"
71///    },
72///    "value": "maru",
73///    "url": "https://example.com/maru"
74/// });
75///
76/// let json_with_url = serde_json::to_value(option_with_url).unwrap();
77///
78/// assert_eq!(json_with_url, expected_with_url);
79///
80/// // If your object has any validation errors, the build method returns Result::Err
81/// let option = Opt::<Text<Plain>>::builder()
82///     .text(plain_text!("Maru")?)
83///     .build();
84///
85/// assert!(option.is_err());
86/// #     Ok(())
87/// # }
88/// # fn main() {
89/// #     try_main().unwrap()
90/// # }
91/// ```
92#[derive(Debug, Clone, Serialize, PartialEq, Builder)]
93#[serde(bound(serialize = "T: Serialize"))]
94pub struct Opt<T = Text<Plain>, P = UrlUnavailable>
95where
96    T: TextExt,
97{
98    #[serde(skip)]
99    #[builder(phantom = "P")]
100    pub(crate) phantom: PhantomData<P>,
101
102    #[builder(validate("required", "text_object::max_75"))]
103    pub(crate) text: Option<T>,
104
105    #[builder(validate("required", "text::max_150"))]
106    pub(crate) value: Option<String>,
107
108    #[serde(skip_serializing_if = "Option::is_none")]
109    #[builder(validate("text_object::max_75"))]
110    pub(crate) description: Option<T>,
111
112    #[serde(skip_serializing_if = "Option::is_none")]
113    #[builder(no_accessors, validate("text::max_3000"))]
114    pub(crate) url: Option<String>,
115}
116
117impl<T: TextExt> OptBuilder<T, UrlAvailable> {
118    /// get url field value.
119    pub fn get_url(&self) -> Option<&String> {
120        self.url.inner_ref()
121    }
122
123    /// set url field value.
124    pub fn set_url(self, value: Option<impl Into<String>>) -> Self {
125        Self {
126            url: Self::new_url(value.map(|v| v.into())),
127            ..self
128        }
129    }
130
131    /// set url field value.
132    pub fn url(self, value: impl Into<String>) -> Self {
133        self.set_url(Some(value))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::composition_objects::test_helpers::*;
141    use crate::errors::*;
142
143    #[test]
144    fn it_implements_builder() {
145        let expected = Opt {
146            phantom: PhantomData::<UrlUnavailable>,
147            text: Some(plain_text("foo")),
148            value: Some("bar".into()),
149            description: Some(plain_text("baz")),
150            url: None,
151        };
152
153        let val = Opt::builder()
154            .set_text(Some(plain_text("foo")))
155            .set_value(Some("bar"))
156            .set_description(Some(plain_text("baz")))
157            .build()
158            .unwrap();
159
160        assert_eq!(val, expected);
161
162        let val = Opt::builder()
163            .text(plain_text("foo"))
164            .value("bar")
165            .description(plain_text("baz"))
166            .build()
167            .unwrap();
168
169        assert_eq!(val, expected);
170    }
171
172    #[test]
173    fn url_field_is_available_if_the_valid_type_is_used() {
174        let expected = Opt::<_, UrlAvailable> {
175            phantom: PhantomData,
176            text: Some(plain_text("foo")),
177            value: Some("bar".into()),
178            description: Some(plain_text("baz")),
179            url: Some("foobarbaz".into()),
180        };
181
182        let val = Opt::<_, UrlAvailable>::builder()
183            .set_text(Some(plain_text("foo")))
184            .set_value(Some("bar"))
185            .set_description(Some(plain_text("baz")))
186            .set_url(Some("foobarbaz"))
187            .build()
188            .unwrap();
189
190        assert_eq!(val, expected);
191
192        let val = Opt::<_, UrlAvailable>::builder()
193            .text(plain_text("foo"))
194            .value("bar")
195            .description(plain_text("baz"))
196            .url("foobarbaz")
197            .build()
198            .unwrap();
199
200        assert_eq!(val, expected);
201    }
202
203    #[test]
204    fn it_requires_text_field() {
205        let err = Opt::<Text<Plain>>::builder()
206            .value("bar")
207            .build()
208            .unwrap_err();
209        assert_eq!(err.object(), "Opt");
210
211        let errors = err.field("text");
212        assert!(errors.includes(ValidationErrorKind::Required));
213    }
214
215    #[test]
216    fn it_requires_text_field_less_than_75_characters_long() {
217        let err = Opt::<Text<Plain>>::builder()
218            .text(plain_text("a".repeat(76)))
219            .value("bar")
220            .build()
221            .unwrap_err();
222        assert_eq!(err.object(), "Opt");
223
224        let errors = err.field("text");
225        assert!(errors.includes(ValidationErrorKind::MaxTextLength(75)));
226    }
227
228    #[test]
229    fn it_requires_value_field() {
230        let err = Opt::<Text<Plain>>::builder()
231            .text(plain_text("foo"))
232            .build()
233            .unwrap_err();
234        assert_eq!(err.object(), "Opt");
235
236        let errors = err.field("value");
237        assert!(errors.includes(ValidationErrorKind::Required));
238    }
239
240    #[test]
241    fn it_requires_value_field_less_than_150_characters_long() {
242        let err = Opt::<Text<Plain>>::builder()
243            .text(plain_text("foo"))
244            .value("a".repeat(151))
245            .build()
246            .unwrap_err();
247        assert_eq!(err.object(), "Opt");
248
249        let errors = err.field("value");
250        assert!(errors.includes(ValidationErrorKind::MaxTextLength(150)));
251    }
252
253    #[test]
254    fn it_requires_description_field_less_than_75_characters_long() {
255        let err = Opt::<Text<Plain>>::builder()
256            .text(plain_text("foo"))
257            .value("bar")
258            .description(plain_text("a".repeat(76)))
259            .build()
260            .unwrap_err();
261        assert_eq!(err.object(), "Opt");
262
263        let errors = err.field("description");
264        assert!(errors.includes(ValidationErrorKind::MaxTextLength(75)));
265    }
266
267    #[test]
268    fn it_requires_url_field_less_than_3000_characters_long() {
269        let err = Opt::<Text<Plain>, UrlAvailable>::builder()
270            .text(plain_text("foo"))
271            .value("bar")
272            .description(plain_text("baz"))
273            .url("a".repeat(3001))
274            .build()
275            .unwrap_err();
276        assert_eq!(err.object(), "Opt");
277
278        let errors = err.field("url");
279        assert!(errors.includes(ValidationErrorKind::MaxTextLength(3000)));
280    }
281}