1use std::collections::BTreeMap;
16
17use chrono::NaiveDate;
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use serde_json::Value;
20use thiserror::Error;
21use url::Url;
22
23use crate::serde_util::deserialize_option_u64ish;
24
25macro_rules! string_enum {
26 ($(#[$enum_meta:meta])* $name:ident { $($(#[$variant_meta:meta])* $variant:ident => $value:literal),+ $(,)? }) => {
27 $(#[$enum_meta])*
28 #[derive(Clone, Debug, PartialEq, Eq)]
29 #[non_exhaustive]
30 pub enum $name {
31 $($(#[$variant_meta])* $variant,)+
32 Unknown(
34 String
36 ),
37 }
38
39 impl Serialize for $name {
40 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
41 where
42 S: Serializer,
43 {
44 serializer.serialize_str(match self {
45 $(Self::$variant => $value,)+
46 Self::Unknown(value) => value.as_str(),
47 })
48 }
49 }
50
51 impl<'de> Deserialize<'de> for $name {
52 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
53 where
54 D: Deserializer<'de>,
55 {
56 let value = String::deserialize(deserializer)?;
57 Ok(match value.as_str() {
58 $($value => Self::$variant,)+
59 _ => Self::Unknown(value),
60 })
61 }
62 }
63 };
64}
65
66string_enum!(
67 UploadType {
69 Dataset => "dataset",
71 Publication => "publication",
73 Poster => "poster",
75 Presentation => "presentation",
77 Software => "software",
79 Image => "image",
81 Video => "video",
83 Lesson => "lesson",
85 PhysicalObject => "physicalobject",
87 Other => "other"
89 }
90);
91
92string_enum!(
93 AccessRight {
95 Open => "open",
97 Embargoed => "embargoed",
99 Restricted => "restricted",
101 Closed => "closed"
103 }
104);
105
106#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
108pub struct Creator {
109 pub name: String,
111 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub affiliation: Option<String>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
116 pub orcid: Option<String>,
117 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub gnd: Option<String>,
120 #[serde(flatten, default)]
122 pub extra: BTreeMap<String, Value>,
123}
124
125impl Creator {
126 #[must_use]
143 pub fn builder() -> CreatorBuilder {
144 CreatorBuilder::default()
145 }
146
147 #[must_use]
159 pub fn named(name: impl Into<String>) -> Self {
160 Self {
161 name: name.into(),
162 ..Self::default()
163 }
164 }
165}
166
167#[derive(Clone, Debug, PartialEq, Eq, Default)]
169pub struct CreatorBuilder {
170 name: Option<String>,
171 affiliation: Option<String>,
172 orcid: Option<String>,
173 gnd: Option<String>,
174 extra: BTreeMap<String, Value>,
175}
176
177impl CreatorBuilder {
178 #[must_use]
180 pub fn name(mut self, name: impl Into<String>) -> Self {
181 self.name = Some(name.into());
182 self
183 }
184
185 #[must_use]
187 pub fn affiliation(mut self, affiliation: impl Into<String>) -> Self {
188 self.affiliation = Some(affiliation.into());
189 self
190 }
191
192 #[must_use]
194 pub fn orcid(mut self, orcid: impl Into<String>) -> Self {
195 self.orcid = Some(orcid.into());
196 self
197 }
198
199 #[must_use]
201 pub fn gnd(mut self, gnd: impl Into<String>) -> Self {
202 self.gnd = Some(gnd.into());
203 self
204 }
205
206 #[must_use]
208 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
209 self.extra = extra;
210 self
211 }
212
213 #[must_use]
215 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
216 self.extra.insert(key.into(), value);
217 self
218 }
219
220 pub fn build(self) -> Result<Creator, MetadataEntryBuildError> {
226 Ok(Creator {
227 name: required_entry_field(self.name, "Creator", "name")?,
228 affiliation: self.affiliation,
229 orcid: self.orcid,
230 gnd: self.gnd,
231 extra: self.extra,
232 })
233 }
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
238pub struct Contributor {
239 pub name: String,
241 #[serde(rename = "type")]
243 pub type_: String,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub affiliation: Option<String>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub orcid: Option<String>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub gnd: Option<String>,
253 #[serde(flatten, default)]
255 pub extra: BTreeMap<String, Value>,
256}
257
258impl Contributor {
259 #[must_use]
261 pub fn builder() -> ContributorBuilder {
262 ContributorBuilder::default()
263 }
264
265 #[must_use]
267 pub fn new(name: impl Into<String>, role: impl Into<String>) -> Self {
268 Self {
269 name: name.into(),
270 type_: role.into(),
271 ..Self::default()
272 }
273 }
274}
275
276#[derive(Clone, Debug, PartialEq, Eq, Default)]
278pub struct ContributorBuilder {
279 name: Option<String>,
280 role: Option<String>,
281 affiliation: Option<String>,
282 orcid: Option<String>,
283 gnd: Option<String>,
284 extra: BTreeMap<String, Value>,
285}
286
287impl ContributorBuilder {
288 #[must_use]
290 pub fn name(mut self, name: impl Into<String>) -> Self {
291 self.name = Some(name.into());
292 self
293 }
294
295 #[must_use]
297 pub fn role(mut self, role: impl Into<String>) -> Self {
298 self.role = Some(role.into());
299 self
300 }
301
302 #[must_use]
304 pub fn affiliation(mut self, affiliation: impl Into<String>) -> Self {
305 self.affiliation = Some(affiliation.into());
306 self
307 }
308
309 #[must_use]
311 pub fn orcid(mut self, orcid: impl Into<String>) -> Self {
312 self.orcid = Some(orcid.into());
313 self
314 }
315
316 #[must_use]
318 pub fn gnd(mut self, gnd: impl Into<String>) -> Self {
319 self.gnd = Some(gnd.into());
320 self
321 }
322
323 #[must_use]
325 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
326 self.extra = extra;
327 self
328 }
329
330 #[must_use]
332 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
333 self.extra.insert(key.into(), value);
334 self
335 }
336
337 pub fn build(self) -> Result<Contributor, MetadataEntryBuildError> {
343 Ok(Contributor {
344 name: required_entry_field(self.name, "Contributor", "name")?,
345 type_: required_entry_field(self.role, "Contributor", "role")?,
346 affiliation: self.affiliation,
347 orcid: self.orcid,
348 gnd: self.gnd,
349 extra: self.extra,
350 })
351 }
352}
353
354#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
356pub struct Subject {
357 pub term: String,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub identifier: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub scheme: Option<String>,
365 #[serde(flatten, default)]
367 pub extra: BTreeMap<String, Value>,
368}
369
370impl Subject {
371 #[must_use]
373 pub fn builder() -> SubjectBuilder {
374 SubjectBuilder::default()
375 }
376
377 #[must_use]
379 pub fn new(term: impl Into<String>) -> Self {
380 Self {
381 term: term.into(),
382 ..Self::default()
383 }
384 }
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Default)]
389pub struct SubjectBuilder {
390 term: Option<String>,
391 identifier: Option<String>,
392 scheme: Option<String>,
393 extra: BTreeMap<String, Value>,
394}
395
396impl SubjectBuilder {
397 #[must_use]
399 pub fn term(mut self, term: impl Into<String>) -> Self {
400 self.term = Some(term.into());
401 self
402 }
403
404 #[must_use]
406 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
407 self.identifier = Some(identifier.into());
408 self
409 }
410
411 #[must_use]
413 pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
414 self.scheme = Some(scheme.into());
415 self
416 }
417
418 #[must_use]
420 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
421 self.extra = extra;
422 self
423 }
424
425 #[must_use]
427 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
428 self.extra.insert(key.into(), value);
429 self
430 }
431
432 pub fn build(self) -> Result<Subject, MetadataEntryBuildError> {
438 Ok(Subject {
439 term: required_entry_field(self.term, "Subject", "term")?,
440 identifier: self.identifier,
441 scheme: self.scheme,
442 extra: self.extra,
443 })
444 }
445}
446
447#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
449pub struct RecordIdentifier {
450 pub identifier: String,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub scheme: Option<String>,
455 #[serde(flatten, default)]
457 pub extra: BTreeMap<String, Value>,
458}
459
460impl RecordIdentifier {
461 #[must_use]
463 pub fn builder() -> RecordIdentifierBuilder {
464 RecordIdentifierBuilder::default()
465 }
466
467 #[must_use]
469 pub fn new(identifier: impl Into<String>) -> Self {
470 Self {
471 identifier: identifier.into(),
472 ..Self::default()
473 }
474 }
475}
476
477#[derive(Clone, Debug, PartialEq, Eq, Default)]
479pub struct RecordIdentifierBuilder {
480 identifier: Option<String>,
481 scheme: Option<String>,
482 extra: BTreeMap<String, Value>,
483}
484
485impl RecordIdentifierBuilder {
486 #[must_use]
488 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
489 self.identifier = Some(identifier.into());
490 self
491 }
492
493 #[must_use]
495 pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
496 self.scheme = Some(scheme.into());
497 self
498 }
499
500 #[must_use]
502 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
503 self.extra = extra;
504 self
505 }
506
507 #[must_use]
509 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
510 self.extra.insert(key.into(), value);
511 self
512 }
513
514 pub fn build(self) -> Result<RecordIdentifier, MetadataEntryBuildError> {
520 Ok(RecordIdentifier {
521 identifier: required_entry_field(self.identifier, "RecordIdentifier", "identifier")?,
522 scheme: self.scheme,
523 extra: self.extra,
524 })
525 }
526}
527
528#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
530pub struct RecordDate {
531 pub date: String,
533 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
535 pub type_: Option<String>,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub description: Option<String>,
539 #[serde(flatten, default)]
541 pub extra: BTreeMap<String, Value>,
542}
543
544impl RecordDate {
545 #[must_use]
547 pub fn builder() -> RecordDateBuilder {
548 RecordDateBuilder::default()
549 }
550
551 #[must_use]
553 pub fn new(date: impl Into<String>) -> Self {
554 Self {
555 date: date.into(),
556 ..Self::default()
557 }
558 }
559}
560
561#[derive(Clone, Debug, PartialEq, Eq, Default)]
563pub struct RecordDateBuilder {
564 date: Option<String>,
565 date_type: Option<String>,
566 description: Option<String>,
567 extra: BTreeMap<String, Value>,
568}
569
570impl RecordDateBuilder {
571 #[must_use]
573 pub fn date(mut self, date: impl Into<String>) -> Self {
574 self.date = Some(date.into());
575 self
576 }
577
578 #[must_use]
580 pub fn date_type(mut self, date_type: impl Into<String>) -> Self {
581 self.date_type = Some(date_type.into());
582 self
583 }
584
585 #[must_use]
587 pub fn description(mut self, description: impl Into<String>) -> Self {
588 self.description = Some(description.into());
589 self
590 }
591
592 #[must_use]
594 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
595 self.extra = extra;
596 self
597 }
598
599 #[must_use]
601 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
602 self.extra.insert(key.into(), value);
603 self
604 }
605
606 pub fn build(self) -> Result<RecordDate, MetadataEntryBuildError> {
612 Ok(RecordDate {
613 date: required_entry_field(self.date, "RecordDate", "date")?,
614 type_: self.date_type,
615 description: self.description,
616 extra: self.extra,
617 })
618 }
619}
620
621#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
623pub struct RecordVersionRelation {
624 #[serde(
626 default,
627 deserialize_with = "deserialize_option_u64ish",
628 skip_serializing_if = "Option::is_none"
629 )]
630 pub index: Option<u64>,
631 #[serde(
633 default,
634 deserialize_with = "deserialize_option_u64ish",
635 skip_serializing_if = "Option::is_none"
636 )]
637 pub count: Option<u64>,
638 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub is_last: Option<bool>,
641 #[serde(flatten, default)]
643 pub extra: BTreeMap<String, Value>,
644}
645
646#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
648pub struct RecordRelations {
649 #[serde(default)]
651 pub version: Vec<RecordVersionRelation>,
652 #[serde(flatten, default)]
654 pub extra: BTreeMap<String, Value>,
655}
656
657#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
659pub struct RelatedIdentifier {
660 pub identifier: String,
662 pub relation: String,
664 #[serde(default, skip_serializing_if = "Option::is_none")]
666 pub scheme: Option<String>,
667 #[serde(default, skip_serializing_if = "Option::is_none")]
669 pub resource_type: Option<String>,
670 #[serde(flatten, default)]
672 pub extra: BTreeMap<String, Value>,
673}
674
675impl RelatedIdentifier {
676 #[must_use]
693 pub fn builder() -> RelatedIdentifierBuilder {
694 RelatedIdentifierBuilder::default()
695 }
696
697 #[must_use]
699 pub fn new(identifier: impl Into<String>, relation: impl Into<String>) -> Self {
700 Self {
701 identifier: identifier.into(),
702 relation: relation.into(),
703 ..Self::default()
704 }
705 }
706}
707
708#[derive(Clone, Debug, PartialEq, Eq, Default)]
710pub struct RelatedIdentifierBuilder {
711 identifier: Option<String>,
712 relation: Option<String>,
713 scheme: Option<String>,
714 resource_type: Option<String>,
715 extra: BTreeMap<String, Value>,
716}
717
718impl RelatedIdentifierBuilder {
719 #[must_use]
721 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
722 self.identifier = Some(identifier.into());
723 self
724 }
725
726 #[must_use]
728 pub fn relation(mut self, relation: impl Into<String>) -> Self {
729 self.relation = Some(relation.into());
730 self
731 }
732
733 #[must_use]
735 pub fn scheme(mut self, scheme: impl Into<String>) -> Self {
736 self.scheme = Some(scheme.into());
737 self
738 }
739
740 #[must_use]
742 pub fn resource_type(mut self, resource_type: impl Into<String>) -> Self {
743 self.resource_type = Some(resource_type.into());
744 self
745 }
746
747 #[must_use]
749 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
750 self.extra = extra;
751 self
752 }
753
754 #[must_use]
756 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
757 self.extra.insert(key.into(), value);
758 self
759 }
760
761 pub fn build(self) -> Result<RelatedIdentifier, MetadataEntryBuildError> {
767 Ok(RelatedIdentifier {
768 identifier: required_entry_field(self.identifier, "RelatedIdentifier", "identifier")?,
769 relation: required_entry_field(self.relation, "RelatedIdentifier", "relation")?,
770 scheme: self.scheme,
771 resource_type: self.resource_type,
772 extra: self.extra,
773 })
774 }
775}
776
777#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
779pub struct CommunityRef {
780 #[serde(alias = "id")]
782 pub identifier: String,
783 #[serde(flatten, default)]
785 pub extra: BTreeMap<String, Value>,
786}
787
788impl CommunityRef {
789 #[must_use]
791 pub fn builder() -> CommunityRefBuilder {
792 CommunityRefBuilder::default()
793 }
794
795 #[must_use]
806 pub fn new(identifier: impl Into<String>) -> Self {
807 Self {
808 identifier: identifier.into(),
809 ..Self::default()
810 }
811 }
812}
813
814#[derive(Clone, Debug, PartialEq, Eq, Default)]
816pub struct CommunityRefBuilder {
817 identifier: Option<String>,
818 extra: BTreeMap<String, Value>,
819}
820
821impl CommunityRefBuilder {
822 #[must_use]
824 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
825 self.identifier = Some(identifier.into());
826 self
827 }
828
829 #[must_use]
831 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
832 self.extra = extra;
833 self
834 }
835
836 #[must_use]
838 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
839 self.extra.insert(key.into(), value);
840 self
841 }
842
843 pub fn build(self) -> Result<CommunityRef, MetadataEntryBuildError> {
849 Ok(CommunityRef {
850 identifier: required_entry_field(self.identifier, "CommunityRef", "identifier")?,
851 extra: self.extra,
852 })
853 }
854}
855
856#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
858pub struct GrantRef {
859 pub id: String,
861 #[serde(flatten, default)]
863 pub extra: BTreeMap<String, Value>,
864}
865
866impl GrantRef {
867 #[must_use]
869 pub fn builder() -> GrantRefBuilder {
870 GrantRefBuilder::default()
871 }
872
873 #[must_use]
884 pub fn new(id: impl Into<String>) -> Self {
885 Self {
886 id: id.into(),
887 ..Self::default()
888 }
889 }
890}
891
892#[derive(Clone, Debug, PartialEq, Eq, Default)]
894pub struct GrantRefBuilder {
895 id: Option<String>,
896 extra: BTreeMap<String, Value>,
897}
898
899impl GrantRefBuilder {
900 #[must_use]
902 pub fn id(mut self, id: impl Into<String>) -> Self {
903 self.id = Some(id.into());
904 self
905 }
906
907 #[must_use]
909 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
910 self.extra = extra;
911 self
912 }
913
914 #[must_use]
916 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
917 self.extra.insert(key.into(), value);
918 self
919 }
920
921 pub fn build(self) -> Result<GrantRef, MetadataEntryBuildError> {
927 Ok(GrantRef {
928 id: required_entry_field(self.id, "GrantRef", "id")?,
929 extra: self.extra,
930 })
931 }
932}
933
934#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
936pub struct LicenseRef {
937 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub id: Option<String>,
940 #[serde(default, skip_serializing_if = "Option::is_none")]
942 pub title: Option<String>,
943 #[serde(flatten, default)]
945 pub extra: BTreeMap<String, Value>,
946}
947
948impl LicenseRef {
949 #[must_use]
951 pub fn builder() -> LicenseRefBuilder {
952 LicenseRefBuilder::default()
953 }
954
955 #[must_use]
957 pub fn new(id: impl Into<String>) -> Self {
958 Self {
959 id: Some(id.into()),
960 ..Self::default()
961 }
962 }
963}
964
965#[derive(Clone, Debug, PartialEq, Eq, Default)]
967pub struct LicenseRefBuilder {
968 id: Option<String>,
969 title: Option<String>,
970 extra: BTreeMap<String, Value>,
971}
972
973impl LicenseRefBuilder {
974 #[must_use]
976 pub fn id(mut self, id: impl Into<String>) -> Self {
977 self.id = Some(id.into());
978 self
979 }
980
981 #[must_use]
983 pub fn title(mut self, title: impl Into<String>) -> Self {
984 self.title = Some(title.into());
985 self
986 }
987
988 #[must_use]
990 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
991 self.extra = extra;
992 self
993 }
994
995 #[must_use]
997 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
998 self.extra.insert(key.into(), value);
999 self
1000 }
1001
1002 #[must_use]
1004 pub fn build(self) -> LicenseRef {
1005 LicenseRef {
1006 id: self.id,
1007 title: self.title,
1008 extra: self.extra,
1009 }
1010 }
1011}
1012
1013#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1015pub struct ResourceType {
1016 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
1018 pub type_: Option<String>,
1019 #[serde(default, skip_serializing_if = "Option::is_none")]
1021 pub subtype: Option<String>,
1022 #[serde(default, skip_serializing_if = "Option::is_none")]
1024 pub title: Option<String>,
1025 #[serde(flatten, default)]
1027 pub extra: BTreeMap<String, Value>,
1028}
1029
1030impl ResourceType {
1031 #[must_use]
1033 pub fn builder() -> ResourceTypeBuilder {
1034 ResourceTypeBuilder::default()
1035 }
1036
1037 #[must_use]
1039 pub fn new(type_: impl Into<String>) -> Self {
1040 Self {
1041 type_: Some(type_.into()),
1042 ..Self::default()
1043 }
1044 }
1045}
1046
1047#[derive(Clone, Debug, PartialEq, Eq, Default)]
1049pub struct ResourceTypeBuilder {
1050 type_: Option<String>,
1051 subtype: Option<String>,
1052 title: Option<String>,
1053 extra: BTreeMap<String, Value>,
1054}
1055
1056impl ResourceTypeBuilder {
1057 #[must_use]
1059 pub fn type_(mut self, type_: impl Into<String>) -> Self {
1060 self.type_ = Some(type_.into());
1061 self
1062 }
1063
1064 #[must_use]
1066 pub fn subtype(mut self, subtype: impl Into<String>) -> Self {
1067 self.subtype = Some(subtype.into());
1068 self
1069 }
1070
1071 #[must_use]
1073 pub fn title(mut self, title: impl Into<String>) -> Self {
1074 self.title = Some(title.into());
1075 self
1076 }
1077
1078 #[must_use]
1080 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
1081 self.extra = extra;
1082 self
1083 }
1084
1085 #[must_use]
1087 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
1088 self.extra.insert(key.into(), value);
1089 self
1090 }
1091
1092 #[must_use]
1094 pub fn build(self) -> ResourceType {
1095 ResourceType {
1096 type_: self.type_,
1097 subtype: self.subtype,
1098 title: self.title,
1099 extra: self.extra,
1100 }
1101 }
1102}
1103
1104#[derive(Clone, Debug, PartialEq, Eq, Error)]
1106pub enum MetadataEntryBuildError {
1107 #[error("missing required {entry} field: {field}")]
1109 MissingField {
1110 entry: &'static str,
1112 field: &'static str,
1114 },
1115}
1116
1117fn required_entry_field<T>(
1118 value: Option<T>,
1119 entry: &'static str,
1120 field: &'static str,
1121) -> Result<T, MetadataEntryBuildError> {
1122 value.ok_or(MetadataEntryBuildError::MissingField { entry, field })
1123}
1124
1125#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1127pub struct DepositMetadataUpdate {
1128 pub title: String,
1130 pub upload_type: UploadType,
1132 #[serde(default, skip_serializing_if = "Option::is_none")]
1134 pub publication_date: Option<NaiveDate>,
1135 #[serde(rename = "description")]
1137 pub description_html: String,
1138 #[serde(default)]
1140 pub creators: Vec<Creator>,
1141 pub access_right: AccessRight,
1143 #[serde(default, skip_serializing_if = "Option::is_none")]
1145 pub license: Option<String>,
1146 #[serde(default)]
1148 pub keywords: Vec<String>,
1149 #[serde(default)]
1151 pub related_identifiers: Vec<RelatedIdentifier>,
1152 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub notes: Option<String>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub version: Option<String>,
1158 #[serde(default)]
1160 pub communities: Vec<CommunityRef>,
1161 #[serde(default)]
1163 pub grants: Vec<GrantRef>,
1164 #[serde(flatten, default)]
1166 pub extra: BTreeMap<String, Value>,
1167}
1168
1169#[derive(Clone, Debug, PartialEq, Eq, Error)]
1171pub enum DepositMetadataBuildError {
1172 #[error("missing required deposit metadata field: {field}")]
1174 MissingField {
1175 field: &'static str,
1177 },
1178}
1179
1180#[derive(Clone, Debug, PartialEq, Eq, Default)]
1182pub struct DepositMetadataUpdateBuilder {
1183 title: Option<String>,
1184 upload_type: Option<UploadType>,
1185 publication_date: Option<NaiveDate>,
1186 description_html: Option<String>,
1187 creators: Vec<Creator>,
1188 access_right: Option<AccessRight>,
1189 license: Option<String>,
1190 keywords: Vec<String>,
1191 related_identifiers: Vec<RelatedIdentifier>,
1192 notes: Option<String>,
1193 version: Option<String>,
1194 communities: Vec<CommunityRef>,
1195 grants: Vec<GrantRef>,
1196 extra: BTreeMap<String, Value>,
1197}
1198
1199impl DepositMetadataUpdate {
1200 #[must_use]
1225 pub fn builder() -> DepositMetadataUpdateBuilder {
1226 DepositMetadataUpdateBuilder::default()
1227 }
1228}
1229
1230impl DepositMetadataUpdateBuilder {
1231 #[must_use]
1233 pub fn title(mut self, title: impl Into<String>) -> Self {
1234 self.title = Some(title.into());
1235 self
1236 }
1237
1238 #[must_use]
1240 pub fn upload_type(mut self, upload_type: UploadType) -> Self {
1241 self.upload_type = Some(upload_type);
1242 self
1243 }
1244
1245 #[must_use]
1247 pub fn publication_date(mut self, publication_date: NaiveDate) -> Self {
1248 self.publication_date = Some(publication_date);
1249 self
1250 }
1251
1252 #[must_use]
1254 pub fn description_html(mut self, description_html: impl Into<String>) -> Self {
1255 self.description_html = Some(description_html.into());
1256 self
1257 }
1258
1259 #[must_use]
1261 pub fn creators(mut self, creators: Vec<Creator>) -> Self {
1262 self.creators = creators;
1263 self
1264 }
1265
1266 #[must_use]
1268 pub fn creator(mut self, creator: Creator) -> Self {
1269 self.creators.push(creator);
1270 self
1271 }
1272
1273 #[must_use]
1275 pub fn creator_named(mut self, name: impl Into<String>) -> Self {
1276 self.creators.push(Creator::named(name));
1277 self
1278 }
1279
1280 #[must_use]
1282 pub fn access_right(mut self, access_right: AccessRight) -> Self {
1283 self.access_right = Some(access_right);
1284 self
1285 }
1286
1287 #[must_use]
1289 pub fn license(mut self, license: impl Into<String>) -> Self {
1290 self.license = Some(license.into());
1291 self
1292 }
1293
1294 #[must_use]
1296 pub fn keywords(mut self, keywords: Vec<String>) -> Self {
1297 self.keywords = keywords;
1298 self
1299 }
1300
1301 #[must_use]
1303 pub fn keyword(mut self, keyword: impl Into<String>) -> Self {
1304 self.keywords.push(keyword.into());
1305 self
1306 }
1307
1308 #[must_use]
1310 pub fn related_identifiers(mut self, related_identifiers: Vec<RelatedIdentifier>) -> Self {
1311 self.related_identifiers = related_identifiers;
1312 self
1313 }
1314
1315 #[must_use]
1317 pub fn related_identifier(mut self, related_identifier: RelatedIdentifier) -> Self {
1318 self.related_identifiers.push(related_identifier);
1319 self
1320 }
1321
1322 #[must_use]
1324 pub fn notes(mut self, notes: impl Into<String>) -> Self {
1325 self.notes = Some(notes.into());
1326 self
1327 }
1328
1329 #[must_use]
1331 pub fn version(mut self, version: impl Into<String>) -> Self {
1332 self.version = Some(version.into());
1333 self
1334 }
1335
1336 #[must_use]
1338 pub fn communities(mut self, communities: Vec<CommunityRef>) -> Self {
1339 self.communities = communities;
1340 self
1341 }
1342
1343 #[must_use]
1345 pub fn community(mut self, community: CommunityRef) -> Self {
1346 self.communities.push(community);
1347 self
1348 }
1349
1350 #[must_use]
1352 pub fn community_identifier(mut self, identifier: impl Into<String>) -> Self {
1353 self.communities.push(CommunityRef::new(identifier));
1354 self
1355 }
1356
1357 #[must_use]
1359 pub fn grants(mut self, grants: Vec<GrantRef>) -> Self {
1360 self.grants = grants;
1361 self
1362 }
1363
1364 #[must_use]
1366 pub fn grant(mut self, grant: GrantRef) -> Self {
1367 self.grants.push(grant);
1368 self
1369 }
1370
1371 #[must_use]
1373 pub fn grant_id(mut self, id: impl Into<String>) -> Self {
1374 self.grants.push(GrantRef::new(id));
1375 self
1376 }
1377
1378 #[must_use]
1380 pub fn extra(mut self, extra: BTreeMap<String, Value>) -> Self {
1381 self.extra = extra;
1382 self
1383 }
1384
1385 #[must_use]
1387 pub fn extra_field(mut self, key: impl Into<String>, value: Value) -> Self {
1388 self.extra.insert(key.into(), value);
1389 self
1390 }
1391
1392 pub fn build(self) -> Result<DepositMetadataUpdate, DepositMetadataBuildError> {
1417 Ok(DepositMetadataUpdate {
1418 title: required(self.title, "title")?,
1419 upload_type: required(self.upload_type, "upload_type")?,
1420 publication_date: self.publication_date,
1421 description_html: required(self.description_html, "description_html")?,
1422 creators: required_non_empty(self.creators, "creators")?,
1423 access_right: required(self.access_right, "access_right")?,
1424 license: self.license,
1425 keywords: self.keywords,
1426 related_identifiers: self.related_identifiers,
1427 notes: self.notes,
1428 version: self.version,
1429 communities: self.communities,
1430 grants: self.grants,
1431 extra: self.extra,
1432 })
1433 }
1434}
1435
1436fn required<T>(value: Option<T>, field: &'static str) -> Result<T, DepositMetadataBuildError> {
1437 value.ok_or(DepositMetadataBuildError::MissingField { field })
1438}
1439
1440fn required_non_empty<T>(
1441 value: Vec<T>,
1442 field: &'static str,
1443) -> Result<Vec<T>, DepositMetadataBuildError> {
1444 if value.is_empty() {
1445 return Err(DepositMetadataBuildError::MissingField { field });
1446 }
1447
1448 Ok(value)
1449}
1450
1451#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
1453pub struct RecordMetadata {
1454 pub title: String,
1456 #[serde(default, skip_serializing_if = "Option::is_none")]
1458 pub publication_date: Option<NaiveDate>,
1459 #[serde(
1461 rename = "description",
1462 default,
1463 skip_serializing_if = "Option::is_none"
1464 )]
1465 pub description_html: Option<String>,
1466 #[serde(default)]
1468 pub creators: Vec<Creator>,
1469 #[serde(default)]
1471 pub contributors: Vec<Contributor>,
1472 #[serde(default)]
1474 pub keywords: Vec<String>,
1475 #[serde(default)]
1477 pub references: Vec<String>,
1478 #[serde(default)]
1480 pub communities: Vec<CommunityRef>,
1481 #[serde(default)]
1483 pub grants: Vec<GrantRef>,
1484 #[serde(default)]
1486 pub subjects: Vec<Subject>,
1487 #[serde(default)]
1489 pub identifiers: Vec<RecordIdentifier>,
1490 #[serde(default)]
1492 pub alternate_identifiers: Vec<RecordIdentifier>,
1493 #[serde(default)]
1495 pub dates: Vec<RecordDate>,
1496 #[serde(default)]
1498 pub related_identifiers: Vec<RelatedIdentifier>,
1499 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub resource_type: Option<ResourceType>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1504 pub access_right: Option<AccessRight>,
1505 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub access_conditions: Option<String>,
1508 #[serde(default, skip_serializing_if = "Option::is_none")]
1510 pub embargo_date: Option<NaiveDate>,
1511 #[serde(default, skip_serializing_if = "Option::is_none")]
1513 pub license: Option<LicenseRef>,
1514 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub publisher: Option<String>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1519 pub language: Option<String>,
1520 #[serde(default)]
1522 pub sizes: Vec<String>,
1523 #[serde(default)]
1525 pub formats: Vec<String>,
1526 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub notes: Option<String>,
1529 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub version: Option<String>,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub journal_title: Option<String>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1537 pub journal_volume: Option<String>,
1538 #[serde(default, skip_serializing_if = "Option::is_none")]
1540 pub journal_issue: Option<String>,
1541 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub journal_pages: Option<String>,
1544 #[serde(default, skip_serializing_if = "Option::is_none")]
1546 pub conference_title: Option<String>,
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1549 pub conference_acronym: Option<String>,
1550 #[serde(default, skip_serializing_if = "Option::is_none")]
1552 pub conference_dates: Option<String>,
1553 #[serde(default, skip_serializing_if = "Option::is_none")]
1555 pub conference_place: Option<String>,
1556 #[serde(default, skip_serializing_if = "Option::is_none")]
1558 pub conference_url: Option<Url>,
1559 #[serde(default, skip_serializing_if = "Option::is_none")]
1561 pub conference_session: Option<String>,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 pub conference_session_part: Option<String>,
1565 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 pub imprint_publisher: Option<String>,
1568 #[serde(default, skip_serializing_if = "Option::is_none")]
1570 pub imprint_isbn: Option<String>,
1571 #[serde(default, skip_serializing_if = "Option::is_none")]
1573 pub imprint_place: Option<String>,
1574 #[serde(default, skip_serializing_if = "Option::is_none")]
1576 pub partof_title: Option<String>,
1577 #[serde(default, skip_serializing_if = "Option::is_none")]
1579 pub partof_pages: Option<String>,
1580 #[serde(default)]
1582 pub thesis_supervisors: Vec<Creator>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1585 pub thesis_university: Option<String>,
1586 #[serde(default, skip_serializing_if = "Option::is_none")]
1588 pub relations: Option<RecordRelations>,
1589 #[serde(flatten, default)]
1591 pub extra: BTreeMap<String, Value>,
1592}
1593
1594#[cfg(test)]
1595mod tests {
1596 use super::{
1597 AccessRight, CommunityRef, Contributor, Creator, DepositMetadataBuildError,
1598 DepositMetadataUpdate, GrantRef, LicenseRef, MetadataEntryBuildError, RecordDate,
1599 RecordIdentifier, RecordMetadata, RecordRelations, RecordVersionRelation,
1600 RelatedIdentifier, ResourceType, Subject, UploadType,
1601 };
1602 use chrono::NaiveDate;
1603 use serde_json::json;
1604
1605 #[test]
1606 fn metadata_builders_start_with_empty_optional_collections() {
1607 let metadata = DepositMetadataUpdate::builder()
1608 .title("Title")
1609 .upload_type(UploadType::Dataset)
1610 .description_html("<p>Desc</p>")
1611 .creator(Creator::builder().name("Doe, Jane").build().unwrap())
1612 .access_right(AccessRight::Open)
1613 .build()
1614 .unwrap();
1615
1616 assert!(metadata.keywords.is_empty());
1617 assert!(metadata.related_identifiers.is_empty());
1618 assert!(metadata.communities.is_empty());
1619 assert!(metadata.grants.is_empty());
1620 }
1621
1622 #[test]
1623 fn metadata_builder_requires_missing_fields() {
1624 let error = DepositMetadataUpdate::builder()
1625 .title("Title")
1626 .upload_type(UploadType::Dataset)
1627 .access_right(AccessRight::Open)
1628 .build()
1629 .unwrap_err();
1630
1631 assert_eq!(
1632 error,
1633 DepositMetadataBuildError::MissingField {
1634 field: "description_html",
1635 }
1636 );
1637 }
1638
1639 #[test]
1640 fn metadata_builder_requires_at_least_one_creator() {
1641 let error = DepositMetadataUpdate::builder()
1642 .title("Title")
1643 .upload_type(UploadType::Dataset)
1644 .description_html("<p>Desc</p>")
1645 .access_right(AccessRight::Open)
1646 .build()
1647 .unwrap_err();
1648
1649 assert_eq!(
1650 error,
1651 DepositMetadataBuildError::MissingField { field: "creators" }
1652 );
1653 }
1654
1655 #[test]
1656 fn metadata_builder_supports_full_optional_surface() {
1657 let publication_date = NaiveDate::from_ymd_opt(2026, 4, 3).unwrap();
1658 let primary_creator = Creator::builder()
1659 .name("Doe, Jane")
1660 .affiliation("Zenodo")
1661 .orcid("0000-0000-0000-0001")
1662 .build()
1663 .unwrap();
1664 let secondary_creator = Creator::builder()
1665 .name("Doe, John")
1666 .gnd("123456")
1667 .build()
1668 .unwrap();
1669 let related_identifier = RelatedIdentifier::builder()
1670 .identifier("10.5281/zenodo.42")
1671 .relation("isSupplementTo")
1672 .scheme("doi")
1673 .resource_type("dataset")
1674 .build()
1675 .unwrap();
1676 let related_identifier_extra = RelatedIdentifier::builder()
1677 .identifier("https://example.org")
1678 .relation("references")
1679 .build()
1680 .unwrap();
1681 let community = CommunityRef::new("zenodo");
1682 let extra_community = CommunityRef::new("sandbox");
1683 let grant = GrantRef::new("grant-1");
1684 let extra_grant = GrantRef::new("grant-2");
1685 let mut extra = std::collections::BTreeMap::new();
1686 extra.insert("language".into(), json!("en"));
1687
1688 let metadata = DepositMetadataUpdate::builder()
1689 .title("Complete")
1690 .upload_type(UploadType::Dataset)
1691 .publication_date(publication_date)
1692 .description_html("<p>Complete</p>")
1693 .creators(vec![primary_creator.clone()])
1694 .creator(secondary_creator.clone())
1695 .access_right(AccessRight::Embargoed)
1696 .license("cc-by-4.0")
1697 .keywords(vec!["rust".into()])
1698 .keyword("zenodo")
1699 .related_identifiers(vec![related_identifier.clone()])
1700 .related_identifier(related_identifier_extra.clone())
1701 .notes("generated in tests")
1702 .version("1.2.3")
1703 .communities(vec![community.clone()])
1704 .community(extra_community.clone())
1705 .grants(vec![grant.clone()])
1706 .grant(extra_grant.clone())
1707 .extra(extra)
1708 .extra_field("source", json!("tarpaulin"))
1709 .build()
1710 .unwrap();
1711
1712 assert_eq!(metadata.publication_date, Some(publication_date));
1713 assert_eq!(metadata.creators, vec![primary_creator, secondary_creator]);
1714 assert_eq!(metadata.access_right, AccessRight::Embargoed);
1715 assert_eq!(metadata.license.as_deref(), Some("cc-by-4.0"));
1716 assert_eq!(metadata.keywords, vec!["rust", "zenodo"]);
1717 assert_eq!(
1718 metadata.related_identifiers,
1719 vec![related_identifier, related_identifier_extra]
1720 );
1721 assert_eq!(metadata.notes.as_deref(), Some("generated in tests"));
1722 assert_eq!(metadata.version.as_deref(), Some("1.2.3"));
1723 assert_eq!(metadata.communities, vec![community, extra_community]);
1724 assert_eq!(metadata.grants, vec![grant, extra_grant]);
1725 assert_eq!(metadata.extra.get("language"), Some(&json!("en")));
1726 assert_eq!(metadata.extra.get("source"), Some(&json!("tarpaulin")));
1727 }
1728
1729 #[test]
1730 fn nested_metadata_entry_builders_cover_common_construction_paths() {
1731 let creator = Creator::builder()
1732 .name("Doe, Jane")
1733 .affiliation("Zenodo")
1734 .orcid("0000-0000-0000-0001")
1735 .gnd("98765")
1736 .extra_field("department", json!("Research"))
1737 .build()
1738 .unwrap();
1739 let contributor = Contributor::builder()
1740 .name("Doe, John")
1741 .role("DataManager")
1742 .affiliation("Zenodo")
1743 .build()
1744 .unwrap();
1745 let subject = Subject::builder()
1746 .term("chemistry")
1747 .identifier("123")
1748 .scheme("custom")
1749 .build()
1750 .unwrap();
1751 let identifier = RecordIdentifier::builder()
1752 .identifier("10.5281/zenodo.42")
1753 .scheme("doi")
1754 .build()
1755 .unwrap();
1756 let date = RecordDate::builder()
1757 .date("2026-04-03")
1758 .date_type("Collected")
1759 .description("Sampling day")
1760 .build()
1761 .unwrap();
1762 let related = RelatedIdentifier::builder()
1763 .identifier("10.5281/zenodo.41")
1764 .relation("isVersionOf")
1765 .scheme("doi")
1766 .resource_type("dataset")
1767 .build()
1768 .unwrap();
1769 let community = CommunityRef::builder()
1770 .identifier("zenodo")
1771 .build()
1772 .unwrap();
1773 let grant = GrantRef::builder().id("grant-1").build().unwrap();
1774 let license = LicenseRef::builder()
1775 .id("cc-by-4.0")
1776 .title("CC BY 4.0")
1777 .build();
1778 let resource_type = ResourceType::builder()
1779 .type_("dataset")
1780 .subtype("image")
1781 .title("Dataset")
1782 .build();
1783
1784 assert_eq!(creator.name, "Doe, Jane");
1785 assert_eq!(contributor.type_, "DataManager");
1786 assert_eq!(subject.term, "chemistry");
1787 assert_eq!(identifier.scheme.as_deref(), Some("doi"));
1788 assert_eq!(date.type_.as_deref(), Some("Collected"));
1789 assert_eq!(related.resource_type.as_deref(), Some("dataset"));
1790 assert_eq!(community.identifier, "zenodo");
1791 assert_eq!(grant.id, "grant-1");
1792 assert_eq!(license.id.as_deref(), Some("cc-by-4.0"));
1793 assert_eq!(resource_type.type_.as_deref(), Some("dataset"));
1794 }
1795
1796 #[test]
1797 fn person_and_relation_metadata_builders_cover_full_surface() {
1798 let mut creator_extra = std::collections::BTreeMap::new();
1799 creator_extra.insert("department".into(), json!("Research"));
1800 let creator = Creator::builder()
1801 .name("Doe, Jane")
1802 .affiliation("Zenodo")
1803 .orcid("0000-0000-0000-0001")
1804 .gnd("98765")
1805 .extra(creator_extra.clone())
1806 .extra_field("lab", json!("Core"))
1807 .build()
1808 .unwrap();
1809 assert_eq!(Creator::named("Named Only").name, "Named Only");
1810 assert_eq!(creator.extra.get("department"), Some(&json!("Research")));
1811 assert_eq!(creator.extra.get("lab"), Some(&json!("Core")));
1812
1813 let mut contributor_extra = std::collections::BTreeMap::new();
1814 contributor_extra.insert("x".into(), json!(1));
1815 let contributor = Contributor::builder()
1816 .name("Doe, John")
1817 .role("Editor")
1818 .affiliation("Zenodo")
1819 .orcid("0000-0000-0000-0002")
1820 .gnd("12345")
1821 .extra(contributor_extra)
1822 .extra_field("y", json!(2))
1823 .build()
1824 .unwrap();
1825 assert_eq!(Contributor::new("Ada", "Supervisor").type_, "Supervisor");
1826 assert_eq!(contributor.extra.get("x"), Some(&json!(1)));
1827 assert_eq!(contributor.extra.get("y"), Some(&json!(2)));
1828
1829 let mut related_extra = std::collections::BTreeMap::new();
1830 related_extra.insert("strength".into(), json!("primary"));
1831 let related = RelatedIdentifier::builder()
1832 .identifier("10.5281/zenodo.1")
1833 .relation("isVersionOf")
1834 .scheme("doi")
1835 .resource_type("dataset")
1836 .extra(related_extra)
1837 .extra_field("note", json!("important"))
1838 .build()
1839 .unwrap();
1840 assert_eq!(
1841 RelatedIdentifier::new("10.5281/zenodo.2", "references").relation,
1842 "references"
1843 );
1844 assert_eq!(related.extra.get("strength"), Some(&json!("primary")));
1845 assert_eq!(related.extra.get("note"), Some(&json!("important")));
1846 }
1847
1848 #[test]
1849 fn classification_metadata_builders_cover_full_surface() {
1850 let mut subject_extra = std::collections::BTreeMap::new();
1851 subject_extra.insert("priority".into(), json!("high"));
1852 let subject = Subject::builder()
1853 .term("chemistry")
1854 .identifier("123")
1855 .scheme("custom")
1856 .extra(subject_extra)
1857 .extra_field("group", json!("A"))
1858 .build()
1859 .unwrap();
1860 assert_eq!(Subject::new("physics").term, "physics");
1861 assert_eq!(subject.extra.get("priority"), Some(&json!("high")));
1862 assert_eq!(subject.extra.get("group"), Some(&json!("A")));
1863
1864 let mut identifier_extra = std::collections::BTreeMap::new();
1865 identifier_extra.insert("kind".into(), json!("alternate"));
1866 let identifier = RecordIdentifier::builder()
1867 .identifier("10.5281/zenodo.42")
1868 .scheme("doi")
1869 .extra(identifier_extra)
1870 .extra_field("source", json!("Zenodo"))
1871 .build()
1872 .unwrap();
1873 assert_eq!(RecordIdentifier::new("ark:/123").identifier, "ark:/123");
1874 assert_eq!(identifier.extra.get("kind"), Some(&json!("alternate")));
1875 assert_eq!(identifier.extra.get("source"), Some(&json!("Zenodo")));
1876
1877 let mut date_extra = std::collections::BTreeMap::new();
1878 date_extra.insert("certainty".into(), json!("exact"));
1879 let date = RecordDate::builder()
1880 .date("2026-04-03")
1881 .date_type("Collected")
1882 .description("Sampling day")
1883 .extra(date_extra)
1884 .extra_field("timezone", json!("UTC"))
1885 .build()
1886 .unwrap();
1887 assert_eq!(RecordDate::new("2026-04-04").date, "2026-04-04");
1888 assert_eq!(date.extra.get("certainty"), Some(&json!("exact")));
1889 assert_eq!(date.extra.get("timezone"), Some(&json!("UTC")));
1890 }
1891
1892 #[test]
1893 fn reference_metadata_builders_cover_full_surface() {
1894 let mut community_extra = std::collections::BTreeMap::new();
1895 community_extra.insert("owner".into(), json!("zenodo"));
1896 let community = CommunityRef::builder()
1897 .identifier("zenodo")
1898 .extra(community_extra)
1899 .extra_field("scope", json!("public"))
1900 .build()
1901 .unwrap();
1902 assert_eq!(CommunityRef::new("sandbox").identifier, "sandbox");
1903 assert_eq!(community.extra.get("owner"), Some(&json!("zenodo")));
1904 assert_eq!(community.extra.get("scope"), Some(&json!("public")));
1905
1906 let mut grant_extra = std::collections::BTreeMap::new();
1907 grant_extra.insert("agency".into(), json!("EU"));
1908 let grant = GrantRef::builder()
1909 .id("grant-1")
1910 .extra(grant_extra)
1911 .extra_field("call", json!("Horizon"))
1912 .build()
1913 .unwrap();
1914 assert_eq!(GrantRef::new("grant-2").id, "grant-2");
1915 assert_eq!(grant.extra.get("agency"), Some(&json!("EU")));
1916 assert_eq!(grant.extra.get("call"), Some(&json!("Horizon")));
1917
1918 let mut license_extra = std::collections::BTreeMap::new();
1919 license_extra.insert("jurisdiction".into(), json!("EU"));
1920 let license = LicenseRef::builder()
1921 .id("cc-by-4.0")
1922 .title("CC BY 4.0")
1923 .extra(license_extra)
1924 .extra_field("version", json!("4.0"))
1925 .build();
1926 assert_eq!(LicenseRef::new("mit").id.as_deref(), Some("mit"));
1927 assert_eq!(license.extra.get("jurisdiction"), Some(&json!("EU")));
1928 assert_eq!(license.extra.get("version"), Some(&json!("4.0")));
1929
1930 let mut resource_type_extra = std::collections::BTreeMap::new();
1931 resource_type_extra.insert("family".into(), json!("research-data"));
1932 let resource_type = ResourceType::builder()
1933 .type_("dataset")
1934 .subtype("image")
1935 .title("Dataset")
1936 .extra(resource_type_extra)
1937 .extra_field("display", json!("Data set"))
1938 .build();
1939 assert_eq!(
1940 ResourceType::new("software").type_.as_deref(),
1941 Some("software")
1942 );
1943 assert_eq!(
1944 resource_type.extra.get("family"),
1945 Some(&json!("research-data"))
1946 );
1947 assert_eq!(resource_type.extra.get("display"), Some(&json!("Data set")));
1948 }
1949
1950 #[test]
1951 fn deposit_metadata_builder_shortcuts_are_exercised() {
1952 let metadata = DepositMetadataUpdate::builder()
1953 .title("Example")
1954 .upload_type(UploadType::Software)
1955 .description_html("<p>Example</p>")
1956 .creator_named("Doe, Jane")
1957 .access_right(AccessRight::Open)
1958 .community_identifier("zenodo")
1959 .grant_id("grant-1")
1960 .build()
1961 .unwrap();
1962
1963 assert_eq!(metadata.creators[0].name, "Doe, Jane");
1964 assert_eq!(metadata.communities[0].identifier, "zenodo");
1965 assert_eq!(metadata.grants[0].id, "grant-1");
1966 }
1967
1968 #[test]
1969 fn nested_metadata_entry_builders_require_mandatory_fields() {
1970 assert_eq!(
1971 Creator::builder().build().unwrap_err(),
1972 MetadataEntryBuildError::MissingField {
1973 entry: "Creator",
1974 field: "name",
1975 }
1976 );
1977 assert_eq!(
1978 Contributor::builder().name("Doe").build().unwrap_err(),
1979 MetadataEntryBuildError::MissingField {
1980 entry: "Contributor",
1981 field: "role",
1982 }
1983 );
1984 assert_eq!(
1985 RelatedIdentifier::builder()
1986 .identifier("10.5281/zenodo.1")
1987 .build()
1988 .unwrap_err(),
1989 MetadataEntryBuildError::MissingField {
1990 entry: "RelatedIdentifier",
1991 field: "relation",
1992 }
1993 );
1994 }
1995
1996 #[test]
1997 fn enums_preserve_unknown_values() {
1998 let upload: UploadType = serde_json::from_str("\"posterish\"").unwrap();
1999 let access: AccessRight = serde_json::from_str("\"members-only\"").unwrap();
2000
2001 assert_eq!(serde_json::to_string(&upload).unwrap(), "\"posterish\"");
2002 assert_eq!(serde_json::to_string(&access).unwrap(), "\"members-only\"");
2003 }
2004
2005 #[test]
2006 fn known_enum_values_round_trip() {
2007 let upload = serde_json::to_string(&UploadType::Dataset).unwrap();
2008 let access = serde_json::to_string(&AccessRight::Open).unwrap();
2009 let metadata = RecordMetadata::default();
2010
2011 assert_eq!(upload, "\"dataset\"");
2012 assert_eq!(access, "\"open\"");
2013 assert!(metadata.creators.is_empty());
2014 }
2015
2016 #[test]
2017 fn published_record_metadata_deserializes_richer_typed_fields() {
2018 let metadata: RecordMetadata = serde_json::from_str(
2019 r#"{
2020 "title": "Rich record",
2021 "publication_date": "2026-04-03",
2022 "description": "<p>Rich</p>",
2023 "creators": [{ "name": "Doe, Jane" }],
2024 "contributors": [{ "name": "Doe, John", "type": "Editor" }],
2025 "keywords": ["rust", "zenodo"],
2026 "references": ["Doe J. Example."],
2027 "communities": [{ "identifier": "zenodo" }],
2028 "grants": [{ "id": "777541" }],
2029 "subjects": [{ "term": "Metadata", "scheme": "custom" }],
2030 "identifiers": [{ "identifier": "sha256:abc", "scheme": "sha256" }],
2031 "alternate_identifiers": [{ "identifier": "arXiv:1234.5678", "scheme": "arxiv" }],
2032 "dates": [{ "date": "2026-04-03", "type": "Collected" }],
2033 "related_identifiers": [{ "identifier": "10.5281/zenodo.42", "relation": "isSupplementTo" }],
2034 "resource_type": { "type": "dataset", "title": "Dataset" },
2035 "access_right": "open",
2036 "access_conditions": "By request",
2037 "license": { "id": "cc-by-4.0", "title": "Creative Commons Attribution 4.0 International" },
2038 "publisher": "Zenodo",
2039 "language": "eng",
2040 "sizes": ["1 file"],
2041 "formats": ["application/gzip"],
2042 "notes": "Some notes",
2043 "version": "1.2.3",
2044 "journal_title": "Journal of Rust",
2045 "journal_volume": "12",
2046 "journal_issue": "3",
2047 "journal_pages": "10-20",
2048 "conference_title": "RustConf",
2049 "conference_acronym": "RC",
2050 "conference_dates": "2026-04-03",
2051 "conference_place": "Rome, Italy",
2052 "conference_url": "https://example.org/conf",
2053 "conference_session": "A",
2054 "conference_session_part": "1",
2055 "imprint_publisher": "Example Press",
2056 "imprint_isbn": "978-1-234",
2057 "imprint_place": "Rome, Italy",
2058 "partof_title": "Collected Works",
2059 "partof_pages": "44-50",
2060 "thesis_supervisors": [{ "name": "Professor, Ada" }],
2061 "thesis_university": "Example University",
2062 "relations": {
2063 "version": [{ "index": 2, "count": 4, "is_last": false }]
2064 }
2065 }"#,
2066 )
2067 .unwrap();
2068
2069 assert_eq!(
2070 metadata.contributors,
2071 vec![Contributor {
2072 name: "Doe, John".into(),
2073 type_: "Editor".into(),
2074 affiliation: None,
2075 orcid: None,
2076 gnd: None,
2077 extra: std::collections::BTreeMap::default(),
2078 }]
2079 );
2080 assert_eq!(
2081 metadata.subjects,
2082 vec![Subject {
2083 term: "Metadata".into(),
2084 identifier: None,
2085 scheme: Some("custom".into()),
2086 extra: std::collections::BTreeMap::default(),
2087 }]
2088 );
2089 assert_eq!(metadata.access_right, Some(AccessRight::Open));
2090 assert_eq!(metadata.publisher.as_deref(), Some("Zenodo"));
2091 assert_eq!(
2092 metadata.relations,
2093 Some(RecordRelations {
2094 version: vec![RecordVersionRelation {
2095 index: Some(2),
2096 count: Some(4),
2097 is_last: Some(false),
2098 extra: std::collections::BTreeMap::default(),
2099 }],
2100 extra: std::collections::BTreeMap::default(),
2101 })
2102 );
2103 assert_eq!(
2104 metadata.thesis_university.as_deref(),
2105 Some("Example University")
2106 );
2107 }
2108
2109 #[test]
2110 fn published_record_metadata_accepts_community_id_alias() {
2111 let metadata: RecordMetadata = serde_json::from_str(
2112 r#"{
2113 "title": "Rich record",
2114 "creators": [{ "name": "Doe, Jane" }],
2115 "communities": [
2116 { "id": "earth-metabolome", "title": "Earth Metabolome" }
2117 ]
2118 }"#,
2119 )
2120 .unwrap();
2121
2122 assert_eq!(metadata.communities.len(), 1);
2123 assert_eq!(metadata.communities[0].identifier, "earth-metabolome");
2124 assert_eq!(
2125 metadata.communities[0].extra.get("title"),
2126 Some(&json!("Earth Metabolome"))
2127 );
2128 }
2129
2130 #[test]
2131 fn record_version_relations_accept_integer_like_numeric_shapes() {
2132 let metadata: RecordMetadata = serde_json::from_str(
2133 r#"{
2134 "title": "Rich record",
2135 "creators": [{ "name": "Doe, Jane" }],
2136 "relations": {
2137 "version": [{ "index": "2", "count": 4.0, "is_last": true }]
2138 }
2139 }"#,
2140 )
2141 .unwrap();
2142
2143 assert_eq!(
2144 metadata.relations,
2145 Some(RecordRelations {
2146 version: vec![RecordVersionRelation {
2147 index: Some(2),
2148 count: Some(4),
2149 is_last: Some(true),
2150 extra: std::collections::BTreeMap::default(),
2151 }],
2152 extra: std::collections::BTreeMap::default(),
2153 })
2154 );
2155 }
2156}