payto.rs (13904B)
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 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(String), 121 #[error("unsupported payto kind, expected {0} got {1}")] 122 UnsupportedKind(&'static str, String), 123 #[error("to much path segment for a {0} payto uri")] 124 TooLong(&'static str), 125 #[error(transparent)] 126 Custom(Box<dyn std::error::Error + Sync + Send + 'static>), 127 } 128 129 impl PaytoErr { 130 pub fn custom<E: std::error::Error + Sync + Send + 'static>(e: E) -> Self { 131 Self::Custom(Box::new(e)) 132 } 133 } 134 135 impl FromStr for PaytoURI { 136 type Err = PaytoErr; 137 138 fn from_str(s: &str) -> Result<Self, Self::Err> { 139 // Parse url 140 let url: Url = s.parse()?; 141 // Check scheme 142 if url.scheme() != "payto" { 143 return Err(PaytoErr::NotPayto(url.scheme().to_owned())); 144 } 145 Ok(Self(url)) 146 } 147 } 148 149 pub type IbanPayto = Payto<BankID>; 150 pub type FullIbanPayto = FullPayto<BankID>; 151 152 #[derive(Debug, Clone, PartialEq, Eq)] 153 pub struct BankID { 154 pub iban: IBAN, 155 pub bic: Option<BIC>, 156 } 157 158 const IBAN: &str = "iban"; 159 160 #[derive(Debug, thiserror::Error)] 161 #[error("missing IBAN in path")] 162 pub struct MissingIban; 163 164 impl PaytoImpl for BankID { 165 fn as_payto(&self) -> PaytoURI { 166 PaytoURI::from_parts(IBAN, format_args!("/{}", self.iban)) 167 } 168 169 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 170 let url = raw.as_ref(); 171 if url.domain() != Some(IBAN) { 172 return Err(PaytoErr::UnsupportedKind( 173 IBAN, 174 url.domain().unwrap_or_default().to_owned(), 175 )); 176 } 177 let Some(mut segments) = url.path_segments() else { 178 return Err(PaytoErr::custom(MissingIban)); 179 }; 180 let Some(first) = segments.next() else { 181 return Err(PaytoErr::custom(MissingIban)); 182 }; 183 let (iban, bic) = match segments.next() { 184 Some(second) => ( 185 second.parse().map_err(PaytoErr::custom)?, 186 Some(first.parse().map_err(PaytoErr::custom)?), 187 ), 188 None => (first.parse().map_err(PaytoErr::custom)?, None), 189 }; 190 191 Ok(Self { iban, bic }) 192 } 193 } 194 195 impl PaytoImpl for IBAN { 196 fn as_payto(&self) -> PaytoURI { 197 PaytoURI::from_parts(IBAN, format_args!("/{self}")) 198 } 199 200 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 201 let payto = BankID::parse(raw)?; 202 Ok(payto.iban) 203 } 204 } 205 206 /// Full payto query 207 #[derive(Debug, Clone, Deserialize)] 208 pub struct FullQuery { 209 #[serde(rename = "receiver-name")] 210 receiver_name: CompactString, 211 } 212 213 /// Transfer payto query 214 #[derive(Debug, Clone, Deserialize)] 215 pub struct TransferQuery { 216 #[serde(rename = "receiver-name")] 217 receiver_name: CompactString, 218 amount: Option<Amount>, 219 message: Option<CompactString>, 220 } 221 222 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 223 pub struct Payto<P>(P); 224 225 impl<P: PaytoImpl> Payto<P> { 226 pub fn new(inner: P) -> Self { 227 Self(inner) 228 } 229 230 pub fn as_payto(&self) -> PaytoURI { 231 self.0.as_payto() 232 } 233 234 pub fn into_inner(self) -> P { 235 self.0 236 } 237 } 238 239 impl<P: PaytoImpl> TryFrom<&PaytoURI> for Payto<P> { 240 type Error = PaytoErr; 241 242 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 243 Ok(Self(P::parse(value)?)) 244 } 245 } 246 247 impl<P: PaytoImpl> From<FullPayto<P>> for Payto<P> { 248 fn from(value: FullPayto<P>) -> Payto<P> { 249 Payto(value.inner) 250 } 251 } 252 253 impl<P: PaytoImpl> From<TransferPayto<P>> for Payto<P> { 254 fn from(value: TransferPayto<P>) -> Payto<P> { 255 Payto(value.inner) 256 } 257 } 258 259 impl<P: PaytoImpl> std::fmt::Display for Payto<P> { 260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 261 std::fmt::Display::fmt(&self.as_payto(), f) 262 } 263 } 264 265 impl<P: PaytoImpl> FromStr for Payto<P> { 266 type Err = PaytoErr; 267 268 fn from_str(s: &str) -> Result<Self, Self::Err> { 269 let payto: PaytoURI = s.parse()?; 270 Self::try_from(&payto) 271 } 272 } 273 274 impl<P: PaytoImpl> Deref for Payto<P> { 275 type Target = P; 276 277 fn deref(&self) -> &Self::Target { 278 &self.0 279 } 280 } 281 282 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 283 pub struct FullPayto<P> { 284 inner: P, 285 pub name: CompactString, 286 } 287 288 impl<P: PaytoImpl> FullPayto<P> { 289 pub fn new(inner: P, name: &str) -> Self { 290 Self { 291 inner, 292 name: CompactString::new(name), 293 } 294 } 295 296 pub fn as_payto(&self) -> PaytoURI { 297 self.inner.as_full_payto(&self.name) 298 } 299 300 pub fn into_inner(self) -> P { 301 self.inner 302 } 303 } 304 305 impl<P: PaytoImpl> TryFrom<&PaytoURI> for FullPayto<P> { 306 type Error = PaytoErr; 307 308 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 309 let payto = P::parse(value)?; 310 let query: FullQuery = value.query()?; 311 Ok(Self { 312 inner: payto, 313 name: query.receiver_name, 314 }) 315 } 316 } 317 318 impl<P: PaytoImpl> From<TransferPayto<P>> for FullPayto<P> { 319 fn from(value: TransferPayto<P>) -> FullPayto<P> { 320 FullPayto { 321 inner: value.inner, 322 name: value.name, 323 } 324 } 325 } 326 327 impl<P: PaytoImpl> std::fmt::Display for FullPayto<P> { 328 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 329 std::fmt::Display::fmt(&self.as_payto(), f) 330 } 331 } 332 333 impl<P: PaytoImpl> FromStr for FullPayto<P> { 334 type Err = PaytoErr; 335 336 fn from_str(s: &str) -> Result<Self, Self::Err> { 337 let raw: PaytoURI = s.parse()?; 338 Self::try_from(&raw) 339 } 340 } 341 342 impl<P: PaytoImpl> Deref for FullPayto<P> { 343 type Target = P; 344 345 fn deref(&self) -> &Self::Target { 346 &self.inner 347 } 348 } 349 350 #[derive(Debug, Clone, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)] 351 pub struct TransferPayto<P> { 352 inner: P, 353 pub name: CompactString, 354 pub amount: Option<Amount>, 355 pub subject: Option<CompactString>, 356 } 357 358 impl<P: PaytoImpl> TransferPayto<P> { 359 pub fn new(inner: P, name: &str, amount: Option<Amount>, subject: Option<&str>) -> Self { 360 Self { 361 inner, 362 name: CompactString::new(name), 363 amount, 364 subject: subject.map(CompactString::new), 365 } 366 } 367 368 pub fn as_payto(&self) -> PaytoURI { 369 self.inner 370 .as_transfer_payto(&self.name, self.amount.as_ref(), self.subject.as_deref()) 371 } 372 373 pub fn into_inner(self) -> P { 374 self.inner 375 } 376 } 377 378 impl<P: PaytoImpl> TryFrom<&PaytoURI> for TransferPayto<P> { 379 type Error = PaytoErr; 380 381 fn try_from(value: &PaytoURI) -> Result<Self, Self::Error> { 382 let payto = P::parse(value)?; 383 let query: TransferQuery = value.query()?; 384 Ok(Self { 385 inner: payto, 386 name: query.receiver_name, 387 amount: query.amount, 388 subject: query.message, 389 }) 390 } 391 } 392 393 impl<P: PaytoImpl> std::fmt::Display for TransferPayto<P> { 394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 395 std::fmt::Display::fmt(&self.as_payto(), f) 396 } 397 } 398 399 impl<P: PaytoImpl> FromStr for TransferPayto<P> { 400 type Err = PaytoErr; 401 402 fn from_str(s: &str) -> Result<Self, Self::Err> { 403 let raw: PaytoURI = s.parse()?; 404 Self::try_from(&raw) 405 } 406 } 407 408 impl<P: PaytoImpl> Deref for TransferPayto<P> { 409 type Target = P; 410 411 fn deref(&self) -> &Self::Target { 412 &self.inner 413 } 414 } 415 416 #[cfg(test)] 417 mod test { 418 use std::str::FromStr as _; 419 420 use crate::types::{ 421 amount::amount, 422 iban::IBAN, 423 payto::{FullPayto, Payto, TransferPayto}, 424 }; 425 426 #[test] 427 pub fn parse() { 428 let iban = IBAN::from_str("FR1420041010050500013M02606").unwrap(); 429 430 // Simple payto 431 let simple_payto = Payto::new(iban.clone()); 432 assert_eq!( 433 simple_payto, 434 Payto::from_str(&format!("payto://iban/{iban}")).unwrap() 435 ); 436 assert_eq!( 437 simple_payto, 438 Payto::try_from(&simple_payto.as_payto()).unwrap() 439 ); 440 assert_eq!( 441 simple_payto, 442 Payto::from_str(&simple_payto.as_payto().to_string()).unwrap() 443 ); 444 445 // Full payto 446 let full_payto = FullPayto::new(iban.clone(), "John Smith"); 447 assert_eq!( 448 full_payto, 449 FullPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")).unwrap() 450 ); 451 assert_eq!( 452 full_payto, 453 FullPayto::try_from(&full_payto.as_payto()).unwrap() 454 ); 455 assert_eq!( 456 full_payto, 457 FullPayto::from_str(&full_payto.as_payto().to_string()).unwrap() 458 ); 459 assert_eq!(simple_payto, full_payto.clone().into()); 460 461 // Transfer simple payto 462 let transfer_payto = TransferPayto::new(iban.clone(), "John Smith", None, None); 463 assert_eq!( 464 transfer_payto, 465 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith")) 466 .unwrap() 467 ); 468 assert_eq!( 469 transfer_payto, 470 TransferPayto::try_from(&transfer_payto.as_payto()).unwrap() 471 ); 472 assert_eq!( 473 transfer_payto, 474 TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap() 475 ); 476 assert_eq!(full_payto, transfer_payto.clone().into()); 477 478 // Transfer full payto 479 let transfer_payto = TransferPayto::new( 480 iban.clone(), 481 "John Smith", 482 Some(amount("EUR:12")), 483 Some("Wire transfer subject"), 484 ); 485 assert_eq!( 486 transfer_payto, 487 TransferPayto::from_str(&format!("payto://iban/{iban}?receiver-name=John+Smith&amount=EUR:12&message=Wire+transfer+subject")) 488 .unwrap() 489 ); 490 assert_eq!( 491 transfer_payto, 492 TransferPayto::try_from(&transfer_payto.as_payto()).unwrap() 493 ); 494 assert_eq!( 495 transfer_payto, 496 TransferPayto::from_str(&transfer_payto.as_payto().to_string()).unwrap() 497 ); 498 assert_eq!(full_payto, transfer_payto.clone().into()); 499 500 let malformed = FullPayto::<IBAN>::from_str( 501 "payto://iban/CH0400766000103138557?receiver-name=NYM%20Technologies%SA", 502 ) 503 .unwrap(); 504 assert_eq!(malformed.as_ref().to_string(), "CH0400766000103138557"); 505 assert_eq!(malformed.name, "NYM Technologies%SA"); 506 } 507 }