Skip to main content

routex_models/
lib.rs

1use std::{fmt::Display, str::FromStr};
2
3use bytes::Bytes;
4use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
5use isocountry::CountryCode;
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use serde_with::base64::Base64;
9use uuid::Uuid;
10
11#[cfg(feature = "uniffi")]
12uniffi::setup_scaffolding!();
13
14#[cfg(feature = "uniffi")]
15uniffi::custom_type!(Bytes, Vec<u8>, {
16    remote,
17    try_lift: |val| Ok(val.into()),
18    lower: |obj| obj.into(),
19});
20
21#[cfg(feature = "uniffi")]
22uniffi::custom_type!(ConnectionId, String, {
23    try_lift: |val| Ok(val.parse()?),
24    lower: |obj| obj.to_string(),
25});
26
27#[cfg(feature = "uniffi")]
28uniffi::custom_type!(Decimal, String, {
29    remote,
30    try_lift: |val| Ok(val.parse()?),
31    lower: |obj| obj.to_string(),
32});
33
34#[cfg(feature = "uniffi")]
35uniffi::custom_type!(CountryCode, String, {
36    remote,
37    try_lift: |val| Ok(Self::for_alpha2(&val)?),
38    lower: |obj| obj.alpha2().to_string(),
39});
40
41/// Identifier of a specific service connection.
42///
43/// Supports serialization, comparison, and hashing for use e.g. in a `HashMap`.
44#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
45pub struct ConnectionId(String);
46
47impl Display for ConnectionId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        self.0.fmt(f)
50    }
51}
52
53impl<'de> Deserialize<'de> for ConnectionId {
54    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
55    where
56        D: serde::Deserializer<'de>,
57    {
58        String::deserialize(deserializer)?
59            .parse()
60            .map_err(serde::de::Error::custom)
61    }
62}
63
64impl From<Uuid> for ConnectionId {
65    fn from(value: Uuid) -> Self {
66        Self(format!("connection-{value}"))
67    }
68}
69
70impl From<&ConnectionId> for Uuid {
71    fn from(value: &ConnectionId) -> Self {
72        (value.0[11..]).parse().unwrap()
73    }
74}
75
76impl FromStr for ConnectionId {
77    type Err = uuid::Error;
78
79    fn from_str(s: &str) -> Result<Self, Self::Err> {
80        s.trim_start_matches("connection-")
81            .parse::<Uuid>()
82            .map(Into::into)
83    }
84}
85
86/// Requirements for user identifier and password.
87#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
88#[non_exhaustive]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
90#[serde(rename_all = "camelCase")]
91pub struct CredentialsModel {
92    /// A full set of credentials may be provided to support fully embedded authentication (including scraped redirects).
93    pub full: bool,
94
95    /// Only a user identifier without a password may be provided.
96    /// This is typically the case for decoupled authentication where the user e.g. authorizes access in a mobile application.
97    /// Note that if password-less authentication fails (e.g. as no device for decoupled authentication is set up for the user and
98    /// a redirect is not supported), an error is returned and the transaction has to get restarted with a full set of credentials.
99    pub user_id: bool,
100
101    /// Credentials are not required. The user will provide them to the service provider during a redirect.
102    pub none: bool,
103}
104
105#[cfg(feature = "kitx")]
106impl CredentialsModel {
107    pub const FULL: CredentialsModel = CredentialsModel {
108        full: true,
109        user_id: false,
110        none: false,
111    };
112
113    pub const USER_ID: CredentialsModel = CredentialsModel {
114        full: false,
115        user_id: true,
116        none: false,
117    };
118
119    pub const NONE: CredentialsModel = CredentialsModel {
120        full: false,
121        user_id: false,
122        none: true,
123    };
124
125    pub const OPT_USER: CredentialsModel = CredentialsModel {
126        full: false,
127        user_id: true,
128        none: true,
129    };
130
131    pub const OPT_FULL: CredentialsModel = CredentialsModel {
132        full: true,
133        user_id: false,
134        none: true,
135    };
136}
137
138#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
139#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
140#[non_exhaustive]
141pub enum PaymentErrorCode {
142    LimitExceeded,
143    InsufficientFunds,
144}
145
146#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
147#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
148#[non_exhaustive]
149pub enum ProviderErrorCode {
150    Maintenance,
151}
152
153#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
154#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
155#[non_exhaustive]
156pub enum ServiceBlockedCode {
157    /// Something is not set up for the user, e.g., there are no TAN methods.
158    MissingSetup,
159    /// User attention is required via another channel. Typically the user needs to log into the Online Banking.
160    ActionRequired,
161}
162
163#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
164#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
165#[non_exhaustive]
166pub enum UnsupportedProductReason {
167    /// The amount is not allowed for the payment product.
168    Limit,
169    /// The recipient is not capable to receive the payment product.
170    Recipient,
171    /// Scheduled payments are not supported.
172    Scheduled,
173}
174
175#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
176#[serde(rename_all = "camelCase")]
177#[non_exhaustive]
178pub struct Account {
179    /// ISO 20022 IBAN2007Identifier.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub iban: Option<String>,
182
183    /// Account number that is not an IBAN, e.g. ISO 20022 BBANIdentifier or primary account number (PAN) of a card account.
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub number: Option<String>,
186
187    /// ISO 20022 BICFIIdentifier.
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub bic: Option<String>,
190
191    /// National bank code.
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub bank_code: Option<String>,
194
195    /// ISO 4217 Alpha 3 currency code.
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub currency: Option<String>,
198
199    /// Name of account, assigned by ASPSP.
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub name: Option<String>,
202
203    /// Display name of account, assigned by PSU.
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub display_name: Option<String>,
206
207    /// Legal account owner.
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub owner_name: Option<String>,
210
211    /// Product name.
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub product_name: Option<String>,
214
215    /// Account Status.
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub status: Option<AccountStatus>,
218
219    #[serde(skip_serializing_if = "Option::is_none")]
220    #[serde(rename = "type")]
221    pub type_: Option<AccountType>,
222
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub capabilities: Option<Vec<Capability>>,
225}
226
227#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
228#[non_exhaustive]
229#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
230pub enum AccountStatus {
231    Available,
232    Terminated,
233    Blocked,
234}
235
236#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
237#[non_exhaustive]
238#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
239pub enum AccountType {
240    /// Account used to post debits and credits.
241    /// ISO 20022 ExternalCashAccountType1Code CACC.
242    Current,
243    /// Account used for credit card payments.
244    /// ISO 20022 ExternalCashAccountType1Code CARD.
245    Card,
246    /// Account used for savings.
247    /// ISO 20022 ExternalCashAccountType1Code SVGS.
248    Savings,
249    /// Account used for call money.
250    /// No dedicated ISO 20022 code (falls into SVGS).
251    CallMoney,
252    /// Account used for time deposits.
253    /// No dedicated ISO 20022 code (falls into SVGS).
254    TimeDeposit,
255    /// Account used for loans.
256    /// ISO 20022 ExternalCashAccountType1Code LOAN.
257    Loan,
258    Securities,
259    Insurance,
260    Commerce,
261    Rewards,
262}
263
264#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
265#[serde(rename_all = "camelCase")]
266#[non_exhaustive]
267#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
268pub struct Amount {
269    /// ISO 4217 Alpha 3 currency code.
270    pub currency: String,
271
272    pub amount: Decimal,
273}
274
275impl Amount {
276    pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
277        Self {
278            amount: amount.into(),
279            currency: currency.into(),
280        }
281    }
282}
283
284#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
285#[non_exhaustive]
286pub enum Capability {
287    Balances,
288    Documents,
289    Securities,
290    Transactions,
291    SinglePayment,
292    BulkPayment,
293    StandingOrders,
294    ScheduledTransfers,
295}
296
297#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
298#[non_exhaustive]
299#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
300pub enum TransactionStatus {
301    /// The transaction is expected / planned.
302    Pending,
303    /// The transaction is booked to the account. This is typically the final state for most accounts.
304    Booked,
305    /// The credit card transaction is booked and invoiced but not yet paid.
306    Invoiced,
307    /// The credit card transaction is paid. This is typically the final state for card accounts.
308    Paid,
309    /// The transaction has been canceled in some way.
310    Canceled,
311}
312
313#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
314#[serde(rename_all = "camelCase")]
315#[non_exhaustive]
316#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
317pub struct Fee {
318    /// Amount of the fee.
319    pub amount: Amount,
320
321    /// ISO 20022 `ExternalChargeType1Code` for the fee.
322    #[serde(skip_serializing_if = "Option::is_none")]
323    #[cfg_attr(feature = "uniffi", uniffi(default))]
324    pub kind: Option<String>,
325
326    /// ISO 20022 `BICFIIdentifier` of the agent to whom the charges are due.
327    #[serde(skip_serializing_if = "Option::is_none")]
328    #[cfg_attr(feature = "uniffi", uniffi(default))]
329    pub bic: Option<String>,
330}
331
332impl Fee {
333    pub fn new(amount: impl Into<Amount>) -> Self {
334        Self {
335            amount: amount.into(),
336            kind: None,
337            bic: None,
338        }
339    }
340}
341
342/// User dialog.
343///
344/// This is meant to be displayed as a dialog in some User Interface and consists of:
345///
346/// - A way to cancel the dialog (typically an X symbol and / or a "Cancel" button).
347/// - The display part:
348///   - The `message`.
349///   - An optional `image`.
350/// - The interactive part defined by `input`.
351///
352/// The [`DialogInput`] contains a context for continuing the
353/// process at the service that issued the dialog object.
354#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
355#[serde(rename_all = "camelCase")]
356#[non_exhaustive]
357pub struct Dialog<ConfirmationContext, InputContext> {
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub context: Option<DialogContext>,
360    #[serde(skip_serializing_if = "Option::is_none")]
361    pub message: Option<String>,
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub image: Option<Image>,
364    #[serde(bound(
365        serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
366        deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
367    ))]
368    pub input: DialogInput<ConfirmationContext, InputContext>,
369}
370
371impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
372    pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
373        Self {
374            context: None,
375            message: None,
376            image: None,
377            input,
378        }
379    }
380}
381
382/// Context of a user dialog.
383#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
384#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
385#[non_exhaustive]
386pub enum DialogContext {
387    /// SCA or TAN process.
388    ///
389    /// There are multiple cases, distinguishable by the [`DialogInput`]:
390    /// - [`DialogInput::Confirmation`]: Decoupled process (e.g. confirmation in a SCA app).
391    /// - [`DialogInput::Selection`]: TAN method selection.
392    /// - [`DialogInput::Field`]: TAN entry.
393    Sca,
394
395    /// Account selection.
396    ///
397    /// A [`DialogInput::Selection`] gets returned with this context when an account has to be selected.
398    /// Note that there might be just a single option that may be chosen automatically without user interaction.
399    Accounts,
400
401    /// Pending redirect confirmation.
402    ///
403    /// A [`DialogInput::Confirmation`] gets returned with this context when a redirect got confirmed but no result is known yet.
404    Redirect,
405
406    /// Pending SCT Inst payment.
407    ///
408    /// A [`DialogInput::Confirmation`] gets returned with this context when an SCT Inst payment has been initialized and not reached the final status yet.
409    PaymentStatus,
410
411    /// Verification of Payee confirmation.
412    ///
413    /// A [`DialogInput::Confirmation`] gets returned with this context when an explicit confirmation of the creditor is required due to a name mismatch.
414    /// Note that this confirmation has legal implications, releasing the bank from liabilities in case of the transfer to an unintended receiver due to incorrect creditor data.
415    VopConfirmation,
416
417    /// Pending Verification of Payee check.
418    ///
419    /// A [`DialogInput::Confirmation`] gets returned with this context when a Verification of Payee check for a bulk payment is still pending.
420    VopCheck,
421}
422
423/// Data defining the interactive part of a user dialog.
424#[serde_with::serde_as]
425#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
426#[serde(rename_all_fields = "camelCase")]
427pub enum DialogInput<ConfirmationContext, InputContext> {
428    /// Just a primary action to confirm the dialog.
429    Confirmation {
430        /// Context object that can be used to confirm the dialog.
431        #[serde(bound(
432            serialize = "ConfirmationContext: AsRef<[u8]>",
433            deserialize = "ConfirmationContext: From<Vec<u8>>"
434        ))]
435        #[serde_as(as = "Base64")]
436        context: ConfirmationContext,
437
438        /// If polling is acceptable, a delay in seconds is specified for which the client has to wait before automatically confirming.
439        #[serde(skip_serializing_if = "Option::is_none")]
440        polling_delay_secs: Option<u32>,
441    },
442
443    /// A selection of options the user can choose from.
444    Selection {
445        /// Options are meant to be rendered e.g. as radio buttons where the user must select exactly
446        /// one to for a confirmation button to get enabled. Another example for an implementation is
447        /// one button per option that immediately confirms the selection.
448        options: Vec<DialogOption>,
449
450        /// Context object that can be used to respond to the dialog.
451        #[serde(bound(
452            serialize = "ConfirmationContext: AsRef<[u8]>",
453            deserialize = "ConfirmationContext: From<Vec<u8>>"
454        ))]
455        #[serde_as(as = "Base64")]
456        context: InputContext,
457    },
458
459    /// An input field.
460    Field {
461        /// Type that may be used for showing hints or dedicated keyboard layouts and for applying input restrictions or validation.
462        #[serde(rename = "type")]
463        type_: InputType,
464
465        /// Indicates if the input should be masked.
466        secrecy_level: SecrecyLevel,
467
468        /// Minimal length to allow.
469        #[serde(skip_serializing_if = "Option::is_none")]
470        min_length: Option<u32>,
471
472        /// Maximum length to allow.
473        #[serde(skip_serializing_if = "Option::is_none")]
474        max_length: Option<u32>,
475        #[serde(bound(
476            serialize = "InputContext: AsRef<[u8]>",
477            deserialize = "InputContext: From<Vec<u8>>"
478        ))]
479
480        /// Context object that can be used to respond to the dialog.
481        #[serde_as(as = "Base64")]
482        context: InputContext,
483    },
484}
485
486/// A dialog option.
487#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
488#[serde(rename_all = "camelCase")]
489#[non_exhaustive]
490#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
491pub struct DialogOption {
492    pub key: String,
493    pub label: String,
494    #[serde(skip_serializing_if = "Option::is_none")]
495    #[cfg_attr(feature = "uniffi", uniffi(default))]
496    pub explanation: Option<String>,
497}
498
499impl DialogOption {
500    pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
501        Self {
502            key: key.into(),
503            label: label.into(),
504            explanation: None,
505        }
506    }
507}
508
509/// Image data for a dialog.
510#[serde_with::serde_as]
511#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
512#[serde(rename_all = "camelCase")]
513#[non_exhaustive]
514#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
515pub struct Image {
516    pub mime_type: String,
517    /// Binary data in the format defined by `mime_type`.
518    #[serde_as(as = "Base64")]
519    pub data: Bytes,
520    #[allow(clippy::doc_markdown)]
521    /// HHD_UC data block
522    ///
523    /// In cases where the ASPSP provides HHD_UC data for optical coupling with a HandHeld-Device
524    /// for the generation of an OTP, especially for an HHD_OPT animated graphic, the raw HHD_UC
525    /// data stream is provided here.
526    ///
527    /// The publicly available document "HandHeld-Device (HHD) for the generation of an OTP HHD
528    /// enhancement for optical interfaces" describes how to implement the animated graphic for
529    /// HHD_OPT in section C. `data` provides a pre-rendered animated GIF
530    /// to be presented with a width of 62.5 mm.
531    #[serde_as(as = "Option<Base64>")]
532    #[serde(skip_serializing_if = "Option::is_none")]
533    #[cfg_attr(feature = "uniffi", uniffi(default))]
534    pub hhd_uc_data: Option<Bytes>,
535}
536
537impl Image {
538    pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
539        Self {
540            mime_type: mime_type.into(),
541            data: data.into(),
542            hhd_uc_data: None,
543        }
544    }
545}
546
547/// Level of secrecy for an input field.
548#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
549#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
550pub enum SecrecyLevel {
551    /// The data is not a secret.
552    Plain,
553    /// The data is a one-time password. This can usually be treated as
554    /// no secret but the implementer might still choose to mask the input.
555    Otp,
556    /// The data is a secret password. Input must be masked.
557    Password,
558}
559
560/// Type of an input field.
561#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
562#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
563pub enum InputType {
564    Date,
565    Email,
566    Number,
567    Phone,
568    Text,
569}
570
571#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
572#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
573#[non_exhaustive]
574pub enum PaymentProduct {
575    /// SEPA Credit Transfer (SCT) in EUR
576    SepaCreditTransfer,
577
578    /// SEPA Instant Credit Transfer (SCT Inst) in EUR
579    SepaInstantCreditTransfer,
580
581    /// Default SEPA Credit Transfer in EUR
582    ///
583    /// Tries SCT Inst with a fallback to SCT if this is supported.
584    /// Otherwise, SCT is used.
585    DefaultSepaCreditTransfer,
586
587    /// International credit transfer outside of SEPA (typically SWIFT)
588    CrossBorderCreditTransfer,
589
590    /// Domestic credit transfer in the domestic, non-EUR currency
591    DomesticCreditTransfer,
592
593    /// Instant domestic credit transfer in the domestic, non-EUR currency
594    DomesticInstantCreditTransfer,
595}
596
597#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
598#[serde(untagged)]
599pub enum ISODateTimeOrDate {
600    Date(NaiveDate),
601    NaiveDateTime(NaiveDateTime),
602    OffsetDateTime(DateTime<FixedOffset>),
603}
604
605impl ISODateTimeOrDate {
606    pub fn date(&self, tz: &impl TimeZone) -> NaiveDate {
607        match self {
608            Self::Date(d) => *d,
609            Self::NaiveDateTime(dt) => dt.date(),
610            Self::OffsetDateTime(dt) => dt.with_timezone(tz).date_naive(),
611        }
612    }
613}
614
615#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
616#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
617pub enum ChargeBearer {
618    #[serde(rename = "DEBT")]
619    BorneByDebtor,
620    #[serde(rename = "CRED")]
621    BorneByCreditor,
622    #[serde(rename = "SHAR")]
623    Shared,
624    #[serde(rename = "SLEV")]
625    FollowingServiceLevel,
626}
627
628impl Display for ChargeBearer {
629    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
630        self.serialize(f)
631    }
632}
633
634#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
635#[serde(rename_all = "camelCase")]
636#[non_exhaustive]
637#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
638pub struct CreditorAddress {
639    pub town_name: String,
640    pub country: CountryCode,
641}
642
643impl CreditorAddress {
644    #[must_use]
645    pub fn new(town_name: String, country: CountryCode) -> Self {
646        Self { town_name, country }
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use std::str::FromStr;
653
654    use uuid::Uuid;
655
656    use crate::ConnectionId;
657
658    #[test]
659    fn uuid_conversion() {
660        let uuid = Uuid::new_v4();
661        assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
662    }
663
664    #[test]
665    fn str_conversion() {
666        let uuid = Uuid::new_v4();
667        let s = format!("connection-{uuid}");
668
669        assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
670        assert_eq!(
671            s,
672            ConnectionId::from_str(&uuid.to_string())
673                .unwrap()
674                .to_string()
675        );
676    }
677
678    #[test]
679    fn deserialize_prefixed_connection_id() {
680        let _ = Uuid::from(
681            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
682                "connection-00000000-0000-0000-0000-000000000000".to_string(),
683            ))
684            .unwrap(),
685        );
686    }
687
688    #[test]
689    fn deserialize_stripped_connection_id() {
690        let _ = Uuid::from(
691            &serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
692                "00000000-0000-0000-0000-000000000000".to_string(),
693            ))
694            .unwrap(),
695        );
696    }
697
698    #[test]
699    fn deserialize_invalid_connection_id() {
700        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
701            .unwrap_err();
702    }
703
704    #[test]
705    fn deserialize_invalid_prefixed_connection_id() {
706        serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
707            "connection-0000".to_string(),
708        ))
709        .unwrap_err();
710    }
711
712    #[cfg(feature = "uniffi")]
713    fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
714        use uniffi::FfiConverter;
715
716        <ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
717            val.into(),
718        ))
719    }
720
721    #[cfg(feature = "uniffi")]
722    #[test]
723    fn convert_uuid_like_connection_id() {
724        let uuid = Uuid::new_v4();
725
726        assert_eq!(
727            try_lift(uuid.to_string()).unwrap(),
728            ConnectionId::from(uuid),
729        );
730    }
731
732    #[cfg(feature = "uniffi")]
733    #[test]
734    fn convert_prefixed_connection_id() {
735        let uuid = Uuid::new_v4();
736
737        assert_eq!(
738            try_lift(format!("connection-{uuid}")).unwrap(),
739            ConnectionId::from(uuid),
740        );
741    }
742
743    #[cfg(feature = "uniffi")]
744    #[test]
745    fn convert_connection_id() {
746        let connection_id = ConnectionId::from(Uuid::new_v4());
747
748        assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
749    }
750
751    #[cfg(feature = "uniffi")]
752    #[test]
753    fn convert_invalid_string() {
754        assert_eq!(
755            try_lift(String::new()).unwrap_err().to_string(),
756            "Lifting custom type `routex_models::ConnectionId` from FFI type `alloc::string::String` failed at routex-models/src/lib.rs:22"
757        );
758    }
759}