slack_morphism/models/blocks/
dsl.rs1#![macro_use]
2
3#[macro_export]
23macro_rules! md {
24 ($e : expr) => {
25 $crate::blocks::SlackBlockMarkDownText::new($e.into()).into()
26 };
27
28 ($e : expr, $($es:expr),+ $(,)?) => {
29 $crate::md!(::std::format!($e,$($es),+))
30 };
31}
32
33#[macro_export]
47macro_rules! pt {
48 ($e : expr) => {
49 $crate::blocks::SlackBlockPlainText::new($e.into()).into()
50 };
51
52 ($e : expr, $($es:expr),+ $(,)?) => {
53 $crate::pt!(::std::format!($e,$($es),+))
54 };
55}
56
57#[macro_export]
58macro_rules! slack_block_item {
59 (optionally ($pred:expr => $item:expr)) => {{
60 if $pred {
61 $crate::slack_block_item! (some_into $item)
62 }
63 else {
64 None
65 }
66 }};
67
68 (some $item:expr) => {{
69 Some($item)
70 }};
71
72 (optionally_into ($pred:expr => $item:expr)) => {{
73 if $pred {
74 $crate::slack_block_item! (some_into $item)
75 }
76 else {
77 None
78 }
79 }};
80
81 (some_into $item:expr ) => {{
82 Some($item.into())
83 }};
84}
85
86#[macro_export]
127macro_rules! slack_blocks {
128 (@acc $v:ident;) => {};
129
130 (@acc $v:ident; some_into($item:expr) $(, $($rest:tt)*)?) => {
131 $v.push(::core::convert::Into::into($item));
132 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
133 };
134
135 (@acc $v:ident; some($item:expr) $(, $($rest:tt)*)?) => {
136 $v.push($item);
137 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
138 };
139
140 (@acc $v:ident; optionally($pred:expr => $item:expr) $(, $($rest:tt)*)?) => {
141 if $pred { $v.push(::core::convert::Into::into($item)); }
142 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
143 };
144
145 (@acc $v:ident; optionally_into($pred:expr => $item:expr) $(, $($rest:tt)*)?) => {
146 if $pred { $v.push(::core::convert::Into::into($item)); }
147 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
148 };
149
150 (@acc $v:ident; ..$iter:expr $(, $($rest:tt)*)?) => {
151 for __slack_item in $iter { $v.push(::core::convert::Into::into(__slack_item)); }
152 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
153 };
154
155 (@acc $v:ident; $kw:ident($pred:expr => $item:expr) $(, $($rest:tt)*)?) => {
156 ::core::compile_error!(::core::concat!("slack_blocks!: unknown keyword `", ::core::stringify!($kw),
157 "`; expected `optionally(pred => item)` or `optionally_into(pred => item)`"));
158 };
159
160 (@acc $v:ident; $item:expr $(, $($rest:tt)*)?) => {
161 $v.push(::core::convert::Into::into($item));
162 $crate::slack_blocks!(@acc $v; $($($rest)*)?);
163 };
164
165 (@acc $v:ident; $($t:tt)+) => {
166 ::core::compile_error!(::core::concat!("slack_blocks!: expected a comma-separated list of items, found `",
167 ::core::stringify!($($t)+), "`"));
168 };
169
170 () => { ::std::vec::Vec::new() };
171
172 ($($t:tt)*) => {{
173 let mut __slack_blocks = ::std::vec::Vec::new();
174 { $crate::slack_blocks!(@acc __slack_blocks; $($t)*); }
175 __slack_blocks
176 }};
177}
178
179#[cfg(test)]
180mod tests {
181 use crate::blocks::{
182 SlackActionBlockElement, SlackActionsBlock, SlackBlock, SlackBlockButtonElement,
183 SlackBlockMarkDownText, SlackContextBlock, SlackContextBlockElement, SlackDividerBlock,
184 SlackHeaderBlock,
185 };
186
187 #[test]
188 fn legacy_mixed_forms_build_vec_slack_block() {
189 let blocks: Vec<SlackBlock> = crate::slack_blocks![
190 some_into(SlackHeaderBlock::new(crate::pt!("Title"))),
191 some_into(SlackDividerBlock::new()),
192 optionally_into(true => SlackDividerBlock::new()),
193 optionally_into(false => SlackDividerBlock::new()),
194 ];
195 assert_eq!(blocks.len(), 3);
196 }
197
198 #[test]
199 fn bare_items_include_method_chain_and_nested_list() {
200 let blocks: Vec<SlackBlock> = crate::slack_blocks![
201 SlackHeaderBlock::new(crate::pt!("Title")),
202 SlackContextBlock::new(crate::slack_blocks![some(crate::md!("x"))]),
203 ];
204 assert_eq!(blocks.len(), 2);
205 let json = serde_json::to_value(&blocks).unwrap();
206 assert_eq!(json[1]["elements"][0]["text"], "x");
207 }
208
209 #[test]
210 fn empty_list_is_empty() {
211 let blocks: Vec<SlackBlock> = crate::slack_blocks![];
212 assert!(blocks.is_empty());
213 }
214
215 #[test]
216 fn trailing_comma_allowed_for_bare_and_legacy_items() {
217 let bare: Vec<SlackBlock> = crate::slack_blocks![SlackDividerBlock::new(),];
218 assert_eq!(bare.len(), 1);
219
220 let legacy: Vec<SlackBlock> = crate::slack_blocks![some_into(SlackDividerBlock::new()),];
221 assert_eq!(legacy.len(), 1);
222 }
223
224 #[test]
225 fn mixed_legacy_and_bare_items_in_one_list() {
226 let blocks: Vec<SlackBlock> = crate::slack_blocks![
227 some_into(SlackDividerBlock::new()),
228 SlackDividerBlock::new(),
229 optionally(true => SlackBlock::Divider(SlackDividerBlock::new())),
230 ];
231 assert_eq!(blocks.len(), 3);
232 }
233
234 #[test]
235 fn spread_from_iterator_map_converts_each_item() {
236 let ids = ["a", "b", "c"];
237 let blocks: Vec<SlackBlock> = crate::slack_blocks![
238 ..ids
239 .iter()
240 .map(|id| SlackDividerBlock::new().with_block_id(id.to_string().into())),
241 ];
242 assert_eq!(blocks.len(), 3);
243 }
244
245 #[test]
246 fn spread_of_vec_slack_block() {
247 let source: Vec<SlackBlock> = vec![
248 SlackDividerBlock::new().into(),
249 SlackDividerBlock::new().into(),
250 ];
251 let blocks: Vec<SlackBlock> = crate::slack_blocks![..source];
252 assert_eq!(blocks.len(), 2);
253 }
254
255 #[test]
256 fn spread_of_option_yields_zero_or_one_items() {
257 let some_block: Option<SlackBlock> = Some(SlackDividerBlock::new().into());
258 let none_block: Option<SlackBlock> = None;
259
260 let blocks: Vec<SlackBlock> = crate::slack_blocks![..some_block, ..none_block];
261 assert_eq!(blocks.len(), 1);
262 }
263
264 #[test]
265 fn spread_only_list_is_typed_by_the_let_binding() {
266 let source: Vec<SlackBlock> = vec![SlackDividerBlock::new().into()];
267 let blocks: Vec<SlackBlock> = crate::slack_blocks![..source];
268 assert_eq!(blocks.len(), 1);
269 }
270
271 #[test]
272 fn optionally_predicate_is_lazy_and_short_circuits_item_evaluation() {
273 let mut evaluations = 0;
274 let items: Vec<SlackBlock> = crate::slack_blocks![optionally(false => {
275 evaluations += 1;
276 SlackBlock::Divider(SlackDividerBlock::new())
277 })];
278 assert!(items.is_empty());
279 assert_eq!(evaluations, 0);
280 }
281
282 #[test]
283 fn context_block_elements_from_some_and_bare_markdown() {
284 let elements: Vec<SlackContextBlockElement> = crate::slack_blocks![
285 some(crate::md!("a")),
286 SlackBlockMarkDownText::new("b".into()),
287 ];
288 assert_eq!(elements.len(), 2);
289 }
290
291 #[test]
292 fn action_block_elements_from_bare_button() {
293 let elements: Vec<SlackActionBlockElement> =
294 crate::slack_blocks![SlackBlockButtonElement::new(
295 "btn-1".into(),
296 crate::pt!("Click")
297 ),];
298 assert_eq!(elements.len(), 1);
299 let action_block = SlackActionsBlock::new(elements);
300 let json = serde_json::to_value(&action_block).unwrap();
301 assert_eq!(json["elements"][0]["action_id"], "btn-1");
302 }
303
304 #[test]
305 fn optionally_converts_its_item_like_a_bare_item() {
306 let blocks: Vec<SlackBlock> = crate::slack_blocks![
307 optionally(true => SlackDividerBlock::new()),
308 optionally(false => SlackDividerBlock::new()),
309 ];
310 assert_eq!(blocks.len(), 1);
311
312 let item: Option<SlackBlock> =
313 crate::slack_block_item!(optionally (true => SlackDividerBlock::new()));
314 assert!(item.is_some());
315 }
316
317 #[test]
318 fn slack_block_item_still_works() {
319 let some_item =
320 crate::slack_block_item!(some SlackBlock::Divider(SlackDividerBlock::new()));
321 assert!(some_item.is_some());
322
323 let none_item: Option<SlackBlock> = crate::slack_block_item!(optionally (false => SlackBlock::Divider(SlackDividerBlock::new())));
324 assert!(none_item.is_none());
325 }
326
327 #[test]
328 fn md_and_pt_format_args_arms_produce_expected_text() {
329 let markdown: SlackBlockMarkDownText = crate::md!("hi {}", "there");
330 assert_eq!(markdown.text, "hi there");
331
332 let plain: crate::blocks::SlackBlockPlainText = crate::pt!("hi {}", "there");
333 assert_eq!(plain.text, "hi there");
334 }
335
336 #[test]
337 fn macros_are_crate_hygienic_without_prelude_glob_import() {
338 let blocks: Vec<SlackBlock> = crate::slack_blocks![
339 SlackDividerBlock::new(),
340 some(SlackBlock::Divider(SlackDividerBlock::new())),
341 ..[SlackBlock::Divider(SlackDividerBlock::new())],
342 ];
343 assert_eq!(blocks.len(), 3);
344
345 let markdown: SlackBlockMarkDownText = crate::md!("x");
346 assert_eq!(markdown.text, "x");
347
348 let plain: crate::blocks::SlackBlockPlainText = crate::pt!("x");
349 assert_eq!(plain.text, "x");
350 }
351}