payto.rs (17173B)
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, DerefMut}, 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 full(self, name: &str) -> FullPayto<Self> { 40 FullPayto::new(self, name) 41 } 42 43 fn transfer( 44 self, 45 name: &str, 46 amount: Option<Amount>, 47 subject: Option<&str>, 48 ) -> TransferPayto<Self> { 49 TransferPayto::new(self, name, amount, subject) 50 } 51 52 fn as_uri(&self) -> PaytoURI; 53 fn as_full_uri(&self, name: &str) -> PaytoURI { 54 self.as_uri().as_full_payto(name) 55 } 56 fn as_transfer_uri( 57 &self, 58 name: &str, 59 amount: Option<&Amount>, 60 subject: Option<&str>, 61 ) -> PaytoURI { 62 self.as_uri().as_transfer_payto(name, amount, subject) 63 } 64 fn parse(uri: &PaytoURI) -> Result<Self, PaytoErr>; 65 } 66 67 /// A generic RFC 8905 payto URI 68 #[derive( 69 Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 70 )] 71 pub struct PaytoURI(Url); 72 73 impl PaytoURI { 74 pub fn raw(&self) -> &str { 75 self.0.as_str() 76 } 77 78 pub fn from_parts(domain: &str, path: impl Display) -> Self { 79 payto(format!("payto://{domain}{path}")) 80 } 81 82 pub fn as_full_payto(self, name: &str) -> PaytoURI { 83 self.with_query([("receiver-name", name)]) 84 } 85 86 pub fn as_transfer_payto( 87 self, 88 name: &str, 89 amount: Option<&Amount>, 90 subject: Option<&str>, 91 ) -> PaytoURI { 92 self.as_full_payto(name) 93 .with_query([("amount", amount)]) 94 .with_query([("message", subject)]) 95 } 96 97 pub fn query<Q: DeserializeOwned>(&self) -> Result<Q, PaytoErr> { 98 let query = self.0.query().unwrap_or_default().as_bytes(); 99 let de = serde_urlencoded::Deserializer::new(url::form_urlencoded::parse(query)); 100 serde_path_to_error::deserialize(de).map_err(PaytoErr::Query) 101 } 102 103 fn with_query(mut self, query: impl Serialize) -> Self { 104 let mut urlencoder = self.0.query_pairs_mut(); 105 query 106 .serialize(serde_urlencoded::Serializer::new(&mut urlencoder)) 107 .unwrap(); 108 let _ = urlencoder.finish(); 109 drop(urlencoder); 110 self 111 } 112 } 113 114 impl AsRef<Url> for PaytoURI { 115 fn as_ref(&self) -> &Url { 116 &self.0 117 } 118 } 119 120 impl std::fmt::Display for PaytoURI { 121 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 122 std::fmt::Display::fmt(self.raw(), f) 123 } 124 } 125 126 #[derive(Debug, thiserror::Error)] 127 pub enum PaytoErr { 128 #[error("invalid payto URI: {0}")] 129 Url(#[from] url::ParseError), 130 #[error("malformed payto URI query: {0}")] 131 Query(#[from] serde_path_to_error::Error<serde_urlencoded::de::Error>), 132 #[error("expected a payto URI got {0}")] 133 NotPayto(CompactString), 134 #[error("unsupported payto kind, expected {0} got {1}")] 135 UnsupportedKind(&'static str, CompactString), 136 #[error("to much path segment for a {0} payto uri")] 137 TooLong(&'static str), 138 #[error("missing segment {0} in path")] 139 MissingSegment(&'static str), 140 #[error("malformed segment {0}: {1}")] 141 MalformedSegment( 142 &'static str, 143 Box<dyn std::error::Error + Sync + Send + 'static>, 144 ), 145 } 146 147 impl PaytoErr { 148 pub fn malformed_segment<E: std::error::Error + Sync + Send + 'static>( 149 segment: &'static str, 150 e: E, 151 ) -> Self { 152 Self::MalformedSegment(segment, Box::new(e)) 153 } 154 } 155 156 impl FromStr for PaytoURI { 157 type Err = PaytoErr; 158 159 fn from_str(s: &str) -> Result<Self, Self::Err> { 160 // Parse url 161 let url: Url = s.parse()?; 162 // Check scheme 163 if url.scheme() != "payto" { 164 return Err(PaytoErr::NotPayto(url.scheme().into())); 165 } 166 Ok(Self(url)) 167 } 168 } 169 170 pub type IbanPayto = Payto<BankID>; 171 pub type FullIbanPayto = FullPayto<BankID>; 172 pub type TransferIbanPayto = TransferPayto<BankID>; 173 174 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 175 pub struct BankID { 176 pub iban: IBAN, 177 pub bic: Option<BIC>, 178 } 179 180 const IBAN: &str = "iban"; 181 182 impl PaytoImpl for BankID { 183 fn as_uri(&self) -> PaytoURI { 184 PaytoURI::from_parts( 185 IBAN, 186 format_args!( 187 "/{}", 188 std::fmt::from_fn(|f| { 189 if let Some(bic) = &self.bic { 190 write!(f, "{bic}/")?; 191 } 192 write!(f, "{}", self.iban) 193 }) 194 ), 195 ) 196 } 197 198 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 199 let url = raw.as_ref(); 200 if url.domain() != Some(IBAN) { 201 return Err(PaytoErr::UnsupportedKind( 202 IBAN, 203 url.domain().unwrap_or_default().into(), 204 )); 205 } 206 let Some(mut segments) = url.path_segments() else { 207 return Err(PaytoErr::MissingSegment("iban")); 208 }; 209 let Some(first) = segments.next() else { 210 return Err(PaytoErr::MissingSegment("iban")); 211 }; 212 let (iban, bic) = match segments.next() { 213 Some(second) => (second, Some(first)), 214 None => (first, None), 215 }; 216 217 Ok(Self { 218 iban: iban 219 .parse() 220 .map_err(|e| PaytoErr::malformed_segment("iban", e))?, 221 bic: bic 222 .map(|bic| { 223 bic.parse() 224 .map_err(|e| PaytoErr::malformed_segment("bic", e)) 225 }) 226 .transpose()?, 227 }) 228 } 229 } 230 231 impl PaytoImpl for IBAN { 232 fn as_uri(&self) -> PaytoURI { 233 PaytoURI::from_parts("iban", format_args!("/{self}")) 234 } 235 236 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 237 raw.as_ref().path_segments().unwrap_or("".split('/')); 238 let payto = BankID::parse(raw)?; 239 Ok(payto.iban) 240 } 241 } 242 243 /// Full payto query 244 #[derive(Debug, Clone, Deserialize)] 245 pub struct FullQuery { 246 #[serde(rename = "receiver-name")] 247 receiver_name: CompactString, 248 } 249 250 /// Transfer payto query 251 #[derive(Debug, Clone, Deserialize)] 252 pub struct TransferQuery { 253 #[serde(rename = "receiver-name")] 254 receiver_name: CompactString, 255 amount: Option<Amount>, 256 message: Option<CompactString>, 257 } 258 259 /// Parsed payto query 260 #[derive(Debug, Clone, Deserialize)] 261 pub struct ParsedQuery { 262 #[serde(rename = "receiver-name")] 263 receiver_name: Option<CompactString>, 264 amount: Option<Amount>, 265 message: Option<CompactString>, 266 } 267 268 #[derive(Debug, Clone, Copy, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 269 pub struct Payto<P> { 270 inner: P, 271 } 272 273 impl<P> Payto<P> { 274 pub fn convert<T: From<P>>(self) -> Payto<T> { 275 Payto { 276 inner: self.inner.into(), 277 } 278 } 279 } 280 281 impl<P: PaytoImpl> Payto<P> { 282 pub fn new(inner: P) -> Self { 283 Self { inner } 284 } 285 286 pub fn as_uri(&self) -> PaytoURI { 287 self.inner.as_uri() 288 } 289 290 pub fn into_inner(self) -> P { 291 self.inner 292 } 293 } 294 295 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { 296 type Error = PaytoErr; 297 298 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 299 Ok(Self::new(P::parse(value)?)) 300 } 301 } 302 303 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { 304 fn from(value: FullPayto<P>) -> Payto<P> { 305 Self::new(value.inner) 306 } 307 } 308 309 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { 310 fn from(value: TransferPayto<P>) -> Payto<P> { 311 Self::new(value.inner) 312 } 313 } 314 315 impl<P: PaytoImpl> std::fmt::Display for Payto<P> { 316 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 317 std::fmt::Display::fmt(&self.as_uri(), f) 318 } 319 } 320 321 impl<P: PaytoImpl> FromStr for Payto<P> { 322 type Err = PaytoErr; 323 324 fn from_str(s: &str) -> Result<Self, Self::Err> { 325 let payto: PaytoURI = s.parse()?; 326 Self::try_from(&payto) 327 } 328 } 329 330 impl<P: PaytoImpl> Deref for Payto<P> { 331 type Target = P; 332 333 fn deref(&self) -> &Self::Target { 334 &self.inner 335 } 336 } 337 338 impl<P: PaytoImpl> DerefMut for Payto<P> { 339 fn deref_mut(&mut self) -> &mut Self::Target { 340 &mut self.inner 341 } 342 } 343 344 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 345 pub struct FullPayto<P> { 346 inner: P, 347 pub name: CompactString, 348 } 349 350 impl<P: PaytoImpl> FullPayto<P> { 351 pub fn new(inner: P, name: &str) -> Self { 352 Self { 353 inner, 354 name: CompactString::new(name), 355 } 356 } 357 358 pub fn as_uri(&self) -> PaytoURI { 359 self.inner.as_full_uri(&self.name) 360 } 361 362 pub fn into_inner(self) -> P { 363 self.inner 364 } 365 } 366 367 impl<P> FullPayto<P> { 368 pub fn convert<T: From<P>>(self) -> FullPayto<T> { 369 FullPayto { 370 inner: self.inner.into(), 371 name: self.name, 372 } 373 } 374 } 375 376 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { 377 type Error = PaytoErr; 378 379 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 380 let payto = P::parse(value)?; 381 let query: FullQuery = value.query()?; 382 Ok(Self { 383 inner: payto, 384 name: query.receiver_name, 385 }) 386 } 387 } 388 389 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> { 390 fn from(value: TransferPayto<P>) -> FullPayto<P> { 391 FullPayto { 392 inner: value.inner, 393 name: value.name, 394 } 395 } 396 } 397 398 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> { 399 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 400 std::fmt::Display::fmt(&self.as_uri(), f) 401 } 402 } 403 404 impl<P: PaytoImpl> FromStr for FullPayto<P> { 405 type Err = PaytoErr; 406 407 fn from_str(s: &str) -> Result<Self, Self::Err> { 408 let raw: PaytoURI = s.parse()?; 409 Self::try_from(&raw) 410 } 411 } 412 413 impl<P: PaytoImpl> Deref for FullPayto<P> { 414 type Target = P; 415 416 fn deref(&self) -> &Self::Target { 417 &self.inner 418 } 419 } 420 421 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 422 pub struct TransferPayto<P> { 423 inner: P, 424 pub name: CompactString, 425 pub amount: Option<Amount>, 426 pub subject: Option<CompactString>, 427 } 428 429 impl<P: PaytoImpl> TransferPayto<P> { 430 pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self { 431 Self { 432 inner, 433 name: CompactString::new(name), 434 amount, 435 subject: subject.map(CompactString::new), 436 } 437 } 438 439 pub fn as_uri(&self) -> PaytoURI { 440 self.inner 441 .as_transfer_uri(&self.name, self.amount.as_ref(), self.subject.as_deref()) 442 } 443 444 pub fn into_inner(self) -> P { 445 self.inner 446 } 447 } 448 449 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { 450 type Error = PaytoErr; 451 452 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 453 let payto = P::parse(value)?; 454 let query: TransferQuery = value.query()?; 455 Ok(Self { 456 inner: payto, 457 name: query.receiver_name, 458 amount: query.amount, 459 subject: query.message, 460 }) 461 } 462 } 463 464 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { 465 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 466 std::fmt::Display::fmt(&self.as_uri(), f) 467 } 468 } 469 470 impl<P: PaytoImpl> FromStr for TransferPayto<P> { 471 type Err = PaytoErr; 472 473 fn from_str(s: &str) -> Result<Self, Self::Err> { 474 let raw: PaytoURI = s.parse()?; 475 Self::try_from(&raw) 476 } 477 } 478 479 impl<P: PaytoImpl> Deref for TransferPayto<P> { 480 type Target = P; 481 482 fn deref(&self) -> &Self::Target { 483 &self.inner 484 } 485 } 486 487 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 488 pub struct ParsedPayto<P> { 489 inner: P, 490 pub name: Option<CompactString>, 491 pub amount: Option<Amount>, 492 pub subject: Option<CompactString>, 493 } 494 495 impl<P: PaytoImpl> ParsedPayto<P> { 496 pub fn new( 497 inner: P, 498 name: Option<&str>, 499 amount: Option<Amount>, 500 subject: Option<&str>, 501 ) -> Self { 502 Self { 503 inner, 504 name: name.map(CompactString::new), 505 amount, 506 subject: subject.map(CompactString::new), 507 } 508 } 509 510 pub fn into_inner(self) -> P { 511 self.inner 512 } 513 } 514 515 impl<P: PaytoImpl> TryFrom<&PaytoURI> for ParsedPayto<P> { 516 type Error = PaytoErr; 517 518 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 519 let payto = P::parse(value)?; 520 let query: ParsedQuery = value.query()?; 521 Ok(Self { 522 inner: payto, 523 name: query.receiver_name, 524 amount: query.amount, 525 subject: query.message, 526 }) 527 } 528 } 529 530 impl<P: PaytoImpl> std::fmt::Display for ParsedPayto<P> { 531 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 532 std::fmt::Display::fmt(&self.as_uri(), f) 533 } 534 } 535 536 impl<P: PaytoImpl> FromStr for ParsedPayto<P> { 537 type Err = PaytoErr; 538 539 fn from_str(s: &str) -> Result<Self, Self::Err> { 540 let raw: PaytoURI = s.parse()?; 541 Self::try_from(&raw) 542 } 543 } 544 545 impl<P: PaytoImpl> Deref for ParsedPayto<P> { 546 type Target = P; 547 548 fn deref(&self) -> &Self::Target { 549 &self.inner 550 } 551 } 552 553 #[cfg(test)] 554 mod test { 555 use std::str::FromStr as _; 556 557 use crate::types::{ 558 amount::amount, 559 iban::IBAN, 560 payto::{FullPayto, Payto, TransferPayto}, 561 }; 562 563 #[test] 564 pub fn parse() { 565 let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); 566 567 // Simple payto 568 let simple_payto = Payto::new(iban); 569 assert_eq!( 570 simple_payto, 571 Payto::from_str(&format!("payto://iban/{iban}")).unwrap() 572 ); 573 assert_eq!( 574 simple_payto, 575 Payto::try_from(&simple_payto.as_uri()).unwrap() 576 ); 577 assert_eq!( 578 simple_payto, 579 Payto::from_str(&simple_payto.as_uri().to_string()).unwrap() 580 ); 581 582 // Full payto 583 let full_payto = FullPayto::new(iban, "John Smith"); 584 assert_eq!( 585 full_payto, 586 FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() 587 ); 588 assert_eq!( 589 full_payto, 590 FullPayto::try_from(&full_payto.as_uri()).unwrap() 591 ); 592 assert_eq!( 593 full_payto, 594 FullPayto::from_str(&full_payto.as_uri().to_string()).unwrap() 595 ); 596 assert_eq!(simple_payto, full_payto.clone().into()); 597 598 // Transfer simple payto 599 let transfer_payto = TransferPayto::new(iban, "John Smith", None, None); 600 assert_eq!( 601 transfer_payto, 602 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) 603 .unwrap() 604 ); 605 assert_eq!( 606 transfer_payto, 607 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 608 ); 609 assert_eq!( 610 transfer_payto, 611 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 612 ); 613 assert_eq!(full_payto, transfer_payto.clone().into()); 614 615 // Transfer full payto 616 let transfer_payto = TransferPayto::new( 617 iban, 618 "John Smith", 619 Some(amount("EUR:12")), 620 Some("Wire transfer subject"), 621 ); 622 assert_eq!( 623 transfer_payto, 624 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) 625 .unwrap() 626 ); 627 assert_eq!( 628 transfer_payto, 629 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 630 ); 631 assert_eq!( 632 transfer_payto, 633 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 634 ); 635 assert_eq!(full_payto, transfer_payto.clone().into()); 636 637 let malformed = FullPayto::<IBAN>::from_str( 638 "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA", 639 ) 640 .unwrap(); 641 assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557"); 642 assert_eq!(malformed.name, "NYM Technologies%SA"); 643 } 644 }