///|
fn is_control_tag_for_serialization(tag : String) -> Bool {
tag >= "001" && tag <= "009"
}
///|
fn valid_ascii_char(value : Char) -> Bool {
value.to_int() <= 0x7F
}
///|
fn valid_tag(tag : String) -> Bool {
if tag.length() != 3 {
return false
}
for char in tag {
if !valid_ascii_char(char) {
return false
}
}
true
}
///|
fn has_iso2709_delimiter(bytes : Bytes) -> Bool {
for byte in bytes {
let value = byte.to_int()
if value == 0x1D || value == 0x1E || value == 0x1F {
return true
}
}
false
}
///|
fn ascii_number(value : Int, width : Int) -> String? {
let text = value.to_string()
if value < 0 || text.length() > width {
None
} else {
Some(text.pad_start(width, '0'))
}
}
///|
fn patch_ascii_range(raw : String, start : Int, value : String) -> String {
let chars : Array[Char] = []
for char in raw {
chars.push(char)
}
let mut index = 0
for char in value {
chars[start + index] = char
index = index + 1
}
String::from_array(chars)
}
///|
fn encode_field(
field : @record.MarcField,
) -> Result[(String, Bytes), SerializeError] {
match field {
@record.MarcField::ControlField(tag~, value~) => {
if !valid_tag(tag) {
return Err(InvalidTag(tag))
}
if !is_control_tag_for_serialization(tag) {
return Err(ControlFieldTag(tag))
}
let content = @utf8.encode(value)
if has_iso2709_delimiter(content) {
return Err(InvalidFieldValue(tag))
}
let field_bytes = @binary.concat([content, Bytes::from_array([0x1E])])
if field_bytes.length() > 9999 {
return Err(FieldTooLong(tag, field_bytes.length()))
}
Ok((tag, field_bytes))
}
@record.MarcField::DataField(tag~, indicator1~, indicator2~, subfields~) => {
if !valid_tag(tag) {
return Err(InvalidTag(tag))
}
if is_control_tag_for_serialization(tag) {
return Err(ControlFieldTag(tag))
}
if !valid_ascii_char(indicator1) || !valid_ascii_char(indicator2) {
return Err(InvalidIndicator(tag))
}
let pieces : Array[Bytes] = [
@binary.ascii(indicator1.to_string() + indicator2.to_string()),
]
for subfield in subfields {
if !valid_ascii_char(subfield.code) ||
subfield.code == '\u{1D}' ||
subfield.code == '\u{1E}' ||
subfield.code == '\u{1F}' {
return Err(InvalidSubfieldCode(subfield.code))
}
let value = @utf8.encode(subfield.value)
if has_iso2709_delimiter(value) {
return Err(InvalidFieldValue(tag))
}
pieces.push(Bytes::from_array([0x1F]))
pieces.push(@binary.ascii(subfield.code.to_string()))
pieces.push(value)
}
let content = @binary.concat(pieces)
let field_bytes = @binary.concat([content, Bytes::from_array([0x1E])])
if field_bytes.length() > 9999 {
return Err(FieldTooLong(tag, field_bytes.length()))
}
Ok((tag, field_bytes))
}
}
}
///|
/// Serialize one parsed MARC 21 record to ISO 2709 bytes.
pub fn serialize_record(
record : @record.MarcRecord,
) -> Result[Bytes, SerializeError] {
let encoded_fields : Array[(String, Bytes)] = []
let mut variable_length = 0
for field in record.fields() {
let encoded = match encode_field(field) {
Err(error) => return Err(error)
Ok(value) => value
}
encoded_fields.push(encoded)
variable_length = variable_length + encoded.1.length()
}
let directory_length = encoded_fields.length() * 12
let base_address = 24 + directory_length + 1
let total_length = base_address + variable_length + 1
if base_address > 99999 {
return Err(DirectoryOverflow(base_address))
}
if total_length > 99999 {
return Err(RecordTooLong(total_length))
}
let mut directory = ""
let mut start = 0
for encoded in encoded_fields {
let length = match ascii_number(encoded.1.length(), 4) {
None => return Err(FieldTooLong(encoded.0, encoded.1.length()))
Some(value) => value
}
let offset = match ascii_number(start, 5) {
None => return Err(DirectoryOverflow(start))
Some(value) => value
}
directory = directory + encoded.0 + length + offset
start = start + encoded.1.length()
}
let leader = record.leader().raw()
let record_length = match ascii_number(total_length, 5) {
None => return Err(RecordTooLong(total_length))
Some(value) => value
}
let base = match ascii_number(base_address, 5) {
None => return Err(DirectoryOverflow(base_address))
Some(value) => value
}
// MoonMARC always emits standard MARC 21 directory entries: 3 + 4 + 5 + 0.
let leader = patch_ascii_range(leader, 10, "22")
let leader = patch_ascii_range(leader, 20, "4500")
let leader = patch_ascii_range(leader, 0, record_length)
let leader = patch_ascii_range(leader, 12, base)
let parts : Array[Bytes] = [
@binary.ascii(leader),
@binary.ascii(directory),
Bytes::from_array([0x1E]),
]
for encoded in encoded_fields {
parts.push(encoded.1)
}
parts.push(Bytes::from_array([0x1D]))
Ok(@binary.concat(parts))
}
///|
/// Serialize multiple MARC records as one concatenated ISO 2709 stream.
pub fn serialize_records(
records : Array[@record.MarcRecord],
) -> Result[Bytes, SerializeError] {
let parts : Array[Bytes] = []
for record in records {
match serialize_record(record) {
Err(error) => return Err(error)
Ok(value) => parts.push(value)
}
}
Ok(@binary.concat(parts))
}