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