balius_sdk/
http.rs

1use std::time::Duration;
2
3use crate::wit::balius::app::http as wit;
4use serde::{Deserialize, Serialize};
5use url::Url;
6pub use wit::ErrorCode as HttpError;
7
8#[derive(Clone)]
9pub struct HttpRequest {
10    pub method: wit::Method,
11    pub url: Url,
12    pub headers: Vec<(wit::FieldName, wit::FieldValue)>,
13    pub body: Option<Vec<u8>>,
14    pub timeout: Option<Duration>,
15}
16
17macro_rules! helper_method {
18    ($name:ident, $method:expr) => {
19        pub fn $name(url: Url) -> Self {
20            Self::new($method, url)
21        }
22    };
23}
24
25impl HttpRequest {
26    pub fn new(method: wit::Method, url: Url) -> Self {
27        Self {
28            method,
29            url,
30            headers: vec![],
31            body: None,
32            timeout: None,
33        }
34    }
35
36    pub fn header(mut self, name: impl AsRef<str>, value: impl AsHeader) -> Self {
37        self.headers
38            .push((name.as_ref().to_string(), value.into_bytes()));
39        self
40    }
41
42    pub fn json<T: Serialize>(self, body: &T) -> Result<Self, HttpError> {
43        let body_bytes = serde_json::to_vec(body)
44            .map_err(|e| HttpError::InternalError(Some(format!("Invalid JSON: {e}"))))?;
45        Ok(Self {
46            body: Some(body_bytes),
47            ..self.header("content-type", "application/json")
48        })
49    }
50
51    pub fn send(self) -> Result<HttpResponse, HttpError> {
52        let scheme = match self.url.scheme() {
53            "http" => Some(wit::Scheme::Http),
54            "https" => Some(wit::Scheme::Https),
55            "" => None,
56            other => Some(wit::Scheme::Other(other.to_string())),
57        };
58        let authority = match self.url.authority() {
59            "" => None,
60            auth => Some(auth.to_string()),
61        };
62        let path_and_query = match (self.url.path(), self.url.query()) {
63            ("", None) => None,
64            (path, None) => Some(path.to_string()),
65            (path, Some(query)) => Some(format!("{path}?{query}")),
66        };
67        let request = wit::OutgoingRequest {
68            scheme,
69            authority,
70            path_and_query,
71            method: self.method,
72            headers: self.headers,
73            body: self.body,
74        };
75        let options = self.timeout.map(|t| wit::RequestOptions {
76            connect_timeout: None,
77            first_byte_timeout: None,
78            between_bytes_timeout: Some(t.as_nanos() as u64),
79        });
80        let response = wit::request(&request, options)?;
81        Ok(HttpResponse {
82            status: response.status,
83            headers: response.headers,
84            body: response.body,
85        })
86    }
87
88    helper_method!(get, wit::Method::Get);
89    helper_method!(put, wit::Method::Put);
90    helper_method!(post, wit::Method::Post);
91    helper_method!(patch, wit::Method::Patch);
92    helper_method!(delete, wit::Method::Delete);
93}
94
95pub struct HttpResponse {
96    pub status: u16,
97    pub headers: Vec<(wit::FieldName, wit::FieldValue)>,
98    pub body: Vec<u8>,
99}
100
101impl HttpResponse {
102    pub fn is_ok(&self) -> bool {
103        self.status >= 200 && self.status < 400
104    }
105
106    pub fn json<'a, T: Deserialize<'a>>(&'a self) -> Result<T, HttpError> {
107        serde_json::from_slice(&self.body)
108            .map_err(|e| HttpError::InternalError(Some(format!("Invalid JSON: {e}"))))?
109    }
110}
111
112pub trait AsHeader {
113    fn into_bytes(self) -> Vec<u8>;
114}
115
116impl AsHeader for String {
117    fn into_bytes(self) -> Vec<u8> {
118        self.into_bytes()
119    }
120}
121
122impl AsHeader for &str {
123    fn into_bytes(self) -> Vec<u8> {
124        self.as_bytes().to_vec()
125    }
126}
127
128impl AsHeader for Vec<u8> {
129    fn into_bytes(self) -> Vec<u8> {
130        self
131    }
132}
133
134impl AsHeader for &[u8] {
135    fn into_bytes(self) -> Vec<u8> {
136        self.to_vec()
137    }
138}