service.rs (24272B)
1 //! Generic, bounded detailed-status envelope. 2 3 use core::{fmt, time::Duration}; 4 use std::io::{self, Write}; 5 6 use radroots_runtime_paths::{InstanceId, ServiceId}; 7 use serde::{Serialize, ser::SerializeMap}; 8 9 use crate::BuildInfo; 10 11 use super::ServiceOperationalState; 12 13 pub const SERVICE_STATUS_CONTRACT_VERSION: u32 = 1; 14 pub const CONFIGURATION_SCHEMA_VERSION: u32 = 1; 15 pub const SERVICE_STATUS_MAX_UTF8_BYTES: usize = 1_048_576; 16 pub const STATUS_ID_MAX_BYTES: usize = 128; 17 18 /// A bounded identifier matching the frozen operator-contract grammar. 19 #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] 20 #[serde(transparent)] 21 pub struct StatusId(String); 22 23 impl StatusId { 24 pub fn new(value: impl AsRef<str>) -> Result<Self, StatusModelError> { 25 let value = value.as_ref(); 26 if !valid_status_id(value) { 27 return Err(StatusModelError::InvalidStatusId); 28 } 29 Ok(Self(value.to_owned())) 30 } 31 32 #[must_use] 33 pub fn as_str(&self) -> &str { 34 &self.0 35 } 36 } 37 38 /// A lowercase SHA-256 digest without a wire prefix. 39 #[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize)] 40 #[serde(transparent)] 41 pub struct Sha256Digest(String); 42 43 impl Sha256Digest { 44 pub fn new(value: impl AsRef<str>) -> Result<Self, StatusModelError> { 45 let value = value.as_ref(); 46 if value.len() != 64 47 || !value 48 .bytes() 49 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f')) 50 { 51 return Err(StatusModelError::InvalidSha256Digest); 52 } 53 Ok(Self(value.to_owned())) 54 } 55 56 #[must_use] 57 pub fn as_str(&self) -> &str { 58 &self.0 59 } 60 } 61 62 /// Source of the effective service configuration. 63 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 64 #[serde(rename_all = "snake_case")] 65 pub enum ConfigurationSource { 66 ExplicitConfig, 67 DerivedRepoLocal, 68 } 69 70 /// Safe identity of the effective configuration document. 71 #[derive(Clone, Debug, PartialEq, Eq, Serialize)] 72 pub struct ConfigurationIdentity { 73 schema: StatusId, 74 schema_version: u32, 75 digest: Sha256Digest, 76 source: ConfigurationSource, 77 } 78 79 impl ConfigurationIdentity { 80 /// Derives the exact v1 configuration-schema identity from the validated service. 81 pub fn for_service( 82 service: &ServiceId, 83 digest: Sha256Digest, 84 source: ConfigurationSource, 85 ) -> Result<Self, StatusModelError> { 86 let schema = StatusId::new(format!("radroots.{service}.config"))?; 87 Ok(Self { 88 schema, 89 schema_version: CONFIGURATION_SCHEMA_VERSION, 90 digest, 91 source, 92 }) 93 } 94 95 fn validate_for_service(&self, service: &ServiceId) -> Result<(), StatusModelError> { 96 validate_configuration_binding(service, &self.schema, self.schema_version) 97 } 98 } 99 100 /// Common persistence health classification. 101 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 102 #[serde(rename_all = "snake_case")] 103 pub enum PersistenceHealth { 104 Ready, 105 ReadOnly, 106 RepairRequired, 107 Unavailable, 108 } 109 110 /// Common persistence integrity state. 111 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 112 #[serde(rename_all = "snake_case")] 113 pub enum IntegrityState { 114 Verified, 115 VerificationRequired, 116 Failed, 117 } 118 119 /// Shared persistence-status fields; service stores retain detailed policy. 120 #[derive(Clone, Debug, PartialEq, Eq, Serialize)] 121 pub struct PersistenceSummary { 122 health: PersistenceHealth, 123 schema_version: u32, 124 generation: u64, 125 integrity: IntegrityState, 126 reason_codes: super::ReasonCodes, 127 } 128 129 impl PersistenceSummary { 130 pub fn new( 131 health: PersistenceHealth, 132 schema_version: u32, 133 generation: u64, 134 integrity: IntegrityState, 135 reason_codes: super::ReasonCodes, 136 ) -> Result<Self, StatusModelError> { 137 if schema_version == 0 { 138 return Err(StatusModelError::InvalidSchemaVersion); 139 } 140 Ok(Self { 141 health, 142 schema_version, 143 generation, 144 integrity, 145 reason_codes, 146 }) 147 } 148 } 149 150 /// Common provider-health classification used inside service-owned summaries. 151 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 152 #[serde(rename_all = "snake_case")] 153 pub enum ProviderHealth { 154 Ready, 155 Degraded, 156 Unavailable, 157 } 158 159 /// Common transport-health classification used inside service-owned summaries. 160 #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] 161 #[serde(rename_all = "snake_case")] 162 pub enum TransportHealth { 163 Ready, 164 Degraded, 165 Unavailable, 166 } 167 168 /// Monotonic process uptime in exact whole milliseconds. 169 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] 170 #[serde(transparent)] 171 pub struct UptimeMillis(u64); 172 173 impl UptimeMillis { 174 pub fn from_duration(duration: Duration) -> Result<Self, StatusModelError> { 175 u64::try_from(duration.as_millis()) 176 .map(Self) 177 .map_err(|_| StatusModelError::UptimeOverflow) 178 } 179 180 #[must_use] 181 pub const fn get(self) -> u64 { 182 self.0 183 } 184 } 185 186 /// Connects typed service detail with service-owned provider and transport summaries. 187 pub trait ServiceStatusDetail: Serialize { 188 type Provider: Serialize; 189 type Transport: Serialize; 190 191 /// Frozen detail field. It must equal the service identifier. 192 const FIELD_NAME: &'static str; 193 } 194 195 /// Shared detailed-status envelope with service-owned typed detail. 196 pub struct ServiceStatus<D> 197 where 198 D: ServiceStatusDetail, 199 { 200 service: ServiceId, 201 instance: InstanceId, 202 state: ServiceOperationalState, 203 uptime: UptimeMillis, 204 build: BuildInfo, 205 configuration: ConfigurationIdentity, 206 persistence: PersistenceSummary, 207 provider: D::Provider, 208 transport: D::Transport, 209 detail: D, 210 } 211 212 impl<D> ServiceStatus<D> 213 where 214 D: ServiceStatusDetail, 215 { 216 #[allow(clippy::too_many_arguments)] 217 pub fn new( 218 service: ServiceId, 219 instance: InstanceId, 220 state: ServiceOperationalState, 221 uptime: UptimeMillis, 222 build: BuildInfo, 223 configuration: ConfigurationIdentity, 224 persistence: PersistenceSummary, 225 provider: D::Provider, 226 transport: D::Transport, 227 detail: D, 228 ) -> Result<Self, StatusModelError> { 229 if D::FIELD_NAME != service.as_str() || !valid_status_detail_field(D::FIELD_NAME) { 230 return Err(StatusModelError::InvalidDetailField); 231 } 232 configuration.validate_for_service(&service)?; 233 Ok(Self { 234 service, 235 instance, 236 state, 237 uptime, 238 build, 239 configuration, 240 persistence, 241 provider, 242 transport, 243 detail, 244 }) 245 } 246 247 #[must_use] 248 pub fn service(&self) -> &ServiceId { 249 &self.service 250 } 251 252 #[must_use] 253 pub fn instance(&self) -> &InstanceId { 254 &self.instance 255 } 256 257 #[must_use] 258 pub fn state(&self) -> &ServiceOperationalState { 259 &self.state 260 } 261 262 /// Serializes without allowing the response buffer to exceed the frozen 1 MiB ceiling. 263 pub fn to_bounded_json(&self) -> Result<Vec<u8>, StatusEncodingError> { 264 let mut writer = BoundedWriter::new(SERVICE_STATUS_MAX_UTF8_BYTES); 265 match serde_json::to_writer(&mut writer, self) { 266 Ok(()) => Ok(writer.bytes), 267 Err(_) if writer.exceeded => Err(StatusEncodingError::ResponseTooLarge), 268 Err(_) => Err(StatusEncodingError::EncodingFailed), 269 } 270 } 271 } 272 273 impl<D> Serialize for ServiceStatus<D> 274 where 275 D: ServiceStatusDetail, 276 { 277 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 278 where 279 S: serde::Serializer, 280 { 281 let mut map = serializer.serialize_map(Some(13))?; 282 map.serialize_entry("contract_version", &SERVICE_STATUS_CONTRACT_VERSION)?; 283 map.serialize_entry("service", &self.service)?; 284 map.serialize_entry("instance", &self.instance)?; 285 map.serialize_entry("phase", &self.state.phase())?; 286 map.serialize_entry("ready", &self.state.readiness())?; 287 map.serialize_entry("uptime_millis", &self.uptime)?; 288 map.serialize_entry("reason_codes", self.state.reasons())?; 289 map.serialize_entry("build_info", &self.build.status_projection())?; 290 map.serialize_entry("configuration", &self.configuration)?; 291 map.serialize_entry("persistence", &self.persistence)?; 292 map.serialize_entry("provider", &self.provider)?; 293 map.serialize_entry("transport", &self.transport)?; 294 map.serialize_entry(D::FIELD_NAME, &self.detail)?; 295 map.end() 296 } 297 } 298 299 /// Validation failure for shared detailed-status models. 300 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 301 pub enum StatusModelError { 302 InvalidStatusId, 303 InvalidSha256Digest, 304 InvalidSchemaVersion, 305 ConfigurationServiceMismatch, 306 InvalidDetailField, 307 UptimeOverflow, 308 } 309 310 impl fmt::Display for StatusModelError { 311 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 312 formatter.write_str(match self { 313 Self::InvalidStatusId => "status identifier is invalid", 314 Self::InvalidSha256Digest => "status digest is invalid", 315 Self::InvalidSchemaVersion => "status schema version is invalid", 316 Self::ConfigurationServiceMismatch => "configuration schema does not match the service", 317 Self::InvalidDetailField => "status detail field is invalid", 318 Self::UptimeOverflow => "status uptime exceeds its u64 millisecond representation", 319 }) 320 } 321 } 322 323 impl std::error::Error for StatusModelError {} 324 325 /// Safe result category for bounded JSON encoding. 326 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 327 pub enum StatusEncodingError { 328 EncodingFailed, 329 ResponseTooLarge, 330 } 331 332 impl fmt::Display for StatusEncodingError { 333 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 334 formatter.write_str(match self { 335 Self::EncodingFailed => "status response encoding failed", 336 Self::ResponseTooLarge => "status response exceeds its byte limit", 337 }) 338 } 339 } 340 341 impl std::error::Error for StatusEncodingError {} 342 343 struct BoundedWriter { 344 bytes: Vec<u8>, 345 maximum: usize, 346 exceeded: bool, 347 } 348 349 impl BoundedWriter { 350 fn new(maximum: usize) -> Self { 351 Self { 352 bytes: Vec::with_capacity(4096), 353 maximum, 354 exceeded: false, 355 } 356 } 357 } 358 359 impl Write for BoundedWriter { 360 fn write(&mut self, buffer: &[u8]) -> io::Result<usize> { 361 let Some(next_length) = self.bytes.len().checked_add(buffer.len()) else { 362 self.exceeded = true; 363 return Err(io::Error::other("bounded status response overflow")); 364 }; 365 if next_length > self.maximum { 366 self.exceeded = true; 367 return Err(io::Error::other("bounded status response exceeded")); 368 } 369 self.bytes.extend_from_slice(buffer); 370 Ok(buffer.len()) 371 } 372 373 fn flush(&mut self) -> io::Result<()> { 374 Ok(()) 375 } 376 } 377 378 fn valid_status_id(value: &str) -> bool { 379 let mut bytes = value.bytes(); 380 let Some(first) = bytes.next() else { 381 return false; 382 }; 383 value.len() <= STATUS_ID_MAX_BYTES 384 && first.is_ascii_alphanumeric() 385 && bytes 386 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b':' | b'-')) 387 } 388 389 fn valid_status_detail_field(value: &str) -> bool { 390 !matches!( 391 value, 392 "contract_version" 393 | "service" 394 | "instance" 395 | "phase" 396 | "ready" 397 | "uptime_millis" 398 | "reason_codes" 399 | "build_info" 400 | "configuration" 401 | "persistence" 402 | "provider" 403 | "transport" 404 ) 405 } 406 407 fn validate_configuration_binding( 408 service: &ServiceId, 409 schema: &StatusId, 410 schema_version: u32, 411 ) -> Result<(), StatusModelError> { 412 if schema_version != CONFIGURATION_SCHEMA_VERSION { 413 return Err(StatusModelError::InvalidSchemaVersion); 414 } 415 let expected = format!("radroots.{service}.config"); 416 if schema.as_str() != expected { 417 return Err(StatusModelError::ConfigurationServiceMismatch); 418 } 419 Ok(()) 420 } 421 422 #[cfg(test)] 423 mod tests { 424 use serde::Serialize; 425 426 use crate::{ 427 BuildInfoEnvironment, BuildMode, CommonReasonCode, ContractVersions, Readiness, 428 ServiceOperationalState, ServicePhase, 429 }; 430 431 use super::*; 432 433 const SERVICE_REVISION: &str = "0123456789abcdef0123456789abcdef01234567"; 434 const LIB_REVISION: &str = "89abcdef0123456789abcdef0123456789abcdef"; 435 436 #[derive(Serialize)] 437 struct Provider { 438 health: ProviderHealth, 439 reason_codes: super::super::ReasonCodes, 440 } 441 442 #[derive(Serialize)] 443 struct Transport { 444 health: TransportHealth, 445 required_relays_ready: bool, 446 connected_relay_count: u64, 447 reason_codes: super::super::ReasonCodes, 448 } 449 450 #[derive(Serialize)] 451 struct Detail { 452 active_connections: u64, 453 } 454 455 impl ServiceStatusDetail for Detail { 456 type Provider = Provider; 457 type Transport = Transport; 458 const FIELD_NAME: &'static str = "myc"; 459 } 460 461 struct FailingDetail; 462 463 impl Serialize for FailingDetail { 464 fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error> 465 where 466 S: serde::Serializer, 467 { 468 Err(serde::ser::Error::custom("injected detail failure")) 469 } 470 } 471 472 impl ServiceStatusDetail for FailingDetail { 473 type Provider = Provider; 474 type Transport = Transport; 475 const FIELD_NAME: &'static str = "myc"; 476 } 477 478 #[derive(Serialize)] 479 struct ReservedDetail; 480 481 impl ServiceStatusDetail for ReservedDetail { 482 type Provider = Provider; 483 type Transport = Transport; 484 const FIELD_NAME: &'static str = "service"; 485 } 486 487 #[derive(Serialize)] 488 struct MismatchedDetail; 489 490 impl ServiceStatusDetail for MismatchedDetail { 491 type Provider = Provider; 492 type Transport = Transport; 493 const FIELD_NAME: &'static str = "rhi"; 494 } 495 496 fn status(detail: Detail) -> ServiceStatus<Detail> { 497 let build = BuildInfo::from_compile_time( 498 BuildMode::Release, 499 BuildInfoEnvironment { 500 service_version: Some("0.1.0-alpha"), 501 service_commit: Some(SERVICE_REVISION), 502 lib_revision: Some(LIB_REVISION), 503 rust_version: Some("1.97.1"), 504 target: Some("x86_64-unknown-linux-gnu"), 505 feature_profile: Some("service-host"), 506 contract_versions: ContractVersions::new(1, 1, 1, 1, 1).unwrap(), 507 }, 508 ) 509 .unwrap(); 510 let state = ServiceOperationalState::new( 511 ServicePhase::Degraded, 512 Readiness::READY, 513 super::super::ReasonCodes::new([CommonReasonCode::DatabaseLowDisk.into()]).unwrap(), 514 ) 515 .unwrap(); 516 let service = ServiceId::new("myc").unwrap(); 517 let configuration = ConfigurationIdentity::for_service( 518 &service, 519 Sha256Digest::new("a".repeat(64)).unwrap(), 520 ConfigurationSource::ExplicitConfig, 521 ) 522 .unwrap(); 523 let persistence = PersistenceSummary::new( 524 PersistenceHealth::Ready, 525 1, 526 7, 527 IntegrityState::Verified, 528 super::super::ReasonCodes::empty(), 529 ) 530 .unwrap(); 531 ServiceStatus::new( 532 service, 533 InstanceId::new("default").unwrap(), 534 state, 535 UptimeMillis::from_duration(Duration::from_millis(120_000)).unwrap(), 536 build, 537 configuration, 538 persistence, 539 Provider { 540 health: ProviderHealth::Degraded, 541 reason_codes: super::super::ReasonCodes::empty(), 542 }, 543 Transport { 544 health: TransportHealth::Ready, 545 required_relays_ready: true, 546 connected_relay_count: 2, 547 reason_codes: super::super::ReasonCodes::empty(), 548 }, 549 detail, 550 ) 551 .unwrap() 552 } 553 554 #[test] 555 fn status_json_snapshot_matches_the_frozen_envelope() { 556 let json = String::from_utf8( 557 status(Detail { 558 active_connections: 3, 559 }) 560 .to_bounded_json() 561 .unwrap(), 562 ) 563 .unwrap(); 564 assert_eq!( 565 json, 566 r#"{"contract_version":1,"service":"myc","instance":"default","phase":"degraded","ready":true,"uptime_millis":120000,"reason_codes":["database_low_disk"],"build_info":{"version":"0.1.0-alpha","revision":"0123456789abcdef0123456789abcdef01234567","toolchain":"1.97.1","contract_versions":{"config":1,"state":1,"admin":1,"status":1,"provider":1}},"configuration":{"schema":"radroots.myc.config","schema_version":1,"digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","source":"explicit_config"},"persistence":{"health":"ready","schema_version":1,"generation":7,"integrity":"verified","reason_codes":[]},"provider":{"health":"degraded","reason_codes":[]},"transport":{"health":"ready","required_relays_ready":true,"connected_relay_count":2,"reason_codes":[]},"myc":{"active_connections":3}}"# 567 ); 568 assert!(json.len() < SERVICE_STATUS_MAX_UTF8_BYTES); 569 for forbidden in ["secret", "credential", "private_key", "password", "path"] { 570 assert!(!json.contains(forbidden)); 571 } 572 } 573 574 #[derive(Serialize)] 575 struct OversizedDetail { 576 content: String, 577 } 578 579 impl ServiceStatusDetail for OversizedDetail { 580 type Provider = Provider; 581 type Transport = Transport; 582 const FIELD_NAME: &'static str = "myc"; 583 } 584 585 #[test] 586 fn bounded_encoder_rejects_oversized_detail() { 587 let ordinary = status(Detail { 588 active_connections: 0, 589 }); 590 let oversized = ServiceStatus::new( 591 ordinary.service, 592 ordinary.instance, 593 ordinary.state, 594 ordinary.uptime, 595 ordinary.build, 596 ordinary.configuration, 597 ordinary.persistence, 598 ordinary.provider, 599 ordinary.transport, 600 OversizedDetail { 601 content: "x".repeat(SERVICE_STATUS_MAX_UTF8_BYTES), 602 }, 603 ) 604 .unwrap(); 605 assert_eq!( 606 oversized.to_bounded_json(), 607 Err(StatusEncodingError::ResponseTooLarge) 608 ); 609 } 610 611 #[test] 612 fn identifiers_versions_digests_and_uptime_fail_closed() { 613 for invalid in ["", ".schema", "bad schema", "slash/name"] { 614 assert!(StatusId::new(invalid).is_err()); 615 } 616 assert!(StatusId::new("a".repeat(STATUS_ID_MAX_BYTES)).is_ok()); 617 assert!(StatusId::new("a".repeat(STATUS_ID_MAX_BYTES + 1)).is_err()); 618 let very_large = "a".repeat(4 * 1024 * 1024); 619 assert_eq!( 620 StatusId::new(&very_large), 621 Err(StatusModelError::InvalidStatusId) 622 ); 623 assert!(Sha256Digest::new("a".repeat(63)).is_err()); 624 assert!(Sha256Digest::new("a".repeat(64)).is_ok()); 625 assert!(Sha256Digest::new("A".repeat(64)).is_err()); 626 assert_eq!( 627 Sha256Digest::new(&very_large), 628 Err(StatusModelError::InvalidSha256Digest) 629 ); 630 let myc = ServiceId::new("myc").unwrap(); 631 assert_eq!( 632 validate_configuration_binding( 633 &myc, 634 &StatusId::new("radroots.rhi.config").unwrap(), 635 CONFIGURATION_SCHEMA_VERSION, 636 ), 637 Err(StatusModelError::ConfigurationServiceMismatch) 638 ); 639 assert_eq!( 640 validate_configuration_binding( 641 &myc, 642 &StatusId::new("radroots.myc.config").unwrap(), 643 CONFIGURATION_SCHEMA_VERSION + 1, 644 ), 645 Err(StatusModelError::InvalidSchemaVersion) 646 ); 647 assert_eq!( 648 UptimeMillis::from_duration(Duration::MAX), 649 Err(StatusModelError::UptimeOverflow) 650 ); 651 } 652 653 #[test] 654 fn model_accessors_reserved_fields_and_encoding_errors_are_closed() { 655 let ordinary = status(Detail { 656 active_connections: 1, 657 }); 658 assert_eq!(ordinary.service().as_str(), "myc"); 659 assert_eq!(ordinary.instance().as_str(), "default"); 660 assert_eq!(ordinary.state().phase(), ServicePhase::Degraded); 661 assert_eq!(ordinary.uptime.get(), 120_000); 662 assert_eq!( 663 ordinary.configuration.schema.as_str(), 664 "radroots.myc.config" 665 ); 666 assert_eq!(ordinary.configuration.digest.as_str(), "a".repeat(64)); 667 668 assert_eq!( 669 PersistenceSummary::new( 670 PersistenceHealth::Unavailable, 671 0, 672 0, 673 IntegrityState::Failed, 674 super::super::ReasonCodes::empty(), 675 ), 676 Err(StatusModelError::InvalidSchemaVersion) 677 ); 678 assert!(Sha256Digest::new("0".repeat(64)).is_ok()); 679 assert_eq!( 680 Sha256Digest::new(format!("{}g", "a".repeat(63))), 681 Err(StatusModelError::InvalidSha256Digest) 682 ); 683 684 let failing = ServiceStatus::new( 685 ordinary.service, 686 ordinary.instance, 687 ordinary.state, 688 ordinary.uptime, 689 ordinary.build, 690 ordinary.configuration, 691 ordinary.persistence, 692 ordinary.provider, 693 ordinary.transport, 694 FailingDetail, 695 ) 696 .unwrap(); 697 assert_eq!( 698 failing.to_bounded_json(), 699 Err(StatusEncodingError::EncodingFailed) 700 ); 701 702 let ordinary = status(Detail { 703 active_connections: 1, 704 }); 705 let service = ServiceId::new("service").unwrap(); 706 let configuration = ConfigurationIdentity::for_service( 707 &service, 708 Sha256Digest::new("b".repeat(64)).unwrap(), 709 ConfigurationSource::DerivedRepoLocal, 710 ) 711 .unwrap(); 712 assert!(matches!( 713 ServiceStatus::new( 714 service, 715 ordinary.instance, 716 ordinary.state, 717 ordinary.uptime, 718 ordinary.build, 719 configuration, 720 ordinary.persistence, 721 ordinary.provider, 722 ordinary.transport, 723 ReservedDetail, 724 ), 725 Err(StatusModelError::InvalidDetailField) 726 )); 727 728 let ordinary = status(Detail { 729 active_connections: 1, 730 }); 731 assert!(matches!( 732 ServiceStatus::new( 733 ordinary.service, 734 ordinary.instance, 735 ordinary.state, 736 ordinary.uptime, 737 ordinary.build, 738 ordinary.configuration, 739 ordinary.persistence, 740 ordinary.provider, 741 ordinary.transport, 742 MismatchedDetail, 743 ), 744 Err(StatusModelError::InvalidDetailField) 745 )); 746 747 let mut writer = BoundedWriter::new(2); 748 assert_eq!(writer.write(b"ab").unwrap(), 2); 749 writer.flush().unwrap(); 750 assert!(writer.write(b"c").is_err()); 751 assert!(writer.exceeded); 752 753 for error in [ 754 StatusModelError::InvalidStatusId, 755 StatusModelError::InvalidSha256Digest, 756 StatusModelError::InvalidSchemaVersion, 757 StatusModelError::ConfigurationServiceMismatch, 758 StatusModelError::InvalidDetailField, 759 StatusModelError::UptimeOverflow, 760 ] { 761 assert!(!error.to_string().is_empty()); 762 assert!(std::error::Error::source(&error).is_none()); 763 } 764 for error in [ 765 StatusEncodingError::EncodingFailed, 766 StatusEncodingError::ResponseTooLarge, 767 ] { 768 assert!(!error.to_string().is_empty()); 769 assert!(std::error::Error::source(&error).is_none()); 770 } 771 } 772 }