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