mrrc/format_queries.rs
1//! Format-specific query traits for different MARC record types.
2//!
3//! This module provides specialized query traits for bibliographic, authority,
4//! and holdings records, enabling domain-specific helper methods tailored to
5//! each record type's purpose.
6
7use crate::authority_record::AuthorityRecord;
8use crate::holdings_record::HoldingsRecord;
9use crate::record::{Field, Record};
10
11/// Query helpers specific to bibliographic records.
12///
13/// This trait provides methods for navigating and querying bibliographic-specific
14/// fields like titles, authors, subjects, and linked field information.
15pub trait BibliographicQueries {
16 /// Get all main titles and their statement of responsibility (245 fields).
17 ///
18 /// In MARC, there is typically only one 245 field, but this method returns
19 /// any additional title fields that may be present.
20 ///
21 /// # Returns
22 ///
23 /// Vector of title fields (245) with their indicators and subfields.
24 ///
25 /// # Examples
26 ///
27 /// ```ignore
28 /// let record = Record::new(leader);
29 /// for title_field in record.get_titles() {
30 /// if let Some(title) = title_field.get_subfield('a') {
31 /// println!("Title: {}", title);
32 /// }
33 /// }
34 /// ```
35 #[must_use]
36 fn get_titles(&self) -> Vec<&Field>;
37
38 /// Get all subject fields (6XX range).
39 ///
40 /// Returns all subject authority fields including:
41 /// - 600: Personal name subject
42 /// - 610: Corporate name subject
43 /// - 611: Meeting name subject
44 /// - 630: Uniform title subject
45 /// - 648: Chronological term subject
46 /// - 650: Topical term subject
47 /// - 651: Geographic name subject
48 /// - 655: Genre/form term
49 ///
50 /// # Returns
51 ///
52 /// Vector of all subject fields in the record.
53 ///
54 /// # Examples
55 ///
56 /// ```ignore
57 /// let record = Record::new(leader);
58 /// for subject in record.get_all_subjects() {
59 /// if let Some(label) = subject.get_subfield('a') {
60 /// println!("Subject: {}", label);
61 /// }
62 /// }
63 /// ```
64 #[must_use]
65 fn get_all_subjects(&self) -> Vec<&Field>;
66
67 /// Get all topical subjects (650 fields).
68 ///
69 /// Topical subjects describe the main subject content of the work.
70 ///
71 /// # Returns
72 ///
73 /// Vector of all 650 fields in the record.
74 ///
75 /// # Examples
76 ///
77 /// ```ignore
78 /// let record = Record::new(leader);
79 /// for subject in record.get_topical_subjects() {
80 /// if let Some(term) = subject.get_subfield('a') {
81 /// println!("Topic: {}", term);
82 /// }
83 /// }
84 /// ```
85 #[must_use]
86 fn get_topical_subjects(&self) -> Vec<&Field>;
87
88 /// Get all geographic subjects (651 fields).
89 ///
90 /// Geographic subjects describe places discussed or depicted in the work.
91 ///
92 /// # Returns
93 ///
94 /// Vector of all 651 fields in the record.
95 ///
96 /// # Examples
97 ///
98 /// ```ignore
99 /// let record = Record::new(leader);
100 /// for subject in record.get_geographic_subjects() {
101 /// if let Some(place) = subject.get_subfield('a') {
102 /// println!("Place: {}", place);
103 /// }
104 /// }
105 /// ```
106 #[must_use]
107 fn get_geographic_subjects(&self) -> Vec<&Field>;
108
109 /// Get all name fields (1XX, 6XX name fields, 7XX).
110 ///
111 /// Returns all name-based fields including:
112 /// - 100: Main entry—personal name
113 /// - 600: Subject added entry—personal name
114 /// - 610: Subject added entry—corporate name
115 /// - 611: Subject added entry—meeting name
116 /// - 700: Added entry—personal name
117 /// - 710: Added entry—corporate name
118 /// - 711: Added entry—meeting name
119 ///
120 /// # Returns
121 ///
122 /// Vector of all name fields in the record.
123 ///
124 /// # Examples
125 ///
126 /// ```ignore
127 /// let record = Record::new(leader);
128 /// for name in record.get_all_names() {
129 /// if let Some(label) = name.get_subfield('a') {
130 /// println!("Name: {}", label);
131 /// }
132 /// }
133 /// ```
134 #[must_use]
135 fn get_all_names(&self) -> Vec<&Field>;
136
137 /// Get linked field pairs (original field with optional 880 counterpart).
138 ///
139 /// For a specified tag, returns tuples of the original field paired with its
140 /// linked 880 (alternate graphical representation) if one exists.
141 ///
142 /// # Arguments
143 ///
144 /// * `tag` - The original field tag (e.g., "100", "245", "650")
145 ///
146 /// # Returns
147 ///
148 /// Vector of (`original_field`, Option<`880_field`>) tuples.
149 ///
150 /// # Examples
151 ///
152 /// ```ignore
153 /// let record = Record::new(leader);
154 /// for (original, linked_880) in record.get_linked_field_pairs("100") {
155 /// if let Some(name) = original.get_subfield('a') {
156 /// println!("Name: {}", name);
157 /// if let Some(field_880) = linked_880 {
158 /// if let Some(alt) = field_880.get_subfield('a') {
159 /// println!(" Alternate: {}", alt);
160 /// }
161 /// }
162 /// }
163 /// }
164 /// ```
165 #[must_use]
166 fn get_linked_field_pairs(&self, tag: &str) -> Vec<(&Field, Option<&Field>)>;
167
168 /// Get all 880 fields (alternate graphical representations).
169 ///
170 /// These fields provide alternate script forms (romanized, vernacular, etc.)
171 /// linked to original fields via subfield 6 linkage.
172 ///
173 /// # Returns
174 ///
175 /// Vector of all 880 fields in the record.
176 ///
177 /// # Examples
178 ///
179 /// ```ignore
180 /// let record = Record::new(leader);
181 /// for field_880 in record.get_all_880_fields() {
182 /// println!("Alternate graphical: {:?}", field_880);
183 /// }
184 /// ```
185 #[must_use]
186 fn get_all_880_fields(&self) -> Vec<&Field>;
187}
188
189impl BibliographicQueries for Record {
190 fn get_titles(&self) -> Vec<&Field> {
191 self.get_fields("245")
192 .map(|f| f.iter().collect())
193 .unwrap_or_default()
194 }
195
196 fn get_all_subjects(&self) -> Vec<&Field> {
197 let mut results = Vec::new();
198 for tag in &["600", "610", "611", "630", "648", "650", "651", "655"] {
199 if let Some(fields) = self.get_fields(tag) {
200 results.extend(fields.iter());
201 }
202 }
203 results
204 }
205
206 fn get_topical_subjects(&self) -> Vec<&Field> {
207 self.get_fields("650")
208 .map(|f| f.iter().collect())
209 .unwrap_or_default()
210 }
211
212 fn get_geographic_subjects(&self) -> Vec<&Field> {
213 self.get_fields("651")
214 .map(|f| f.iter().collect())
215 .unwrap_or_default()
216 }
217
218 fn get_all_names(&self) -> Vec<&Field> {
219 let mut results = Vec::new();
220 for tag in &["100", "600", "610", "611", "700", "710", "711"] {
221 if let Some(fields) = self.get_fields(tag) {
222 results.extend(fields.iter());
223 }
224 }
225 results
226 }
227
228 fn get_linked_field_pairs(&self, tag: &str) -> Vec<(&Field, Option<&Field>)> {
229 self.get_field_pairs(tag)
230 }
231
232 fn get_all_880_fields(&self) -> Vec<&Field> {
233 self.get_all_880_fields()
234 }
235}
236
237/// Query helpers specific to authority records.
238///
239/// This trait provides methods for navigating authority-specific fields
240/// like see-from tracings, see-also fields, and relationship information.
241pub trait AuthoritySpecificQueries {
242 /// Get the preferred heading from the record.
243 ///
244 /// Authority records have one main heading (1XX field) that represents
245 /// the established form of the term.
246 ///
247 /// # Returns
248 ///
249 /// The preferred heading field, or `None` if not found.
250 ///
251 /// # Examples
252 ///
253 /// ```ignore
254 /// let record = AuthorityRecord::new(leader);
255 /// if let Some(heading) = record.get_preferred_heading() {
256 /// if let Some(label) = heading.get_subfield('a') {
257 /// println!("Preferred heading: {}", label);
258 /// }
259 /// }
260 /// ```
261 #[must_use]
262 fn get_preferred_heading(&self) -> Option<&Field>;
263
264 /// Get all variant forms (4XX fields) for this heading.
265 ///
266 /// These are non-preferred forms that users might search for,
267 /// with instructions to "see" the preferred heading instead.
268 ///
269 /// # Returns
270 ///
271 /// Vector of all 4XX fields in the record.
272 ///
273 /// # Examples
274 ///
275 /// ```ignore
276 /// let record = AuthorityRecord::new(leader);
277 /// for variant in record.get_variant_headings() {
278 /// if let Some(label) = variant.get_subfield('a') {
279 /// println!("Variant: {}", label);
280 /// }
281 /// }
282 /// ```
283 #[must_use]
284 fn get_variant_headings(&self) -> Vec<&Field>;
285
286 /// Get all related headings (5XX fields) for this term.
287 ///
288 /// These are established headings that are related but not synonymous,
289 /// with instructions to "see also" these related terms.
290 ///
291 /// # Returns
292 ///
293 /// Vector of all 5XX fields in the record.
294 ///
295 /// # Examples
296 ///
297 /// ```ignore
298 /// let record = AuthorityRecord::new(leader);
299 /// for related in record.get_broader_related_headings() {
300 /// if let Some(label) = related.get_subfield('a') {
301 /// println!("Related: {}", label);
302 /// }
303 /// }
304 /// ```
305 #[must_use]
306 fn get_broader_related_headings(&self) -> Vec<&Field>;
307
308 /// Get the scope note from the record.
309 ///
310 /// Scope notes (field 680) explain the meaning and usage of the heading.
311 ///
312 /// # Returns
313 ///
314 /// The scope note text, or `None` if not found.
315 ///
316 /// # Examples
317 ///
318 /// ```ignore
319 /// let record = AuthorityRecord::new(leader);
320 /// if let Some(note) = record.get_scope_note() {
321 /// println!("Scope: {}", note);
322 /// }
323 /// ```
324 #[must_use]
325 fn get_scope_note(&self) -> Option<&str>;
326}
327
328impl AuthoritySpecificQueries for AuthorityRecord {
329 fn get_preferred_heading(&self) -> Option<&Field> {
330 self.heading()
331 }
332
333 fn get_variant_headings(&self) -> Vec<&Field> {
334 self.see_from_tracings()
335 }
336
337 fn get_broader_related_headings(&self) -> Vec<&Field> {
338 self.see_also_tracings()
339 }
340
341 fn get_scope_note(&self) -> Option<&str> {
342 self.get_fields("680")
343 .and_then(|fields| fields.first())
344 .and_then(|f| f.get_subfield('a'))
345 }
346}
347
348/// Query helpers specific to holdings records.
349///
350/// This trait provides methods for navigating holdings-specific information
351/// like item locations, call numbers, and holdings notes.
352pub trait HoldingsSpecificQueries {
353 /// Get the call number from field 050 or 090.
354 ///
355 /// Call numbers are used to organize and locate items in a library.
356 /// Tries field 090 (local call number) first, then 050 (LC call number).
357 ///
358 /// # Returns
359 ///
360 /// The call number, or `None` if not found.
361 ///
362 /// # Examples
363 ///
364 /// ```ignore
365 /// let record = HoldingsRecord::new(leader);
366 /// if let Some(call_num) = record.get_call_number() {
367 /// println!("Call number: {}", call_num);
368 /// }
369 /// ```
370 #[must_use]
371 fn get_call_number(&self) -> Option<&str>;
372
373 /// Get the holding location from field 852 (Location).
374 ///
375 /// Specifies the physical location or repository of the item.
376 ///
377 /// # Returns
378 ///
379 /// The location text, or `None` if not found.
380 ///
381 /// # Examples
382 ///
383 /// ```ignore
384 /// let record = HoldingsRecord::new(leader);
385 /// if let Some(location) = record.get_holding_location() {
386 /// println!("Location: {}", location);
387 /// }
388 /// ```
389 #[must_use]
390 fn get_holding_location(&self) -> Option<&str>;
391
392 /// Get all notes on holdings (5XX fields).
393 ///
394 /// General notes that apply to the holdings information.
395 ///
396 /// # Returns
397 ///
398 /// Vector of all note text values from 5XX fields.
399 ///
400 /// # Examples
401 ///
402 /// ```ignore
403 /// let record = HoldingsRecord::new(leader);
404 /// for note in record.get_holding_notes() {
405 /// println!("Note: {}", note);
406 /// }
407 /// ```
408 #[must_use]
409 fn get_holding_notes(&self) -> Vec<&str>;
410}
411
412impl HoldingsSpecificQueries for HoldingsRecord {
413 fn get_call_number(&self) -> Option<&str> {
414 // Try local call number first (090), then LC call number (050)
415 self.get_fields("090")
416 .and_then(|f| f.first())
417 .and_then(|f| f.get_subfield('a'))
418 .or_else(|| {
419 self.get_fields("050")
420 .and_then(|f| f.first())
421 .and_then(|f| f.get_subfield('a'))
422 })
423 }
424
425 fn get_holding_location(&self) -> Option<&str> {
426 self.get_fields("852")
427 .and_then(|f| f.first())
428 .and_then(|f| f.get_subfield('b'))
429 }
430
431 fn get_holding_notes(&self) -> Vec<&str> {
432 let mut notes = Vec::new();
433 for tag in &[
434 "500", "501", "502", "503", "504", "505", "506", "507", "508", "509",
435 ] {
436 if let Some(fields) = self.get_fields(tag) {
437 for field in fields {
438 if let Some(note) = field.get_subfield('a') {
439 notes.push(note);
440 }
441 }
442 }
443 }
444 notes
445 }
446}
447
448#[cfg(test)]
449mod tests {
450 use super::*;
451 use crate::leader::Leader;
452 use crate::record::Subfield;
453
454 fn make_bib_leader() -> Leader {
455 Leader {
456 record_length: 1000,
457 record_status: 'a',
458 record_type: 'a', // Bibliographic record
459 bibliographic_level: 'm',
460 control_record_type: 'a',
461 character_coding: ' ',
462 indicator_count: 2,
463 subfield_code_count: 2,
464 data_base_address: 100,
465 encoding_level: ' ',
466 cataloging_form: ' ',
467 multipart_level: ' ',
468 reserved: "4500".to_string(),
469 }
470 }
471
472 fn make_auth_leader() -> Leader {
473 Leader {
474 record_length: 1000,
475 record_status: 'a',
476 record_type: 'z', // Authority record
477 bibliographic_level: ' ',
478 control_record_type: 'a',
479 character_coding: ' ',
480 indicator_count: 2,
481 subfield_code_count: 2,
482 data_base_address: 100,
483 encoding_level: ' ',
484 cataloging_form: ' ',
485 multipart_level: ' ',
486 reserved: "4500".to_string(),
487 }
488 }
489
490 fn make_hold_leader() -> Leader {
491 Leader {
492 record_length: 1000,
493 record_status: 'a',
494 record_type: 'x', // Holdings record
495 bibliographic_level: ' ',
496 control_record_type: 'a',
497 character_coding: ' ',
498 indicator_count: 2,
499 subfield_code_count: 2,
500 data_base_address: 100,
501 encoding_level: ' ',
502 cataloging_form: ' ',
503 multipart_level: ' ',
504 reserved: "4500".to_string(),
505 }
506 }
507
508 #[test]
509 fn test_bibliographic_get_titles() {
510 let mut record = Record::new(make_bib_leader());
511 let mut title_field = Field::new("245".to_string(), '1', '0');
512 title_field.subfields.push(Subfield {
513 code: 'a',
514 value: "Test title".to_string(),
515 });
516 record.add_field(title_field);
517
518 let titles = record.get_titles();
519 assert_eq!(titles.len(), 1);
520 assert_eq!(titles[0].tag, "245");
521 }
522
523 #[test]
524 fn test_bibliographic_get_all_subjects() {
525 let mut record = Record::new(make_bib_leader());
526 let mut subject1 = Field::new("650".to_string(), ' ', '0');
527 subject1.subfields.push(Subfield {
528 code: 'a',
529 value: "Topic".to_string(),
530 });
531 record.add_field(subject1);
532
533 let mut subject2 = Field::new("651".to_string(), ' ', '0');
534 subject2.subfields.push(Subfield {
535 code: 'a',
536 value: "Place".to_string(),
537 });
538 record.add_field(subject2);
539
540 let subjects = record.get_all_subjects();
541 assert_eq!(subjects.len(), 2);
542 }
543
544 #[test]
545 fn test_bibliographic_get_all_names() {
546 let mut record = Record::new(make_bib_leader());
547 let mut name1 = Field::new("100".to_string(), '1', ' ');
548 name1.subfields.push(Subfield {
549 code: 'a',
550 value: "Author".to_string(),
551 });
552 record.add_field(name1);
553
554 let mut name2 = Field::new("700".to_string(), '1', ' ');
555 name2.subfields.push(Subfield {
556 code: 'a',
557 value: "Contributor".to_string(),
558 });
559 record.add_field(name2);
560
561 let names = record.get_all_names();
562 assert_eq!(names.len(), 2);
563 }
564
565 #[test]
566 fn test_authority_get_preferred_heading() {
567 let mut record = AuthorityRecord::new(make_auth_leader());
568 let mut heading = Field::new("150".to_string(), ' ', ' ');
569 heading.subfields.push(Subfield {
570 code: 'a',
571 value: "Computer science".to_string(),
572 });
573 record.set_heading(heading);
574
575 assert!(record.get_preferred_heading().is_some());
576 }
577
578 #[test]
579 fn test_authority_get_variant_headings() {
580 let mut record = AuthorityRecord::new(make_auth_leader());
581 let mut variant = Field::new("450".to_string(), ' ', ' ');
582 variant.subfields.push(Subfield {
583 code: 'a',
584 value: "Computing".to_string(),
585 });
586 record.add_see_from_tracing(variant);
587
588 let variants = record.get_variant_headings();
589 assert_eq!(variants.len(), 1);
590 }
591
592 #[test]
593 fn test_holdings_get_call_number() {
594 let mut record = HoldingsRecord::new(make_hold_leader());
595 let mut call_field = Field::new("090".to_string(), ' ', ' ');
596 call_field.subfields.push(Subfield {
597 code: 'a',
598 value: "QA76.9.D3".to_string(),
599 });
600 record.add_field(call_field);
601
602 assert_eq!(record.get_call_number(), Some("QA76.9.D3"));
603 }
604
605 #[test]
606 fn test_holdings_get_holding_location() {
607 let mut record = HoldingsRecord::new(make_hold_leader());
608 let mut loc_field = Field::new("852".to_string(), ' ', ' ');
609 loc_field.subfields.push(Subfield {
610 code: 'b',
611 value: "Main Library".to_string(),
612 });
613 record.add_field(loc_field);
614
615 assert_eq!(record.get_holding_location(), Some("Main Library"));
616 }
617}