codex

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 4 Imported by: 0

README

libcodex

A small, dependency-free Go module for reading, writing and converting MARC 21 bibliographic records across four serializations: binary ISO 2709 (.mrc), MARCXML, MARC-in-JSON, and the MARCMaker mnemonic text format (.mrk).

  • Module path: github.com/freeeve/libcodex
  • Core package codex: the format-agnostic MARC model and the codec interfaces
  • One subpackage per serialization, all sharing the same surface
  • Standard library only — no third-party dependencies
  • Go 1.25+

The model is domain-agnostic: it exposes leaders, fields, subfields and indicators, and leaves interpretation of specific tags to the caller.

Install

go get github.com/freeeve/libcodex
import (
	"github.com/freeeve/libcodex"          // package codex — the model + interfaces
	"github.com/freeeve/libcodex/iso2709"  // a format codec (one of four)
)

(The core import path ends in libcodex; the package identifier is codex.)

Architecture

codex holds the shared in-memory model — Record, Field, Subfield, Leader — and two interfaces:

type RecordReader interface{ Read() (*Record, error) }  // io.EOF at end of stream
type RecordWriter interface{ Write(*Record) error }

Each serialization is a subpackage that maps that one model onto a wire format, so the same record round-trips through any of them and converts between any two.

Package Format Notes
iso2709 binary ISO 2709 (.mrc) the interchange format; fastest path
marcxml MARCXML (LoC slim schema) Writer wraps a <collection>; needs Close
marcjson MARC-in-JSON (pymarc layout) Writer emits a JSON array; needs Close
mrk MARCMaker mnemonic text human-readable, line-based

Every codec exposes the same surface:

NewReader(io.Reader) *Reader        Decode([]byte) (*codex.Record, …)
NewWriter(io.Writer) *Writer        Encode(*codex.Record) ([]byte, error)
ReadFile(path) ([]*codex.Record, …) WriteFile(path, []*codex.Record) error
(*Reader).All() iter.Seq2[*codex.Record, error]

Reading

Readers stream one record at a time. The API is identical for every format — just pick the package:

f, _ := os.Open("catalog.mrc")
defer f.Close()

r := iso2709.NewReader(f) // or marcxml.NewReader, marcjson.NewReader, mrk.NewReader
for rec, err := range r.All() {
	if err != nil {
		log.Println("bad record:", err) // iteration stops at the first error
		break
	}
	title := rec.SubfieldValue("245", 'a')
	author := rec.SubfieldValue("100", 'a')
	fmt.Printf("%s — %s (id %s)\n", title, author, rec.ControlField("001"))
	for _, subject := range rec.SubfieldValues("650", 'a') {
		fmt.Println("  subject:", subject)
	}
}

Convenience helpers per format: iso2709.ReadFile(path) and iso2709.Decode(raw) (the binary Decode also returns a lossy bool — see Character encoding).

Writing

Build a record from scratch and serialize it. Builders are chainable:

rec := codex.NewRecord().
	AddField(codex.NewControlField("001", "qll-00042")).
	AddField(codex.NewDataField("245", '1', '0',
		codex.NewSubfield('a', "Stone butch blues :"),
		codex.NewSubfield('b', "a novel"),
		codex.NewSubfield('c', "Leslie Feinberg."))).
	AddField(codex.NewDataField("650", ' ', '0',
		codex.NewSubfield('a', "Lesbians")))

raw, err := iso2709.Encode(rec) // or marcxml.Encode, marcjson.Encode, mrk.Encode

The marcxml and marcjson writers buffer a wrapper element, so call Close when finished:

w := marcxml.NewWriter(out)
for _, rec := range recs {
	w.Write(rec)
}
w.Close() // emits </collection>

Converting between formats

Because the codecs share interfaces, codex.Convert pipes any reader into any writer — that is the whole conversion engine:

// Binary MARC on stdin → MARCXML on stdout.
w := marcxml.NewWriter(os.Stdout)
if err := codex.Convert(iso2709.NewReader(os.Stdin), w); err != nil {
	log.Fatal(err)
}
w.Close()

Any of the 4 × 4 source/target combinations works and preserves the model.

Export converters (one-way)

Beyond the four round-trip serializations, the library exports to formats that use a different, lossy model — a documented MARC→target crosswalk, not a codec. Their Writers implement codex.RecordWriter, so they also work with codex.Convert:

