1use crate::{
2 enums::{ContributorRole, InstitutionIdType, Iso639_1, MediaTypeDate, NameStyle, Sequence},
3 journal::metadata::{ArchiveLocations, DoiData},
4 regex::{INSTITUTION_PID_REGEX, NAME_REGEX, ORCID_REGEX},
5 serializers::*,
6};
7use serde::Serialize;
8use validator::Validate;
9
10#[derive(Debug, Clone, Default, Serialize, Validate)]
11pub struct JournalIssue {
12 #[serde(default, skip_serializing_if = "Vec::is_empty")]
13 #[validate(nested)]
14 pub contributors: Vec<Contributor>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 #[validate(nested)]
17 pub titles: Option<Titles>,
18 #[validate(nested)]
19 pub publication_date: PublicationDate,
20 #[serde(skip_serializing_if = "Option::is_none")]
22 #[validate(length(min = 1, max = 32))]
23 pub issue: Option<String>,
24 #[serde(skip_serializing_if = "Option::is_none")]
25 #[validate(length(min = 1, max = 15))]
26 pub special_numbering: Option<String>,
27 #[validate(nested)]
28 pub archive_locations: ArchiveLocations,
29 #[validate(nested)]
30 pub doi_data: DoiData,
31}
32
33#[derive(Debug, Clone, Default, Serialize, Validate)]
34pub struct PublicationDate {
35 #[serde(rename = "@media_type")]
36 pub media_type: MediaTypeDate,
37 #[serde(serialize_with = "serialize_two_digits")]
38 #[serde(skip_serializing_if = "Option::is_none")]
39 #[validate(range(min = 1, max = 34))]
40 pub month: Option<u32>,
41 #[serde(serialize_with = "serialize_two_digits")]
42 #[serde(skip_serializing_if = "Option::is_none")]
43 #[validate(range(min = 1, max = 31))]
44 pub day: Option<u32>,
45 #[validate(range(min = 1400, max = 2200))]
46 pub year: i32,
47}
48
49#[derive(Debug, Clone, Default, Serialize, Validate)]
50pub struct Titles {
51 pub title: String,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 pub subtitle: Option<String>,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 #[validate(nested)]
56 pub original_language_title: Option<OriginalLanguageTitle>,
57 #[serde(rename = "subtitle")]
58 #[serde(skip_serializing_if = "Option::is_none")]
59 pub orginal_language_subtitle: Option<String>,
60}
61
62#[derive(Debug, Clone, Default, Serialize, Validate)]
63pub struct OriginalLanguageTitle {
64 #[serde(rename = "@language")]
65 pub language: Iso639_1,
66 #[serde(rename = "$text")]
67 pub value: String,
68}
69
70#[derive(Debug, Clone, Default, Serialize, Validate)]
85pub struct Contributors {
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
87 #[serde(rename = "$text")]
88 pub value: Vec<Contributor>,
89}
90
91#[derive(Debug, Clone, Default, Serialize, Validate)]
92pub struct Contributor {
93 #[serde(skip_serializing_if = "Vec::is_empty")]
94 #[validate(nested)]
95 pub organization: Vec<Organization>,
96 #[serde(skip_serializing_if = "Vec::is_empty")]
97 #[validate(nested)]
98 pub person_name: Vec<PersonName>,
99 #[serde(skip_serializing_if = "Vec::is_empty")]
100 #[validate(nested)]
101 pub anonymous: Vec<Anonymous>,
102}
103
104#[derive(Debug, Clone, Default, Serialize, Validate)]
105pub struct Organization {
106 #[serde(rename = "$text")]
107 pub value: String,
108 #[serde(rename = "@language")]
109 pub language: Iso639_1,
110 #[serde(rename = "@sequence")]
111 pub sequence: Sequence,
112 #[serde(rename = "@contributor_role")]
113 pub contributor_role: ContributorRole,
114}
115
116#[derive(Debug, Clone, Default, Serialize, Validate)]
117pub struct Anonymous {
118 #[serde(rename = "@language")]
119 pub language: Iso639_1,
120 #[serde(rename = "@sequence")]
121 pub sequence: Sequence,
122 #[serde(rename = "@contributor_role")]
123 pub contributor_role: ContributorRole,
124 #[serde(rename = "@name-style")]
125 #[serde(skip_serializing_if = "Option::is_none")]
126 pub name_style: Option<NameStyle>,
127}
128
129#[derive(Debug, Clone, Default, Serialize, Validate)]
130pub struct PersonName {
131 #[serde(rename = "@language")]
132 pub language: Iso639_1,
133 #[serde(rename = "@sequence")]
134 pub sequence: Sequence,
135 #[serde(rename = "@contributor_role")]
136 pub contributor_role: ContributorRole,
137 #[serde(rename = "@name-style")]
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub name_style: Option<NameStyle>,
140 #[validate(length(min = 1, max = 60))]
141 #[validate(regex(path = *NAME_REGEX))]
142 pub given_name: String,
143 #[validate(length(min = 1, max = 60))]
144 #[validate(regex(path = *NAME_REGEX))]
145 pub surname: String,
146 #[validate(length(min = 1, max = 10))]
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub suffix: Option<String>,
149 #[serde(skip_serializing_if = "Affiliations::is_empty")]
150 #[validate(nested)]
151 pub affiliations: Affiliations,
152 #[serde(rename = "ORCID")]
153 #[serde(skip_serializing_if = "Option::is_none")]
154 #[validate(nested)]
155 pub orcid: Option<Orcid>,
156 }
158
159impl Affiliations {
160 pub fn is_empty(&self) -> bool {
161 self.institution.is_empty()
162 }
163}
164
165#[derive(Debug, Clone, Default, Serialize, Validate)]
166pub struct Orcid {
167 #[serde(rename = "@authenticated")]
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub authenticated: Option<bool>,
170 #[serde(rename = "$text")]
171 #[validate(regex(path = *ORCID_REGEX))]
172 pub value: String,
173}
174
175#[derive(Debug, Clone, Default, Serialize, Validate)]
176pub struct Affiliations {
177 #[validate(nested)]
178 pub institution: Vec<Institution>,
179}
180
181#[derive(Debug, Clone, Default, Serialize, Validate)]
182pub struct Institution {
183 #[validate(nested)]
184 pub institution_id: InstitutionId,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Validate)]
188pub struct InstitutionId {
189 #[serde(rename = "@type")]
190 pub institution_type: InstitutionIdType,
191 #[serde(rename = "$text")]
192 #[validate(regex(path = *INSTITUTION_PID_REGEX))]
193 pub value: String,
194}
195
196#[cfg(test)]
197mod unit {
198 use super::*;
199
200 mod institution_id {
201 use super::*;
202
203 fn valid_institution_id() -> InstitutionId {
204 InstitutionId {
205 institution_type: InstitutionIdType::Ror,
206 value: "https://ror.org/12345".to_string(),
207 }
208 }
209
210 #[test]
211 fn valid_institution_id_passes() {
212 let inst_id = valid_institution_id();
213 assert!(inst_id.validate().is_ok());
214 }
215
216 #[test]
217 fn invalid_url_fails() {
218 let mut inst_id = valid_institution_id();
219 inst_id.value = "http://example.com".to_string(); assert!(inst_id.validate().is_err());
221 }
222
223 #[test]
224 fn various_institution_types_pass() {
225 let types = [
226 InstitutionIdType::Ror,
227 InstitutionIdType::Isni,
228 InstitutionIdType::Wikidata,
229 ];
230 for inst_type in types {
231 let mut inst_id = valid_institution_id();
232 inst_id.institution_type = inst_type;
233 assert!(inst_id.validate().is_ok());
234 }
235 }
236 }
237
238 mod institution {
239 use super::*;
240
241 fn valid_institution() -> Institution {
242 Institution {
243 institution_id: InstitutionId {
244 institution_type: InstitutionIdType::Ror,
245 value: "https://ror.org/12345".to_string(),
246 },
247 }
248 }
249
250 #[test]
251 fn valid_institution_passes() {
252 let inst = valid_institution();
253 assert!(inst.validate().is_ok());
254 }
255 }
256
257 mod affiliations {
258 use super::*;
259
260 fn valid_affiliation() -> Affiliations {
261 Affiliations {
262 institution: vec![Institution {
263 institution_id: InstitutionId {
264 institution_type: InstitutionIdType::Ror,
265 value: "https://ror.org/12345".to_string(),
266 },
267 }],
268 }
269 }
270
271 #[test]
272 fn valid_affiliations_passes() {
273 let aff = valid_affiliation();
274 assert!(aff.validate().is_ok());
275 assert!(!aff.is_empty());
276 }
277
278 #[test]
279 fn empty_affiliations_passes() {
280 let aff = Affiliations::default();
281 assert!(aff.validate().is_ok());
282 assert!(aff.is_empty());
283 }
284
285 #[test]
286 fn multiple_institutions_pass() {
287 let aff = Affiliations {
288 institution: vec![
289 Institution {
290 institution_id: InstitutionId {
291 institution_type: InstitutionIdType::Ror,
292 value: "https://ror.org/12345".to_string(),
293 },
294 },
295 Institution {
296 institution_id: InstitutionId {
297 institution_type: InstitutionIdType::Isni,
298 value: "https://isni.org/abc".to_string(),
299 },
300 },
301 ],
302 };
303 assert!(aff.validate().is_ok());
304 }
305 }
306
307 mod orcid {
308 use super::*;
309
310 fn valid_orcid() -> Orcid {
311 Orcid {
312 authenticated: Some(true),
313 value: "https://orcid.org/0000-0002-1825-0097".to_string(),
314 }
315 }
316
317 #[test]
318 fn valid_orcid_passes() {
319 let orcid = valid_orcid();
320 assert!(orcid.validate().is_ok());
321 }
322
323 #[test]
324 fn invalid_orcid_format_fails() {
325 let mut orcid = valid_orcid();
326 orcid.value = "invalid-orcid".to_string();
327 assert!(orcid.validate().is_err());
328 }
329
330 #[test]
331 fn orcid_without_protocol_fails() {
332 let mut orcid = valid_orcid();
333 orcid.value = "orcid.org/0000-0002-1825-0097".to_string();
334 assert!(orcid.validate().is_err());
335 }
336
337 #[test]
338 fn authenticated_false_passes() {
339 let orcid = Orcid {
340 authenticated: Some(false),
341 value: "https://orcid.org/0000-0002-1825-0097".to_string(),
342 };
343 assert!(orcid.validate().is_ok());
344 }
345
346 #[test]
347 fn authenticated_none_passes() {
348 let orcid = Orcid {
349 authenticated: None,
350 value: "https://orcid.org/0000-0002-1825-0097".to_string(),
351 };
352 assert!(orcid.validate().is_ok());
353 }
354
355 #[test]
356 fn various_valid_orcids_pass() {
357 let orcids = [
358 "https://orcid.org/0000-0002-1825-0097",
359 "http://orcid.org/0000-0001-5109-3700",
360 "https://orcid.org/0000-0002-1694-233X",
361 ];
362 for orcid_str in orcids {
363 let orcid = Orcid {
364 authenticated: None,
365 value: orcid_str.to_string(),
366 };
367 assert!(
368 orcid.validate().is_ok(),
369 "ORCID should be valid: {}",
370 orcid_str
371 );
372 }
373 }
374 }
375
376 mod person_name {
377 use super::*;
378
379 fn valid_person_name() -> PersonName {
380 PersonName {
381 language: Iso639_1::En,
382 sequence: Sequence::First,
383 contributor_role: ContributorRole::Author,
384 name_style: None,
385 given_name: "Jane".to_string(),
386 surname: "Smith".to_string(),
387 suffix: None,
388 affiliations: Affiliations::default(),
389 orcid: None,
390 }
391 }
392
393 #[test]
394 fn valid_person_name_passes() {
395 let person = valid_person_name();
396 assert!(person.validate().is_ok());
397 }
398
399 #[test]
400 fn given_name_invalid_regex_fails() {
401 let mut person = valid_person_name();
402 person.given_name = "123".to_string();
403 assert!(person.validate().is_err());
404 }
405
406 #[test]
407 fn surname_invalid_regex_fails() {
408 let mut person = valid_person_name();
409 person.surname = "456".to_string();
410 assert!(person.validate().is_err());
411 }
412
413 #[test]
414 fn suffix_none_passes() {
415 let mut person = valid_person_name();
416 person.suffix = None;
417 assert!(person.validate().is_ok());
418 }
419
420 #[test]
421 fn with_valid_orcid_passes() {
422 let mut person = valid_person_name();
423 person.orcid = Some(Orcid {
424 authenticated: Some(true),
425 value: "https://orcid.org/0000-0002-1825-0097".to_string(),
426 });
427 assert!(person.validate().is_ok());
428 }
429
430 #[test]
431 fn with_invalid_orcid_fails() {
432 let mut person = valid_person_name();
433 person.orcid = Some(Orcid {
434 authenticated: None,
435 value: "invalid".to_string(),
436 });
437 assert!(person.validate().is_err());
438 }
439
440 #[test]
441 fn with_affiliations_passes() {
442 let mut person = valid_person_name();
443 person.affiliations = Affiliations {
444 institution: vec![Institution {
445 institution_id: InstitutionId {
446 institution_type: InstitutionIdType::Ror,
447 value: "https://ror.org/12345".to_string(),
448 },
449 }],
450 };
451 assert!(person.validate().is_ok());
452 }
453 }
454
455 mod anonymous {
456 use super::*;
457
458 fn valid_anonymous() -> Anonymous {
459 Anonymous {
460 language: Iso639_1::En,
461 sequence: Sequence::First,
462 contributor_role: ContributorRole::Author,
463 name_style: None,
464 }
465 }
466
467 #[test]
468 fn valid_anonymous_passes() {
469 let anon = valid_anonymous();
470 assert!(anon.validate().is_ok());
471 }
472
473 #[test]
474 fn with_name_style_passes() {
475 let mut anon = valid_anonymous();
476 anon.name_style = Some(NameStyle::Western);
477 assert!(anon.validate().is_ok());
478 }
479 }
480
481 mod organization {
482 use super::*;
483
484 fn valid_organization() -> Organization {
485 Organization {
486 value: "Example University".to_string(),
487 language: Iso639_1::En,
488 sequence: Sequence::First,
489 contributor_role: ContributorRole::Author,
490 }
491 }
492
493 #[test]
494 fn valid_organization_passes() {
495 let org = valid_organization();
496 assert!(org.validate().is_ok());
497 }
498 }
499
500 mod contributors {
501 use super::*;
502
503 fn valid_contributors_person() -> Contributor {
504 Contributor {
505 organization: vec![],
506 person_name: vec![PersonName {
507 language: Iso639_1::En,
508 sequence: Sequence::First,
509 contributor_role: ContributorRole::Author,
510 name_style: None,
511 given_name: "Jane".to_string(),
512 surname: "Smith".to_string(),
513 suffix: None,
514 affiliations: Affiliations::default(),
515 orcid: None,
516 }],
517 anonymous: vec![],
518 }
519 }
520
521 #[test]
522 fn contributors_with_person_passes() {
523 let contrib = valid_contributors_person();
524 assert!(contrib.validate().is_ok());
525 }
526
527 #[test]
528 fn contributors_with_organization_passes() {
529 let contrib = Contributor {
530 organization: vec![Organization {
531 value: "Example University".to_string(),
532 language: Iso639_1::En,
533 sequence: Sequence::First,
534 contributor_role: ContributorRole::Author,
535 }],
536 person_name: vec![],
537 anonymous: vec![],
538 };
539 assert!(contrib.validate().is_ok());
540 }
541
542 #[test]
543 fn contributors_with_anonymous_passes() {
544 let contrib = Contributor {
545 organization: vec![],
546 person_name: vec![],
547 anonymous: vec![Anonymous {
548 language: Iso639_1::En,
549 sequence: Sequence::First,
550 contributor_role: ContributorRole::Author,
551 name_style: None,
552 }],
553 };
554 assert!(contrib.validate().is_ok());
555 }
556
557 #[test]
558 fn contributors_all_none_fails() {
559 }
564
565 #[test]
566 fn vector_of_contributors() {
567 }
569 }
570
571 mod original_language_title {
572 use super::*;
573
574 fn valid_original_language_title() -> OriginalLanguageTitle {
575 OriginalLanguageTitle {
576 language: Iso639_1::Es,
577 value: "Título Original".to_string(),
578 }
579 }
580
581 #[test]
582 fn valid_original_language_title_passes() {
583 let title = valid_original_language_title();
584 assert!(title.validate().is_ok());
585 }
586 }
587
588 mod titles {
589 use super::*;
590
591 fn valid_titles() -> Titles {
592 Titles {
593 title: "Example Article Title".to_string(),
594 subtitle: Some("A Comprehensive Study".to_string()),
595 original_language_title: None,
596 orginal_language_subtitle: None,
597 }
598 }
599
600 #[test]
601 fn valid_titles_passes() {
602 let titles = valid_titles();
603 assert!(titles.validate().is_ok());
604 }
605
606 #[test]
607 fn with_original_language_title_passes() {
608 let mut titles = valid_titles();
609 titles.original_language_title = Some(OriginalLanguageTitle {
610 language: Iso639_1::Es,
611 value: "Título Original".to_string(),
612 });
613 assert!(titles.validate().is_ok());
614 }
615
616 #[test]
617 fn subtitle_none_passes() {
618 let mut titles = valid_titles();
619 titles.subtitle = None;
620 assert!(titles.validate().is_ok());
621 }
622 }
623
624 mod publication_date {
625 use super::*;
626
627 fn valid_publication_date() -> PublicationDate {
628 PublicationDate {
629 media_type: MediaTypeDate::Online,
630 month: Some(6),
631 day: Some(15),
632 year: 2024,
633 }
634 }
635
636 #[test]
637 fn valid_publication_date_passes() {
638 let date = valid_publication_date();
639 assert!(date.validate().is_ok());
640 }
641
642 #[test]
643 fn month_none_passes() {
644 let mut date = valid_publication_date();
645 date.month = None;
646 assert!(date.validate().is_ok());
647 }
648
649 #[test]
650 fn day_none_passes() {
651 let mut date = valid_publication_date();
652 date.day = None;
653 assert!(date.validate().is_ok());
654 }
655
656 #[test]
657 fn minimal_date_passes() {
658 let date = PublicationDate {
659 media_type: MediaTypeDate::Online,
660 month: None,
661 day: None,
662 year: 2024,
663 };
664 assert!(date.validate().is_ok());
665 }
666 }
667
668 mod journal_issue {
669 use super::*;
670 use crate::enums::ContentVersion;
671 use crate::journal::metadata::{DoiData, Resource};
672
673 fn valid_journal_issue() -> JournalIssue {
674 JournalIssue {
675 contributors: vec![Contributor {
676 organization: vec![],
677 person_name: vec![PersonName {
678 language: Iso639_1::En,
679 sequence: Sequence::First,
680 contributor_role: ContributorRole::Author,
681 name_style: None,
682 given_name: "Jane".to_string(),
683 surname: "Smith".to_string(),
684 suffix: None,
685 affiliations: Affiliations::default(),
686 orcid: None,
687 }],
688 anonymous: vec![],
689 }],
690 titles: Some(Titles {
691 title: "Issue Title".to_string(),
692 subtitle: None,
693 original_language_title: None,
694 orginal_language_subtitle: None,
695 }),
696 publication_date: PublicationDate {
697 media_type: MediaTypeDate::Online,
698 month: Some(6),
699 day: Some(15),
700 year: 2024,
701 },
702 issue: Some("1".to_string()),
703 special_numbering: None,
704 archive_locations: ArchiveLocations::default(),
705 doi_data: DoiData {
706 doi: "10.1234/issue.001".to_string(),
707 timestamp: (),
708 resource: Resource {
709 value: "https://example.com/issue/1".to_string(),
710 mime_type: None,
711 content_version: ContentVersion::Vor,
712 },
713 },
714 }
715 }
716
717 #[test]
718 fn valid_journal_issue_passes() {
719 let issue = valid_journal_issue();
720 assert!(issue.validate().is_ok());
721 }
722
723 #[test]
724 fn invalid_publication_date_fails() {
725 let mut issue = valid_journal_issue();
726 issue.publication_date.year = 1000; assert!(issue.validate().is_err());
728 }
729
730 #[test]
731 fn invalid_doi_data_fails() {
732 let mut issue = valid_journal_issue();
733 issue.doi_data.doi = "invalid".to_string();
734 assert!(issue.validate().is_err());
735 }
736
737 #[test]
738 fn minimal_valid_issue_passes() {
739 let issue = JournalIssue {
740 contributors: vec![],
741 titles: None,
742 publication_date: PublicationDate {
743 media_type: MediaTypeDate::Online,
744 month: None,
745 day: None,
746 year: 2024,
747 },
748 issue: None,
749 special_numbering: None,
750 archive_locations: ArchiveLocations::default(),
751 doi_data: DoiData {
752 doi: "10.1234/a".to_string(),
753 timestamp: (),
754 resource: Resource {
755 value: "https://example.com".to_string(),
756 mime_type: None,
757 content_version: ContentVersion::Vor,
758 },
759 },
760 };
761 assert!(issue.validate().is_ok());
762 }
763 }
764}