taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

payto.rs (14746B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 use std::{
     18     fmt::{Debug, Display},
     19     ops::{Deref, DerefMut},
     20     str::FromStr,
     21 };
     22 
     23 use compact_str::CompactString;
     24 use serde::{Deserialize, Serialize, de::DeserializeOwned};
     25 use serde_with::{DeserializeFromStr, SerializeDisplay};
     26 use url::Url;
     27 
     28 use super::{
     29     amount::Amount,
     30     iban::{BIC, IBAN},
     31 };
     32 
     33 /// Parse a payto URI, panic if malformed
     34 pub fn payto(url: impl AsRef<str>) -> PaytoURI {
     35     url.as_ref().parse().expect("invalid payto")
     36 }
     37 
     38 pub trait PaytoImpl: Sized {
     39     fn as_payto(&self) -> PaytoURI;
     40     fn as_full_payto(&self, name: &str) -> PaytoURI {
     41         self.as_payto().as_full_payto(name)
     42     }
     43     fn as_transfer_payto(
     44         &self,
     45         name: &str,
     46         amount: Option<&Amount>,
     47         subject: Option<&str>,
     48     ) -> PaytoURI {
     49         self.as_payto().as_transfer_payto(name, amount, subject)
     50     }
     51     fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>;
     52 }
     53 
     54 /// A generic RFC 8905 payto URI
     55 #[derive(
     56     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     57 )]
     58 pub struct PaytoURI(Url);
     59 
     60 impl PaytoURI {
     61     pub fn raw(&self) -> &str {
     62         self.0.as_str()
     63     }
     64 
     65     pub fn from_parts(domain: &str, path: impl Display) -> Self {
     66         payto(format!("payto://{domain}{path}"))
     67     }
     68 
     69     pub fn as_full_payto(self, name: &str) -> PaytoURI {
     70         self.with_query([("receiver-name", name)])
     71     }
     72 
     73     pub fn as_transfer_payto(
     74         self,
     75         name: &str,
     76         amount: Option<&Amount>,
     77         subject: Option<&str>,
     78     ) -> PaytoURI {
     79         self.as_full_payto(name)
     80             .with_query([("amount", amount)])
     81             .with_query([("message", subject)])
     82     }
     83 
     84     pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> {
     85         let query = self.0.query().unwrap_or_default().as_bytes();
     86         let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query));
     87         serde_path_to_error::deserialize(de).map_err(PaytoErr::Query)
     88     }
     89 
     90     fn with_query(mut self, query: impl Serialize) -> Self {
     91         let mut urlencoder = self.0.query_pairs_mut();
     92         query
     93             .serialize(serde_urlencoded::Serializer::new(&mut urlencoder))
     94             .unwrap();
     95         let _ = urlencoder.finish();
     96         drop(urlencoder);
     97         self
     98     }
     99 }
    100 
    101 impl AsRef<Url> for PaytoURI {
    102     fn as_ref(&self) -> &Url {
    103         &self.0
    104     }
    105 }
    106 
    107 impl std::fmt::Display for PaytoURI {
    108     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    109         std::fmt::Display::fmt(self.raw(), f)
    110     }
    111 }
    112 
    113 #[derive(Debug, thiserror::Error)]
    114 pub enum PaytoErr {
    115     #[error("invalid payto URI: {0}")]
    116     Url(#[from] url::ParseError),
    117     #[error("malformed payto URI query: {0}")]
    118     Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>),
    119     #[error("expected a payto URI got {0}")]
    120     NotPayto(CompactString),
    121     #[error("unsupported payto kind, expected {0} got {1}")]
    122     UnsupportedKind(&'static str, CompactString),
    123     #[error("to much path segment for a {0} payto uri")]
    124     TooLong(&'static str),
    125     #[error("missing segment {0} in path")]
    126     MissingSegment(&'static str),
    127     #[error("malformed segment {0}: {1}")]
    128     MalformedSegment(
    129         &'static str,
    130         Box<dyn std::error::Error + Sync + Send + 'static>,
    131     ),
    132 }
    133 
    134 impl PaytoErr {
    135     pub fn malformed_segment<E: std::error::Error + Sync + Send + 'static>(
    136         segment: &'static str,
    137         e: E,
    138     ) -> Self {
    139         Self::MalformedSegment(segment, Box::new(e))
    140     }
    141 }
    142 
    143 impl FromStr for PaytoURI {
    144     type Err = PaytoErr;
    145 
    146     fn from_str(s: &str) -> Result<Self, Self::Err> {
    147         // Parse url
    148         let url: Url = s.parse()?;
    149         // Check scheme
    150         if url.scheme() != "payto" {
    151             return Err(PaytoErr::NotPayto(url.scheme().into()));
    152         }
    153         Ok(Self(url))
    154     }
    155 }
    156 
    157 pub type IbanPayto = Payto<BankID>;
    158 pub type FullIbanPayto = FullPayto<BankID>;
    159 pub type TransferIbanPayto = TransferPayto<BankID>;
    160 
    161 #[derive(Debug, Clone, PartialEq, Eq)]
    162 pub struct BankID {
    163     pub iban: IBAN,
    164     pub bic: Option<BIC>,
    165 }
    166 
    167 const IBAN: &str = "iban";
    168 
    169 impl PaytoImpl for BankID {
    170     fn as_payto(&self) -> PaytoURI {
    171         PaytoURI::from_parts(
    172             IBAN,
    173             format_args!(
    174                 "/{}",
    175                 std::fmt::from_fn(|f| {
    176                     if let Some(bic) = &self.bic {
    177                         write!(f, "{bic}/")?;
    178                     }
    179                     write!(f, "{}", self.iban)
    180                 })
    181             ),
    182         )
    183     }
    184 
    185     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    186         let url = raw.as_ref();
    187         if url.domain() != Some(IBAN) {
    188             return Err(PaytoErr::UnsupportedKind(
    189                 IBAN,
    190                 url.domain().unwrap_or_default().into(),
    191             ));
    192         }
    193         let Some(mut segments) = url.path_segments() else {
    194             return Err(PaytoErr::MissingSegment("iban"));
    195         };
    196         let Some(first) = segments.next() else {
    197             return Err(PaytoErr::MissingSegment("iban"));
    198         };
    199         let (iban, bic) = match segments.next() {
    200             Some(second) => (second, Some(first)),
    201             None => (first, None),
    202         };
    203 
    204         Ok(Self {
    205             iban: iban
    206                 .parse()
    207                 .map_err(|e| PaytoErr::malformed_segment("iban", e))?,
    208             bic: bic
    209                 .map(|bic| {
    210                     bic.parse()
    211                         .map_err(|e| PaytoErr::malformed_segment("bic", e))
    212                 })
    213                 .transpose()?,
    214         })
    215     }
    216 }
    217 
    218 impl PaytoImpl for IBAN {
    219     fn as_payto(&self) -> PaytoURI {
    220         PaytoURI::from_parts("iban", format_args!("/{self}"))
    221     }
    222 
    223     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    224         raw.as_ref().path_segments().unwrap_or("".split('/'));
    225         let payto = BankID::parse(raw)?;
    226         Ok(payto.iban)
    227     }
    228 }
    229 
    230 /// Full payto query
    231 #[derive(Debug, Clone, Deserialize)]
    232 pub struct FullQuery {
    233     #[serde(rename = "receiver-name")]
    234     receiver_name: CompactString,
    235 }
    236 
    237 /// Transfer payto query
    238 #[derive(Debug, Clone, Deserialize)]
    239 pub struct TransferQuery {
    240     #[serde(rename = "receiver-name")]
    241     receiver_name: CompactString,
    242     amount: Option<Amount>,
    243     message: Option<CompactString>,
    244 }
    245 
    246 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    247 pub struct Payto<P>(P);
    248 
    249 impl<P: PaytoImpl> Payto<P> {
    250     pub fn new(inner: P) -> Self {
    251         Self(inner)
    252     }
    253 
    254     pub fn as_payto(&self) -> PaytoURI {
    255         self.0.as_payto()
    256     }
    257 
    258     pub fn into_inner(self) -> P {
    259         self.0
    260     }
    261 }
    262 
    263 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> {
    264     type Error = PaytoErr;
    265 
    266     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    267         Ok(Self(P::parse(value)?))
    268     }
    269 }
    270 
    271 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> {
    272     fn from(value: FullPayto<P>) -> Payto<P> {
    273         Payto(value.inner)
    274     }
    275 }
    276 
    277 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> {
    278     fn from(value: TransferPayto<P>) -> Payto<P> {
    279         Payto(value.inner)
    280     }
    281 }
    282 
    283 impl<P: PaytoImpl> std::fmt::Display for Payto<P> {
    284     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    285         std::fmt::Display::fmt(&self.as_payto(), f)
    286     }
    287 }
    288 
    289 impl<P: PaytoImpl> FromStr for Payto<P> {
    290     type Err = PaytoErr;
    291 
    292     fn from_str(s: &str) -> Result<Self, Self::Err> {
    293         let payto: PaytoURI = s.parse()?;
    294         Self::try_from(&payto)
    295     }
    296 }
    297 
    298 impl<P: PaytoImpl> Deref for Payto<P> {
    299     type Target = P;
    300 
    301     fn deref(&self) -> &Self::Target {
    302         &self.0
    303     }
    304 }
    305 
    306 impl<P: PaytoImpl> DerefMut for Payto<P> {
    307     fn deref_mut(&mut self) -> &mut Self::Target {
    308         &mut self.0
    309     }
    310 }
    311 
    312 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    313 pub struct FullPayto<P> {
    314     inner: P,
    315     pub name: CompactString,
    316 }
    317 
    318 impl<P: PaytoImpl> FullPayto<P> {
    319     pub fn new(inner: P, name: &str) -> Self {
    320         Self {
    321             inner,
    322             name: CompactString::new(name),
    323         }
    324     }
    325 
    326     pub fn as_payto(&self) -> PaytoURI {
    327         self.inner.as_full_payto(&self.name)
    328     }
    329 
    330     pub fn into_inner(self) -> P {
    331         self.inner
    332     }
    333 }
    334 
    335 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> {
    336     type Error = PaytoErr;
    337 
    338     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    339         let payto = P::parse(value)?;
    340         let query: FullQuery = value.query()?;
    341         Ok(Self {
    342             inner: payto,
    343             name: query.receiver_name,
    344         })
    345     }
    346 }
    347 
    348 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> {
    349     fn from(value: TransferPayto<P>) -> FullPayto<P> {
    350         FullPayto {
    351             inner: value.inner,
    352             name: value.name,
    353         }
    354     }
    355 }
    356 
    357 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> {
    358     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    359         std::fmt::Display::fmt(&self.as_payto(), f)
    360     }
    361 }
    362 
    363 impl<P: PaytoImpl> FromStr for FullPayto<P> {
    364     type Err = PaytoErr;
    365 
    366     fn from_str(s: &str) -> Result<Self, Self::Err> {
    367         let raw: PaytoURI = s.parse()?;
    368         Self::try_from(&raw)
    369     }
    370 }
    371 
    372 impl<P: PaytoImpl> Deref for FullPayto<P> {
    373     type Target = P;
    374 
    375     fn deref(&self) -> &Self::Target {
    376         &self.inner
    377     }
    378 }
    379 
    380 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    381 pub struct TransferPayto<P> {
    382     inner: P,
    383     pub name: CompactString,
    384     pub amount: Option<Amount>,
    385     pub subject: Option<CompactString>,
    386 }
    387 
    388 impl<P: PaytoImpl> TransferPayto<P> {
    389     pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self {
    390         Self {
    391             inner,
    392             name: CompactString::new(name),
    393             amount,
    394             subject: subject.map(CompactString::new),
    395         }
    396     }
    397 
    398     pub fn as_payto(&self) -> PaytoURI {
    399         self.inner
    400             .as_transfer_payto(&self.name, self.amount.as_ref(), self.subject.as_deref())
    401     }
    402 
    403     pub fn into_inner(self) -> P {
    404         self.inner
    405     }
    406 }
    407 
    408 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> {
    409     type Error = PaytoErr;
    410 
    411     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    412         let payto = P::parse(value)?;
    413         let query: TransferQuery = value.query()?;
    414         Ok(Self {
    415             inner: payto,
    416             name: query.receiver_name,
    417             amount: query.amount,
    418             subject: query.message,
    419         })
    420     }
    421 }
    422 
    423 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> {
    424     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    425         std::fmt::Display::fmt(&self.as_payto(), f)
    426     }
    427 }
    428 
    429 impl<P: PaytoImpl> FromStr for TransferPayto<P> {
    430     type Err = PaytoErr;
    431 
    432     fn from_str(s: &str) -> Result<Self, Self::Err> {
    433         let raw: PaytoURI = s.parse()?;
    434         Self::try_from(&raw)
    435     }
    436 }
    437 
    438 impl<P: PaytoImpl> Deref for TransferPayto<P> {
    439     type Target = P;
    440 
    441     fn deref(&self) -> &Self::Target {
    442         &self.inner
    443     }
    444 }
    445 
    446 #[cfg(test)]
    447 mod test {
    448     use std::str::FromStr as _;
    449 
    450     use crate::types::{
    451         amount::amount,
    452         iban::IBAN,
    453         payto::{FullPayto, Payto, TransferPayto},
    454     };
    455 
    456     #[test]
    457     pub fn parse() {
    458         let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap();
    459 
    460         // Simple payto
    461         let simple_payto = Payto::new(iban.clone());
    462         assert_eq!(
    463             simple_payto,
    464             Payto::from_str(&format!("payto://iban/{iban}")).unwrap()
    465         );
    466         assert_eq!(
    467             simple_payto,
    468             Payto::try_from(&simple_payto.as_payto()).unwrap()
    469         );
    470         assert_eq!(
    471             simple_payto,
    472             Payto::from_str(&simple_payto.as_payto().to_string()).unwrap()
    473         );
    474 
    475         // Full payto
    476         let full_payto = FullPayto::new(iban.clone(), "John Smith");
    477         assert_eq!(
    478             full_payto,
    479             FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap()
    480         );
    481         assert_eq!(
    482             full_payto,
    483             FullPayto::try_from(&full_payto.as_payto()).unwrap()
    484         );
    485         assert_eq!(
    486             full_payto,
    487             FullPayto::from_str(&full_payto.as_payto().to_string()).unwrap()
    488         );
    489         assert_eq!(simple_payto, full_payto.clone().into());
    490 
    491         // Transfer simple payto
    492         let transfer_payto = TransferPayto::new(iban.clone(), "John Smith", None, None);
    493         assert_eq!(
    494             transfer_payto,
    495             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith"))
    496                 .unwrap()
    497         );
    498         assert_eq!(
    499             transfer_payto,
    500             TransferPayto::try_from(&transfer_payto.as_payto()).unwrap()
    501         );
    502         assert_eq!(
    503             transfer_payto,
    504             TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap()
    505         );
    506         assert_eq!(full_payto, transfer_payto.clone().into());
    507 
    508         // Transfer full payto
    509         let transfer_payto = TransferPayto::new(
    510             iban.clone(),
    511             "John Smith",
    512             Some(amount("EUR:12")),
    513             Some("Wire transfer subject"),
    514         );
    515         assert_eq!(
    516             transfer_payto,
    517             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject"))
    518                 .unwrap()
    519         );
    520         assert_eq!(
    521             transfer_payto,
    522             TransferPayto::try_from(&transfer_payto.as_payto()).unwrap()
    523         );
    524         assert_eq!(
    525             transfer_payto,
    526             TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap()
    527         );
    528         assert_eq!(full_payto, transfer_payto.clone().into());
    529 
    530         let malformed = FullPayto::<IBAN>::from_str(
    531             "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA",
    532         )
    533         .unwrap();
    534         assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557");
    535         assert_eq!(malformed.name, "NYM Technologies%SA");
    536     }
    537 }