use crate::error::{MarcError, Result};
use crate::holdings_record::HoldingsRecord;
use std::io::Write;
const FIELD_TERMINATOR: u8 = 0x1E;
const SUBFIELD_DELIMITER: u8 = 0x1F;
const RECORD_TERMINATOR: u8 = 0x1D;
#[derive(Debug)]
pub struct HoldingsMarcWriter<W: Write> {
writer: W,
}
impl<W: Write> HoldingsMarcWriter<W> {
pub fn new(writer: W) -> Self {
HoldingsMarcWriter { writer }
}
#[allow(clippy::too_many_lines)]
pub fn write_record(&mut self, record: &HoldingsRecord) -> Result<()> {
let mut directory = Vec::new();
let mut data = Vec::new();
let add_field = |tag: &str,
indicators: Option<(char, char)>,
value: Option<&str>,
subfields: Option<&[crate::record::Subfield]>,
directory: &mut Vec<u8>,
data: &mut Vec<u8>|
-> Result<()> {
let start_pos = data.len();
match (indicators, value, subfields) {
(None, Some(v), None) => {
data.extend_from_slice(v.as_bytes());
},
(Some((ind1, ind2)), _, Some(subs)) => {
data.push(ind1 as u8);
data.push(ind2 as u8);
for subfield in subs {
data.push(SUBFIELD_DELIMITER);
data.push(subfield.code as u8);
data.extend_from_slice(subfield.value.as_bytes());
}
},
_ => {
return Err(MarcError::InvalidRecord(
"Invalid field structure".to_string(),
))
},
}
data.push(FIELD_TERMINATOR);
let length = data.len() - start_pos;
directory.extend_from_slice(tag.as_bytes());
directory.extend_from_slice(format!("{length:04}").as_bytes());
directory.extend_from_slice(format!("{start_pos:05}").as_bytes());
Ok(())
};
for (tag, values) in &record.control_fields {
for value in values {
add_field(tag, None, Some(value), None, &mut directory, &mut data)?;
}
}
for fields in record.fields.values() {
for field in fields {
add_field(
&field.tag,
Some((field.indicator1, field.indicator2)),
None,
Some(&field.subfields),
&mut directory,
&mut data,
)?;
}
}
directory.push(FIELD_TERMINATOR);
let base_address = 24 + directory.len();
if base_address > 99999 {
return Err(MarcError::InvalidRecord(
"Record too large (base address exceeds 5 digits)".to_string(),
));
}
let record_length = base_address + data.len() + 1; if record_length > 99999 {
return Err(MarcError::InvalidRecord(
"Record too large (total length exceeds 5 digits)".to_string(),
));
}
let mut leader = record.leader.clone();
#[allow(clippy::cast_possible_truncation)]
{
leader.record_length = record_length as u32;
leader.data_base_address = base_address as u32;
}
let leader_bytes = leader.as_bytes()?;
self.writer.write_all(&leader_bytes)?;
self.writer.write_all(&directory)?;
self.writer.write_all(&data)?;
self.writer.write_all(&[RECORD_TERMINATOR])?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::holdings_record::HoldingsRecord;
use crate::leader::Leader;
use crate::record::{Field, Subfield};
fn create_test_leader() -> Leader {
Leader {
record_length: 1024,
record_status: 'n',
record_type: 'y',
bibliographic_level: '|',
control_record_type: ' ',
character_coding: ' ',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 300,
encoding_level: '1',
cataloging_form: 'a',
multipart_level: ' ',
reserved: "4500".to_string(),
}
}
#[test]
fn test_write_empty_holdings_record() {
let leader = create_test_leader();
let record = HoldingsRecord::new(leader);
let mut buffer = Vec::new();
let mut writer = HoldingsMarcWriter::new(&mut buffer);
let result = writer.write_record(&record);
assert!(result.is_ok());
assert!(!buffer.is_empty());
}
#[test]
fn test_write_holdings_with_control_fields() {
let leader = create_test_leader();
let mut record = HoldingsRecord::new(leader);
record.add_control_field("001".to_string(), "ocm00098765".to_string());
record.add_control_field(
"008".to_string(),
"0000001pzzzzzzzz1 ".to_string(),
);
let mut buffer = Vec::new();
let mut writer = HoldingsMarcWriter::new(&mut buffer);
let result = writer.write_record(&record);
assert!(result.is_ok());
assert!(!buffer.is_empty());
}
#[test]
fn test_write_holdings_with_location_field() {
let leader = create_test_leader();
let mut record = HoldingsRecord::new(leader);
let location = Field {
tag: "852".to_string(),
indicator1: ' ',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'b',
value: "Main Library".to_string(),
}],
};
record.add_location(location);
let mut buffer = Vec::new();
let mut writer = HoldingsMarcWriter::new(&mut buffer);
let result = writer.write_record(&record);
assert!(result.is_ok());
assert!(!buffer.is_empty());
}
#[test]
fn test_write_holdings_with_multiple_field_types() {
let leader = create_test_leader();
let mut record = HoldingsRecord::new(leader);
record.add_control_field("001".to_string(), "ocm00098765".to_string());
let location = Field {
tag: "852".to_string(),
indicator1: ' ',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'b',
value: "Main Library".to_string(),
}],
};
record.add_location(location);
let textual = Field {
tag: "866".to_string(),
indicator1: '4',
indicator2: '1',
subfields: smallvec::smallvec![Subfield {
code: 'a',
value: "v.1-5".to_string(),
}],
};
record.add_textual_holdings_basic(textual);
let mut buffer = Vec::new();
let mut writer = HoldingsMarcWriter::new(&mut buffer);
let result = writer.write_record(&record);
assert!(result.is_ok());
assert!(!buffer.is_empty());
}
}