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