QuickDID is a high-performance AT Protocol identity resolution service written in Rust. It provides handle-to-DID resolution with Redis-backed caching and queue processing.
1//! Optimized structure for storing handle resolution results.
2//!
3//! This module provides a compact binary representation of handle resolution
4//! results using bincode serialization. The structure efficiently stores
5//! the resolution timestamp, DID method type, and the DID payload.
6
7use serde::{Deserialize, Serialize};
8use std::time::{SystemTime, UNIX_EPOCH};
9use thiserror::Error;
10
11/// Errors that can occur during handle resolution result operations
12#[derive(Debug, Error)]
13pub enum HandleResolutionError {
14 /// System time error when getting timestamp
15 #[error("error-quickdid-result-1 System time error: {0}")]
16 SystemTime(String),
17
18 /// Failed to serialize resolution result to binary format
19 #[error("error-quickdid-result-2 Failed to serialize resolution result: {0}")]
20 Serialization(String),
21
22 /// Failed to deserialize resolution result from binary format
23 #[error("error-quickdid-result-3 Failed to deserialize resolution result: {0}")]
24 Deserialization(String),
25}
26
27/// Represents the type of DID method from a resolution
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29#[repr(i16)]
30pub enum DidMethodType {
31 /// Handle was not resolved
32 NotResolved = 0,
33 /// DID uses the web method (did:web:...)
34 Web = 1,
35 /// DID uses the PLC method (did:plc:...)
36 Plc = 2,
37 /// DID uses another method
38 Other = 3,
39}
40
41/// Optimized structure for storing handle resolution results
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43pub struct HandleResolutionResult {
44 /// Unix timestamp (seconds since epoch) when the handle was resolved
45 pub timestamp: u64,
46 /// The type of DID method (stored as 4-byte integer)
47 pub method_type: DidMethodType,
48 /// The DID payload (without prefix for web/plc methods)
49 pub payload: String,
50}
51
52impl HandleResolutionResult {
53 /// Create a new resolution result for a successfully resolved handle
54 pub fn success(did: &str) -> Result<Self, HandleResolutionError> {
55 let timestamp = SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .map_err(|e| HandleResolutionError::SystemTime(e.to_string()))?
58 .as_secs();
59
60 let (method_type, payload) = Self::parse_did(did);
61
62 Ok(Self {
63 timestamp,
64 method_type,
65 payload,
66 })
67 }
68
69 /// Create a new resolution result for a failed resolution
70 pub fn not_resolved() -> Result<Self, HandleResolutionError> {
71 let timestamp = SystemTime::now()
72 .duration_since(UNIX_EPOCH)
73 .map_err(|e| HandleResolutionError::SystemTime(e.to_string()))?
74 .as_secs();
75
76 Ok(Self {
77 timestamp,
78 method_type: DidMethodType::NotResolved,
79 payload: String::new(),
80 })
81 }
82
83 /// Parse a DID string to extract method type and payload
84 fn parse_did(did: &str) -> (DidMethodType, String) {
85 if let Some(stripped) = did.strip_prefix("did:web:") {
86 (DidMethodType::Web, stripped.to_string())
87 } else if let Some(stripped) = did.strip_prefix("did:plc:") {
88 (DidMethodType::Plc, stripped.to_string())
89 } else if did.starts_with("did:") {
90 // Other DID method - store the full DID
91 (DidMethodType::Other, did.to_string())
92 } else {
93 // Not a valid DID format - treat as other
94 (DidMethodType::Other, did.to_string())
95 }
96 }
97
98 /// Reconstruct the full DID from the stored data
99 pub fn to_did(&self) -> Option<String> {
100 match self.method_type {
101 DidMethodType::NotResolved => None,
102 DidMethodType::Web => Some(format!("did:web:{}", self.payload)),
103 DidMethodType::Plc => Some(format!("did:plc:{}", self.payload)),
104 DidMethodType::Other => Some(self.payload.clone()),
105 }
106 }
107
108 /// Serialize the result to bytes using bincode
109 pub fn to_bytes(&self) -> Result<Vec<u8>, HandleResolutionError> {
110 bincode::serde::encode_to_vec(self, bincode::config::standard())
111 .map_err(|e| HandleResolutionError::Serialization(e.to_string()))
112 }
113
114 /// Deserialize from bytes using bincode
115 pub fn from_bytes(bytes: &[u8]) -> Result<Self, HandleResolutionError> {
116 bincode::serde::decode_from_slice(bytes, bincode::config::standard())
117 .map(|(result, _)| result)
118 .map_err(|e| HandleResolutionError::Deserialization(e.to_string()))
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 impl HandleResolutionResult {
127 /// Test-only helper to create a success result without error handling
128 fn success_unchecked(did: &str) -> Self {
129 let timestamp = SystemTime::now()
130 .duration_since(UNIX_EPOCH)
131 .expect("Time went backwards")
132 .as_secs();
133
134 let (method_type, payload) = Self::parse_did(did);
135
136 Self {
137 timestamp,
138 method_type,
139 payload,
140 }
141 }
142
143 /// Test-only helper to create a not resolved result without error handling
144 fn not_resolved_unchecked() -> Self {
145 let timestamp = SystemTime::now()
146 .duration_since(UNIX_EPOCH)
147 .expect("Time went backwards")
148 .as_secs();
149
150 Self {
151 timestamp,
152 method_type: DidMethodType::NotResolved,
153 payload: String::new(),
154 }
155 }
156
157 /// Test-only helper to create a result with a specific timestamp
158 fn with_timestamp(did: Option<&str>, timestamp: u64) -> Self {
159 match did {
160 Some(did) => {
161 let (method_type, payload) = Self::parse_did(did);
162 Self {
163 timestamp,
164 method_type,
165 payload,
166 }
167 }
168 None => Self {
169 timestamp,
170 method_type: DidMethodType::NotResolved,
171 payload: String::new(),
172 },
173 }
174 }
175 }
176
177 #[test]
178 fn test_parse_did_web() {
179 let did = "did:web:example.com";
180 let result = HandleResolutionResult::success_unchecked(did);
181 assert_eq!(result.method_type, DidMethodType::Web);
182 assert_eq!(result.payload, "example.com");
183 assert_eq!(result.to_did(), Some(did.to_string()));
184 }
185
186 #[test]
187 fn test_parse_did_plc() {
188 let did = "did:plc:abcdef123456";
189 let result = HandleResolutionResult::success_unchecked(did);
190 assert_eq!(result.method_type, DidMethodType::Plc);
191 assert_eq!(result.payload, "abcdef123456");
192 assert_eq!(result.to_did(), Some(did.to_string()));
193 }
194
195 #[test]
196 fn test_parse_did_other() {
197 let did = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK";
198 let result = HandleResolutionResult::success_unchecked(did);
199 assert_eq!(result.method_type, DidMethodType::Other);
200 assert_eq!(result.payload, did);
201 assert_eq!(result.to_did(), Some(did.to_string()));
202 }
203
204 #[test]
205 fn test_not_resolved() {
206 let result = HandleResolutionResult::not_resolved_unchecked();
207 assert_eq!(result.method_type, DidMethodType::NotResolved);
208 assert_eq!(result.payload, "");
209 assert_eq!(result.to_did(), None);
210 }
211
212 #[test]
213 fn test_serialization_deserialization() {
214 // Test did:web
215 let original =
216 HandleResolutionResult::with_timestamp(Some("did:web:example.com:user"), 1234567890);
217 let bytes = original.to_bytes().unwrap();
218 let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap();
219 assert_eq!(original, deserialized);
220 assert_eq!(deserialized.timestamp, 1234567890);
221 assert_eq!(deserialized.method_type, DidMethodType::Web);
222 assert_eq!(deserialized.payload, "example.com:user");
223
224 // Test did:plc
225 let original = HandleResolutionResult::with_timestamp(
226 Some("did:plc:z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr"),
227 9876543210,
228 );
229 let bytes = original.to_bytes().unwrap();
230 let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap();
231 assert_eq!(original, deserialized);
232 assert_eq!(deserialized.timestamp, 9876543210);
233 assert_eq!(deserialized.method_type, DidMethodType::Plc);
234 assert_eq!(
235 deserialized.payload,
236 "z2dmzD81cgPx8Vki7JbuuMmFYrWPgYoytykUZ3eyqht1j9Kbr"
237 );
238
239 // Test not resolved
240 let original = HandleResolutionResult::with_timestamp(None, 1111111111);
241 let bytes = original.to_bytes().unwrap();
242 let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap();
243 assert_eq!(original, deserialized);
244 assert_eq!(deserialized.timestamp, 1111111111);
245 assert_eq!(deserialized.method_type, DidMethodType::NotResolved);
246 assert_eq!(deserialized.payload, "");
247 }
248
249 #[test]
250 fn test_binary_size() {
251 // Test that serialized size is reasonable
252 let result =
253 HandleResolutionResult::with_timestamp(Some("did:plc:abcdef123456"), 1234567890);
254 let bytes = result.to_bytes().unwrap();
255 // Should be relatively compact: 8 bytes (u64) + 4 bytes (i32) + string length + metadata
256 assert!(
257 bytes.len() < 100,
258 "Serialized size is too large: {} bytes",
259 bytes.len()
260 );
261
262 // Verify the size for not resolved (should be minimal)
263 let not_resolved = HandleResolutionResult::not_resolved_unchecked();
264 let bytes = not_resolved.to_bytes().unwrap();
265 assert!(
266 bytes.len() < 50,
267 "Not resolved size is too large: {} bytes",
268 bytes.len()
269 );
270 }
271
272 #[test]
273 fn test_did_web_with_port() {
274 let did = "did:web:localhost:3000";
275 let result = HandleResolutionResult::success_unchecked(did);
276 assert_eq!(result.method_type, DidMethodType::Web);
277 assert_eq!(result.payload, "localhost:3000");
278 assert_eq!(result.to_did(), Some(did.to_string()));
279 }
280
281 #[test]
282 fn test_did_web_with_path() {
283 let did = "did:web:example.com:path:to:did";
284 let result = HandleResolutionResult::success_unchecked(did);
285 assert_eq!(result.method_type, DidMethodType::Web);
286 assert_eq!(result.payload, "example.com:path:to:did");
287 assert_eq!(result.to_did(), Some(did.to_string()));
288 }
289
290 #[test]
291 fn test_invalid_did_format() {
292 let did = "not-a-did";
293 let result = HandleResolutionResult::success_unchecked(did);
294 assert_eq!(result.method_type, DidMethodType::Other);
295 assert_eq!(result.payload, did);
296 assert_eq!(result.to_did(), Some(did.to_string()));
297 }
298
299 #[test]
300 fn test_round_trip_all_types() {
301 let test_cases = vec![
302 ("did:web:example.com", DidMethodType::Web, "example.com"),
303 ("did:plc:xyz789", DidMethodType::Plc, "xyz789"),
304 (
305 "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
306 DidMethodType::Other,
307 "did:key:z6Mkfriq1MqLBoPWecGoDLjguo1sB9brj6wT3qZ5BxkKpuP6",
308 ),
309 ];
310
311 for (did, expected_type, expected_payload) in test_cases {
312 let result = HandleResolutionResult::success_unchecked(did);
313 assert_eq!(result.method_type, expected_type);
314 assert_eq!(result.payload, expected_payload);
315
316 // Test serialization round-trip
317 let bytes = result.to_bytes().unwrap();
318 let deserialized = HandleResolutionResult::from_bytes(&bytes).unwrap();
319 assert_eq!(result, deserialized);
320 assert_eq!(deserialized.to_did(), Some(did.to_string()));
321 }
322 }
323}