screenshotfreeapi 1.0.0

Official Rust client for ScreenshotFreeAPI — Screenshot-as-a-Service
Documentation
use std::sync::Arc;
use std::time::{Duration, Instant};

use tokio::time::sleep;

use crate::error::{Result, ScreenshotFreeAPIError};
use crate::http::HttpClient;
use crate::types::{
    EnqueueResponse, HtmlScreenshotOptions, JobResult, JobStatusResponse, MobileScreenshotOptions,
    WebScreenshotOptions,
};

// ---------------------------------------------------------------------------
// WaitOptions
// ---------------------------------------------------------------------------

/// Controls the polling behaviour of `capture_*_and_wait` methods.
///
/// # Example
///
/// ```rust
/// use screenshotfreeapi::WaitOptions;
///
/// let opts = WaitOptions {
///     interval_ms: 1_500,
///     timeout_ms: 60_000,
///     on_progress: Some(Box::new(|s| {
///         println!("Job {} — {}%", s.job_id, s.progress.unwrap_or(0));
///     })),
/// };
/// ```
pub struct WaitOptions {
    /// How long to sleep between status polls, in milliseconds. Default: 2 000.
    pub interval_ms: u64,
    /// Maximum total time to wait before returning [`ScreenshotFreeAPIError::JobTimeout`], in milliseconds. Default: 120 000.
    pub timeout_ms: u64,
    /// Optional callback invoked after each status poll. Useful for progress indicators.
    pub on_progress: Option<Box<dyn Fn(&JobStatusResponse) + Send>>,
}

impl Default for WaitOptions {
    fn default() -> Self {
        Self { interval_ms: 2_000, timeout_ms: 120_000, on_progress: None }
    }
}

// ---------------------------------------------------------------------------
// ScreenshotsResource
// ---------------------------------------------------------------------------

/// Screenshot capture resource.
///
/// Use the `capture_*` methods to enqueue a job and receive a [`EnqueueResponse`]
/// immediately (non-blocking).  Use the `capture_*_and_wait` variants to
/// enqueue and poll until completion, returning the full [`JobResult`].
#[derive(Clone)]
pub struct ScreenshotsResource {
    pub(crate) http: Arc<HttpClient>,
}

impl ScreenshotsResource {
    /// Enqueue a web screenshot job.
    ///
    /// Returns immediately with a `202 Accepted` and a `job_id` to poll.
    /// Corresponds to `POST /screenshots/web`.
    pub async fn capture_web(&self, options: WebScreenshotOptions) -> Result<EnqueueResponse> {
        self.http.post("/screenshots/web", &options, None).await
    }

    /// Enqueue a mobile app screenshot job.
    ///
    /// Returns immediately with a `202 Accepted` and a `job_id` to poll.
    /// Corresponds to `POST /screenshots/mobile`.
    pub async fn capture_mobile(
        &self,
        options: MobileScreenshotOptions,
    ) -> Result<EnqueueResponse> {
        self.http.post("/screenshots/mobile", &options, None).await
    }

    /// Enqueue an HTML-render screenshot job.
    ///
    /// Returns immediately with a `202 Accepted` and a `job_id` to poll.
    /// Corresponds to `POST /screenshots/html`.
    pub async fn capture_html(&self, options: HtmlScreenshotOptions) -> Result<EnqueueResponse> {
        self.http.post("/screenshots/html", &options, None).await
    }

    // -----------------------------------------------------------------------
    // Blocking convenience wrappers
    // -----------------------------------------------------------------------

    /// Enqueue a web screenshot and poll until the job is complete.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// use screenshotfreeapi::{ScreenshotFreeAPIClient, WebScreenshotOptions, WaitOptions};
    ///
    /// #[tokio::main]
    /// async fn main() -> screenshotfreeapi::Result<()> {
    ///     let client = ScreenshotFreeAPIClient::new("sfa_your_api_key");
    ///     let result = client.screenshots.capture_web_and_wait(
    ///         WebScreenshotOptions { url: "https://stripe.com".into(), ..Default::default() },
    ///         WaitOptions::default(),
    ///     ).await?;
    ///     println!("{}", result.screenshots[0].url);
    ///     Ok(())
    /// }
    /// ```
    pub async fn capture_web_and_wait(
        &self,
        options: WebScreenshotOptions,
        wait_opts: WaitOptions,
    ) -> Result<JobResult> {
        let enq = self.capture_web(options).await?;
        wait_for_job(&self.http, &enq.job_id, wait_opts).await
    }

    /// Enqueue a mobile screenshot and poll until the job is complete.
    pub async fn capture_mobile_and_wait(
        &self,
        options: MobileScreenshotOptions,
        wait_opts: WaitOptions,
    ) -> Result<JobResult> {
        let enq = self.capture_mobile(options).await?;
        wait_for_job(&self.http, &enq.job_id, wait_opts).await
    }

    /// Enqueue an HTML screenshot and poll until the job is complete.
    pub async fn capture_html_and_wait(
        &self,
        options: HtmlScreenshotOptions,
        wait_opts: WaitOptions,
    ) -> Result<JobResult> {
        let enq = self.capture_html(options).await?;
        wait_for_job(&self.http, &enq.job_id, wait_opts).await
    }
}

// ---------------------------------------------------------------------------
// Shared polling helper
// ---------------------------------------------------------------------------

/// Poll `/jobs/:id/status` in a loop until the job reaches a terminal state.
///
/// This is used by all `capture_*_and_wait` methods and by
/// [`crate::client::ScreenshotFreeAPIClient::capture`].
pub(crate) async fn wait_for_job(
    http: &HttpClient,
    job_id: &str,
    opts: WaitOptions,
) -> Result<JobResult> {
    let deadline = Instant::now() + Duration::from_millis(opts.timeout_ms);

    loop {
        if Instant::now() >= deadline {
            return Err(ScreenshotFreeAPIError::JobTimeout { timeout_ms: opts.timeout_ms });
        }

        let status: JobStatusResponse =
            http.get(&format!("/jobs/{job_id}/status"), None).await?;

        if let Some(ref cb) = opts.on_progress {
            cb(&status);
        }

        match status.status.as_str() {
            "completed" => {
                return http.get(&format!("/jobs/{job_id}/result"), None).await;
            }
            "failed" => {
                return Err(ScreenshotFreeAPIError::JobFailed {
                    job_id: job_id.to_string(),
                    reason: status.error.unwrap_or_else(|| "unknown error".into()),
                });
            }
            // "queued" | "processing" | anything else — keep polling
            _ => {}
        }

        sleep(Duration::from_millis(opts.interval_ms)).await;
    }
}