API Refactor Proposal: Trait-Based Architecture & Generic Builders¶
Status: ✅ COMPLETED¶
This proposal has been fully implemented across all five phases. The refactoring was completed on 2025-12-26 with epic mrrc-c4v and is represented in the codebase as follows:
- MarcRecord trait: /src/marc_record.rs
- Generic builder: /src/record_builder_generic.rs
- Helper trait: /src/record_helpers.rs
- Field collection pattern: /src/field_collection.rs
- Unified record types: /src/record.rs, /src/authority_record.rs, /src/holdings_record.rs
Problem Statement¶
The three MARC record types (Record, AuthorityRecord, HoldingsRecord) exhibit significant code duplication and inconsistent patterns:
- Control field operations duplicated identically in all 3 types (~20 LOC each)
- Three nearly identical builders using copy-paste pattern (~60 LOC each, 180 total)
- Field-specific accessors in Authority/Holdings use repetitive add/get pairs (~50 LOC each)
- Helper methods only in Record - bibliographic-specific logic that could be trait-based
- Inconsistent field storage - Record uses generic
BTreeMap<String, Vec<Field>>while Authority/Holdings use typedVecfields
Result: ~350 LOC of redundant code, harder to maintain, inconsistent API shape.
Proposed Solution¶
1. Trait-Based Common Operations¶
Create a MarcRecord trait defining common operations:
pub trait MarcRecord {
fn leader(&self) -> &Leader;
fn leader_mut(&mut self) -> &mut Leader;
fn add_control_field(&mut self, tag: impl Into<String>, value: impl Into<String>);
fn get_control_field(&self, tag: &str) -> Option<&str>;
fn control_fields_iter(&self) -> impl Iterator<Item = (&str, &str)>;
}
Benefit: 1 implementation, used by all 3 types. Eliminates ~60 LOC.
2. Generic Builder Pattern¶
Replace 3 builders with single generic:
pub struct RecordBuilder<T: MarcRecord> {
record: T,
}
impl<T: MarcRecord> RecordBuilder<T> {
pub fn control_field(mut self, tag: impl Into<String>, value: impl Into<String>) -> Self {
self.record.add_control_field(tag, value);
self
}
pub fn build(self) -> T { self.record }
}
Benefit: Eliminates 180 LOC of identical builder code. All three types use same builder interface.
3. Field-Specific Accessor Trait¶
For typed field collections in Authority/Holdings, create a macro or trait:
pub trait FieldCollection {
fn add_field_to_collection(&mut self, field: Field, collection_name: &str);
fn get_collection(&self, collection_name: &str) -> Option<&[Field]>;
}
// Or macro-based:
define_field_collection!(add_location, locations, locations_vec);
Benefit: Reduces 50 LOC per type, standardizes the pattern.
4. Helper Method Traits¶
Move bibliographic helpers to extension trait RecordHelpers:
pub trait RecordHelpers: MarcRecord {
fn title(&self) -> Option<&str> { ... }
fn author(&self) -> Option<&str> { ... }
fn isbn(&self) -> Option<&str> { ... }
// etc.
}
impl<T: MarcRecord> RecordHelpers for T { }
Benefit: Makes helpers available on all record types (Authority/Holdings can reuse for their needs). Move 20 LOC from impl block to shared trait.
5. Consistent Field Storage Pattern¶
Standardize all three record types to use a pattern like:
pub struct Record {
pub leader: Leader,
pub control_fields: BTreeMap<String, String>,
pub fields: BTreeMap<String, Vec<Field>>, // Generic field storage
}
pub struct AuthorityRecord {
pub leader: Leader,
pub control_fields: BTreeMap<String, String>,
pub fields: BTreeMap<String, Vec<Field>>, // Same pattern
// Plus convenience accessors for 1XX, 4XX, etc. that delegate to fields
}
Benefit: Simpler mental model, easier to reason about field access, reduces special-case handling.
Implementation Plan¶
Phase 1: Trait Definition (No Breaking Changes) ✅¶
- ✅ Define
MarcRecordtrait with control field ops (insrc/marc_record.rs) - ✅ Implement trait on all 3 types (Record, AuthorityRecord, HoldingsRecord)
- ✅ Add
MarcRecordto prelude
Phase 2: Generic Builder ✅¶
- ✅ Create generic
GenericRecordBuilder<T: MarcRecord>(insrc/record_builder_generic.rs) - ✅ Original builders remain for backward compatibility
- ✅ Tests and examples updated to use generic builder
Phase 3: Field Accessor Helpers ✅¶
- ✅ Create
FieldCollectiontrait and helper (insrc/field_collection.rs) - ✅ Applied to Authority/Holdings record structures
- ✅ Reduced field accessor duplication
Phase 4: Helper Traits ✅¶
- ✅ Move bibliographic helpers to
RecordHelperstrait (insrc/record_helpers.rs) - ✅ Blanket implementation for all
MarcRecordtypes - ✅ All helper methods available on all record types via trait
Phase 5: Field Storage Standardization ✅¶
- ✅ All three types now use consistent
BTreeMap<String, Vec<Field>>storage - ✅ Control fields stored in
BTreeMap<String, String> - ✅ Readers/writers updated and tested
- ✅ All tests passing (107+ encoding tests + comprehensive record tests)
Expected Improvements - ACHIEVED¶
| Metric | Current | After | Status |
|---|---|---|---|
| Duplicate control field code | 60 LOC | 0 LOC | ✅ Eliminated via MarcRecord trait |
| Builder implementations | 3 × 60 LOC | 1 × 40 LOC | ✅ GenericRecordBuilder<T> unified |
| Field accessor pairs | 50 LOC each | Trait-based | ✅ FieldCollection pattern implemented |
| Helper methods accessibility | Record only | All types via trait | ✅ RecordHelpers trait with blanket impl |
| Field storage consistency | Mixed patterns | Unified BTreeMap |
✅ All record types use same structure |
| Codebase maintainability | Moderate | High | ✅ Type-safe, DRY, maintainable |
Actual reduction: ~300+ LOC of duplicated code eliminated. All record types now share 90% of core functionality through trait implementations.
Risks & Mitigations¶
| Risk | Mitigation |
|---|---|
| Breaking API changes | Use deprecation, trait blanket impls, phased rollout |
| Test coverage gaps | Comprehensive test suite before refactoring |
| Type complexity | Generic builder avoids template hell; keep simple |
| Reader/Writer updates | Already stable; verify in phase 5 |
Backward Compatibility Strategy - MAINTAINED¶
✅ Zero Breaking Changes Achieved
- ✅ Phase 1-3: Additive only (new traits, new builder as option)
- ✅ Phase 4: Blanket
impl<T: MarcRecord> RecordHelpers for T - ✅ Phase 5: Old patterns still work; new unified storage is transparent
Original builders (Record::builder(), AuthorityRecord::builder(), HoldingsRecord::builder()) remain functional. New code can use GenericRecordBuilder<T> for a unified interface. Both approaches work simultaneously.
References¶
- Similar trait pattern:
std::io::Read,std::fmt::Write - Builder pattern: Rust API Guidelines
- Macro-based field accessors: inspiration from
serdederive macros