zarrs_metadata 0.7.5

Zarr metadata support for the zarrs crate
Documentation
use derive_more::Display;
use serde::{Deserialize, Serialize};

use super::AdditionalFieldsV3;

/// Zarr V3 group metadata.
///
/// See <https://zarr-specs.readthedocs.io/en/latest/v3/core/index.html#group-metadata>.
///
/// An example `JSON` document for an explicit Zarr V3 group:
/// ```json
/// {
///     "zarr_format": 3,
///     "node_type": "group",
///     "attributes": {
///         "spam": "ham",
///         "eggs": 42,
///     }
/// }
#[non_exhaustive]
#[derive(Serialize, Clone, Debug, Display)]
#[display("{}", serde_json::to_string(self).unwrap_or_default())]
pub struct GroupMetadataV3 {
    /// An integer defining the version of the storage specification to which the group adheres. Must be `3`.
    pub zarr_format: monostate::MustBe!(3u64),
    /// A string defining the type of hierarchy node element, must be `group` here.
    pub node_type: monostate::MustBe!("group"),
    /// Optional user metadata.
    #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
    pub attributes: serde_json::Map<String, serde_json::Value>,
    // /// Extension definitions (Zarr 3.2, [ZEP0010](https://github.com/zarr-developers/zeps/pull/67)).
    // #[serde(default, skip_serializing_if = "Vec::is_empty")]
    // pub extensions: Vec<MetadataV3>,
    /// Additional fields.
    #[serde(flatten)]
    pub additional_fields: AdditionalFieldsV3,
}

impl std::cmp::PartialEq for GroupMetadataV3 {
    fn eq(&self, other: &Self) -> bool {
        self.attributes == other.attributes
            // && self.consolidated_metadata == other.consolidated_metadata
            && self.additional_fields == other.additional_fields
    }
}

impl Eq for GroupMetadataV3 {}

impl Default for GroupMetadataV3 {
    fn default() -> Self {
        Self::new()
    }
}

impl GroupMetadataV3 {
    /// Create Zarr V3 group metadata.
    #[must_use]
    pub fn new() -> Self {
        Self {
            zarr_format: monostate::MustBe!(3u64),
            node_type: monostate::MustBe!("group"),
            attributes: serde_json::Map::new(),
            // extensions: Vec::default(),
            additional_fields: AdditionalFieldsV3::default(),
        }
    }

    /// Serialize the metadata as a pretty-printed String of JSON.
    #[allow(clippy::missing_panics_doc)]
    #[must_use]
    pub fn to_string_pretty(&self) -> String {
        serde_json::to_string_pretty(self).expect("group metadata is valid JSON")
    }

    /// Set the user attributes.
    #[must_use]
    pub fn with_attributes(
        mut self,
        attributes: serde_json::Map<String, serde_json::Value>,
    ) -> Self {
        self.attributes = attributes;
        self
    }

    /// Set the additional fields.
    #[must_use]
    pub fn with_additional_fields(mut self, additional_fields: AdditionalFieldsV3) -> Self {
        self.additional_fields = additional_fields;
        self
    }

    // /// Set the extension definitions.
    // #[must_use]
    // pub fn with_extensions(mut self, extensions: Vec<MetadataV3>) -> Self {
    //     self.extensions = extensions;
    //     self
    // }
}

/// Custom deserialization to filter out `consolidated_metadata` field if it's null.
/// This is to handle non-conformant Zarr groups generated by zarr-python <=3.1.3.
/// This compatibility hotfix may be removed in future versions.
impl<'de> Deserialize<'de> for GroupMetadataV3 {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        struct GroupMetadataV3Raw {
            zarr_format: monostate::MustBe!(3u64),
            node_type: monostate::MustBe!("group"),
            #[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
            attributes: serde_json::Map<String, serde_json::Value>,
            #[serde(flatten)]
            additional_fields: serde_json::Map<String, serde_json::Value>,
        }

        let raw = GroupMetadataV3Raw::deserialize(deserializer)?;

        // Filter out consolidated_metadata field if it's null
        let additional_fields: AdditionalFieldsV3 = raw
            .additional_fields
            .into_iter()
            .filter(|(key, value)| {
                // Ignore consolidated_metadata if it's null
                !(key == "consolidated_metadata" && value.is_null())
            })
            .map(|(key, value)| (key, value.into()))
            .collect();

        Ok(GroupMetadataV3 {
            zarr_format: raw.zarr_format,
            node_type: raw.node_type,
            attributes: raw.attributes,
            additional_fields,
        })
    }
}

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

    #[test]
    fn test_deserialize_with_consolidated_metadata_null() {
        let json = r#"{
            "attributes": {},
            "zarr_format": 3,
            "consolidated_metadata": null,
            "node_type": "group"
        }"#;

        let metadata: GroupMetadataV3 = serde_json::from_str(json).unwrap();

        // The consolidated_metadata field should be filtered out and not appear in additional_fields
        assert!(metadata.additional_fields.is_empty());
    }
}