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#[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#[derive(Debug, Clone)]
26pub struct CredentialsPrompt {
27 callback: fn() -> Result<Option<Credentials>, CredentialsPromptError>,
28 credentials: Arc<OnceCell<Option<Credentials>>>,
29}
30
31impl CredentialsPrompt {
32 pub fn new(callback: fn() -> Result<Option<Credentials>, CredentialsPromptError>) -> Self {
34 Self {
35 callback,
36 credentials: Arc::new(OnceCell::new()),
37 }
38 }
39
40 pub fn credentials(&self) -> Option<&Credentials> {
42 self.credentials.get().and_then(Option::as_ref)
43 }
44
45 pub fn was_unavailable(&self) -> bool {
47 self.credentials.get().is_some_and(Option::is_none)
48 }
49
50 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}