Skip to main content

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}