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.
at main 323 lines 12 kB view raw
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}