orbis_plugin_api/sdk/
http.rs

1//! HTTP client for making external requests.
2//!
3//! Allows plugins to make HTTP requests to external APIs.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use orbis_plugin_api::sdk::http;
9//!
10//! // Simple GET request
11//! let response = http::get("https://api.example.com/data")?;
12//!
13//! // POST with JSON body
14//! let response = http::post("https://api.example.com/users")
15//!     .json(&CreateUser { name: "John" })?
16//!     .send()?;
17//!
18//! // Parse response as JSON
19//! let user: User = response.json()?;
20//! ```
21
22use super::error::{Error, Result};
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24use std::collections::HashMap;
25
26/// HTTP method
27#[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/// HTTP request builder
54#[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    /// Create a new request
65    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    /// Add a header
75    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    /// Set Content-Type header
81    pub fn content_type(self, content_type: &str) -> Self {
82        self.header("Content-Type", content_type)
83    }
84
85    /// Set Authorization header with Bearer token
86    pub fn bearer_token(self, token: &str) -> Self {
87        self.header("Authorization", format!("Bearer {}", token))
88    }
89
90    /// Set a JSON body
91    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    /// Set a raw body
98    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
99        self.body = Some(body.into());
100        self
101    }
102
103    /// Set form data body
104    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    /// Send the request
115    #[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    /// Send the request (non-WASM stub)
145    #[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/// HTTP response
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct Response {
154    /// HTTP status code
155    pub status: u16,
156
157    /// Response headers
158    #[serde(default)]
159    pub headers: HashMap<String, String>,
160
161    /// Response body (as bytes, base64 encoded in JSON)
162    #[serde(default)]
163    pub body: Vec<u8>,
164
165    /// Error message (if any)
166    #[serde(default)]
167    pub error: Option<String>,
168}
169
170impl Response {
171    /// Check if the response was successful (2xx status)
172    #[inline]
173    pub const fn is_success(&self) -> bool {
174        self.status >= 200 && self.status < 300
175    }
176
177    /// Check if there was an error
178    #[inline]
179    pub fn is_error(&self) -> bool {
180        self.error.is_some() || self.status >= 400
181    }
182
183    /// Get the response body as a string
184    pub fn text(&self) -> Result<String> {
185        String::from_utf8(self.body.clone()).map_err(Error::from)
186    }
187
188    /// Parse the response body as JSON
189    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
190        serde_json::from_slice(&self.body).map_err(Error::from)
191    }
192
193    /// Get a header value (case-insensitive)
194    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    /// Ensure the response was successful, or return an error
203    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/// Create a GET request
216#[inline]
217pub fn get(url: impl Into<String>) -> Request {
218    Request::new(Method::Get, url)
219}
220
221/// Create a POST request
222#[inline]
223pub fn post(url: impl Into<String>) -> Request {
224    Request::new(Method::Post, url)
225}
226
227/// Create a PUT request
228#[inline]
229pub fn put(url: impl Into<String>) -> Request {
230    Request::new(Method::Put, url)
231}
232
233/// Create a PATCH request
234#[inline]
235pub fn patch(url: impl Into<String>) -> Request {
236    Request::new(Method::Patch, url)
237}
238
239/// Create a DELETE request
240#[inline]
241pub fn delete(url: impl Into<String>) -> Request {
242    Request::new(Method::Delete, url)
243}
244
245/// Simple URL encoding module
246mod 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}