1use std::borrow::Cow;
14
15#[derive(Clone, Debug, PartialEq, Eq)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct MetadataValue {
20 text: String,
21 role: Option<String>,
22}
23
24impl MetadataValue {
25 pub fn new(text: impl Into<String>) -> Self {
27 Self {
28 text: text.into(),
29 role: None,
30 }
31 }
32
33 pub fn with_role(text: impl Into<String>, role: impl Into<String>) -> Self {
35 Self {
36 text: text.into(),
37 role: Some(role.into()),
38 }
39 }
40
41 pub fn text(&self) -> &str {
43 &self.text
44 }
45
46 pub fn role(&self) -> Option<&str> {
48 self.role.as_deref()
49 }
50}
51
52#[derive(Clone, Debug, Default, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65#[must_use]
66pub struct Metadata {
67 entries: Vec<(String, Vec<MetadataValue>)>,
68}
69
70impl Metadata {
71 pub fn new() -> Self {
76 Self::default()
77 }
78
79 pub fn add(&mut self, unique_name: impl Into<String>, value: MetadataValue) {
81 let unique_name = unique_name.into();
82 match self
83 .entries
84 .iter_mut()
85 .find(|(name, _)| *name == unique_name)
86 {
87 Some((_, values)) => values.push(value),
88 None => self.entries.push((unique_name, vec![value])),
89 }
90 }
91
92 pub fn add_text(&mut self, unique_name: impl Into<String>, text: impl Into<String>) {
94 self.add(unique_name, MetadataValue::new(text));
95 }
96
97 pub fn add_contributor(&mut self, role: impl Into<String>, name: impl Into<String>) {
101 let role = role.into();
102 let unique_name = if is_contributor_unique_name(&role) {
103 role.clone()
104 } else {
105 "otherContributor".to_string()
106 };
107 self.add(unique_name, MetadataValue::with_role(name, role));
108 }
109
110 pub fn set(&mut self, unique_name: impl Into<String>, values: Vec<MetadataValue>) {
112 let unique_name = unique_name.into();
113 self.entries.retain(|(name, _)| *name != unique_name);
114 if !values.is_empty() {
115 self.entries.push((unique_name, values));
116 }
117 }
118
119 pub fn get(&self, unique_name: &str) -> &[MetadataValue] {
121 self.entries
122 .iter()
123 .find(|(name, _)| name == unique_name)
124 .map_or(&[], |(_, values)| values.as_slice())
125 }
126
127 pub fn first_text(&self, unique_name: &str) -> Option<&str> {
129 self.get(unique_name).first().map(MetadataValue::text)
130 }
131
132 pub fn all(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
136 self.entries
137 .iter()
138 .flat_map(|(name, values)| values.iter().map(move |value| (name.as_str(), value)))
139 }
140
141 pub fn contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
143 self.all()
144 .filter(|(name, _)| is_contributor_unique_name(name))
145 }
146
147 pub fn string_value(&self, unique_name: &str) -> Option<String> {
154 namespace_name(unique_name)?;
155 let values = self.get(unique_name);
156 if values.is_empty() {
157 return None;
158 }
159 if is_contributor_unique_name(unique_name) {
160 return Some(match values {
161 [only] => only.text().to_string(),
162 [first, second] => format!("{} and {}", first.text(), second.text()),
163 [first, rest @ ..] => format!("{} and {} others", first.text(), rest.len()),
164 [] => String::new(),
165 });
166 }
167 let texts: Vec<String> = if ARTICLED_TITLES.contains(&unique_name) {
168 values
169 .iter()
170 .map(|value| {
171 crate::text::prepend_article(value.text(), None)
172 .unwrap_or_else(|_| value.text().to_string())
173 })
174 .collect()
175 } else {
176 values
177 .iter()
178 .map(|value| value.text().to_string())
179 .collect()
180 };
181 Some(texts.join(", "))
182 }
183
184 pub fn title(&self) -> Option<String> {
186 self.string_value("title")
187 }
188
189 pub fn best_title(&self) -> Option<String> {
204 ["title", "popularTitle", "alternativeTitle", "movementName"]
205 .into_iter()
206 .find_map(|name| self.string_value(name).filter(|text| !text.is_empty()))
207 }
208
209 pub fn contributors_by_role(&self, role: Option<&str>) -> Vec<&MetadataValue> {
214 self.contributors()
215 .filter(|(_, value)| value.role() == role)
216 .map(|(_, value)| value)
217 .collect()
218 }
219
220 pub fn is_empty(&self) -> bool {
222 self.entries.is_empty()
223 }
224
225 pub fn all_sorted(&self) -> Vec<(&str, &MetadataValue)> {
228 let mut all: Vec<(&str, &MetadataValue)> = self.all().collect();
229 all.sort_by(|(left, left_value), (right, right_value)| {
230 left.cmp(right)
231 .then_with(|| left_value.text().cmp(right_value.text()))
232 });
233 all
234 }
235
236 pub fn search(
272 &self,
273 query: &MetadataQuery<'_>,
274 field: Option<&str>,
275 ) -> Option<MetadataMatch<'_>> {
276 let field = field.map(str::to_lowercase);
277 self.search_fields(field.as_deref())
278 .into_iter()
279 .find(|(text, _)| query.matches(text))
280 .map(|(text, field)| MetadataMatch { field, text })
281 }
282
283 fn search_fields(&self, field: Option<&str>) -> Vec<(Cow<'_, str>, Option<&str>)> {
286 let mut pairs: Vec<(Cow<'_, str>, Option<&str>)> = Vec::new();
287 match field {
288 Some(field) => {
289 let named = unique_name_of_field(field);
290 let texts =
291 named.map_or_else(Vec::new, |(unique_name, _)| self.field_texts(unique_name));
292 match (texts.is_empty(), named) {
293 (false, Some((_, asked))) => {
296 pairs.extend(texts.into_iter().map(|text| (text, Some(asked))));
297 }
298 _ => pairs.extend(self.partial_field_match(field)),
299 }
300 }
301 None => pairs.extend(
302 self.standard_non_contributors()
303 .map(|(name, value)| (Cow::Borrowed(value.text()), Some(name))),
304 ),
305 }
306 pairs.extend(
307 self.contributors()
308 .filter(|(_, value)| contributor_answers_to(value.role(), field))
309 .map(|(_, value)| (Cow::Borrowed(value.text()), value.role())),
310 );
311 pairs
312 }
313
314 fn field_texts(&self, unique_name: &str) -> Vec<Cow<'_, str>> {
320 self.get(unique_name)
321 .iter()
322 .map(|value| {
323 if ARTICLED_TITLES.contains(&unique_name) {
324 Cow::Owned(
325 crate::text::prepend_article(value.text(), None)
326 .unwrap_or_else(|_| value.text().to_string()),
327 )
328 } else {
329 Cow::Borrowed(value.text())
330 }
331 })
332 .collect()
333 }
334
335 fn standard_non_contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
339 self.all_sorted()
340 .into_iter()
341 .filter(|(name, _)| !is_contributor_unique_name(name) && namespace_name(name).is_some())
342 }
343
344 fn partial_field_match(&self, field: &str) -> Option<(Cow<'_, str>, Option<&str>)> {
348 self.standard_non_contributors().find_map(|(name, value)| {
349 if name.to_lowercase().contains(field) {
350 return Some((Cow::Borrowed(value.text()), Some(name)));
351 }
352 let work_id = old_work_id(name)?;
353 work_id
354 .to_lowercase()
355 .contains(field)
356 .then_some((Cow::Borrowed(value.text()), Some(work_id)))
357 })
358 }
359}
360
361fn contributor_answers_to(role: Option<&str>, field: Option<&str>) -> bool {
365 let Some(field) = field else {
366 return true;
367 };
368 match role {
369 None => field == "contributor",
370 Some(role) => role.to_lowercase().contains(field),
371 }
372}
373
374pub fn unique_name_of(name: &str) -> Option<&'static str> {
391 if let Some((found, _, _)) = STANDARD_PROPERTIES
392 .iter()
393 .find(|(unique, _, _)| *unique == name)
394 {
395 return Some(found);
396 }
397 OLD_MUSIC21_NAMES
398 .iter()
399 .find(|(_, abbreviation, work_id)| *abbreviation == Some(name) || *work_id == Some(name))
400 .map(|(unique, _, _)| *unique)
401}
402
403fn unique_name_of_field(field: &str) -> Option<(&'static str, &'static str)> {
409 if let Some((name, _, _)) = STANDARD_PROPERTIES
410 .iter()
411 .find(|(name, _, _)| *name == field)
412 {
413 return Some((name, name));
414 }
415 OLD_MUSIC21_NAMES
416 .iter()
417 .find_map(
418 |(name, abbreviation, work_id)| match (abbreviation, work_id) {
419 (Some(asked), _) if *asked == field => Some((*name, *asked)),
420 (_, Some(asked)) if *asked == field => Some((*name, *asked)),
421 _ => None,
422 },
423 )
424}
425
426fn old_work_id(unique_name: &str) -> Option<&'static str> {
429 OLD_MUSIC21_NAMES
430 .iter()
431 .find(|(name, _, _)| *name == unique_name)
432 .and_then(|(_, _, work_id)| *work_id)
433}
434
435pub enum MetadataQuery<'a> {
444 Text(&'a str),
447 Matcher(&'a dyn Fn(&str) -> bool),
450}
451
452impl MetadataQuery<'_> {
453 fn matches(&self, text: &str) -> bool {
455 match self {
456 Self::Text(query) => text.to_lowercase().contains(&query.to_lowercase()),
457 Self::Matcher(matcher) => matcher(text),
458 }
459 }
460}
461
462impl std::fmt::Debug for MetadataQuery<'_> {
463 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 match self {
465 Self::Text(query) => formatter.debug_tuple("Text").field(query).finish(),
466 Self::Matcher(_) => formatter.write_str("Matcher(..)"),
467 }
468 }
469}
470
471#[derive(Clone, Debug, PartialEq, Eq)]
476pub struct MetadataMatch<'a> {
477 field: Option<&'a str>,
478 text: Cow<'a, str>,
479}
480
481impl<'a> MetadataMatch<'a> {
482 pub fn field(&self) -> Option<&'a str> {
484 self.field
485 }
486
487 pub fn text(&self) -> &str {
489 &self.text
490 }
491}
492
493pub const OLD_MUSIC21_NAMES: [(&str, Option<&str>, Option<&str>); 21] = [
499 ("alternativeTitle", Some("ota"), None),
500 ("dateCreated", None, Some("date")),
501 ("title", Some("otl"), None),
502 ("textOriginalLanguage", Some("txo"), None),
503 ("textLanguage", Some("txl"), None),
504 ("popularTitle", Some("otp"), None),
505 ("parentTitle", Some("opr"), None),
506 ("actNumber", Some("oac"), None),
507 ("sceneNumber", Some("osc"), None),
508 ("movementNumber", Some("omv"), None),
509 ("movementName", Some("omd"), None),
510 ("opusNumber", Some("ops"), None),
511 ("number", Some("onm"), None),
512 ("volumeNumber", Some("ovm"), Some("volume")),
513 ("dedicatedTo", Some("ode"), Some("dedication")),
514 ("commissionedBy", Some("oco"), Some("commission")),
515 ("countryOfComposition", Some("ocy"), None),
516 ("localeOfComposition", Some("opc"), None),
517 ("groupTitle", Some("gtl"), None),
518 ("associatedWork", Some("gaw"), None),
519 ("collectionDesignation", Some("gco"), None),
520];
521
522const ARTICLED_TITLES: [&str; 6] = [
525 "title",
526 "alternativeTitle",
527 "popularTitle",
528 "parentTitle",
529 "movementName",
530 "groupTitle",
531];
532
533pub fn is_contributor_unique_name(unique_name: &str) -> bool {
536 STANDARD_PROPERTIES
537 .iter()
538 .any(|(name, _, contributor)| *contributor && *name == unique_name)
539}
540
541pub fn namespace_name(unique_name: &str) -> Option<&'static str> {
545 STANDARD_PROPERTIES
546 .iter()
547 .find(|(name, _, _)| *name == unique_name)
548 .map(|(_, namespaced, _)| *namespaced)
549}
550
551pub const STANDARD_PROPERTIES: [(&str, &str, bool); 131] = [
554 ("abstract", "dcterms:abstract", false),
555 ("accessRights", "dcterms:accessRights", false),
556 ("alternativeTitle", "dcterms:alternative", false),
557 ("audience", "dcterms:audience", false),
558 ("dateAvailable", "dcterms:available", false),
559 (
560 "bibliographicCitation",
561 "dcterms:bibliographicCitation",
562 false,
563 ),
564 ("conformsTo", "dcterms:conformsTo", false),
565 ("dateCreated", "dcterms:created", false),
566 ("otherDate", "dcterms:date", false),
567 ("dateAccepted", "dcterms:dateAccepted", false),
568 ("dateCopyrighted", "dcterms:dateCopyrighted", false),
569 ("dateSubmitted", "dcterms:dateSubmitted", false),
570 ("description", "dcterms:description", false),
571 ("educationLevel", "dcterms:educationLevel", false),
572 ("extent", "dcterms:extent", false),
573 ("format", "dcterms:format", false),
574 ("hasFormat", "dcterms:hasFormat", false),
575 ("hasPart", "dcterms:hasPart", false),
576 ("hasVersion", "dcterms:hasVersion", false),
577 ("identifier", "dcterms:identifier", false),
578 ("instructionalMethod", "dcterms:instructionalMethod", false),
579 ("isFormatOf", "dcterms:isFormatOf", false),
580 ("isPartOf", "dcterms:isPartOf", false),
581 ("isReferencedBy", "dcterms:isReferencedBy", false),
582 ("isReplacedBy", "dcterms:isReplacedBy", false),
583 ("isRequiredBy", "dcterms:isRequiredBy", false),
584 ("dateIssued", "dcterms:issued", false),
585 ("isVersionOf", "dcterms:isVersionOf", false),
586 ("language", "dcterms:language", false),
587 ("license", "dcterms:license", false),
588 ("medium", "dcterms:medium", false),
589 ("dateModified", "dcterms:modified", false),
590 ("provenance", "dcterms:provenance", false),
591 ("publisher", "dcterms:publisher", true),
592 ("references", "dcterms:references", false),
593 ("relation", "dcterms:relation", false),
594 ("replaces", "dcterms:replaces", false),
595 ("requires", "dcterms:requires", false),
596 ("copyright", "dcterms:rights", false),
597 ("rightsHolder", "dcterms:rightsHolder", true),
598 ("source", "dcterms:source", false),
599 ("subject", "dcterms:subject", false),
600 ("tableOfContents", "dcterms:tableOfContents", false),
601 ("title", "dcterms:title", false),
602 ("type", "dcterms:type", false),
603 ("dateValid", "dcterms:valid", false),
604 ("adapter", "marcrel:ADP", true),
605 ("analyst", "marcrel:ANL", true),
606 ("annotator", "marcrel:ANN", true),
607 ("arranger", "marcrel:ARR", true),
608 ("quotationsAuthor", "marcrel:AQT", true),
609 ("afterwordAuthor", "marcrel:AFT", true),
610 ("dialogAuthor", "marcrel:AUD", true),
611 ("introductionAuthor", "marcrel:AUI", true),
612 ("calligrapher", "marcrel:CLL", true),
613 ("collaborator", "marcrel:CLB", true),
614 ("collotyper", "marcrel:CLT", true),
615 ("commentaryAuthor", "marcrel:CWT", true),
616 ("compiler", "marcrel:COM", true),
617 ("composer", "marcrel:CMP", true),
618 ("conceptor", "marcrel:CCP", true),
619 ("conductor", "marcrel:CND", true),
620 ("otherContributor", "marcrel:CTB", true),
621 ("editor", "marcrel:EDT", true),
622 ("engraver", "marcrel:EGR", true),
623 ("etcher", "marcrel:ETR", true),
624 ("illuminator", "marcrel:ILU", true),
625 ("illustrator", "marcrel:ILL", true),
626 ("instrumentalist", "marcrel:ITR", true),
627 ("librettist", "marcrel:LBT", true),
628 ("lithographer", "marcrel:LTG", true),
629 ("lyricist", "marcrel:LYR", true),
630 ("metalEngraver", "marcrel:MTE", true),
631 ("musician", "marcrel:MUS", true),
632 ("proofreader", "marcrel:PFR", true),
633 ("platemaker", "marcrel:PLT", true),
634 ("printmaker", "marcrel:PRM", true),
635 ("producer", "marcrel:PRO", true),
636 ("responsibleParty", "marcrel:RPY", true),
637 ("scribe", "marcrel:SCR", true),
638 ("singer", "marcrel:SNG", true),
639 ("transcriber", "marcrel:TRC", true),
640 ("translator", "marcrel:TRL", true),
641 ("woodEngraver", "marcrel:WDE", true),
642 ("woodCutter", "marcrel:WDC", true),
643 ("accompanyingMaterialWriter", "marcrel:WAM", true),
644 ("distributor", "marcrel:DST", true),
645 ("software", "musicxml:software", false),
646 ("textOriginalLanguage", "humdrum:TXO", false),
647 ("textLanguage", "humdrum:TXL", false),
648 ("popularTitle", "humdrum:OTP", false),
649 ("parentTitle", "humdrum:OPR", false),
650 ("actNumber", "humdrum:OAC", false),
651 ("sceneNumber", "humdrum:OSC", false),
652 ("movementNumber", "humdrum:OMV", false),
653 ("movementName", "humdrum:OMD", false),
654 ("opusNumber", "humdrum:OPS", false),
655 ("number", "humdrum:ONM", false),
656 ("volumeNumber", "humdrum:OVM", false),
657 ("dedicatedTo", "humdrum:ODE", false),
658 ("commissionedBy", "humdrum:OCO", false),
659 ("countryOfComposition", "humdrum:OCY", false),
660 ("localeOfComposition", "humdrum:OPC", false),
661 ("groupTitle", "humdrum:GTL", false),
662 ("associatedWork", "humdrum:GAW", false),
663 ("collectionDesignation", "humdrum:GCO", false),
664 ("attributedComposer", "humdrum:COA", true),
665 ("suspectedComposer", "humdrum:COS", true),
666 ("composerAlias", "humdrum:COL", true),
667 ("composerCorporate", "humdrum:COC", true),
668 ("orchestrator", "humdrum:LOR", true),
669 ("firstPublisher", "humdrum:PPR", true),
670 ("dateFirstPublished", "humdrum:PDT", false),
671 ("publicationTitle", "humdrum:PTL", false),
672 ("placeFirstPublished", "humdrum:PPP", false),
673 ("publishersCatalogNumber", "humdrum:PC#", false),
674 ("scholarlyCatalogName", "humdrum:SCA", false),
675 ("scholarlyCatalogAbbreviation", "humdrum:SCT", false),
676 ("manuscriptSourceName", "humdrum:SMS", false),
677 ("manuscriptLocation", "humdrum:SML", false),
678 ("manuscriptAccessAcknowledgement", "humdrum:SMA", false),
679 ("originalDocumentOwner", "humdrum:YOO", true),
680 ("originalEditor", "humdrum:YOE", true),
681 ("electronicEditor", "humdrum:EED", true),
682 ("electronicEncoder", "humdrum:ENC", true),
683 ("electronicPublisher", "humdrum:YEP", true),
684 ("electronicReleaseDate", "humdrum:YER", false),
685 ("fileFormat", "m21FileInfo:fileFormat", false),
686 ("filePath", "m21FileInfo:filePath", false),
687 ("corpusFilePath", "m21FileInfo:corpusFilePath", false),
688 ("fileNumber", "m21FileInfo:fileNumber", false),
689];
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
697 fn the_best_title_is_the_first_one_said() {
698 let title = |name: &str, text: &str| {
699 let mut said = Metadata::new();
700 said.add_text(name, text);
701 said.best_title()
702 };
703 assert_eq!(
704 title("title", "Third Symphony").as_deref(),
705 Some("Third Symphony")
706 );
707 assert_eq!(title("popularTitle", "Eroica").as_deref(), Some("Eroica"));
708 assert_eq!(
709 title("alternativeTitle", "Bonaparte").as_deref(),
710 Some("Bonaparte")
711 );
712 assert_eq!(title("movementName", "Allegro").as_deref(), Some("Allegro"));
713 assert_eq!(Metadata::new().best_title(), None);
714
715 let mut both = Metadata::new();
717 both.add_text("popularTitle", "Eroica");
718 both.add_text("title", "Third Symphony");
719 assert_eq!(both.best_title().as_deref(), Some("Third Symphony"));
720 }
721
722 #[test]
724 fn contributors_are_found_by_their_role() {
725 let mut said = Metadata::new();
726 said.add_text("title", "Violin Concerto");
727 said.add_contributor("composer", "Price, Florence");
728 said.add("otherContributor", MetadataValue::new("Baron van Swieten"));
731
732 let names = |role: Option<&str>| -> Vec<String> {
733 said.contributors_by_role(role)
734 .iter()
735 .map(|value| value.text().to_string())
736 .collect()
737 };
738 assert_eq!(names(Some("composer")), ["Price, Florence"]);
739 assert_eq!(names(None), ["Baron van Swieten"]);
740 assert!(names(Some("lyricist")).is_empty());
741
742 let mut two = Metadata::new();
743 two.add_contributor("composer", "A");
744 two.add_contributor("composer", "B");
745 assert_eq!(
746 two.contributors_by_role(Some("composer"))
747 .iter()
748 .map(|value| value.text())
749 .collect::<Vec<&str>>(),
750 ["A", "B"]
751 );
752 }
753
754 #[test]
755 fn names_keep_the_order_they_were_first_given() {
756 let mut md = Metadata::new();
757 md.add_text("software", "Finale");
758 md.add_text("movementName", "Grave");
759 md.add_text("software", "Dolet");
760 let all: Vec<(&str, &str)> = md.all().map(|(name, value)| (name, value.text())).collect();
761 assert_eq!(
762 all,
763 [
764 ("software", "Finale"),
765 ("software", "Dolet"),
766 ("movementName", "Grave")
767 ]
768 );
769 }
770
771 #[test]
772 fn a_role_outside_the_vocabulary_is_another_contributor() {
773 let mut md = Metadata::new();
774 md.add_contributor("composer", "Caccini");
775 md.add_contributor("tuba player", "Somebody");
776 let names: Vec<&str> = md.contributors().map(|(name, _)| name).collect();
777 assert_eq!(names, ["composer", "otherContributor"]);
778 assert_eq!(md.get("otherContributor")[0].role(), Some("tuba player"));
779 }
780
781 #[test]
782 fn a_search_of_one_name_reads_a_title_with_its_article_in_front() {
783 let mut md = Metadata::new();
784 md.add_text("title", "Grassy Turf, The");
785 let query = MetadataQuery::Text("The Grassy");
788 assert_eq!(
789 md.search(&query, Some("title"))
790 .map(|found| found.text().to_string()),
791 Some("The Grassy Turf".to_string())
792 );
793 assert!(md.search(&query, None).is_none());
794 assert!(md.search(&query, Some("titl")).is_none());
795 }
796
797 #[test]
798 fn a_contributor_answers_under_the_role_it_was_given() {
799 let mut md = Metadata::new();
800 md.add_contributor("tuba player", "Somebody");
801 let query = MetadataQuery::Text("somebody");
802 assert_eq!(
803 md.search(&query, None).and_then(|found| found.field()),
804 Some("tuba player")
805 );
806 assert_eq!(
807 md.search(&query, Some("tuba"))
808 .and_then(|found| found.field()),
809 Some("tuba player")
810 );
811 assert!(md.search(&query, Some("contributor")).is_none());
814 }
815
816 #[test]
817 fn a_matcher_searches_where_music21_would_read_a_pattern() {
818 let mut md = Metadata::new();
819 md.add_text("title", "Maple Leaf Rag");
820 md.add_contributor("composer", "Joplin, Scott");
821 let ends_in_rag = |text: &str| text.ends_with("Rag");
822 let found = md
823 .search(&MetadataQuery::Matcher(&ends_in_rag), None)
824 .expect("the title");
825 assert_eq!(
826 (found.field(), found.text()),
827 (Some("title"), "Maple Leaf Rag")
828 );
829 assert!(
830 md.search(&MetadataQuery::Matcher(&ends_in_rag), Some("composer"))
831 .is_none()
832 );
833 }
834
835 #[test]
836 fn the_vocabulary_names_its_namespaces() {
837 assert_eq!(namespace_name("localeOfComposition"), Some("humdrum:OPC"));
838 assert_eq!(namespace_name("dateCreated"), Some("dcterms:created"));
839 assert_eq!(namespace_name("composer"), Some("marcrel:CMP"));
840 assert_eq!(namespace_name("madeUp"), None);
841 assert!(is_contributor_unique_name("lyricist"));
842 assert!(!is_contributor_unique_name("title"));
843 }
844}