Hello
I feel I have gotten myself confused going down the compiler driven development hole.
Problem
I am developing a Tauri application which has some state (QueueItem
and ClientItem
). These are passed around, and only have the crate requirement they implement the Storer
methods.
Tauri Background Info
In Tauri, app state is managed (attached) like so:
let client_storage: Storage<ClientItem> = Storage::new(app_data_dir.join("client.json"));
tauri_app.manage(client_storage);
let queue_storage: Storage<QueueItem> = Storage::new(app_data_dir.join("queue.json"));
tauri_app.manage(queue_storage);
Then, through macro-magic, the correct state can be retrieved by specifying the type:
#[tauri::command]
fn some_func(client_storage: tauri::state::State<Storage<ClientItem>>) {}
#[tauri::command]
fn other_func(queue_storage: tauri::state::State<Storage<QueueItem>>, client_storage: tauri::state::State<ClientItem>>) {}
Current Implementation
pub trait Storer<T: Serialize + for<'de> Deserialize<'de>>: Send + Sync {
fn get(&self, user_id: &str, id: &str) -> Option<&T>;
}
pub struct Storage<T> {
pub data_path: PathBuf,
phantom: std::marker::PhantomData<T>,
}
impl<T> Storage<T> {
pub fn new(data_path: PathBuf) -> Self {
Storage {
data_path,
phantom: std::marker::PhantomData,
}
}
}
impl Storer<QueueItem> for Storage<QueueItem> {}
impl Storer<ClientItem> for Storage<ClientItem> {}
It does not feel correct to specify the same generic in the trait definition as well as the struct.
Is there a better way of achieving the same thing?
Side Quest
My main reason for structuring the code this way is I wanted one interface for both development and production for another type, DataSource
. That is, I wanted the data source to use the local file system in development, and a remote database in production.
This was the first implementation of that:
if development_mode {
let data_source = FileSystemSource::new(app_data_dir.clone());
tauri_app.manage(data_source);
} else {
let data_source = DatabaseSource::new();
tauri_app.manage(data_source);
}
#[tauri::command]
fn fetch_from_datasource(
data_source: State<&dyn DataSource<User>>,
queue_storage: State<Storage<QueueItem>>,
) -> Result<Vec<QueueItem>, Error> {
let users = data_source.fetch()?;
let queue = get_queue(users);
queue_storage.write_all(queue.clone())?;
Ok(queue)
}
pub trait DataSource<T: Serialize + for<'de> Deserialize<'de>>: Send + Sync {
fn fetch(&self) -> Result<Vec<T>, std::io::Error>;
fn write(&self, data: Vec<T>) -> Result<(), std::io::Error>;
}
pub struct DatabaseSource {
client: Client,
}
impl DatabaseSource {
pub fn new() -> Self {
// Connect to database
let client = ...
DatabaseSource { client }
}
}
impl DataSource<User> for DatabaseSource {
fn fetch(&self) -> Result<Vec<User>, std::io::Error> {
todo!()
}
fn write(&self, data: Vec<User>) -> Result<(), std::io::Error> {
todo!()
}
}
pub struct FileSystemSource {
app_data_dir: PathBuf,
}
impl FileSystemSource {
pub fn new(app_data_dir: PathBuf) -> Self {
FileSystemSource { app_data_dir }
}
}
Something notable is there is no type error that DataSource
is NOT implemented for FileSystemSource
yet. I discovered this with the original Storer
logic, which is why it now uses the two generics approach.