I have a codebase that I am working on in Rust and I intend to have sync and async versions available through a feature flag, as I thought that was the best way to make sure the whole ecosystem remained consistent. That way I don't have to do weird shenanigans in order to convert from async to sync code, however doing this has been incredibly tedious and it is getting more difficult to change small things in the code. How would I reduce the amount of duplicated code here?
There are many functions like this, the only difference being that the functions that are async from the async
flag are being .await -ed afterwards
#[cfg(feature = "async")]
pub async fn new_from_env(realm_id: &str, environment: Environment) -> Result<Self, AuthError> {
let discovery_doc = Self::get_discovery_doc(&environment).await?; // Only difference
let client_id = ClientId::new(std::env::var("INTUIT_CLIENT_ID")?);
let client_secret = ClientSecret::new(std::env::var("INTUIT_CLIENT_SECRET")?);
let redirect_uri = RedirectUrl::new(std::env::var("INTUIT_REDIRECT_URI")?)?;
log::info!("Got Discovery Doc and Intuit Credentials Successfully");
Ok(Self {
redirect_uri,
realm_id: realm_id.to_string(),
environment,
data: Unauthorized {
client_id,
client_secret,
discovery_doc,
},
})
}
#[cfg(not(feature = "async"))]
pub async fn new_from_env(realm_id: &str, environment: Environment) -> Result<Self, AuthError> {
let discovery_doc = Self::get_discovery_doc(&environment)?; // No await
let client_id = ClientId::new(std::env::var("INTUIT_CLIENT_ID")?);
let client_secret = ClientSecret::new(std::env::var("INTUIT_CLIENT_SECRET")?);
let redirect_uri = RedirectUrl::new(std::env::var("INTUIT_REDIRECT_URI")?)?;
log::info!("Got Discovery Doc and Intuit Credentials Successfully");
Ok(Self {
redirect_uri,
realm_id: realm_id.to_string(),
environment,
data: Unauthorized {
client_id,
client_secret,
discovery_doc,
},
})
}
#[cfg(feature = "async")]
async fn default_grab_token_session(
client_ref: &BasicClient,
scopes: Option<&[IntuitScope]>,
) -> Result<TokenSession, AuthError> {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_state) = Self::get_auth_url(client_ref, pkce_challenge, scopes);
let listener = TcpListener::bind("127.0.0.1:3320")
.await // Here
.expect("Error starting localhost callback listener! (async)");
open::that_detached(auth_url.as_str())?;
log::info!("Opened Auth URL: {}", auth_url);
Self::handle_oauth_callback(client_ref, listener, csrf_state, pkce_verifier).await
}
#[cfg(not(feature = "async"))]
fn default_grab_token_session(
client_ref: &BasicClient,
scopes: Option<&[IntuitScope]>,
) -> Result<TokenSession, AuthError> {
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
let (auth_url, csrf_state) = Self::get_auth_url(client_ref, pkce_challenge, scopes);
let listener = TcpListener::bind("127.0.0.1:3320")
.expect("Error starting localhost callback listener!"); // Not here
open::that_detached(auth_url.as_str())?;
log::info!("Opened Auth URL: {}", auth_url);
Self::handle_oauth_callback(client_ref, listener, csrf_state, pkce_verifier)
}
In order to reduce the amount of time just doing any changes to one part of the code in two locations any time, there must be a simple and clean way to do this that I'm missing
I tried using the duplicate
crates and writing a macro_rules! macro for it, but neither solution was simple to use because there's not simple way of determining whether or not a type is available to be awaited
example:
#[cfg(feature = "async")]
use tokio::fs;
#[cfg(not(feature = "async"))]
use std::fs;
macro_rules! cfg_async {
($func_name:ident ($($args:ident: $state_ty:ty),*) -> $output:ident { $body:expr } ) => {
#[cfg(feature = "async")]
async fn $func_name($($args: $state_ty),*) -> $output {
// If object can be awaited, await it, otherwise stay the same
$body
}
#[cfg(not(feature = "async"))]
fn $func_name($($args: $state_ty),*) -> $output {
// All the functions are synchronous, nothing needs to be awaited
$body
}
};
}
cfg_async!(foo (path: &str) -> String {
fs::read_to_string(path).unwrap()
// In async should be fs::read_to_string("foo.txt").await.unwrap()
});
How else can I go about this?