Skip to main content

scirs2_io/zarr/
metadata.rs

1//! Zarr v2 and v3 metadata structures with JSON serialization.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6use super::DataType;
7use crate::error::{IoError, Result};
8
9// ────────────────────────────── Zarr v2 ──────────────────────────────────────
10
11/// Zarr v2 `.zarray` metadata.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ArrayMetadataV2 {
14    /// Zarr format version (always 2).
15    pub zarr_format: u32,
16    /// Array shape.
17    pub shape: Vec<u64>,
18    /// Chunk shape.
19    pub chunks: Vec<u64>,
20    /// NumPy-style dtype string (e.g. `"<f8"`).
21    pub dtype: String,
22    /// Compressor specification (null for uncompressed).
23    pub compressor: Option<CompressorV2>,
24    /// Fill value for uninitialised chunks.
25    pub fill_value: serde_json::Value,
26    /// Memory layout order: `"C"` (row-major) or `"F"` (column-major).
27    pub order: String,
28    /// Optional user-defined filters.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    pub filters: Option<Vec<serde_json::Value>>,
31    /// Dimension separator (default `"."`).
32    #[serde(default = "default_dimension_separator")]
33    pub dimension_separator: String,
34}
35
36fn default_dimension_separator() -> String {
37    ".".to_string()
38}
39
40/// Compressor specification in Zarr v2.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct CompressorV2 {
43    /// Compressor identifier (e.g. `"zstd"`, `"zlib"`, `"blosc"`).
44    pub id: String,
45    /// Compression level (compressor-specific).
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    pub level: Option<i32>,
48    /// Additional configuration.
49    #[serde(flatten)]
50    pub extra: HashMap<String, serde_json::Value>,
51}
52
53/// Zarr v2 `.zgroup` metadata.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct GroupMetadataV2 {
56    /// Zarr format version (always 2).
57    pub zarr_format: u32,
58}
59
60impl GroupMetadataV2 {
61    /// Create default v2 group metadata.
62    pub fn new() -> Self {
63        Self { zarr_format: 2 }
64    }
65}
66
67impl Default for GroupMetadataV2 {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73/// Zarr v2 consolidated metadata (`.zmetadata`).
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ConsolidatedMetadata {
76    /// Top-level zarr_format.
77    pub zarr_format: u32,
78    /// Map of keys (relative paths) to their JSON metadata.
79    pub metadata: HashMap<String, serde_json::Value>,
80}
81
82impl ConsolidatedMetadata {
83    /// Create an empty consolidated metadata container.
84    pub fn new() -> Self {
85        Self {
86            zarr_format: 2,
87            metadata: HashMap::new(),
88        }
89    }
90
91    /// Insert array metadata under the given key.
92    pub fn insert_array(&mut self, path: &str, meta: &ArrayMetadataV2) -> Result<()> {
93        let key = format!("{path}/.zarray");
94        let value = serde_json::to_value(meta).map_err(|e| {
95            IoError::SerializationError(format!("Failed to serialize array metadata: {e}"))
96        })?;
97        self.metadata.insert(key, value);
98        Ok(())
99    }
100
101    /// Insert group metadata under the given key.
102    pub fn insert_group(&mut self, path: &str, meta: &GroupMetadataV2) -> Result<()> {
103        let key = format!("{path}/.zgroup");
104        let value = serde_json::to_value(meta).map_err(|e| {
105            IoError::SerializationError(format!("Failed to serialize group metadata: {e}"))
106        })?;
107        self.metadata.insert(key, value);
108        Ok(())
109    }
110
111    /// Serialize to JSON bytes.
112    pub fn to_json(&self) -> Result<Vec<u8>> {
113        serde_json::to_vec_pretty(self).map_err(|e| {
114            IoError::SerializationError(format!("Failed to serialize consolidated metadata: {e}"))
115        })
116    }
117
118    /// Deserialize from JSON bytes.
119    pub fn from_json(data: &[u8]) -> Result<Self> {
120        serde_json::from_slice(data).map_err(|e| {
121            IoError::DeserializationError(format!(
122                "Failed to deserialize consolidated metadata: {e}"
123            ))
124        })
125    }
126}
127
128impl Default for ConsolidatedMetadata {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl ArrayMetadataV2 {
135    /// Create metadata for a new array.
136    pub fn new(
137        shape: Vec<u64>,
138        chunks: Vec<u64>,
139        dtype: DataType,
140        compressor: Option<CompressorV2>,
141        fill_value: serde_json::Value,
142    ) -> Self {
143        Self {
144            zarr_format: 2,
145            shape,
146            chunks,
147            dtype: dtype.to_v2_dtype().to_string(),
148            compressor,
149            fill_value,
150            order: "C".to_string(),
151            filters: None,
152            dimension_separator: ".".to_string(),
153        }
154    }
155
156    /// Serialize to JSON bytes.
157    pub fn to_json(&self) -> Result<Vec<u8>> {
158        serde_json::to_vec_pretty(self)
159            .map_err(|e| IoError::SerializationError(format!("Failed to serialize .zarray: {e}")))
160    }
161
162    /// Deserialize from JSON bytes.
163    pub fn from_json(data: &[u8]) -> Result<Self> {
164        serde_json::from_slice(data).map_err(|e| {
165            IoError::DeserializationError(format!("Failed to deserialize .zarray: {e}"))
166        })
167    }
168
169    /// Parsed data type.
170    pub fn data_type(&self) -> Result<DataType> {
171        DataType::from_v2_dtype(&self.dtype)
172    }
173}
174
175// ────────────────────────────── Zarr v3 ──────────────────────────────────────
176
177/// Zarr v3 array or group metadata stored in `zarr.json`.
178#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct ArrayMetadataV3 {
180    /// Always 3.
181    pub zarr_format: u32,
182    /// `"array"` or `"group"`.
183    pub node_type: String,
184    /// Array shape (only for arrays).
185    #[serde(default, skip_serializing_if = "Vec::is_empty")]
186    pub shape: Vec<u64>,
187    /// Data type name (e.g. `"float64"`).
188    #[serde(default, skip_serializing_if = "Option::is_none")]
189    pub data_type: Option<String>,
190    /// Chunk grid configuration.
191    #[serde(default, skip_serializing_if = "Option::is_none")]
192    pub chunk_grid: Option<ChunkGrid>,
193    /// Chunk key encoding.
194    #[serde(default, skip_serializing_if = "Option::is_none")]
195    pub chunk_key_encoding: Option<ChunkKeyEncoding>,
196    /// Fill value.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub fill_value: Option<serde_json::Value>,
199    /// Ordered list of codecs.
200    #[serde(default, skip_serializing_if = "Vec::is_empty")]
201    pub codecs: Vec<CodecMetadata>,
202    /// User attributes.
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub attributes: Option<HashMap<String, serde_json::Value>>,
205}
206
207/// Zarr v3 group metadata.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct GroupMetadataV3 {
210    /// Always 3.
211    pub zarr_format: u32,
212    /// Always `"group"`.
213    pub node_type: String,
214    /// User attributes.
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub attributes: Option<HashMap<String, serde_json::Value>>,
217}
218
219impl GroupMetadataV3 {
220    /// Create default v3 group metadata.
221    pub fn new() -> Self {
222        Self {
223            zarr_format: 3,
224            node_type: "group".to_string(),
225            attributes: None,
226        }
227    }
228}
229
230impl Default for GroupMetadataV3 {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236/// Chunk grid specification.
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub struct ChunkGrid {
239    /// Grid name, e.g. `"regular"`.
240    pub name: String,
241    /// Grid configuration.
242    pub configuration: ChunkGridConfig,
243}
244
245/// Chunk grid configuration for regular grids.
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ChunkGridConfig {
248    /// Chunk shape.
249    pub chunk_shape: Vec<u64>,
250}
251
252/// Chunk key encoding.
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct ChunkKeyEncoding {
255    /// Encoding name, e.g. `"default"` or `"v2"`.
256    pub name: String,
257    /// Optional separator configuration.
258    #[serde(default, skip_serializing_if = "Option::is_none")]
259    pub configuration: Option<ChunkKeyEncodingConfig>,
260}
261
262/// Chunk key encoding config.
263#[derive(Debug, Clone, Serialize, Deserialize)]
264pub struct ChunkKeyEncodingConfig {
265    /// Separator character (default `/`).
266    pub separator: String,
267}
268
269/// A single codec in the v3 codec pipeline.
270#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct CodecMetadata {
272    /// Codec name (e.g. `"bytes"`, `"transpose"`, `"zstd"`).
273    pub name: String,
274    /// Codec-specific configuration.
275    #[serde(default, skip_serializing_if = "Option::is_none")]
276    pub configuration: Option<serde_json::Value>,
277}
278
279impl ArrayMetadataV3 {
280    /// Create metadata for a new v3 array.
281    pub fn new_array(
282        shape: Vec<u64>,
283        chunk_shape: Vec<u64>,
284        dtype: DataType,
285        fill_value: serde_json::Value,
286        codecs: Vec<CodecMetadata>,
287    ) -> Self {
288        Self {
289            zarr_format: 3,
290            node_type: "array".to_string(),
291            shape,
292            data_type: Some(dtype.to_v3_name().to_string()),
293            chunk_grid: Some(ChunkGrid {
294                name: "regular".to_string(),
295                configuration: ChunkGridConfig { chunk_shape },
296            }),
297            chunk_key_encoding: Some(ChunkKeyEncoding {
298                name: "default".to_string(),
299                configuration: Some(ChunkKeyEncodingConfig {
300                    separator: "/".to_string(),
301                }),
302            }),
303            fill_value: Some(fill_value),
304            codecs,
305            attributes: None,
306        }
307    }
308
309    /// Serialize to JSON bytes.
310    pub fn to_json(&self) -> Result<Vec<u8>> {
311        serde_json::to_vec_pretty(self)
312            .map_err(|e| IoError::SerializationError(format!("Failed to serialize zarr.json: {e}")))
313    }
314
315    /// Deserialize from JSON bytes.
316    pub fn from_json(data: &[u8]) -> Result<Self> {
317        serde_json::from_slice(data).map_err(|e| {
318            IoError::DeserializationError(format!("Failed to deserialize zarr.json: {e}"))
319        })
320    }
321
322    /// Parsed data type.
323    pub fn data_type_parsed(&self) -> Result<DataType> {
324        let name = self
325            .data_type
326            .as_deref()
327            .ok_or_else(|| IoError::FormatError("Missing data_type in zarr.json".to_string()))?;
328        DataType::from_v3_name(name)
329    }
330
331    /// Chunk shape from the chunk grid.
332    pub fn chunk_shape(&self) -> Result<&[u64]> {
333        self.chunk_grid
334            .as_ref()
335            .map(|g| g.configuration.chunk_shape.as_slice())
336            .ok_or_else(|| IoError::FormatError("Missing chunk_grid in zarr.json".to_string()))
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn test_v2_array_metadata_roundtrip() {
346        let meta = ArrayMetadataV2::new(
347            vec![100, 200],
348            vec![10, 20],
349            DataType::Float64,
350            None,
351            serde_json::json!(0.0),
352        );
353        let json = meta.to_json().expect("serialize");
354        let parsed = ArrayMetadataV2::from_json(&json).expect("deserialize");
355        assert_eq!(parsed.shape, vec![100, 200]);
356        assert_eq!(parsed.chunks, vec![10, 20]);
357        assert_eq!(parsed.dtype, "<f8");
358        assert_eq!(parsed.zarr_format, 2);
359        assert_eq!(parsed.order, "C");
360    }
361
362    #[test]
363    fn test_v2_group_metadata() {
364        let meta = GroupMetadataV2::new();
365        let json = serde_json::to_vec(&meta).expect("serialize");
366        let parsed: GroupMetadataV2 = serde_json::from_slice(&json).expect("deserialize");
367        assert_eq!(parsed.zarr_format, 2);
368    }
369
370    #[test]
371    fn test_v3_array_metadata_roundtrip() {
372        let meta = ArrayMetadataV3::new_array(
373            vec![1000, 2000],
374            vec![100, 200],
375            DataType::Int32,
376            serde_json::json!(0),
377            vec![CodecMetadata {
378                name: "bytes".to_string(),
379                configuration: Some(serde_json::json!({"endian": "little"})),
380            }],
381        );
382        let json = meta.to_json().expect("serialize");
383        let parsed = ArrayMetadataV3::from_json(&json).expect("deserialize");
384        assert_eq!(parsed.zarr_format, 3);
385        assert_eq!(parsed.node_type, "array");
386        assert_eq!(parsed.shape, vec![1000, 2000]);
387        assert_eq!(parsed.data_type_parsed().expect("dtype"), DataType::Int32);
388        assert_eq!(parsed.chunk_shape().expect("chunks"), &[100, 200]);
389        assert_eq!(parsed.codecs.len(), 1);
390    }
391
392    #[test]
393    fn test_v3_group_metadata() {
394        let meta = GroupMetadataV3::new();
395        let json = serde_json::to_vec(&meta).expect("serialize");
396        let parsed: GroupMetadataV3 = serde_json::from_slice(&json).expect("deserialize");
397        assert_eq!(parsed.zarr_format, 3);
398        assert_eq!(parsed.node_type, "group");
399    }
400
401    #[test]
402    fn test_v2_metadata_with_compressor() {
403        let comp = CompressorV2 {
404            id: "zstd".to_string(),
405            level: Some(3),
406            extra: HashMap::new(),
407        };
408        let meta = ArrayMetadataV2::new(
409            vec![50],
410            vec![10],
411            DataType::UInt8,
412            Some(comp),
413            serde_json::json!(255),
414        );
415        let json = meta.to_json().expect("serialize");
416        let parsed = ArrayMetadataV2::from_json(&json).expect("deserialize");
417        let c = parsed.compressor.expect("compressor present");
418        assert_eq!(c.id, "zstd");
419        assert_eq!(c.level, Some(3));
420    }
421
422    #[test]
423    fn test_consolidated_metadata() {
424        let mut cm = ConsolidatedMetadata::new();
425        let arr_meta = ArrayMetadataV2::new(
426            vec![10],
427            vec![5],
428            DataType::Float32,
429            None,
430            serde_json::json!(0.0),
431        );
432        cm.insert_array("data", &arr_meta).expect("insert array");
433        let grp_meta = GroupMetadataV2::new();
434        cm.insert_group("", &grp_meta).expect("insert group");
435
436        let json = cm.to_json().expect("serialize");
437        let parsed = ConsolidatedMetadata::from_json(&json).expect("deserialize");
438        assert!(parsed.metadata.contains_key("data/.zarray"));
439        assert!(parsed.metadata.contains_key("/.zgroup"));
440    }
441
442    #[test]
443    fn test_v2_data_type_parsing() {
444        let meta = ArrayMetadataV2::new(
445            vec![10],
446            vec![5],
447            DataType::Int16,
448            None,
449            serde_json::json!(0),
450        );
451        assert_eq!(meta.data_type().expect("parse"), DataType::Int16);
452    }
453}