Package Target
mods MODS (LoC XML, near-lossless)
dublincore Dublin Core — oai_dc XML and DC-JSON
citation RIS and BibTeX (reference managers)
bibframe BIBFRAME 2.0 — RDF/XML and JSON-LD
// Binary MARC → BibTeX for a reference manager.
codex.Convert(iso2709.NewReader(in), citation.NewBibTeXWriter(out))
// Or a single record: b, _ := mods.Encode(rec) / dublincore.Encode(rec) / citation.RIS(rec)

bibframe is the one that changes data model, not just serialization: each MARC record becomes a small RDF graph of a bf:Work (intellectual content) and a bf:Instance (a publication of it), linked by bf:instanceOf/bf:hasInstance. Both serializations are hand-written with the standard library — no RDF dependency:

// Binary MARC → BIBFRAME RDF/XML (canonical) or JSON-LD.
b, _ := bibframe.Encode(rec)        // RDF/XML
b, _ := bibframe.EncodeJSONLD(rec)  // JSON-LD
// Streaming collections (must be closed): bibframe.NewWriter / NewJSONLDWriter.

These are export-only (the targets cannot round-trip back to full MARC) and carry only the common fields; each package documents its crosswalk.

Accessors

On *Record:

  • Leader() Leader, Encoding() byte, Fields() []Field
  • ControlField(tag string) string
  • DataField(tag string) (Field, bool), DataFields(tag string) []Field
  • SubfieldValue(tag string, code byte) string, SubfieldValues(tag string, code byte) []string
  • Building/editing (chainable): AddField, InsertField (tag-ordered), ReplaceField, RemoveFields(tag), SetLeader
  • Validate() error — structural checks (24-byte leader, 3-byte tags, data fields have subfields)

On Field: IsControl(), Indicators() (byte, byte), Subfield(code), SubfieldValue(code), SubfieldValues(code).

On Leader: RecordStatus() (byte 5), RecordType() (byte 6), Encoding() (byte 9), IsUnicode(), RecordLength() ([0:5]), BaseAddress() ([12:17]).

Performance

Encoding hand-builds output into a reusable buffer, so writers stream at roughly zero allocations per record. Decoding cost tracks the wire format. Indicative numbers for a ~10-field record (Apple M3 Max):

Format Decode (allocs / MB·s) Encode (allocs) Streaming write
iso2709 4 / 864 1 ~0 allocs/record
mrk 35 / 200 7 ~0 allocs/record
marcxml 374 / 60 9 ~0 allocs/record
marcjson 566 / 40 7 ~0 allocs/record

The binary codec is the fast path for bulk work; the XML/JSON decoders are bound by the standard library's encoding/xml and encoding/json tokenizers (a deliberate correctness-over-speed, zero-dependency choice). Run go test -bench=. in any subpackage to reproduce.

Character encoding

MARC 21 with UTF-8 (leader byte 9 == 'a') is the primary, preferred form. The text formats (MARCXML, MARC-in-JSON, .mrk) are UTF-8 throughout.

For older MARC-8 binary records (leader byte 9 == blank), iso2709 transcodes values to UTF-8 on read for the common Western subset, so every value the API exposes is a UTF-8 Go string regardless of source encoding:

  • Basic Latin (ASCII, the default G0 set).
  • ANSEL Extended Latin (the default G1 set), including spacing graphics and combining diacritics. MARC-8 stores a combining mark before its base character (the reverse of Unicode); the decoder reorders it and composes common base+mark pairs to a precomposed (NFC) code point (e.g. combining acute + eé). The ANSEL table is verified against the LoC code tables, including the 2005 alif remapping and the euro/eszett additions.

iso2709 can also write legacy MARC-8 (leader byte 9 = blank) via iso2709.EncodeMARC8, the inverse of the read path over the same Western subset. It returns an error if a value contains a character outside that subset, so you never get a record that claims MARC-8 but holds untranscodable data.

Out of scope (best-effort pass-through, never a crash): EACC/CJK, Cyrillic, Greek, Hebrew, Arabic and the subscript/superscript/Greek-symbol sets. On read, their escape designations are recognized only well enough to skip them; their bytes pass through as Latin-1 rather than being transcoded, and iso2709.Decode returns a lossy bool (and iso2709.Reader.Lossy() reports the last read) so callers can detect this and avoid re-serializing mojibake as clean UTF-8. On write, EncodeMARC8 rejects them outright.

What each format rejects

A record is rejected on encode when a format cannot represent it, rather than producing corrupt output:

  • iso2709: a value, indicator or subfield code containing a reserved delimiter byte (0x1d/0x1e/0x1f); a field over 9999 bytes or a record over 99999.
  • marcxml: any character XML 1.0 forbids (control characters such as NUL).
  • mrk: a line break in any datum, or $ used as a subfield code.
  • marcjson: nothing — JSON can represent every character.

