lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }