zarrs_metadata/
lib.rs

1//! [Zarr](https://zarr-specs.readthedocs.io/) metadata support for the [`zarrs`](https://docs.rs/zarrs/latest/zarrs/index.html) crate.
2//!
3//! This crate supports serialisation and deserialisation of Zarr V2 and V3 core metadata.
4//!
5//! [`ArrayMetadata`] and [`GroupMetadata`] can represent any conformant Zarr array/group metadata.
6//! The [`zarrs_metadata_ext`](https://docs.rs/zarrs/latest/zarrs_metadata_ext/) crate supports the serialisation and deserialisation of known Zarr extension point metadata into concrete structures.
7//!
8//! ## Licence
9//! `zarrs_metadata` is licensed under either of
10//!  - the Apache License, Version 2.0 [LICENSE-APACHE](https://docs.rs/crate/zarrs_metadata/latest/source/LICENCE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0> or
11//!  - the MIT license [LICENSE-MIT](https://docs.rs/crate/zarrs_metadata/latest/source/LICENCE-MIT) or <http://opensource.org/licenses/MIT>, at your option.
12//!
13//! Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
14
15use derive_more::{
16    derive::{Display, From},
17    Deref, Into,
18};
19use serde::{de::DeserializeOwned, Deserialize, Serialize};
20
21mod array;
22
23/// Zarr V3 metadata.
24pub mod v3;
25
26/// Zarr V2 metadata.
27pub mod v2;
28
29pub use array::{
30    ArrayShape, ChunkKeySeparator, ChunkShape, DimensionName, Endianness, IntoDimensionName,
31};
32use thiserror::Error;
33
34/// Zarr array metadata (V2 or V3).
35#[derive(Deserialize, Serialize, Clone, PartialEq, Debug, Display, From)]
36#[serde(untagged)]
37#[allow(clippy::large_enum_variant)]
38pub enum ArrayMetadata {
39    /// Zarr Version 3.
40    V3(v3::ArrayMetadataV3),
41    /// Zarr Version 2.
42    V2(v2::ArrayMetadataV2),
43}
44
45impl ArrayMetadata {
46    /// Serialize the metadata as a pretty-printed String of JSON.
47    #[allow(clippy::missing_panics_doc)]
48    #[must_use]
49    pub fn to_string_pretty(&self) -> String {
50        serde_json::to_string_pretty(self).expect("array metadata is valid JSON")
51    }
52}
53
54impl TryFrom<&str> for ArrayMetadata {
55    type Error = serde_json::Error;
56    fn try_from(metadata_json: &str) -> Result<Self, Self::Error> {
57        serde_json::from_str::<Self>(metadata_json)
58    }
59}
60
61/// Zarr group metadata (V2 or V3).
62#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug, Display, From)]
63#[serde(untagged)]
64pub enum GroupMetadata {
65    /// Zarr Version 3.
66    V3(v3::GroupMetadataV3),
67    /// Zarr Version 2.
68    V2(v2::GroupMetadataV2),
69}
70
71impl GroupMetadata {
72    /// Serialize the metadata as a pretty-printed String of JSON.
73    #[allow(clippy::missing_panics_doc)]
74    #[must_use]
75    pub fn to_string_pretty(&self) -> String {
76        serde_json::to_string_pretty(self).expect("group metadata is valid JSON")
77    }
78}
79
80impl TryFrom<&str> for GroupMetadata {
81    type Error = serde_json::Error;
82    fn try_from(metadata_json: &str) -> Result<Self, Self::Error> {
83        serde_json::from_str::<Self>(metadata_json)
84    }
85}
86
87/// Zarr node metadata ([`ArrayMetadata`] or [`GroupMetadata`]).
88#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
89#[serde(untagged)]
90#[allow(clippy::large_enum_variant)]
91pub enum NodeMetadata {
92    /// Array metadata.
93    Array(ArrayMetadata),
94
95    /// Group metadata.
96    Group(GroupMetadata),
97}
98
99impl NodeMetadata {
100    /// Serialize the metadata as a pretty-printed String of JSON.
101    #[allow(clippy::missing_panics_doc)]
102    #[must_use]
103    pub fn to_string_pretty(&self) -> String {
104        serde_json::to_string_pretty(self).expect("node metadata is valid JSON")
105    }
106}
107
108/// A data type size. Fixed or variable.
109#[derive(Copy, Clone, Debug, Eq, PartialEq)]
110pub enum DataTypeSize {
111    /// Fixed size (in bytes).
112    Fixed(usize),
113    /// Variable sized.
114    ///
115    /// <https://github.com/zarr-developers/zeps/pull/47>
116    Variable,
117}
118
119/// Configuration metadata.
120#[derive(Default, Serialize, Deserialize, Debug, Clone, Deref, From, Into, Eq, PartialEq)]
121pub struct Configuration(serde_json::Map<String, serde_json::Value>);
122
123impl<T: ConfigurationSerialize> From<T> for Configuration {
124    fn from(value: T) -> Self {
125        match serde_json::to_value(value) {
126            Ok(serde_json::Value::Object(configuration)) => configuration.into(),
127            _ => {
128                panic!("the configuration could not be converted to a JSON object")
129            }
130        }
131    }
132}
133
134/// A trait for configurations that are JSON serialisable.
135///
136/// Implementors of this trait guarantee that the configuration is always serialisable to a JSON object.
137pub trait ConfigurationSerialize: Serialize + DeserializeOwned {
138    /// Convert from a configuration.
139    ///
140    /// ### Errors
141    /// Returns a [`serde_json::Error`] if `configuration` cannot be deserialised into the concrete implementation.
142    fn try_from_configuration(configuration: Configuration) -> Result<Self, serde_json::Error> {
143        serde_json::from_value(serde_json::Value::Object(configuration.0))
144    }
145}
146
147/// An invalid configuration error.
148#[derive(Clone, Debug, Error, From)]
149#[error("{name} is unsupported, configuration: {configuration:?}")]
150pub struct ConfigurationError {
151    name: String,
152    configuration: Option<Configuration>,
153}
154
155impl ConfigurationError {
156    /// Create a new invalid configuration error.
157    #[must_use]
158    pub fn new(name: String, configuration: Option<Configuration>) -> Self {
159        Self {
160            name,
161            configuration,
162        }
163    }
164
165    /// Return the name of the invalid configuration.
166    #[must_use]
167    pub fn name(&self) -> &str {
168        &self.name
169    }
170
171    /// Return the underlying configuration metadata of the invalid configuration.
172    #[must_use]
173    pub const fn configuration(&self) -> Option<&Configuration> {
174        self.configuration.as_ref()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use v3::{AdditionalFieldV3, AdditionalFieldsV3, MetadataV3};
182
183    #[test]
184    fn metadata() {
185        let metadata = MetadataV3::try_from(r#""bytes""#);
186        assert!(metadata.is_ok());
187        assert_eq!(metadata.unwrap().to_string(), r#"bytes"#);
188        assert!(MetadataV3::try_from(r#"{ "name": "bytes" }"#).is_ok());
189        let metadata =
190            MetadataV3::try_from(r#"{ "name": "bytes", "configuration": { "endian": "little" } }"#);
191        assert!(metadata.is_ok());
192        let metadata = metadata.unwrap();
193        assert_eq!(metadata.to_string(), r#"bytes {"endian":"little"}"#);
194        assert_eq!(metadata.name(), "bytes");
195        assert!(metadata.configuration().is_some());
196        let configuration = metadata.configuration().unwrap();
197        assert!(configuration.contains_key("endian"));
198        assert_eq!(
199            configuration.get("endian").unwrap().as_str().unwrap(),
200            "little"
201        );
202        assert_eq!(
203            MetadataV3::try_from(r#"{ "name": "bytes", "invalid": { "endian": "little" } }"#)
204                .unwrap_err()
205                .to_string(),
206            r#"Expected metadata "<name>" or {"name":"<name>"} or {"name":"<name>","configuration":{}}"#
207        );
208        let metadata =
209            MetadataV3::try_from(r#"{ "name": "bytes", "configuration": { "endian": "little" } }"#)
210                .unwrap();
211        let mut configuration = serde_json::Map::new();
212        configuration.insert("endian".to_string(), "little".into());
213        assert_eq!(metadata.configuration(), Some(&configuration.into()));
214    }
215
216    #[test]
217    fn additional_fields_constructors() {
218        let additional_field = serde_json::Map::new();
219        let additional_field: AdditionalFieldV3 = additional_field.into();
220        assert!(additional_field.must_understand());
221        assert!(
222            additional_field.as_value() == &serde_json::Value::Object(serde_json::Map::default())
223        );
224        assert!(serde_json::to_string(&additional_field).unwrap() == r#"{"must_understand":true}"#);
225
226        let additional_field = AdditionalFieldV3::new("test", true);
227        assert!(additional_field.must_understand());
228        assert!(additional_field.as_value() == &serde_json::Value::String("test".to_string()));
229        assert!(serde_json::to_string(&additional_field).unwrap() == r#""test""#);
230
231        let additional_field = AdditionalFieldV3::new(123, false);
232        assert!(!additional_field.must_understand());
233        assert!(
234            additional_field.as_value()
235                == &serde_json::Value::Number(serde_json::Number::from(123))
236        );
237        assert!(serde_json::to_string(&additional_field).unwrap() == "123");
238    }
239
240    #[test]
241    fn additional_fields_valid() {
242        let json = r#"{
243            "unknown_field": {
244                "key": "value",
245                "must_understand": false
246            },
247            "unsupported_field_1": {
248                "key": "value",
249                "must_understand": true
250            },
251            "unsupported_field_2": {
252                "key": "value"
253            },
254            "unsupported_field_3": [],
255            "unsupported_field_4": "test"
256        }"#;
257        let additional_fields = serde_json::from_str::<AdditionalFieldsV3>(json).unwrap();
258        assert!(additional_fields.len() == 5);
259        assert!(!additional_fields["unknown_field"].must_understand());
260        assert!(additional_fields["unsupported_field_1"].must_understand());
261        assert!(additional_fields["unsupported_field_2"].must_understand());
262        assert!(additional_fields["unsupported_field_3"].must_understand());
263        assert!(additional_fields["unsupported_field_4"].must_understand());
264    }
265}