#![doc = include_str!("../README.md")]
use serde::{Deserialize, Serialize};
use std::num::NonZeroU32;
use std::ops::{Deref, DerefMut};
pub use zarrs_conventions;
use zarrs_conventions::{
ConventionDefinition, NestedRepr, ZarrConventionImpl,
iref::{Uri, UriBuf, uri},
register_zarr_conventions,
uuid::uuid,
};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Thumbnails(Vec<Thumbnail>);
impl Deref for Thumbnails {
type Target = Vec<Thumbnail>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Thumbnails {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl From<Vec<Thumbnail>> for Thumbnails {
fn from(v: Vec<Thumbnail>) -> Self {
Self(v)
}
}
impl From<Thumbnails> for Vec<Thumbnail> {
fn from(t: Thumbnails) -> Self {
t.0
}
}
impl FromIterator<Thumbnail> for Thumbnails {
fn from_iter<I: IntoIterator<Item = Thumbnail>>(iter: I) -> Self {
Self(iter.into_iter().collect())
}
}
impl AsRef<[Thumbnail]> for Thumbnails {
fn as_ref(&self) -> &[Thumbnail] {
&self.0
}
}
impl AsMut<[Thumbnail]> for Thumbnails {
fn as_mut(&mut self) -> &mut [Thumbnail] {
&mut self.0
}
}
impl IntoIterator for Thumbnails {
type Item = Thumbnail;
type IntoIter = std::vec::IntoIter<Thumbnail>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Thumbnails {
type Item = &'a Thumbnail;
type IntoIter = std::slice::Iter<'a, Thumbnail>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a> IntoIterator for &'a mut Thumbnails {
type Item = &'a mut Thumbnail;
type IntoIter = std::slice::IterMut<'a, Thumbnail>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter_mut()
}
}
impl Thumbnails {
pub fn iter(&self) -> impl Iterator<Item = &Thumbnail> {
self.0.iter()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Thumbnail> {
self.0.iter_mut()
}
}
impl ZarrConventionImpl for Thumbnails {
const DEFINITION: ConventionDefinition = ConventionDefinition {
uuid: uuid!("49326c01-1180-4743-b15f-f7157038a6ab"),
schema_url: uri!(
"https://raw.githubusercontent.com/zarr-conventions/thumbnails/refs/tags/v1/schema.json"
),
spec_url: uri!("https://github.com/zarr-conventions/thumbnails/blob/v1/README.md"),
name: "thumbnails",
description: "Metadata for thumbnails representing Zarr data",
};
}
impl NestedRepr for Thumbnails {
const KEY: &'static str = "thumbnails";
}
register_zarr_conventions!(Thumbnails);
fn is_empty_map(map: &serde_json::Map<String, serde_json::Value>) -> bool {
map.is_empty()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ThumbnailLocation {
Path {
path: String,
},
Url {
url: UriBuf,
},
}
impl ThumbnailLocation {
pub fn new_path(path: impl Into<String>) -> Self {
Self::Path { path: path.into() }
}
pub fn new_url(url: UriBuf) -> Self {
Self::Url { url }
}
pub fn path(&self) -> Option<&str> {
match self {
Self::Path { path } => Some(path),
Self::Url { .. } => None,
}
}
pub fn url(&self) -> Option<&Uri> {
match self {
Self::Path { .. } => None,
Self::Url { url } => Some(url.as_ref()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Thumbnail {
width: NonZeroU32,
height: NonZeroU32,
media_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "is_empty_map")]
attributes: serde_json::Map<String, serde_json::Value>,
#[serde(flatten)]
location: ThumbnailLocation,
}
impl Thumbnail {
pub fn try_new(
width: u32,
height: u32,
media_type: impl Into<String>,
location: ThumbnailLocation,
) -> Result<Self, String> {
let width =
NonZeroU32::new(width).ok_or_else(|| "Thumbnail width must be positive".to_string())?;
let height = NonZeroU32::new(height)
.ok_or_else(|| "Thumbnail height must be positive".to_string())?;
let media_type = media_type.into();
if media_type.is_empty() {
return Err("Thumbnail media_type must not be empty".to_string());
}
Ok(Self {
width,
height,
media_type,
description: None,
attributes: serde_json::Map::new(),
location,
})
}
pub fn width(&self) -> NonZeroU32 {
self.width
}
pub fn height(&self) -> NonZeroU32 {
self.height
}
pub fn media_type(&self) -> &str {
&self.media_type
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn description_mut(&mut self) -> &mut Option<String> {
&mut self.description
}
pub fn attributes(&self) -> &serde_json::Map<String, serde_json::Value> {
&self.attributes
}
pub fn attributes_mut(&mut self) -> &mut serde_json::Map<String, serde_json::Value> {
&mut self.attributes
}
pub fn location(&self) -> &ThumbnailLocation {
&self.location
}
pub fn path(&self) -> Option<&str> {
self.location.path()
}
pub fn url(&self) -> Option<&Uri> {
self.location.url()
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use zarrs_conventions::{
AttributesBuilder, AttributesParser, ConventionId, DEFAULT_ZARR_CONVENTION_REGISTRY,
ZarrConventionImpl,
};
use crate::{Thumbnail, ThumbnailLocation, Thumbnails};
#[test]
fn is_registered() {
assert!(
DEFAULT_ZARR_CONVENTION_REGISTRY
.contains(&ConventionId::Uuid(Thumbnails::DEFINITION.uuid))
);
assert!(
DEFAULT_ZARR_CONVENTION_REGISTRY.contains(&ConventionId::SchemaUrl(
Thumbnails::DEFINITION.schema_url.to_owned()
))
);
assert!(
DEFAULT_ZARR_CONVENTION_REGISTRY.contains(&ConventionId::SpecUrl(
Thumbnails::DEFINITION.spec_url.to_owned()
))
);
}
#[test]
fn pass_expected_with_path() {
let value = json!({
"zarr_conventions": [{"uuid": Thumbnails::DEFINITION.uuid}],
"thumbnails": [
{
"width": 96,
"height": 96,
"media_type": "image/jpeg",
"path": "thumbnails/thumb96.jpeg"
}
]
});
let parser: AttributesParser = serde_json::from_value(value).unwrap();
let thumbnails: Thumbnails = parser.parse_nested().unwrap().unwrap();
assert_eq!(thumbnails.len(), 1);
assert_eq!(thumbnails[0].width().get(), 96);
assert_eq!(thumbnails[0].path(), Some("thumbnails/thumb96.jpeg"));
}
#[test]
fn pass_expected_with_url() {
let value = json!({
"zarr_conventions": [{"uuid": Thumbnails::DEFINITION.uuid}],
"thumbnails": [
{
"width": 48,
"height": 48,
"media_type": "image/png",
"url": "https://image.host/thumb48.png"
}
]
});
let parser: AttributesParser = serde_json::from_value(value).unwrap();
let thumbnails: Thumbnails = parser.parse_nested().unwrap().unwrap();
assert_eq!(thumbnails.len(), 1);
assert_eq!(thumbnails[0].width().get(), 48);
assert!(thumbnails[0].url().is_some());
}
#[test]
fn fail_missing_location() {
let value = json!({
"zarr_conventions": [{"uuid": Thumbnails::DEFINITION.uuid}],
"thumbnails": [
{
"width": 96,
"height": 96,
"media_type": "image/jpeg"
}
]
});
let parser: AttributesParser = serde_json::from_value(value).unwrap();
assert!(parser.parse_nested::<Thumbnails>().is_err());
}
#[test]
fn can_build_with_path() {
let mut thumb = Thumbnail::try_new(
96,
96,
"image/jpeg",
ThumbnailLocation::new_path("thumbnails/thumb96.jpeg"),
)
.unwrap();
*thumb.description_mut() = Some("A test thumbnail".to_string());
let thumbnails: Thumbnails = vec![thumb].into();
let mut builder = AttributesBuilder::default();
builder.add_nested(&thumbnails).unwrap();
let attrs = builder.build().unwrap();
println!("{attrs:#}");
}
#[test]
fn can_build_with_url() {
let thumb = Thumbnail::try_new(
48,
48,
"image/png",
ThumbnailLocation::new_url("https://image.host/thumb48.png".parse().unwrap()),
)
.unwrap();
let thumbnails: Thumbnails = vec![thumb].into();
let mut builder = AttributesBuilder::default();
builder.add_nested(&thumbnails).unwrap();
let attrs = builder.build().unwrap();
println!("{attrs:#}");
}
#[test]
fn try_new_fails_with_empty_media_type() {
assert!(
Thumbnail::try_new(96, 96, "", ThumbnailLocation::new_path("thumb.jpeg"),).is_err()
);
}
#[test]
fn try_new_fails_with_zero_dimensions() {
assert!(
Thumbnail::try_new(
0,
96,
"image/jpeg",
ThumbnailLocation::new_path("thumb.jpeg")
)
.is_err()
);
assert!(
Thumbnail::try_new(
96,
0,
"image/jpeg",
ThumbnailLocation::new_path("thumb.jpeg")
)
.is_err()
);
}
#[test]
fn attributes_default_empty_and_skip_serializing() {
let thumb = Thumbnail::try_new(
96,
96,
"image/jpeg",
ThumbnailLocation::new_path("thumb.jpeg"),
)
.unwrap();
assert!(thumb.attributes().is_empty());
let json = serde_json::to_value(&thumb).unwrap();
assert!(!json.as_object().unwrap().contains_key("attributes"));
}
#[test]
fn attributes_serialized_when_non_empty() {
let mut thumb = Thumbnail::try_new(
96,
96,
"image/jpeg",
ThumbnailLocation::new_path("thumb.jpeg"),
)
.unwrap();
thumb
.attributes_mut()
.insert("z_slice".to_string(), serde_json::json!(123));
let json = serde_json::to_value(&thumb).unwrap();
assert!(json.as_object().unwrap().contains_key("attributes"));
assert_eq!(json["attributes"]["z_slice"], 123);
}
}