taler-rust

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

payto.rs (13956B)


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