I'm relatively new to Rust, and running into an idiosyncracy with how I'd write a constructor in another language. I have a struct, S3Logger, which creates a temporary on disk file, and when a certain amount of data is written to this file will upload to S3 and rotate to another file.
I would like my new function to use a method on the class, gen_new_file, which will open a new file with the right name in the write location. However, in order to make it a method, I must already have a S3Logger to pass in, which I don't have yet during the new function. In the example below, I show how I might do this in another language, to partially construct the object before using object methods to finish the construction; however this obviously doesn't work in Rust.
I could use an Option for the current_log_file, but this feels a little gross. I'd like to enforce the invariant that if I have an S3Logger, I know it has an open file.
What is the best practice in Rust for such problems?
pub struct S3Logger {
local_folder: PathBuf,
destination_path: String,
max_log_size: usize,
current_log_file: File,
current_logged_data: usize
}
impl S3Logger {
pub fn new<P: AsRef<Path>>(
local_folder: P,
destination_path: &str,
max_log_size: usize
) -> Self {
std::fs::create_dir_all(&local_folder).unwrap();
let mut ret = Self {
local_folder: local_folder.as_ref().to_path_buf(),
destination_path: destination_path.to_string(),
max_log_size: max_log_size,
current_logged_data: 0
};
// fails ^^^^ missing `current_log_file
ret.gen_new_file();
return ret
}
fn gen_new_file(&mut self) -> () {
let time = Utc::now().to_rfc3339();
let file_name = format!("{}.log", time);
let file_path = self.local_folder.join(file_name);
self.current_log_file = File::create(&file_path).unwrap();
}
}
The easiest way would be making gen_new_file take local_folder: P instead of &mut self, return the File from there and call that in the constructor:
use std::fs::File;
use std::path::{Path, PathBuf};
use chrono::Utc;
pub struct S3Logger {
local_folder: PathBuf,
destination_path: String,
max_log_size: usize,
current_log_file: File,
current_logged_data: usize
}
impl S3Logger {
pub fn new<P: AsRef<Path>>(
local_folder: P,
destination_path: &str,
max_log_size: usize
) -> Self {
std::fs::create_dir_all(&local_folder).unwrap();
let current_log_file = Self::gen_new_file(&local_folder);
Self {
local_folder: local_folder.as_ref().to_path_buf(),
destination_path: destination_path.to_string(),
max_log_size: max_log_size,
current_logged_data: 0,
current_log_file,
}
}
fn gen_new_file<P: AsRef<Path>>(local_folder: P) -> File {
let time = Utc::now().to_rfc3339();
let file_name = format!("{}.log", time);
let file_path = local_folder.as_ref().join(file_name);
File::create(&file_path).unwrap()
}
}
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=ee447eabc799035e8db246d3bccb225b
If you need to replace current_log_file later, you can do it the same way:
fn replace_log_file<P: AsRef<Path>>(&mut self, new_path: P) {
let new_file = Self::gen_new_file(&new_path);
self.current_log_file = new_file;
}
One way, as you noted would be indeed to make the field optional.
You can also divide your struct into 2 parts.
struct S3LoggerInternal {
pub local_folder: PathBuf,
pub destination_path: String,
pub max_log_size: usize,
}
pub struct S3Logger {
internal: S3LoggerInternal,
current_log_file: File,
current_logged_data: usize,
}
impl S3Logger {
pub fn new(...) -> Result<Self, ...> {
let internal = S3LoggerInternal {...};
let file = internal.open_file()?;
Self {internal, current_log_file: file, current_logged_data: 0}
}
}
impl S3LoggerInternal {
fn open_file(&self) -> Result<File, ...> {
// ... The code in your gen_new_file function
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With