taler-rust

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

payto.rs (14184B)


      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,
     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 /// Generate an iban payto URI, panic if malformed
     39 pub fn iban_payto(iban: impl AsRef<str>, name: impl AsRef<str>) -> PaytoURI {
     40     IbanPayto::new(BankID {
     41         iban: iban.as_ref().parse().expect("invalid IBAN"),
     42         bic: None,
     43     })
     44     .as_full_payto(name.as_ref())
     45 }
     46 
     47 pub trait PaytoImpl: Sized {
     48     fn as_payto(&self) -> PaytoURI;
     49     fn as_full_payto(&self, name: &str) -> PaytoURI {
     50         self.as_payto().as_full_payto(name)
     51     }
     52     fn as_transfer_payto(
     53         &self,
     54         name: &str,
     55         amount: Option<&Amount>,
     56         subject: Option<&str>,
     57     ) -> PaytoURI {
     58         self.as_payto().as_transfer_payto(name, amount, subject)
     59     }
     60     fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>;
     61 }
     62 
     63 /// A generic RFC 8905 payto URI
     64 #[derive(
     65     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     66 )]
     67 pub struct PaytoURI(Url);
     68 
     69 impl PaytoURI {
     70     pub fn raw(&self) -> &str {
     71         self.0.as_str()
     72     }
     73 
     74     pub fn from_parts(domain: &str, path: impl Display) -> Self {
     75         payto(format!("payto://{domain}{path}"))
     76     }
     77 
     78     pub fn as_full_payto(self, name: &str) -> PaytoURI {
     79         self.with_query([("receiver-name", name)])
     80     }
     81 
     82     pub fn as_transfer_payto(
     83         self,
     84         name: &str,
     85         amount: Option<&Amount>,
     86         subject: Option<&str>,
     87     ) -> PaytoURI {
     88         self.as_full_payto(name)
     89             .with_query([("amount", amount)])
     90             .with_query([("message", subject)])
     91     }
     92 
     93     pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> {
     94         let query = self.0.query().unwrap_or_default().as_bytes();
     95         let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query));
     96         serde_path_to_error::deserialize(de).map_err(PaytoErr::Query)
     97     }
     98 
     99     fn with_query(mut self, query: impl Serialize) -> Self {
    100         let mut urlencoder = self.0.query_pairs_mut();
    101         query
    102             .serialize(serde_urlencoded::Serializer::new(&mut urlencoder))
    103             .unwrap();
    104         let _ = urlencoder.finish();
    105         drop(urlencoder);
    106         self
    107     }
    108 }
    109 
    110 impl AsRef<Url> for PaytoURI {
    111     fn as_ref(&self) -> &Url {
    112         &self.0
    113     }
    114 }
    115 
    116 impl std::fmt::Display for PaytoURI {
    117     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    118         std::fmt::Display::fmt(self.raw(), f)
    119     }
    120 }
    121 
    122 #[derive(Debug, thiserror::Error)]
    123 pub enum PaytoErr {
    124     #[error("invalid payto URI: {0}")]
    125     Url(#[from] url::ParseError),
    126     #[error("malformed payto URI query: {0}")]
    127     Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>),
    128     #[error("expected a payto URI got {0}")]
    129     NotPayto(String),
    130     #[error("unsupported payto kind, expected {0} got {1}")]
    131     UnsupportedKind(&'static str, String),
    132     #[error("to much path segment for a {0} payto uri")]
    133     TooLong(&'static str),
    134     #[error(transparent)]
    135     Custom(Box<dyn std::error::Error + Sync + Send + 'static>),
    136 }
    137 
    138 impl PaytoErr {
    139     pub fn custom<E: std::error::Error + Sync + Send + 'static>(e: E) -> Self {
    140         Self::Custom(Box::new(e))
    141     }
    142 }
    143 
    144 impl FromStr for PaytoURI {
    145     type Err = PaytoErr;
    146 
    147     fn from_str(s: &str) -> Result<Self, Self::Err> {
    148         // Parse url
    149         let url: Url = s.parse()?;
    150         // Check scheme
    151         if url.scheme() != "payto" {
    152             return Err(PaytoErr::NotPayto(url.scheme().to_owned()));
    153         }
    154         Ok(Self(url))
    155     }
    156 }
    157 
    158 pub type IbanPayto = Payto<BankID>;
    159 pub type FullIbanPayto = FullPayto<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 #[derive(Debug, thiserror::Error)]
    170 #[error("missing IBAN in path")]
    171 pub struct MissingIban;
    172 
    173 impl PaytoImpl for BankID {
    174     fn as_payto(&self) -> PaytoURI {
    175         PaytoURI::from_parts(IBAN, format_args!("/{}", self.iban))
    176     }
    177 
    178     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    179         let url = raw.as_ref();
    180         if url.domain() != Some(IBAN) {
    181             return Err(PaytoErr::UnsupportedKind(
    182                 IBAN,
    183                 url.domain().unwrap_or_default().to_owned(),
    184             ));
    185         }
    186         let Some(mut segments) = url.path_segments() else {
    187             return Err(PaytoErr::custom(MissingIban));
    188         };
    189         let Some(first) = segments.next() else {
    190             return Err(PaytoErr::custom(MissingIban));
    191         };
    192         let (iban, bic) = match segments.next() {
    193             Some(second) => (
    194                 second.parse().map_err(PaytoErr::custom)?,
    195                 Some(first.parse().map_err(PaytoErr::custom)?),
    196             ),
    197             None => (first.parse().map_err(PaytoErr::custom)?, None),
    198         };
    199 
    200         Ok(Self { iban, bic })
    201     }
    202 }
    203 
    204 impl PaytoImpl for IBAN {
    205     fn as_payto(&self) -> PaytoURI {
    206         PaytoURI::from_parts(IBAN, format_args!("/{self}"))
    207     }
    208 
    209     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    210         let payto = BankID::parse(raw)?;
    211         Ok(payto.iban)
    212     }
    213 }
    214 
    215 /// Full payto query
    216 #[derive(Debug, Clone, Deserialize)]
    217 pub struct FullQuery {
    218     #[serde(rename = "receiver-name")]
    219     receiver_name: CompactString,
    220 }
    221 
    222 /// Transfer payto query
    223 #[derive(Debug, Clone, Deserialize)]
    224 pub struct TransferQuery {
    225     #[serde(rename = "receiver-name")]
    226     receiver_name: CompactString,
    227     amount: Option<Amount>,
    228     message: Option<CompactString>,
    229 }
    230 
    231 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    232 pub struct Payto<P>(P);
    233 
    234 impl<P: PaytoImpl> Payto<P> {
    235     pub fn new(inner: P) -> Self {
    236         Self(inner)
    237     }
    238 
    239     pub fn as_payto(&self) -> PaytoURI {
    240         self.0.as_payto()
    241     }
    242 
    243     pub fn into_inner(self) -> P {
    244         self.0
    245     }
    246 }
    247 
    248 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> {
    249     type Error = PaytoErr;
    250 
    251     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    252         Ok(Self(P::parse(value)?))
    253     }
    254 }
    255 
    256 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> {
    257     fn from(value: FullPayto<P>) -> Payto<P> {
    258         Payto(value.inner)
    259     }
    260 }
    261 
    262 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> {
    263     fn from(value: TransferPayto<P>) -> Payto<P> {
    264         Payto(value.inner)
    265     }
    266 }
    267 
    268 impl<P: PaytoImpl> std::fmt::Display for Payto<P> {
    269     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    270         std::fmt::Display::fmt(&self.as_payto(), f)
    271     }
    272 }
    273 
    274 impl<P: PaytoImpl> FromStr for Payto<P> {
    275     type Err = PaytoErr;
    276 
    277     fn from_str(s: &str) -> Result<Self, Self::Err> {
    278         let payto: PaytoURI = s.parse()?;
    279         Self::try_from(&payto)
    280     }
    281 }
    282 
    283 impl<P: PaytoImpl> Deref for Payto<P> {
    284     type Target = P;
    285 
    286     fn deref(&self) -> &Self::Target {
    287         &self.0
    288     }
    289 }
    290 
    291 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    292 pub struct FullPayto<P> {
    293     inner: P,
    294     pub name: CompactString,
    295 }
    296 
    297 impl<P: PaytoImpl> FullPayto<P> {
    298     pub fn new(inner: P, name: &str) -> Self {
    299         Self {
    300             inner,
    301             name: CompactString::new(name),
    302         }
    303     }
    304 
    305     pub fn as_payto(&self) -> PaytoURI {
    306         self.inner.as_full_payto(&self.name)
    307     }
    308 
    309     pub fn into_inner(self) -> P {
    310         self.inner
    311     }
    312 }
    313 
    314 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> {
    315     type Error = PaytoErr;
    316 
    317     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    318         let payto = P::parse(value)?;
    319         let query: FullQuery = value.query()?;
    320         Ok(Self {
    321             inner: payto,
    322             name: query.receiver_name,
    323         })
    324     }
    325 }
    326 
    327 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> {
    328     fn from(value: TransferPayto<P>) -> FullPayto<P> {
    329         FullPayto {
    330             inner: value.inner,
    331             name: value.name,
    332         }
    333     }
    334 }
    335 
    336 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> {
    337     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    338         std::fmt::Display::fmt(&self.as_payto(), f)
    339     }
    340 }
    341 
    342 impl<P: PaytoImpl> FromStr for FullPayto<P> {
    343     type Err = PaytoErr;
    344 
    345     fn from_str(s: &str) -> Result<Self, Self::Err> {
    346         let raw: PaytoURI = s.parse()?;
    347         Self::try_from(&raw)
    348     }
    349 }
    350 
    351 impl<P: PaytoImpl> Deref for FullPayto<P> {
    352     type Target = P;
    353 
    354     fn deref(&self) -> &Self::Target {
    355         &self.inner
    356     }
    357 }
    358 
    359 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
    360 pub struct TransferPayto<P> {
    361     inner: P,
    362     pub name: CompactString,
    363     pub amount: Option<Amount>,
    364     pub subject: Option<CompactString>,
    365 }
    366 
    367 impl<P: PaytoImpl> TransferPayto<P> {
    368     pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self {
    369         Self {
    370             inner,
    371             name: CompactString::new(name),
    372             amount,
    373             subject: subject.map(CompactString::new),
    374         }
    375     }
    376 
    377     pub fn as_payto(&self) -> PaytoURI {
    378         self.inner
    379             .as_transfer_payto(&self.name, self.amount.as_ref(), self.subject.as_deref())
    380     }
    381 
    382     pub fn into_inner(self) -> P {
    383         self.inner
    384     }
    385 }
    386 
    387 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> {
    388     type Error = PaytoErr;
    389 
    390     fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> {
    391         let payto = P::parse(value)?;
    392         let query: TransferQuery = value.query()?;
    393         Ok(Self {
    394             inner: payto,
    395             name: query.receiver_name,
    396             amount: query.amount,
    397             subject: query.message,
    398         })
    399     }
    400 }
    401 
    402 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> {
    403     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    404         std::fmt::Display::fmt(&self.as_payto(), f)
    405     }
    406 }
    407 
    408 impl<P: PaytoImpl> FromStr for TransferPayto<P> {
    409     type Err = PaytoErr;
    410 
    411     fn from_str(s: &str) -> Result<Self, Self::Err> {
    412         let raw: PaytoURI = s.parse()?;
    413         Self::try_from(&raw)
    414     }
    415 }
    416 
    417 impl<P: PaytoImpl> Deref for TransferPayto<P> {
    418     type Target = P;
    419 
    420     fn deref(&self) -> &Self::Target {
    421         &self.inner
    422     }
    423 }
    424 
    425 #[cfg(test)]
    426 mod test {
    427     use std::str::FromStr as _;
    428 
    429     use crate::types::{
    430         amount::amount,
    431         iban::IBAN,
    432         payto::{FullPayto, Payto, TransferPayto},
    433     };
    434 
    435     #[test]
    436     pub fn parse() {
    437         let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap();
    438 
    439         // Simple payto
    440         let simple_payto = Payto::new(iban.clone());
    441         assert_eq!(
    442             simple_payto,
    443             Payto::from_str(&format!("payto://iban/{iban}")).unwrap()
    444         );
    445         assert_eq!(
    446             simple_payto,
    447             Payto::try_from(&simple_payto.as_payto()).unwrap()
    448         );
    449         assert_eq!(
    450             simple_payto,
    451             Payto::from_str(&simple_payto.as_payto().to_string()).unwrap()
    452         );
    453 
    454         // Full payto
    455         let full_payto = FullPayto::new(iban.clone(), "John Smith");
    456         assert_eq!(
    457             full_payto,
    458             FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap()
    459         );
    460         assert_eq!(
    461             full_payto,
    462             FullPayto::try_from(&full_payto.as_payto()).unwrap()
    463         );
    464         assert_eq!(
    465             full_payto,
    466             FullPayto::from_str(&full_payto.as_payto().to_string()).unwrap()
    467         );
    468         assert_eq!(simple_payto, full_payto.clone().into());
    469 
    470         // Transfer simple payto
    471         let transfer_payto = TransferPayto::new(iban.clone(), "John Smith", None, None);
    472         assert_eq!(
    473             transfer_payto,
    474             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith"))
    475                 .unwrap()
    476         );
    477         assert_eq!(
    478             transfer_payto,
    479             TransferPayto::try_from(&transfer_payto.as_payto()).unwrap()
    480         );
    481         assert_eq!(
    482             transfer_payto,
    483             TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap()
    484         );
    485         assert_eq!(full_payto, transfer_payto.clone().into());
    486 
    487         // Transfer full payto
    488         let transfer_payto = TransferPayto::new(
    489             iban.clone(),
    490             "John Smith",
    491             Some(amount("EUR:12")),
    492             Some("Wire transfer subject"),
    493         );
    494         assert_eq!(
    495             transfer_payto,
    496             TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject"))
    497                 .unwrap()
    498         );
    499         assert_eq!(
    500             transfer_payto,
    501             TransferPayto::try_from(&transfer_payto.as_payto()).unwrap()
    502         );
    503         assert_eq!(
    504             transfer_payto,
    505             TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap()
    506         );
    507         assert_eq!(full_payto, transfer_payto.clone().into());
    508 
    509         let malformed = FullPayto::<IBAN>::from_str(
    510             "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA",
    511         )
    512         .unwrap();
    513         assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557");
    514         assert_eq!(malformed.name, "NYM Technologies%SA");
    515     }
    516 }