stripe/
ids.rs

1use smart_default::SmartDefault;
2
3macro_rules! def_id_serde_impls {
4    ($struct_name:ident) => {
5        impl serde::Serialize for $struct_name {
6            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
7            where
8                S: serde::ser::Serializer,
9            {
10                self.as_str().serialize(serializer)
11            }
12        }
13
14        impl<'de> serde::Deserialize<'de> for $struct_name {
15            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
16            where
17                D: serde::de::Deserializer<'de>,
18            {
19                let s: String = serde::Deserialize::deserialize(deserializer)?;
20                s.parse::<Self>().map_err(::serde::de::Error::custom)
21            }
22        }
23    };
24    ($struct_name:ident, _) => {};
25}
26
27macro_rules! def_id {
28    ($struct_name:ident: String) => {
29        #[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
30        pub struct $struct_name(smol_str::SmolStr);
31
32        impl $struct_name {
33            /// Extracts a string slice containing the entire id.
34            #[inline(always)]
35            pub fn as_str(&self) -> &str {
36                self.0.as_str()
37            }
38        }
39
40        impl PartialEq<str> for $struct_name {
41            fn eq(&self, other: &str) -> bool {
42                self.as_str() == other
43            }
44        }
45
46        impl PartialEq<&str> for $struct_name {
47            fn eq(&self, other: &&str) -> bool {
48                self.as_str() == *other
49            }
50        }
51
52        impl PartialEq<String> for $struct_name {
53            fn eq(&self, other: &String) -> bool {
54                self.as_str() == other
55            }
56        }
57
58        impl PartialOrd for $struct_name {
59            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
60                Some(self.cmp(other))
61            }
62        }
63
64        impl Ord for $struct_name {
65            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
66                self.as_str().cmp(other.as_str())
67            }
68        }
69
70        impl AsRef<str> for $struct_name {
71            fn as_ref(&self) -> &str {
72                self.as_str()
73            }
74        }
75
76        impl crate::params::AsCursor for $struct_name {}
77
78        impl std::ops::Deref for $struct_name {
79            type Target = str;
80
81            fn deref(&self) -> &str {
82                self.as_str()
83            }
84        }
85
86        impl std::fmt::Display for $struct_name {
87            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88                self.0.fmt(f)
89            }
90        }
91
92        impl std::str::FromStr for $struct_name {
93            type Err = ParseIdError;
94
95            fn from_str(s: &str) -> Result<Self, Self::Err> {
96                Ok($struct_name(s.into()))
97            }
98        }
99
100        impl serde::Serialize for $struct_name {
101            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102                where S: serde::ser::Serializer
103            {
104                self.as_str().serialize(serializer)
105            }
106        }
107
108        impl<'de> serde::Deserialize<'de> for $struct_name {
109            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110                where D: serde::de::Deserializer<'de>
111            {
112                let s: String = serde::Deserialize::deserialize(deserializer)?;
113                s.parse::<Self>().map_err(::serde::de::Error::custom)
114            }
115        }
116    };
117    ($struct_name:ident, $prefix:literal $(| $alt_prefix:literal)* $(, { $generate_hint:tt })?) => {
118        /// An id for the corresponding object type.
119        ///
120        /// This type _typically_ will not allocate and
121        /// therefore is usually cheaply clonable.
122        #[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
123        pub struct $struct_name(smol_str::SmolStr);
124
125        impl $struct_name {
126            /// The prefix of the id type (e.g. `cus_` for a `CustomerId`).
127            #[inline(always)]
128            #[deprecated(note = "Please use prefixes or is_valid_prefix")]
129            pub fn prefix() -> &'static str {
130                $prefix
131            }
132
133            /// The valid prefixes of the id type (e.g. [`ch_`, `py_`\ for a `ChargeId`).
134            #[inline(always)]
135            pub fn prefixes() -> &'static [&'static str] {
136                &[$prefix$(, $alt_prefix)*]
137            }
138
139            /// Extracts a string slice containing the entire id.
140            #[inline(always)]
141            pub fn as_str(&self) -> &str {
142                self.0.as_str()
143            }
144
145            /// Check is provided prefix would be a valid prefix for id's of this type
146            pub fn is_valid_prefix(prefix: &str) -> bool {
147                prefix == $prefix $( || prefix == $alt_prefix )*
148            }
149        }
150
151        impl PartialEq<str> for $struct_name {
152            fn eq(&self, other: &str) -> bool {
153                self.as_str() == other
154            }
155        }
156
157        impl PartialEq<&str> for $struct_name {
158            fn eq(&self, other: &&str) -> bool {
159                self.as_str() == *other
160            }
161        }
162
163        impl PartialEq<String> for $struct_name {
164            fn eq(&self, other: &String) -> bool {
165                self.as_str() == other
166            }
167        }
168
169        impl PartialOrd for $struct_name {
170            fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
171                Some(self.cmp(other))
172            }
173        }
174
175        impl Ord for $struct_name {
176            fn cmp(&self, other: &Self) -> std::cmp::Ordering {
177                self.as_str().cmp(other.as_str())
178            }
179        }
180
181        impl AsRef<str> for $struct_name {
182            fn as_ref(&self) -> &str {
183                self.as_str()
184            }
185        }
186
187        impl crate::params::AsCursor for $struct_name {}
188
189        impl std::ops::Deref for $struct_name {
190            type Target = str;
191
192            fn deref(&self) -> &str {
193                self.as_str()
194            }
195        }
196
197        impl std::fmt::Display for $struct_name {
198            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199                self.0.fmt(f)
200            }
201        }
202
203        impl std::str::FromStr for $struct_name {
204            type Err = ParseIdError;
205
206            fn from_str(s: &str) -> Result<Self, Self::Err> {
207                if !s.starts_with($prefix) $(
208                    && !s.starts_with($alt_prefix)
209                )* {
210                    // N.B. For debugging
211                    eprintln!("bad id is: {} (expected: {:?}) for {}", s, $prefix, stringify!($struct_name));
212
213                    Err(ParseIdError {
214                        typename: stringify!($struct_name),
215                        expected: stringify!(id to start with $prefix $(or $alt_prefix)*),
216                    })
217                } else {
218                    Ok($struct_name(s.into()))
219                }
220            }
221        }
222
223        def_id_serde_impls!($struct_name $(, $generate_hint )*);
224    };
225    (#[optional] enum $enum_name:ident { $( $variant_name:ident($($variant_type:tt)*) ),* $(,)* }) => {
226        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
227        pub enum $enum_name {
228            None,
229            $( $variant_name($($variant_type)*), )*
230        }
231
232        impl $enum_name {
233            pub fn as_str(&self) -> &str {
234                match *self {
235                    $enum_name::None => "",
236                    $( $enum_name::$variant_name(ref id) => id.as_str(), )*
237                }
238            }
239        }
240
241        impl PartialEq<str> for $enum_name {
242            fn eq(&self, other: &str) -> bool {
243                self.as_str() == other
244            }
245        }
246
247        impl PartialEq<&str> for $enum_name {
248            fn eq(&self, other: &&str) -> bool {
249                self.as_str() == *other
250            }
251        }
252
253        impl PartialEq<String> for $enum_name {
254            fn eq(&self, other: &String) -> bool {
255                self.as_str() == other
256            }
257        }
258
259        impl AsRef<str> for $enum_name {
260            fn as_ref(&self) -> &str {
261                self.as_str()
262            }
263        }
264
265        impl crate::params::AsCursor for $enum_name {}
266
267        impl std::ops::Deref for $enum_name {
268            type Target = str;
269
270            fn deref(&self) -> &str {
271                self.as_str()
272            }
273        }
274
275        impl std::fmt::Display for $enum_name {
276            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
277                match *self {
278                    $enum_name::None => Ok(()),
279                    $( $enum_name::$variant_name(ref id) => id.fmt(f), )*
280                }
281            }
282        }
283
284        impl std::default::Default for $enum_name {
285            fn default() -> Self {
286                $enum_name::None
287            }
288        }
289
290        impl std::str::FromStr for $enum_name {
291            type Err = ParseIdError;
292
293            fn from_str(s: &str) -> Result<Self, Self::Err> {
294                let prefix = s.find('_')
295                    .map(|i| &s[0..=i])
296                    .ok_or_else(|| ParseIdError {
297                        typename: stringify!($enum_name),
298                        expected: "id to start with a prefix (as in 'prefix_')"
299                    })?;
300
301                match prefix {
302                    $(_ if $($variant_type)*::is_valid_prefix(prefix) => {
303                        Ok($enum_name::$variant_name(s.parse()?))
304                    })*
305                    _ => {
306                        Err(ParseIdError {
307                            typename: stringify!($enum_name),
308                            expected: "unknown id prefix",
309                        })
310                    }
311                }
312            }
313        }
314
315        impl serde::Serialize for $enum_name {
316            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
317                where S: serde::ser::Serializer
318            {
319                self.as_str().serialize(serializer)
320            }
321        }
322
323        impl<'de> serde::Deserialize<'de> for $enum_name {
324            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
325                where D: serde::de::Deserializer<'de>
326            {
327                let s: String = serde::Deserialize::deserialize(deserializer)?;
328                s.parse::<Self>().map_err(::serde::de::Error::custom)
329            }
330        }
331
332        $(
333            impl From<$($variant_type)*> for $enum_name {
334                fn from(id: $($variant_type)*) -> Self {
335                    $enum_name::$variant_name(id)
336                }
337            }
338        )*
339    };
340    (enum $enum_name:ident { $( $(#[$test:meta])? $variant_name:ident($($variant_type:tt)*) ),+ $(,)? }) => {
341        #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
342        #[derive(SmartDefault)]
343        pub enum $enum_name {
344            $( $(#[$test])* $variant_name($($variant_type)*), )*
345        }
346
347        impl $enum_name {
348            pub fn as_str(&self) -> &str {
349                match *self {
350                    $( $enum_name::$variant_name(ref id) => id.as_str(), )*
351                }
352            }
353        }
354
355        impl PartialEq<str> for $enum_name {
356            fn eq(&self, other: &str) -> bool {
357                self.as_str() == other
358            }
359        }
360
361        impl PartialEq<&str> for $enum_name {
362            fn eq(&self, other: &&str) -> bool {
363                self.as_str() == *other
364            }
365        }
366
367        impl PartialEq<String> for $enum_name {
368            fn eq(&self, other: &String) -> bool {
369                self.as_str() == other
370            }
371        }
372
373        impl AsRef<str> for $enum_name {
374            fn as_ref(&self) -> &str {
375                self.as_str()
376            }
377        }
378
379        impl crate::params::AsCursor for $enum_name {}
380
381        impl std::ops::Deref for $enum_name {
382            type Target = str;
383
384            fn deref(&self) -> &str {
385                self.as_str()
386            }
387        }
388
389        impl std::fmt::Display for $enum_name {
390            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
391                match *self {
392                    $( $enum_name::$variant_name(ref id) => id.fmt(f), )*
393                }
394            }
395        }
396
397        impl std::str::FromStr for $enum_name {
398            type Err = ParseIdError;
399
400            fn from_str(s: &str) -> Result<Self, Self::Err> {
401                let prefix = s.find('_')
402                    .map(|i| &s[0..=i])
403                    .ok_or_else(|| ParseIdError {
404                        typename: stringify!($enum_name),
405                        expected: "id to start with a prefix (as in 'prefix_')"
406                    })?;
407
408                match prefix {
409                    $(_ if $($variant_type)*::is_valid_prefix(prefix) => {
410                        Ok($enum_name::$variant_name(s.parse()?))
411                    })*
412                    _ => {
413                        Err(ParseIdError {
414                            typename: stringify!($enum_name),
415                            expected: "unknown id prefix",
416                        })
417                    }
418                }
419            }
420        }
421
422        impl serde::Serialize for $enum_name {
423            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
424                where S: serde::ser::Serializer
425            {
426                self.as_str().serialize(serializer)
427            }
428        }
429
430        impl<'de> serde::Deserialize<'de> for $enum_name {
431            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
432                where D: serde::de::Deserializer<'de>
433            {
434                let s: String = serde::Deserialize::deserialize(deserializer)?;
435                s.parse::<Self>().map_err(::serde::de::Error::custom)
436            }
437        }
438
439        $(
440            impl From<$($variant_type)*> for $enum_name {
441                fn from(id: $($variant_type)*) -> Self {
442                    $enum_name::$variant_name(id)
443                }
444            }
445        )*
446    };
447}
448
449#[derive(Clone, Debug)]
450pub struct ParseIdError {
451    typename: &'static str,
452    expected: &'static str,
453}
454
455impl std::fmt::Display for ParseIdError {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        write!(f, "invalid `{}`, expected {}", self.typename, self.expected)
458    }
459}
460
461impl std::error::Error for ParseIdError {
462    fn description(&self) -> &str {
463        "error parsing an id"
464    }
465}
466
467def_id!(AccountId, "acct_");
468def_id!(AlipayAccountId, "aliacc_");
469def_id!(ApplicationFeeId, "fee_");
470def_id!(ApplicationId, "ca_");
471def_id!(ApplicationFeeRefundId, "fr_");
472def_id!(BalanceTransactionId, "txn_");
473def_id!(BankAccountId, "ba_" | "card_");
474def_id!(BillingPortalSessionId, "bps_");
475def_id!(BillingPortalConfigurationId, "bpc_");
476def_id!(BankTokenId, "btok_");
477def_id!(
478    #[optional]
479    enum BalanceTransactionSourceId {
480        ApplicationFee(ApplicationFeeId),
481        Charge(ChargeId),
482        Dispute(DisputeId),
483        ApplicationFeeRefund(ApplicationFeeRefundId),
484        IssuingAuthorization(IssuingAuthorizationId),
485        IssuingDispute(IssuingDisputeId),
486        IssuingTransaction(IssuingTransactionId),
487        Payout(PayoutId),
488        Refund(RefundId),
489        Topup(TopupId),
490        Transfer(TransferId),
491        TransferReversal(TransferReversalId),
492    }
493);
494def_id!(CardId, "card_");
495def_id!(CardTokenId, "tok_");
496def_id!(ChargeId, "ch_" | "py_"); // TODO: Understand (and then document) why "py_" is a valid charge id
497def_id!(CheckoutSessionId, "cs_");
498def_id!(CheckoutSessionItemId, "li_");
499def_id!(ConnectCollectionTransferId, "connct_");
500def_id!(ConnectTokenId, "ct_");
501def_id!(CouponId: String); // N.B. A coupon id can be user-provided so can be any arbitrary string
502def_id!(CreditNoteId, "cn_");
503def_id!(CreditNoteLineItemId, "cnli_");
504def_id!(CustomerBalanceTransactionId, "cbtxn_");
505def_id!(CustomerId, "cus_");
506def_id!(DiscountId, "di_");
507def_id!(DisputeId, "dp_" | "du_" | "pdp_");
508def_id!(EphemeralKeyId, "ephkey_");
509def_id!(EventId, "evt_");
510def_id!(FileId, "file_");
511def_id!(FileLinkId, "link_");
512def_id!(InvoiceId, "in_", { _ });
513def_id!(InvoiceItemId, "ii_");
514def_id!(InvoiceLineItemIdWebhook, "il_");
515
516def_id!(
517    enum InvoiceLineItemId {
518        #[default]
519        Item(InvoiceItemId),
520        Subscription(SubscriptionLineId),
521        InvoiceLineItemIdWebhook(InvoiceLineItemIdWebhook),
522    }
523);
524def_id!(IssuingAuthorizationId, "iauth_");
525def_id!(IssuingCardId, "ic_");
526def_id!(IssuingCardholderId, "ich_");
527def_id!(IssuingDisputeId, "idp_");
528def_id!(IssuingTransactionId, "ipi_");
529def_id!(IssuingTokenId: String);
530def_id!(OrderId, "or_");
531def_id!(OrderReturnId, "orret_");
532def_id!(MandateId, "mandate_");
533def_id!(PaymentMethodConfigurationId: String);
534def_id!(PaymentIntentId, "pi_");
535def_id!(PaymentLinkId, "plink_");
536def_id!(PaymentMethodId, "pm_" | "card_" | "src_" | "ba_");
537def_id!(
538    enum PaymentSourceId {
539        #[default]
540        Account(AccountId),
541        AlipayAccount(AlipayAccountId),
542        BankAccount(BankAccountId),
543        Card(CardId),
544        Source(SourceId),
545    }
546);
547def_id!(PayoutId, "po_");
548def_id!(
549    enum PayoutDestinationId {
550        #[default]
551        BankAccount(BankAccountId),
552        Card(CardId),
553    }
554);
555def_id!(PersonId, "person_");
556def_id!(PlanId: String); // N.B. A plan id can be user-provided so can be any arbitrary string
557def_id!(PlatformTaxFeeId, "ptf");
558def_id!(PriceId: String); // N.B. A price id can be user-provided so can be any arbitrary string
559def_id!(ProductId: String); // N.B. A product id can be user-provided so can be any arbitrary string
560def_id!(PromotionCodeId, "promo_");
561def_id!(QuoteId, "qt_");
562def_id!(RecipientId: String); // FIXME: This doesn't seem to be documented yet
563def_id!(RefundId, "re_" | "pyr_");
564def_id!(ReserveTransactionId, "rtx_");
565def_id!(ReviewId, "prv_");
566def_id!(ScheduledQueryRunId, "sqr_");
567def_id!(SetupAttemptId, "setatt_");
568def_id!(SetupIntentId, "seti_");
569def_id!(SkuId, "sku_");
570def_id!(ShippingRateId, "shr_");
571def_id!(SourceId, "src_");
572def_id!(SubscriptionId, "sub_");
573def_id!(SubscriptionItemId, "si_");
574def_id!(SubscriptionLineId, "sli_");
575def_id!(SubscriptionScheduleId, "sub_sched_");
576def_id!(TaxIdId, "txi_" | "atxi_");
577def_id!(TaxCalculationId: String);
578def_id!(TaxCalculationLineItemId: String);
579def_id!(TaxCodeId, "txcd_");
580def_id!(TaxDeductedAtSourceId, "itds");
581def_id!(TaxRateId, "txr_");
582def_id!(TerminalConfigurationId, "tmc_");
583def_id!(TerminalLocationId, "tml_");
584def_id!(TerminalReaderId, "tmr_");
585def_id!(TestHelpersTestClockId, "clock_");
586def_id!(
587    enum TokenId {
588        #[default]
589        Card(CardTokenId),
590        Bank(BankTokenId),
591        Connect(ConnectTokenId),
592    }
593);
594def_id!(TopupId, "tu_");
595def_id!(TransferId, "tr_");
596def_id!(TransferReversalId, "trr_");
597def_id!(UsageRecordId, "mbur_");
598def_id!(UsageRecordSummaryId, "urs_" | "sis_");
599def_id!(WebhookEndpointId, "we_");
600
601impl InvoiceId {
602    pub(crate) fn none() -> Self {
603        Self("".into())
604    }
605
606    /// An InvoiceId may have a `None` representation when
607    /// received from Stripe if the Invoice is an upcoming invoice.
608    pub fn is_none(&self) -> bool {
609        self.0.is_empty()
610    }
611}
612impl serde::Serialize for InvoiceId {
613    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
614    where
615        S: serde::ser::Serializer,
616    {
617        if self.0.is_empty() {
618            let val: Option<&str> = None;
619            val.serialize(serializer)
620        } else {
621            self.as_str().serialize(serializer)
622        }
623    }
624}
625impl<'de> serde::Deserialize<'de> for InvoiceId {
626    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
627    where
628        D: serde::de::Deserializer<'de>,
629    {
630        let s: Option<String> = serde::Deserialize::deserialize(deserializer)?;
631        match s {
632            None => Ok(InvoiceId::none()),
633            Some(s) => {
634                if s.is_empty() {
635                    Ok(InvoiceId::none())
636                } else {
637                    s.parse::<Self>().map_err(::serde::de::Error::custom)
638                }
639            }
640        }
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use std::fmt::{Debug, Display};
647    use std::str::FromStr;
648
649    use serde::de::DeserializeOwned;
650    use serde::{Deserialize, Serialize};
651    use serde_json::json;
652
653    use super::*;
654
655    fn assert_ser_de_roundtrip<T>(id: &str)
656    where
657        T: DeserializeOwned + Serialize + FromStr + Display + Debug,
658        <T as FromStr>::Err: Debug,
659    {
660        let parsed_id = T::from_str(id).expect("Could not parse id");
661        let ser = serde_json::to_string(&parsed_id).expect("Could not serialize id");
662        let deser: T = serde_json::from_str(&ser).expect("Could not deserialize id");
663        assert_eq!(deser.to_string(), id.to_string());
664    }
665
666    fn assert_deser_err<T: DeserializeOwned + Debug>(id: &str) {
667        let json_str = format!(r#""{}""#, id);
668        let deser: Result<T, _> = serde_json::from_str(&json_str);
669        assert!(deser.is_err(), "Expected error, got {:?}", deser);
670    }
671
672    #[test]
673    fn test_empty_invoice_id_default() {
674        #[derive(Deserialize)]
675        struct WithInvoiceId {
676            id: InvoiceId,
677        }
678
679        for body in [json!({"id": ""}), json!({})] {
680            let deser: WithInvoiceId = serde_json::from_value(body).expect("Could not deser");
681            assert_eq!(deser.id, InvoiceId::none());
682        }
683    }
684
685    #[test]
686    fn test_ser_de_roundtrip() {
687        // InvoiceId special cased
688        for id in ["in_12345", "in_"] {
689            assert_ser_de_roundtrip::<InvoiceId>(id);
690        }
691
692        // Single prefix
693        assert_ser_de_roundtrip::<PriceId>("price_abc");
694
695        // Case where multiple possible prefixes
696        for id in ["re_bcd", "pyr_123"] {
697            assert_ser_de_roundtrip::<RefundId>(id);
698        }
699
700        // Case where id can be anything
701        for id in ["anything", ""] {
702            assert_ser_de_roundtrip::<ProductId>(id);
703        }
704
705        // Case where enum id
706        for id in ["tok_123", "btok_456"] {
707            assert_ser_de_roundtrip::<TokenId>(id);
708        }
709    }
710
711    #[test]
712    fn test_deser_err() {
713        // InvoiceId special cased
714        assert_deser_err::<InvoiceId>("in");
715
716        // Single prefix
717        for id in ["sub", ""] {
718            assert_deser_err::<SubscriptionId>(id);
719        }
720
721        // Case where multiple possible prefixes
722        for id in ["abc_bcd", "pyr_123"] {
723            assert_deser_err::<PaymentMethodId>(id);
724        }
725
726        // Case where enum id
727        for id in ["tok_123", "btok_456"] {
728            assert_deser_err::<PaymentSourceId>(id);
729        }
730    }
731
732    #[test]
733    fn test_parse_customer() {
734        assert!("cus_123".parse::<CustomerId>().is_ok());
735        let bad_parse = "zzz_123".parse::<CustomerId>();
736        assert!(bad_parse.is_err());
737        if let Err(err) = bad_parse {
738            assert_eq!(
739                format!("{}", err),
740                "invalid `CustomerId`, expected id to start with \"cus_\""
741            );
742        }
743    }
744
745    #[test]
746    fn test_parse_charge() {
747        assert!("ch_123".parse::<ChargeId>().is_ok());
748        assert!("py_123".parse::<ChargeId>().is_ok());
749        let bad_parse = "zz_123".parse::<ChargeId>();
750        assert!(bad_parse.is_err());
751        if let Err(err) = bad_parse {
752            assert_eq!(
753                format!("{}", err),
754                "invalid `ChargeId`, expected id to start with \"ch_\" or \"py_\""
755            );
756        }
757    }
758}