use marc_rs_derive::MarcPaths;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
mod types;
pub use types::*;
use crate::Encoding;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathKind {
VecPush,
VecStructCreator,
VecStructField,
OptionInit,
OptionSet,
}
pub trait MarcPaths: Sized {
const IS_LEAF: bool;
fn from_marc_str(s: &str) -> Self;
fn to_marc_str(&self) -> String;
fn marc_set(&mut self, path: &str, value: &str) -> bool;
fn marc_get_option(&self, path: &str) -> Option<String>;
fn marc_get_vec(&self, path: &str) -> Option<Vec<String>>;
fn marc_path_kind(path: &str) -> Option<PathKind>;
fn marc_has_path(path: &str) -> bool;
fn marc_is_vec_leaf(path: &str) -> bool;
fn marc_creator_field() -> &'static str;
}
pub trait FromRuleValue: Sized + DeserializeOwned + Serialize {
fn from_rule_value(s: &str) -> Self;
fn to_rule_value(&self) -> String;
}
macro_rules! impl_from_rule_value {
($type:ty, $other:path) => {
impl FromRuleValue for $type {
fn from_rule_value(s: &str) -> Self {
serde_json::from_value(serde_json::Value::String(s.to_string())).unwrap_or_else(|_| $other(s.to_string()))
}
fn to_rule_value(&self) -> String {
match serde_json::to_value(self).ok() {
Some(serde_json::Value::String(s)) => s,
_ => match self {
$other(s) => s.clone(),
_ => unreachable!(),
},
}
}
}
};
}
impl_from_rule_value!(Language, Language::Other);
impl_from_rule_value!(Country, Country::Other);
impl_from_rule_value!(TargetAudience, TargetAudience::Other);
impl_from_rule_value!(ClassificationScheme, ClassificationScheme::Other);
impl_from_rule_value!(SubjectType, SubjectType::Other);
impl_from_rule_value!(NoteType, NoteType::Other);
impl_from_rule_value!(LinkType, LinkType::Other);
impl_from_rule_value!(Relator, Relator::Other);
macro_rules! impl_from_rule_value_char {
($type:ty, $other:path) => {
impl FromRuleValue for $type {
fn from_rule_value(s: &str) -> Self {
serde_json::from_value(serde_json::Value::String(s.to_string())).unwrap_or_else(|_| $other(s.chars().next().unwrap_or(' ')))
}
fn to_rule_value(&self) -> String {
match serde_json::to_value(self).ok() {
Some(serde_json::Value::String(s)) => s,
_ => match self {
$other(c) => c.to_string(),
_ => unreachable!(),
},
}
}
}
};
}
impl_from_rule_value_char!(RecordStatus, RecordStatus::Other);
impl_from_rule_value_char!(RecordType, RecordType::Other);
impl_from_rule_value_char!(BibliographicLevel, BibliographicLevel::Other);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordValidationIssue {
pub tag: String,
pub subfield: Option<char>,
pub target_path: String,
pub value: String,
pub pattern: String,
}
fn default_record_valid() -> bool {
true
}
macro_rules! impl_marc_leaf {
($ty:ty) => {
impl MarcPaths for $ty {
const IS_LEAF: bool = true;
fn from_marc_str(s: &str) -> Self {
<$ty as FromRuleValue>::from_rule_value(s)
}
fn to_marc_str(&self) -> String {
<$ty as FromRuleValue>::to_rule_value(self)
}
fn marc_set(&mut self, _: &str, _: &str) -> bool {
false
}
fn marc_get_option(&self, _: &str) -> Option<String> {
None
}
fn marc_get_vec(&self, _: &str) -> Option<Vec<String>> {
None
}
fn marc_path_kind(_: &str) -> Option<PathKind> {
None
}
fn marc_has_path(_: &str) -> bool {
false
}
fn marc_is_vec_leaf(_: &str) -> bool {
false
}
fn marc_creator_field() -> &'static str {
""
}
}
};
}
impl_marc_leaf!(Language);
impl_marc_leaf!(Country);
impl_marc_leaf!(TargetAudience);
impl_marc_leaf!(ClassificationScheme);
impl_marc_leaf!(SubjectType);
impl_marc_leaf!(NoteType);
impl_marc_leaf!(LinkType);
impl_marc_leaf!(Relator);
#[derive(Debug, Clone, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Record {
#[marc(skip)]
pub leader: Leader,
#[marc(skip)]
#[serde(skip)]
pub encoding: Option<Encoding>,
#[marc(skip)]
#[serde(default = "default_record_valid")]
pub valid: bool,
#[marc(skip)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub validation_issues: Vec<RecordValidationIssue>,
#[serde(default)]
pub identification: Identification,
#[serde(default)]
pub coded: Coded,
#[serde(default)]
pub description: Description,
#[serde(default)]
pub notes: Notes,
#[serde(default)]
pub links: Links,
#[serde(default)]
pub associated_titles: AssociatedTitles,
#[serde(default)]
pub indexing: Indexing,
#[serde(default)]
pub responsibility: Responsibility,
#[serde(default)]
pub international: International,
#[serde(default)]
pub local: Local,
}
impl Default for Record {
fn default() -> Self {
Self {
leader: Leader::default(),
encoding: None,
valid: true,
validation_issues: Vec::new(),
identification: Identification::default(),
coded: Coded::default(),
description: Description::default(),
notes: Notes::default(),
links: Links::default(),
associated_titles: AssociatedTitles::default(),
indexing: Indexing::default(),
responsibility: Responsibility::default(),
international: International::default(),
local: Local::default(),
}
}
}
impl Record {
pub fn validation_report(&self) -> String {
if self.validation_issues.is_empty() {
return String::new();
}
let mut s = String::from("catalog pattern validation failed:\n");
for issue in &self.validation_issues {
let sub = issue.subfield.map(|c| format!("${}", c)).unwrap_or_else(|| "-".to_string());
s.push_str(&format!(
" tag {} subfield {} path {} value {:?} pattern {}\n",
issue.tag, sub, issue.target_path, issue.value, issue.pattern
));
}
s
}
pub fn authors(&self) -> impl Iterator<Item = &Agent> {
self.responsibility.main_entry.iter().chain(self.responsibility.added_entries.iter())
}
pub fn languages(&self) -> &[Language] {
&self.coded.languages
}
pub fn titles(&self) -> Vec<&Title> {
let mut out = Vec::new();
if let Some(t) = &self.description.title {
out.push(t);
}
if let Some(t) = &self.associated_titles.uniform_title {
out.push(t);
}
out
}
pub fn audience(&self) -> Option<&TargetAudience> {
self.coded.target_audience.as_ref()
}
pub fn isbn(&self) -> &[Isbn] {
&self.identification.isbn
}
pub fn items(&self) -> &[Item] {
&self.local.items
}
pub fn media_type(&self) -> &RecordType {
&self.leader.record_type
}
pub fn isbn_string(&self) -> Option<String> {
if self.identification.isbn.is_empty() {
return None;
}
Some(self.identification.isbn.iter().map(|i| i.value.as_str()).collect::<Vec<_>>().join(", "))
}
pub fn title_main(&self) -> Option<&str> {
self.description.title.as_ref().map(|t| t.main.as_str())
}
pub fn subject_main(&self) -> Option<&str> {
self.indexing.subjects.first().map(|s| s.value.as_str())
}
pub fn keywords(&self) -> &[String] {
&self.indexing.uncontrolled_terms
}
pub fn publication_date(&self) -> Option<&str> {
self.description.publication.first().and_then(|p| p.date.as_deref())
}
pub fn page_extent(&self) -> Option<&str> {
self.description.physical_description.as_ref().and_then(|p| p.extent.as_deref())
}
pub fn dimensions(&self) -> Option<&str> {
self.description.physical_description.as_ref().and_then(|p| p.dimensions.as_deref())
}
pub fn accompanying_material_text(&self) -> Option<&str> {
self.description.physical_description.as_ref().and_then(|p| p.accompanying_material.as_deref())
}
pub fn table_of_contents_text(&self) -> Option<&str> {
self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::Contents)).then(|| n.text.as_str()))
}
pub fn abstract_text(&self) -> Option<&str> {
self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::Summary)).then(|| n.text.as_str()))
}
pub fn general_note_text(&self) -> Option<&str> {
self.notes.items.iter().find_map(|n| matches!(n.note_type, Some(NoteType::General)).then(|| n.text.as_str()))
}
pub fn lang_primary(&self) -> Option<&Language> {
self.coded.languages.first()
}
pub fn lang_original(&self) -> Option<&Language> {
self.coded.original_languages.first()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Identification {
#[serde(skip_serializing_if = "Option::is_none")]
pub record_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub agency_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub record_version_date: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub isbn: Vec<Isbn>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issn: Vec<Issn>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub national_bibliography_numbers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub national_library_record_numbers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub legal_deposit_numbers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub lccn: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub system_control_numbers: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub patent_numbers: Vec<PatentNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub technical_report_numbers: Vec<TechnicalReportNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub publisher_numbers: Vec<PublisherNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub codens: Vec<Coden>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub original_study_numbers: Vec<OriginalStudyNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub government_document_numbers: Vec<GovernmentDocumentNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub report_numbers: Vec<ReportNumber>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Coded {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub languages: Vec<Language>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub original_languages: Vec<Language>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<Country>,
#[serde(skip_serializing_if = "Option::is_none")]
pub publication_dates: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target_audience: Option<TargetAudience>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub geographic_area_codes: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub time_period_codes: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_entered_on_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_of_date: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date1: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date2: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub government_publication: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modified_record: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cataloging_language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transliteration_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_set: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_character_set: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub script_of_title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub place_of_publication_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cataloging_source_code: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Description {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub edition: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub publication: Vec<Publication>,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_description: Option<PhysicalDescription>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub series: Vec<SeriesStatement>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub varying_titles: Vec<VaryingTitle>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Notes {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<Note>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Links {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub records: Vec<LinkedRecord>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct AssociatedTitles {
#[serde(skip_serializing_if = "Option::is_none")]
pub uniform_title: Option<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub key_title: Option<Title>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub former_titles: Vec<Title>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub variant_titles: Vec<Title>,
#[serde(skip_serializing_if = "Option::is_none")]
pub abbreviated_title: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Indexing {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subjects: Vec<Subject>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub classifications: Vec<Classification>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub uncontrolled_terms: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Responsibility {
#[marc(skip)]
#[serde(skip_serializing_if = "Option::is_none")]
pub main_entry: Option<Agent>,
#[marc(skip)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub added_entries: Vec<Agent>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct International {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cataloging_sources: Vec<CatalogingSource>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub location_call_numbers: Vec<LocationCallNumber>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub electronic_locations: Vec<ElectronicLocation>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub holding_institutions: Vec<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, MarcPaths)]
#[serde(rename_all = "camelCase")]
pub struct Local {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub items: Vec<Item>,
}