use super::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Context {
pub method: String,
pub path: String,
#[serde(default)]
pub params: HashMap<String, String>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub query: HashMap<String, String>,
#[serde(default)]
pub body: serde_json::Value,
#[serde(default)]
pub user_id: Option<String>,
#[serde(default)]
pub is_admin: bool,
#[serde(default)]
pub request_id: Option<String>,
}
impl Context {
#[cfg(target_arch = "wasm32")]
pub fn from_raw(ptr: i32, len: i32) -> Result<Self> {
let bytes = unsafe {
std::slice::from_raw_parts(ptr as *const u8, len as usize)
};
serde_json::from_slice(bytes).map_err(Error::from)
}
#[cfg(not(target_arch = "wasm32"))]
pub fn from_raw(_ptr: i32, _len: i32) -> Result<Self> {
Err(Error::internal("from_raw only available in WASM"))
}
#[inline]
pub fn param(&self, name: &str) -> Option<&str> {
self.params.get(name).map(String::as_str)
}
#[inline]
pub fn param_required(&self, name: &str) -> Result<&str> {
self.params
.get(name)
.map(String::as_str)
.ok_or_else(|| Error::invalid_input(format!("Missing path parameter: {}", name)))
}
#[inline]
pub fn query_param(&self, name: &str) -> Option<&str> {
self.query.get(name).map(String::as_str)
}
#[inline]
pub fn query_param_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
self.query.get(name).map(String::as_str).unwrap_or(default)
}
pub fn query_param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
self.query.get(name).and_then(|v| v.parse().ok())
}
#[inline]
pub fn header(&self, name: &str) -> Option<&str> {
let name_lower = name.to_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_lowercase() == name_lower)
.map(|(_, v)| v.as_str())
}
#[inline]
pub fn body_as<T: for<'de> Deserialize<'de>>(&self) -> Result<T> {
serde_json::from_value(self.body.clone()).map_err(Error::from)
}
#[inline]
pub fn body_field(&self, name: &str) -> Option<&serde_json::Value> {
self.body.get(name)
}
pub fn body_field_as<T: for<'de> Deserialize<'de>>(&self, name: &str) -> Result<Option<T>> {
match self.body.get(name) {
Some(v) => serde_json::from_value(v.clone()).map(Some).map_err(Error::from),
None => Ok(None),
}
}
#[inline]
pub const fn is_authenticated(&self) -> bool {
self.user_id.is_some()
}
#[inline]
pub fn require_auth(&self) -> Result<&str> {
self.user_id
.as_deref()
.ok_or_else(|| Error::permission_denied("Authentication required"))
}
#[inline]
pub fn require_admin(&self) -> Result<()> {
if self.is_admin {
Ok(())
} else {
Err(Error::permission_denied("Admin access required"))
}
}
#[inline]
pub fn is_method(&self, method: &str) -> bool {
self.method.eq_ignore_ascii_case(method)
}
pub fn pagination(&self) -> (u32, u32) {
let page = self.query_param_as("page").unwrap_or(1).max(1);
let per_page = self.query_param_as("per_page").unwrap_or(20).clamp(1, 100);
(page, per_page)
}
pub fn pagination_offset(&self) -> (u32, u32) {
let (page, per_page) = self.pagination();
let offset = (page - 1) * per_page;
(offset, per_page)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_parsing() {
let json = r#"{
"method": "GET",
"path": "/users/123",
"params": {"id": "123"},
"query": {"page": "2", "per_page": "10"},
"headers": {"Content-Type": "application/json"},
"body": {"name": "Test"},
"user_id": "user123",
"is_admin": false
}"#;
let ctx: Context = serde_json::from_str(json).unwrap();
assert_eq!(ctx.method, "GET");
assert_eq!(ctx.path, "/users/123");
assert_eq!(ctx.param("id"), Some("123"));
assert_eq!(ctx.query_param("page"), Some("2"));
assert_eq!(ctx.header("content-type"), Some("application/json"));
assert!(ctx.is_authenticated());
assert!(!ctx.is_admin);
}
#[test]
fn test_pagination() {
let ctx = Context {
method: "GET".into(),
path: "/".into(),
params: HashMap::new(),
headers: HashMap::new(),
query: [("page".into(), "3".into()), ("per_page".into(), "50".into())].into(),
body: serde_json::Value::Null,
user_id: None,
is_admin: false,
request_id: None,
};
assert_eq!(ctx.pagination(), (3, 50));
assert_eq!(ctx.pagination_offset(), (100, 50));
}
}