payto.rs (3635B)
1 /* 2 This file is part of TALER 3 Copyright (C) 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::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr}; 18 19 use compact_str::CompactString; 20 use taler_common::types::payto::{FullPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto}; 21 22 #[derive(Debug, Clone, PartialEq, Eq)] 23 pub struct CyclosAccount { 24 pub id: CyclosId, 25 pub root: CompactString, 26 } 27 28 #[derive( 29 Debug, Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay, 30 )] 31 pub struct CyclosId(pub i64); 32 33 impl Deref for CyclosId { 34 type Target = i64; 35 36 fn deref(&self) -> &Self::Target { 37 &self.0 38 } 39 } 40 41 impl Display for CyclosId { 42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 43 self.0.fmt(f) 44 } 45 } 46 47 impl FromStr for CyclosId { 48 type Err = ParseIntError; 49 50 fn from_str(s: &str) -> Result<Self, Self::Err> { 51 Ok(Self(i64::from_str(s)?)) 52 } 53 } 54 55 const CYCLOS: &str = "cyclos"; 56 57 impl PaytoImpl for CyclosAccount { 58 fn as_payto(&self) -> PaytoURI { 59 PaytoURI::from_parts(CYCLOS, format_args!("/{}/{}", self.root, self.id)) 60 } 61 62 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 63 let url = raw.as_ref(); 64 if url.domain() != Some(CYCLOS) { 65 return Err(PaytoErr::UnsupportedKind( 66 CYCLOS, 67 url.domain().unwrap_or_default().into(), 68 )); 69 } 70 let Some((root, id)) = url.path().trim_start_matches('/').rsplit_once('/') else { 71 return Err(PaytoErr::MissingSegment("cyclos account id")); 72 }; 73 74 Ok(CyclosAccount { 75 id: CyclosId::from_str(id) 76 .map_err(|e| PaytoErr::malformed_segment("cyclos account id", e))?, 77 root: CompactString::new(root), 78 }) 79 } 80 } 81 82 /// Parse a cyclos payto URI, panic if malformed 83 pub fn cyclos_payto(url: impl AsRef<str>) -> FullCyclosPayto { 84 url.as_ref().parse().expect("invalid cyclos payto") 85 } 86 87 // TODO should we check the root url ? 88 89 pub type CyclosPayto = Payto<CyclosAccount>; 90 pub type FullCyclosPayto = FullPayto<CyclosAccount>; 91 pub type TransferCyclosPayto = TransferPayto<CyclosAccount>; 92 93 #[cfg(test)] 94 mod test { 95 use crate::payto::cyclos_payto; 96 97 #[test] 98 pub fn parse() { 99 let simple = "payto://cyclos/demo.cyclos.org/7762070814194619199?receiver-name=John+Smith"; 100 let payto = cyclos_payto(simple); 101 102 assert_eq!(*payto.id, 7762070814194619199); 103 assert_eq!(payto.name, "John Smith"); 104 assert_eq!(payto.root, "demo.cyclos.org"); 105 106 assert_eq!(payto.to_string(), simple); 107 108 let complex = "payto://cyclos/communities.cyclos.org/utrecht/7762070814194619199?receiver-name=John+Smith"; 109 let payto = cyclos_payto(complex); 110 111 assert_eq!(*payto.id, 7762070814194619199); 112 assert_eq!(payto.name, "John Smith"); 113 assert_eq!(payto.root, "communities.cyclos.org/utrecht"); 114 115 assert_eq!(payto.to_string(), complex); 116 } 117 }