payto.rs (17092B)
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>(P); 270 271 impl<P> Payto<P> { 272 pub fn convert<T: From<P>>(self) -> Payto<T> { 273 Payto(self.0.into()) 274 } 275 } 276 277 impl<P: PaytoImpl> Payto<P> { 278 pub fn new(inner: P) -> Self { 279 Self(inner) 280 } 281 282 pub fn as_uri(&self) -> PaytoURI { 283 self.0.as_uri() 284 } 285 286 pub fn into_inner(self) -> P { 287 self.0 288 } 289 } 290 291 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { 292 type Error = PaytoErr; 293 294 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 295 Ok(Self(P::parse(value)?)) 296 } 297 } 298 299 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { 300 fn from(value: FullPayto<P>) -> Payto<P> { 301 Payto(value.inner) 302 } 303 } 304 305 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { 306 fn from(value: TransferPayto<P>) -> Payto<P> { 307 Payto(value.inner) 308 } 309 } 310 311 impl<P: PaytoImpl> std::fmt::Display for Payto<P> { 312 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 313 std::fmt::Display::fmt(&self.as_uri(), f) 314 } 315 } 316 317 impl<P: PaytoImpl> FromStr for Payto<P> { 318 type Err = PaytoErr; 319 320 fn from_str(s: &str) -> Result<Self, Self::Err> { 321 let payto: PaytoURI = s.parse()?; 322 Self::try_from(&payto) 323 } 324 } 325 326 impl<P: PaytoImpl> Deref for Payto<P> { 327 type Target = P; 328 329 fn deref(&self) -> &Self::Target { 330 &self.0 331 } 332 } 333 334 impl<P: PaytoImpl> DerefMut for Payto<P> { 335 fn deref_mut(&mut self) -> &mut Self::Target { 336 &mut self.0 337 } 338 } 339 340 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 341 pub struct FullPayto<P> { 342 inner: P, 343 pub name: CompactString, 344 } 345 346 impl<P: PaytoImpl> FullPayto<P> { 347 pub fn new(inner: P, name: &str) -> Self { 348 Self { 349 inner, 350 name: CompactString::new(name), 351 } 352 } 353 354 pub fn as_uri(&self) -> PaytoURI { 355 self.inner.as_full_uri(&self.name) 356 } 357 358 pub fn into_inner(self) -> P { 359 self.inner 360 } 361 } 362 363 impl<P> FullPayto<P> { 364 pub fn convert<T: From<P>>(self) -> FullPayto<T> { 365 FullPayto { 366 inner: self.inner.into(), 367 name: self.name, 368 } 369 } 370 } 371 372 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { 373 type Error = PaytoErr; 374 375 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 376 let payto = P::parse(value)?; 377 let query: FullQuery = value.query()?; 378 Ok(Self { 379 inner: payto, 380 name: query.receiver_name, 381 }) 382 } 383 } 384 385 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> { 386 fn from(value: TransferPayto<P>) -> FullPayto<P> { 387 FullPayto { 388 inner: value.inner, 389 name: value.name, 390 } 391 } 392 } 393 394 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> { 395 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 396 std::fmt::Display::fmt(&self.as_uri(), f) 397 } 398 } 399 400 impl<P: PaytoImpl> FromStr for FullPayto<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 FullPayto<P> { 410 type Target = P; 411 412 fn deref(&self) -> &Self::Target { 413 &self.inner 414 } 415 } 416 417 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 418 pub struct TransferPayto<P> { 419 inner: P, 420 pub name: CompactString, 421 pub amount: Option<Amount>, 422 pub subject: Option<CompactString>, 423 } 424 425 impl<P: PaytoImpl> TransferPayto<P> { 426 pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self { 427 Self { 428 inner, 429 name: CompactString::new(name), 430 amount, 431 subject: subject.map(CompactString::new), 432 } 433 } 434 435 pub fn as_uri(&self) -> PaytoURI { 436 self.inner 437 .as_transfer_uri(&self.name, self.amount.as_ref(), self.subject.as_deref()) 438 } 439 440 pub fn into_inner(self) -> P { 441 self.inner 442 } 443 } 444 445 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { 446 type Error = PaytoErr; 447 448 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 449 let payto = P::parse(value)?; 450 let query: TransferQuery = value.query()?; 451 Ok(Self { 452 inner: payto, 453 name: query.receiver_name, 454 amount: query.amount, 455 subject: query.message, 456 }) 457 } 458 } 459 460 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { 461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 462 std::fmt::Display::fmt(&self.as_uri(), f) 463 } 464 } 465 466 impl<P: PaytoImpl> FromStr for TransferPayto<P> { 467 type Err = PaytoErr; 468 469 fn from_str(s: &str) -> Result<Self, Self::Err> { 470 let raw: PaytoURI = s.parse()?; 471 Self::try_from(&raw) 472 } 473 } 474 475 impl<P: PaytoImpl> Deref for TransferPayto<P> { 476 type Target = P; 477 478 fn deref(&self) -> &Self::Target { 479 &self.inner 480 } 481 } 482 483 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 484 pub struct ParsedPayto<P> { 485 inner: P, 486 pub name: Option<CompactString>, 487 pub amount: Option<Amount>, 488 pub subject: Option<CompactString>, 489 } 490 491 impl<P: PaytoImpl> ParsedPayto<P> { 492 pub fn new( 493 inner: P, 494 name: Option<&str>, 495 amount: Option<Amount>, 496 subject: Option<&str>, 497 ) -> Self { 498 Self { 499 inner, 500 name: name.map(CompactString::new), 501 amount, 502 subject: subject.map(CompactString::new), 503 } 504 } 505 506 pub fn into_inner(self) -> P { 507 self.inner 508 } 509 } 510 511 impl<P: PaytoImpl> TryFrom<&PaytoURI> for ParsedPayto<P> { 512 type Error = PaytoErr; 513 514 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 515 let payto = P::parse(value)?; 516 let query: ParsedQuery = value.query()?; 517 Ok(Self { 518 inner: payto, 519 name: query.receiver_name, 520 amount: query.amount, 521 subject: query.message, 522 }) 523 } 524 } 525 526 impl<P: PaytoImpl> std::fmt::Display for ParsedPayto<P> { 527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 528 std::fmt::Display::fmt(&self.as_uri(), f) 529 } 530 } 531 532 impl<P: PaytoImpl> FromStr for ParsedPayto<P> { 533 type Err = PaytoErr; 534 535 fn from_str(s: &str) -> Result<Self, Self::Err> { 536 let raw: PaytoURI = s.parse()?; 537 Self::try_from(&raw) 538 } 539 } 540 541 impl<P: PaytoImpl> Deref for ParsedPayto<P> { 542 type Target = P; 543 544 fn deref(&self) -> &Self::Target { 545 &self.inner 546 } 547 } 548 549 #[cfg(test)] 550 mod test { 551 use std::str::FromStr as _; 552 553 use crate::types::{ 554 amount::amount, 555 iban::IBAN, 556 payto::{FullPayto, Payto, TransferPayto}, 557 }; 558 559 #[test] 560 pub fn parse() { 561 let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); 562 563 // Simple payto 564 let simple_payto = Payto::new(iban); 565 assert_eq!( 566 simple_payto, 567 Payto::from_str(&format!("payto://iban/{iban}")).unwrap() 568 ); 569 assert_eq!( 570 simple_payto, 571 Payto::try_from(&simple_payto.as_uri()).unwrap() 572 ); 573 assert_eq!( 574 simple_payto, 575 Payto::from_str(&simple_payto.as_uri().to_string()).unwrap() 576 ); 577 578 // Full payto 579 let full_payto = FullPayto::new(iban, "John Smith"); 580 assert_eq!( 581 full_payto, 582 FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() 583 ); 584 assert_eq!( 585 full_payto, 586 FullPayto::try_from(&full_payto.as_uri()).unwrap() 587 ); 588 assert_eq!( 589 full_payto, 590 FullPayto::from_str(&full_payto.as_uri().to_string()).unwrap() 591 ); 592 assert_eq!(simple_payto, full_payto.clone().into()); 593 594 // Transfer simple payto 595 let transfer_payto = TransferPayto::new(iban, "John Smith", None, None); 596 assert_eq!( 597 transfer_payto, 598 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) 599 .unwrap() 600 ); 601 assert_eq!( 602 transfer_payto, 603 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 604 ); 605 assert_eq!( 606 transfer_payto, 607 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 608 ); 609 assert_eq!(full_payto, transfer_payto.clone().into()); 610 611 // Transfer full payto 612 let transfer_payto = TransferPayto::new( 613 iban, 614 "John Smith", 615 Some(amount("EUR:12")), 616 Some("Wire transfer subject"), 617 ); 618 assert_eq!( 619 transfer_payto, 620 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) 621 .unwrap() 622 ); 623 assert_eq!( 624 transfer_payto, 625 TransferPayto::try_from(&transfer_payto.as_uri()).unwrap() 626 ); 627 assert_eq!( 628 transfer_payto, 629 TransferPayto::from_str(&transfer_payto.as_uri().to_string()).unwrap() 630 ); 631 assert_eq!(full_payto, transfer_payto.clone().into()); 632 633 let malformed = FullPayto::<IBAN>::from_str( 634 "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA", 635 ) 636 .unwrap(); 637 assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557"); 638 assert_eq!(malformed.name, "NYM Technologies%SA"); 639 } 640 }