use std::{fmt::Display, str::FromStr};
use bytes::Bytes;
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, TimeZone};
use isocountry::CountryCode;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::base64::Base64;
use uuid::Uuid;
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!();
#[cfg(feature = "uniffi")]
uniffi::custom_type!(Bytes, Vec<u8>, {
remote,
try_lift: |val| Ok(val.into()),
lower: |obj| obj.into(),
});
#[cfg(feature = "uniffi")]
uniffi::custom_type!(ConnectionId, String, {
try_lift: |val| Ok(val.parse()?),
lower: |obj| obj.to_string(),
});
#[cfg(feature = "uniffi")]
uniffi::custom_type!(Decimal, String, {
remote,
try_lift: |val| Ok(val.parse()?),
lower: |obj| obj.to_string(),
});
#[cfg(feature = "uniffi")]
uniffi::custom_type!(CountryCode, String, {
remote,
try_lift: |val| Ok(Self::for_alpha2(&val)?),
lower: |obj| obj.alpha2().to_string(),
});
#[derive(Serialize, Eq, PartialEq, Clone, Hash, Debug)]
pub struct ConnectionId(String);
impl Display for ConnectionId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<'de> Deserialize<'de> for ConnectionId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer)?
.parse()
.map_err(serde::de::Error::custom)
}
}
impl From<Uuid> for ConnectionId {
fn from(value: Uuid) -> Self {
Self(format!("connection-{value}"))
}
}
impl From<&ConnectionId> for Uuid {
fn from(value: &ConnectionId) -> Self {
(value.0[11..]).parse().unwrap()
}
}
impl FromStr for ConnectionId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.trim_start_matches("connection-")
.parse::<Uuid>()
.map(Into::into)
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
#[serde(rename_all = "camelCase")]
pub struct CredentialsModel {
pub full: bool,
pub user_id: bool,
pub none: bool,
}
#[cfg(feature = "kitx")]
impl CredentialsModel {
pub const FULL: CredentialsModel = CredentialsModel {
full: true,
user_id: false,
none: false,
};
pub const USER_ID: CredentialsModel = CredentialsModel {
full: false,
user_id: true,
none: false,
};
pub const NONE: CredentialsModel = CredentialsModel {
full: false,
user_id: false,
none: true,
};
pub const OPT_USER: CredentialsModel = CredentialsModel {
full: false,
user_id: true,
none: true,
};
pub const OPT_FULL: CredentialsModel = CredentialsModel {
full: true,
user_id: false,
none: true,
};
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum PaymentErrorCode {
LimitExceeded,
InsufficientFunds,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum ProviderErrorCode {
Maintenance,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum ServiceBlockedCode {
MissingSetup,
ActionRequired,
}
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum UnsupportedProductReason {
Limit,
Recipient,
Scheduled,
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Default, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Account {
#[serde(skip_serializing_if = "Option::is_none")]
pub iban: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub number: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bic: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bank_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub currency: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub owner_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub product_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<AccountStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "type")]
pub type_: Option<AccountType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub capabilities: Option<Vec<Capability>>,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum AccountStatus {
Available,
Terminated,
Blocked,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum AccountType {
Current,
Card,
Savings,
CallMoney,
TimeDeposit,
Loan,
Securities,
Insurance,
Commerce,
Rewards,
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Amount {
pub currency: String,
pub amount: Decimal,
}
impl Amount {
pub fn new(amount: impl Into<Decimal>, currency: impl Into<String>) -> Self {
Self {
amount: amount.into(),
currency: currency.into(),
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum Capability {
Balances,
Documents,
Securities,
Transactions,
SinglePayment,
BulkPayment,
StandingOrders,
ScheduledTransfers,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum TransactionStatus {
Pending,
Booked,
Invoiced,
Paid,
Canceled,
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Fee {
pub amount: Amount,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub bic: Option<String>,
}
impl Fee {
pub fn new(amount: impl Into<Amount>) -> Self {
Self {
amount: amount.into(),
kind: None,
bic: None,
}
}
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Dialog<ConfirmationContext, InputContext> {
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<DialogContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<Image>,
#[serde(bound(
serialize = "ConfirmationContext: AsRef<[u8]>, InputContext: AsRef<[u8]>",
deserialize = "ConfirmationContext: From<Vec<u8>>, InputContext: From<Vec<u8>>"
))]
pub input: DialogInput<ConfirmationContext, InputContext>,
}
impl<ConCtx, InpCtx> Dialog<ConCtx, InpCtx> {
pub fn new(input: DialogInput<ConCtx, InpCtx>) -> Self {
Self {
context: None,
message: None,
image: None,
input,
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum DialogContext {
Sca,
Accounts,
Redirect,
PaymentStatus,
VopConfirmation,
VopCheck,
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all_fields = "camelCase")]
pub enum DialogInput<ConfirmationContext, InputContext> {
Confirmation {
#[serde(bound(
serialize = "ConfirmationContext: AsRef<[u8]>",
deserialize = "ConfirmationContext: From<Vec<u8>>"
))]
#[serde_as(as = "Base64")]
context: ConfirmationContext,
#[serde(skip_serializing_if = "Option::is_none")]
polling_delay_secs: Option<u32>,
},
Selection {
options: Vec<DialogOption>,
#[serde(bound(
serialize = "ConfirmationContext: AsRef<[u8]>",
deserialize = "ConfirmationContext: From<Vec<u8>>"
))]
#[serde_as(as = "Base64")]
context: InputContext,
},
Field {
#[serde(rename = "type")]
type_: InputType,
secrecy_level: SecrecyLevel,
#[serde(skip_serializing_if = "Option::is_none")]
min_length: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
max_length: Option<u32>,
#[serde(bound(
serialize = "InputContext: AsRef<[u8]>",
deserialize = "InputContext: From<Vec<u8>>"
))]
#[serde_as(as = "Base64")]
context: InputContext,
},
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct DialogOption {
pub key: String,
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub explanation: Option<String>,
}
impl DialogOption {
pub fn new(key: impl Into<String>, label: impl Into<String>) -> Self {
Self {
key: key.into(),
label: label.into(),
explanation: None,
}
}
}
#[serde_with::serde_as]
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct Image {
pub mime_type: String,
#[serde_as(as = "Base64")]
pub data: Bytes,
#[allow(clippy::doc_markdown)]
#[serde_as(as = "Option<Base64>")]
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "uniffi", uniffi(default))]
pub hhd_uc_data: Option<Bytes>,
}
impl Image {
pub fn new(mime_type: impl Into<String>, data: impl Into<Bytes>) -> Self {
Self {
mime_type: mime_type.into(),
data: data.into(),
hhd_uc_data: None,
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum SecrecyLevel {
Plain,
Otp,
Password,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum InputType {
Date,
Email,
Number,
Phone,
Text,
}
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
#[non_exhaustive]
pub enum PaymentProduct {
SepaCreditTransfer,
SepaInstantCreditTransfer,
DefaultSepaCreditTransfer,
CrossBorderCreditTransfer,
DomesticCreditTransfer,
DomesticInstantCreditTransfer,
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug)]
#[serde(untagged)]
pub enum ISODateTimeOrDate {
Date(NaiveDate),
NaiveDateTime(NaiveDateTime),
OffsetDateTime(DateTime<FixedOffset>),
}
impl ISODateTimeOrDate {
pub fn date(&self, tz: &impl TimeZone) -> NaiveDate {
match self {
Self::Date(d) => *d,
Self::NaiveDateTime(dt) => dt.date(),
Self::OffsetDateTime(dt) => dt.with_timezone(tz).date_naive(),
}
}
}
#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Debug)]
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
pub enum ChargeBearer {
#[serde(rename = "DEBT")]
BorneByDebtor,
#[serde(rename = "CRED")]
BorneByCreditor,
#[serde(rename = "SHAR")]
Shared,
#[serde(rename = "SLEV")]
FollowingServiceLevel,
}
impl Display for ChargeBearer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.serialize(f)
}
}
#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
pub struct CreditorAddress {
pub town_name: String,
pub country: CountryCode,
}
impl CreditorAddress {
#[must_use]
pub fn new(town_name: String, country: CountryCode) -> Self {
Self { town_name, country }
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use uuid::Uuid;
use crate::ConnectionId;
#[test]
fn uuid_conversion() {
let uuid = Uuid::new_v4();
assert_eq!(uuid, Uuid::from(&ConnectionId::from(uuid)));
}
#[test]
fn str_conversion() {
let uuid = Uuid::new_v4();
let s = format!("connection-{uuid}");
assert_eq!(s, ConnectionId::from_str(&s).unwrap().to_string());
assert_eq!(
s,
ConnectionId::from_str(&uuid.to_string())
.unwrap()
.to_string()
);
}
#[test]
fn deserialize_prefixed_connection_id() {
let _ = Uuid::from(
&serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
"connection-00000000-0000-0000-0000-000000000000".to_string(),
))
.unwrap(),
);
}
#[test]
fn deserialize_stripped_connection_id() {
let _ = Uuid::from(
&serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
"00000000-0000-0000-0000-000000000000".to_string(),
))
.unwrap(),
);
}
#[test]
fn deserialize_invalid_connection_id() {
serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(String::new()))
.unwrap_err();
}
#[test]
fn deserialize_invalid_prefixed_connection_id() {
serde_json::from_value::<super::ConnectionId>(serde_json::Value::String(
"connection-0000".to_string(),
))
.unwrap_err();
}
#[cfg(feature = "uniffi")]
fn try_lift(val: impl Into<String>) -> anyhow::Result<ConnectionId> {
use uniffi::FfiConverter;
<ConnectionId as FfiConverter<()>>::try_lift(<String as FfiConverter<()>>::lower(
val.into(),
))
}
#[cfg(feature = "uniffi")]
#[test]
fn convert_uuid_like_connection_id() {
let uuid = Uuid::new_v4();
assert_eq!(
try_lift(uuid.to_string()).unwrap(),
ConnectionId::from(uuid),
);
}
#[cfg(feature = "uniffi")]
#[test]
fn convert_prefixed_connection_id() {
let uuid = Uuid::new_v4();
assert_eq!(
try_lift(format!("connection-{uuid}")).unwrap(),
ConnectionId::from(uuid),
);
}
#[cfg(feature = "uniffi")]
#[test]
fn convert_connection_id() {
let connection_id = ConnectionId::from(Uuid::new_v4());
assert_eq!(try_lift(connection_id.to_string()).unwrap(), connection_id);
}
#[cfg(feature = "uniffi")]
#[test]
fn convert_invalid_string() {
assert_eq!(
try_lift(String::new()).unwrap_err().to_string(),
"Lifting custom type `routex_models::ConnectionId` from FFI type `alloc::string::String` failed at routex-models/src/lib.rs:22"
);
}
}