use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "PascalCase")]
pub struct SchemaVersion {
pub major: u32,
pub minor: u32,
}
impl Default for SchemaVersion {
fn default() -> Self {
Self { major: 2, minor: 1 }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ComputeSystem {
pub owner: String,
pub schema_version: SchemaVersion,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub hosting_system_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub container: Option<Container>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub virtual_machine: Option<VirtualMachine>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub should_terminate_on_last_handle_closed: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Container {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub storage: Option<Storage>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub networking: Option<ContainerNetworking>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mapped_directories: Vec<MappedDirectory>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mapped_pipes: Vec<MappedPipe>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub processor: Option<ContainerProcessor>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<ContainerMemory>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Storage {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layers: Vec<Layer>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Layer {
pub id: String,
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerNetworking {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allow_unqualified_dns_query: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dns_search_list: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub namespace: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub network_shared_container_name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MappedDirectory {
pub host_path: String,
pub container_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MappedPipe {
pub host_path: String,
pub container_pipe_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerProcessor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub maximum: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weight: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ContainerMemory {
#[serde(rename = "SizeInMB", default, skip_serializing_if = "Option::is_none")]
pub size_in_mb: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct VirtualMachine {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chipset: Option<Chipset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compute_topology: Option<Topology>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub devices: Option<Devices>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest_state: Option<GuestState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub runtime_state_file_path: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Chipset {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uefi: Option<Uefi>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Uefi {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub boot_this: Option<UefiBootEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct UefiBootEntry {
pub device_type: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub device_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub disk_number: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Topology {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory: Option<TopologyMemory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub processor: Option<TopologyProcessor>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TopologyMemory {
#[serde(rename = "SizeInMB")]
pub size_in_mb: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct TopologyProcessor {
pub count: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Devices {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub scsi: BTreeMap<String, ScsiController>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub virtual_smb: BTreeMap<String, VirtualSmbShare>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ScsiController {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub attachments: BTreeMap<String, ScsiAttachment>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ScsiAttachment {
pub path: String,
#[serde(rename = "Type")]
pub r#type: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub read_only: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct VirtualSmbShare {
pub name: String,
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flags: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct GuestState {
pub guest_state_file_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessParameters {
pub command_line: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub working_directory: String,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub environment: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub emulate_console: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_in_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_out_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub create_std_err_pipe: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub console_size: Option<ConsoleSize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub user: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ConsoleSize {
pub height: u16,
pub width: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessStatus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub process_id: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit_code: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_wait_result: Option<i32>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct Statistics {
#[serde(default)]
pub timestamp: Option<String>,
#[serde(default)]
pub container_start_time: Option<String>,
#[serde(default, rename = "Uptime100ns")]
pub uptime_100ns: u64,
#[serde(default)]
pub processor: Option<ProcessorStats>,
#[serde(default)]
pub memory: Option<MemoryStats>,
#[serde(default)]
pub storage: Option<StorageStats>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct ProcessorStats {
#[serde(default, rename = "TotalRuntime100ns")]
pub total_runtime_100ns: u64,
#[serde(default, rename = "RuntimeUser100ns")]
pub runtime_user_100ns: u64,
#[serde(default, rename = "RuntimeKernel100ns")]
pub runtime_kernel_100ns: u64,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct MemoryStats {
#[serde(default)]
pub memory_usage_commit_bytes: u64,
#[serde(default)]
pub memory_usage_commit_peak_bytes: u64,
#[serde(default)]
pub memory_usage_private_working_set_bytes: u64,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "PascalCase")]
pub struct StorageStats {
#[serde(default)]
pub read_count_normalized: u64,
#[serde(default)]
pub read_size_bytes: u64,
#[serde(default)]
pub write_count_normalized: u64,
#[serde(default)]
pub write_size_bytes: u64,
}
#[cfg(test)]
mod tests {
use super::{
ComputeSystem, Container, ContainerMemory, ContainerProcessor, Layer, SchemaVersion,
Statistics, Storage,
};
#[test]
fn schema_version_default_is_v2_1() {
let v = SchemaVersion::default();
assert_eq!(v.major, 2);
assert_eq!(v.minor, 1);
}
#[test]
fn compute_system_json_round_trip_container() {
let doc = ComputeSystem {
owner: "zlayer".to_string(),
schema_version: SchemaVersion::default(),
hosting_system_id: String::new(),
container: Some(Container {
storage: Some(Storage {
layers: vec![Layer {
id: "0f2c0c2a-1111-2222-3333-444455556666".to_string(),
path: r"C:\ProgramData\zlayer\layers\base".to_string(),
}],
path: Some(r"C:\ProgramData\zlayer\scratch\abc".to_string()),
}),
networking: None,
mapped_directories: Vec::new(),
mapped_pipes: Vec::new(),
hostname: Some("test-host".to_string()),
processor: Some(ContainerProcessor {
count: Some(2),
maximum: None,
weight: None,
}),
memory: Some(ContainerMemory {
size_in_mb: Some(1024),
}),
}),
virtual_machine: None,
should_terminate_on_last_handle_closed: Some(true),
};
let json = serde_json::to_string(&doc).expect("serialize");
assert!(json.contains("\"Owner\":\"zlayer\""));
assert!(json.contains("\"SchemaVersion\":{\"Major\":2,\"Minor\":1}"));
assert!(json.contains("\"HostName\"") || json.contains("\"Hostname\":\"test-host\""));
assert!(json.contains("\"SizeInMB\":1024"));
assert!(json.contains("\"ShouldTerminateOnLastHandleClosed\":true"));
let back: ComputeSystem = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.owner, "zlayer");
assert_eq!(back.schema_version, SchemaVersion { major: 2, minor: 1 });
let container = back.container.expect("container present");
let storage = container.storage.expect("storage present");
assert_eq!(storage.layers.len(), 1);
assert_eq!(storage.layers[0].id, "0f2c0c2a-1111-2222-3333-444455556666");
assert_eq!(
storage.path.as_deref(),
Some(r"C:\ProgramData\zlayer\scratch\abc"),
);
assert_eq!(container.hostname.as_deref(), Some("test-host"));
assert_eq!(container.processor.and_then(|p| p.count), Some(2));
assert_eq!(container.memory.and_then(|m| m.size_in_mb), Some(1024));
assert_eq!(back.should_terminate_on_last_handle_closed, Some(true));
}
#[test]
fn statistics_parses_sample_json() {
let payload = r#"{"Timestamp":"2026-04-21T12:34:56Z","Uptime100ns":2960000000,"Processor":{"TotalRuntime100ns":1234567,"RuntimeUser100ns":900000,"RuntimeKernel100ns":334567},"Memory":{"MemoryUsageCommitBytes":268435456,"MemoryUsageCommitPeakBytes":314572800,"MemoryUsagePrivateWorkingSetBytes":201326592},"Storage":{"ReadCountNormalized":42,"ReadSizeBytes":1048576,"WriteCountNormalized":13,"WriteSizeBytes":262144}}"#;
let stats: Statistics = serde_json::from_str(payload).expect("parse statistics");
assert_eq!(stats.timestamp.as_deref(), Some("2026-04-21T12:34:56Z"));
assert_eq!(stats.uptime_100ns, 2_960_000_000);
let cpu = stats.processor.expect("processor");
assert_eq!(cpu.total_runtime_100ns, 1_234_567);
assert_eq!(cpu.runtime_user_100ns, 900_000);
assert_eq!(cpu.runtime_kernel_100ns, 334_567);
let mem = stats.memory.expect("memory");
assert_eq!(mem.memory_usage_commit_bytes, 268_435_456);
assert_eq!(mem.memory_usage_commit_peak_bytes, 314_572_800);
assert_eq!(mem.memory_usage_private_working_set_bytes, 201_326_592);
let storage = stats.storage.expect("storage");
assert_eq!(storage.read_count_normalized, 42);
assert_eq!(storage.read_size_bytes, 1_048_576);
assert_eq!(storage.write_count_normalized, 13);
assert_eq!(storage.write_size_bytes, 262_144);
}
}