orbis_plugin_api/sdk/
http.rs1use super::error::{Error, Result};
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24use std::collections::HashMap;
25
26#[derive(Debug, Clone, Copy, Serialize)]
28#[serde(rename_all = "UPPERCASE")]
29pub enum Method {
30 Get,
31 Post,
32 Put,
33 Patch,
34 Delete,
35 Head,
36 Options,
37}
38
39impl std::fmt::Display for Method {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Get => write!(f, "GET"),
43 Self::Post => write!(f, "POST"),
44 Self::Put => write!(f, "PUT"),
45 Self::Patch => write!(f, "PATCH"),
46 Self::Delete => write!(f, "DELETE"),
47 Self::Head => write!(f, "HEAD"),
48 Self::Options => write!(f, "OPTIONS"),
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55#[allow(dead_code)]
56pub struct Request {
57 method: Method,
58 url: String,
59 headers: HashMap<String, String>,
60 body: Option<Vec<u8>>,
61}
62
63impl Request {
64 fn new(method: Method, url: impl Into<String>) -> Self {
66 Self {
67 method,
68 url: url.into(),
69 headers: HashMap::new(),
70 body: None,
71 }
72 }
73
74 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
76 self.headers.insert(name.into(), value.into());
77 self
78 }
79
80 pub fn content_type(self, content_type: &str) -> Self {
82 self.header("Content-Type", content_type)
83 }
84
85 pub fn bearer_token(self, token: &str) -> Self {
87 self.header("Authorization", format!("Bearer {}", token))
88 }
89
90 pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self> {
92 let json = serde_json::to_vec(body)?;
93 self.body = Some(json);
94 Ok(self.content_type("application/json"))
95 }
96
97 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
99 self.body = Some(body.into());
100 self
101 }
102
103 pub fn form(mut self, data: &HashMap<String, String>) -> Self {
105 let encoded = data
106 .iter()
107 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
108 .collect::<Vec<_>>()
109 .join("&");
110 self.body = Some(encoded.into_bytes());
111 self.content_type("application/x-www-form-urlencoded")
112 }
113
114 #[cfg(target_arch = "wasm32")]
116 pub fn send(self) -> Result<Response> {
117 let method_str = self.method.to_string();
118 let headers_json = serde_json::to_vec(&self.headers)?;
119 let body = self.body.unwrap_or_default();
120
121 let result_ptr = unsafe {
122 super::ffi::http_request(
123 method_str.as_ptr() as i32,
124 method_str.len() as i32,
125 self.url.as_ptr() as i32,
126 self.url.len() as i32,
127 headers_json.as_ptr() as i32,
128 headers_json.len() as i32,
129 body.as_ptr() as i32,
130 body.len() as i32,
131 )
132 };
133
134 if result_ptr == 0 {
135 return Err(Error::http("HTTP request failed"));
136 }
137
138 let result_bytes = unsafe { super::ffi::read_length_prefixed(result_ptr) };
139 let response: Response = serde_json::from_slice(&result_bytes)?;
140
141 Ok(response)
142 }
143
144 #[cfg(not(target_arch = "wasm32"))]
146 pub fn send(self) -> Result<Response> {
147 Err(Error::http("HTTP not available outside WASM"))
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Response {
154 pub status: u16,
156
157 #[serde(default)]
159 pub headers: HashMap<String, String>,
160
161 #[serde(default)]
163 pub body: Vec<u8>,
164
165 #[serde(default)]
167 pub error: Option<String>,
168}
169
170impl Response {
171 #[inline]
173 pub const fn is_success(&self) -> bool {
174 self.status >= 200 && self.status < 300
175 }
176
177 #[inline]
179 pub fn is_error(&self) -> bool {
180 self.error.is_some() || self.status >= 400
181 }
182
183 pub fn text(&self) -> Result<String> {
185 String::from_utf8(self.body.clone()).map_err(Error::from)
186 }
187
188 pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
190 serde_json::from_slice(&self.body).map_err(Error::from)
191 }
192
193 pub fn header(&self, name: &str) -> Option<&str> {
195 let name_lower = name.to_lowercase();
196 self.headers
197 .iter()
198 .find(|(k, _)| k.to_lowercase() == name_lower)
199 .map(|(_, v)| v.as_str())
200 }
201
202 pub fn error_for_status(self) -> Result<Self> {
204 if self.is_success() {
205 Ok(self)
206 } else {
207 let msg = self.error.clone().unwrap_or_else(|| {
208 format!("HTTP {}: {}", self.status, self.text().unwrap_or_default())
209 });
210 Err(Error::http(msg))
211 }
212 }
213}
214
215#[inline]
217pub fn get(url: impl Into<String>) -> Request {
218 Request::new(Method::Get, url)
219}
220
221#[inline]
223pub fn post(url: impl Into<String>) -> Request {
224 Request::new(Method::Post, url)
225}
226
227#[inline]
229pub fn put(url: impl Into<String>) -> Request {
230 Request::new(Method::Put, url)
231}
232
233#[inline]
235pub fn patch(url: impl Into<String>) -> Request {
236 Request::new(Method::Patch, url)
237}
238
239#[inline]
241pub fn delete(url: impl Into<String>) -> Request {
242 Request::new(Method::Delete, url)
243}
244
245mod urlencoding {
247 pub fn encode(s: &str) -> String {
248 let mut result = String::with_capacity(s.len() * 3);
249 for c in s.chars() {
250 match c {
251 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
252 result.push(c);
253 }
254 ' ' => result.push('+'),
255 _ => {
256 for b in c.to_string().as_bytes() {
257 result.push_str(&format!("%{:02X}", b));
258 }
259 }
260 }
261 }
262 result
263 }
264}