google_chat_types/
lib.rs

1//! Google Chat Types
2//!
3//! ## About Google Chat Message
4//!
5//! type helper for construct Google Chat [message](https://developers.google.com/chat/api/guides/message-formats/basic)
6//! There two type of Google Chat message
7//! - Text Message
8//! - Card Message
9//!
10//! they are all represented as a json string.
11//!  
12//! Text Message represented like
13//! ```json
14//! {
15//!     "text":"some text"
16//! }  
17//!```
18//! Card Message represented like
19//!```json
20//!{
21//! "cards": [
22//!    {
23//!      "sections": [
24//!        {
25//!          "widgets": [
26//!            {
27//!              "image": { "imageUrl": "https://..." }
28//!            },
29//!            {
30//!              "buttons": [
31//!                {
32//!                  "textButton": {
33//!                    "text": "OPEN IN GOOGLE MAPS",
34//!                    "onClick": {
35//!                      "openLink": {
36//!                        "url": "https://..."
37//!                      }
38//!                    }
39//!                  }
40//!                }
41//!              ]
42//!            }
43//!          ]
44//!        }
45//!      ]
46//!    }
47//!  ]
48//!}
49//! ```
50//!
51//! the relationship between elements of cards should looks like below
52//!
53//! <img src="https://future-architect.github.io/images/20210913a/screenshot_card_message.png" width="900px"></img>
54//!
55//! ## How to use this crate
56//!
57//! you should construct Cards or Text struct,
58//! then serialize them to json string as a Google Chat API(for instance [incoming webhook](https://developers.google.com/chat/how-tos/webhooks)) http request body.
59
60use std::fmt::Display;
61
62use derive_builder::{Builder, UninitializedFieldError};
63use serde::Serialize;
64use thiserror::Error;
65
66#[doc = "The Text message type"]
67#[derive(Serialize, Clone, Builder)]
68#[serde(rename_all = "camelCase")]
69#[builder(build_fn(error = "ChatTypeBuildError"))]
70pub struct Text {
71    #[builder(setter(into))]
72    text: String,
73}
74
75#[doc = "The Card message type"]
76#[derive(Serialize, Clone, Builder)]
77#[serde(rename_all = "camelCase")]
78#[builder(build_fn(error = "ChatTypeBuildError"))]
79pub struct Cards {
80    cards: Vec<Card>,
81}
82
83/// the Card response.
84/// construct this by call default() method of this type
85#[derive(Serialize, Clone, Default, Builder)]
86#[serde(rename_all = "camelCase")]
87#[builder(build_fn(error = "ChatTypeBuildError"))]
88pub struct Card {
89    #[builder(setter(strip_option), default)]
90    #[serde(skip_serializing_if = "Option::is_none")]
91    header: Option<Header>,
92    #[builder(setter(strip_option), default)]
93    #[serde(skip_serializing_if = "Option::is_none")]
94    sections: Option<Vec<Section>>,
95}
96
97#[derive(Serialize, Clone, Default, Builder)]
98#[serde(rename_all = "camelCase")]
99#[builder(build_fn(error = "ChatTypeBuildError"))]
100pub struct Header {
101    #[builder(setter(into, strip_option), default)]
102    #[serde(skip_serializing_if = "Option::is_none")]
103    title: Option<String>,
104    #[builder(setter(into, strip_option), default)]
105    #[serde(skip_serializing_if = "Option::is_none")]
106    subtitle: Option<String>,
107    #[builder(setter(into, strip_option), default)]
108    #[serde(skip_serializing_if = "Option::is_none")]
109    image_url: Option<String>,
110    #[builder(setter(into, strip_option), default)]
111    #[serde(skip_serializing_if = "Option::is_none")]
112    image_style: Option<String>,
113}
114
115#[derive(Serialize, Clone, Default, Builder)]
116#[serde(rename_all = "camelCase")]
117#[builder(build_fn(error = "ChatTypeBuildError"))]
118pub struct Section {
119    #[builder(setter(into, strip_option), default)]
120    #[serde(skip_serializing_if = "Option::is_none")]
121    header: Option<String>,
122    #[builder(setter(strip_option), default)]
123    #[serde(skip_serializing_if = "Option::is_none")]
124    widgets: Option<Vec<Widget>>,
125}
126
127#[derive(Serialize, Clone, Default, Builder)]
128#[serde(rename_all = "camelCase")]
129#[builder(build_fn(error = "ChatTypeBuildError"))]
130pub struct Widget {
131    #[builder(setter(strip_option), default)]
132    #[serde(skip_serializing_if = "Option::is_none")]
133    text_paragraph: Option<TextParagraph>,
134    #[builder(setter(strip_option), default)]
135    #[serde(skip_serializing_if = "Option::is_none")]
136    key_value: Option<KeyValue>,
137    #[builder(setter(strip_option), default)]
138    #[serde(skip_serializing_if = "Option::is_none")]
139    image: Option<Image>,
140    #[builder(setter(strip_option), default)]
141    #[serde(skip_serializing_if = "Option::is_none")]
142    buttons: Option<Vec<Button>>,
143}
144
145#[derive(Serialize, Clone, Builder)]
146#[serde(rename_all = "camelCase")]
147#[builder(build_fn(error = "ChatTypeBuildError"))]
148pub struct TextParagraph {
149    #[builder(setter(into))]
150    text: String,
151}
152
153#[derive(Serialize, Clone, Default, Builder)]
154#[serde(rename_all = "camelCase")]
155#[builder(build_fn(error = "ChatTypeBuildError"))]
156pub struct KeyValue {
157    #[builder(setter(into, strip_option), default)]
158    #[serde(skip_serializing_if = "Option::is_none")]
159    top_label: Option<String>,
160    #[builder(setter(into, strip_option), default)]
161    #[serde(skip_serializing_if = "Option::is_none")]
162    content: Option<String>,
163    #[builder(setter(into, strip_option), default)]
164    #[serde(skip_serializing_if = "Option::is_none")]
165    icon: Option<String>,
166    #[builder(setter(into, strip_option), default)]
167    #[serde(skip_serializing_if = "Option::is_none")]
168    content_multiline: Option<String>,
169    #[builder(setter(into, strip_option), default)]
170    #[serde(skip_serializing_if = "Option::is_none")]
171    bottom_label: Option<String>,
172    #[builder(setter(strip_option), default)]
173    #[serde(skip_serializing_if = "Option::is_none")]
174    on_click: Option<OnClick>,
175    #[builder(setter(strip_option), default)]
176    #[serde(skip_serializing_if = "Option::is_none")]
177    button: Option<Button>,
178}
179
180impl KeyValue {
181    pub fn to_widget(&self) -> Widget {
182        Widget {
183            text_paragraph: None,
184            image: None,
185            buttons: None,
186            key_value: Some(self.to_owned()),
187        }
188    }
189}
190
191#[derive(Serialize, Clone, Default, Builder)]
192#[serde(rename_all = "camelCase")]
193#[builder(build_fn(error = "ChatTypeBuildError"))]
194pub struct Image {
195    #[builder(setter(into, strip_option), default)]
196    #[serde(skip_serializing_if = "Option::is_none")]
197    image_url: Option<String>,
198    #[builder(setter(strip_option), default)]
199    #[serde(skip_serializing_if = "Option::is_none")]
200    on_click: Option<OnClick>,
201}
202
203impl Image {
204    pub fn to_widget(&self) -> Widget {
205        Widget {
206            text_paragraph: None,
207            key_value: None,
208            image: Some(self.to_owned()),
209            buttons: None,
210        }
211    }
212}
213
214#[derive(Serialize, Clone, Default, Builder)]
215#[serde(rename_all = "camelCase")]
216#[builder(build_fn(error = "ChatTypeBuildError"))]
217pub struct Button {
218    #[builder(setter(strip_option), default)]
219    #[serde(skip_serializing_if = "Option::is_none")]
220    text_button: Option<TextButton>,
221    #[builder(setter(strip_option), default)]
222    #[serde(skip_serializing_if = "Option::is_none")]
223    image_button: Option<ImageButton>,
224}
225
226#[derive(Serialize, Clone, Builder)]
227#[serde(rename_all = "camelCase")]
228#[builder(build_fn(error = "ChatTypeBuildError"))]
229pub struct TextButton {
230    text: String,
231    on_click: OnClick,
232}
233
234#[derive(Serialize, Clone, Default, Builder)]
235#[serde(rename_all = "camelCase")]
236#[builder(build_fn(error = "ChatTypeBuildError"))]
237pub struct ImageButton {
238    #[builder(setter(into, strip_option), default)]
239    #[serde(skip_serializing_if = "Option::is_none")]
240    icon_url: Option<String>,
241    #[builder(setter(into, strip_option), default)]
242    #[serde(skip_serializing_if = "Option::is_none")]
243    icon: Option<String>,
244    #[builder(setter(strip_option), default)]
245    #[serde(skip_serializing_if = "Option::is_none")]
246    on_click: Option<OnClick>,
247}
248
249#[derive(Serialize, Clone, Builder)]
250#[serde(rename_all = "camelCase")]
251#[builder(build_fn(error = "ChatTypeBuildError"))]
252pub struct OnClick {
253    #[builder(private)]
254    open_link: OpenLink,
255}
256
257#[derive(Serialize, Clone, Builder)]
258#[serde(rename_all = "camelCase")]
259#[builder(build_fn(error = "ChatTypeBuildError"))]
260pub struct OpenLink {
261    #[builder(private)]
262    url: String,
263}
264
265#[derive(Debug, Error)]
266pub struct ChatTypeBuildError(String);
267
268impl Display for ChatTypeBuildError {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.write_fmt(format_args!("Error in build process {}", self.0))
271    }
272}
273
274impl From<UninitializedFieldError> for ChatTypeBuildError {
275    fn from(e: UninitializedFieldError) -> Self {
276        Self(e.to_string())
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use crate::*;
283    #[test]
284    fn build_cards() {
285        let text_paragraph = TextParagraphBuilder::default()
286            .text("some text")
287            .build()
288            .unwrap();
289        let widget = WidgetBuilder::default()
290            .text_paragraph(text_paragraph)
291            .build()
292            .unwrap();
293        let section = SectionBuilder::default()
294            .widgets(vec![widget])
295            .build()
296            .unwrap();
297        let header = HeaderBuilder::default().title("some tile").build().unwrap();
298        let card = CardBuilder::default()
299            .sections(vec![section])
300            .header(header)
301            .build()
302            .unwrap();
303        let cards = CardsBuilder::default().cards(vec![card]).build().unwrap();
304        let json = serde_json::json!(cards).to_string();
305        assert_eq!(json,"{\"cards\":[{\"header\":{\"title\":\"some tile\"},\"sections\":[{\"widgets\":[{\"textParagraph\":{\"text\":\"some text\"}}]}]}]}")
306    }
307
308    #[test]
309    fn build_text() {
310        let text = TextBuilder::default().text("some text").build().unwrap();
311        let json = serde_json::json!(text).to_string();
312        assert_eq!(json, "{\"text\":\"some text\"}")
313    }
314}