contract.rs (12332B)
1 #![forbid(unsafe_code)] 2 3 #[cfg(all(not(feature = "std"), not(test)))] 4 use alloc::{string::String, vec::Vec}; 5 #[cfg(any(feature = "std", test))] 6 use std::{string::String, vec::Vec}; 7 8 use core::fmt; 9 10 /// Exact package version implementing this event contract surface. 11 pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 12 13 #[doc(hidden)] 14 pub mod registry_v7; 15 16 pub use registry_v7::*; 17 18 /// Maximum UTF-8 byte length of a versioned Radroots event-contract ID. 19 pub const CONTRACT_ID_MAX_BYTES: usize = 255; 20 21 /// A syntactically valid, bounded Radroots event-contract identifier. 22 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 23 pub struct ContractId(String); 24 25 impl ContractId { 26 pub fn parse(value: impl Into<String>) -> Result<Self, ContractIdentityError> { 27 let value = value.into(); 28 if value.is_empty() { 29 return Err(ContractIdentityError::ContractIdMissing); 30 } 31 if value.len() > CONTRACT_ID_MAX_BYTES { 32 return Err(ContractIdentityError::ContractIdTooLong { 33 max: CONTRACT_ID_MAX_BYTES, 34 actual: value.len(), 35 }); 36 } 37 if !value.starts_with("radroots.") { 38 return Err(ContractIdentityError::ContractIdNamespace); 39 } 40 let mut segments = value.split('.'); 41 let namespace = segments.next(); 42 let remaining = segments.collect::<Vec<_>>(); 43 let Some(version) = remaining.last() else { 44 return Err(ContractIdentityError::ContractIdSyntax); 45 }; 46 if namespace != Some("radroots") 47 || remaining.len() < 3 48 || remaining[..remaining.len() - 1].iter().any(|segment| { 49 segment.is_empty() 50 || !segment.bytes().all(|byte| { 51 byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' 52 }) 53 }) 54 || !version.strip_prefix('v').is_some_and(|number| { 55 !number.is_empty() 56 && !number.starts_with('0') 57 && number.bytes().all(|byte| byte.is_ascii_digit()) 58 }) 59 { 60 return Err(ContractIdentityError::ContractIdSyntax); 61 } 62 Ok(Self(value)) 63 } 64 65 #[must_use] 66 pub fn as_str(&self) -> &str { 67 self.0.as_str() 68 } 69 70 #[must_use] 71 pub fn into_string(self) -> String { 72 self.0 73 } 74 } 75 76 impl fmt::Display for ContractId { 77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 78 formatter.write_str(self.as_str()) 79 } 80 } 81 82 /// A nonzero immutable event-contract registry version. 83 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 84 pub struct RegistryVersion(u32); 85 86 impl RegistryVersion { 87 /// Frozen registry profile used by the initial authored-plan storage wire. 88 pub const V7: Self = Self(7); 89 pub const CURRENT: Self = Self(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION); 90 91 pub const fn new(value: u32) -> Result<Self, ContractIdentityError> { 92 if value == 0 { 93 return Err(ContractIdentityError::RegistryVersionZero); 94 } 95 Ok(Self(value)) 96 } 97 98 #[must_use] 99 pub const fn get(self) -> u32 { 100 self.0 101 } 102 } 103 104 impl fmt::Display for RegistryVersion { 105 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 106 write!(formatter, "{}", self.get()) 107 } 108 } 109 110 /// One event contract resolved against an immutable historical registry. 111 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 112 pub struct ContractKey { 113 registry_version: RegistryVersion, 114 contract_id: ContractId, 115 } 116 117 impl ContractKey { 118 pub fn current(contract_id: impl Into<String>) -> Result<Self, ContractIdentityError> { 119 Self::new(RegistryVersion::CURRENT, ContractId::parse(contract_id)?) 120 } 121 122 pub fn new( 123 registry_version: RegistryVersion, 124 contract_id: ContractId, 125 ) -> Result<Self, ContractIdentityError> { 126 let contract = match registry_version { 127 RegistryVersion::V7 => event_contract_registry_v7(contract_id.as_str()), 128 _ => { 129 return Err(ContractIdentityError::UnsupportedRegistryVersion { 130 actual: registry_version.get(), 131 }); 132 } 133 }; 134 if contract.is_none() { 135 return Err(ContractIdentityError::UnknownContract { 136 contract_id: contract_id.into_string(), 137 }); 138 } 139 Ok(Self { 140 registry_version, 141 contract_id, 142 }) 143 } 144 145 #[must_use] 146 pub const fn registry_version(&self) -> RegistryVersion { 147 self.registry_version 148 } 149 150 #[must_use] 151 pub const fn contract_id(&self) -> &ContractId { 152 &self.contract_id 153 } 154 155 #[must_use] 156 pub fn contract(&self) -> &'static EventContract { 157 match self.registry_version { 158 RegistryVersion::V7 => event_contract_registry_v7(self.contract_id.as_str()) 159 .expect("validated registry-v7 contract key must remain resolvable"), 160 _ => unreachable!("validated contract key must have a supported registry profile"), 161 } 162 } 163 } 164 165 #[derive(Clone, Debug, PartialEq, Eq)] 166 pub enum ContractIdentityError { 167 ContractIdMissing, 168 ContractIdTooLong { max: usize, actual: usize }, 169 ContractIdNamespace, 170 ContractIdSyntax, 171 RegistryVersionZero, 172 UnsupportedRegistryVersion { actual: u32 }, 173 UnknownContract { contract_id: String }, 174 } 175 176 impl ContractIdentityError { 177 #[must_use] 178 pub const fn code(&self) -> &'static str { 179 match self { 180 Self::ContractIdMissing => "contract_id_missing", 181 Self::ContractIdTooLong { .. } => "contract_id_too_long", 182 Self::ContractIdNamespace => "contract_id_namespace", 183 Self::ContractIdSyntax => "contract_id_syntax", 184 Self::RegistryVersionZero => "registry_version_zero", 185 Self::UnsupportedRegistryVersion { .. } => "unsupported_registry_version", 186 Self::UnknownContract { .. } => "unknown_contract", 187 } 188 } 189 } 190 191 impl fmt::Display for ContractIdentityError { 192 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 193 match self { 194 Self::ContractIdMissing => formatter.write_str("event contract ID must not be empty"), 195 Self::ContractIdTooLong { max, actual } => { 196 write!( 197 formatter, 198 "event contract ID is {actual} bytes; max is {max}" 199 ) 200 } 201 Self::ContractIdNamespace => { 202 formatter.write_str("event contract ID must use the `radroots.` namespace") 203 } 204 Self::ContractIdSyntax => { 205 formatter.write_str("event contract ID must be a canonical versioned identifier") 206 } 207 Self::RegistryVersionZero => { 208 formatter.write_str("event contract registry version must be nonzero") 209 } 210 Self::UnsupportedRegistryVersion { actual } => { 211 write!( 212 formatter, 213 "unsupported event contract registry version {actual}" 214 ) 215 } 216 Self::UnknownContract { contract_id } => { 217 write!(formatter, "unknown event contract `{contract_id}`") 218 } 219 } 220 } 221 } 222 223 #[cfg(feature = "std")] 224 impl std::error::Error for ContractIdentityError {} 225 226 #[cfg(any(feature = "serde", test))] 227 impl serde::Serialize for ContractId { 228 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 229 where 230 S: serde::Serializer, 231 { 232 serializer.serialize_str(self.as_str()) 233 } 234 } 235 236 #[cfg(any(feature = "serde", test))] 237 impl<'de> serde::Deserialize<'de> for ContractId { 238 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 239 where 240 D: serde::Deserializer<'de>, 241 { 242 let value = String::deserialize(deserializer)?; 243 Self::parse(value).map_err(serde::de::Error::custom) 244 } 245 } 246 247 #[cfg(any(feature = "serde", test))] 248 impl serde::Serialize for RegistryVersion { 249 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 250 where 251 S: serde::Serializer, 252 { 253 serializer.serialize_u32(self.get()) 254 } 255 } 256 257 #[cfg(any(feature = "serde", test))] 258 impl<'de> serde::Deserialize<'de> for RegistryVersion { 259 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 260 where 261 D: serde::Deserializer<'de>, 262 { 263 Self::new(u32::deserialize(deserializer)?).map_err(serde::de::Error::custom) 264 } 265 } 266 267 #[cfg(any(feature = "serde", test))] 268 impl serde::Serialize for ContractKey { 269 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 270 where 271 S: serde::Serializer, 272 { 273 use serde::ser::SerializeStruct; 274 275 let mut state = serializer.serialize_struct("ContractKey", 2)?; 276 state.serialize_field("registry_version", &self.registry_version)?; 277 state.serialize_field("contract_id", &self.contract_id)?; 278 state.end() 279 } 280 } 281 282 #[cfg(any(feature = "serde", test))] 283 impl<'de> serde::Deserialize<'de> for ContractKey { 284 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 285 where 286 D: serde::Deserializer<'de>, 287 { 288 #[derive(serde::Deserialize)] 289 #[serde(deny_unknown_fields)] 290 struct ContractKeySerde { 291 registry_version: RegistryVersion, 292 contract_id: ContractId, 293 } 294 295 let value = ContractKeySerde::deserialize(deserializer)?; 296 Self::new(value.registry_version, value.contract_id).map_err(serde::de::Error::custom) 297 } 298 } 299 300 #[cfg(test)] 301 mod identity_tests { 302 use super::*; 303 304 #[test] 305 fn contract_identity_values_are_bounded_canonical_and_resolved() { 306 let id = ContractId::parse("radroots.social.geochat.v1").expect("contract ID"); 307 assert_eq!(id.as_str(), "radroots.social.geochat.v1"); 308 assert_eq!(RegistryVersion::CURRENT.get(), 7); 309 assert_eq!(RegistryVersion::V7.get(), 7); 310 let key = ContractKey::new(RegistryVersion::CURRENT, id).expect("contract key"); 311 assert_eq!(key.contract().id, "radroots.social.geochat.v1"); 312 313 for invalid in [ 314 "", 315 "social.geochat.v1", 316 "radroots.social.Geochat.v1", 317 "radroots.social.geochat", 318 "radroots.social.geochat.v01", 319 "radroots..geochat.v1", 320 ] { 321 assert!(ContractId::parse(invalid).is_err(), "{invalid}"); 322 } 323 assert!(ContractId::parse(format!("radroots.{}.event.v1", "x".repeat(256))).is_err()); 324 assert_eq!( 325 RegistryVersion::new(0), 326 Err(ContractIdentityError::RegistryVersionZero) 327 ); 328 assert!(matches!( 329 ContractKey::new( 330 RegistryVersion::new(8).expect("nonzero version"), 331 ContractId::parse("radroots.social.geochat.v1").expect("contract ID") 332 ), 333 Err(ContractIdentityError::UnsupportedRegistryVersion { actual: 8 }) 334 )); 335 assert!(matches!( 336 ContractKey::current("radroots.social.unknown.v1"), 337 Err(ContractIdentityError::UnknownContract { .. }) 338 )); 339 } 340 341 #[test] 342 fn serde_reconstruction_revalidates_identity_and_registry_membership() { 343 let key = ContractKey::current("radroots.social.geochat.v1").expect("contract key"); 344 let value = serde_json::to_value(&key).expect("key JSON"); 345 assert_eq!( 346 serde_json::from_value::<ContractKey>(value.clone()).expect("decoded key"), 347 key 348 ); 349 350 let mut invalid = value.clone(); 351 invalid["registry_version"] = serde_json::json!(0); 352 assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); 353 let mut invalid = value.clone(); 354 invalid["registry_version"] = serde_json::json!(8); 355 assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); 356 let mut invalid = value.clone(); 357 invalid["contract_id"] = serde_json::json!("radroots.social.unknown.v1"); 358 assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); 359 let mut invalid = value; 360 invalid["unexpected"] = serde_json::json!(true); 361 assert!(serde_json::from_value::<ContractKey>(invalid).is_err()); 362 } 363 }