marctk/
breaker.rs

1//! Routines for reading and writing MARC Breaker text
2use super::Controlfield;
3use super::Field;
4use super::Record;
5use super::Subfield;
6
7const MARC_BREAKER_SF_DELIMITER: &str = "$";
8const MARC_BREAKER_SF_DELIMITER_ESCAPE: &str = "{dollar}";
9
10/// Replace bare subfield delimiter values with their escaped version.
11pub fn escape_to_breaker(value: &str) -> String {
12    value.replace(MARC_BREAKER_SF_DELIMITER, MARC_BREAKER_SF_DELIMITER_ESCAPE)
13}
14
15/// Replace escaped subfield delimiter values with the bare version.
16pub fn unescape_from_breaker(value: &str) -> String {
17    value.replace(MARC_BREAKER_SF_DELIMITER_ESCAPE, MARC_BREAKER_SF_DELIMITER)
18}
19
20impl Controlfield {
21    /// Generate breaker text for a [`Controlfield`]
22    pub fn to_breaker(&self) -> String {
23        if !self.content().is_empty() {
24            format!("={} {}", self.tag(), escape_to_breaker(self.content()))
25        } else {
26            format!("={}", self.tag())
27        }
28    }
29}
30
31impl Subfield {
32    /// Generate breaker text for a [`Subfield`]
33    ///
34    /// # Examples
35    ///
36    /// ```
37    /// let sf = marctk::Subfield::new("q", "Howdy, folks").unwrap();
38    /// assert_eq!(sf.to_breaker(), "$qHowdy, folks");
39    /// ```
40    pub fn to_breaker(&self) -> String {
41        format!(
42            "${}{}",
43            escape_to_breaker(self.code()),
44            escape_to_breaker(self.content()),
45        )
46    }
47}
48
49impl Field {
50    /// Generate breaker text for a [`Field`]
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// let mut field = marctk::Field::new("856").unwrap();
56    /// field.set_ind1("1");
57    /// field.add_subfield("q", "https://example.org").unwrap();
58    /// assert_eq!(field.to_breaker(), "=856 1\\$qhttps://example.org");
59    /// ```
60    pub fn to_breaker(&self) -> String {
61        let mut s = format!(
62            "={} {}{}",
63            self.tag(),
64            if self.ind1() == " " {
65                "\\"
66            } else {
67                self.ind1()
68            },
69            if self.ind2() == " " {
70                "\\"
71            } else {
72                self.ind2()
73            },
74        );
75
76        for sf in self.subfields() {
77            s += sf.to_breaker().as_str();
78        }
79
80        s
81    }
82}
83
84impl Record {
85    /// Generate breaker text for a [`Record`]
86    ///
87    /// # References
88    ///
89    /// * <https://www.loc.gov/marc/makrbrkr.html>
90    pub fn to_breaker(&self) -> String {
91        let mut s = format!("=LDR {}", &escape_to_breaker(self.leader()));
92
93        for cfield in self.control_fields() {
94            s += format!("\n{}", cfield.to_breaker()).as_str();
95        }
96
97        for field in self.fields() {
98            s += format!("\n{}", field.to_breaker()).as_str();
99        }
100
101        s
102    }
103
104    /// Create a MARC [`Record`] from a MARC Breaker string.
105    ///
106    /// Assumes one record per input string.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use marctk::Record;
112    /// let breaker_str = r#"=LDR 01716cas a2200433 i 4500
113    /// =008 071030c20079999nvumr p       0    0eng d
114    /// =035 \\$a(OCoLC)ocn179901451
115    /// =245 00$aRenoOut.
116    /// =336 \\$atext$btxt$2rdacontent
117    /// =650 \0$aLesbians$zNevada$zReno$vPeriodicals$0(uri) http://id.loc.gov/authorities/subjects/sh85076160"#;
118    /// let record = Record::from_breaker(breaker_str).unwrap();
119    ///
120    /// assert_eq!(record.leader(), "01716cas a2200433 i 4500");
121    ///
122    /// let field_035s = record.get_fields("035");
123    /// let subfield_contents: Vec<_> = field_035s
124    ///     .first()
125    ///     .unwrap()
126    ///     .subfields()
127    ///     .iter()
128    ///     .map(|sf| sf.content())
129    ///     .collect();
130    /// assert_eq!(subfield_contents, ["(OCoLC)ocn179901451"]);
131    /// ```
132    pub fn from_breaker(breaker: &str) -> Result<Self, String> {
133        if !breaker.starts_with('=') {
134            return Err("Breaker fields must begin with =".to_owned());
135        };
136        let mut record = Record::new();
137
138        for line in breaker
139            // Each field will start on a new line beginning with =
140            .split("\n=")
141            // Make sure that the preceding .split() did not remove the needed =
142            .map(|line| format!("={}", line.trim_start_matches('=')))
143        {
144            record.add_breaker_line(&line)?;
145        }
146
147        Ok(record)
148    }
149
150    /// Create a MARC [`Record`] from a file containing MARC Breaker text.
151    ///
152    /// Assumes one record per file.
153    pub fn from_breaker_file(filename: &str) -> Result<Self, String> {
154        let breaker = std::fs::read_to_string(filename)
155            .map_err(|e| format!("Error reading breaker file: {e}"))?;
156        Record::from_breaker(&breaker)
157    }
158
159    /// Process one line of breaker text and append the result to [`self`]
160    fn add_breaker_line(&mut self, line: &str) -> Result<(), String> {
161        let mut len = line.len();
162        if len < 4 {
163            // Skip unusable lines
164            return Ok(());
165        }
166
167        // Step past the opening '=' character
168        let line = &line[1..];
169        len -= 1;
170
171        let tag = &line[..3];
172
173        if tag.eq("LDR") {
174            if len > 4 {
175                self.set_leader(&line[4..])?;
176            }
177            return Ok(());
178        }
179
180        if tag < "010" {
181            let content = if len > 4 {
182                unescape_from_breaker(&line[4..])
183            } else {
184                "".to_string()
185            };
186            let cf = Controlfield::new(tag, content)?;
187            self.control_fields_mut().push(cf);
188            return Ok(());
189        }
190
191        let mut field = Field::new(tag)?;
192
193        // There is a space between the tag and the 1st indicator.
194
195        if len > 4 {
196            field.set_ind1(line[4..5].replace('\\', " "))?;
197        }
198
199        if len > 5 {
200            field.set_ind2(line[5..6].replace('\\', " "))?;
201        }
202
203        if len > 6 {
204            for sf in line[6..].split(MARC_BREAKER_SF_DELIMITER) {
205                if sf.is_empty() {
206                    continue;
207                }
208                let code = &sf[..1];
209                let content = &sf[1..]; // maybe ""
210                field.subfields_mut().push(Subfield::new(code, content)?);
211            }
212        }
213
214        self.fields_mut().push(field);
215
216        Ok(())
217    }
218}
219
220#[cfg(test)]
221mod breaker_tests {
222    #[test]
223    fn test_add_breaker_line() {
224        let mut record = crate::Record::default();
225
226        assert!(record.add_breaker_line("=LDR too short").is_err());
227
228        record
229            .add_breaker_line("=100 11$aSunshine$b$csquashes")
230            .unwrap();
231        assert_eq!(record.get_field_values("100", "a")[0], "Sunshine");
232        assert_eq!(record.get_field_values("100", "b")[0], "");
233    }
234}