use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue};
use reqwest::{Method, StatusCode};
use serde::Serialize;
use serde::de::DeserializeOwned;
use url::Url;
use crate::http::constants::{APPLICATION_FORM, APPLICATION_JSON, EMPTY_JSON_OBJECT};
use crate::http::error::{Error, Result, body_preview};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Site {
#[default]
Portal,
Rates,
}
#[derive(Debug, Clone, bon::Builder)]
#[builder(derive(Debug, Clone))]
pub struct Request {
#[builder(start_fn)]
pub(crate) method: Method,
#[builder(start_fn, into)]
pub(crate) path: String,
#[builder(field)]
pub(crate) headers: HeaderMap,
#[builder(field)]
pub(crate) query: Option<String>,
#[builder(field)]
pub(crate) body: Option<Vec<u8>>,
#[builder(field)]
pub(crate) site: Site,
#[builder(field)]
pub(crate) unwrap_d: bool,
}
impl Request {
pub fn get(path: impl Into<String>) -> RequestBuilder {
Self::builder(Method::GET, path)
}
pub fn post(path: impl Into<String>) -> RequestBuilder {
Self::builder(Method::POST, path)
}
pub fn put(path: impl Into<String>) -> RequestBuilder {
Self::builder(Method::PUT, path)
}
pub fn patch(path: impl Into<String>) -> RequestBuilder {
Self::builder(Method::PATCH, path)
}
pub fn delete(path: impl Into<String>) -> RequestBuilder {
Self::builder(Method::DELETE, path)
}
pub fn page_method(method: Method, path: impl Into<String>) -> RequestBuilder {
let body = (method != Method::GET).then(|| EMPTY_JSON_OBJECT.to_vec());
let mut builder = Self::builder(method, path);
builder.body = body;
builder
.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON));
builder.unwrap_d = true;
builder
}
pub fn method(&self) -> &Method {
&self.method
}
pub fn path(&self) -> &str {
&self.path
}
pub fn site(&self) -> Site {
self.site
}
pub(crate) fn url(&self, base: &Url) -> Result<Url> {
let mut url = base.join(self.path.trim_start_matches('/'))?;
if let Some(query) = &self.query {
url.set_query(Some(query));
}
Ok(url)
}
}
impl<S: request_builder::State> RequestBuilder<S> {
pub fn query<Q: Serialize + ?Sized>(mut self, query: &Q) -> Result<Self> {
let encoded = serde_urlencoded::to_string(query)?;
self.query = (!encoded.is_empty()).then_some(encoded);
Ok(self)
}
pub fn json<B: Serialize + ?Sized>(mut self, body: &B) -> Result<Self> {
self.body = Some(serde_json::to_vec(body).map_err(Error::Serialize)?);
self.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_JSON));
Ok(self)
}
pub fn form<B: Serialize + ?Sized>(mut self, body: &B) -> Result<Self> {
self.body = Some(serde_urlencoded::to_string(body)?.into_bytes());
self.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(APPLICATION_FORM));
Ok(self)
}
pub fn site(mut self, site: Site) -> Self {
self.site = site;
self
}
pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.headers.insert(name, value);
self
}
}
pub trait Endpoint {
type Response: DeserializeOwned;
fn request(&self) -> Result<Request>;
}
#[derive(serde::Deserialize)]
struct Wrapped<T> {
d: T,
}
pub(crate) fn decode<T: DeserializeOwned>(
status: StatusCode,
bytes: &[u8],
unwrap_d: bool,
) -> Result<T> {
if !status.is_success() {
return Err(Error::Status {
status,
body: body_preview(bytes),
});
}
let json: &[u8] = if bytes.is_empty() { b"null" } else { bytes };
let decoded = if unwrap_d {
serde_json::from_slice::<Wrapped<T>>(json).map(|w| w.d)
} else {
serde_json::from_slice(json)
};
decoded.map_err(|source| Error::Deserialize {
status,
body: body_preview(bytes),
source,
})
}
pub(crate) fn text(status: StatusCode, bytes: &[u8]) -> Result<String> {
if !status.is_success() {
return Err(Error::Status {
status,
body: body_preview(bytes),
});
}
Ok(String::from_utf8_lossy(bytes).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Url {
crate::http::ClientConfig::builder()
.base_url("https://api.example.com/v1")
.unwrap()
.build()
.base_url()
.clone()
}
#[test]
fn url_keeps_base_path_even_with_leading_slash() {
let url = Request::get("/users/42").build().url(&base()).unwrap();
assert_eq!(url.as_str(), "https://api.example.com/v1/users/42");
}
#[test]
fn url_encodes_query() {
let url = Request::get("search")
.query(&[("q", "a b&c"), ("page", "2")])
.unwrap()
.build()
.url(&base())
.unwrap();
assert_eq!(
url.as_str(),
"https://api.example.com/v1/search?q=a+b%26c&page=2"
);
}
#[test]
fn decode_empty_body_as_unit_and_none() {
decode::<()>(StatusCode::NO_CONTENT, b"", false).unwrap();
assert_eq!(
decode::<Option<u32>>(StatusCode::OK, b"", false).unwrap(),
None
);
}
#[test]
fn decode_non_success_is_status_error_with_body() {
let err = decode::<()>(StatusCode::NOT_FOUND, b"{\"error\":\"nope\"}", true).unwrap_err();
assert!(
matches!(&err, Error::Status { status, body } if *status == StatusCode::NOT_FOUND && body.contains("nope"))
);
assert_eq!(err.status(), Some(StatusCode::NOT_FOUND));
}
#[test]
fn decode_invalid_json_is_deserialize_error() {
let err = decode::<u32>(StatusCode::OK, b"not json", false).unwrap_err();
assert!(matches!(err, Error::Deserialize { .. }));
}
#[test]
fn decode_unwraps_d_envelope() {
assert_eq!(
decode::<Vec<u32>>(StatusCode::OK, b"{\"d\":[1,2]}", true).unwrap(),
[1, 2]
);
decode::<()>(StatusCode::OK, b"{\"d\":null}", true).unwrap();
}
#[test]
fn decode_missing_d_envelope_is_deserialize_error() {
let err = decode::<Vec<u32>>(StatusCode::OK, b"[1,2]", true).unwrap_err();
assert!(matches!(err, Error::Deserialize { .. }));
}
}