Compression

Compression composes through io; it is not built in. Wrap the stream:

gz, _ := gzip.NewReader(f)              // compress/gzip, stdlib
for rec, err := range iso2709.NewReader(gz).All() { … }

Adding your own format

Implement codex.RecordReader and/or codex.RecordWriter over *codex.Record in your own package — no changes to this module required. Your type then works with codex.Convert, codex.All, and everything else built on the interfaces. The four bundled codecs are the reference implementations.

Tolerance of malformed input

  • Decoders never panic on arbitrary bytes (verified by sustained fuzzing of every format).
  • iso2709.Reader.Read returns a non-EOF error for a malformed record but advances past it using the declared length, so reading can continue. Directory entries pointing outside the field data are skipped rather than failing the record.
  • The text readers skip blank/separator lines and unknown elements/keys.
  • ReadFile stops at the first malformed record and returns the records parsed so far together with the error.

License

MIT — see LICENSE. Copyright Queer Liberation Library.

Documentation

Overview

Package codex is a format-agnostic in-memory model for MARC 21 bibliographic records, shared by the serialization subpackages.

A Record is a 24-byte Leader plus an ordered list of Fields. A control field (tag below "010") holds raw data; a data field carries two indicators and zero or more Subfields. Every MARC serialization maps onto this one model, so the same record round-trips through any format.

The model is domain-agnostic: it exposes leaders, fields, subfields and indicators, and leaves interpretation of specific tags to the caller.

Serialization lives in subpackages, each implementing RecordReader and RecordWriter:

  • iso2709 — the binary ISO 2709 interchange format (.mrc)
  • marcxml — the Library of Congress MARCXML slim schema (planned)
  • marcjson — the MARC-in-JSON structure (planned)
  • mrk — the MARCMaker mnemonic text format (planned)

All values exposed by this package are UTF-8 Go strings; each codec is responsible for transcoding its wire encoding to and from UTF-8.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func All

func All(r RecordReader) iter.Seq2[*Record, error]

All returns an iterator over the records produced by r, for use as

for rec, err := range codex.All(reader) { ... }

It stops at the first error, yielding (nil, err) once; io.EOF ends iteration cleanly without yielding. It works with any RecordReader, so every format shares one iterator.

func Convert

func Convert(r RecordReader, w RecordWriter) error

Convert reads every record from r and writes it to w, returning the first error encountered (io.EOF is the normal end and is not returned). Because it is written against the interfaces, it converts between any two formats. A writer that buffers a wrapper (e.g. a marcxml <collection> or a marcjson array) still needs the caller to call its Close method afterward to finalize the output.

Example

ExampleConvert converts a binary ISO 2709 record to MARCMaker text using only the format-agnostic RecordReader and RecordWriter interfaces.

rec := codex.NewRecord().
	AddField(codex.NewControlField("001", "ex-1")).
	AddField(codex.NewDataField("245", '0', '0', codex.NewSubfield('a', "Example record")))

var binary bytes.Buffer
if err := iso2709.NewWriter(&binary).Write(rec); err != nil {
	log.Fatal(err)
}

if err := codex.Convert(iso2709.NewReader(&binary), mrk.NewWriter(os.Stdout)); err != nil {
	log.Fatal(err)
}
Output:
=LDR  00074nam a2200049   4500
=001  ex-1
=245  00$aExample record

Types

type Field

type Field struct {
	Tag       string
	Ind1      byte
	Ind2      byte
	Value     string
	Subfields []Subfield
}

Field is a single MARC field. A control field (Tag < "010") carries raw data in Value and has no indicators or subfields. A data field (Tag >= "010") carries two indicators and zero or more subfields. The conventional blank indicator is the ASCII space (' '); an unset (zero) data-field indicator is treated as a blank space when the field is serialized.

func NewControlField

func NewControlField(tag, value string) Field

NewControlField constructs a control field (e.g. 001, 003, 008) holding raw data. The tag is not validated here; values below "010" are treated as control fields by the reader and writer.

func NewDataField

func NewDataField(tag string, ind1, ind2 byte, subfields ...Subfield) Field

NewDataField constructs a data field with the given tag, two indicators and subfields. A blank indicator is conventionally the ASCII space (' ').

func (Field) Indicators

func (f Field) Indicators() (byte, byte)

Indicators returns the field's two indicator bytes. For control fields both are zero.

