payto.rs (3821B)
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 #[derive(Debug, thiserror::Error)] 48 #[error("malformed cyclos id: {0}")] 49 pub struct CyclosIdError(ParseIntError); 50 51 impl FromStr for CyclosId { 52 type Err = CyclosIdError; 53 54 fn from_str(s: &str) -> Result<Self, Self::Err> { 55 Ok(Self(i64::from_str(s).map_err(CyclosIdError)?)) 56 } 57 } 58 59 const CYCLOS: &str = "cyclos"; 60 61 #[derive(Debug, thiserror::Error)] 62 #[error("missing cyclos root and account id in path")] 63 pub struct MissingParts; 64 65 impl PaytoImpl for CyclosAccount { 66 fn as_payto(&self) -> PaytoURI { 67 PaytoURI::from_parts(CYCLOS, format_args!("/{}/{}", self.root, self.id)) 68 } 69 70 fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> { 71 let url = raw.as_ref(); 72 if url.domain() != Some(CYCLOS) { 73 return Err(PaytoErr::UnsupportedKind( 74 CYCLOS, 75 url.domain().unwrap_or_default().to_owned(), 76 )); 77 } 78 let Some((root, id)) = url.path().trim_start_matches("/").rsplit_once('/') else { 79 return Err(PaytoErr::custom(MissingParts)); 80 }; 81 82 Ok(CyclosAccount { 83 id: CyclosId::from_str(id).map_err(PaytoErr::custom)?, 84 root: CompactString::new(root), 85 }) 86 } 87 } 88 89 /// Parse a cyclos payto URI, panic if malformed 90 pub fn cyclos_payto(url: impl AsRef<str>) -> FullCyclosPayto { 91 url.as_ref().parse().expect("invalid cyclos payto") 92 } 93 94 // TODO should we check the root url ? 95 96 pub type CyclosPayto = Payto<CyclosAccount>; 97 pub type FullCyclosPayto = FullPayto<CyclosAccount>; 98 pub type TransferCyclosPayto = TransferPayto<CyclosAccount>; 99 100 #[cfg(test)] 101 mod test { 102 use crate::payto::cyclos_payto; 103 104 #[test] 105 pub fn parse() { 106 let simple = "payto://cyclos/demo.cyclos.org/7762070814194619199?receiver-name=John+Smith"; 107 let payto = cyclos_payto(simple); 108 109 assert_eq!(*payto.id, 7762070814194619199); 110 assert_eq!(payto.name, "John Smith"); 111 assert_eq!(payto.root, "demo.cyclos.org"); 112 113 assert_eq!(payto.to_string(), simple); 114 115 let complex = "payto://cyclos/communities.cyclos.org/utrecht/7762070814194619199?receiver-name=John+Smith"; 116 let payto = cyclos_payto(complex); 117 118 assert_eq!(*payto.id, 7762070814194619199); 119 assert_eq!(payto.name, "John Smith"); 120 assert_eq!(payto.root, "communities.cyclos.org/utrecht"); 121 122 assert_eq!(payto.to_string(), complex); 123 } 124 }