schema.rs (19904B)
1 //! Validated schema identities and deterministic module dispatch. 2 3 use alloc::{string::String, vec::Vec}; 4 use core::{fmt, str::FromStr}; 5 6 /// Maximum UTF-8 byte length accepted for a schema identifier. 7 pub const MAX_SCHEMA_ID_BYTES: usize = 255; 8 9 /// Passive metadata that preserves an externally governed schema identity. 10 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 11 #[cfg_attr(feature = "serde", serde(deny_unknown_fields))] 12 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 13 pub struct Metadata { 14 /// Historical generated type name bound by the schema contract. 15 pub type_name: &'static str, 16 /// Canonical schema identifier. 17 pub schema_id: &'static str, 18 /// Declared schema generation. 19 pub schema_version: u16, 20 } 21 22 /// A canonical, version-suffixed schema identifier. 23 /// 24 /// Schema identifiers contain one or more dot-separated namespace segments 25 /// followed by a positive canonical version segment such as `v1`. Namespace 26 /// segments begin with a lowercase ASCII letter and contain only lowercase 27 /// ASCII letters, digits, and underscores. 28 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 29 pub struct SchemaId { 30 value: String, 31 version: u16, 32 } 33 34 impl SchemaId { 35 /// Parses and validates a schema identifier. 36 pub fn parse(value: impl Into<String>) -> Result<Self, Error> { 37 let value = value.into(); 38 if value.is_empty() { 39 return Err(Error::EmptySchemaId); 40 } 41 if value.len() > MAX_SCHEMA_ID_BYTES { 42 return Err(Error::SchemaIdTooLong { 43 actual: value.len(), 44 max: MAX_SCHEMA_ID_BYTES, 45 }); 46 } 47 48 let (namespace, version_segment) = value 49 .rsplit_once('.') 50 .ok_or(Error::MissingSchemaNamespace)?; 51 if namespace.is_empty() { 52 return Err(Error::MissingSchemaNamespace); 53 } 54 for (index, segment) in namespace.split('.').enumerate() { 55 if !valid_namespace_segment(segment) { 56 return Err(Error::InvalidSchemaNamespaceSegment { index }); 57 } 58 } 59 60 let version = parse_version_segment(version_segment)?; 61 Ok(Self { value, version }) 62 } 63 64 /// Returns the canonical identifier text. 65 pub fn as_str(&self) -> &str { 66 self.value.as_str() 67 } 68 69 /// Returns the positive schema generation encoded by the final segment. 70 pub const fn version(&self) -> u16 { 71 self.version 72 } 73 } 74 75 impl AsRef<str> for SchemaId { 76 fn as_ref(&self) -> &str { 77 self.as_str() 78 } 79 } 80 81 impl fmt::Display for SchemaId { 82 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 83 formatter.write_str(self.as_str()) 84 } 85 } 86 87 impl FromStr for SchemaId { 88 type Err = Error; 89 90 fn from_str(value: &str) -> Result<Self, Self::Err> { 91 Self::parse(value) 92 } 93 } 94 95 impl TryFrom<&str> for SchemaId { 96 type Error = Error; 97 98 fn try_from(value: &str) -> Result<Self, Self::Error> { 99 Self::parse(value) 100 } 101 } 102 103 impl TryFrom<String> for SchemaId { 104 type Error = Error; 105 106 fn try_from(value: String) -> Result<Self, Self::Error> { 107 Self::parse(value) 108 } 109 } 110 111 /// A supported versioned module in the protocol package. 112 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 113 #[non_exhaustive] 114 pub enum ModuleVersion { 115 /// `capability::v1`. 116 CapabilityV1, 117 /// `error::v1`. 118 ErrorV1, 119 /// `event::v1`. 120 EventV1, 121 /// `runtime::v1`. 122 RuntimeV1, 123 /// `radrootsd::transport_publish::v5`. 124 RadrootsdTransportPublishV5, 125 } 126 127 impl ModuleVersion { 128 /// Every module generation supported by this package version. 129 pub const ALL: [Self; 5] = [ 130 Self::CapabilityV1, 131 Self::ErrorV1, 132 Self::EventV1, 133 Self::RuntimeV1, 134 Self::RadrootsdTransportPublishV5, 135 ]; 136 137 /// Returns the stable Rust module path relative to `radroots_protocol`. 138 pub const fn path(self) -> &'static str { 139 match self { 140 Self::CapabilityV1 => "capability::v1", 141 Self::ErrorV1 => "error::v1", 142 Self::EventV1 => "event::v1", 143 Self::RuntimeV1 => "runtime::v1", 144 Self::RadrootsdTransportPublishV5 => "radrootsd::transport_publish::v5", 145 } 146 } 147 148 /// Returns the explicit contract generation for the module. 149 pub const fn generation(self) -> u16 { 150 match self { 151 Self::CapabilityV1 | Self::ErrorV1 | Self::EventV1 | Self::RuntimeV1 => 1, 152 Self::RadrootsdTransportPublishV5 => 5, 153 } 154 } 155 } 156 157 /// One validated schema-to-module registration. 158 #[derive(Clone, Debug, Eq, PartialEq)] 159 pub struct Descriptor { 160 id: SchemaId, 161 module: ModuleVersion, 162 } 163 164 impl Descriptor { 165 /// Creates a registration after validating its schema identifier. 166 pub fn try_new(id: impl Into<String>, module: ModuleVersion) -> Result<Self, Error> { 167 Ok(Self { 168 id: SchemaId::parse(id)?, 169 module, 170 }) 171 } 172 173 /// Returns the schema identifier. 174 pub const fn id(&self) -> &SchemaId { 175 &self.id 176 } 177 178 /// Returns the module generation that owns the schema. 179 pub const fn module(&self) -> ModuleVersion { 180 self.module 181 } 182 } 183 184 /// A canonical registry of unique schema identifiers. 185 /// 186 /// Construction sorts descriptors by schema identifier so iteration and 187 /// lookup remain deterministic regardless of input order. 188 #[derive(Clone, Debug, Default, Eq, PartialEq)] 189 pub struct Registry { 190 descriptors: Vec<Descriptor>, 191 } 192 193 impl Registry { 194 /// Builds a canonical registry and rejects duplicate schema identifiers. 195 pub fn try_new(descriptors: impl IntoIterator<Item = Descriptor>) -> Result<Self, Error> { 196 let mut descriptors: Vec<_> = descriptors.into_iter().collect(); 197 descriptors.sort_by(|left, right| left.id.cmp(&right.id)); 198 199 for adjacent in descriptors.windows(2) { 200 if adjacent[0].id == adjacent[1].id { 201 return Err(Error::DuplicateSchemaId { 202 schema_id: adjacent[0].id.as_str().into(), 203 }); 204 } 205 } 206 207 Ok(Self { descriptors }) 208 } 209 210 /// Builds a registry from governed schema metadata and module ownership. 211 pub fn try_from_metadata( 212 entries: impl IntoIterator<Item = (Metadata, ModuleVersion)>, 213 ) -> Result<Self, Error> { 214 let descriptors = entries 215 .into_iter() 216 .map(|(metadata, module)| { 217 let descriptor = Descriptor::try_new(metadata.schema_id, module)?; 218 let encoded = descriptor.id().version(); 219 if metadata.schema_version != encoded { 220 return Err(Error::SchemaVersionMismatch { 221 schema_id: metadata.schema_id.into(), 222 declared: metadata.schema_version, 223 encoded, 224 }); 225 } 226 Ok(descriptor) 227 }) 228 .collect::<Result<Vec<_>, _>>()?; 229 Self::try_new(descriptors) 230 } 231 232 /// Returns the canonical descriptor sequence. 233 pub fn descriptors(&self) -> &[Descriptor] { 234 self.descriptors.as_slice() 235 } 236 237 /// Returns the number of registered schemas. 238 pub fn len(&self) -> usize { 239 self.descriptors.len() 240 } 241 242 /// Reports whether the registry contains no schemas. 243 pub fn is_empty(&self) -> bool { 244 self.descriptors.is_empty() 245 } 246 247 /// Returns the descriptor for an exact schema identifier. 248 pub fn descriptor(&self, id: &SchemaId) -> Option<&Descriptor> { 249 self.descriptors 250 .binary_search_by(|descriptor| descriptor.id.cmp(id)) 251 .ok() 252 .map(|index| &self.descriptors[index]) 253 } 254 255 /// Dispatches an exact schema identifier to its owning module generation. 256 pub fn module_for(&self, id: &SchemaId) -> Option<ModuleVersion> { 257 self.descriptor(id).map(Descriptor::module) 258 } 259 } 260 261 /// Builds the complete protocol V1 schema registry currently owned here. 262 pub fn protocol_v1_registry() -> Result<Registry, Error> { 263 let capability = crate::capability::v1::SCHEMAS 264 .iter() 265 .copied() 266 .map(|metadata| (metadata, ModuleVersion::CapabilityV1)); 267 let event = crate::event::v1::SCHEMAS 268 .iter() 269 .copied() 270 .map(|metadata| (metadata, ModuleVersion::EventV1)); 271 let error = crate::error::v1::SCHEMAS 272 .iter() 273 .copied() 274 .map(|metadata| (metadata, ModuleVersion::ErrorV1)); 275 let mut descriptors = Registry::try_from_metadata(capability.chain(event).chain(error))? 276 .descriptors() 277 .to_vec(); 278 descriptors.extend( 279 crate::runtime::v1::schema_registry()? 280 .descriptors() 281 .iter() 282 .cloned(), 283 ); 284 descriptors.extend( 285 crate::radrootsd::transport_publish::v5::schema_registry()? 286 .descriptors() 287 .iter() 288 .cloned(), 289 ); 290 Registry::try_new(descriptors) 291 } 292 293 /// Schema identity or registry validation failure. 294 #[derive(Clone, Debug, Eq, PartialEq)] 295 #[non_exhaustive] 296 pub enum Error { 297 /// The schema identifier is empty. 298 EmptySchemaId, 299 /// The schema identifier exceeds the byte-length limit. 300 SchemaIdTooLong { 301 /// Actual UTF-8 byte length. 302 actual: usize, 303 /// Maximum accepted UTF-8 byte length. 304 max: usize, 305 }, 306 /// The schema identifier does not include a namespace before its version. 307 MissingSchemaNamespace, 308 /// A namespace segment is empty or noncanonical. 309 InvalidSchemaNamespaceSegment { 310 /// Zero-based namespace segment index. 311 index: usize, 312 }, 313 /// The final segment is not a canonical positive `vN` generation. 314 InvalidSchemaVersion, 315 /// Metadata declares a generation different from the schema ID suffix. 316 SchemaVersionMismatch { 317 /// Canonical schema identifier. 318 schema_id: String, 319 /// Generation stored in metadata. 320 declared: u16, 321 /// Generation encoded in the schema ID. 322 encoded: u16, 323 }, 324 /// The registry contains an identifier more than once. 325 DuplicateSchemaId { 326 /// Duplicated canonical schema identifier. 327 schema_id: String, 328 }, 329 } 330 331 impl fmt::Display for Error { 332 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 333 match self { 334 Self::EmptySchemaId => formatter.write_str("schema id must not be empty"), 335 Self::SchemaIdTooLong { actual, max } => { 336 write!(formatter, "schema id length {actual} exceeds {max} bytes") 337 } 338 Self::MissingSchemaNamespace => { 339 formatter.write_str("schema id must contain a namespace and version") 340 } 341 Self::InvalidSchemaNamespaceSegment { index } => { 342 write!(formatter, "schema id namespace segment {index} is invalid") 343 } 344 Self::InvalidSchemaVersion => { 345 formatter.write_str("schema id version must be canonical positive vN") 346 } 347 Self::SchemaVersionMismatch { 348 schema_id, 349 declared, 350 encoded, 351 } => write!( 352 formatter, 353 "schema id {schema_id} encodes v{encoded} but metadata declares v{declared}" 354 ), 355 Self::DuplicateSchemaId { schema_id } => { 356 write!(formatter, "duplicate schema id {schema_id}") 357 } 358 } 359 } 360 } 361 362 #[cfg(feature = "std")] 363 impl std::error::Error for Error {} 364 365 fn valid_namespace_segment(segment: &str) -> bool { 366 let mut bytes = segment.bytes(); 367 matches!(bytes.next(), Some(b'a'..=b'z')) 368 && bytes.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_') 369 } 370 371 fn parse_version_segment(segment: &str) -> Result<u16, Error> { 372 let Some(digits) = segment.strip_prefix('v') else { 373 return Err(Error::InvalidSchemaVersion); 374 }; 375 if digits.is_empty() 376 || digits.starts_with('0') 377 || !digits.bytes().all(|byte| byte.is_ascii_digit()) 378 { 379 return Err(Error::InvalidSchemaVersion); 380 } 381 digits 382 .parse::<u16>() 383 .ok() 384 .filter(|version| *version > 0) 385 .ok_or(Error::InvalidSchemaVersion) 386 } 387 388 #[cfg(test)] 389 mod tests { 390 use alloc::{string::ToString, vec}; 391 use std::collections::BTreeSet; 392 393 use super::*; 394 395 #[test] 396 fn schema_id_parsing_accepts_existing_contract_shape() { 397 let id = SchemaId::parse("radroots.protocol.transport_kind.v1").expect("schema id"); 398 assert_eq!(id.as_str(), "radroots.protocol.transport_kind.v1"); 399 assert_eq!(id.version(), 1); 400 assert_eq!(id.to_string(), id.as_str()); 401 assert_eq!(id, id.as_str().parse().expect("FromStr schema id")); 402 assert_eq!( 403 SchemaId::try_from(id.as_str()).expect("borrowed conversion"), 404 id 405 ); 406 assert_eq!( 407 SchemaId::try_from(id.as_str().to_string()).expect("owned conversion"), 408 id 409 ); 410 assert_eq!(id.as_ref(), id.as_str()); 411 } 412 413 #[test] 414 fn schema_id_parsing_rejects_every_noncanonical_shape() { 415 let too_long = alloc::format!("{}.v1", "a".repeat(MAX_SCHEMA_ID_BYTES)); 416 for (value, expected) in [ 417 ("", Error::EmptySchemaId), 418 ("v1", Error::MissingSchemaNamespace), 419 (".v1", Error::MissingSchemaNamespace), 420 ( 421 "radroots..event.v1", 422 Error::InvalidSchemaNamespaceSegment { index: 1 }, 423 ), 424 ( 425 "Radroots.protocol.event.v1", 426 Error::InvalidSchemaNamespaceSegment { index: 0 }, 427 ), 428 ( 429 "radroots.protocol.event-name.v1", 430 Error::InvalidSchemaNamespaceSegment { index: 2 }, 431 ), 432 ("radroots.protocol.event", Error::InvalidSchemaVersion), 433 ("radroots.protocol.event.v0", Error::InvalidSchemaVersion), 434 ("radroots.protocol.event.v01", Error::InvalidSchemaVersion), 435 ( 436 "radroots.protocol.event.v65536", 437 Error::InvalidSchemaVersion, 438 ), 439 ] { 440 assert_eq!(SchemaId::parse(value), Err(expected), "{value}"); 441 } 442 assert_eq!( 443 SchemaId::parse(too_long), 444 Err(Error::SchemaIdTooLong { 445 actual: MAX_SCHEMA_ID_BYTES + 3, 446 max: MAX_SCHEMA_ID_BYTES, 447 }) 448 ); 449 } 450 451 #[test] 452 fn module_inventory_has_unique_paths_and_explicit_generations() { 453 let paths = ModuleVersion::ALL 454 .into_iter() 455 .map(ModuleVersion::path) 456 .collect::<BTreeSet<_>>(); 457 assert_eq!(paths.len(), ModuleVersion::ALL.len()); 458 assert_eq!(ModuleVersion::CapabilityV1.generation(), 1); 459 assert_eq!(ModuleVersion::ErrorV1.generation(), 1); 460 assert_eq!(ModuleVersion::EventV1.generation(), 1); 461 assert_eq!(ModuleVersion::RuntimeV1.generation(), 1); 462 assert_eq!(ModuleVersion::RadrootsdTransportPublishV5.generation(), 5); 463 } 464 465 #[test] 466 fn registry_is_canonical_unique_and_dispatches_exact_ids() { 467 let event = Descriptor::try_new( 468 "radroots.protocol.event_descriptor.v1", 469 ModuleVersion::EventV1, 470 ) 471 .expect("event descriptor"); 472 let capability = Descriptor::try_new( 473 "radroots.protocol.transport_kind.v1", 474 ModuleVersion::CapabilityV1, 475 ) 476 .expect("capability descriptor"); 477 let registry = Registry::try_new(vec![event, capability.clone()]).expect("registry"); 478 479 assert_eq!(registry.len(), 2); 480 assert!(!registry.is_empty()); 481 assert_eq!( 482 registry 483 .descriptors() 484 .iter() 485 .map(|descriptor| descriptor.id().as_str()) 486 .collect::<Vec<_>>(), 487 vec![ 488 "radroots.protocol.event_descriptor.v1", 489 "radroots.protocol.transport_kind.v1", 490 ] 491 ); 492 assert_eq!( 493 registry.module_for(capability.id()), 494 Some(ModuleVersion::CapabilityV1) 495 ); 496 let unknown = SchemaId::parse("radroots.protocol.unknown.v1").expect("unknown id"); 497 assert_eq!(registry.module_for(&unknown), None); 498 assert_eq!(registry.descriptor(&unknown), None); 499 assert!(Registry::default().is_empty()); 500 } 501 502 #[test] 503 fn registry_rejects_duplicate_schema_ids() { 504 let first = Descriptor::try_new( 505 "radroots.protocol.event_descriptor.v1", 506 ModuleVersion::EventV1, 507 ) 508 .expect("first descriptor"); 509 let second = Descriptor::try_new( 510 "radroots.protocol.event_descriptor.v1", 511 ModuleVersion::CapabilityV1, 512 ) 513 .expect("second descriptor"); 514 assert_eq!( 515 Registry::try_new(vec![first, second]), 516 Err(Error::DuplicateSchemaId { 517 schema_id: "radroots.protocol.event_descriptor.v1".into(), 518 }) 519 ); 520 } 521 522 #[test] 523 fn metadata_registry_rejects_version_mismatch() { 524 let metadata = Metadata { 525 type_name: "EventDescriptor", 526 schema_id: "radroots.protocol.event_descriptor.v1", 527 schema_version: 2, 528 }; 529 assert_eq!( 530 Registry::try_from_metadata([(metadata, ModuleVersion::EventV1)]), 531 Err(Error::SchemaVersionMismatch { 532 schema_id: metadata.schema_id.into(), 533 declared: 2, 534 encoded: 1, 535 }) 536 ); 537 } 538 539 #[test] 540 fn protocol_v1_registry_dispatches_all_migrated_schemas() { 541 let registry = protocol_v1_registry().expect("protocol V1 registry"); 542 assert_eq!(registry.len(), 7 + crate::runtime::v1::CATALOG.len() * 2); 543 for descriptor in registry.descriptors() { 544 let expected = if descriptor.id().as_str() 545 == crate::radrootsd::transport_publish::v5::API_VERSION 546 { 547 ModuleVersion::RadrootsdTransportPublishV5 548 } else if descriptor.id().as_str() == crate::error::v1::SCHEMA_ID { 549 ModuleVersion::ErrorV1 550 } else if descriptor.id().as_str().starts_with("radroots.runtime.") { 551 ModuleVersion::RuntimeV1 552 } else if descriptor.id().as_str().contains("event_descriptor") 553 || descriptor.id().as_str().contains("trade_state") 554 { 555 ModuleVersion::EventV1 556 } else { 557 ModuleVersion::CapabilityV1 558 }; 559 assert_eq!(registry.module_for(descriptor.id()), Some(expected)); 560 } 561 } 562 563 #[test] 564 fn schema_errors_have_stable_messages() { 565 let errors = [ 566 Error::EmptySchemaId, 567 Error::SchemaIdTooLong { 568 actual: 256, 569 max: 255, 570 }, 571 Error::MissingSchemaNamespace, 572 Error::InvalidSchemaNamespaceSegment { index: 2 }, 573 Error::InvalidSchemaVersion, 574 Error::SchemaVersionMismatch { 575 schema_id: "radroots.test.v1".into(), 576 declared: 2, 577 encoded: 1, 578 }, 579 Error::DuplicateSchemaId { 580 schema_id: "radroots.test.v1".into(), 581 }, 582 ]; 583 for error in errors { 584 assert!(!error.to_string().is_empty()); 585 } 586 587 for value in [ 588 "radroots.protocol.event.v", 589 "radroots.protocol.event.v-1", 590 "radroots.protocol.event.vx", 591 "radroots.protocol.event.v999999", 592 ] { 593 assert_eq!(SchemaId::parse(value), Err(Error::InvalidSchemaVersion)); 594 } 595 } 596 }