monzo_webhook/
lib.rs

1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code)]
3#![deny(clippy::pedantic)]
4#![allow(
5    clippy::struct_excessive_bools,
6    reason = "structs cannot be changed due to serialization"
7)]
8
9use std::collections::HashMap;
10
11use chrono::{DateTime, Utc};
12
13pub use crate::string_boolean::StringBoolean;
14
15#[cfg(feature = "decode_everything")]
16pub type ExtraValues = HashMap<String, serde_json::Value>;
17
18#[cfg(feature = "decode_everything")]
19pub mod has_extra_data;
20#[cfg(feature = "decode_everything")]
21pub use has_extra_data::*;
22
23/// Define an enum with an automatic `SomethingElse` variant that
24/// consumes any fields when no other variants match, but only when the
25/// `decode_everything` feature is enabled.
26macro_rules! enum_with_extra {
27    (
28        $(#[$attrs: meta])*
29        untagged $name: ident, $($(#[$variant_attrs: meta])* $variant: ident($variant_inner_ty: ty),)*
30    ) => {
31        $(#[$attrs])*
32        #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
33        #[serde(untagged)]
34        pub enum $name {
35            $($(#[$variant_attrs])* $variant($variant_inner_ty),)*
36
37            /// The value didn't match anything currently parsed.
38            #[cfg(feature = "decode_everything")]
39            SomethingElse(crate::ExtraValues),
40        }
41
42        #[cfg(feature = "decode_everything")]
43        impl crate::HasExtraData for $name {
44            fn has_extra_data(&self) -> bool {
45                match self {
46                    Self::SomethingElse(_vals) => true,
47                    $(Self::$variant(inner) => inner.has_extra_data(),)*
48                }
49            }
50        }
51    };
52    (
53        $case: expr => $(#[$attrs: meta])* $name: ident,
54        $($(#[$variant_attrs: meta])* $variant: ident,)*
55    ) => {
56        $(#[$attrs])*
57        #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
58        #[serde(rename_all = $case)]
59        pub enum $name {
60            $($(#[$variant_attrs])* $variant,)*
61
62            /// The value didn't match anything currently parsed.
63            #[cfg(feature = "decode_everything")]
64            #[serde(untagged)]
65            SomethingElse(String),
66        }
67
68        #[cfg(feature = "decode_everything")]
69        impl crate::HasExtraData for $name {
70            fn has_extra_data(&self) -> bool {
71                match self {
72                    Self::SomethingElse(_value) => true,
73                    _ => false,
74                }
75            }
76        }
77    };
78}
79
80/// Define a struct with an automatic `extra` field that consumes any
81/// remaining fields, but only when the `decode_everything` feature is
82/// enabled.
83macro_rules! struct_with_extra {
84    (
85        no_extra $(#[$attrs: meta])*
86        $name: ident, $($(#[$field_attrs: meta])* $field_name: ident: $field_typ: ty,)*
87    ) => {
88        $(#[$attrs])*
89        #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
90        pub struct $name {
91            $($(#[$field_attrs])* pub $field_name: $field_typ,)*
92        }
93
94        #[cfg(feature = "decode_everything")]
95        impl crate::HasExtraData for $name {
96            fn has_extra_data(&self) -> bool {
97                false $(|| self.$field_name.has_extra_data())*
98            }
99        }
100    };
101    (
102        $(#[$attrs: meta])*
103        $name: ident, $($(#[$field_attrs: meta])* $field_name: ident: $field_typ: ty,)*
104    ) => {
105        $(#[$attrs])*
106        #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
107        pub struct $name {
108            $($(#[$field_attrs])* pub $field_name: $field_typ,)*
109
110            /// The value had extra fields that weren't parsed into
111            /// another field.
112            #[cfg(feature = "decode_everything")]
113            #[cfg_attr(feature = "decode_everything", serde(flatten))]
114            pub extra: crate::ExtraValues,
115        }
116
117        #[cfg(feature = "decode_everything")]
118        impl crate::HasExtraData for $name {
119            fn has_extra_data(&self) -> bool {
120                let this_has_extra = !self.extra.is_empty();
121                this_has_extra
122                    $(|| self.$field_name.has_extra_data())*
123            }
124        }
125    };
126}
127
128#[cfg(test)]
129mod tests;
130
131pub mod counterparty;
132pub mod merchant;
133pub mod metadata;
134pub mod string_boolean;
135
136struct_with_extra! {
137    /// The main webhook data type.
138    Webhook,
139    /// The type of webhook update.
140    r#type: WebhookType,
141    /// The webhook data
142    data: WebhookData,
143}
144
145enum_with_extra! {
146    "snake_case" => WebhookType,
147    #[serde(rename = "transaction.created")]
148    TransactionCreated,
149    #[serde(rename = "transaction.updated")]
150    TransactionUpdated,
151}
152
153struct_with_extra! { WebhookData,
154    id: String,
155    created: DateTime<Utc>,
156    description: String,
157    /// The amount of money in the transaction, in whole pence (or equivalent for foreign currency)
158    amount: i64,
159    /// The ISO 4127 currency code of [`Self::amount`]
160    currency: String,
161    is_load: bool,
162    settled: SettledTimestamp,
163    /// The amount of money in the transaction, in whole pence (or equivalent for foreign currency)
164    local_amount: i64,
165    /// The ISO 4127 currency code of [`Self::local_amount`]
166    local_currency: String,
167    merchant: Option<merchant::Merchant>,
168    merchant_feedback_uri: String,
169    notes: String,
170    metadata: metadata::WebhookMetadata,
171    category: String,
172    updated: DateTime<Utc>,
173    account_id: String,
174    user_id: String,
175    counterparty: counterparty::CounterpartyOrNone,
176    scheme: Scheme,
177    dedupe_id: String,
178    originator: bool,
179    include_in_spending: bool,
180    can_be_excluded_from_breakdown: bool,
181    can_be_made_subscription: bool,
182    can_split_the_bill: bool,
183    can_add_to_tab: bool,
184    can_match_transactions_in_categorization: bool,
185    amount_is_pending: bool,
186    parent_account_id: String,
187    categories: Option<HashMap<String, i64>>,
188    // TODO The following fields are known about, but we don't know what types they hold
189    attachments: (),
190    atm_fees_detailed: (),
191    #[allow(clippy::zero_sized_map_values, reason = "this needs refactor when we establish type anyway")]
192    fees: HashMap<(), ()>,
193    international: (),
194    labels: (),
195}
196
197enum_with_extra! {
198    untagged SettledTimestamp,
199    Settled(DateTime<Utc>),
200    /// If not yet settled, a string it returned, however it always seems to be empty.
201    NotYetSettled(String),
202}
203
204enum_with_extra! { "snake_case" => Scheme,
205    Mastercard,
206    PayportFasterPayments,
207    UkRetailPot,
208    MonzoFlex,
209    MonzoToMonzo,
210}