wait_human/
client.rs

1use crate::error::{Result, WaitHumanError};
2use crate::types::*;
3use reqwest::Client;
4use std::time::Instant;
5use tokio::time::{sleep, Duration};
6
7const DEFAULT_ENDPOINT: &str = "https://api.waithuman.com";
8const POLL_INTERVAL_MS: u64 = 3000;
9
10/// Main WaitHuman client for making requests
11#[derive(Debug, Clone)]
12pub struct WaitHuman {
13    api_key: String,
14    endpoint: String,
15    client: Client,
16}
17
18impl WaitHuman {
19    /// Creates a new WaitHuman client from just an API key
20    ///
21    /// This is a convenience wrapper around `WaitHuman::new()` that uses the default endpoint.
22    ///
23    /// # Arguments
24    ///
25    /// * `api_key` - Your WaitHuman API key
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if the API key is empty
30    ///
31    /// # Example
32    ///
33    /// ```no_run
34    /// use wait_human::WaitHuman;
35    ///
36    /// let client = WaitHuman::new_from_key("your-api-key")?;
37    /// # Ok::<(), wait_human::WaitHumanError>(())
38    /// ```
39    pub fn new_from_key<S: Into<String>>(api_key: S) -> Result<Self> {
40        Self::new(WaitHumanConfig::new(api_key))
41    }
42
43    /// Creates a new WaitHuman client
44    ///
45    /// # Arguments
46    ///
47    /// * `config` - Configuration containing API key and optional endpoint
48    ///
49    /// # Errors
50    ///
51    /// Returns an error if the API key is empty
52    pub fn new(config: WaitHumanConfig) -> Result<Self> {
53        if config.api_key.is_empty() {
54            return Err(WaitHumanError::InvalidResponse(
55                "api_key is mandatory".to_string(),
56            ));
57        }
58
59        let mut endpoint = config
60            .endpoint
61            .unwrap_or_else(|| DEFAULT_ENDPOINT.to_string());
62
63        // Remove trailing slash
64        if endpoint.ends_with('/') {
65            endpoint.pop();
66        }
67
68        Ok(Self {
69            api_key: config.api_key,
70            endpoint,
71            client: Client::new(),
72        })
73    }
74
75    /// General method to ask a question and wait for an answer
76    ///
77    /// # Arguments
78    ///
79    /// * `question` - The confirmation question to ask
80    /// * `options` - Optional settings like timeout
81    ///
82    /// # Errors
83    ///
84    /// Returns an error if:
85    /// - The confirmation cannot be created
86    /// - Network errors occur
87    /// - The request times out
88    /// - Polling fails
89    pub async fn ask(
90        &self,
91        question: ConfirmationQuestion,
92        options: Option<AskOptions>,
93    ) -> Result<ConfirmationAnswerWithDate> {
94        let confirmation_id = self.create_confirmation(question).await?;
95        let timeout_seconds = options.and_then(|o| o.timeout_seconds);
96        self.poll_for_answer(confirmation_id, timeout_seconds).await
97    }
98
99    /// Convenience method for free-text questions
100    ///
101    /// # Arguments
102    ///
103    /// * `subject` - The question subject/title
104    /// * `body` - Optional detailed question body
105    /// * `options` - Optional settings like timeout
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if:
110    /// - The request fails or times out
111    /// - The answer type doesn't match (not free text)
112    pub async fn ask_free_text<S, B>(
113        &self,
114        subject: S,
115        body: Option<B>,
116        options: Option<AskOptions>,
117    ) -> Result<String>
118    where
119        S: Into<String>,
120        B: Into<String>,
121    {
122        let question = ConfirmationQuestion {
123            method: QuestionMethod::Push,
124            subject: subject.into(),
125            body: body.map(|b| b.into()),
126            answer_format: AnswerFormat::FreeText,
127        };
128
129        let answer = self.ask(question, options).await?;
130
131        match answer.answer.answer_content {
132            AnswerContent::FreeText { text } => Ok(text),
133            other => Err(WaitHumanError::UnexpectedAnswerType {
134                expected: "free_text".to_string(),
135                actual: format!("{:?}", other),
136            }),
137        }
138    }
139
140    /// Convenience method for multiple-choice questions (single selection)
141    ///
142    /// # Arguments
143    ///
144    /// * `subject` - The question subject/title
145    /// * `choices` - Available choices for the user to select from
146    /// * `body` - Optional detailed question body
147    /// * `options` - Optional settings like timeout
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if:
152    /// - The request fails or times out
153    /// - The answer type doesn't match (not options)
154    /// - The selected index is invalid
155    pub async fn ask_multiple_choice<S, B, C>(
156        &self,
157        subject: S,
158        choices: C,
159        body: Option<B>,
160        options: Option<AskOptions>,
161    ) -> Result<String>
162    where
163        S: Into<String>,
164        B: Into<String>,
165        C: IntoIterator,
166        C::Item: Into<String>,
167    {
168        let choices_vec: Vec<String> = choices.into_iter().map(|c| c.into()).collect();
169
170        let question = ConfirmationQuestion {
171            method: QuestionMethod::Push,
172            subject: subject.into(),
173            body: body.map(|b| b.into()),
174            answer_format: AnswerFormat::Options {
175                options: choices_vec.clone(),
176                multiple: false,
177            },
178        };
179
180        let answer = self.ask(question, options).await?;
181
182        match answer.answer.answer_content {
183            AnswerContent::Options { selected_indexes } => {
184                let index = selected_indexes.first().ok_or_else(|| {
185                    WaitHumanError::InvalidResponse("No selection received".to_string())
186                })?;
187
188                let index_usize = *index as usize;
189
190                choices_vec
191                    .get(index_usize)
192                    .cloned()
193                    .ok_or_else(|| WaitHumanError::InvalidSelectedIndex { index: *index })
194            }
195            other => Err(WaitHumanError::UnexpectedAnswerType {
196                expected: "options".to_string(),
197                actual: format!("{:?}", other),
198            }),
199        }
200    }
201
202    // Private helper methods
203
204    async fn create_confirmation(&self, question: ConfirmationQuestion) -> Result<String> {
205        let url = format!("{}/confirmations/create", self.endpoint);
206        let request_body = CreateConfirmationRequest { question };
207
208        let response = self
209            .client
210            .post(&url)
211            .header("Authorization", &self.api_key)
212            .json(&request_body)
213            .send()
214            .await?;
215
216        if !response.status().is_success() {
217            return Err(WaitHumanError::CreateFailed {
218                status_text: response.status().to_string(),
219            });
220        }
221
222        let data: CreateConfirmationResponse = response.json().await?;
223        Ok(data.confirmation_request_id)
224    }
225
226    async fn poll_for_answer(
227        &self,
228        confirmation_id: String,
229        timeout_seconds: Option<u64>,
230    ) -> Result<ConfirmationAnswerWithDate> {
231        let start = Instant::now();
232
233        loop {
234            let elapsed_seconds = start.elapsed().as_secs_f64();
235
236            if let Some(timeout) = timeout_seconds {
237                if elapsed_seconds > timeout as f64 {
238                    return Err(WaitHumanError::Timeout { elapsed_seconds });
239                }
240            }
241
242            let url = format!(
243                "{}/confirmations/get/{}?long_poll=false",
244                self.endpoint, confirmation_id
245            );
246
247            let response = self
248                .client
249                .get(&url)
250                .header("Authorization", &self.api_key)
251                .send()
252                .await?;
253
254            if !response.status().is_success() {
255                return Err(WaitHumanError::PollFailed {
256                    status_text: response.status().to_string(),
257                });
258            }
259
260            let data: GetConfirmationResponse = response.json().await?;
261
262            if let Some(answer) = data.maybe_answer {
263                return Ok(answer);
264            }
265
266            // Wait before next poll
267            sleep(Duration::from_millis(POLL_INTERVAL_MS)).await;
268        }
269    }
270}