use std::fmt::Write;
use crate::error::Result;
use crate::record::Record;
#[derive(Debug, Clone, Default)]
pub struct DublinCoreRecord {
pub title: Vec<String>,
pub creator: Vec<String>,
pub subject: Vec<String>,
pub description: Vec<String>,
pub publisher: Vec<String>,
pub contributor: Vec<String>,
pub date: Vec<String>,
pub dc_type: Vec<String>,
pub format: Vec<String>,
pub identifier: Vec<String>,
pub source: Vec<String>,
pub language: Vec<String>,
pub relation: Vec<String>,
pub coverage: Vec<String>,
pub rights: Vec<String>,
}
pub fn record_to_dublin_core(record: &Record) -> Result<DublinCoreRecord> {
let mut dc = DublinCoreRecord::default();
extract_titles(record, &mut dc);
extract_creators(record, &mut dc);
extract_subjects(record, &mut dc);
extract_descriptions(record, &mut dc);
extract_publishers_and_dates(record, &mut dc);
extract_contributors(record, &mut dc);
extract_identifiers(record, &mut dc);
extract_language(record, &mut dc);
extract_formats(record, &mut dc);
extract_coverage(record, &mut dc);
extract_rights(record, &mut dc);
Ok(dc)
}
pub fn record_to_dublin_core_xml(record: &Record) -> Result<String> {
let dc = record_to_dublin_core(record)?;
Ok(dublin_core_to_xml(&dc))
}
fn extract_titles(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields_245) = record.fields.get("245") {
for field in fields_245 {
let mut title_parts = Vec::new();
for subfield in &field.subfields {
match subfield.code {
'a' | 'b' | 'c' => title_parts.push(subfield.value.clone()),
_ => {},
}
}
if !title_parts.is_empty() {
dc.title.push(title_parts.join(" "));
}
}
}
}
fn extract_creators(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("100") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.creator.push(subfield.value.clone());
}
}
}
if let Some(fields) = record.fields.get("110") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.creator.push(subfield.value.clone());
}
}
}
}
fn extract_subjects(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("600") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.subject.push(subfield.value.clone());
}
}
}
if let Some(fields) = record.fields.get("610") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.subject.push(subfield.value.clone());
}
}
}
if let Some(fields) = record.fields.get("650") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.subject.push(subfield.value.clone());
}
}
}
}
fn extract_descriptions(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("520") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.description.push(subfield.value.clone());
}
}
}
if let Some(fields) = record.fields.get("500") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.description.push(subfield.value.clone());
}
}
}
}
fn extract_publishers_and_dates(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("260") {
for field in fields {
for subfield in &field.subfields {
match subfield.code {
'a' => dc.publisher.push(subfield.value.clone()),
'c' => dc.date.push(subfield.value.clone()),
_ => {},
}
}
}
}
}
fn extract_contributors(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("700") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.contributor.push(subfield.value.clone());
}
}
}
if let Some(fields) = record.fields.get("710") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.contributor.push(subfield.value.clone());
}
}
}
}
fn extract_identifiers(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("020") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.identifier.push(format!("ISBN: {}", subfield.value));
}
}
}
if let Some(control_001) = record.control_fields.get("001").and_then(|v| v.first()) {
dc.identifier.push(format!("Control#: {control_001}"));
}
}
fn extract_language(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("041") {
for field in fields {
for subfield in &field.subfields {
if subfield.code == 'a' {
let langs: Vec<&str> = subfield.value.split_whitespace().collect();
for lang in langs {
if !lang.is_empty() {
dc.language.push(lang.to_string());
}
}
}
}
}
}
}
fn extract_formats(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("300") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.format.push(subfield.value.clone());
}
}
}
}
fn extract_coverage(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("651") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.coverage.push(subfield.value.clone());
}
}
}
}
fn extract_rights(record: &Record, dc: &mut DublinCoreRecord) {
if let Some(fields) = record.fields.get("540") {
for field in fields {
if let Some(subfield) = field.subfields.iter().find(|s| s.code == 'a') {
dc.rights.push(subfield.value.clone());
}
}
}
}
#[must_use]
pub fn dublin_core_to_xml(dc: &DublinCoreRecord) -> String {
let mut xml = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" ");
xml.push_str("xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n");
xml.push_str(" <rdf:Description>\n");
write_elements(&mut xml, "dc:title", &dc.title);
write_elements(&mut xml, "dc:creator", &dc.creator);
write_elements(&mut xml, "dc:subject", &dc.subject);
write_elements(&mut xml, "dc:description", &dc.description);
write_elements(&mut xml, "dc:publisher", &dc.publisher);
write_elements(&mut xml, "dc:contributor", &dc.contributor);
write_elements(&mut xml, "dc:date", &dc.date);
write_elements(&mut xml, "dc:type", &dc.dc_type);
write_elements(&mut xml, "dc:format", &dc.format);
write_elements(&mut xml, "dc:identifier", &dc.identifier);
write_elements(&mut xml, "dc:source", &dc.source);
write_elements(&mut xml, "dc:language", &dc.language);
write_elements(&mut xml, "dc:relation", &dc.relation);
write_elements(&mut xml, "dc:coverage", &dc.coverage);
write_elements(&mut xml, "dc:rights", &dc.rights);
xml.push_str(" </rdf:Description>\n");
xml.push_str("</rdf:RDF>\n");
xml
}
fn write_elements(xml: &mut String, tag: &str, values: &[String]) {
for value in values {
writeln!(xml, " <{tag}>{}</{tag}>", escape_xml(value)).ok();
}
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[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_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 dc = record_to_dublin_core(&record).expect("Failed to convert");
assert_eq!(dc.title.len(), 1);
assert_eq!(dc.title[0], "Test 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 dc = record_to_dublin_core(&record).expect("Failed to convert");
assert_eq!(dc.title.len(), 1);
assert!(dc.title[0].contains("Main Title"));
assert!(dc.title[0].contains("Subtitle"));
}
#[test]
fn test_creator_extraction() {
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());
record.add_field(field);
let dc = record_to_dublin_core(&record).expect("Failed to convert");
assert_eq!(dc.creator.len(), 1);
assert_eq!(dc.creator[0], "Smith, John");
}
#[test]
fn test_subject_extraction() {
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 dc = record_to_dublin_core(&record).expect("Failed to convert");
assert_eq!(dc.subject.len(), 1);
assert_eq!(dc.subject[0], "Science Fiction");
}
#[test]
fn test_isbn_identifier() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("020".to_string(), ' ', ' ');
field.add_subfield('a', "1234567890".to_string());
record.add_field(field);
let dc = record_to_dublin_core(&record).expect("Failed to convert");
assert!(dc.identifier.iter().any(|id| id.contains("ISBN")));
}
#[test]
fn test_control_number_identifier() {
let mut record = Record::new(make_test_leader());
record.add_control_field("001".to_string(), "12345".to_string());
let dc = record_to_dublin_core(&record).expect("Failed to convert");
assert!(dc.identifier.iter().any(|id| id.contains("Control#")));
}
#[test]
fn test_dublin_core_to_xml() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("245".to_string(), '1', '0');
field.add_subfield('a', "Test".to_string());
record.add_field(field);
let dc = record_to_dublin_core(&record).expect("Failed to convert");
let xml = dublin_core_to_xml(&dc);
assert!(xml.contains("<?xml"));
assert!(xml.contains("rdf:RDF"));
assert!(xml.contains("dc:title"));
assert!(xml.contains("Test"));
}
#[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 dc = record_to_dublin_core(&record).expect("Failed to convert");
let xml = dublin_core_to_xml(&dc);
assert!(xml.contains("<"));
assert!(xml.contains(">"));
assert!(xml.contains("&"));
}
#[test]
fn test_language_extraction() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("041".to_string(), ' ', ' ');
field.add_subfield('a', "eng fre".to_string());
record.add_field(field);
let dc = record_to_dublin_core(&record).expect("Failed to convert");
assert!(!dc.language.is_empty());
}
#[test]
fn test_description_from_summary() {
let mut record = Record::new(make_test_leader());
let mut field = Field::new("520".to_string(), ' ', ' ');
field.add_subfield('a', "This is a summary".to_string());
record.add_field(field);
let dc = record_to_dublin_core(&record).expect("Failed to convert");
assert!(dc.description.iter().any(|d| d.contains("summary")));
}
}