我正在使用 Rust 开发一个代码库,我打算通过功能标志提供同步和异步版本,因为我认为这是确保整个生态系统保持一致的最佳方式。这样我就不必为了从异步代码转换为同步代码而进行奇怪的恶作剧,但是这样做非常乏味,并且更改代码中的小事情变得越来越困难。我如何减少这里重复代码的数量?
有很多这样的函数,唯一的区别是与
async
标志异步的函数随后被 .await -ed
#[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)
}
为了减少随时在两个位置对代码的一部分进行任何更改的时间,必须有一种我缺少的简单而干净的方法来执行此操作
我尝试使用
duplicate
板条箱并编写宏规则!宏,但这两种解决方案都不容易使用,因为没有简单的方法来确定类型是否可供等待
示例:
#[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()
});
我还能怎样解决这个问题?
这是一个已知问题。您可以在这篇 Inside Rust 博客文章中阅读相关内容:宣布关键字泛型计划。那篇文章提到了 maybe-async,你可能想尝试一下,但我还没有检查过。
由于大部分功能都是同步的,因此您可以将其移至其自己的功能以减少重复。
#[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
Self::new_from_env_internal(realm_id, environment, discovery_doc)
}
#[cfg(not(feature = "async"))]
pub fn new_from_env(realm_id: &str, environment: Environment) -> Result<Self, AuthError> {
let discovery_doc = Self::get_discovery_doc(&environment)?; // No await
Self::new_from_env_internal(realm_id, environment, discovery_doc)
}
fn new_from_env_internal(
realm_id: &str,
environment: Environment,
discovery_doc: DiscoveryDoc,
) -> Result<Self, AuthError> {
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,
},
})
}
在整个代码库中保持这一点可能很容易,也可能不容易,但泛型和重组可以有所帮助。
请注意,不建议使用这样的功能,因为 Cargo 打算将功能添加到。如果其他人尝试同时依赖您的 crate 的同步和异步部分,他们会发现同步 API 不可用,并且没有干净的方法来获取这两者。通常,这是通过将同步和异步部分分离到不同的模块中来解决的,例如在 reqwest 中,但这也会创建额外的包装函数和命名空间组织。另一种解决方案是为异步和同步函数指定不同的名称,但这通常仅在小范围内使用,例如 Tokio 中的
recv
和 blocking_recv
。