func (Field) IsControl

func (f Field) IsControl() bool

IsControl reports whether the field is a control field (tag below "010").

func (Field) Subfield

func (f Field) Subfield(code byte) (string, bool)

Subfield returns the value of the first subfield with the given code and reports whether one was found.

func (Field) SubfieldValue

func (f Field) SubfieldValue(code byte) string

SubfieldValue returns the value of the first subfield with the given code, or the empty string if none is present.

func (Field) SubfieldValues

func (f Field) SubfieldValues(code byte) []string

SubfieldValues returns the values of every subfield with the given code, in order.

type Leader

type Leader string

Leader is the 24-byte MARC record leader. Helper methods decode the fields this package needs; the raw bytes are available via String.

func (Leader) BaseAddress

func (l Leader) BaseAddress() int

BaseAddress returns the base address of data declared in leader bytes [12:17]. It returns 0 if those bytes are not a valid number.

func (Leader) BibLevel

func (l Leader) BibLevel() byte

BibLevel returns leader byte 7 (the bibliographic level: 'm' monograph, 's' serial, 'a' monographic component part, etc.), or 0 if the leader is malformed.

func (Leader) Encoding

func (l Leader) Encoding() byte

Encoding returns leader byte 9, the character coding scheme: 'a' for UTF-8 (Unicode) or blank for MARC-8.

func (Leader) IsUnicode

func (l Leader) IsUnicode() bool

IsUnicode reports whether the leader declares UTF-8 (Unicode) encoding.

func (Leader) RecordLength

func (l Leader) RecordLength() int

RecordLength returns the total record length declared in leader bytes [0:5]. It returns 0 if those bytes are not a valid number.

func (Leader) RecordStatus

func (l Leader) RecordStatus() byte

RecordStatus returns leader byte 5 (the record status), or 0 if the leader is malformed.

func (Leader) RecordType

func (l Leader) RecordType() byte

RecordType returns leader byte 6 (the type of record), or 0 if the leader is malformed.

func (Leader) String

func (l Leader) String() string

String returns the leader as a 24-byte string.

type Record

type Record struct {
	// contains filtered or unexported fields
}

Record is a parsed MARC record: its leader and ordered fields.

func NewRecord

func NewRecord() *Record

NewRecord creates an empty record with a syntactically valid default leader (UTF-8 encoding). Fields are added with AddField.

func NewRecordCap

func NewRecordCap(n int) *Record

NewRecordCap creates an empty record like NewRecord but with space preallocated for n fields. Codecs that know the field count up front use it to avoid reallocating the field slice while appending.

func (*Record) AddField

func (r *Record) AddField(f Field) *Record

AddField appends a field to the record and returns the record for chaining.

func (*Record) ControlField

func (r *Record) ControlField(tag string) string

ControlField returns the raw value of the first control field with the given tag, or the empty string if none is present.

func (*Record) DataField

func (r *Record) DataField(tag string) (Field, bool)

DataField returns the first data field with the given tag and reports whether one was found.

func (*Record) DataFields

func (r *Record) DataFields(tag string) []Field

DataFields returns every data field with the given tag, in order.

func (*Record) Encoding

func (r *Record) Encoding() byte

Encoding returns the record's declared character encoding (leader byte 9).

func (*Record) Fields

func (r *Record) Fields() []Field

Fields returns all fields in record order.

func (*Record) InsertField

func (r *Record) InsertField(f Field) *Record

InsertField inserts f keeping the record ordered by ascending tag: f is placed after any existing fields with a tag less than or equal to f's. It returns the record. Insert into an already tag-ordered record to keep it ordered.

func (*Record) Leader

func (r *Record) Leader() Leader

Leader returns the record's leader.

func (*Record) RemoveFields

func (r *Record) RemoveFields(tag string) *Record

RemoveFields removes every field with the given tag and returns the record.

func (*Record) ReplaceField

func (r *Record) ReplaceField(f Field) *Record

ReplaceField replaces the first field that shares f's tag with f, or appends f when no field has that tag. It returns the record.

func (*Record) SetLeader

func (r *Record) SetLeader(l Leader) *Record

SetLeader replaces the record's leader.

func (*Record) SubfieldValue

func (r *Record) SubfieldValue(tag string, code byte) string

SubfieldValue returns the value of the first subfield with the given code in the first data field with the given tag, or the empty string.

func (*Record) SubfieldValues

func (r *Record) SubfieldValues(tag string, code byte) []string

SubfieldValues returns the values of every subfield with the given code across all data fields with the given tag, in order.

