1pub mod primitives;
14
15use std::borrow::Cow;
16
17#[derive(Clone, Debug, PartialEq, Eq)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21pub enum MetadataContent {
22 Text(primitives::Text),
25 Contributor(primitives::Contributor),
28 Date(primitives::DatePrimitive),
31}
32
33#[derive(Clone, Debug, PartialEq, Eq)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48pub struct MetadataValue {
49 text: String,
50 role: Option<String>,
51 content: MetadataContent,
52}
53
54impl MetadataValue {
55 pub fn new(text: impl Into<String>) -> Self {
57 Self::of_text(primitives::Text::new(text))
58 }
59
60 pub fn with_role(text: impl Into<String>, role: impl Into<String>) -> Self {
62 Self::of_text(primitives::Text::new(text).in_role(role))
63 }
64
65 pub fn of_text(text: primitives::Text) -> Self {
68 Self {
69 text: text.data().to_string(),
70 role: text.role().map(str::to_string),
71 content: MetadataContent::Text(text),
72 }
73 }
74
75 pub fn of_contributor(contributor: primitives::Contributor) -> Self {
79 Self {
80 text: contributor
81 .name()
82 .map(primitives::Text::data)
83 .unwrap_or_default()
84 .to_string(),
85 role: contributor.role().map(str::to_string),
86 content: MetadataContent::Contributor(contributor),
87 }
88 }
89
90 pub fn of_date(date: primitives::DatePrimitive) -> Self {
93 Self {
94 text: date.to_string(),
95 role: None,
96 content: MetadataContent::Date(date),
97 }
98 }
99
100 pub fn text(&self) -> &str {
102 &self.text
103 }
104
105 pub fn role(&self) -> Option<&str> {
107 self.role.as_deref()
108 }
109
110 pub fn content(&self) -> &MetadataContent {
112 &self.content
113 }
114
115 pub fn as_text(&self) -> Option<&primitives::Text> {
117 match &self.content {
118 MetadataContent::Text(text) => Some(text),
119 _ => None,
120 }
121 }
122
123 pub fn as_contributor(&self) -> Option<&primitives::Contributor> {
125 match &self.content {
126 MetadataContent::Contributor(contributor) => Some(contributor),
127 _ => None,
128 }
129 }
130
131 pub fn as_date(&self) -> Option<&primitives::DatePrimitive> {
133 match &self.content {
134 MetadataContent::Date(date) => Some(date),
135 _ => None,
136 }
137 }
138}
139
140#[derive(Clone, Debug, Default, PartialEq, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153#[must_use]
154pub struct Metadata {
155 entries: Vec<(String, Vec<MetadataValue>)>,
156}
157
158impl Metadata {
159 pub fn new() -> Self {
164 Self::default()
165 }
166
167 pub fn add(&mut self, unique_name: impl Into<String>, value: MetadataValue) {
169 let unique_name = unique_name.into();
170 match self
171 .entries
172 .iter_mut()
173 .find(|(name, _)| *name == unique_name)
174 {
175 Some((_, values)) => values.push(value),
176 None => self.entries.push((unique_name, vec![value])),
177 }
178 }
179
180 pub fn add_text(&mut self, unique_name: impl Into<String>, text: impl Into<String>) {
182 self.add(unique_name, MetadataValue::new(text));
183 }
184
185 pub fn add_contributor(&mut self, role: impl Into<String>, name: impl Into<String>) {
189 let role = role.into();
190 let unique_name = if is_contributor_unique_name(&role) {
191 role.clone()
192 } else {
193 "otherContributor".to_string()
194 };
195 self.add(unique_name, MetadataValue::with_role(name, role));
196 }
197
198 pub fn set(&mut self, unique_name: impl Into<String>, values: Vec<MetadataValue>) {
200 let unique_name = unique_name.into();
201 self.entries.retain(|(name, _)| *name != unique_name);
202 if !values.is_empty() {
203 self.entries.push((unique_name, values));
204 }
205 }
206
207 pub fn get(&self, unique_name: &str) -> &[MetadataValue] {
209 self.entries
210 .iter()
211 .find(|(name, _)| name == unique_name)
212 .map_or(&[], |(_, values)| values.as_slice())
213 }
214
215 pub fn first_text(&self, unique_name: &str) -> Option<&str> {
217 self.get(unique_name).first().map(MetadataValue::text)
218 }
219
220 pub fn all(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
224 self.entries
225 .iter()
226 .flat_map(|(name, values)| values.iter().map(move |value| (name.as_str(), value)))
227 }
228
229 pub fn contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
231 self.all()
232 .filter(|(name, _)| is_contributor_unique_name(name))
233 }
234
235 pub fn string_value(&self, unique_name: &str) -> Option<String> {
242 namespace_name(unique_name)?;
243 let values = self.get(unique_name);
244 if values.is_empty() {
245 return None;
246 }
247 if is_contributor_unique_name(unique_name) {
248 return Some(match values {
249 [only] => only.text().to_string(),
250 [first, second] => format!("{} and {}", first.text(), second.text()),
251 [first, rest @ ..] => format!("{} and {} others", first.text(), rest.len()),
252 [] => String::new(),
253 });
254 }
255 let texts: Vec<String> = if ARTICLED_TITLES.contains(&unique_name) {
256 values
257 .iter()
258 .map(|value| {
259 crate::text::prepend_article(value.text(), None)
260 .unwrap_or_else(|_| value.text().to_string())
261 })
262 .collect()
263 } else {
264 values
265 .iter()
266 .map(|value| value.text().to_string())
267 .collect()
268 };
269 Some(texts.join(", "))
270 }
271
272 pub fn title(&self) -> Option<String> {
274 self.string_value("title")
275 }
276
277 pub fn best_title(&self) -> Option<String> {
292 ["title", "popularTitle", "alternativeTitle", "movementName"]
293 .into_iter()
294 .find_map(|name| self.string_value(name).filter(|text| !text.is_empty()))
295 }
296
297 pub fn contributors_by_role(&self, role: Option<&str>) -> Vec<&MetadataValue> {
302 self.contributors()
303 .filter(|(_, value)| value.role() == role)
304 .map(|(_, value)| value)
305 .collect()
306 }
307
308 pub fn is_empty(&self) -> bool {
310 self.entries.is_empty()
311 }
312
313 pub fn all_sorted(&self) -> Vec<(&str, &MetadataValue)> {
316 let mut all: Vec<(&str, &MetadataValue)> = self.all().collect();
317 all.sort_by(|(left, left_value), (right, right_value)| {
318 left.cmp(right)
319 .then_with(|| left_value.text().cmp(right_value.text()))
320 });
321 all
322 }
323
324 pub fn search(
360 &self,
361 query: &MetadataQuery<'_>,
362 field: Option<&str>,
363 ) -> Option<MetadataMatch<'_>> {
364 let field = field.map(str::to_lowercase);
365 self.search_fields(field.as_deref())
366 .into_iter()
367 .find(|(text, _)| query.matches(text))
368 .map(|(text, field)| MetadataMatch { field, text })
369 }
370
371 fn search_fields(&self, field: Option<&str>) -> Vec<(Cow<'_, str>, Option<&str>)> {
374 let mut pairs: Vec<(Cow<'_, str>, Option<&str>)> = Vec::new();
375 match field {
376 Some(field) => {
377 let named = unique_name_of_field(field);
378 let texts =
379 named.map_or_else(Vec::new, |(unique_name, _)| self.field_texts(unique_name));
380 match (texts.is_empty(), named) {
381 (false, Some((_, asked))) => {
384 pairs.extend(texts.into_iter().map(|text| (text, Some(asked))));
385 }
386 _ => pairs.extend(self.partial_field_match(field)),
387 }
388 }
389 None => pairs.extend(
390 self.standard_non_contributors()
391 .map(|(name, value)| (Cow::Borrowed(value.text()), Some(name))),
392 ),
393 }
394 pairs.extend(
395 self.contributors()
396 .filter(|(_, value)| contributor_answers_to(value.role(), field))
397 .map(|(_, value)| (Cow::Borrowed(value.text()), value.role())),
398 );
399 pairs
400 }
401
402 fn field_texts(&self, unique_name: &str) -> Vec<Cow<'_, str>> {
408 self.get(unique_name)
409 .iter()
410 .map(|value| {
411 if ARTICLED_TITLES.contains(&unique_name) {
412 Cow::Owned(
413 crate::text::prepend_article(value.text(), None)
414 .unwrap_or_else(|_| value.text().to_string()),
415 )
416 } else {
417 Cow::Borrowed(value.text())
418 }
419 })
420 .collect()
421 }
422
423 fn standard_non_contributors(&self) -> impl Iterator<Item = (&str, &MetadataValue)> {
427 self.all_sorted()
428 .into_iter()
429 .filter(|(name, _)| !is_contributor_unique_name(name) && namespace_name(name).is_some())
430 }
431
432 fn partial_field_match(&self, field: &str) -> Option<(Cow<'_, str>, Option<&str>)> {
436 self.standard_non_contributors().find_map(|(name, value)| {
437 if name.to_lowercase().contains(field) {
438 return Some((Cow::Borrowed(value.text()), Some(name)));
439 }
440 let work_id = old_work_id(name)?;
441 work_id
442 .to_lowercase()
443 .contains(field)
444 .then_some((Cow::Borrowed(value.text()), Some(work_id)))
445 })
446 }
447}
448
449fn contributor_answers_to(role: Option<&str>, field: Option<&str>) -> bool {
453 let Some(field) = field else {
454 return true;
455 };
456 match role {
457 None => field == "contributor",
458 Some(role) => role.to_lowercase().contains(field),
459 }
460}
461
462pub fn unique_name_of(name: &str) -> Option<&'static str> {
479 if let Some((found, _, _)) = STANDARD_PROPERTIES
480 .iter()
481 .find(|(unique, _, _)| *unique == name)
482 {
483 return Some(found);
484 }
485 OLD_MUSIC21_NAMES
486 .iter()
487 .find(|(_, abbreviation, work_id)| *abbreviation == Some(name) || *work_id == Some(name))
488 .map(|(unique, _, _)| *unique)
489}
490
491fn unique_name_of_field(field: &str) -> Option<(&'static str, &'static str)> {
497 if let Some((name, _, _)) = STANDARD_PROPERTIES
498 .iter()
499 .find(|(name, _, _)| *name == field)
500 {
501 return Some((name, name));
502 }
503 OLD_MUSIC21_NAMES
504 .iter()
505 .find_map(
506 |(name, abbreviation, work_id)| match (abbreviation, work_id) {
507 (Some(asked), _) if *asked == field => Some((*name, *asked)),
508 (_, Some(asked)) if *asked == field => Some((*name, *asked)),
509 _ => None,
510 },
511 )
512}
513
514fn old_work_id(unique_name: &str) -> Option<&'static str> {
517 OLD_MUSIC21_NAMES
518 .iter()
519 .find(|(name, _, _)| *name == unique_name)
520 .and_then(|(_, _, work_id)| *work_id)
521}
522
523pub enum MetadataQuery<'a> {
532 Text(&'a str),
535 Matcher(&'a dyn Fn(&str) -> bool),
538}
539
540impl MetadataQuery<'_> {
541 fn matches(&self, text: &str) -> bool {
543 match self {
544 Self::Text(query) => text.to_lowercase().contains(&query.to_lowercase()),
545 Self::Matcher(matcher) => matcher(text),
546 }
547 }
548}
549
550impl std::fmt::Debug for MetadataQuery<'_> {
551 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 match self {
553 Self::Text(query) => formatter.debug_tuple("Text").field(query).finish(),
554 Self::Matcher(_) => formatter.write_str("Matcher(..)"),
555 }
556 }
557}
558
559#[derive(Clone, Debug, PartialEq, Eq)]
564pub struct MetadataMatch<'a> {
565 field: Option<&'a str>,
566 text: Cow<'a, str>,
567}
568
569impl<'a> MetadataMatch<'a> {
570 pub fn field(&self) -> Option<&'a str> {
572 self.field
573 }
574
575 pub fn text(&self) -> &str {
577 &self.text
578 }
579}
580
581pub const OLD_MUSIC21_NAMES: [(&str, Option<&str>, Option<&str>); 21] = [
587 ("alternativeTitle", Some("ota"), None),
588 ("dateCreated", None, Some("date")),
589 ("title", Some("otl"), None),
590 ("textOriginalLanguage", Some("txo"), None),
591 ("textLanguage", Some("txl"), None),
592 ("popularTitle", Some("otp"), None),
593 ("parentTitle", Some("opr"), None),
594 ("actNumber", Some("oac"), None),
595 ("sceneNumber", Some("osc"), None),
596 ("movementNumber", Some("omv"), None),
597 ("movementName", Some("omd"), None),
598 ("opusNumber", Some("ops"), None),
599 ("number", Some("onm"), None),
600 ("volumeNumber", Some("ovm"), Some("volume")),
601 ("dedicatedTo", Some("ode"), Some("dedication")),
602 ("commissionedBy", Some("oco"), Some("commission")),
603 ("countryOfComposition", Some("ocy"), None),
604 ("localeOfComposition", Some("opc"), None),
605 ("groupTitle", Some("gtl"), None),
606 ("associatedWork", Some("gaw"), None),
607 ("collectionDesignation", Some("gco"), None),
608];
609
610const ARTICLED_TITLES: [&str; 6] = [
613 "title",
614 "alternativeTitle",
615 "popularTitle",
616 "parentTitle",
617 "movementName",
618 "groupTitle",
619];
620
621const DATE_NAMES: [&str; 11] = [
624 "dateAvailable",
625 "dateCreated",
626 "otherDate",
627 "dateAccepted",
628 "dateCopyrighted",
629 "dateSubmitted",
630 "dateIssued",
631 "dateModified",
632 "dateValid",
633 "dateFirstPublished",
634 "electronicReleaseDate",
635];
636
637#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
641#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
642pub enum ValueKind {
643 Text,
645 Copyright,
648 Contributor,
651 Date,
654 Number,
656}
657
658#[must_use]
672pub fn value_kind_of(name: &str) -> Option<ValueKind> {
673 let unique_name = unique_name_of(name).or_else(|| unique_name_from_namespace(name))?;
674 if unique_name == "copyright" {
675 return Some(ValueKind::Copyright);
676 }
677 if unique_name == "fileNumber" {
678 return Some(ValueKind::Number);
679 }
680 if DATE_NAMES.contains(&unique_name) {
681 return Some(ValueKind::Date);
682 }
683 if is_contributor_unique_name(unique_name) {
684 return Some(ValueKind::Contributor);
685 }
686 Some(ValueKind::Text)
687}
688
689pub fn is_contributor_unique_name(unique_name: &str) -> bool {
692 STANDARD_PROPERTIES
693 .iter()
694 .any(|(name, _, contributor)| *contributor && *name == unique_name)
695}
696
697pub fn namespace_name(unique_name: &str) -> Option<&'static str> {
701 STANDARD_PROPERTIES
702 .iter()
703 .find(|(name, _, _)| *name == unique_name)
704 .map(|(_, namespaced, _)| *namespaced)
705}
706
707#[must_use]
718pub fn unique_name_from_namespace(namespaced: &str) -> Option<&'static str> {
719 STANDARD_PROPERTIES
720 .iter()
721 .find(|(_, named, _)| *named == namespaced)
722 .map(|(unique, _, _)| *unique)
723}
724
725#[must_use]
739pub fn is_standard_name(name: &str) -> bool {
740 STANDARD_PROPERTIES
741 .iter()
742 .any(|(unique, named, _)| *unique == name || *named == name)
743}
744
745#[must_use]
758pub fn unique_name_of_plural(plural: &str) -> Option<&'static str> {
759 match plural {
760 "composers" => Some("composer"),
761 "librettists" => Some("librettist"),
762 "lyricists" => Some("lyricist"),
763 "software" => Some("software"),
764 _ => None,
765 }
766}
767
768pub const STANDARD_PROPERTIES: [(&str, &str, bool); 131] = [
771 ("abstract", "dcterms:abstract", false),
772 ("accessRights", "dcterms:accessRights", false),
773 ("alternativeTitle", "dcterms:alternative", false),
774 ("audience", "dcterms:audience", false),
775 ("dateAvailable", "dcterms:available", false),
776 (
777 "bibliographicCitation",
778 "dcterms:bibliographicCitation",
779 false,
780 ),
781 ("conformsTo", "dcterms:conformsTo", false),
782 ("dateCreated", "dcterms:created", false),
783 ("otherDate", "dcterms:date", false),
784 ("dateAccepted", "dcterms:dateAccepted", false),
785 ("dateCopyrighted", "dcterms:dateCopyrighted", false),
786 ("dateSubmitted", "dcterms:dateSubmitted", false),
787 ("description", "dcterms:description", false),
788 ("educationLevel", "dcterms:educationLevel", false),
789 ("extent", "dcterms:extent", false),
790 ("format", "dcterms:format", false),
791 ("hasFormat", "dcterms:hasFormat", false),
792 ("hasPart", "dcterms:hasPart", false),
793 ("hasVersion", "dcterms:hasVersion", false),
794 ("identifier", "dcterms:identifier", false),
795 ("instructionalMethod", "dcterms:instructionalMethod", false),
796 ("isFormatOf", "dcterms:isFormatOf", false),
797 ("isPartOf", "dcterms:isPartOf", false),
798 ("isReferencedBy", "dcterms:isReferencedBy", false),
799 ("isReplacedBy", "dcterms:isReplacedBy", false),
800 ("isRequiredBy", "dcterms:isRequiredBy", false),
801 ("dateIssued", "dcterms:issued", false),
802 ("isVersionOf", "dcterms:isVersionOf", false),
803 ("language", "dcterms:language", false),
804 ("license", "dcterms:license", false),
805 ("medium", "dcterms:medium", false),
806 ("dateModified", "dcterms:modified", false),
807 ("provenance", "dcterms:provenance", false),
808 ("publisher", "dcterms:publisher", true),
809 ("references", "dcterms:references", false),
810 ("relation", "dcterms:relation", false),
811 ("replaces", "dcterms:replaces", false),
812 ("requires", "dcterms:requires", false),
813 ("copyright", "dcterms:rights", false),
814 ("rightsHolder", "dcterms:rightsHolder", true),
815 ("source", "dcterms:source", false),
816 ("subject", "dcterms:subject", false),
817 ("tableOfContents", "dcterms:tableOfContents", false),
818 ("title", "dcterms:title", false),
819 ("type", "dcterms:type", false),
820 ("dateValid", "dcterms:valid", false),
821 ("adapter", "marcrel:ADP", true),
822 ("analyst", "marcrel:ANL", true),
823 ("annotator", "marcrel:ANN", true),
824 ("arranger", "marcrel:ARR", true),
825 ("quotationsAuthor", "marcrel:AQT", true),
826 ("afterwordAuthor", "marcrel:AFT", true),
827 ("dialogAuthor", "marcrel:AUD", true),
828 ("introductionAuthor", "marcrel:AUI", true),
829 ("calligrapher", "marcrel:CLL", true),
830 ("collaborator", "marcrel:CLB", true),
831 ("collotyper", "marcrel:CLT", true),
832 ("commentaryAuthor", "marcrel:CWT", true),
833 ("compiler", "marcrel:COM", true),
834 ("composer", "marcrel:CMP", true),
835 ("conceptor", "marcrel:CCP", true),
836 ("conductor", "marcrel:CND", true),
837 ("otherContributor", "marcrel:CTB", true),
838 ("editor", "marcrel:EDT", true),
839 ("engraver", "marcrel:EGR", true),
840 ("etcher", "marcrel:ETR", true),
841 ("illuminator", "marcrel:ILU", true),
842 ("illustrator", "marcrel:ILL", true),
843 ("instrumentalist", "marcrel:ITR", true),
844 ("librettist", "marcrel:LBT", true),
845 ("lithographer", "marcrel:LTG", true),
846 ("lyricist", "marcrel:LYR", true),
847 ("metalEngraver", "marcrel:MTE", true),
848 ("musician", "marcrel:MUS", true),
849 ("proofreader", "marcrel:PFR", true),
850 ("platemaker", "marcrel:PLT", true),
851 ("printmaker", "marcrel:PRM", true),
852 ("producer", "marcrel:PRO", true),
853 ("responsibleParty", "marcrel:RPY", true),
854 ("scribe", "marcrel:SCR", true),
855 ("singer", "marcrel:SNG", true),
856 ("transcriber", "marcrel:TRC", true),
857 ("translator", "marcrel:TRL", true),
858 ("woodEngraver", "marcrel:WDE", true),
859 ("woodCutter", "marcrel:WDC", true),
860 ("accompanyingMaterialWriter", "marcrel:WAM", true),
861 ("distributor", "marcrel:DST", true),
862 ("software", "musicxml:software", false),
863 ("textOriginalLanguage", "humdrum:TXO", false),
864 ("textLanguage", "humdrum:TXL", false),
865 ("popularTitle", "humdrum:OTP", false),
866 ("parentTitle", "humdrum:OPR", false),
867 ("actNumber", "humdrum:OAC", false),
868 ("sceneNumber", "humdrum:OSC", false),
869 ("movementNumber", "humdrum:OMV", false),
870 ("movementName", "humdrum:OMD", false),
871 ("opusNumber", "humdrum:OPS", false),
872 ("number", "humdrum:ONM", false),
873 ("volumeNumber", "humdrum:OVM", false),
874 ("dedicatedTo", "humdrum:ODE", false),
875 ("commissionedBy", "humdrum:OCO", false),
876 ("countryOfComposition", "humdrum:OCY", false),
877 ("localeOfComposition", "humdrum:OPC", false),
878 ("groupTitle", "humdrum:GTL", false),
879 ("associatedWork", "humdrum:GAW", false),
880 ("collectionDesignation", "humdrum:GCO", false),
881 ("attributedComposer", "humdrum:COA", true),
882 ("suspectedComposer", "humdrum:COS", true),
883 ("composerAlias", "humdrum:COL", true),
884 ("composerCorporate", "humdrum:COC", true),
885 ("orchestrator", "humdrum:LOR", true),
886 ("firstPublisher", "humdrum:PPR", true),
887 ("dateFirstPublished", "humdrum:PDT", false),
888 ("publicationTitle", "humdrum:PTL", false),
889 ("placeFirstPublished", "humdrum:PPP", false),
890 ("publishersCatalogNumber", "humdrum:PC#", false),
891 ("scholarlyCatalogName", "humdrum:SCA", false),
892 ("scholarlyCatalogAbbreviation", "humdrum:SCT", false),
893 ("manuscriptSourceName", "humdrum:SMS", false),
894 ("manuscriptLocation", "humdrum:SML", false),
895 ("manuscriptAccessAcknowledgement", "humdrum:SMA", false),
896 ("originalDocumentOwner", "humdrum:YOO", true),
897 ("originalEditor", "humdrum:YOE", true),
898 ("electronicEditor", "humdrum:EED", true),
899 ("electronicEncoder", "humdrum:ENC", true),
900 ("electronicPublisher", "humdrum:YEP", true),
901 ("electronicReleaseDate", "humdrum:YER", false),
902 ("fileFormat", "m21FileInfo:fileFormat", false),
903 ("filePath", "m21FileInfo:filePath", false),
904 ("corpusFilePath", "m21FileInfo:corpusFilePath", false),
905 ("fileNumber", "m21FileInfo:fileNumber", false),
906];
907
908#[cfg(test)]
909mod tests {
910 use super::*;
911
912 #[test]
914 fn the_best_title_is_the_first_one_said() {
915 let title = |name: &str, text: &str| {
916 let mut said = Metadata::new();
917 said.add_text(name, text);
918 said.best_title()
919 };
920 assert_eq!(
921 title("title", "Third Symphony").as_deref(),
922 Some("Third Symphony")
923 );
924 assert_eq!(title("popularTitle", "Eroica").as_deref(), Some("Eroica"));
925 assert_eq!(
926 title("alternativeTitle", "Bonaparte").as_deref(),
927 Some("Bonaparte")
928 );
929 assert_eq!(title("movementName", "Allegro").as_deref(), Some("Allegro"));
930 assert_eq!(Metadata::new().best_title(), None);
931
932 let mut both = Metadata::new();
934 both.add_text("popularTitle", "Eroica");
935 both.add_text("title", "Third Symphony");
936 assert_eq!(both.best_title().as_deref(), Some("Third Symphony"));
937 }
938
939 #[test]
941 fn contributors_are_found_by_their_role() {
942 let mut said = Metadata::new();
943 said.add_text("title", "Violin Concerto");
944 said.add_contributor("composer", "Price, Florence");
945 said.add("otherContributor", MetadataValue::new("Baron van Swieten"));
948
949 let names = |role: Option<&str>| -> Vec<String> {
950 said.contributors_by_role(role)
951 .iter()
952 .map(|value| value.text().to_string())
953 .collect()
954 };
955 assert_eq!(names(Some("composer")), ["Price, Florence"]);
956 assert_eq!(names(None), ["Baron van Swieten"]);
957 assert!(names(Some("lyricist")).is_empty());
958
959 let mut two = Metadata::new();
960 two.add_contributor("composer", "A");
961 two.add_contributor("composer", "B");
962 assert_eq!(
963 two.contributors_by_role(Some("composer"))
964 .iter()
965 .map(|value| value.text())
966 .collect::<Vec<&str>>(),
967 ["A", "B"]
968 );
969 }
970
971 #[test]
972 fn names_keep_the_order_they_were_first_given() {
973 let mut md = Metadata::new();
974 md.add_text("software", "Finale");
975 md.add_text("movementName", "Grave");
976 md.add_text("software", "Dolet");
977 let all: Vec<(&str, &str)> = md.all().map(|(name, value)| (name, value.text())).collect();
978 assert_eq!(
979 all,
980 [
981 ("software", "Finale"),
982 ("software", "Dolet"),
983 ("movementName", "Grave")
984 ]
985 );
986 }
987
988 #[test]
989 fn a_role_outside_the_vocabulary_is_another_contributor() {
990 let mut md = Metadata::new();
991 md.add_contributor("composer", "Caccini");
992 md.add_contributor("tuba player", "Somebody");
993 let names: Vec<&str> = md.contributors().map(|(name, _)| name).collect();
994 assert_eq!(names, ["composer", "otherContributor"]);
995 assert_eq!(md.get("otherContributor")[0].role(), Some("tuba player"));
996 }
997
998 #[test]
999 fn a_search_of_one_name_reads_a_title_with_its_article_in_front() {
1000 let mut md = Metadata::new();
1001 md.add_text("title", "Grassy Turf, The");
1002 let query = MetadataQuery::Text("The Grassy");
1005 assert_eq!(
1006 md.search(&query, Some("title"))
1007 .map(|found| found.text().to_string()),
1008 Some("The Grassy Turf".to_string())
1009 );
1010 assert!(md.search(&query, None).is_none());
1011 assert!(md.search(&query, Some("titl")).is_none());
1012 }
1013
1014 #[test]
1015 fn a_contributor_answers_under_the_role_it_was_given() {
1016 let mut md = Metadata::new();
1017 md.add_contributor("tuba player", "Somebody");
1018 let query = MetadataQuery::Text("somebody");
1019 assert_eq!(
1020 md.search(&query, None).and_then(|found| found.field()),
1021 Some("tuba player")
1022 );
1023 assert_eq!(
1024 md.search(&query, Some("tuba"))
1025 .and_then(|found| found.field()),
1026 Some("tuba player")
1027 );
1028 assert!(md.search(&query, Some("contributor")).is_none());
1031 }
1032
1033 #[test]
1034 fn a_matcher_searches_where_music21_would_read_a_pattern() {
1035 let mut md = Metadata::new();
1036 md.add_text("title", "Maple Leaf Rag");
1037 md.add_contributor("composer", "Joplin, Scott");
1038 let ends_in_rag = |text: &str| text.ends_with("Rag");
1039 let found = md
1040 .search(&MetadataQuery::Matcher(&ends_in_rag), None)
1041 .expect("the title");
1042 assert_eq!(
1043 (found.field(), found.text()),
1044 (Some("title"), "Maple Leaf Rag")
1045 );
1046 assert!(
1047 md.search(&MetadataQuery::Matcher(&ends_in_rag), Some("composer"))
1048 .is_none()
1049 );
1050 }
1051
1052 #[test]
1053 fn the_vocabulary_names_its_namespaces() {
1054 assert_eq!(namespace_name("localeOfComposition"), Some("humdrum:OPC"));
1055 assert_eq!(namespace_name("dateCreated"), Some("dcterms:created"));
1056 assert_eq!(namespace_name("composer"), Some("marcrel:CMP"));
1057 assert_eq!(namespace_name("madeUp"), None);
1058 assert!(is_contributor_unique_name("lyricist"));
1059 assert!(!is_contributor_unique_name("title"));
1060 }
1061}