Skip to main content

marc_rs/record/
mod.rs

1use marc_rs_derive::MarcPaths;
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4
5mod types;
6pub use types::*;
7
8use crate::Encoding;
9
10// ── Path resolution types ───────────────────────────────────────────────────
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum PathKind {
14    VecPush,
15    VecStructCreator,
16    VecStructField,
17    OptionInit,
18    OptionSet,
19}
20
21pub trait MarcPaths: Sized {
22    const IS_LEAF: bool;
23    fn from_marc_str(s: &str) -> Self;
24    fn to_marc_str(&self) -> String;
25    fn marc_set(&mut self, path: &str, value: &str) -> bool;
26    fn marc_get_option(&self, path: &str) -> Option<String>;
27    fn marc_get_vec(&self, path: &str) -> Option<Vec<String>>;
28    fn marc_path_kind(path: &str) -> Option<PathKind>;
29    fn marc_has_path(path: &str) -> bool;
30    fn marc_is_vec_leaf(path: &str) -> bool;
31    fn marc_creator_field() -> &'static str;
32}
33
34// ── FromRuleValue: serde bridge for enum ↔ string conversion ────────────────
35
36pub trait FromRuleValue: Sized + DeserializeOwned + Serialize {
37    fn from_rule_value(s: &str) -> Self;
38    fn to_rule_value(&self) -> String;
39}
40
41macro_rules! impl_from_rule_value {
42    ($type:ty, $other:path) => {
43        impl FromRuleValue for $type {
44            fn from_rule_value(s: &str) -> Self {
45                serde_json::from_value(serde_json::Value::String(s.to_string())).unwrap_or_else(|_| $other(s.to_string()))
46            }
47            fn to_rule_value(&self) -> String {
48                match serde_json::to_value(self).ok() {
49                    Some(serde_json::Value::String(s)) => s,
50                    _ => match self {
51                        $other(s) => s.clone(),
52                        _ => unreachable!(),
53                    },
54                }
55            }
56        }
57    };
58}
59
60impl_from_rule_value!(Language, Language::Other);
61impl_from_rule_value!(Country, Country::Other);
62impl_from_rule_value!(TargetAudience, TargetAudience::Other);
63impl_from_rule_value!(ClassificationScheme, ClassificationScheme::Other);
64impl_from_rule_value!(SubjectType, SubjectType::Other);
65impl_from_rule_value!(NoteType, NoteType::Other);
66impl_from_rule_value!(LinkType, LinkType::Other);
67impl_from_rule_value!(Relator, Relator::Other);
68
69macro_rules! impl_from_rule_value_char {
70    ($type:ty, $other:path) => {
71        impl FromRuleValue for $type {
72            fn from_rule_value(s: &str) -> Self {
73                serde_json::from_value(serde_json::Value::String(s.to_string())).unwrap_or_else(|_| $other(s.chars().next().unwrap_or(' ')))
74            }
75            fn to_rule_value(&self) -> String {
76                match serde_json::to_value(self).ok() {
77                    Some(serde_json::Value::String(s)) => s,
78                    _ => match self {
79                        $other(c) => c.to_string(),
80                        _ => unreachable!(),
81                    },
82                }
83            }
84        }
85    };
86}
87
88impl_from_rule_value_char!(RecordStatus, RecordStatus::Other);
89impl_from_rule_value_char!(RecordType, RecordType::Other);
90impl_from_rule_value_char!(BibliographicLevel, BibliographicLevel::Other);
91
92/// One catalog pattern validation failure when mapping raw MARC → [`Record`].
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct RecordValidationIssue {
96    pub tag: String,
97    pub subfield: Option<char>,
98    pub target_path: String,
99    pub value: String,
100    pub pattern: String,
101}
102
103fn default_record_valid() -> bool {
104    true
105}
106
107// ── MarcPaths leaf implementations for value enums ──────────────────────────
108
109macro_rules! impl_marc_leaf {
110    ($ty:ty) => {
111        impl MarcPaths for $ty {
112            const IS_LEAF: bool = true;
113            fn from_marc_str(s: &str) -> Self {
114                <$ty as FromRuleValue>::from_rule_value(s)
115            }
116            fn to_marc_str(&self) -> String {
117                <$ty as FromRuleValue>::to_rule_value(self)
118            }
119            fn marc_set(&mut self, _: &str, _: &str) -> bool {
120                false
121            }
122            fn marc_get_option(&self, _: &str) -> Option<String> {
123                None
124            }
125            fn marc_get_vec(&self, _: &str) -> Option<Vec<String>> {
126                None
127            }
128            fn marc_path_kind(_: &str) -> Option<PathKind> {
129                None
130            }
131            fn marc_has_path(_: &str) -> bool {
132                false
133            }
134            fn marc_is_vec_leaf(_: &str) -> bool {
135                false
136            }
137            fn marc_creator_field() -> &'static str {
138                ""
139            }
140        }
141    };
142}
143
144impl_marc_leaf!(Language);
145impl_marc_leaf!(Country);
146impl_marc_leaf!(TargetAudience);
147impl_marc_leaf!(ClassificationScheme);
148impl_marc_leaf!(SubjectType);
149impl_marc_leaf!(NoteType);
150impl_marc_leaf!(LinkType);
151impl_marc_leaf!(Relator);
152
153/// High-level semantic representation of a MARC bibliographic record,
154/// organized following the standard block numbering (0XX-9XX).
155#[derive(Debug, Clone, Serialize, Deserialize, MarcPaths)]
156#[serde(rename_all = "camelCase")]
157pub struct Record {
158    #[marc(skip)]
159    pub leader: Leader,
160    #[marc(skip)]
161    #[serde(skip)]
162    pub encoding: Option<Encoding>,
163    /// False when any bound value failed a catalog `pattern` check (see `validation_issues`).
164    #[marc(skip)]
165    #[serde(default = "default_record_valid")]
166    pub valid: bool,
167    /// Details for each pattern mismatch (non-fatal: record is still returned).
168    #[marc(skip)]
169    #[serde(default, skip_serializing_if = "Vec::is_empty")]
170    pub validation_issues: Vec<RecordValidationIssue>,
171    /// 0XX - Identification
172    #[serde(default)]
173    pub identification: Identification,
174    /// 1XX - Coded information
175    #[serde(default)]
176    pub coded: Coded,
177    /// 2XX - Descriptive information
178    #[serde(default)]
179    pub description: Description,
180    /// 3XX - Notes
181    #[serde(default)]
182    pub notes: Notes,
183    /// 4XX - Links to other bibliographic records
184    #[serde(default)]
185    pub links: Links,
186    /// 5XX - Associated titles
187    #[serde(default)]
188    pub associated_titles: AssociatedTitles,
189    /// 6XX - Subject indexing
190    #[serde(default)]
191    pub indexing: Indexing,
192    /// 7XX - Responsibility
193    #[serde(default)]
194    pub responsibility: Responsibility,
195    /// 8XX - International data
196    #[serde(default)]
197    pub international: International,
198    /// 9XX - National and local data
199    #[serde(default)]
200    pub local: Local,
201}
202
203impl Default for Record {
204    fn default() -> Self {
205        Self {
206            leader: Leader::default(),
207            encoding: None,
208            valid: true,
209            validation_issues: Vec::new(),
210            identification: Identification::default(),
211            coded: Coded::default(),
212            description: Description::default(),
213            notes: Notes::default(),
214            links: Links::default(),
215            associated_titles: AssociatedTitles::default(),
216            indexing: Indexing::default(),
217            responsibility: Responsibility::default(),
218            international: International::default(),
219            local: Local::default(),
220        }
221    }
222}
223
224impl Record {
225    /// Multi-line report of [`Self::validation_issues`] for logging or CLI output.
226    pub fn validation_report(&self) -> String {
227        if self.validation_issues.is_empty() {
228            return String::new();
229        }
230        let mut s = String::from("catalog pattern validation failed:\n");
231        for issue in &self.validation_issues {
232            let sub = issue.subfield.map(|c| format!("${}", c)).unwrap_or_else(|| "-".to_string());
233            s.push_str(&format!(
234                "  tag {} subfield {} path {} value {:?} pattern {}\n",
235                issue.tag, sub, issue.target_path, issue.value, issue.pattern
236            ));
237        }
238        s
239    }
240
241    pub fn authors(&self) -> impl Iterator<Item = &Agent> {
242        self.responsibility.main_entry.iter().chain(self.responsibility.added_entries.iter())
243    }
244
245    pub fn languages(&self) -> &[Language] {
246        &self.coded.languages
247    }
248
249    pub fn titles(&self) -> Vec<&Title> {
250        let mut out = Vec::new();
251        if let Some(t) = &self.description.title {
252            out.push(t);
253        }
254        if let Some(t) = &self.associated_titles.uniform_title {
255            out.push(t);
256        }
257        out
258    }
259
260    pub fn audience(&self) -> Option<&TargetAudience> {
261        self.coded.target_audience.as_ref()
262    }
263
264    pub fn isbn(&self) -> &[Isbn] {
265        &self.identification.isbn
266    }
267
268    pub fn items(&self) -> &[Item] {
269        &self.local.items
270    }
271
272    pub fn media_type(&self) -> &RecordType {
273        &self.leader.record_type
274    }
275
276    /// Join all ISBN values with ", ". Returns None if there are no ISBNs.
277    pub fn isbn_string(&self) -> Option<String> {
278        if self.identification.isbn.is_empty() {
279            return None;
280        }
281        Some(self.identification.isbn.iter().map(|i| i.value.as_str()).collect::<Vec<_>>().join(", "))
282    }
283
284    /// Main title string (`description.title.main`).
285    pub fn title_main(&self) -> Option<&str> {
286        self.description.title.as_ref().map(|t| t.main.as_str())
287    }
288
289    /// Value of the first subject entry.
290    pub fn subject_main(&self) -> Option<&str> {
291        self.indexing.subjects.first().map(|s| s.value.as_str())
292    }
293
294    /// All uncontrolled index terms.
295    pub fn keywords(&self) -> &[String] {
296        &self.indexing.uncontrolled_terms
297    }
298
299    /// Date of the first publication entry.
300    pub fn publication_date(&self) -> Option<&str> {
301        self.description.publication.first().and_then(|p| p.date.as_deref())
302    }
303
304    /// Extent field of the physical description.
305    pub fn page_extent(&self) -> Option<&str> {
306        self.description.physical_description.as_ref().and_then(|p| p.extent.as_deref())
307    }
308
309    /// Dimensions field of the physical description.
310    pub fn dimensions(&self) -> Option<&str> {
311        self.description.physical_description.as_ref().and_then(|p| p.dimensions.as_deref())
312    }
313
314    /// Accompanying material field of the physical description.
315    pub fn accompanying_material_text(&self) -> Option<&str> {
316        self.description.physical_description.as_ref().and_then(|p| p.accompanying_material.as_deref())
317    }
318
319    /// Text of the first note of type `Contents`.
320    pub fn table_of_contents_text(&self) -> Option<&str> {
321        self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::Contents)).then(|| n.text.as_str()))
322    }
323
324    /// Text of the first note of type `Summary`.
325    pub fn abstract_text(&self) -> Option<&str> {
326        self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::Summary)).then(|| n.text.as_str()))
327    }
328
329    /// Text of the first note of type `General`.
330    pub fn general_note_text(&self) -> Option<&str> {
331        self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::General)).then(|| n.text.as_str()))
332    }
333
334    /// Primary language (first in `coded.languages`).
335    pub fn lang_primary(&self) -> Option<&Language> {
336        self.coded.languages.first()
337    }
338
339    /// Original language (first in `coded.original_languages`).
340    pub fn lang_original(&self) -> Option<&Language> {
341        self.coded.original_languages.first()
342    }
343}
344
345/// 0XX - Identification block
346#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
347#[serde(rename_all = "camelCase")]
348pub struct Identification {
349    #[serde(skip_serializing_if = "Option::is_none")]
350    pub record_id: Option<String>,
351    #[serde(skip_serializing_if = "Option::is_none")]
352    pub agency_id: Option<String>,
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub record_version_date: Option<String>,
355    #[serde(default, skip_serializing_if = "Vec::is_empty")]
356    pub isbn: Vec<Isbn>,
357    #[serde(default, skip_serializing_if = "Vec::is_empty")]
358    pub issn: Vec<Issn>,
359    #[serde(default, skip_serializing_if = "Vec::is_empty")]
360    pub national_bibliography_numbers: Vec<String>,
361    #[serde(default, skip_serializing_if = "Vec::is_empty")]
362    pub national_library_record_numbers: Vec<String>,
363    #[serde(default, skip_serializing_if = "Vec::is_empty")]
364    pub legal_deposit_numbers: Vec<String>,
365    #[serde(default, skip_serializing_if = "Vec::is_empty")]
366    pub lccn: Vec<String>,
367    #[serde(default, skip_serializing_if = "Vec::is_empty")]
368    pub system_control_numbers: Vec<String>,
369    #[serde(default, skip_serializing_if = "Vec::is_empty")]
370    pub patent_numbers: Vec<PatentNumber>,
371    #[serde(default, skip_serializing_if = "Vec::is_empty")]
372    pub technical_report_numbers: Vec<TechnicalReportNumber>,
373    #[serde(default, skip_serializing_if = "Vec::is_empty")]
374    pub publisher_numbers: Vec<PublisherNumber>,
375    #[serde(default, skip_serializing_if = "Vec::is_empty")]
376    pub codens: Vec<Coden>,
377    #[serde(default, skip_serializing_if = "Vec::is_empty")]
378    pub original_study_numbers: Vec<OriginalStudyNumber>,
379    #[serde(default, skip_serializing_if = "Vec::is_empty")]
380    pub government_document_numbers: Vec<GovernmentDocumentNumber>,
381    #[serde(default, skip_serializing_if = "Vec::is_empty")]
382    pub report_numbers: Vec<ReportNumber>,
383}
384
385/// 1XX - Coded information block
386#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
387#[serde(rename_all = "camelCase")]
388pub struct Coded {
389    #[serde(default, skip_serializing_if = "Vec::is_empty")]
390    pub languages: Vec<Language>,
391    #[serde(default, skip_serializing_if = "Vec::is_empty")]
392    pub original_languages: Vec<Language>,
393    #[serde(skip_serializing_if = "Option::is_none")]
394    pub country: Option<Country>,
395    #[serde(skip_serializing_if = "Option::is_none")]
396    pub publication_dates: Option<String>,
397    #[serde(skip_serializing_if = "Option::is_none")]
398    pub target_audience: Option<TargetAudience>,
399    #[serde(default, skip_serializing_if = "Vec::is_empty")]
400    pub geographic_area_codes: Vec<String>,
401    #[serde(default, skip_serializing_if = "Vec::is_empty")]
402    pub time_period_codes: Vec<String>,
403    #[serde(skip_serializing_if = "Option::is_none")]
404    pub date_entered_on_file: Option<String>,
405    #[serde(skip_serializing_if = "Option::is_none")]
406    pub type_of_date: Option<String>,
407    #[serde(skip_serializing_if = "Option::is_none")]
408    pub date1: Option<String>,
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub date2: Option<String>,
411    #[serde(skip_serializing_if = "Option::is_none")]
412    pub government_publication: Option<String>,
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub modified_record: Option<String>,
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub cataloging_language: Option<String>,
417    #[serde(skip_serializing_if = "Option::is_none")]
418    pub transliteration_code: Option<String>,
419    #[serde(skip_serializing_if = "Option::is_none")]
420    pub character_set: Option<String>,
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub additional_character_set: Option<String>,
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub script_of_title: Option<String>,
425    #[serde(skip_serializing_if = "Option::is_none")]
426    pub place_of_publication_code: Option<String>,
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub cataloging_source_code: Option<String>,
429}
430
431/// 2XX - Descriptive information block
432#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
433#[serde(rename_all = "camelCase")]
434pub struct Description {
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub title: Option<Title>,
437    #[serde(skip_serializing_if = "Option::is_none")]
438    pub edition: Option<String>,
439    #[serde(default, skip_serializing_if = "Vec::is_empty")]
440    pub publication: Vec<Publication>,
441    #[serde(skip_serializing_if = "Option::is_none")]
442    pub physical_description: Option<PhysicalDescription>,
443    #[serde(default, skip_serializing_if = "Vec::is_empty")]
444    pub series: Vec<SeriesStatement>,
445    #[serde(default, skip_serializing_if = "Vec::is_empty")]
446    pub varying_titles: Vec<VaryingTitle>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub frequency: Option<String>,
449}
450
451/// 3XX - Notes block
452#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
453#[serde(rename_all = "camelCase")]
454pub struct Notes {
455    #[serde(default, skip_serializing_if = "Vec::is_empty")]
456    pub items: Vec<Note>,
457}
458
459/// 4XX - Links to other bibliographic records
460#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
461#[serde(rename_all = "camelCase")]
462pub struct Links {
463    #[serde(default, skip_serializing_if = "Vec::is_empty")]
464    pub records: Vec<LinkedRecord>,
465}
466
467/// 5XX - Associated titles block
468#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
469#[serde(rename_all = "camelCase")]
470pub struct AssociatedTitles {
471    #[serde(skip_serializing_if = "Option::is_none")]
472    pub uniform_title: Option<Title>,
473    #[serde(skip_serializing_if = "Option::is_none")]
474    pub key_title: Option<Title>,
475    #[serde(default, skip_serializing_if = "Vec::is_empty")]
476    pub former_titles: Vec<Title>,
477    #[serde(default, skip_serializing_if = "Vec::is_empty")]
478    pub variant_titles: Vec<Title>,
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub abbreviated_title: Option<String>,
481}
482
483/// 6XX - Subject indexing block
484#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
485#[serde(rename_all = "camelCase")]
486pub struct Indexing {
487    #[serde(default, skip_serializing_if = "Vec::is_empty")]
488    pub subjects: Vec<Subject>,
489    #[serde(default, skip_serializing_if = "Vec::is_empty")]
490    pub classifications: Vec<Classification>,
491    #[serde(default, skip_serializing_if = "Vec::is_empty")]
492    pub uncontrolled_terms: Vec<String>,
493}
494
495/// 7XX - Responsibility block
496#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
497#[serde(rename_all = "camelCase")]
498pub struct Responsibility {
499    #[marc(skip)]
500    #[serde(skip_serializing_if = "Option::is_none")]
501    pub main_entry: Option<Agent>,
502    #[marc(skip)]
503    #[serde(default, skip_serializing_if = "Vec::is_empty")]
504    pub added_entries: Vec<Agent>,
505}
506
507/// 8XX - International data block
508#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
509#[serde(rename_all = "camelCase")]
510pub struct International {
511    #[serde(default, skip_serializing_if = "Vec::is_empty")]
512    pub cataloging_sources: Vec<CatalogingSource>,
513    #[serde(default, skip_serializing_if = "Vec::is_empty")]
514    pub location_call_numbers: Vec<LocationCallNumber>,
515    #[serde(default, skip_serializing_if = "Vec::is_empty")]
516    pub electronic_locations: Vec<ElectronicLocation>,
517    /// MARC21 850 - Holding institution
518    #[serde(default, skip_serializing_if = "Vec::is_empty")]
519    pub holding_institutions: Vec<String>,
520}
521
522/// 9XX - National and local data block
523#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
524#[serde(rename_all = "camelCase")]
525pub struct Local {
526    #[serde(default, skip_serializing_if = "Vec::is_empty")]
527    pub items: Vec<Item>,
528}