orbis_plugin_api/sdk/
response.rs

1//! Response builder for plugin handlers.
2
3use super::error::{Error, Result};
4use serde::Serialize;
5use std::collections::HashMap;
6
7/// HTTP response returned by plugin handlers.
8#[derive(Debug, Clone, Serialize)]
9pub struct Response {
10    /// HTTP status code
11    pub status: u16,
12
13    /// Response headers
14    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
15    pub headers: HashMap<String, String>,
16
17    /// Response body
18    pub body: serde_json::Value,
19}
20
21impl Response {
22    /// Create a new response with status and body
23    #[inline]
24    pub fn new(status: u16, body: serde_json::Value) -> Self {
25        Self {
26            status,
27            headers: HashMap::new(),
28            body,
29        }
30    }
31
32    /// Create a 200 OK response with JSON body
33    #[inline]
34    pub fn json<T: Serialize>(data: &T) -> Result<Self> {
35        let body = serde_json::to_value(data)?;
36        Ok(Self::new(200, body))
37    }
38
39    /// Create a 200 OK response with raw JSON value
40    #[inline]
41    pub fn ok(body: serde_json::Value) -> Self {
42        Self {
43            status: 200,
44            headers: HashMap::new(),
45            body,
46        }
47    }
48
49    /// Create a 201 Created response
50    #[inline]
51    pub fn created<T: Serialize>(data: &T) -> Result<Self> {
52        let body = serde_json::to_value(data)?;
53        Ok(Self::new(201, body))
54    }
55
56    /// Create a 204 No Content response
57    #[inline]
58    pub fn no_content() -> Self {
59        Self {
60            status: 204,
61            headers: HashMap::new(),
62            body: serde_json::Value::Null,
63        }
64    }
65
66    /// Create an error response
67    #[inline]
68    pub fn error(status: u16, message: &str) -> Self {
69        Self::new(
70            status,
71            serde_json::json!({
72                "error": true,
73                "message": message
74            }),
75        )
76    }
77
78    /// Create a 400 Bad Request response
79    #[inline]
80    pub fn bad_request(message: &str) -> Self {
81        Self::error(400, message)
82    }
83
84    /// Create a 401 Unauthorized response
85    #[inline]
86    pub fn unauthorized(message: &str) -> Self {
87        Self::error(401, message)
88    }
89
90    /// Create a 403 Forbidden response
91    #[inline]
92    pub fn forbidden(message: &str) -> Self {
93        Self::error(403, message)
94    }
95
96    /// Create a 404 Not Found response
97    #[inline]
98    pub fn not_found(message: &str) -> Self {
99        Self::error(404, message)
100    }
101
102    /// Create a 500 Internal Server Error response
103    #[inline]
104    pub fn internal_error(message: &str) -> Self {
105        Self::error(500, message)
106    }
107
108    /// Create a response from an SDK Error
109    #[inline]
110    pub fn from_error(err: &Error) -> Self {
111        Self::error(err.status_code(), &err.to_string())
112    }
113
114    /// Add a header to the response
115    #[inline]
116    pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
117        self.headers.insert(name.into(), value.into());
118        self
119    }
120
121    /// Set Content-Type header
122    #[inline]
123    pub fn content_type(self, content_type: &str) -> Self {
124        self.with_header("Content-Type", content_type)
125    }
126
127    /// Set Cache-Control header
128    #[inline]
129    pub fn cache_control(self, value: &str) -> Self {
130        self.with_header("Cache-Control", value)
131    }
132
133    /// Set no-cache headers
134    #[inline]
135    pub fn no_cache(self) -> Self {
136        self.cache_control("no-store, no-cache, must-revalidate")
137    }
138
139    /// Serialize response to raw FFI pointer for returning to host
140    #[cfg(target_arch = "wasm32")]
141    pub fn to_raw(&self) -> Result<i32> {
142        let json = serde_json::to_vec(self)?;
143        Ok(super::ffi::return_bytes(&json))
144    }
145
146    /// Serialize response (non-WASM stub)
147    #[cfg(not(target_arch = "wasm32"))]
148    pub fn to_raw(&self) -> Result<i32> {
149        Err(Error::internal("to_raw only available in WASM"))
150    }
151}
152
153impl From<Error> for Response {
154    fn from(err: Error) -> Self {
155        Self::from_error(&err)
156    }
157}
158
159/// Builder for paginated responses
160#[derive(Debug, Clone, Serialize)]
161pub struct PaginatedResponse<T> {
162    /// The data items
163    pub data: Vec<T>,
164
165    /// Pagination metadata
166    pub pagination: PaginationMeta,
167}
168
169/// Pagination metadata
170#[derive(Debug, Clone, Serialize)]
171pub struct PaginationMeta {
172    /// Current page (1-indexed)
173    pub page: u32,
174
175    /// Items per page
176    pub per_page: u32,
177
178    /// Total number of items
179    pub total: u64,
180
181    /// Total number of pages
182    pub total_pages: u32,
183
184    /// Whether there is a next page
185    pub has_next: bool,
186
187    /// Whether there is a previous page
188    pub has_prev: bool,
189}
190
191impl<T: Serialize> PaginatedResponse<T> {
192    /// Create a new paginated response
193    pub fn new(data: Vec<T>, page: u32, per_page: u32, total: u64) -> Self {
194        let total_pages = ((total as f64) / (per_page as f64)).ceil() as u32;
195        Self {
196            data,
197            pagination: PaginationMeta {
198                page,
199                per_page,
200                total,
201                total_pages,
202                has_next: page < total_pages,
203                has_prev: page > 1,
204            },
205        }
206    }
207
208    /// Convert to Response
209    pub fn into_response(self) -> Result<Response> {
210        Response::json(&self)
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_response_json() {
220        let data = serde_json::json!({"name": "Test"});
221        let resp = Response::json(&data).unwrap();
222
223        assert_eq!(resp.status, 200);
224        assert_eq!(resp.body["name"], "Test");
225    }
226
227    #[test]
228    fn test_response_error() {
229        let resp = Response::not_found("User not found");
230
231        assert_eq!(resp.status, 404);
232        assert_eq!(resp.body["error"], true);
233        assert_eq!(resp.body["message"], "User not found");
234    }
235
236    #[test]
237    fn test_paginated_response() {
238        let items = vec![1, 2, 3];
239        let paginated = PaginatedResponse::new(items, 2, 10, 35);
240
241        assert_eq!(paginated.pagination.page, 2);
242        assert_eq!(paginated.pagination.total_pages, 4);
243        assert!(paginated.pagination.has_next);
244        assert!(paginated.pagination.has_prev);
245    }
246}