I'm working on a pet project involving syncing steam and notion data, partly to improve my understanding of Rust, but I've been a bit lazy about integration testing it and I'm trying to get it into a state where I can properly integration test it.
I have a Sync
struct which does a load of scraping from both steam games and notion notes and updates a database. This is achieved with owned Repo
, SteamClient
and NotionGamesRepo
properties respectively:
pub struct Sync {
steam_account_id: String,
pub repo: Repo,
steam: SteamClient,
notion: NotionGamesRepo,
}
and this has been working fine.
I have SteamClient
broken into traits already, so to properly test the sync process I want Sync
to take a reference to the trait instead, so I can sub in a mock. Following advice on a stack overflow thread I changed it to this, which also worked fine:
pub struct Sync<'s> {
steam_account_id: String,
pub repo: Repo,
steam: &'s dyn SteamHandling,
notion: NotionGamesRepo
}
impl<'s> Sync<'s> {
...
}
I'm fairly new to Rust so my understanding of lifecycle parameters probably isn't up to scratch, but my understanding is 's
is specifying that the &'s dyn SteamHandling
borrow must live at least as long as my Sync<'s>
.
So far, so good; I treated NotionGamesRepo
the same way, creating a trait for it:
pub struct Sync<'s, 'n> {
steam_account_id: String,
pub repo: Repo,
steam: &'s dyn SteamHandling,
notion: &'n dyn NotionHandling
}
impl<'s, 'n> Sync<'s, 'n> {
...
}
Now it all falls apart and my understanding of lifecycles and compiler queries fails me; I can't make much sense out of the compiler error:
error[E0391]: cycle detected when checking effective visibilities
|
note: ...which requires computing type of `cli::sync::<impl at src/cli/sync.rs:99:1: 99:13>::run::{opaque#0}`...
--> src/cli/sync.rs:101:5
|
101 | pub(super) async fn run(&self) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...which requires computing type of opaque `cli::sync::<impl at src/cli/sync.rs:99:1: 99:13>::run::{opaque#0}`...
--> src/cli/sync.rs:101:5
|
101 | pub(super) async fn run(&self) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...which requires type-checking `cli::sync::<impl at src/cli/sync.rs:99:1: 99:13>::run`...
--> src/cli/sync.rs:101:5
|
101 | pub(super) async fn run(&self) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: ...which again requires checking effective visibilities, completing the cycle
note: cycle used when checking that `db::sync::Sync` is well-formed
--> src/db/sync.rs:30:1
|
30 | pub struct Sync<'s, 'n> {
| ^^^^^^^^^^^^^^^^^^^^^^^
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
For more information about this error, try `rustc --explain E0391`.
I can see it's struggling to prove that the type for Sync
makes sense during compilation, and I had a quick read of the link it provides, but it doesn't really give me much clue how to properly achieve what I'm attempting here. I also tried using a single lifecycle parameter 'a
instead of two distinct ones, but got an identical error that way too.
I've also pushed this draft to a branch: rusteam/src/db/sync.rs at it-test2 · giftig/rusteam · GitHub
I initially made everything owned as it was simpler, and the advice I saw is that a struct should own its members unless there's a good reason not to, and lifecycle rules are well understood: clearly this supports that idea. But taking a trait, it can't own it as it's abstract. I also considered making the struct generic with two type parameters, instead of using borrows, but that seems like it'd get quite complex as well. I also need to reuse the Repo
later so I've made it public on the struct so it can be reused from there -- which seems like code smell and implying it should also be borrowed.
So, two questions:
(1) what's causing a cycle here exactly and can it be avoided with a simple change?
(2) Is this the right way to go about allowing DI for testing? Is there a simpler approach which will avoid falling afoul of lifecycle complexity?
Thanks in advance.