taler-rust

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

payto.rs (13579B)


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