mailchimp_api/
lib.rs

1/*!
2 * A rust library for interacting with the MailChimp API.
3 *
4 * For more information, the MailChimp API is documented at [docs.mailchimp.com](https://docs.mailchimp.com/).
5 *
6 * Example:
7 *
8 * ```
9 * use mailchimp_api::MailChimp;
10 * use serde::{Deserialize, Serialize};
11 *
12 * async fn get_subscribers() {
13 *     // Initialize the MailChimp client.
14 *     let mailchimp = MailChimp::new_from_env("", "", "");
15 *
16 *     // Get the subscribers for a mailing list.
17 *     let subscribers = mailchimp.get_subscribers("some_id").await.unwrap();
18 *
19 *     println!("{:?}", subscribers);
20 * }
21 * ```
22 */
23use std::{collections::HashMap, env, error, fmt, fmt::Debug, sync::Arc};
24
25use chrono::{DateTime, Utc};
26use reqwest::{header, Client, Method, RequestBuilder, StatusCode, Url};
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29
30/// Entrypoint for interacting with the MailChimp API.
31pub struct MailChimp {
32    token: String,
33    // This expires in 101 days. It is hardcoded in the GitHub Actions secrets,
34    // We might want something a bit better like storing it in the database.
35    refresh_token: String,
36    client_id: String,
37    client_secret: String,
38    redirect_uri: String,
39    endpoint: String,
40
41    client: Arc<Client>,
42}
43
44impl MailChimp {
45    /// Create a new MailChimp client struct. It takes a type that can convert into
46    /// an &str (`String` or `Vec<u8>` for example). As long as the function is
47    /// given a valid API key your requests will work.
48    pub fn new<I, K, R, T, Q, C>(
49        client_id: I,
50        client_secret: K,
51        redirect_uri: R,
52        token: T,
53        refresh_token: Q,
54        endpoint: C,
55    ) -> Self
56    where
57        I: ToString,
58        K: ToString,
59        R: ToString,
60        T: ToString,
61        Q: ToString,
62        C: ToString,
63    {
64        let client = Client::builder().build();
65        match client {
66            Ok(c) => {
67                let g = MailChimp {
68                    client_id: client_id.to_string(),
69                    client_secret: client_secret.to_string(),
70                    redirect_uri: redirect_uri.to_string(),
71                    token: token.to_string(),
72                    refresh_token: refresh_token.to_string(),
73                    endpoint: endpoint.to_string(),
74
75                    client: Arc::new(c),
76                };
77
78                if g.token.is_empty() {
79                    // This is super hacky and a work around since there is no way to
80                    // auth without using the browser.
81                    println!("mailchimp consent URL: {}", g.user_consent_url());
82                }
83                // We do not refresh the access token since we leave that up to the
84                // user to do so they can re-save it to their database.
85
86                g
87            }
88            Err(e) => panic!("creating client failed: {:?}", e),
89        }
90    }
91
92    /// Create a new MailChimp client struct from environment variables. It
93    /// takes a type that can convert into
94    /// an &str (`String` or `Vec<u8>` for example). As long as the function is
95    /// given a valid API key and your requests will work.
96    /// We pass in the token and refresh token to the client so if you are storing
97    /// it in a database, you can get it first.
98    pub fn new_from_env<T, R, C>(token: T, refresh_token: R, endpoint: C) -> Self
99    where
100        T: ToString,
101        R: ToString,
102        C: ToString,
103    {
104        let client_id = env::var("MAILCHIMP_CLIENT_ID").unwrap();
105        let client_secret = env::var("MAILCHIMP_CLIENT_SECRET").unwrap();
106        let redirect_uri = env::var("MAILCHIMP_REDIRECT_URI").unwrap();
107
108        MailChimp::new(client_id, client_secret, redirect_uri, token, refresh_token, endpoint)
109    }
110
111    fn request<P>(&self, method: Method, path: P) -> RequestBuilder
112    where
113        P: ToString,
114    {
115        // Build the url.
116        let base = Url::parse(&self.endpoint).unwrap();
117        let mut p = path.to_string();
118        // Make sure we have the leading "/".
119        if !p.starts_with('/') {
120            p = format!("/{}", p);
121        }
122        let url = base.join(&p).unwrap();
123
124        let bt = format!("Bearer {}", self.token);
125        let bearer = header::HeaderValue::from_str(&bt).unwrap();
126
127        // Set the default headers.
128        let mut headers = header::HeaderMap::new();
129        headers.append(header::AUTHORIZATION, bearer);
130        headers.append(
131            header::CONTENT_TYPE,
132            header::HeaderValue::from_static("application/json"),
133        );
134
135        self.client.request(method, url).headers(headers)
136    }
137
138    pub fn user_consent_url(&self) -> String {
139        format!(
140            "https://login.mailchimp.com/oauth2/authorize?response_type=code&client_id={}&redirect_uri={}",
141            self.client_id, self.redirect_uri
142        )
143    }
144
145    pub async fn refresh_access_token(&mut self) -> Result<AccessToken, APIError> {
146        let mut headers = header::HeaderMap::new();
147        headers.append(
148            header::CONTENT_TYPE,
149            header::HeaderValue::from_static("application/x-www-form-urlencoded"),
150        );
151
152        let body = format!(
153            "grant_type=refresh_token&client_id={}&client_secret={}&\
154             redirect_uri={}refresh_token={}",
155            self.client_id,
156            self.client_secret,
157            urlencoding::encode(&self.redirect_uri),
158            self.refresh_token
159        );
160
161        let client = reqwest::Client::new();
162        let req = client
163            .post("https://login.mailchimp.com/oauth2/token")
164            .headers(headers)
165            .body(bytes::Bytes::from(body));
166        let resp = req.send().await.unwrap();
167
168        // Unwrap the response.
169        let t: AccessToken = resp.json().await.unwrap();
170
171        self.token = t.access_token.to_string();
172        self.refresh_token = t.refresh_token.to_string();
173
174        Ok(t)
175    }
176
177    pub async fn get_access_token(&mut self, code: &str) -> Result<AccessToken, APIError> {
178        let mut headers = header::HeaderMap::new();
179        headers.append(
180            header::CONTENT_TYPE,
181            header::HeaderValue::from_static("application/x-www-form-urlencoded"),
182        );
183
184        let body = format!(
185            "grant_type=authorization_code&client_id={}&client_secret={}&redirect_uri={}&code={}",
186            self.client_id,
187            self.client_secret,
188            urlencoding::encode(&self.redirect_uri),
189            code
190        );
191
192        let client = reqwest::Client::new();
193        let req = client
194            .post("https://login.mailchimp.com/oauth2/token")
195            .headers(headers)
196            .body(bytes::Bytes::from(body));
197        let resp = req.send().await.unwrap();
198
199        // Unwrap the response.
200        let t: AccessToken = resp.json().await.unwrap();
201
202        self.token = t.access_token.to_string();
203        self.refresh_token = t.refresh_token.to_string();
204
205        Ok(t)
206    }
207
208    /// Get metadata information.
209    pub async fn metadata(&self) -> Result<Metadata, APIError> {
210        let mut headers = header::HeaderMap::new();
211        headers.append(header::ACCEPT, header::HeaderValue::from_static("application/json"));
212        headers.append(
213            header::AUTHORIZATION,
214            header::HeaderValue::from_str(&format!("OAuth {}", self.token)).unwrap(),
215        );
216
217        // Build the request.
218        let client = reqwest::Client::new();
219        let resp = client
220            .get("https://login.mailchimp.com/oauth2/metadata")
221            .headers(headers)
222            .send()
223            .await
224            .unwrap();
225        match resp.status() {
226            StatusCode::OK => (),
227            s => {
228                return Err(APIError {
229                    status_code: s,
230                    body: resp.text().await.unwrap(),
231                })
232            }
233        };
234
235        // Try to deserialize the response.
236        Ok(resp.json().await.unwrap())
237    }
238
239    /// Returns a list of subscribers.
240    pub async fn get_subscribers(&self, list_id: &str) -> Result<Vec<Member>, APIError> {
241        let per_page = 500;
242        let mut offset: usize = 0;
243
244        let mut members: Vec<Member> = Default::default();
245
246        let mut has_more_rows = true;
247
248        while has_more_rows {
249            // Build the request.
250            let rb = self.request(
251                Method::GET,
252                &format!("3.0/lists/{}/members?count={}&offset={}", list_id, per_page, offset,),
253            );
254            let request = rb.build().unwrap();
255
256            let resp = self.client.execute(request).await.unwrap();
257            match resp.status() {
258                StatusCode::OK => (),
259                s => {
260                    return Err(APIError {
261                        status_code: s,
262                        body: resp.text().await.unwrap(),
263                    })
264                }
265            };
266
267            let text = resp.text().await.unwrap();
268
269            let mut r: ListMembersResponse = serde_json::from_str(&text).unwrap();
270
271            has_more_rows = !r.members.is_empty();
272            offset += r.members.len();
273
274            members.append(&mut r.members);
275        }
276
277        Ok(members)
278    }
279}
280
281/// Error type returned by our library.
282pub struct APIError {
283    pub status_code: StatusCode,
284    pub body: String,
285}
286
287impl fmt::Display for APIError {
288    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
289        write!(
290            f,
291            "APIError: status code -> {}, body -> {}",
292            self.status_code.to_string(),
293            self.body
294        )
295    }
296}
297
298impl fmt::Debug for APIError {
299    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
300        write!(
301            f,
302            "APIError: status code -> {}, body -> {}",
303            self.status_code.to_string(),
304            self.body
305        )
306    }
307}
308
309// This is important for other errors to wrap this one.
310impl error::Error for APIError {
311    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
312        // Generic error, underlying cause isn't tracked.
313        None
314    }
315}
316
317#[derive(Debug, JsonSchema, Clone, Default, Serialize, Deserialize)]
318pub struct AccessToken {
319    #[serde(
320        default,
321        skip_serializing_if = "String::is_empty",
322        deserialize_with = "deserialize_null_string::deserialize"
323    )]
324    pub access_token: String,
325    #[serde(
326        default,
327        skip_serializing_if = "String::is_empty",
328        deserialize_with = "deserialize_null_string::deserialize"
329    )]
330    pub token_type: String,
331    #[serde(default)]
332    pub expires_in: i64,
333    #[serde(default)]
334    pub x_refresh_token_expires_in: i64,
335    #[serde(
336        default,
337        skip_serializing_if = "String::is_empty",
338        deserialize_with = "deserialize_null_string::deserialize"
339    )]
340    pub refresh_token: String,
341}
342
343pub mod deserialize_null_string {
344    use serde::{self, Deserialize, Deserializer};
345
346    // The signature of a deserialize_with function must follow the pattern:
347    //
348    //    fn deserialize<'de, D>(D) -> Result<T, D::Error>
349    //    where
350    //        D: Deserializer<'de>
351    //
352    // although it may also be generic over the output types T.
353    pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
354    where
355        D: Deserializer<'de>,
356    {
357        let s = String::deserialize(deserializer).unwrap_or_default();
358
359        Ok(s)
360    }
361}
362
363#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
364pub struct MergeFields {
365    #[serde(default, skip_serializing_if = "String::is_empty", alias = "FNAME")]
366    pub first_name: String,
367    #[serde(default, skip_serializing_if = "String::is_empty", alias = "LNAME")]
368    pub last_name: String,
369    #[serde(default, skip_serializing_if = "String::is_empty", alias = "NAME")]
370    pub name: String,
371    #[serde(
372        default,
373        skip_serializing_if = "String::is_empty",
374        alias = "COMPANY",
375        alias = "CNAME"
376    )]
377    pub company: String,
378    #[serde(default, skip_serializing_if = "String::is_empty", alias = "CSIZE")]
379    pub company_size: String,
380    #[serde(default, skip_serializing_if = "String::is_empty", alias = "INTEREST")]
381    pub interest: String,
382    #[serde(default, skip_serializing_if = "String::is_empty", alias = "NOTES")]
383    pub notes: String,
384    #[serde(default, skip_serializing_if = "String::is_empty", alias = "BIRTHDAY")]
385    pub birthday: String,
386    #[serde(default, skip_serializing_if = "String::is_empty", alias = "PHONE")]
387    pub phone: String,
388    #[serde(default, alias = "ADDRESS")]
389    pub address: serde_json::Value,
390}
391
392#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
393pub struct Address {
394    #[serde(default, skip_serializing_if = "String::is_empty")]
395    pub addr1: String,
396    #[serde(default, skip_serializing_if = "String::is_empty")]
397    pub addr2: String,
398    #[serde(default, skip_serializing_if = "String::is_empty")]
399    pub city: String,
400    #[serde(default, skip_serializing_if = "String::is_empty")]
401    pub state: String,
402    #[serde(default, skip_serializing_if = "String::is_empty")]
403    pub zip: String,
404    #[serde(default, skip_serializing_if = "String::is_empty")]
405    pub country: String,
406}
407
408#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
409pub struct Location {
410    /// The location latitude.
411    #[serde(default)]
412    pub latitude: f64,
413    /// The location longitude.
414    #[serde(default)]
415    pub longitude: f64,
416    /// The time difference in hours from GMT.
417    #[serde(default)]
418    pub gmtoff: i32,
419    /// The offset for timezones where daylight saving time is observed.
420    #[serde(default)]
421    pub dstoff: i32,
422    /// The unique code for the location country.
423    #[serde(default, skip_serializing_if = "String::is_empty")]
424    pub country_code: String,
425    /// The timezone for the location.
426    #[serde(default, skip_serializing_if = "String::is_empty")]
427    pub time_zone: String,
428}
429
430#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
431pub struct MarketingPermissions {
432    /// The id for the marketing permission on the list.
433    #[serde(default, skip_serializing_if = "String::is_empty")]
434    pub marketing_permission_id: String,
435    /// The text of the marketing permission.
436    #[serde(default, skip_serializing_if = "String::is_empty")]
437    pub text: String,
438    /// If the subscriber has opted-in to the marketing permission.
439    #[serde(default)]
440    pub enabled: bool,
441}
442
443#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
444pub struct LastNote {
445    /// The note id.
446    #[serde(default)]
447    pub note_id: i64,
448    /// The date and time the note was created in ISO 8601 format.
449    #[serde(default)]
450    pub created_at: Option<DateTime<Utc>>,
451    /// The author of the note.
452    #[serde(default, skip_serializing_if = "String::is_empty")]
453    pub created_by: String,
454    /// The content of the note.
455    #[serde(default, skip_serializing_if = "String::is_empty")]
456    pub note: String,
457}
458
459#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
460pub struct Tag {
461    /// The tag id.
462    #[serde(default)]
463    pub id: i64,
464    /// The name of the tag.
465    #[serde(default, skip_serializing_if = "String::is_empty")]
466    pub name: String,
467}
468
469/// The data type for the webhook from Mailchimp.
470///
471/// FROM: https://mailchimp.com/developer/guides/sync-audience-data-with-webhooks/#handling-the-webhook-response-in-your-application
472#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
473pub struct Webhook {
474    #[serde(rename = "type")]
475    pub webhook_type: String,
476    #[serde(
477        deserialize_with = "mailchimp_date_format::deserialize",
478        serialize_with = "mailchimp_date_format::serialize"
479    )]
480    pub fired_at: DateTime<Utc>,
481    pub data: WebhookData,
482}
483
484mod mailchimp_date_format {
485    use chrono::{DateTime, TimeZone, Utc};
486    use serde::{self, Deserialize, Deserializer, Serializer};
487
488    const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
489
490    // The signature of a serialize_with function must follow the pattern:
491    //
492    //    fn serialize<S>(&T, S) -> Result<S::Ok, S::Error>
493    //    where
494    //        S: Serializer
495    //
496    // although it may also be generic over the input types T.
497    pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
498    where
499        S: Serializer,
500    {
501        let s = format!("{}", date.format(FORMAT));
502        serializer.serialize_str(&s)
503    }
504
505    // The signature of a deserialize_with function must follow the pattern:
506    //
507    //    fn deserialize<'de, D>(D) -> Result<T, D::Error>
508    //    where
509    //        D: Deserializer<'de>
510    //
511    // although it may also be generic over the output types T.
512    pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
513    where
514        D: Deserializer<'de>,
515    {
516        let s = String::deserialize(deserializer).unwrap();
517        Utc.datetime_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
518    }
519}
520
521#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
522pub struct WebhookData {
523    #[serde(skip_serializing_if = "Option::is_none")]
524    pub id: Option<String>,
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub list_id: Option<String>,
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub email: Option<String>,
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub email_type: Option<String>,
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub ip_opt: Option<String>,
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub ip_signup: Option<String>,
535    #[serde(skip_serializing_if = "Option::is_none")]
536    pub reason: Option<String>,
537    #[serde(skip_serializing_if = "Option::is_none")]
538    pub status: Option<String>,
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub web_id: Option<String>,
541    #[serde(skip_serializing_if = "Option::is_none")]
542    pub merges: Option<WebhookMerges>,
543}
544
545#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
546pub struct WebhookMerges {
547    #[serde(skip_serializing_if = "Option::is_none", rename = "FNAME")]
548    pub first_name: Option<String>,
549    #[serde(skip_serializing_if = "Option::is_none", rename = "LNAME")]
550    pub last_name: Option<String>,
551    #[serde(skip_serializing_if = "Option::is_none", rename = "NAME")]
552    pub name: Option<String>,
553    #[serde(skip_serializing_if = "Option::is_none", rename = "EMAIL")]
554    pub email: Option<String>,
555    #[serde(skip_serializing_if = "Option::is_none", rename = "ADDRESS")]
556    pub address: Option<String>,
557    #[serde(skip_serializing_if = "Option::is_none", rename = "PHONE")]
558    pub phone: Option<String>,
559    #[serde(skip_serializing_if = "Option::is_none", alias = "COMPANY", alias = "CNAME")]
560    pub company: Option<String>,
561    #[serde(skip_serializing_if = "Option::is_none", alias = "CSIZE")]
562    pub company_size: Option<String>,
563    #[serde(skip_serializing_if = "Option::is_none", rename = "INTEREST")]
564    pub interest: Option<String>,
565    #[serde(skip_serializing_if = "Option::is_none", rename = "NOTES")]
566    pub notes: Option<String>,
567    #[serde(skip_serializing_if = "Option::is_none", rename = "BIRTHDAY")]
568    pub birthday: Option<String>,
569    #[serde(skip_serializing_if = "Option::is_none", rename = "GROUPINGS")]
570    pub groupings: Option<Vec<WebhookGrouping>>,
571}
572
573#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
574pub struct WebhookGrouping {
575    pub id: String,
576    pub unique_id: String,
577    pub name: String,
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub groups: Option<String>,
580}
581
582#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
583pub struct Metadata {
584    #[serde(
585        default,
586        skip_serializing_if = "String::is_empty",
587        deserialize_with = "deserialize_null_string::deserialize"
588    )]
589    pub dc: String,
590    #[serde(
591        default,
592        skip_serializing_if = "String::is_empty",
593        deserialize_with = "deserialize_null_string::deserialize"
594    )]
595    pub accountname: String,
596    #[serde(
597        default,
598        skip_serializing_if = "String::is_empty",
599        deserialize_with = "deserialize_null_string::deserialize"
600    )]
601    pub api_endpoint: String,
602    #[serde(default)]
603    pub login: Login,
604}
605
606#[derive(Debug, Default, Clone, JsonSchema, Deserialize, Serialize)]
607pub struct Login {
608    #[serde(
609        default,
610        skip_serializing_if = "String::is_empty",
611        deserialize_with = "deserialize_null_string::deserialize"
612    )]
613    pub avatar: String,
614    #[serde(
615        default,
616        skip_serializing_if = "String::is_empty",
617        deserialize_with = "deserialize_null_string::deserialize"
618    )]
619    pub email: String,
620    #[serde(
621        default,
622        skip_serializing_if = "String::is_empty",
623        deserialize_with = "deserialize_null_string::deserialize"
624    )]
625    pub login_email: String,
626    #[serde(default)]
627    pub login_id: i64,
628    #[serde(
629        default,
630        skip_serializing_if = "String::is_empty",
631        deserialize_with = "deserialize_null_string::deserialize"
632    )]
633    pub login_name: String,
634    #[serde(
635        default,
636        skip_serializing_if = "String::is_empty",
637        deserialize_with = "deserialize_null_string::deserialize"
638    )]
639    pub login_url: String,
640    #[serde(
641        default,
642        skip_serializing_if = "String::is_empty",
643        deserialize_with = "deserialize_null_string::deserialize"
644    )]
645    pub role: String,
646    #[serde(default)]
647    pub user_id: i64,
648}
649
650/// The data type for the response to Mailchimp's API for listing members
651/// of a mailing list.
652///
653/// FROM: https://mailchimp.com/developer/api/marketing/list-members/list-members-info/
654#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
655pub struct ListMembersResponse {
656    /// An array of objects, each representing a specific list member.
657    #[serde(default, skip_serializing_if = "Vec::is_empty")]
658    pub members: Vec<Member>,
659    #[serde(default, skip_serializing_if = "String::is_empty")]
660    pub list_id: String,
661    #[serde(default)]
662    pub total_items: i64,
663}
664
665/// The data type for a member of a  Mailchimp mailing list.
666///
667/// FROM: https://mailchimp.com/developer/api/marketing/list-members/get-member-info/
668#[derive(Debug, Clone, JsonSchema, Deserialize, Serialize)]
669pub struct Member {
670    /// The MD5 hash of the lowercase version of the list member's email address.
671    #[serde(default, skip_serializing_if = "String::is_empty")]
672    pub id: String,
673    /// Email address for a subscriber.
674    #[serde(default, skip_serializing_if = "String::is_empty")]
675    pub email_address: String,
676    /// An identifier for the address across all of Mailchimp.
677    #[serde(default, skip_serializing_if = "String::is_empty")]
678    pub unique_email_id: String,
679    /// The ID used in the Mailchimp web application.
680    /// View this member in your Mailchimp account at:
681    ///     https://{dc}.admin.mailchimp.com/lists/members/view?id={web_id}.
682    #[serde(default)]
683    pub web_id: i64,
684    /// Type of email this member asked to get ('html' or 'text').
685    #[serde(default, skip_serializing_if = "String::is_empty")]
686    pub email_type: String,
687    /// Subscriber's current status.
688    /// Possible values:
689    ///     "subscribed", "unsubscribed", "cleaned", "pending", "transactional", or "archived".
690    #[serde(default, skip_serializing_if = "String::is_empty")]
691    pub status: String,
692    /// A subscriber's reason for unsubscribing.
693    #[serde(default, skip_serializing_if = "String::is_empty")]
694    pub unsubscribe_reason: String,
695    /// An individual merge var and value for a member.
696    #[serde(default)]
697    pub merge_fields: MergeFields,
698    /// The key of this object's properties is the ID of the interest in question.
699    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
700    pub interests: HashMap<String, bool>,
701    /// IP address the subscriber signed up from.
702    #[serde(default, skip_serializing_if = "String::is_empty")]
703    pub ip_signup: String,
704    /// The date and time the subscriber signed up for the list in ISO 8601 format.
705    #[serde(default, skip_serializing_if = "String::is_empty")]
706    pub timestamp_signup: String,
707    /// The IP address the subscriber used to confirm their opt-in status.
708    #[serde(default, skip_serializing_if = "String::is_empty")]
709    pub ip_opt: String,
710    /// The date and time the subscribe confirmed their opt-in status in ISO 8601 format.
711    #[serde(default, skip_serializing_if = "String::is_empty")]
712    pub timestamp_opt: String,
713    /// Star rating for this member, between 1 and 5.
714    #[serde(default)]
715    pub star_rating: i32,
716    /// The date and time the member's info was last changed in ISO 8601 format.
717    pub last_changed: DateTime<Utc>,
718    /// If set/detected, the subscriber's language.
719    #[serde(default, skip_serializing_if = "String::is_empty")]
720    pub language: String,
721    /// VIP status for subscriber.
722    #[serde(default)]
723    pub vip_status: bool,
724    /// The list member's email client.
725    #[serde(default, skip_serializing_if = "String::is_empty")]
726    pub email_client: String,
727    /// Subscriber location information.
728    #[serde(default)]
729    pub location: Location,
730    /// The marketing permissions for the subscriber.
731    #[serde(default, skip_serializing_if = "Vec::is_empty")]
732    pub marketing_permissions: Vec<MarketingPermissions>,
733    /// The most recent Note added about this member.
734    #[serde(default)]
735    pub last_note: LastNote,
736    /// The source from which the subscriber was added to this list.
737    #[serde(default, skip_serializing_if = "String::is_empty")]
738    pub source: String,
739    /// The number of tags applied to this member.
740    /// Returns up to 50 tags applied to this member. To retrieve all tags see Member Tags.
741    #[serde(default, skip_serializing_if = "Vec::is_empty")]
742    pub tags: Vec<Tag>,
743    /// The list id.
744    #[serde(default, skip_serializing_if = "String::is_empty")]
745    pub list_id: String,
746    #[serde(default)]
747    pub stats: Stats,
748}
749
750#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
751pub struct Stats {
752    #[serde(default)]
753    pub avg_open_rate: f32,
754    #[serde(default)]
755    pub avg_click_rate: f32,
756    #[serde(default)]
757    pub ecommerce_data: EcommerceData,
758}
759
760#[derive(Debug, Clone, Default, JsonSchema, Deserialize, Serialize)]
761pub struct EcommerceData {
762    #[serde(default)]
763    pub total_revenue: f32,
764    #[serde(default)]
765    pub number_of_orders: i32,
766    #[serde(default, skip_serializing_if = "String::is_empty")]
767    pub currency_code: String,
768}
769
770#[cfg(test)]
771mod tests {
772    use serde_qs::Config as QSConfig;
773
774    use super::*;
775
776    #[test]
777    fn test_mailchimp_webhook_parsing() {
778        let body = r#"type=subscribe&fired_at=2020-09-07 21:31:09&data[id]=b748506b63&data[email]=example@gmail.com&data[email_type]=html&data[ip_opt]=98.128.229.135&data[web_id]=404947702&data[merges][EMAIL]=example@gmail.com&data[merges][FNAME]=&data[merges][LNAME]=&data[merges][ADDRESS]=&data[merges][PHONE]=&data[merges][BIRTHDAY]=&data[merges][COMPANY]=&data[merges][INTEREST]=8&data[merges][INTERESTS]=Yes&data[merges][GROUPINGS][0][id]=6197&data[merges][GROUPINGS][0][unique_id]=458a556058&data[merges][GROUPINGS][0][name]=Interested in On the Metal podcast updates?&data[merges][GROUPINGS][0][groups]=Yes&data[merges][GROUPINGS][1][id]=6245&data[merges][GROUPINGS][1][unique_id]=f64af23d78&data[merges][GROUPINGS][1][name]=Interested in the Oxide newsletter?&data[merges][GROUPINGS][1][groups]=Yes&data[merges][GROUPINGS][2][id]=7518&data[merges][GROUPINGS][2][unique_id]=a9829c90a6&data[merges][GROUPINGS][2][name]=Interested in product updates?&data[merges][GROUPINGS][2][groups]=Yes&data[list_id]=8a6d823488"#;
779
780        let qs_non_strict = QSConfig::new(10, false);
781
782        // Parse the request body as a MailchimpWebhook.
783        let webhook: Webhook = qs_non_strict.deserialize_bytes(body.as_bytes()).unwrap();
784
785        println!("{:#?}", webhook);
786    }
787}