use crate::{HeaderField, HttpCertificationError, HttpCertificationResult};
use candid::{
types::{Serializer, Type, TypeInner},
CandidType, Deserialize,
};
pub use http::Method;
use http::Uri;
use serde::Deserializer;
use std::{borrow::Cow, str::FromStr};
#[derive(Debug, Clone, PartialEq, Eq)]
struct MethodWrapper(Method);
impl CandidType for MethodWrapper {
fn _ty() -> Type {
TypeInner::Text.into()
}
fn idl_serialize<S>(&self, serializer: S) -> Result<(), S::Error>
where
S: Serializer,
{
self.0.as_str().idl_serialize(serializer)
}
}
impl<'de> Deserialize<'de> for MethodWrapper {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
String::deserialize(deserializer).and_then(|method| {
Method::from_str(&method)
.map(Into::into)
.map_err(|_| serde::de::Error::custom("Invalid HTTP method"))
})
}
}
impl From<Method> for MethodWrapper {
fn from(method: Method) -> Self {
Self(method)
}
}
#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)]
pub struct HttpRequest<'a> {
method: MethodWrapper,
url: String,
headers: Vec<HeaderField>,
body: Cow<'a, [u8]>,
certificate_version: Option<u16>,
}
impl<'a> HttpRequest<'a> {
pub fn get(url: impl Into<String>) -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
.with_method(Method::GET)
.with_url(url)
}
pub fn post(url: impl Into<String>) -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
.with_method(Method::POST)
.with_url(url)
}
pub fn put(url: impl Into<String>) -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
.with_method(Method::PUT)
.with_url(url)
}
pub fn patch(url: impl Into<String>) -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
.with_method(Method::PATCH)
.with_url(url)
}
pub fn delete(url: impl Into<String>) -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
.with_method(Method::DELETE)
.with_url(url)
}
#[inline]
pub fn builder() -> HttpRequestBuilder<'a> {
HttpRequestBuilder::new()
}
#[inline]
pub fn method(&self) -> &Method {
&self.method.0
}
#[inline]
pub fn url(&self) -> &str {
&self.url
}
#[inline]
pub fn headers(&self) -> &[HeaderField] {
&self.headers
}
#[inline]
pub fn headers_mut(&mut self) -> &mut Vec<HeaderField> {
&mut self.headers
}
#[inline]
pub fn body(&self) -> &[u8] {
&self.body
}
#[inline]
pub fn certificate_version(&self) -> Option<u16> {
self.certificate_version
}
pub fn get_path(&self) -> HttpCertificationResult<String> {
let uri = self
.url
.parse::<Uri>()
.map_err(|_| HttpCertificationError::MalformedUrl(self.url.to_string()))?;
let decoded_path = urlencoding::decode(uri.path()).map(|path| path.into_owned())?;
Ok(decoded_path)
}
pub fn get_query(&self) -> HttpCertificationResult<Option<String>> {
self.url
.parse::<Uri>()
.map(|uri| uri.query().map(|uri| uri.to_owned()))
.map_err(|_| HttpCertificationError::MalformedUrl(self.url.to_string()))
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpRequestBuilder<'a> {
method: Option<MethodWrapper>,
url: Option<String>,
headers: Vec<HeaderField>,
body: Cow<'a, [u8]>,
certificate_version: Option<u16>,
}
impl<'a> HttpRequestBuilder<'a> {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn with_method(mut self, method: Method) -> Self {
self.method = Some(method.into());
self
}
#[inline]
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[inline]
pub fn with_headers(mut self, headers: Vec<HeaderField>) -> Self {
self.headers = headers;
self
}
#[inline]
pub fn with_body(mut self, body: impl Into<Cow<'a, [u8]>>) -> Self {
self.body = body.into();
self
}
#[inline]
pub fn with_certificate_version(mut self, certificate_version: u16) -> Self {
self.certificate_version = Some(certificate_version);
self
}
#[inline]
pub fn build(self) -> HttpRequest<'a> {
HttpRequest {
method: self.method.unwrap_or(Method::GET.into()),
url: self.url.unwrap_or("/".to_string()),
headers: self.headers,
body: self.body,
certificate_version: self.certificate_version,
}
}
#[inline]
pub fn build_update(self) -> HttpUpdateRequest<'a> {
HttpUpdateRequest {
method: self.method.unwrap_or(Method::GET.into()),
url: self.url.unwrap_or("/".to_string()),
headers: self.headers,
body: self.body,
}
}
}
#[derive(Clone, Debug, CandidType, Deserialize, PartialEq, Eq)]
pub struct HttpUpdateRequest<'a> {
method: MethodWrapper,
url: String,
headers: Vec<HeaderField>,
body: Cow<'a, [u8]>,
}
impl<'a> HttpUpdateRequest<'a> {
#[inline]
pub fn method(&self) -> &Method {
&self.method.0
}
#[inline]
pub fn url(&self) -> &str {
&self.url
}
#[inline]
pub fn headers(&self) -> &[HeaderField] {
&self.headers
}
#[inline]
pub fn body(&self) -> &[u8] {
&self.body
}
pub fn get_path(&self) -> HttpCertificationResult<String> {
let uri = self
.url
.parse::<Uri>()
.map_err(|_| HttpCertificationError::MalformedUrl(self.url.to_string()))?;
let decoded_path = urlencoding::decode(uri.path()).map(|path| path.into_owned())?;
Ok(decoded_path)
}
pub fn get_query(&self) -> HttpCertificationResult<Option<String>> {
self.url
.parse::<Uri>()
.map(|uri| uri.query().map(|uri| uri.to_owned()))
.map_err(|_| HttpCertificationError::MalformedUrl(self.url.to_string()))
}
}
impl<'a> From<HttpRequest<'a>> for HttpUpdateRequest<'a> {
fn from(req: HttpRequest<'a>) -> Self {
HttpUpdateRequest {
method: req.method,
url: req.url,
headers: req.headers,
body: req.body,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_get_uri() {
let req = HttpRequest::get("https://canister.com/sample-asset.txt").build();
let path = req.get_path().unwrap();
let query = req.get_query().unwrap();
assert_eq!(path, "/sample-asset.txt");
assert!(query.is_none());
}
#[test]
fn request_get_encoded_uri() {
let test_requests = [
(
HttpRequest::get("https://canister.com/%73ample-asset.txt").build(),
"/sample-asset.txt",
"",
),
(
HttpRequest::get("https://canister.com/path/123?foo=test%20component&bar=1").build(),
"/path/123",
"foo=test%20component&bar=1",
),
(
HttpRequest::get("https://canister.com/a%20file.txt").build(),
"/a file.txt",
"",
),
(
HttpRequest::get("https://canister.com/mujin0722/3888-zjfrd-tqaaa-aaaaf-qakia-cai/%E6%97%A0%E8%AE%BA%E7%BE%8E%E8%81%94%E5%82%A8%E6%98%AF%E5%90%A6%E5%8A%A0%E6%81%AFbtc%E4%BB%8D%E5%B0%86%E5%9B%9E%E5%88%B07%E4%B8%87%E5%88%80").build(),
"/mujin0722/3888-zjfrd-tqaaa-aaaaf-qakia-cai/无论美联储是否加息btc仍将回到7万刀",
"",
),
];
for (req, expected_path, expected_query) in test_requests.iter() {
let path = req.get_path().unwrap();
let query = req.get_query().unwrap();
assert_eq!(path, *expected_path);
assert_eq!(query.unwrap_or_default(), *expected_query);
}
}
}