1use serde::de::{self, Deserializer};
39use serde_derive::{Deserialize, Serialize};
40use serde_json::{Map, Value};
41
42use super::{global::ExtensionDeclaration, recording::RecordingRef};
43
44#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct Collection {
50 pub collection: CollectionMetadata,
52 #[serde(flatten)]
53 extra_fields: Map<String, Value>,
54}
55
56#[derive(Debug, Clone, Deserialize, Serialize)]
62pub struct CollectionMetadata {
63 #[serde(rename = "core:version")]
66 pub version: String,
67
68 #[serde(rename = "core:description", skip_serializing_if = "Option::is_none")]
71 pub description: Option<String>,
72 #[serde(rename = "core:author", skip_serializing_if = "Option::is_none")]
74 pub author: Option<String>,
75 #[serde(
77 rename = "core:collection_doi",
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub collection_doi: Option<String>,
81 #[serde(rename = "core:license", skip_serializing_if = "Option::is_none")]
84 pub license: Option<String>,
85 #[serde(rename = "core:extensions", skip_serializing_if = "Option::is_none")]
87 pub extensions: Option<Vec<ExtensionDeclaration>>,
88 #[serde(rename = "core:streams", deserialize_with = "deserialize_streams")]
94 pub streams: Vec<RecordingRef>,
95 #[serde(flatten)]
96 extra_fields: Map<String, Value>,
97}
98
99impl Collection {
100 pub fn new(collection: CollectionMetadata) -> Self {
102 Self {
103 collection,
104 extra_fields: Map::new(),
105 }
106 }
107
108 pub fn extra_fields(&self) -> &Map<String, Value> {
110 &self.extra_fields
111 }
112
113 pub fn from_slice(bytes: &[u8]) -> Result<Self, crate::SigmfError> {
129 if bytes.len() > crate::MAX_META_SIZE {
130 return Err(crate::SigmfError::ResourceLimit(format!(
131 "collection metadata exceeds the {} byte limit",
132 crate::MAX_META_SIZE
133 )));
134 }
135 let value = crate::json::parse_unique_value(bytes)?;
136 let root = value.as_object().ok_or_else(|| {
137 crate::SigmfError::InvalidMetadata("expected collection object at /".to_string())
138 })?;
139 let collection = root
140 .get("collection")
141 .and_then(Value::as_object)
142 .ok_or_else(|| {
143 crate::SigmfError::InvalidMetadata(
144 "expected collection object at /collection".to_string(),
145 )
146 })?;
147 for required in ["core:version", "core:streams"] {
148 if !collection.contains_key(required) {
149 return Err(crate::SigmfError::InvalidMetadata(format!(
150 "missing required collection field at /collection/{required}"
151 )));
152 }
153 }
154 if !collection["core:version"].is_string() {
155 return Err(crate::SigmfError::InvalidMetadata(
156 "expected a string at /collection/core:version".into(),
157 ));
158 }
159 let Some(streams) = collection["core:streams"].as_array() else {
160 return Err(crate::SigmfError::InvalidMetadata(
161 "expected an array at /collection/core:streams".into(),
162 ));
163 };
164 for (index, stream) in streams.iter().enumerate() {
165 let pointer = format!("/collection/core:streams/{index}");
166 match stream {
167 Value::Array(tuple) => {
168 if tuple.len() != 2 || !tuple[0].is_string() || !tuple[1].is_string() {
169 return Err(crate::SigmfError::InvalidMetadata(format!(
170 "expected a two-string legacy tuple at {pointer}"
171 )));
172 }
173 }
174 Value::Object(object) => {
175 for field in ["name", "hash"] {
176 if !object.get(field).is_some_and(Value::is_string) {
177 return Err(crate::SigmfError::InvalidMetadata(format!(
178 "expected a string at {pointer}/{field}"
179 )));
180 }
181 }
182 }
183 _ => {
184 return Err(crate::SigmfError::InvalidMetadata(format!(
185 "expected an object or legacy tuple at {pointer}"
186 )));
187 }
188 }
189 }
190 for field in [
191 "core:description",
192 "core:author",
193 "core:collection_doi",
194 "core:license",
195 "core:extensions",
196 ] {
197 if collection.get(field).is_some_and(Value::is_null) {
198 return Err(crate::SigmfError::InvalidMetadata(format!(
199 "null is not permitted at /collection/{field}; omit the field instead"
200 )));
201 }
202 }
203 serde_json::from_value(value)
204 .map_err(|error| crate::SigmfError::InvalidMetadata(error.to_string()))
205 }
206
207 pub fn to_vec(&self) -> Result<Vec<u8>, crate::SigmfError> {
215 Ok(serde_json::to_vec(self)?)
216 }
217
218 pub fn to_vec_preserving_stream_forms(&self) -> Result<Vec<u8>, crate::SigmfError> {
227 let mut value = serde_json::to_value(self)?;
228 let streams = value
229 .get_mut("collection")
230 .and_then(Value::as_object_mut)
231 .and_then(|collection| collection.get_mut("core:streams"))
232 .and_then(Value::as_array_mut)
233 .ok_or_else(|| {
234 crate::SigmfError::InvalidMetadata(
235 "serialized collection is missing /collection/core:streams".into(),
236 )
237 })?;
238 for (reference, serialized) in self.collection.streams.iter().zip(streams) {
239 if reference.was_legacy_tuple() {
240 *serialized = Value::Array(vec![
241 Value::String(reference.get_name().to_owned()),
242 Value::String(reference.get_hash().to_owned()),
243 ]);
244 }
245 }
246 Ok(serde_json::to_vec(&value)?)
247 }
248
249 pub fn insert_extra(
258 &mut self,
259 key: impl Into<String>,
260 value: Value,
261 ) -> Result<Option<Value>, crate::SigmfError> {
262 let key = key.into();
263 if key == "collection" {
264 return Err(crate::SigmfError::InvalidMetadata(
265 "cannot insert known field \"collection\" as an extension".into(),
266 ));
267 }
268 Ok(self.extra_fields.insert(key, value))
269 }
270}
271
272impl CollectionMetadata {
273 pub fn new(version: impl Into<String>, streams: Vec<RecordingRef>) -> Self {
292 Self {
293 version: version.into(),
294 description: None,
295 author: None,
296 collection_doi: None,
297 license: None,
298 extensions: None,
299 streams,
300 extra_fields: Map::new(),
301 }
302 }
303
304 pub fn extra_fields(&self) -> &Map<String, Value> {
307 &self.extra_fields
308 }
309
310 pub(crate) fn extension_fields_mut(&mut self) -> &mut Map<String, Value> {
311 &mut self.extra_fields
312 }
313
314 pub fn insert_extra(
324 &mut self,
325 key: impl Into<String>,
326 value: Value,
327 ) -> Result<Option<Value>, crate::SigmfError> {
328 let key = key.into();
329 if matches!(
330 key.as_str(),
331 "core:version"
332 | "core:description"
333 | "core:author"
334 | "core:collection_doi"
335 | "core:license"
336 | "core:extensions"
337 | "core:streams"
338 ) {
339 return Err(crate::SigmfError::InvalidMetadata(format!(
340 "cannot insert known field {key:?} as an extension"
341 )));
342 }
343 Ok(self.extra_fields.insert(key, value))
344 }
345}
346
347fn deserialize_streams<'de, D>(deserializer: D) -> Result<Vec<RecordingRef>, D::Error>
348where
349 D: Deserializer<'de>,
350{
351 let entries = <Vec<Value> as serde::Deserialize>::deserialize(deserializer)?;
352 entries
353 .into_iter()
354 .map(|entry| {
355 if let Some(tuple) = entry.as_array() {
356 if tuple.len() != 2 {
357 return Err(de::Error::custom(
358 "legacy recording references must be exact two-element tuples",
359 ));
360 }
361 let name = tuple[0]
362 .as_str()
363 .ok_or_else(|| de::Error::custom("tuple recording name must be a string"))?;
364 let hash = tuple[1]
365 .as_str()
366 .ok_or_else(|| de::Error::custom("tuple recording hash must be a string"))?;
367 Ok(RecordingRef::from_legacy_tuple(name, hash))
368 } else {
369 serde_json::from_value(entry).map_err(de::Error::custom)
370 }
371 })
372 .collect()
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
380 fn reads_legacy_tuples_and_writes_normalized_reference_objects() {
381 let collection = Collection::from_slice(
382 br#"{"collection":{"core:version":"1.2.6","core:streams":[["a","abcd"],{"name":"b","hash":"ef01","vendor:tag":1}]}}"#,
383 )
384 .unwrap();
385 let value: Value = serde_json::from_slice(&collection.to_vec().unwrap()).unwrap();
386 assert_eq!(value["collection"]["core:streams"][0]["name"], "a");
387 assert_eq!(value["collection"]["core:streams"][0]["hash"], "abcd");
388 assert_eq!(value["collection"]["core:streams"][1]["vendor:tag"], 1);
389 let preserved: Value =
390 serde_json::from_slice(&collection.to_vec_preserving_stream_forms().unwrap()).unwrap();
391 assert_eq!(
392 preserved["collection"]["core:streams"][0],
393 serde_json::json!(["a", "abcd"])
394 );
395 assert!(preserved["collection"]["core:streams"][1].is_object());
396 }
397
398 #[test]
399 fn collection_parser_enforces_metadata_size_limit() {
400 let oversized = vec![b' '; crate::MAX_META_SIZE + 1];
401 assert!(matches!(
402 Collection::from_slice(&oversized),
403 Err(crate::SigmfError::ResourceLimit(_))
404 ));
405 }
406}