Trait and impl generics

Hello :wave:

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.

An associated type maybe? Seems to capture the fact better that Storage<T> only ever implements Storer for T and never for another generic type U.

use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Serialize, Deserialize)]
struct QueueItem;

#[derive(Serialize, Deserialize)]
struct ClientItem;

pub trait Storer: Send + Sync {
    type Item: Serialize + DeserializeOwned;

    fn get(&self, user_id: &str, id: &str) -> Option<&Self::Item>;
}

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 for Storage<QueueItem> {
    type Item = QueueItem;

    fn get(&self, user_id: &str, id: &str) -> Option<&Self::Item> {
        None
    }
}

impl Storer for Storage<ClientItem> {
    type Item = ClientItem;

    fn get(&self, user_id: &str, id: &str) -> Option<&Self::Item> {
        None
    }
}

Playground.

1 Like

Thank you. That does work for the storage, and the DeserializeOwned is a helpful tip.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.