Applied Walkthrough โ Type-Safe Redfish Server ๐ก
What you'll learn: How to compose response builder type-state, source-availability tokens, dimensional serialization, health rollup, schema versioning, and typed action dispatch into a Redfish server that cannot produce a schema-non-compliant response โ the mirror of the client walkthrough in ch17.
Cross-references: ch02 (typed commands โ inverted for action dispatch), ch04 (capability tokens โ source availability), ch06 (dimensional types โ serialization side), ch07 (validated boundaries โ inverted: "construct, don't serialize"), ch09 (phantom types โ schema versioning), ch11 (trick 3 โ
#[non_exhaustive], trick 4 โ builder type-state), ch17 (client counterpart)
The Mirror Problem
Chapter 17 asks: "How do I consume Redfish correctly?" This chapter asks the mirror question: "How do I produce Redfish correctly?"
On the client side, the danger is trusting bad data. On the server side, the danger is emitting bad data โ and every client in the fleet trusts what you send.
A single GET /redfish/v1/Systems/1 response must fuse data from many sources:
flowchart LR
subgraph Sources
SMBIOS["SMBIOS\nType 1, Type 17"]
SDR["IPMI Sensors\n(SDR + readings)"]
SEL["IPMI SEL\n(critical events)"]
PCIe["PCIe Config\nSpace"]
FW["Firmware\nVersion Table"]
PWR["Power State\nRegister"]
end
subgraph Server["Redfish Server"]
Handler["GET handler"]
Builder["ComputerSystem\nBuilder"]
end
SMBIOS -->|"Name, UUID, Serial"| Handler
SDR -->|"Temperatures, Fans"| Handler
SEL -->|"Health escalation"| Handler
PCIe -->|"Device links"| Handler
FW -->|"BIOS version"| Handler
PWR -->|"PowerState"| Handler
Handler --> Builder
Builder -->|".build()"| JSON["Schema-compliant\nJSON response"]
style JSON fill:#c8e6c9,color:#000
style Builder fill:#e1f5fe,color:#000
In C, this is a 500-line handler that calls into six subsystems, manually builds
a JSON tree with json_object_set(), and hopes every required field was populated.
Forget one? The response violates the Redfish schema. Get the unit wrong? Every
client sees corrupted telemetry.
// C โ the assembly problem
json_t *get_computer_system(const char *id) {
json_t *obj = json_object();
json_object_set_new(obj, "@odata.type",
json_string("#ComputerSystem.v1_13_0.ComputerSystem"));
// ๐ Forgot to set "Name" โ schema requires it
// ๐ Forgot to set "UUID" โ schema requires it
smbios_type1_t *t1 = smbios_get_type1();
if (t1) {
json_object_set_new(obj, "Manufacturer",
json_string(t1->manufacturer));
}
json_object_set_new(obj, "PowerState",
json_string(get_power_state())); // at least this one is always available
// ๐ Reading is in raw ADC counts, not Celsius โ no type to catch it
double cpu_temp = read_sensor(SENSOR_CPU_TEMP);
// This number ends up in a Thermal response somewhere else...
// but nothing ties it to "Celsius" at the type level
// ๐ Health is manually computed โ forgot to include PSU status
json_object_set_new(obj, "Status",
build_status("Enabled", "OK")); // should be "Critical" โ PSU is failing
return obj; // missing 2 required fields, wrong health, raw units
}
Four bugs in one handler. On the client side, each bug affects one client. On the server side, each bug affects every client that queries this BMC.
Section 1 โ Response Builder Type-State: "Construct, Don't Serialize" (ch07 Inverted)
Chapter 7 teaches "parse, don't validate" โ validate inbound data once, carry the
proof in a type. The server-side mirror is "construct, don't serialize" โ build
the outbound response through a builder that gates .build() on all required fields
being present.
use std::marker::PhantomData;
// โโโโ Type-level field tracking โโโโ
pub struct HasField;
pub struct MissingField;
// โโโโ Response Builder โโโโ
/// Builder for a ComputerSystem Redfish resource.
/// Type parameters track which REQUIRED fields have been supplied.
/// Optional fields don't need type-level tracking.
pub struct ComputerSystemBuilder<Name, Uuid, PowerState, Status> {
// Required fields โ tracked at the type level
name: Option<String>,
uuid: Option<String>,
power_state: Option<PowerStateValue>,
status: Option<ResourceStatus>,
// Optional fields โ not tracked (always settable)
manufacturer: Option<String>,
model: Option<String>,
serial_number: Option<String>,
bios_version: Option<String>,
processor_summary: Option<ProcessorSummary>,
memory_summary: Option<MemorySummary>,
_markers: PhantomData<(Name, Uuid, PowerState, Status)>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub enum PowerStateValue { On, Off, PoweringOn, PoweringOff }
#[derive(Debug, Clone, serde::Serialize)]
pub struct ResourceStatus {
#[serde(rename = "State")]
pub state: StatusState,
#[serde(rename = "Health")]
pub health: HealthValue,
#[serde(rename = "HealthRollup", skip_serializing_if = "Option::is_none")]
pub health_rollup: Option<HealthValue>,
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
pub enum StatusState { Enabled, Disabled, Absent, StandbyOffline, Starting }
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)]
pub enum HealthValue { OK, Warning, Critical }
#[derive(Debug, Clone, serde::Serialize)]
pub struct ProcessorSummary {
#[serde(rename = "Count")]
pub count: u32,
#[serde(rename = "Status")]
pub status: ResourceStatus,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct MemorySummary {
#[serde(rename = "TotalSystemMemoryGiB")]
pub total_gib: f64,
#[serde(rename = "Status")]
pub status: ResourceStatus,
}
// โโโโ Constructor: all fields start MissingField โโโโ
impl ComputerSystemBuilder<MissingField, MissingField, MissingField, MissingField> {
pub fn new() -> Self {
ComputerSystemBuilder {
name: None, uuid: None, power_state: None, status: None,
manufacturer: None, model: None, serial_number: None,
bios_version: None, processor_summary: None, memory_summary: None,
_markers: PhantomData,
}
}
}
// โโโโ Required field setters โ each transitions one type parameter โโโโ
impl<U, P, S> ComputerSystemBuilder<MissingField, U, P, S> {
pub fn name(self, name: String) -> ComputerSystemBuilder<HasField, U, P, S> {
ComputerSystemBuilder {
name: Some(name), uuid: self.uuid,
power_state: self.power_state, status: self.status,
manufacturer: self.manufacturer, model: self.model,
serial_number: self.serial_number, bios_version: self.bios_version,
processor_summary: self.processor_summary,
memory_summary: self.memory_summary, _markers: PhantomData,
}
}
}
impl<N, P, S> ComputerSystemBuilder<N, MissingField, P, S> {
pub fn uuid(self, uuid: String) -> ComputerSystemBuilder<N, HasField, P, S> {
ComputerSystemBuilder {
name: self.name, uuid: Some(uuid),
power_state: self.power_state, status: self.status,
manufacturer: self.manufacturer, model: self.model,
serial_number: self.serial_number, bios_version: self.bios_version,
processor_summary: self.processor_summary,
memory_summary: self.memory_summary, _markers: PhantomData,
}
}
}
impl<N, U, S> ComputerSystemBuilder<N, U, MissingField, S> {
pub fn power_state(self, ps: PowerStateValue)
-> ComputerSystemBuilder<N, U, HasField, S>
{
ComputerSystemBuilder {
name: self.name, uuid: self.uuid,
power_state: Some(ps), status: self.status,
manufacturer: self.manufacturer, model: self.model,
serial_number: self.serial_number, bios_version: self.bios_version,
processor_summary: self.processor_summary,
memory_summary: self.memory_summary, _markers: PhantomData,
}
}
}
impl<N, U, P> ComputerSystemBuilder<N, U, P, MissingField> {
pub fn status(self, status: ResourceStatus)
-> ComputerSystemBuilder<N, U, P, HasField>
{
ComputerSystemBuilder {
name: self.name, uuid: self.uuid,
power_state: self.power_state, status: Some(status),
manufacturer: self.manufacturer, model: self.model,
serial_number: self.serial_number, bios_version: self.bios_version,
processor_summary: self.processor_summary,
memory_summary: self.memory_summary, _markers: PhantomData,
}
}
}
// โโโโ Optional field setters โ available in any state โโโโ
impl<N, U, P, S> ComputerSystemBuilder<N, U, P, S> {
pub fn manufacturer(mut self, m: String) -> Self {
self.manufacturer = Some(m); self
}
pub fn model(mut self, m: String) -> Self {
self.model = Some(m); self
}
pub fn serial_number(mut self, s: String) -> Self {
self.serial_number = Some(s); self
}
pub fn bios_version(mut self, v: String) -> Self {
self.bios_version = Some(v); self
}
pub fn processor_summary(mut self, ps: ProcessorSummary) -> Self {
self.processor_summary = Some(ps); self
}
pub fn memory_summary(mut self, ms: MemorySummary) -> Self {
self.memory_summary = Some(ms); self
}
}
// โโโโ .build() ONLY exists when all required fields are HasField โโโโ
impl ComputerSystemBuilder<HasField, HasField, HasField, HasField> {
pub fn build(self, id: &str) -> serde_json::Value {
let mut obj = serde_json::json!({
"@odata.id": format!("/redfish/v1/Systems/{id}"),
"@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
"Id": id,
"Name": self.name.unwrap(),
"UUID": self.uuid.unwrap(),
"PowerState": self.power_state.unwrap(),
"Status": self.status.unwrap(),
});
// Optional fields โ included only if present
if let Some(m) = self.manufacturer {
obj["Manufacturer"] = serde_json::json!(m);
}
if let Some(m) = self.model {
obj["Model"] = serde_json::json!(m);
}
if let Some(s) = self.serial_number {
obj["SerialNumber"] = serde_json::json!(s);
}
if let Some(v) = self.bios_version {
obj["BiosVersion"] = serde_json::json!(v);
}
if let Some(ps) = self.processor_summary {
obj["ProcessorSummary"] = serde_json::to_value(ps).unwrap();
}
if let Some(ms) = self.memory_summary {
obj["MemorySummary"] = serde_json::to_value(ms).unwrap();
}
obj
}
}
//
// โโ The Compiler Enforces Completeness โโ
//
// โ
All required fields set โ .build() is available:
// ComputerSystemBuilder::new()
// .name("PowerEdge R750".into())
// .uuid("4c4c4544-...".into())
// .power_state(PowerStateValue::On)
// .status(ResourceStatus { ... })
// .manufacturer("Dell".into()) // optional โ fine to include
// .build("1")
//
// โ Missing "Name" โ compile error:
// ComputerSystemBuilder::new()
// .uuid("4c4c4544-...".into())
// .power_state(PowerStateValue::On)
// .status(ResourceStatus { ... })
// .build("1")
// ERROR: method `build` not found for
// `ComputerSystemBuilder<MissingField, HasField, HasField, HasField>`
Bug class eliminated: schema-non-compliant responses. The handler physically
cannot serialize a ComputerSystem without supplying every required field. The
compiler error message even tells you which field is missing โ it's right there
in the type parameter: MissingField in the Name position.
Section 2 โ Source-Availability Tokens (Capability Tokens, ch04 โ New Twist)
In ch04 and ch17, capability tokens prove authorization โ "the caller is allowed to do this." On the server side, the same pattern proves availability โ "this data source was successfully initialized."
Each subsystem the BMC queries can fail independently. SMBIOS tables might be corrupt. The sensor subsystem might still be initializing. PCIe bus scan might have timed out. Encode each as a proof token:
/// Proof that SMBIOS tables were successfully parsed.
/// Only produced by the SMBIOS init function.
pub struct SmbiosReady {
_private: (),
}
/// Proof that IPMI sensor subsystem is responsive.
pub struct SensorsReady {
_private: (),
}
/// Proof that PCIe bus scan completed.
pub struct PcieReady {
_private: (),
}
/// Proof that the SEL was successfully read.
pub struct SelReady {
_private: (),
}
// โโโโ Data source initialization โโโโ
pub struct SmbiosTables {
pub product_name: String,
pub manufacturer: String,
pub serial_number: String,
pub uuid: String,
}
pub struct SensorCache {
pub cpu_temp: Celsius,
pub inlet_temp: Celsius,
pub fan_readings: Vec<(String, Rpm)>,
pub psu_power: Vec<(String, Watts)>,
}
/// Rich SEL summary โ per-subsystem health derived from typed events.
/// Built by the consumer pipeline in ch07's SEL section.
/// Replaces the lossy `has_critical_events: bool` with typed granularity.
pub struct TypedSelSummary {
pub total_entries: u32,
pub processor_health: HealthValue,
pub memory_health: HealthValue,
pub power_health: HealthValue,
pub thermal_health: HealthValue,
pub fan_health: HealthValue,
pub storage_health: HealthValue,
pub security_health: HealthValue,
}
pub fn init_smbios() -> Option<(SmbiosReady, SmbiosTables)> {
// Read SMBIOS entry point, parse tables...
// Returns None if tables are absent or corrupt
Some((
SmbiosReady { _private: () },
SmbiosTables {
product_name: "PowerEdge R750".into(),
manufacturer: "Dell Inc.".into(),
serial_number: "SVC1234567".into(),
uuid: "4c4c4544-004d-5610-804c-b2c04f435031".into(),
},
))
}
pub fn init_sensors() -> Option<(SensorsReady, SensorCache)> {
// Initialize SDR repository, read all sensors...
// Returns None if IPMI subsystem is not responsive
Some((
SensorsReady { _private: () },
SensorCache {
cpu_temp: Celsius(68.0),
inlet_temp: Celsius(24.0),
fan_readings: vec![
("Fan1".into(), Rpm(8400)),
("Fan2".into(), Rpm(8200)),
],
psu_power: vec![
("PSU1".into(), Watts(285.0)),
("PSU2".into(), Watts(290.0)),
],
},
))
}
pub fn init_sel() -> Option<(SelReady, TypedSelSummary)> {
// In production: read SEL entries, parse via ch07's TryFrom,
// classify via classify_event_health(), aggregate via summarize_sel().
Some((
SelReady { _private: () },
TypedSelSummary {
total_entries: 42,
processor_health: HealthValue::OK,
memory_health: HealthValue::OK,
power_health: HealthValue::OK,
thermal_health: HealthValue::OK,
fan_health: HealthValue::OK,
storage_health: HealthValue::OK,
security_health: HealthValue::OK,
},
))
}
Now, functions that populate builder fields from a data source require the corresponding proof token:
/// Populate SMBIOS-sourced fields. Requires proof SMBIOS is available.
fn populate_from_smbios<P, S>(
builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
_proof: &SmbiosReady,
tables: &SmbiosTables,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
builder
.name(tables.product_name.clone())
.uuid(tables.uuid.clone())
.manufacturer(tables.manufacturer.clone())
.serial_number(tables.serial_number.clone())
}
/// Fallback when SMBIOS is unavailable โ supplies required fields
/// with safe defaults.
fn populate_smbios_fallback<P, S>(
builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
builder
.name("Unknown System".into())
.uuid("00000000-0000-0000-0000-000000000000".into())
}
The handler chooses the path based on which tokens are available:
fn build_computer_system(
smbios: &Option<(SmbiosReady, SmbiosTables)>,
power_state: PowerStateValue,
health: ResourceStatus,
) -> serde_json::Value {
let builder = ComputerSystemBuilder::new()
.power_state(power_state)
.status(health);
let builder = match smbios {
Some((proof, tables)) => populate_from_smbios(builder, proof, tables),
None => populate_smbios_fallback(builder),
};
// Both paths produce HasField for Name and UUID.
// .build() is available either way.
builder.build("1")
}
Bug class eliminated: calling into a subsystem that failed initialization.
If SMBIOS didn't parse, you don't have a SmbiosReady token โ the compiler forces
you through the fallback path. No runtime if (smbios != NULL) to forget.
Combining Source Tokens with Capability Mixins (ch08)
With multiple Redfish resource types to serve (ComputerSystem, Chassis, Manager, Thermal, Power), source-population logic repeats across handlers. The mixin pattern from ch08 eliminates this duplication. Declare what sources a handler has, and blanket impls provide the population methods automatically:
/// โโ Ingredient Traits (ch08) for data sources โโ
pub trait HasSmbios {
fn smbios(&self) -> &(SmbiosReady, SmbiosTables);
}
pub trait HasSensors {
fn sensors(&self) -> &(SensorsReady, SensorCache);
}
pub trait HasSel {
fn sel(&self) -> &(SelReady, TypedSelSummary);
}
/// โโ Mixin: any handler with SMBIOS + Sensors gets identity population โโ
pub trait IdentityMixin: HasSmbios {
fn populate_identity<P, S>(
&self,
builder: ComputerSystemBuilder<MissingField, MissingField, P, S>,
) -> ComputerSystemBuilder<HasField, HasField, P, S> {
let (_, tables) = self.smbios();
builder
.name(tables.product_name.clone())
.uuid(tables.uuid.clone())
.manufacturer(tables.manufacturer.clone())
.serial_number(tables.serial_number.clone())
}
}
/// Auto-implement for any type that has SMBIOS capability.
impl<T: HasSmbios> IdentityMixin for T {}
/// โโ Mixin: any handler with Sensors + SEL gets health rollup โโ
pub trait HealthMixin: HasSensors + HasSel {
fn compute_health(&self) -> ResourceStatus {
let (_, cache) = self.sensors();
let (_, sel_summary) = self.sel();
compute_system_health(
Some(&(SensorsReady { _private: () }, cache.clone())).as_ref(),
Some(&(SelReady { _private: () }, sel_summary.clone())).as_ref(),
)
}
}
impl<T: HasSensors + HasSel> HealthMixin for T {}
/// โโ Concrete handler owns available sources โโ
struct FullPlatformHandler {
smbios: (SmbiosReady, SmbiosTables),
sensors: (SensorsReady, SensorCache),
sel: (SelReady, TypedSelSummary),
}
impl HasSmbios for FullPlatformHandler {
fn smbios(&self) -> &(SmbiosReady, SmbiosTables) { &self.smbios }
}
impl HasSensors for FullPlatformHandler {
fn sensors(&self) -> &(SensorsReady, SensorCache) { &self.sensors }
}
impl HasSel for FullPlatformHandler {
fn sel(&self) -> &(SelReady, TypedSelSummary) { &self.sel }
}
// FullPlatformHandler automatically gets:
// IdentityMixin::populate_identity() (via HasSmbios)
// HealthMixin::compute_health() (via HasSensors + HasSel)
//
// A SensorsOnlyHandler that impls HasSensors but NOT HasSel
// would get IdentityMixin (if it has SMBIOS) but NOT HealthMixin.
// Calling .compute_health() on it โ compile error.
This directly mirrors ch08's BaseBoardController pattern: ingredient traits
declare what you have, mixin traits provide behavior via blanket impls, and
the compiler gates each mixin on its prerequisites. Adding a new data
source (e.g., HasNvme) plus a mixin (e.g., StorageMixin: HasNvme + HasSel)
gives health rollup for storage to every handler that has both โ automatically.
Section 3 โ Dimensional Types at the Serialization Boundary (ch06)
On the client side (ch17 ยง4), dimensional types prevent reading ยฐC as RPM. On the server side, they prevent writing RPM into a Celsius JSON field. This is arguably more dangerous โ a wrong value on the server propagates to every client.
use serde::Serialize;
// โโโโ Dimensional types from ch06, with Serialize โโโโ
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Rpm(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize)]
pub struct Watts(pub f64);
// โโโโ Redfish Thermal response members โโโโ
// Field types enforce which unit belongs in which JSON property.
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct TemperatureMember {
pub member_id: String,
pub name: String,
pub reading_celsius: Celsius, // โ must be Celsius
#[serde(skip_serializing_if = "Option::is_none")]
pub upper_threshold_critical: Option<Celsius>,
#[serde(skip_serializing_if = "Option::is_none")]
pub upper_threshold_fatal: Option<Celsius>,
pub status: ResourceStatus,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct FanMember {
pub member_id: String,
pub name: String,
pub reading: Rpm, // โ must be Rpm
pub reading_units: &'static str, // always "RPM"
pub status: ResourceStatus,
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct PowerControlMember {
pub member_id: String,
pub name: String,
pub power_consumed_watts: Watts, // โ must be Watts
#[serde(skip_serializing_if = "Option::is_none")]
pub power_capacity_watts: Option<Watts>,
pub status: ResourceStatus,
}
// โโโโ Building a Thermal response from sensor cache โโโโ
fn build_thermal_response(
_proof: &SensorsReady,
cache: &SensorCache,
) -> serde_json::Value {
let temps = vec![
TemperatureMember {
member_id: "0".into(),
name: "CPU Temp".into(),
reading_celsius: cache.cpu_temp, // Celsius โ Celsius โ
upper_threshold_critical: Some(Celsius(95.0)),
upper_threshold_fatal: Some(Celsius(105.0)),
status: ResourceStatus {
state: StatusState::Enabled,
health: if cache.cpu_temp < Celsius(95.0) {
HealthValue::OK
} else {
HealthValue::Critical
},
health_rollup: None,
},
},
TemperatureMember {
member_id: "1".into(),
name: "Inlet Temp".into(),
reading_celsius: cache.inlet_temp, // Celsius โ Celsius โ
upper_threshold_critical: Some(Celsius(42.0)),
upper_threshold_fatal: None,
status: ResourceStatus {
state: StatusState::Enabled,
health: HealthValue::OK,
health_rollup: None,
},
},
// โ Compile error โ can't put Rpm in a Celsius field:
// TemperatureMember {
// reading_celsius: cache.fan_readings[0].1, // Rpm โ Celsius
// ...
// }
];
let fans: Vec<FanMember> = cache.fan_readings.iter().enumerate().map(|(i, (name, rpm))| {
FanMember {
member_id: i.to_string(),
name: name.clone(),
reading: *rpm, // Rpm โ Rpm โ
reading_units: "RPM",
status: ResourceStatus {
state: StatusState::Enabled,
health: if *rpm > Rpm(1000) { HealthValue::OK } else { HealthValue::Critical },
health_rollup: None,
},
}
}).collect();
serde_json::json!({
"@odata.type": "#Thermal.v1_7_0.Thermal",
"Temperatures": temps,
"Fans": fans,
})
}
Bug class eliminated: unit confusion at serialization. The Redfish schema says
ReadingCelsius is in ยฐC. The Rust type system says reading_celsius must be
Celsius. If a developer accidentally passes Rpm(8400) or Watts(285.0), the
compiler catches it before the value ever reaches JSON.
Section 4 โ Health Rollup as a Typed Fold
Redfish Status.Health is a rollup โ the worst health of all sub-components.
In C, this is typically a series of if checks that inevitably misses a source.
With typed enums and Ord, the rollup is a one-line fold โ and the compiler
ensures every source contributes:
/// Roll up health from multiple sources.
/// Ord on HealthValue: OK < Warning < Critical.
/// Returns the worst (max) value.
fn rollup(sources: &[HealthValue]) -> HealthValue {
sources.iter().copied().max().unwrap_or(HealthValue::OK)
}
/// Compute system-level health from all sub-components.
/// Takes explicit references to every source โ the caller must provide ALL of them.
fn compute_system_health(
sensors: Option<&(SensorsReady, SensorCache)>,
sel: Option<&(SelReady, TypedSelSummary)>,
) -> ResourceStatus {
let mut inputs = Vec::new();
// โโ Live sensor readings โโ
if let Some((_proof, cache)) = sensors {
// Temperature health (dimensional: Celsius comparison)
if cache.cpu_temp > Celsius(95.0) {
inputs.push(HealthValue::Critical);
} else if cache.cpu_temp > Celsius(85.0) {
inputs.push(HealthValue::Warning);
} else {
inputs.push(HealthValue::OK);
}
// Fan health (dimensional: Rpm comparison)
for (_name, rpm) in &cache.fan_readings {
if *rpm < Rpm(500) {
inputs.push(HealthValue::Critical);
} else if *rpm < Rpm(1000) {
inputs.push(HealthValue::Warning);
} else {
inputs.push(HealthValue::OK);
}
}
// PSU health (dimensional: Watts comparison)
for (_name, watts) in &cache.psu_power {
if *watts > Watts(800.0) {
inputs.push(HealthValue::Critical);
} else {
inputs.push(HealthValue::OK);
}
}
}
// โโ SEL per-subsystem health (from ch07's TypedSelSummary) โโ
// Each subsystem's health was derived by exhaustive matching over
// every sensor type and event variant. No information was lost.
if let Some((_proof, sel_summary)) = sel {
inputs.push(sel_summary.processor_health);
inputs.push(sel_summary.memory_health);
inputs.push(sel_summary.power_health);
inputs.push(sel_summary.thermal_health);
inputs.push(sel_summary.fan_health);
inputs.push(sel_summary.storage_health);
inputs.push(sel_summary.security_health);
}
let health = rollup(&inputs);
ResourceStatus {
state: StatusState::Enabled,
health,
health_rollup: Some(health),
}
}
Bug class eliminated: incomplete health rollup. In C, forgetting to include PSU
status in the health calculation is a silent bug โ the system reports "OK" while a
PSU is failing. Here, compute_system_health takes explicit references to every
data source. The SEL contribution is no longer a lossy bool โ it's seven
per-subsystem HealthValue fields derived by exhaustive matching in ch07's consumer
pipeline. Adding a new SEL sensor type forces the classifier to handle it; adding a
new subsystem field forces the rollup to include it.
Section 5 โ Schema Versioning with Phantom Types (ch09)
If the BMC advertises ComputerSystem.v1_13_0, the response must include
properties introduced in that schema version (LastResetTime, BootProgress).
Advertising v1.13 without those fields is a Redfish Interop Validator failure.
Phantom version markers make this a compile-time contract:
use std::marker::PhantomData;
// โโโโ Schema Version Markers โโโโ
pub struct V1_5;
pub struct V1_13;
// โโโโ Version-Aware Response โโโโ
pub struct ComputerSystemResponse<V> {
pub base: ComputerSystemBase,
_version: PhantomData<V>,
}
pub struct ComputerSystemBase {
pub id: String,
pub name: String,
pub uuid: String,
pub power_state: PowerStateValue,
pub status: ResourceStatus,
pub manufacturer: Option<String>,
pub serial_number: Option<String>,
pub bios_version: Option<String>,
}
// Methods available on ALL versions:
impl<V> ComputerSystemResponse<V> {
pub fn base_json(&self) -> serde_json::Value {
serde_json::json!({
"Id": self.base.id,
"Name": self.base.name,
"UUID": self.base.uuid,
"PowerState": self.base.power_state,
"Status": self.base.status,
})
}
}
// โโโโ v1.13-specific fields โโโโ
/// Date and time of the last system reset.
pub struct LastResetTime(pub String);
/// Boot progress information.
pub struct BootProgress {
pub last_state: String,
pub last_state_time: String,
}
impl ComputerSystemResponse<V1_13> {
/// LastResetTime โ REQUIRED in v1.13+.
/// This method only exists on V1_13. If the BMC advertises v1.13
/// and the handler doesn't call this, the field is missing.
pub fn last_reset_time(&self) -> LastResetTime {
// Read from RTC or boot timestamp register
LastResetTime("2026-03-16T08:30:00Z".to_string())
}
/// BootProgress โ REQUIRED in v1.13+.
pub fn boot_progress(&self) -> BootProgress {
BootProgress {
last_state: "OSRunning".to_string(),
last_state_time: "2026-03-16T08:32:00Z".to_string(),
}
}
/// Build the full v1.13 JSON response, including version-specific fields.
pub fn to_json(&self) -> serde_json::Value {
let mut obj = self.base_json();
obj["@odata.type"] =
serde_json::json!("#ComputerSystem.v1_13_0.ComputerSystem");
let reset_time = self.last_reset_time();
obj["LastResetTime"] = serde_json::json!(reset_time.0);
let boot = self.boot_progress();
obj["BootProgress"] = serde_json::json!({
"LastState": boot.last_state,
"LastStateTime": boot.last_state_time,
});
obj
}
}
impl ComputerSystemResponse<V1_5> {
/// v1.5 JSON โ no LastResetTime, no BootProgress.
pub fn to_json(&self) -> serde_json::Value {
let mut obj = self.base_json();
obj["@odata.type"] =
serde_json::json!("#ComputerSystem.v1_5_0.ComputerSystem");
obj
}
// last_reset_time() doesn't exist here.
// Calling it โ compile error:
// let resp: ComputerSystemResponse<V1_5> = ...;
// resp.last_reset_time();
// โ ERROR: method `last_reset_time` not found for
// `ComputerSystemResponse<V1_5>`
}
Bug class eliminated: schema version mismatch. If the BMC is configured to
advertise v1.13, use ComputerSystemResponse<V1_13> and the compiler ensures
every v1.13-required field is produced. Downgrade to v1.5? Change the type
parameter โ the v1.13 methods vanish, and no dead fields leak into the response.
Section 6 โ Typed Action Dispatch (ch02 Inverted)
In ch02, the typed command pattern binds Request โ Response on the client
side. On the server side, the same pattern validates incoming action payloads
and dispatches them type-safely โ the inverse direction.
use serde::Deserialize;
// โโโโ Action Trait (mirror of ch02's IpmiCmd trait) โโโโ
/// A Redfish action: the framework deserializes Params from the POST body,
/// then calls execute(). If the JSON doesn't match Params, deserialization
/// fails โ execute() is never called with bad input.
pub trait RedfishAction {
/// The expected JSON body structure.
type Params: serde::de::DeserializeOwned;
/// The result of executing the action.
type Result: serde::Serialize;
fn execute(&self, params: Self::Params) -> Result<Self::Result, RedfishError>;
}
#[derive(Debug)]
pub enum RedfishError {
InvalidPayload(String),
ActionFailed(String),
}
// โโโโ ComputerSystem.Reset โโโโ
pub struct ComputerSystemReset;
#[derive(Debug, Deserialize)]
pub enum ResetType {
On,
ForceOff,
GracefulShutdown,
GracefulRestart,
ForceRestart,
ForceOn,
PushPowerButton,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetParams {
pub reset_type: ResetType,
}
impl RedfishAction for ComputerSystemReset {
type Params = ResetParams;
type Result = ();
fn execute(&self, params: ResetParams) -> Result<(), RedfishError> {
match params.reset_type {
ResetType::GracefulShutdown => {
// Send ACPI shutdown to host
println!("Initiating ACPI shutdown");
Ok(())
}
ResetType::ForceOff => {
// Assert power-off to host
println!("Forcing power off");
Ok(())
}
ResetType::On | ResetType::ForceOn => {
println!("Powering on");
Ok(())
}
ResetType::GracefulRestart => {
println!("ACPI restart");
Ok(())
}
ResetType::ForceRestart => {
println!("Forced restart");
Ok(())
}
ResetType::PushPowerButton => {
println!("Simulating power button press");
Ok(())
}
// Exhaustive โ compiler catches missing variants
}
}
}
// โโโโ Manager.ResetToDefaults โโโโ
pub struct ManagerResetToDefaults;
#[derive(Debug, Deserialize)]
pub enum ResetToDefaultsType {
ResetAll,
PreserveNetworkAndUsers,
PreserveNetwork,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ResetToDefaultsParams {
pub reset_to_defaults_type: ResetToDefaultsType,
}
impl RedfishAction for ManagerResetToDefaults {
type Params = ResetToDefaultsParams;
type Result = ();
fn execute(&self, params: ResetToDefaultsParams) -> Result<(), RedfishError> {
match params.reset_to_defaults_type {
ResetToDefaultsType::ResetAll => {
println!("Full factory reset");
Ok(())
}
ResetToDefaultsType::PreserveNetworkAndUsers => {
println!("Reset preserving network + users");
Ok(())
}
ResetToDefaultsType::PreserveNetwork => {
println!("Reset preserving network config");
Ok(())
}
}
}
}
// โโโโ Generic Action Dispatcher โโโโ
fn dispatch_action<A: RedfishAction>(
action: &A,
raw_body: &str,
) -> Result<A::Result, RedfishError> {
// Deserialization validates the payload structure.
// If the JSON doesn't match A::Params, this fails
// and execute() is never called.
let params: A::Params = serde_json::from_str(raw_body)
.map_err(|e| RedfishError::InvalidPayload(e.to_string()))?;
action.execute(params)
}
// โโ Usage โโ
fn handle_reset_action(body: &str) -> Result<(), RedfishError> {
// Type-safe: ResetParams is validated by serde before execute()
dispatch_action(&ComputerSystemReset, body)?;
Ok(())
// Invalid JSON: {"ResetType": "Explode"}
// โ serde error: "unknown variant `Explode`"
// โ execute() never called
// Missing field: {}
// โ serde error: "missing field `ResetType`"
// โ execute() never called
}
Bug classes eliminated:
- Invalid action payload: serde rejects unknown enum variants and missing fields
before
execute()is called. No manualif (body["ResetType"] == ...)chains. - Missing variant handling:
match params.reset_typeis exhaustive โ adding a newResetTypevariant forces every action handler to be updated. - Type confusion:
ComputerSystemResetexpectsResetParams;ManagerResetToDefaultsexpectsResetToDefaultsParams. The trait system prevents passing one action's params to another action's handler.
Section 7 โ Putting It All Together: The GET Handler
Here's the complete handler that composes all six sections into a single schema-compliant response:
/// Complete GET /redfish/v1/Systems/1 handler.
///
/// Every required field is enforced by the builder type-state.
/// Every data source is gated by availability tokens.
/// Every unit is locked to its dimensional type.
/// Every health input feeds the typed rollup.
fn handle_get_computer_system(
smbios: &Option<(SmbiosReady, SmbiosTables)>,
sensors: &Option<(SensorsReady, SensorCache)>,
sel: &Option<(SelReady, TypedSelSummary)>,
power_state: PowerStateValue,
bios_version: Option<String>,
) -> serde_json::Value {
// โโ 1. Health rollup (Section 4) โโ
// Folds health from sensors + SEL into a single typed status
let health = compute_system_health(
sensors.as_ref(),
sel.as_ref(),
);
// โโ 2. Builder type-state (Section 1) โโ
let builder = ComputerSystemBuilder::new()
.power_state(power_state)
.status(health);
// โโ 3. Source-availability tokens (Section 2) โโ
let builder = match smbios {
Some((proof, tables)) => {
// SMBIOS available โ populate from hardware
populate_from_smbios(builder, proof, tables)
}
None => {
// SMBIOS unavailable โ safe defaults
populate_smbios_fallback(builder)
}
};
// โโ 4. Optional enrichment from sensors (Section 3) โโ
let builder = if let Some((_proof, cache)) = sensors {
builder
.processor_summary(ProcessorSummary {
count: 2,
status: ResourceStatus {
state: StatusState::Enabled,
health: if cache.cpu_temp < Celsius(95.0) {
HealthValue::OK
} else {
HealthValue::Critical
},
health_rollup: None,
},
})
} else {
builder
};
let builder = match bios_version {
Some(v) => builder.bios_version(v),
None => builder,
};
// โโ 5. Build (Section 1) โโ
// .build() is available because both paths (SMBIOS present / absent)
// produce HasField for Name and UUID. The compiler verified this.
builder.build("1")
}
// โโโโ Server Startup โโโโ
fn main() {
// Initialize all data sources โ each returns an availability token
let smbios = init_smbios();
let sensors = init_sensors();
let sel = init_sel();
// Simulate handler call
let response = handle_get_computer_system(
&smbios,
&sensors,
&sel,
PowerStateValue::On,
Some("2.10.1".into()),
);
println!("{}", serde_json::to_string_pretty(&response).unwrap());
}
Expected output:
{
"@odata.id": "/redfish/v1/Systems/1",
"@odata.type": "#ComputerSystem.v1_13_0.ComputerSystem",
"Id": "1",
"Name": "PowerEdge R750",
"UUID": "4c4c4544-004d-5610-804c-b2c04f435031",
"PowerState": "On",
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
},
"Manufacturer": "Dell Inc.",
"SerialNumber": "SVC1234567",
"BiosVersion": "2.10.1",
"ProcessorSummary": {
"Count": 2,
"Status": {
"State": "Enabled",
"Health": "OK"
}
}
}
What the Compiler Proves (Server Side)
| # | Bug class | How it's prevented | Pattern (Section) |
|---|---|---|---|
| 1 | Missing required field in response | .build() requires all type-state markers to be HasField | Builder type-state (ยง1) |
| 2 | Calling into failed subsystem | Source-availability tokens gate data access | Capability tokens (ยง2) |
| 3 | No fallback for unavailable source | Both match arms (present/absent) must produce HasField | Type-state + exhaustive match (ยง2) |
| 4 | Wrong unit in JSON field | reading_celsius: Celsius โ Rpm โ Watts | Dimensional types (ยง3) |
| 5 | Incomplete health rollup | compute_system_health takes explicit source refs; SEL provides per-subsystem HealthValue via ch07's TypedSelSummary | Typed function signature + exhaustive matching (ยง4) |
| 6 | Schema version mismatch | ComputerSystemResponse<V1_13> has last_reset_time(); V1_5 doesn't | Phantom types (ยง5) |
| 7 | Invalid action payload accepted | serde rejects unknown/missing fields before execute() | Typed action dispatch (ยง6) |
| 8 | Missing action variant handling | match params.reset_type is exhaustive | Enum exhaustiveness (ยง6) |
| 9 | Wrong action params to wrong handler | RedfishAction::Params is an associated type | Typed commands inverted (ยง6) |
Total runtime overhead: zero. The builder markers, availability tokens, phantom version types, and dimensional newtypes all compile away. The JSON produced is identical to the hand-rolled C version โ minus nine classes of bugs.
The Mirror: Client vs. Server Pattern Map
| Concern | Client (ch17) | Server (this chapter) |
|---|---|---|
| Boundary direction | Inbound: JSON โ typed values | Outbound: typed values โ JSON |
| Core principle | "Parse, don't validate" | "Construct, don't serialize" |
| Field completeness | TryFrom validates required fields are present | Builder type-state gates .build() on required fields |
| Unit safety | Celsius โ Rpm when reading | Celsius โ Rpm when writing |
| Privilege / availability | Capability tokens gate requests | Availability tokens gate data source access |
| Data sources | Single source (BMC) | Multiple sources (SMBIOS, sensors, SEL, PCIe, ...) |
| Schema version | Phantom types prevent accessing unsupported fields | Phantom types enforce providing version-required fields |
| Actions | Client sends typed action POST | Server validates + dispatches via RedfishAction trait |
| Health | Read and trust Status.Health | Compute Status.Health via typed rollup |
| Failure propagation | One bad parse โ one client error | One bad serialization โ every client sees wrong data |
The two chapters form a complete story. Ch17: "Every response I consume is type-checked." This chapter: "Every response I produce is type-checked." The same patterns flow in both directions โ the type system doesn't know or care which end of the wire you're on.
Key Takeaways
- "Construct, don't serialize" is the server-side mirror of "parse, don't
validate" โ use builder type-state so
.build()only exists when all required fields are present. - Source-availability tokens prove initialization โ the same capability token pattern from ch04, repurposed to prove a data source is ready.
- Dimensional types protect producers and consumers โ putting
Rpmin aReadingCelsiusfield is a compile error, not a customer-reported bug. - Health rollup is a typed fold โ
OrdonHealthValueplus explicit source references mean the compiler catches "forgot to include PSU status." - Schema versioning at the type level โ phantom type parameters make version-specific fields appear and disappear at compile time.
- Action dispatch inverts ch02 โ
serdedeserializes the payload into a typedParamsstruct, and exhaustive matching on enum variants means adding a newResetTypeforces every handler to be updated. - Server-side bugs propagate to every client โ that's why compile-time correctness on the producer side is even more critical than on the consumer side.