zyte_api_rs/
lib.rs

1//! # zyte-api-rs
2//!
3//! This is an unofficial rust package for the Zyte API.
4use base64::{engine::general_purpose, Engine as _};
5use http::Method;
6use serde::{Deserialize, Serialize, Serializer};
7use std::error::Error;
8use std::str::FromStr;
9
10#[serde_with::skip_serializing_none]
11#[derive(Serialize, Debug, Default)]
12#[serde(rename_all = "camelCase")]
13pub struct Request {
14    #[serde(with = "http_serde::uri")]
15    url: http::Uri,
16    http_response_body: Option<bool>,
17    #[serde(serialize_with = "method_opt")]
18    http_request_method: Option<http::Method>,
19    #[serde(flatten)]
20    http_request_body_type: Option<HttpRequestBodyType>,
21}
22
23/// https://stackoverflow.com/a/76143471/196870
24fn method_opt<S: Serializer>(method_opt: &Option<http::Method>, ser: S) -> Result<S::Ok, S::Error> {
25    match method_opt {
26        Some(method) => http_serde::method::serialize(method, ser),
27        // This won't be encountered when using skip_serializing_none
28        None => ser.serialize_none(),
29    }
30}
31
32#[derive(Serialize, Debug)]
33#[serde(rename_all = "camelCase")]
34pub enum HttpRequestBodyType {
35    HttpRequestBody(String), // todo: body should be bytes
36    HttpRequestText(String),
37}
38
39#[allow(dead_code)]
40#[derive(Deserialize, Debug)]
41#[serde(rename_all = "camelCase")]
42#[serde(deny_unknown_fields)]
43pub struct Response {
44    #[serde(with = "http_serde::uri")]
45    pub url: http::Uri,
46    pub http_response_body: String,
47    #[serde(with = "http_serde::status_code")]
48    pub status_code: http::StatusCode,
49}
50
51pub struct RequestBuilder {
52    client: ZyteApi,
53    request: Request,
54}
55
56impl RequestBuilder {
57    pub fn new(client: ZyteApi, method: Method, url: http::Uri) -> RequestBuilder {
58        RequestBuilder {
59            client,
60            request: Request {
61                url,
62                http_response_body: Some(true),
63                http_request_method: Some(method),
64                ..Default::default()
65            },
66        }
67    }
68    pub fn body(mut self, body: &str) -> RequestBuilder {
69        // todo: body should be bytes
70        self.request.http_request_body_type =
71            Some(HttpRequestBodyType::HttpRequestBody(body.to_owned()));
72        self
73    }
74    pub fn text(mut self, text: &str) -> RequestBuilder {
75        self.request.http_request_body_type =
76            Some(HttpRequestBodyType::HttpRequestText(text.to_owned()));
77        self
78    }
79    pub async fn send(self) -> Result<Response, Box<dyn Error>> {
80        // Use Reqwest client to POST to Zyte API
81        let mut response = self
82            .client
83            .client
84            .post(&self.client.api_url)
85            .basic_auth(&self.client.api_key, Some(""))
86            .json(&self.request)
87            .send()
88            .await?
89            .json::<Response>()
90            .await?;
91
92        // decode http_response_body from base 64
93        let b = &general_purpose::STANDARD.decode(response.http_response_body)?;
94
95        // Convert (unicode) byte vec (Vec<u8>) to String, ignoring errors
96        let b = String::from_utf8_lossy(b);
97
98        // Convert to real String and insert back into Response
99        response.http_response_body = b.to_string(); // TODO: do this with serde
100
101        Ok(response)
102    }
103}
104
105#[derive(Clone)]
106pub struct ZyteApi {
107    client: reqwest::Client,
108    api_key: String,
109    api_url: String,
110}
111impl ZyteApi {
112    pub fn new(api_key: &str) -> ZyteApi {
113        ZyteApi {
114            client: reqwest::Client::new(),
115            api_key: api_key.to_string(),
116            api_url: "https://api.zyte.com/v1/extract".to_string(),
117        }
118    }
119    pub async fn get(&self, url: &str) -> Result<Response, Box<dyn Error>> {
120        let url = http::Uri::from_str(url)?;
121        Ok(RequestBuilder::new(self.clone(), Method::GET, url)
122            .send()
123            .await?)
124    }
125    pub fn post(&self, url: &str) -> Result<RequestBuilder, Box<dyn Error>> {
126        let url = http::Uri::from_str(url)?;
127        Ok(RequestBuilder::new(self.clone(), Method::POST, url))
128    }
129}