zlayer-types 0.14.1

Shared wire types for the ZLayer platform — API DTOs, OCI image references, and related serde types.
Documentation
//! Local image-store metadata sidecar.
//!
//! A natively-built image — most notably one produced by the macOS Seatbelt
//! builder — never round-trips a registry, so the OCI manifest + config blobs
//! that normally carry `os` / `architecture` are never written anywhere. That
//! left two gaps: `inspect` had no platform metadata to report, and dispatch
//! routing had no `os` hint, so a Mac-native bundle could be mis-routed to the
//! Linux VM.
//!
//! This sidecar closes both. It is a plain JSON file written beside the image's
//! `rootfs/` at `{images}/{sanitized_ref}/metadata.json`, so it is a durable,
//! single-writer source of truth with none of the redb single-writer-per-file
//! fragility of stamping the shared blob cache from a second process. The
//! builder writes it; image inspection and the composite's OS-resolution read
//! it back.

use serde::{Deserialize, Serialize};

/// File name of the per-image metadata sidecar, stored next to `rootfs/` inside
/// the image's directory.
pub const LOCAL_IMAGE_METADATA_FILE: &str = "metadata.json";

/// Platform + identity metadata recorded for a locally-built image.
///
/// Serialized to [`LOCAL_IMAGE_METADATA_FILE`]. Only `reference`, `os`, and
/// `architecture` are guaranteed; the remaining fields are populated when they
/// are cheaply available at write time and are otherwise `None` (and resolved
/// live at read time).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LocalImageMetadata {
    /// Canonical image reference (the first build tag), e.g. `myapp:latest`.
    pub reference: String,
    /// OCI `os` field (`"darwin"` for a macOS Seatbelt build). Maps to
    /// [`crate::spec::OsKind`] via `OsKind::from_oci_str`.
    pub os: String,
    /// OCI `architecture` field (Go `GOARCH`: `"arm64"`, `"amd64"`, …).
    pub architecture: String,
    /// Total on-disk size of the `rootfs/` tree in bytes, when recorded.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub size: Option<u64>,
    /// Content digest of the image, when one is known.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub digest: Option<String>,
    /// RFC 3339 creation timestamp, when recorded.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub created: Option<String>,
}

impl LocalImageMetadata {
    /// Construct the minimal sidecar (reference + platform), leaving the
    /// optional fields unset.
    #[must_use]
    pub fn new(
        reference: impl Into<String>,
        os: impl Into<String>,
        architecture: impl Into<String>,
    ) -> Self {
        Self {
            reference: reference.into(),
            os: os.into(),
            architecture: architecture.into(),
            size: None,
            digest: None,
            created: None,
        }
    }
}

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

    #[test]
    fn round_trips_through_json() {
        let meta = LocalImageMetadata {
            reference: "myapp:latest".to_string(),
            os: "darwin".to_string(),
            architecture: "arm64".to_string(),
            size: Some(4096),
            digest: Some("sha256:abc".to_string()),
            created: None,
        };
        let json = serde_json::to_string(&meta).expect("serialize");
        let back: LocalImageMetadata = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(meta, back);
    }

    #[test]
    fn unset_optionals_are_omitted_and_default_back() {
        let meta = LocalImageMetadata::new("myapp:latest", "darwin", "arm64");
        let json = serde_json::to_string(&meta).expect("serialize");
        // Absent optionals must not appear on the wire.
        assert!(!json.contains("size"));
        assert!(!json.contains("digest"));
        assert!(!json.contains("created"));
        let back: LocalImageMetadata = serde_json::from_str(&json).expect("deserialize");
        assert_eq!(meta, back);
        assert!(back.size.is_none());
    }
}