Skip to main content

uv_auth/
prompt.rs

1use std::io;
2use std::sync::Arc;
3
4use thiserror::Error;
5use tokio::sync::OnceCell;
6use tokio::task::JoinError;
7
8use crate::Credentials;
9
10/// A failure to read credentials or run the credential prompt.
11#[derive(Debug, Error)]
12pub enum CredentialsPromptError {
13    #[error("Failed to read username")]
14    ReadUsername(#[source] io::Error),
15    #[error("Failed to read password")]
16    ReadPassword(#[source] io::Error),
17    #[error("Credential prompt task failed")]
18    Task(#[from] JoinError),
19}
20
21/// An optional prompt for credentials after other authentication sources are exhausted.
22///
23/// Use this for a client dedicated to a single destination. Clones share the response so retries
24/// reuse the entered credentials, even if the first request fails.
25#[derive(Debug, Clone)]
26pub struct CredentialsPrompt {
27    callback: fn() -> Result<Option<Credentials>, CredentialsPromptError>,
28    credentials: Arc<OnceCell<Option<Credentials>>>,
29}
30
31impl CredentialsPrompt {
32    /// Create a prompt whose callback returns `None` when interactive input is unavailable.
33    pub fn new(callback: fn() -> Result<Option<Credentials>, CredentialsPromptError>) -> Self {
34        Self {
35            callback,
36            credentials: Arc::new(OnceCell::new()),
37        }
38    }
39
40    /// Return credentials already entered through this prompt, without prompting.
41    pub fn credentials(&self) -> Option<&Credentials> {
42        self.credentials.get().and_then(Option::as_ref)
43    }
44
45    /// Return whether a prompt was attempted but interactive input was unavailable.
46    pub fn was_unavailable(&self) -> bool {
47        self.credentials.get().is_some_and(Option::is_none)
48    }
49
50    /// Prompt on a blocking thread, retaining the response for subsequent requests.
51    pub async fn prompt(&self) -> Result<Option<Credentials>, CredentialsPromptError> {
52        self.credentials
53            .get_or_try_init(|| async { tokio::task::spawn_blocking(self.callback).await? })
54            .await
55            .cloned()
56    }
57}