func (*Record) Validate

func (r *Record) Validate() error

Validate reports the first structural problem with the record: a leader that is not 24 bytes, a field tag that is not 3 bytes, or a data field with no subfields. It returns nil when the record is structurally well-formed. It does not check tag semantics, indicator values, or character encoding.

type RecordReader

type RecordReader interface {
	Read() (*Record, error)
}

RecordReader reads MARC records one at a time from an underlying stream. Each format subpackage provides an implementation; Read returns io.EOF when the stream is exhausted.

type RecordWriter

type RecordWriter interface {
	Write(*Record) error
}

RecordWriter serializes MARC records to an underlying stream. Each format subpackage provides an implementation.

type Subfield

type Subfield struct {
	Code  byte
	Value string
}

Subfield is a single subfield of a MARC data field: a one-byte code and its UTF-8 value.

func NewSubfield

func NewSubfield(code byte, value string) Subfield

NewSubfield constructs a subfield with the given code and value.

Directories

Path Synopsis
Package bibframe converts MARC 21 records to BIBFRAME 2.0, the Library of Congress linked-data model that replaces the flat MARC record with an RDF graph of related resources: a bf:Work (the intellectual content) and a bf:Instance (a particular publication of it), linked by bf:instanceOf / bf:hasInstance.
Package bibframe converts MARC 21 records to BIBFRAME 2.0, the Library of Congress linked-data model that replaces the flat MARC record with an RDF graph of related resources: a bf:Work (the intellectual content) and a bf:Instance (a particular publication of it), linked by bf:instanceOf / bf:hasInstance.
Package citation converts MARC 21 records to the RIS and BibTeX citation formats used by reference managers (Zotero, EndNote, Mendeley) and LaTeX.
Package citation converts MARC 21 records to the RIS and BibTeX citation formats used by reference managers (Zotero, EndNote, Mendeley) and LaTeX.
Package dublincore converts MARC 21 records to Dublin Core (DCMI), the lowest-common-denominator metadata used by OAI-PMH and most repository software.
Package dublincore converts MARC 21 records to Dublin Core (DCMI), the lowest-common-denominator metadata used by OAI-PMH and most repository software.
internal
marc8
Package marc8 decodes MARC-8 byte sequences to UTF-8 for the common Western subset: Basic Latin (ASCII, the default G0 set) and ANSEL Extended Latin (the default G1 set), including combining diacritics.
Package marc8 decodes MARC-8 byte sequences to UTF-8 for the common Western subset: Basic Latin (ASCII, the default G0 set) and ANSEL Extended Latin (the default G1 set), including combining diacritics.
Package iso2709 reads and writes MARC 21 records in the binary ISO 2709 interchange format (.mrc), implementing codex.RecordReader and codex.RecordWriter.
Package iso2709 reads and writes MARC 21 records in the binary ISO 2709 interchange format (.mrc), implementing codex.RecordReader and codex.RecordWriter.
Package marcjson reads and writes MARC 21 records in the de-facto "MARC-in-JSON" structure (the pymarc/ruby-marc/marc4j layout), implementing codex.RecordReader and codex.RecordWriter using only encoding/json.
Package marcjson reads and writes MARC 21 records in the de-facto "MARC-in-JSON" structure (the pymarc/ruby-marc/marc4j layout), implementing codex.RecordReader and codex.RecordWriter using only encoding/json.
Package marcxml reads and writes MARC 21 records in the Library of Congress MARCXML "slim" serialization (namespace http://www.loc.gov/MARC21/slim), implementing codex.RecordReader and codex.RecordWriter using only encoding/xml.
Package marcxml reads and writes MARC 21 records in the Library of Congress MARCXML "slim" serialization (namespace http://www.loc.gov/MARC21/slim), implementing codex.RecordReader and codex.RecordWriter using only encoding/xml.
Package mods converts MARC 21 records to MODS (Metadata Object Description Schema), the Library of Congress XML standard that is richer than MARCXML and near-lossless from MARC.
Package mods converts MARC 21 records to MODS (Metadata Object Description Schema), the Library of Congress XML standard that is richer than MARCXML and near-lossless from MARC.
Package mrk reads and writes MARC 21 records in the MARCMaker / MARCBreaker mnemonic line format (".mrk"), implementing codex.RecordReader and codex.RecordWriter using only the standard library.
Package mrk reads and writes MARC 21 records in the MARCMaker / MARCBreaker mnemonic line format (".mrk"), implementing codex.RecordReader and codex.RecordWriter using only the standard library.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL