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