taler-rust

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

payto.rs (13437B)


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