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 }