orbis_plugin_api/sdk/
context.rs1use super::error::{Error, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Context {
12 pub method: String,
14
15 pub path: String,
17
18 #[serde(default)]
20 pub params: HashMap<String, String>,
21
22 #[serde(default)]
24 pub headers: HashMap<String, String>,
25
26 #[serde(default)]
28 pub query: HashMap<String, String>,
29
30 #[serde(default)]
32 pub body: serde_json::Value,
33
34 #[serde(default)]
36 pub user_id: Option<String>,
37
38 #[serde(default)]
40 pub is_admin: bool,
41
42 #[serde(default)]
44 pub request_id: Option<String>,
45}
46
47impl Context {
48 #[cfg(target_arch = "wasm32")]
53 pub fn from_raw(ptr: i32, len: i32) -> Result<Self> {
54 let bytes = unsafe {
55 std::slice::from_raw_parts(ptr as *const u8, len as usize)
56 };
57 serde_json::from_slice(bytes).map_err(Error::from)
58 }
59
60 #[cfg(not(target_arch = "wasm32"))]
62 pub fn from_raw(_ptr: i32, _len: i32) -> Result<Self> {
63 Err(Error::internal("from_raw only available in WASM"))
64 }
65
66 #[inline]
68 pub fn param(&self, name: &str) -> Option<&str> {
69 self.params.get(name).map(String::as_str)
70 }
71
72 #[inline]
74 pub fn param_required(&self, name: &str) -> Result<&str> {
75 self.params
76 .get(name)
77 .map(String::as_str)
78 .ok_or_else(|| Error::invalid_input(format!("Missing path parameter: {}", name)))
79 }
80
81 #[inline]
83 pub fn query_param(&self, name: &str) -> Option<&str> {
84 self.query.get(name).map(String::as_str)
85 }
86
87 #[inline]
89 pub fn query_param_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
90 self.query.get(name).map(String::as_str).unwrap_or(default)
91 }
92
93 pub fn query_param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
95 self.query.get(name).and_then(|v| v.parse().ok())
96 }
97
98 #[inline]
100 pub fn header(&self, name: &str) -> Option<&str> {
101 let name_lower = name.to_lowercase();
102 self.headers
103 .iter()
104 .find(|(k, _)| k.to_lowercase() == name_lower)
105 .map(|(_, v)| v.as_str())
106 }
107
108 #[inline]
110 pub fn body_as<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
111 serde_json::from_value(self.body.clone()).map_err(Error::from)
112 }
113
114 #[inline]
116 pub fn body_field(&self, name: &str) -> Option<&serde_json::Value> {
117 self.body.get(name)
118 }
119
120 pub fn body_field_as<T: for<'de> Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
122 match self.body.get(name) {
123 Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(Error::from),
124 None => Ok(None),
125 }
126 }
127
128 #[inline]
130 pub const fn is_authenticated(&self) -> bool {
131 self.user_id.is_some()
132 }
133
134 #[inline]
136 pub fn require_auth(&self) -> Result<&str> {
137 self.user_id
138 .as_deref()
139 .ok_or_else(|| Error::permission_denied("Authentication required"))
140 }
141
142 #[inline]
144 pub fn require_admin(&self) -> Result<()> {
145 if self.is_admin {
146 Ok(())
147 } else {
148 Err(Error::permission_denied("Admin access required"))
149 }
150 }
151
152 #[inline]
154 pub fn is_method(&self, method: &str) -> bool {
155 self.method.eq_ignore_ascii_case(method)
156 }
157
158 pub fn pagination(&self) -> (u32, u32) {
162 let page = self.query_param_as("page").unwrap_or(1).max(1);
163 let per_page = self.query_param_as("per_page").unwrap_or(20).clamp(1, 100);
164 (page, per_page)
165 }
166
167 pub fn pagination_offset(&self) -> (u32, u32) {
171 let (page, per_page) = self.pagination();
172 let offset = (page - 1) * per_page;
173 (offset, per_page)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_context_parsing() {
183 let json = r#"{
184 "method": "GET",
185 "path": "/users/123",
186 "params": {"id": "123"},
187 "query": {"page": "2", "per_page": "10"},
188 "headers": {"Content-Type": "application/json"},
189 "body": {"name": "Test"},
190 "user_id": "user123",
191 "is_admin": false
192 }"#;
193
194 let ctx: Context = serde_json::from_str(json).unwrap();
195
196 assert_eq!(ctx.method, "GET");
197 assert_eq!(ctx.path, "/users/123");
198 assert_eq!(ctx.param("id"), Some("123"));
199 assert_eq!(ctx.query_param("page"), Some("2"));
200 assert_eq!(ctx.header("content-type"), Some("application/json"));
201 assert!(ctx.is_authenticated());
202 assert!(!ctx.is_admin);
203 }
204
205 #[test]
206 fn test_pagination() {
207 let ctx = Context {
208 method: "GET".into(),
209 path: "/".into(),
210 params: HashMap::new(),
211 headers: HashMap::new(),
212 query: [("page".into(), "3".into()), ("per_page".into(), "50".into())].into(),
213 body: serde_json::Value::Null,
214 user_id: None,
215 is_admin: false,
216 request_id: None,
217 };
218
219 assert_eq!(ctx.pagination(), (3, 50));
220 assert_eq!(ctx.pagination_offset(), (100, 50));
221 }
222}