use std::fmt::Write;
use quick_xml::events::Event;
use quick_xml::Reader;
use crate::error::{MarcError, Result};
use crate::leader::Leader;
use crate::record::{Field, Record};
pub fn record_to_mods_xml(record: &Record) -> Result<String> {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<mods xmlns=\"http://www.loc.gov/mods/v3\" ");
xml.push_str("xmlns:mods=\"http://www.loc.gov/mods/v3\" ");
xml.push_str("xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n");
write_titles(&mut xml, record);
write_names(&mut xml, record);
write_type_of_resource(&mut xml, record);
write_origin_info(&mut xml, record);
write_physical_description(&mut xml, record);
write_abstract(&mut xml, record);
write_subjects(&mut xml, record);
write_identifiers(&mut xml, record);
write_language(&mut xml, record);
xml.push_str("</mods>\n");
Ok(xml)
}
fn write_titles(xml: &mut String, record: &Record) {
if let Some(fields_245) = record.fields.get("245") {
for field in fields_245 {
xml.push_str(" <mods:titleInfo>\n");
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
writeln!(
xml,
" <mods:title>{}</mods:title>",
escape_xml(&subfield.value)
)
.ok();
}
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'b') {
writeln!(
xml,
" <mods:subTitle>{}</mods:subTitle>",
escape_xml(&subfield.value)
)
.ok();
}
xml.push_str(" </mods:titleInfo>\n");
}
}
}
fn write_names(xml: &mut String, record: &Record) {
for tag in &["100", "700"] {
if let Some(fields) = record.fields.get(*tag) {
for field in fields {
if let Some(name_subfield) = field.subfields.iter().find(|s| s.code == 'a') {
xml.push_str(" <mods:name type=\"personal\">\n");
writeln!(
xml,
" <mods:namePart>{}</mods:namePart>",
escape_xml(&name_subfield.value)
)
.ok();
if let Some(date_subfield) = field.subfields.iter().find(|s| s.code == 'd') {
writeln!(
xml,
" <mods:namePart type=\"date\">{}</mods:namePart>",
escape_xml(&date_subfield.value)
)
.ok();
}
if let Some(role_subfield) = field.subfields.iter().find(|s| s.code == 'e') {
writeln!(
xml,
" <mods:role><mods:roleTerm>{}</mods:roleTerm></mods:role>",
escape_xml(&role_subfield.value)
)
.ok();
} else if *tag == "100" {
xml.push_str(
" <mods:role><mods:roleTerm>creator</mods:roleTerm></mods:role>\n",
);
}
xml.push_str(" </mods:name>\n");
}
}
}
}
for tag in &["110", "710"] {
if let Some(fields) = record.fields.get(*tag) {
for field in fields {
if let Some(name_subfield) = field.subfields.iter().find(|s| s.code == 'a') {
xml.push_str(" <mods:name type=\"corporate\">\n");
writeln!(
xml,
" <mods:namePart>{}</mods:namePart>",
escape_xml(&name_subfield.value)
)
.ok();
if let Some(role_subfield) = field.subfields.iter().find(|s| s.code == 'e') {
writeln!(
xml,
" <mods:role><mods:roleTerm>{}</mods:roleTerm></mods:role>",
escape_xml(&role_subfield.value)
)
.ok();
}
xml.push_str(" </mods:name>\n");
}
}
}
}
}
fn write_type_of_resource(xml: &mut String, record: &Record) {
let resource_type = match record.leader.record_type {
'a' | 'c' | 'd' | 't' => "text",
'e' | 'f' => "cartographic",
'g' | 'k' => "still image",
'i' | 'j' => "sound recording",
'm' => "computer resource",
'p' => "mixed material",
'r' => "three dimensional object",
_ => "unknown",
};
writeln!(
xml,
" <mods:typeOfResource>{resource_type}</mods:typeOfResource>"
)
.ok();
}
fn write_origin_info(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("260") {
for field in fields {
xml.push_str(" <mods:originInfo>\n");
for subfield in field.subfields.iter().filter(|s| s.code == 'a') {
writeln!(
xml,
" <mods:place><mods:placeTerm>{}</mods:placeTerm></mods:place>",
escape_xml(&subfield.value)
)
.ok();
}
for subfield in field.subfields.iter().filter(|s| s.code == 'b') {
writeln!(
xml,
" <mods:publisher>{}</mods:publisher>",
escape_xml(&subfield.value)
)
.ok();
}
for subfield in field.subfields.iter().filter(|s| s.code == 'c') {
writeln!(
xml,
" <mods:dateIssued>{}</mods:dateIssued>",
escape_xml(&subfield.value)
)
.ok();
}
xml.push_str(" </mods:originInfo>\n");
}
}
}
fn write_physical_description(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("300") {
for field in fields {
xml.push_str(" <mods:physicalDescription>\n");
for subfield in field.subfields.iter().filter(|s| s.code == 'a') {
writeln!(
xml,
" <mods:extent>{}</mods:extent>",
escape_xml(&subfield.value)
)
.ok();
}
for subfield in field.subfields.iter().filter(|s| s.code == 'b') {
writeln!(
xml,
" <mods:form>{}</mods:form>",
escape_xml(&subfield.value)
)
.ok();
}
for subfield in field.subfields.iter().filter(|s| s.code == 'c') {
writeln!(
xml,
" <mods:dimensions>{}</mods:dimensions>",
escape_xml(&subfield.value)
)
.ok();
}
xml.push_str(" </mods:physicalDescription>\n");
}
}
}
fn write_abstract(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("520") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
writeln!(
xml,
" <mods:abstract>{}</mods:abstract>",
escape_xml(&subfield.value)
)
.ok();
}
}
}
if let Some(fields) = record.fields.get("500") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
writeln!(
xml,
" <mods:note>{}</mods:note>",
escape_xml(&subfield.value)
)
.ok();
}
}
}
}
fn write_subjects(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("650") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
xml.push_str(" <mods:subject>\n");
writeln!(
xml,
" <mods:topic>{}</mods:topic>",
escape_xml(&subfield.value)
)
.ok();
xml.push_str(" </mods:subject>\n");
}
}
}
if let Some(fields) = record.fields.get("651") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
xml.push_str(" <mods:subject>\n");
writeln!(
xml,
" <mods:geographic>{}</mods:geographic>",
escape_xml(&subfield.value)
)
.ok();
xml.push_str(" </mods:subject>\n");
}
}
}
}
fn write_identifiers(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("020") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
writeln!(
xml,
" <mods:identifier type=\"isbn\">{}</mods:identifier>",
escape_xml(&subfield.value)
)
.ok();
}
}
}
if let Some(fields) = record.fields.get("022") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
writeln!(
xml,
" <mods:identifier type=\"issn\">{}</mods:identifier>",
escape_xml(&subfield.value)
)
.ok();
}
}
}
if let Some(control_001) = record.control_fields.get("001").and_then(|v| v.first()) {
writeln!(
xml,
" <mods:identifier type=\"local\">{}</mods:identifier>",
escape_xml(control_001)
)
.ok();
}
}
fn write_language(xml: &mut String, record: &Record) {
if let Some(fields) = record.fields.get("041") {
for field in fields {
for subfield in field.subfields.iter().filter(|s| s.code == 'a') {
xml.push_str(" <mods:language>\n");
let langs: Vec<&str> = subfield.value.split_whitespace().collect();
for lang in langs {
if !lang.is_empty() {
writeln!(
xml,
" <mods:languageTerm type=\"code\" authority=\"iso639-2b\">{lang}</mods:languageTerm>"
)
.ok();
}
}
xml.push_str(" </mods:language>\n");
}
}
}
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn strip_ns_owned(name: &[u8]) -> Vec<u8> {
match memchr::memchr(b':', name) {
Some(pos) => name[pos + 1..].to_vec(),
None => name.to_vec(),
}
}
struct StartInfo {
local_name: Vec<u8>,
attrs: Vec<(Vec<u8>, String)>,
}
impl StartInfo {
fn from_event(e: &quick_xml::events::BytesStart<'_>) -> Self {
let local_name = strip_ns_owned(e.name().as_ref());
let attrs: Vec<(Vec<u8>, String)> = e
.attributes()
.flatten()
.map(|a| {
(
a.key.as_ref().to_vec(),
String::from_utf8_lossy(&a.value).to_string(),
)
})
.collect();
Self { local_name, attrs }
}
fn attr(&self, key: &[u8]) -> Option<&str> {
self.attrs
.iter()
.find(|(k, _)| k.as_slice() == key)
.map(|(_, v)| v.as_str())
}
}
fn read_text(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Result<String> {
let mut text = String::new();
loop {
match reader.read_event_into(buf) {
Ok(Event::Text(e)) => {
text.push_str(
&e.decode()
.map_err(|err| MarcError::ParseError(format!("XML decode: {err}")))?,
);
},
Ok(Event::CData(e)) => {
text.push_str(&String::from_utf8_lossy(&e));
},
Ok(Event::GeneralRef(e)) => {
if let Ok(Some(c)) = e.resolve_char_ref() {
text.push(c);
} else {
match e.as_ref() {
b"amp" => text.push('&'),
b"lt" => text.push('<'),
b"gt" => text.push('>'),
b"apos" => text.push('\''),
b"quot" => text.push('"'),
other => {
return Err(MarcError::ParseError(format!(
"Unknown XML entity: &{};",
String::from_utf8_lossy(other)
)));
},
}
}
},
Ok(Event::End(_) | Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
Ok(text)
}
fn skip_element(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Result<()> {
let mut depth: u32 = 1;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(_)) => depth += 1,
Ok(Event::End(_)) => {
depth -= 1;
if depth == 0 {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML skip: {e}"))),
_ => {},
}
buf.clear();
}
Ok(())
}
fn make_default_leader() -> Leader {
Leader {
record_length: 0,
record_status: 'n',
record_type: 'a',
bibliographic_level: 'm',
control_record_type: ' ',
character_coding: 'a',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 0,
encoding_level: ' ',
cataloging_form: 'a',
multipart_level: ' ',
reserved: "4500".to_string(),
}
}
fn resource_type_to_leader_code(s: &str) -> char {
match s.trim() {
"cartographic" => 'e',
"notated music" => 'c',
"sound recording" | "sound recording-musical" | "sound recording-nonmusical" => 'i',
"still image" => 'k',
"moving image" => 'g',
"software, multimedia" | "computer resource" => 'm',
"three dimensional object" => 'r',
"mixed material" => 'p',
_ => 'a',
}
}
fn next_start(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Result<Option<StartInfo>> {
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let info = StartInfo::from_event(e);
buf.clear();
return Ok(Some(info));
},
Ok(Event::End(_) | Event::Eof) => return Ok(None),
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
}
pub fn mods_xml_to_record(xml: &str) -> Result<Record> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"mods" {
buf.clear();
return parse_mods_element(&mut reader, &mut buf);
}
},
Ok(Event::Eof) => {
return Err(MarcError::ParseError("No <mods> element found".to_string()));
},
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
}
pub fn mods_xml_to_records(xml: &str) -> Result<Vec<Record>> {
let mut reader = Reader::from_str(xml);
reader.config_mut().trim_text(false);
let mut buf = Vec::new();
let mut records = Vec::new();
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
if local == b"mods" {
records.push(parse_mods_element(&mut reader, &mut buf)?);
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if records.is_empty() {
return mods_xml_to_record(xml).map(|r| vec![r]);
}
Ok(records)
}
fn parse_mods_element(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>) -> Result<Record> {
let mut record = Record::new(make_default_leader());
let mut has_100 = false;
let mut has_110 = false;
let mut has_111 = false;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let info = StartInfo::from_event(e);
buf.clear();
match info.local_name.as_slice() {
b"titleInfo" => parse_title_info(reader, buf, &info, &mut record)?,
b"name" => parse_name(
reader,
buf,
&info,
&mut record,
&mut has_100,
&mut has_110,
&mut has_111,
)?,
b"typeOfResource" => parse_type_of_resource(reader, buf, &mut record)?,
b"originInfo" => parse_origin_info(reader, buf, &mut record)?,
b"physicalDescription" => {
parse_physical_description(reader, buf, &mut record)?;
},
b"abstract" => parse_abstract(reader, buf, &mut record)?,
b"note" => parse_note(reader, buf, &mut record)?,
b"subject" => parse_subject(reader, buf, &mut record)?,
b"identifier" => parse_identifier(reader, buf, &info, &mut record)?,
b"language" => parse_language(reader, buf, &mut record)?,
b"genre" => parse_genre(reader, buf, &mut record)?,
b"classification" => {
parse_classification(reader, buf, &info, &mut record)?;
},
b"location" => parse_location(reader, buf, &mut record)?,
b"relatedItem" => parse_related_item(reader, buf, &info, &mut record)?,
b"recordInfo" => parse_record_info(reader, buf, &mut record)?,
b"accessCondition" => {
parse_access_condition(reader, buf, &info, &mut record)?;
},
b"tableOfContents" => parse_table_of_contents(reader, buf, &mut record)?,
b"targetAudience" => parse_target_audience(reader, buf, &mut record)?,
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"mods" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
Ok(record)
}
fn parse_title_info(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
) -> Result<()> {
let title_type = info.attr(b"type");
let tag = match title_type {
Some("alternative" | "abbreviated" | "translated" | "uniform") => "246",
_ => "245",
};
let (ind1, ind2) = if tag == "245" { ('0', '0') } else { ('1', ' ') };
let mut field = Field::new(tag.to_string(), ind1, ind2);
let mut has_content = false;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
match local.as_slice() {
b"title" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
field.add_subfield('a', text);
has_content = true;
}
},
b"subTitle" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
field.add_subfield('b', text);
has_content = true;
}
},
b"partNumber" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
field.add_subfield('n', text);
has_content = true;
}
},
b"partName" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
field.add_subfield('p', text);
has_content = true;
}
},
b"nonSort" => {
let _text = read_text(reader, buf)?;
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"titleInfo" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if has_content {
record.add_field(field);
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn parse_name(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
has_100: &mut bool,
has_110: &mut bool,
has_111: &mut bool,
) -> Result<()> {
let name_type = info.attr(b"type").map(String::from);
let mut name_parts: Vec<String> = Vec::new();
let mut date_part: Option<String> = None;
let mut role_term: Option<String> = None;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let child = StartInfo::from_event(e);
buf.clear();
match child.local_name.as_slice() {
b"namePart" => {
let part_type = child.attr(b"type").map(String::from);
let text = read_text(reader, buf)?;
if !text.is_empty() {
if part_type.as_deref() == Some("date") {
date_part = Some(text);
} else {
name_parts.push(text);
}
}
},
b"role" => {
while let Some(role_child) = next_start(reader, buf)? {
if role_child.local_name == b"roleTerm" {
let text = read_text(reader, buf)?;
if !text.is_empty() {
role_term = Some(text);
}
} else {
skip_element(reader, buf)?;
}
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"name" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if name_parts.is_empty() {
return Ok(());
}
let is_creator = role_term.as_deref() == Some("creator");
let (tag, ind1) = match name_type.as_deref() {
Some("corporate") => {
if is_creator && !*has_110 {
*has_110 = true;
("110", '2')
} else {
("710", '2')
}
},
Some("conference") => {
if is_creator && !*has_111 {
*has_111 = true;
("111", '2')
} else {
("711", '2')
}
},
_ => {
if is_creator && !*has_100 {
*has_100 = true;
("100", '1')
} else {
("700", '1')
}
},
};
let mut field = Field::new(tag.to_string(), ind1, ' ');
field.add_subfield('a', name_parts.join(" "));
if let Some(d) = date_part {
field.add_subfield('d', d);
}
if let Some(ref role) = role_term {
field.add_subfield('e', role.clone());
}
record.add_field(field);
Ok(())
}
fn parse_type_of_resource(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
record.leader.record_type = resource_type_to_leader_code(&text);
}
Ok(())
}
fn parse_origin_info(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let mut place: Option<String> = None;
let mut publisher: Option<String> = None;
let mut date_issued: Option<String> = None;
let mut edition: Option<String> = None;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
match local.as_slice() {
b"place" => {
while let Some(child) = next_start(reader, buf)? {
if child.local_name == b"placeTerm" {
let text = read_text(reader, buf)?;
if !text.is_empty() {
place = Some(text);
}
} else {
skip_element(reader, buf)?;
}
}
},
b"publisher" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
publisher = Some(text);
}
},
b"dateIssued" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
date_issued = Some(text);
}
},
b"dateCreated" => {
let text = read_text(reader, buf)?;
if date_issued.is_none() && !text.is_empty() {
date_issued = Some(text);
}
},
b"edition" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
edition = Some(text);
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"originInfo" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if place.is_some() || publisher.is_some() || date_issued.is_some() {
let mut field = Field::new("260".to_string(), ' ', ' ');
if let Some(p) = place {
field.add_subfield('a', p);
}
if let Some(p) = publisher {
field.add_subfield('b', p);
}
if let Some(d) = date_issued {
field.add_subfield('c', d);
}
record.add_field(field);
}
if let Some(ed) = edition {
let mut field = Field::new("250".to_string(), ' ', ' ');
field.add_subfield('a', ed);
record.add_field(field);
}
Ok(())
}
fn parse_physical_description(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let mut extent: Option<String> = None;
let mut form: Option<String> = None;
let mut dimensions: Option<String> = None;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
match local.as_slice() {
b"extent" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
extent = Some(text);
}
},
b"form" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
form = Some(text);
}
},
b"dimensions" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
dimensions = Some(text);
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"physicalDescription" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if extent.is_some() || form.is_some() || dimensions.is_some() {
let mut field = Field::new("300".to_string(), ' ', ' ');
if let Some(ext) = extent {
field.add_subfield('a', ext);
}
if let Some(f) = form {
field.add_subfield('b', f);
}
if let Some(d) = dimensions {
field.add_subfield('c', d);
}
record.add_field(field);
}
Ok(())
}
fn parse_abstract(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("520".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
Ok(())
}
fn parse_note(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>, record: &mut Record) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("500".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
Ok(())
}
fn parse_subject(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>, record: &mut Record) -> Result<()> {
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
match local.as_slice() {
b"topic" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("650".to_string(), ' ', '0');
field.add_subfield('a', text);
record.add_field(field);
}
},
b"geographic" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("651".to_string(), ' ', '0');
field.add_subfield('a', text);
record.add_field(field);
}
},
b"temporal" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("650".to_string(), ' ', '0');
field.add_subfield('y', text);
record.add_field(field);
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"subject" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
Ok(())
}
fn parse_identifier(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
) -> Result<()> {
let id_type = info.attr(b"type").map(String::from);
let text = read_text(reader, buf)?;
if text.is_empty() {
return Ok(());
}
match id_type.as_deref() {
Some("isbn") => {
let mut field = Field::new("020".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
},
Some("issn") => {
let mut field = Field::new("022".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
},
Some("lccn") => {
let mut field = Field::new("010".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
},
Some("doi" | "hdl" | "uri") => {
let mut field = Field::new("024".to_string(), '7', ' ');
field.add_subfield('a', text);
if let Some(ref t) = id_type {
field.add_subfield('2', t.clone());
}
record.add_field(field);
},
Some("local") => {
record.add_control_field("001".to_string(), text);
},
_ => {
let mut field = Field::new("024".to_string(), '8', ' ');
field.add_subfield('a', text);
record.add_field(field);
},
}
Ok(())
}
fn parse_language(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let mut codes = Vec::new();
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
if local == b"languageTerm" {
let text = read_text(reader, buf)?;
if !text.is_empty() {
codes.push(text);
}
} else {
skip_element(reader, buf)?;
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"language" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
for code in codes {
let mut field = Field::new("041".to_string(), ' ', ' ');
field.add_subfield('a', code);
record.add_field(field);
}
Ok(())
}
fn parse_genre(reader: &mut Reader<&[u8]>, buf: &mut Vec<u8>, record: &mut Record) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("655".to_string(), ' ', '7');
field.add_subfield('a', text);
record.add_field(field);
}
Ok(())
}
fn parse_classification(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
) -> Result<()> {
let authority = info.attr(b"authority").map(String::from);
let text = read_text(reader, buf)?;
if text.is_empty() {
return Ok(());
}
match authority.as_deref() {
Some("lcc") => {
let mut field = Field::new("050".to_string(), ' ', '4');
field.add_subfield('a', text);
record.add_field(field);
},
Some("ddc") => {
let mut field = Field::new("082".to_string(), '0', '4');
field.add_subfield('a', text);
record.add_field(field);
},
_ => {
let mut field = Field::new("084".to_string(), ' ', ' ');
field.add_subfield('a', text);
if let Some(auth) = authority {
field.add_subfield('2', auth);
}
record.add_field(field);
},
}
Ok(())
}
fn parse_location(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
match local.as_slice() {
b"url" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("856".to_string(), '4', '0');
field.add_subfield('u', text);
record.add_field(field);
}
},
b"physicalLocation" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("852".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
},
b"shelfLocator" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("852".to_string(), ' ', ' ');
field.add_subfield('h', text);
record.add_field(field);
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"location" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
Ok(())
}
fn parse_related_item(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
) -> Result<()> {
let rel_type = info.attr(b"type");
let tag = match rel_type {
Some("host") => "773",
Some("preceding") => "780",
Some("succeeding") => "785",
Some("series") => "830",
_ => "787",
};
let mut title: Option<String> = None;
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
buf.clear();
if local == b"titleInfo" {
while let Some(child) = next_start(reader, buf)? {
if child.local_name == b"title" {
let text = read_text(reader, buf)?;
if !text.is_empty() {
title = Some(text);
}
} else {
skip_element(reader, buf)?;
}
}
} else {
skip_element(reader, buf)?;
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"relatedItem" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
if let Some(t) = title {
let ind1 = if tag == "830" { ' ' } else { '0' };
let mut field = Field::new(tag.to_string(), ind1, ' ');
let sub_code = if tag == "830" { 'a' } else { 't' };
field.add_subfield(sub_code, t);
record.add_field(field);
}
Ok(())
}
fn parse_record_info(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
loop {
match reader.read_event_into(buf) {
Ok(Event::Start(ref e)) => {
let child = StartInfo::from_event(e);
buf.clear();
match child.local_name.as_slice() {
b"recordIdentifier" => {
let source = child.attr(b"source").map(String::from);
let text = read_text(reader, buf)?;
if !text.is_empty() {
record.add_control_field("001".to_string(), text);
}
if let Some(src) = source {
if !src.is_empty() {
record.add_control_field("003".to_string(), src);
}
}
},
b"recordContentSource" => {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("040".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
},
b"languageOfCataloging" => {
while let Some(inner) = next_start(reader, buf)? {
if inner.local_name == b"languageTerm" {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("040".to_string(), ' ', ' ');
field.add_subfield('b', text);
record.add_field(field);
}
} else {
skip_element(reader, buf)?;
}
}
},
_ => skip_element(reader, buf)?,
}
},
Ok(Event::End(ref e)) => {
let local = strip_ns_owned(e.name().as_ref());
if local == b"recordInfo" {
break;
}
},
Ok(Event::Eof) => break,
Err(e) => return Err(MarcError::ParseError(format!("XML read: {e}"))),
_ => {},
}
buf.clear();
}
Ok(())
}
fn parse_access_condition(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
info: &StartInfo,
record: &mut Record,
) -> Result<()> {
let cond_type = info.attr(b"type");
let text = read_text(reader, buf)?;
if text.is_empty() {
return Ok(());
}
let tag = match cond_type {
Some("restriction on access" | "restrictionOnAccess") => "506",
_ => "540",
};
let mut field = Field::new(tag.to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
Ok(())
}
fn parse_table_of_contents(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("505".to_string(), '0', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
Ok(())
}
fn parse_target_audience(
reader: &mut Reader<&[u8]>,
buf: &mut Vec<u8>,
record: &mut Record,
) -> Result<()> {
let text = read_text(reader, buf)?;
if !text.is_empty() {
let mut field = Field::new("521".to_string(), ' ', ' ');
field.add_subfield('a', text);
record.add_field(field);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record::{Field, Record};
use crate::Leader;
fn make_test_leader() -> Leader {
Leader {
record_length: 1000,
record_status: 'n',
record_type: 'a',
bibliographic_level: 'm',
control_record_type: ' ',
character_coding: 'a',
indicator_count: 2,
subfield_code_count: 2,
data_base_address: 100,
encoding_level: ' ',
cataloging_form: 'a',
multipart_level: ' ',
reserved: "4500".to_string(),
}
}
#[test]
fn test_basic_mods_structure() {
let record = Record::new(make_test_leader());
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<?xml"));
assert!(mods.contains("<mods"));
assert!(mods.contains("</mods>"));
}
#[test]
fn test_title_extraction() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Test Title".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:title>Test Title</mods:title>"));
}
#[test]
fn test_title_with_subtitle() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Main Title".to_string());
field.add_subfield('b', "Subtitle".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:title>Main Title</mods:title>"));
assert!(mods.contains("<mods:subTitle>Subtitle</mods:subTitle>"));
}
#[test]
fn test_personal_name() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("100".to_string(), '1', ' ');
field.add_subfield('a', "Smith, John".to_string());
field.add_subfield('d', "1920-2000".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:name type=\"personal\">"));
assert!(mods.contains("Smith, John"));
assert!(mods.contains("1920-2000"));
}
#[test]
fn test_corporate_name() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("110".to_string(), '2', ' ');
field.add_subfield('a', "Library of Congress".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:name type=\"corporate\">"));
assert!(mods.contains("Library of Congress"));
}
#[test]
fn test_origin_info() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("260".to_string(), ' ', ' ');
field.add_subfield('a', "New York :".to_string());
field.add_subfield('b', "Penguin Books,".to_string());
field.add_subfield('c', "2020.".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:originInfo>"));
assert!(mods.contains("New York"));
assert!(mods.contains("Penguin Books"));
assert!(mods.contains("2020"));
}
#[test]
fn test_physical_description() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("300".to_string(), ' ', ' ');
field.add_subfield('a', "300 pages :".to_string());
field.add_subfield('b', "illustrations ;".to_string());
field.add_subfield('c', "24 cm".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:physicalDescription>"));
assert!(mods.contains("<mods:extent>300 pages :</mods:extent>"));
assert!(mods.contains("<mods:dimensions>24 cm</mods:dimensions>"));
}
#[test]
fn test_subject_topical() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("650".to_string(), ' ', '0');
field.add_subfield('a', "Science Fiction".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:subject>"));
assert!(mods.contains("<mods:topic>Science Fiction</mods:topic>"));
}
#[test]
fn test_subject_geographic() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("651".to_string(), ' ', '0');
field.add_subfield('a', "United States".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:subject>"));
assert!(mods.contains("<mods:geographic>United States</mods:geographic>"));
}
#[test]
fn test_isbn() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("020".to_string(), ' ', ' ');
field.add_subfield('a', "9780142424346".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:identifier type=\"isbn\">9780142424346</mods:identifier>"));
}
#[test]
fn test_xml_escaping() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Title with <brackets> & ampersand".to_string());
record.add_field(field);
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<"));
assert!(mods.contains(">"));
assert!(mods.contains("&"));
}
#[test]
fn test_type_of_resource() {
let record = Record::new(make_test_leader());
let mods = record_to_mods_xml(&record).expect("Failed to generate MODS");
assert!(mods.contains("<mods:typeOfResource>text</mods:typeOfResource>"));
}
#[test]
fn test_mods_parse_unprefixed() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo><title>Test Title</title></titleInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("245").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Test Title"));
}
#[test]
fn test_mods_parse_prefixed() {
let xml = r#"<mods:mods xmlns:mods="http://www.loc.gov/mods/v3">
<mods:titleInfo><mods:title>Prefixed Title</mods:title></mods:titleInfo>
</mods:mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("245").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Prefixed Title"));
}
#[test]
fn test_mods_parse_title_info() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo><title>Main Title</title></titleInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("245").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Main Title"));
}
#[test]
fn test_mods_parse_title_with_subtitle() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo>
<title>Main Title</title>
<subTitle>A Subtitle</subTitle>
</titleInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("245").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Main Title"));
assert_eq!(fields[0].get_subfield('b'), Some("A Subtitle"));
}
#[test]
fn test_mods_parse_alternative_title() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo type="alternative">
<title>Alt Title</title>
</titleInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("246").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Alt Title"));
}
#[test]
fn test_mods_parse_personal_name() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<name type="personal">
<namePart>Smith, John</namePart>
<namePart type="date">1920-2000</namePart>
<role><roleTerm>creator</roleTerm></role>
</name>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("100").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Smith, John"));
assert_eq!(fields[0].get_subfield('d'), Some("1920-2000"));
assert_eq!(fields[0].get_subfield('e'), Some("creator"));
}
#[test]
fn test_mods_parse_corporate_name() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<name type="corporate">
<namePart>Library of Congress</namePart>
<role><roleTerm>creator</roleTerm></role>
</name>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("110").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Library of Congress"));
}
#[test]
fn test_mods_parse_multiple_names() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<name type="personal">
<namePart>Smith, Jane</namePart>
<role><roleTerm>creator</roleTerm></role>
</name>
<name type="personal">
<namePart>Jones, Bob</namePart>
<role><roleTerm>author</roleTerm></role>
</name>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields_100 = record.get_fields("100").unwrap();
assert_eq!(fields_100[0].get_subfield('a'), Some("Smith, Jane"));
let fields_700 = record.get_fields("700").unwrap();
assert_eq!(fields_700[0].get_subfield('a'), Some("Jones, Bob"));
}
#[test]
fn test_mods_parse_type_of_resource() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<typeOfResource>text</typeOfResource>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
assert_eq!(record.leader.record_type, 'a');
let xml2 = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<typeOfResource>cartographic</typeOfResource>
</mods>"#;
let record2 = mods_xml_to_record(xml2).unwrap();
assert_eq!(record2.leader.record_type, 'e');
let xml3 = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<typeOfResource>sound recording</typeOfResource>
</mods>"#;
let record3 = mods_xml_to_record(xml3).unwrap();
assert_eq!(record3.leader.record_type, 'i');
}
#[test]
fn test_mods_parse_origin_info() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<originInfo>
<place><placeTerm>New York</placeTerm></place>
<publisher>Penguin Books</publisher>
<dateIssued>2020</dateIssued>
</originInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("260").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("New York"));
assert_eq!(fields[0].get_subfield('b'), Some("Penguin Books"));
assert_eq!(fields[0].get_subfield('c'), Some("2020"));
}
#[test]
fn test_mods_parse_physical_description() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<physicalDescription>
<extent>300 pages</extent>
<form>print</form>
<dimensions>24 cm</dimensions>
</physicalDescription>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("300").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("300 pages"));
assert_eq!(fields[0].get_subfield('b'), Some("print"));
assert_eq!(fields[0].get_subfield('c'), Some("24 cm"));
}
#[test]
fn test_mods_parse_abstract() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<abstract>This is a test abstract.</abstract>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("520").unwrap();
assert_eq!(
fields[0].get_subfield('a'),
Some("This is a test abstract.")
);
}
#[test]
fn test_mods_parse_note() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<note>A general note.</note>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("500").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("A general note."));
}
#[test]
fn test_mods_parse_subject_topic() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<subject><topic>Rust programming</topic></subject>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("650").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Rust programming"));
}
#[test]
fn test_mods_parse_subject_geographic() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<subject><geographic>United States</geographic></subject>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("651").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("United States"));
}
#[test]
fn test_mods_parse_identifiers() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<identifier type="isbn">9780142424346</identifier>
<identifier type="issn">0028-0836</identifier>
<identifier type="lccn">2020012345</identifier>
<identifier type="local">ocm12345</identifier>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let isbn = record.get_fields("020").unwrap();
assert_eq!(isbn[0].get_subfield('a'), Some("9780142424346"));
let issn = record.get_fields("022").unwrap();
assert_eq!(issn[0].get_subfield('a'), Some("0028-0836"));
let lccn = record.get_fields("010").unwrap();
assert_eq!(lccn[0].get_subfield('a'), Some("2020012345"));
assert_eq!(record.get_control_field("001"), Some("ocm12345"));
}
#[test]
fn test_mods_parse_language() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<language>
<languageTerm type="code" authority="iso639-2b">eng</languageTerm>
</language>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("041").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("eng"));
}
#[test]
fn test_mods_parse_genre() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<genre>Handbooks and manuals</genre>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("655").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Handbooks and manuals"));
}
#[test]
fn test_mods_parse_classification() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<classification authority="lcc">QA76.73</classification>
<classification authority="ddc">005.133</classification>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let lcc = record.get_fields("050").unwrap();
assert_eq!(lcc[0].get_subfield('a'), Some("QA76.73"));
let ddc = record.get_fields("082").unwrap();
assert_eq!(ddc[0].get_subfield('a'), Some("005.133"));
}
#[test]
fn test_mods_parse_location_url() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<location>
<url>https://example.com/resource</url>
</location>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("856").unwrap();
assert_eq!(
fields[0].get_subfield('u'),
Some("https://example.com/resource")
);
}
#[test]
fn test_mods_parse_related_item() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<relatedItem type="host">
<titleInfo><title>Host Journal</title></titleInfo>
</relatedItem>
<relatedItem type="series">
<titleInfo><title>Book Series</title></titleInfo>
</relatedItem>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let host = record.get_fields("773").unwrap();
assert_eq!(host[0].get_subfield('t'), Some("Host Journal"));
let series = record.get_fields("830").unwrap();
assert_eq!(series[0].get_subfield('a'), Some("Book Series"));
}
#[test]
fn test_mods_parse_record_info() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<recordInfo>
<recordIdentifier source="OCoLC">12345678</recordIdentifier>
<recordContentSource>DLC</recordContentSource>
</recordInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
assert_eq!(record.get_control_field("001"), Some("12345678"));
assert_eq!(record.get_control_field("003"), Some("OCoLC"));
let fields_040 = record.get_fields("040").unwrap();
assert_eq!(fields_040[0].get_subfield('a'), Some("DLC"));
}
#[test]
fn test_mods_parse_access_condition() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<accessCondition type="restriction on access">Restricted</accessCondition>
<accessCondition type="use and reproduction">Public domain</accessCondition>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let f506 = record.get_fields("506").unwrap();
assert_eq!(f506[0].get_subfield('a'), Some("Restricted"));
let f540 = record.get_fields("540").unwrap();
assert_eq!(f540[0].get_subfield('a'), Some("Public domain"));
}
#[test]
fn test_mods_parse_table_of_contents() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<tableOfContents>Chapter 1 -- Chapter 2</tableOfContents>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("505").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Chapter 1 -- Chapter 2"));
}
#[test]
fn test_mods_parse_target_audience() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<targetAudience>General</targetAudience>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("521").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("General"));
}
#[test]
fn test_mods_parse_empty_elements() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo><title></title></titleInfo>
<abstract></abstract>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
assert!(record.get_fields("245").is_none());
assert!(record.get_fields("520").is_none());
}
#[test]
fn test_mods_parse_xml_entities() {
let xml = r#"<mods xmlns="http://www.loc.gov/mods/v3">
<titleInfo><title>Title & More <stuff></title></titleInfo>
</mods>"#;
let record = mods_xml_to_record(xml).unwrap();
let fields = record.get_fields("245").unwrap();
assert_eq!(fields[0].get_subfield('a'), Some("Title & More <stuff>"));
}
#[test]
fn test_mods_collection_parse() {
let xml = r#"<modsCollection xmlns="http://www.loc.gov/mods/v3">
<mods>
<titleInfo><title>First Record</title></titleInfo>
</mods>
<mods>
<titleInfo><title>Second Record</title></titleInfo>
</mods>
</modsCollection>"#;
let records = mods_xml_to_records(xml).unwrap();
assert_eq!(records.len(), 2);
let f1 = records[0].get_fields("245").unwrap();
assert_eq!(f1[0].get_subfield('a'), Some("First Record"));
let f2 = records[1].get_fields("245").unwrap();
assert_eq!(f2[0].get_subfield('a'), Some("Second Record"));
}
#[test]
fn test_mods_roundtrip() {
let mut record = Record::new(make_test_leader());
record.add_control_field("001".to_string(), "test123".to_string());
let mut f245 = Field::new("245".to_string(), '1', '0');
f245.add_subfield('a', "Test Title".to_string());
f245.add_subfield('b', "A Subtitle".to_string());
record.add_field(f245);
let mut f100 = Field::new("100".to_string(), '1', ' ');
f100.add_subfield('a', "Smith, Jane".to_string());
record.add_field(f100);
let mut f260 = Field::new("260".to_string(), ' ', ' ');
f260.add_subfield('a', "New York :".to_string());
f260.add_subfield('b', "Penguin,".to_string());
f260.add_subfield('c', "2020.".to_string());
record.add_field(f260);
let mut f020 = Field::new("020".to_string(), ' ', ' ');
f020.add_subfield('a', "9780142424346".to_string());
record.add_field(f020);
let mut f650 = Field::new("650".to_string(), ' ', '0');
f650.add_subfield('a', "Rust programming".to_string());
record.add_field(f650);
let mods_xml = record_to_mods_xml(&record).unwrap();
let restored = mods_xml_to_record(&mods_xml).unwrap();
let title = restored.get_fields("245").unwrap();
assert_eq!(title[0].get_subfield('a'), Some("Test Title"));
assert_eq!(title[0].get_subfield('b'), Some("A Subtitle"));
let name = restored.get_fields("100").unwrap();
assert_eq!(name[0].get_subfield('a'), Some("Smith, Jane"));
let pub_info = restored.get_fields("260").unwrap();
assert_eq!(pub_info[0].get_subfield('a'), Some("New York :"));
assert_eq!(pub_info[0].get_subfield('b'), Some("Penguin,"));
assert_eq!(pub_info[0].get_subfield('c'), Some("2020."));
let isbn = restored.get_fields("020").unwrap();
assert_eq!(isbn[0].get_subfield('a'), Some("9780142424346"));
let subj = restored.get_fields("650").unwrap();
assert_eq!(subj[0].get_subfield('a'), Some("Rust programming"));
assert_eq!(restored.get_control_field("001"), Some("test123"));
}
}