use std::sync::Arc;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::fetch::{FetchOptions, Page, fetch_blocking};
use crate::visibility::VisibilityPolicy;
use crate::{ScreenshotOptions, client};
#[derive(Debug, Clone)]
pub struct Client {
inner: Arc<client::ClientInner>,
}
impl Default for Client {
fn default() -> Self {
Self::new()
}
}
impl Client {
#[must_use]
pub fn new() -> Self {
Self::builder().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn fetch(&self, url: &str) -> Result<Page> {
fetch_blocking(&self.inner.options(url))
}
pub fn fetch_with(&self, opts: &FetchOptions) -> Result<Page> {
fetch_blocking(&self.inner.apply_defaults(opts.clone()))
}
pub fn markdown(&self, url: &str) -> Result<String> {
self.fetch(url)?.markdown_with_url(url)
}
pub fn text(&self, url: &str) -> Result<String> {
Ok(self.fetch(url)?.inner_text)
}
pub fn extract_json(&self, url: &str) -> Result<String> {
self.fetch(url)?.extract_json_with_url(url)
}
pub fn screenshot(&self, url: &str, opts: &ScreenshotOptions) -> Result<Vec<u8>> {
let fopts = self.inner.apply_defaults(FetchOptions::screenshot(url, opts.full_page));
let page = fetch_blocking(&fopts)?;
page.screenshot_png()
.map(<[u8]>::to_vec)
.ok_or_else(|| Error::screenshot(anyhow::anyhow!("screenshot returned no data"), Some(url.to_string())))
}
pub fn execute_js(&self, url: &str, expression: impl Into<String>) -> Result<String> {
let fopts = self.inner.apply_defaults(FetchOptions::javascript(url, expression));
let page = fetch_blocking(&fopts)?;
page.js_result
.ok_or_else(|| Error::javascript(anyhow::anyhow!("execute_js returned no result"), Some(url.to_string())))
}
}
#[must_use = "ClientBuilder does nothing until .build() is called"]
#[derive(Debug, Default)]
pub struct ClientBuilder {
inner: client::ClientBuilder,
}
impl ClientBuilder {
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn settle(mut self, settle: Duration) -> Self {
self.inner = self.inner.settle(settle);
self
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.inner = self.inner.user_agent(ua);
self
}
pub fn visibility(mut self, policy: VisibilityPolicy) -> Self {
self.inner = self.inner.visibility(policy);
self
}
#[must_use]
pub fn build(self) -> Client {
Client {
inner: Arc::new(self.inner.build_inner()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_default_uses_30s_timeout() {
assert_eq!(Client::default().inner.timeout, Duration::from_secs(30));
}
#[test]
fn client_builder_sets_timeout() {
let client = Client::builder().timeout(Duration::from_secs(60)).build();
assert_eq!(client.inner.timeout, Duration::from_secs(60));
}
#[test]
fn client_builder_sets_settle() {
let client = Client::builder().settle(Duration::from_millis(500)).build();
assert_eq!(client.inner.settle, Duration::from_millis(500));
}
#[test]
fn client_builder_sets_visibility() {
let client = Client::builder().visibility(VisibilityPolicy::off()).build();
assert_eq!(client.inner.visibility, VisibilityPolicy::off());
}
#[test]
fn client_builder_sanitizes_user_agent() {
let client = Client::builder().user_agent("Bot\r\nX-Evil: yes").build();
assert_eq!(client.inner.user_agent.as_deref(), Some("Bot X-Evil: yes"));
}
#[test]
fn client_clone_shares_inner() {
let client = Client::new();
assert!(Arc::ptr_eq(&client.inner, &client.clone().inner));
}
#[test]
fn assert_send_sync() {
fn check<T: Send + Sync>() {}
check::<Client>();
check::<ClientBuilder>();
}
#[test]
fn fetch_invalid_url_returns_invalid_url_error() {
let client = Client::new();
let err = client.fetch("not a url").unwrap_err();
assert!(matches!(err, Error::InvalidUrl { .. }));
}
#[test]
fn fetch_private_address_is_rejected() {
let client = Client::new();
let err = client.fetch("http://127.0.0.1/").unwrap_err();
assert!(err.is_network(), "got: {err:?}");
}
}