如何减少仅通过 .await -ing 异步函数而有所不同的重复代码量?

问题描述 投票:0回答:1

我正在使用 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()
});

我还能怎样解决这个问题?

asynchronous rust configuration code-generation
1个回答
0
投票

这是一个已知问题。您可以在这篇 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

© www.soinside.com 2019 - 2024. All rights reserved.