1use std::fmt::{self, Debug, Display, Formatter};
9
10use compact_str::CompactString;
11
12use crate::network::ChainId;
13
14pub trait SchemeId {
16 fn namespace(&self) -> &str;
18 fn scheme(&self) -> &str;
20 fn caip_family(&self) -> String {
22 format!("{}:*", self.namespace())
23 }
24 fn id(&self) -> String {
26 format!("{}-{}", self.namespace(), self.scheme())
27 }
28}
29
30#[derive(Debug, PartialEq, Eq, Hash, Clone)]
32pub struct SchemeSlug {
33 pub chain_id: ChainId,
35 pub name: CompactString,
37}
38
39impl SchemeSlug {
40 #[must_use]
42 pub const fn new(chain_id: ChainId, name: CompactString) -> Self {
43 Self { chain_id, name }
44 }
45
46 #[must_use]
48 pub fn as_wildcard(&self) -> Self {
49 Self {
50 chain_id: ChainId::new(self.chain_id.namespace(), "*"),
51 name: self.name.clone(),
52 }
53 }
54
55 #[must_use]
57 pub fn is_wildcard(&self) -> bool {
58 self.chain_id.reference() == "*"
59 }
60}
61
62impl Display for SchemeSlug {
63 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
64 write!(
65 f,
66 "{}:{}:{}",
67 self.chain_id.namespace(),
68 self.chain_id.reference(),
69 self.name,
70 )
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
76#[error("expected '{expected}', got '{got}'")]
77pub struct SchemeMarkerError {
78 pub expected: &'static str,
80 pub got: String,
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86pub struct ExactScheme;
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub struct UptoScheme;
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
94pub struct BatchSettlementScheme;
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub struct AuthCaptureScheme;
99
100macro_rules! impl_scheme_marker {
101 ($ty:ty, $value:literal) => {
102 impl $ty {
103 pub const VALUE: &'static str = $value;
105 }
106
107 impl std::fmt::Display for $ty {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 f.write_str(Self::VALUE)
110 }
111 }
112
113 impl AsRef<str> for $ty {
114 fn as_ref(&self) -> &str {
115 Self::VALUE
116 }
117 }
118
119 impl std::str::FromStr for $ty {
120 type Err = SchemeMarkerError;
121 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 if s == Self::VALUE {
123 Ok(Self)
124 } else {
125 Err(SchemeMarkerError {
126 expected: Self::VALUE,
127 got: s.to_owned(),
128 })
129 }
130 }
131 }
132
133 impl serde::Serialize for $ty {
134 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
135 serializer.serialize_str(Self::VALUE)
136 }
137 }
138
139 impl<'de> serde::Deserialize<'de> for $ty {
140 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
141 let s = <std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
142 let s = s.as_ref();
143 if s == Self::VALUE {
144 Ok(Self)
145 } else {
146 Err(serde::de::Error::custom(format!(
147 "expected '{}', got '{s}'",
148 Self::VALUE
149 )))
150 }
151 }
152 }
153 };
154}
155
156impl_scheme_marker!(ExactScheme, "exact");
157impl_scheme_marker!(UptoScheme, "upto");
158impl_scheme_marker!(BatchSettlementScheme, "batch-settlement");
159impl_scheme_marker!(AuthCaptureScheme, "auth-capture");
160
161#[cfg(test)]
162#[allow(clippy::unwrap_used, reason = "unit tests panic on assertion failure")]
163mod marker_tests {
164 use super::*;
165
166 #[test]
167 fn exact_serde_roundtrip() {
168 let encoded = serde_json::to_string(&ExactScheme).unwrap();
169 assert_eq!(encoded, r#""exact""#);
170 let decoded: ExactScheme = serde_json::from_str(&encoded).unwrap();
171 assert_eq!(decoded, ExactScheme);
172 }
173
174 #[test]
175 fn upto_serde_roundtrip() {
176 let encoded = serde_json::to_string(&UptoScheme).unwrap();
177 assert_eq!(encoded, r#""upto""#);
178 let decoded: UptoScheme = serde_json::from_str(&encoded).unwrap();
179 assert_eq!(decoded, UptoScheme);
180 }
181
182 #[test]
183 fn batch_settlement_serde_roundtrip() {
184 let encoded = serde_json::to_string(&BatchSettlementScheme).unwrap();
185 assert_eq!(encoded, r#""batch-settlement""#);
186 let decoded: BatchSettlementScheme = serde_json::from_str(&encoded).unwrap();
187 assert_eq!(decoded, BatchSettlementScheme);
188 }
189
190 #[test]
191 fn auth_capture_serde_roundtrip() {
192 let encoded = serde_json::to_string(&AuthCaptureScheme).unwrap();
193 assert_eq!(encoded, r#""auth-capture""#);
194 let decoded: AuthCaptureScheme = serde_json::from_str(&encoded).unwrap();
195 assert_eq!(decoded, AuthCaptureScheme);
196 }
197
198 #[test]
199 fn wrong_scheme_rejected() {
200 assert!(serde_json::from_str::<ExactScheme>(r#""upto""#).is_err());
201 assert!(serde_json::from_str::<UptoScheme>(r#""exact""#).is_err());
202 assert!(serde_json::from_str::<BatchSettlementScheme>(r#""exact""#).is_err());
203 assert!(serde_json::from_str::<AuthCaptureScheme>(r#""batch-settlement""#).is_err());
204 }
205
206 #[test]
207 fn markers_decode_from_owned_value() {
208 let exact: ExactScheme = serde_json::from_value(serde_json::json!("exact")).unwrap();
209 assert_eq!(exact, ExactScheme);
210 let upto: UptoScheme = serde_json::from_value(serde_json::json!("upto")).unwrap();
211 assert_eq!(upto, UptoScheme);
212 let batch: BatchSettlementScheme =
213 serde_json::from_value(serde_json::json!("batch-settlement")).unwrap();
214 assert_eq!(batch, BatchSettlementScheme);
215 let auth: AuthCaptureScheme =
216 serde_json::from_value(serde_json::json!("auth-capture")).unwrap();
217 assert_eq!(auth, AuthCaptureScheme);
218 assert!(serde_json::from_value::<ExactScheme>(serde_json::json!("upto")).is_err());
219 }
220
221 #[test]
222 fn slug_display_and_wildcard() {
223 let slug = SchemeSlug::new(ChainId::new("eip155", "8453"), "exact".into());
224 assert_eq!(slug.to_string(), "eip155:8453:exact");
225 let wild = slug.as_wildcard();
226 assert!(wild.is_wildcard());
227 assert_eq!(wild.to_string(), "eip155:*:exact");
228 }
229}