zlayer-types 0.14.0

Shared wire types for the ZLayer platform — API DTOs, OCI image references, and related serde types.
Documentation
//! System disk-usage and prune API DTOs.
//!
//! Wire-format types shared between the daemon's `zlayer system df` /
//! `zlayer system prune` endpoints and SDK clients. The daemon
//! (`zlayer-api`) returns these shapes and the client (`zlayer-client`)
//! decodes them. Moved out of `zlayer-api` so SDK crates can depend on
//! them without pulling in the full server stack.

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

/// Disk usage for one `ZLayer` storage category (mirrors `docker system df`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
pub struct DiskUsageCategory {
    /// Category name: "images", "containers", "volumes", "build-cache",
    /// "layers", "toolchains", "vms", "wasm", "logs", "tmp", "registry", ...
    pub name: String,
    /// Total on-disk bytes for this category.
    pub total_bytes: u64,
    /// Bytes reclaimable (unreferenced/dangling/dead). For shared layer stores
    /// this is "not referenced by any live image/container".
    pub reclaimable_bytes: u64,
    /// Item count (images, containers, volumes, layers, ...).
    pub item_count: u64,
}

/// Aggregate disk-usage report for `zlayer system df`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, ToSchema)]
pub struct SystemDiskUsage {
    pub categories: Vec<DiskUsageCategory>,
    /// Sum of category `total_bytes` (de-duplicated: shared lower layers counted once).
    pub total_bytes: u64,
    /// Sum of category `reclaimable_bytes`.
    pub total_reclaimable_bytes: u64,
}

/// Summary of what `zlayer system prune` removed, per category + totals.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, ToSchema)]
pub struct SystemPruneReport {
    /// Per-category removed-item identifiers (container ids, image refs, volume
    /// names, layer digests, deployment names, ...).
    pub stopped_containers: Vec<String>,
    pub dead_deployments: Vec<String>,
    pub deleted_images: Vec<String>,
    pub deleted_volumes: Vec<String>,
    pub deleted_networks: Vec<String>,
    pub reclaimed_layers: Vec<String>,
    pub reclaimed_blobs: Vec<String>,
    pub reclaimed_bundles: Vec<String>,
    /// Total bytes reclaimed across all categories.
    pub space_reclaimed_bytes: u64,
}

impl SystemPruneReport {
    /// Total number of removed items across all categories.
    #[must_use]
    pub fn total_items(&self) -> usize {
        self.stopped_containers.len()
            + self.dead_deployments.len()
            + self.deleted_images.len()
            + self.deleted_volumes.len()
            + self.deleted_networks.len()
            + self.reclaimed_layers.len()
            + self.reclaimed_blobs.len()
            + self.reclaimed_bundles.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_system_disk_usage_roundtrip() {
        let usage = SystemDiskUsage {
            categories: vec![
                DiskUsageCategory {
                    name: "images".to_string(),
                    total_bytes: 10_000,
                    reclaimable_bytes: 2_500,
                    item_count: 7,
                },
                DiskUsageCategory {
                    name: "layers".to_string(),
                    total_bytes: 40_000,
                    reclaimable_bytes: 5_000,
                    item_count: 23,
                },
            ],
            total_bytes: 50_000,
            total_reclaimable_bytes: 7_500,
        };

        let json = serde_json::to_string(&usage).unwrap();
        assert!(json.contains("\"images\""));
        assert!(json.contains("\"total_bytes\":50000"));
        assert!(json.contains("\"total_reclaimable_bytes\":7500"));

        let back: SystemDiskUsage = serde_json::from_str(&json).unwrap();
        assert_eq!(back, usage);
    }

    #[test]
    fn test_system_disk_usage_default() {
        let usage = SystemDiskUsage::default();
        assert!(usage.categories.is_empty());
        assert_eq!(usage.total_bytes, 0);
        assert_eq!(usage.total_reclaimable_bytes, 0);
    }

    #[test]
    fn test_system_prune_report_roundtrip() {
        let report = SystemPruneReport {
            stopped_containers: vec!["c1".to_string(), "c2".to_string()],
            dead_deployments: vec!["dep-old".to_string()],
            deleted_images: vec!["nginx:latest".to_string()],
            deleted_volumes: vec!["vol-a".to_string()],
            deleted_networks: vec![],
            reclaimed_layers: vec!["sha256:aaa".to_string(), "sha256:bbb".to_string()],
            reclaimed_blobs: vec!["sha256:ccc".to_string()],
            reclaimed_bundles: vec!["bundle-1".to_string()],
            space_reclaimed_bytes: 123_456,
        };

        let json = serde_json::to_string(&report).unwrap();
        let back: SystemPruneReport = serde_json::from_str(&json).unwrap();
        assert_eq!(back, report);
        assert!(json.contains("\"space_reclaimed_bytes\":123456"));
    }

    #[test]
    fn test_system_prune_report_total_items() {
        let report = SystemPruneReport {
            stopped_containers: vec!["c1".to_string(), "c2".to_string()],
            dead_deployments: vec!["dep-old".to_string()],
            deleted_images: vec!["nginx:latest".to_string()],
            deleted_volumes: vec!["vol-a".to_string()],
            deleted_networks: vec!["net-1".to_string()],
            reclaimed_layers: vec!["sha256:aaa".to_string(), "sha256:bbb".to_string()],
            reclaimed_blobs: vec!["sha256:ccc".to_string()],
            reclaimed_bundles: vec!["bundle-1".to_string()],
            space_reclaimed_bytes: 0,
        };
        // 2 + 1 + 1 + 1 + 1 + 2 + 1 + 1 = 10
        assert_eq!(report.total_items(), 10);
    }

    #[test]
    fn test_system_prune_report_default_empty() {
        let report = SystemPruneReport::default();
        assert_eq!(report.total_items(), 0);
        assert_eq!(report.space_reclaimed_bytes, 0);
    }
}