I'm building a stream-based file parser. The file has the format $TYPE$CONTENT\n
where TYPE
is 4 bytes and content
up to \n.
$TYPE
is either an attribute of the current record, or indicates that a new record starts. For example:
strtPerson
nameMustermann
surnMax
strtPerson
nameMustermann
surnRenate
My approach was to open the file, get an iterator over the lines, split the lines and then feed the lines to a parser:
use std::io::{self, BufRead, BufReader};
pub trait Record {
fn add_line(&mut self, type_: String, content: String) -> &mut dyn Record;
}
pub struct File {
pub current_record: Option<Box<dyn Record>>,
pub records: Vec<Box<dyn Record>>,
}
impl File {
pub fn push_last_record(&mut self) {
if self.current_record.is_some() {
self.records.push(self.current_record.take().unwrap());
}
}
}
impl Record for File {
fn add_line(&mut self, type_: String, content: String) -> &mut dyn Record {
match &type_[..] {
"strt" => {
self.push_last_record();
self.current_record = Some(Box::new(File {
current_record: None,
records: Vec::new(),
}));
}
_ => {
if let Some(current) = &mut self.current_record {
current.add_line(type_, content);
}
}
}
self
}
}
fn main() -> io::Result<()> {
let f =
"strtPerson\nnameMustermann\nsurnMax\nstrtPerson\nnameMustermann\nsurnRenate".as_bytes();
let reader = BufReader::new(f);
let mut f = File {
current_record: None,
records: Vec::new(),
};
for line in reader.lines() {
let line = line.unwrap();
f.add_line(line[..4].to_owned(), line[5..].to_owned());
}
println!("there are {} records read", f.records.len());
f.push_last_record();
println!("there are now {} records read", f.records.len());
Ok(())
}
This solution works, but I think it's cumbersome and error prone to require a call to push_last_record
and I think there could be a more idiomatic solution to this.
My main issue is how to create the new Record
if the line starts with "start" and somehow save a mutable reference to that so I can directly call the add_line
function on the latest record.
It boils down to creating a Box
, putting it in the vector and save a mutable reference to it in the struct.
Is there a ready-made pattern for this or could anyone give me a hint on how to make this clean?
EDIT:
Thanks to the hint with last_mut()
, I rewrote my code. I'm not sure if it's idiomatic or efficient, but I think it's way cleaner than before.
use std::io::{self, BufRead, BufReader};
pub trait Record {
fn add_line(&mut self, type_: String, content: String) -> &mut dyn Record;
}
pub struct File<'a> {
pub current_record: Option<&'a mut Box<dyn Record>>,
pub records: Vec<Box<dyn Record>>,
}
impl Record for File<'_> {
fn add_line(&mut self, type_: String, content: String) -> &mut dyn Record {
match &type_[..] {
"strt" => {
self.records.push(Box::new(File {
current_record: None,
records: Vec::new(),
}));
}
_ => {
if let Some(current) = &mut self.records.last_mut() {
current.add_line(type_, content);
}
}
}
self
}
}
fn main() -> io::Result<()> {
let f =
"strtPerson\nnameMustermann\nsurnMax\nstrtPerson\nnameMustermann\nsurnRenate".as_bytes();
let reader = BufReader::new(f);
let mut f = File {
current_record: None,
records: Vec::new(),
};
for line in reader.lines() {
let line = line.unwrap();
f.add_line(line[..4].to_owned(), line[5..].to_owned());
}
println!("there are {} records read", f.records.len());
Ok(())
}