rufish 0.4.0

An asynchronous Redfish client library for BMC/server management in Rust.
Documentation
//! Registry metadata types for UI-friendly attribute presentation.
//!
//! Redfish registries provide metadata about resource attributes including
//! display names, descriptions, allowed values, and types. This module
//! parses that metadata into structured types suitable for building
//! management UIs.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// The type of a registry attribute.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "PascalCase")]
pub enum AttrType {
    Enumeration,
    String,
    Integer,
    Boolean,
    Password,
}

/// A single attribute with full metadata for UI rendering.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryAttribute {
    /// Internal attribute name (e.g. "BootMode").
    pub name: String,
    /// Human-readable display name (e.g. "Boot Mode").
    pub display_name: String,
    /// Description of what this attribute controls.
    pub description: String,
    /// Data type.
    pub attr_type: AttrType,
    /// Current value from the resource.
    pub current_value: Option<Value>,
    /// Pending value (for BIOS settings applied on next boot).
    pub pending_value: Option<Value>,
    /// Default value defined by the registry.
    pub default_value: Option<Value>,
    /// Allowed values for Enumeration type.
    pub allowed_values: Option<Vec<AllowedValue>>,
    /// Lower bound for Integer type.
    pub lower_bound: Option<i64>,
    /// Upper bound for Integer type.
    pub upper_bound: Option<i64>,
    /// Minimum string length for String type.
    pub min_length: Option<u64>,
    /// Maximum string length for String type.
    pub max_length: Option<u64>,
    /// Whether this attribute is read-only.
    pub read_only: bool,
    /// Whether this attribute is hidden from normal UI.
    pub hidden: bool,
    /// Grouping name for UI categorization (e.g. "Boot", "Processor").
    pub group: Option<String>,
    /// Display order within the group.
    pub display_order: Option<u64>,
}

/// An allowed value with optional display name.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AllowedValue {
    pub value: String,
    pub display_name: Option<String>,
}

/// A message registry entry (for interpreting event/error messages).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryMessage {
    /// Message ID (e.g. "Base.1.0.Success").
    pub message_id: String,
    /// Human-readable description.
    pub description: String,
    /// Message template with %1, %2 placeholders.
    pub message: String,
    /// Severity: "OK", "Warning", "Critical".
    pub severity: Option<String>,
    /// Number of arguments expected.
    pub number_of_args: Option<u32>,
    /// Resolution suggestion.
    pub resolution: Option<String>,
}

/// Parsed registry file metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegistryInfo {
    pub registry_id: String,
    pub name: String,
    pub description: Option<String>,
    pub registry_version: Option<String>,
    pub owning_entity: Option<String>,
}

/// Parse BIOS attribute registry JSON into structured attributes.
///
/// Merges registry metadata with current and pending BIOS values.
pub fn parse_bios_registry(
    registry_json: &Value,
    current_attrs: Option<&Value>,
    pending_attrs: Option<&Value>,
) -> Vec<RegistryAttribute> {
    let entries = registry_json
        .pointer("/RegistryEntries/Attributes")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();

    entries
        .iter()
        .filter_map(|entry| parse_attribute_entry(entry, current_attrs, pending_attrs))
        .collect()
}

/// Parse a generic attribute registry (non-BIOS) into structured attributes.
pub fn parse_attribute_registry(
    registry_json: &Value,
    current_attrs: Option<&Value>,
) -> Vec<RegistryAttribute> {
    let entries = registry_json
        .pointer("/RegistryEntries/Attributes")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_default();

    entries
        .iter()
        .filter_map(|entry| parse_attribute_entry(entry, current_attrs, None))
        .collect()
}

/// Parse a message registry JSON into structured messages.
pub fn parse_message_registry(registry_json: &Value) -> (RegistryInfo, Vec<RegistryMessage>) {
    let info = RegistryInfo {
        registry_id: registry_json
            .get("Id")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string(),
        name: registry_json
            .get("Name")
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string(),
        description: registry_json.get("Description").and_then(|v| v.as_str()).map(String::from),
        registry_version: registry_json
            .get("RegistryVersion")
            .and_then(|v| v.as_str())
            .map(String::from),
        owning_entity: registry_json
            .get("OwningEntity")
            .and_then(|v| v.as_str())
            .map(String::from),
    };

    let messages = registry_json
        .get("Messages")
        .and_then(|v| v.as_object())
        .map(|obj| {
            obj.iter()
                .map(|(key, val)| RegistryMessage {
                    message_id: key.clone(),
                    description: val
                        .get("Description")
                        .and_then(|v| v.as_str())
                        .unwrap_or_default()
                        .to_string(),
                    message: val
                        .get("Message")
                        .and_then(|v| v.as_str())
                        .unwrap_or_default()
                        .to_string(),
                    severity: val.get("Severity").and_then(|v| v.as_str()).map(String::from),
                    number_of_args: val
                        .get("NumberOfArgs")
                        .and_then(|v| v.as_u64())
                        .map(|n| n as u32),
                    resolution: val.get("Resolution").and_then(|v| v.as_str()).map(String::from),
                })
                .collect()
        })
        .unwrap_or_default();

    (info, messages)
}

fn parse_attribute_entry(
    entry: &Value,
    current_attrs: Option<&Value>,
    pending_attrs: Option<&Value>,
) -> Option<RegistryAttribute> {
    let name = entry.get("AttributeName").and_then(|v| v.as_str())?;

    let attr_type = match entry.get("Type").and_then(|v| v.as_str()).unwrap_or("String") {
        "Enumeration" => AttrType::Enumeration,
        "Integer" => AttrType::Integer,
        "Boolean" => AttrType::Boolean,
        "Password" => AttrType::Password,
        _ => AttrType::String,
    };

    let allowed_values = entry.get("Value").and_then(|v| v.as_array()).map(|arr| {
        arr.iter()
            .filter_map(|item| {
                let value = item.get("ValueName").and_then(|v| v.as_str())?;
                let display_name = item
                    .get("ValueDisplayName")
                    .and_then(|v| v.as_str())
                    .map(String::from);
                Some(AllowedValue {
                    value: value.to_string(),
                    display_name,
                })
            })
            .collect()
    });

    let current_value = current_attrs.and_then(|attrs| attrs.get(name)).cloned();
    let pending_value = pending_attrs.and_then(|attrs| attrs.get(name)).cloned();

    Some(RegistryAttribute {
        name: name.to_string(),
        display_name: entry
            .get("DisplayName")
            .and_then(|v| v.as_str())
            .unwrap_or(name)
            .to_string(),
        description: entry
            .get("HelpText")
            .or_else(|| entry.get("Description"))
            .and_then(|v| v.as_str())
            .unwrap_or_default()
            .to_string(),
        attr_type,
        current_value,
        pending_value,
        default_value: entry.get("DefaultValue").cloned(),
        allowed_values,
        lower_bound: entry.get("LowerBound").and_then(|v| v.as_i64()),
        upper_bound: entry.get("UpperBound").and_then(|v| v.as_i64()),
        min_length: entry.get("MinLength").and_then(|v| v.as_u64()),
        max_length: entry.get("MaxLength").and_then(|v| v.as_u64()),
        read_only: entry.get("ReadOnly").and_then(|v| v.as_bool()).unwrap_or(false),
        hidden: entry.get("Hidden").and_then(|v| v.as_bool()).unwrap_or(false),
        group: entry
            .get("MenuPath")
            .or_else(|| entry.get("GroupName"))
            .and_then(|v| v.as_str())
            .map(String::from),
        display_order: entry.get("DisplayOrder").and_then(|v| v.as_u64()),
    })
}