murgamu 0.8.4

A NestJS-inspired web framework for Rust
Documentation
use crate::core::utils::MurCodec;
use crate::server::error::MurError;
use crate::server::router::MurRouteAccessControl;
use crate::server::service::MurService;
use crate::server::service::MurServiceContainer;
use http::request::Parts;
use hyper::body::Bytes;
use serde::de::DeserializeOwned;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::sync::OnceLock;

#[derive(Clone)]
pub struct MurRequestContext {
	pub parts: Parts,
	pub body: Option<Bytes>,
	pub path_params: HashMap<String, String>,
	pub container: Arc<MurServiceContainer>,
	query_cache: OnceLock<HashMap<String, String>>,
	pub(crate) access_control: Option<MurRouteAccessControl>,
}

impl MurRequestContext {
	pub fn new(
		parts: Parts,
		body: Option<Bytes>,
		path_params: HashMap<String, String>,
		container: Arc<MurServiceContainer>,
	) -> Self {
		Self {
			parts,
			body,
			path_params,
			container,
			query_cache: OnceLock::new(),
			access_control: None,
		}
	}

	pub fn service<T: MurService>(&self) -> Option<Arc<T>> {
		self.container.get::<T>()
	}

	pub fn service_required<T: MurService>(&self) -> Arc<T> {
		self.container.get_required::<T>()
	}

	pub fn path_param(&self, name: &str) -> Option<&str> {
		self.path_params.get(name).map(|s| s.as_str())
	}

	pub fn param_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
		self.path_param(name).unwrap_or(default)
	}

	pub fn param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
		self.path_param(name).and_then(|s| s.parse().ok())
	}

	pub fn params(&self) -> &HashMap<String, String> {
		&self.path_params
	}

	pub fn has_param(&self, name: &str) -> bool {
		self.path_params.contains_key(name)
	}

	pub fn path(&self) -> &str {
		self.parts.uri.path()
	}

	pub fn path_segment(&self, index: usize) -> Option<&str> {
		self.path().trim_start_matches('/').split('/').nth(index)
	}

	pub fn path_segments(&self) -> Vec<&str> {
		self.path()
			.trim_start_matches('/')
			.split('/')
			.filter(|s| !s.is_empty())
			.collect()
	}

	pub fn query_map(&self) -> &HashMap<String, String> {
		self.query_cache.get_or_init(|| {
			self.parts
				.uri
				.query()
				.and_then(|q| serde_urlencoded::from_str(q).ok())
				.unwrap_or_default()
		})
	}

	pub fn query_param(&self, name: &str) -> Option<&str> {
		self.query_map().get(name).map(|s| s.as_str())
	}

	pub fn query_param_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
		self.query_param(name).unwrap_or(default)
	}

	pub fn query_param_as<T: std::str::FromStr>(&self, name: &str) -> Option<T> {
		self.query_param(name).and_then(|s| s.parse().ok())
	}

	pub fn has_query_param(&self, name: &str) -> bool {
		self.query_param(name).is_some()
	}

	pub fn query_string(&self) -> Option<&str> {
		self.parts.uri.query()
	}

	pub fn header(&self, name: &str) -> Option<&str> {
		self.parts.headers.get(name).and_then(|v| v.to_str().ok())
	}

	pub fn header_or<'a>(&'a self, name: &str, default: &'a str) -> &'a str {
		self.header(name).unwrap_or(default)
	}

	pub fn has_header(&self, name: &str) -> bool {
		self.parts.headers.contains_key(name)
	}

	pub fn cookie(&self, name: &str) -> Option<&str> {
		self.header("Cookie").and_then(|cookies| {
			cookies.split(';').map(|s| s.trim()).find_map(|cookie| {
				let (key, value) = cookie.split_once('=')?;
				if key == name {
					Some(value)
				} else {
					None
				}
			})
		})
	}

	pub fn cookies(&self) -> HashMap<String, String> {
		self.header("Cookie")
			.map(|cookies| {
				cookies
					.split(';')
					.filter_map(|cookie| {
						let (key, value) = cookie.trim().split_once('=')?;
						Some((key.to_string(), value.to_string()))
					})
					.collect()
			})
			.unwrap_or_default()
	}

	pub fn has_cookie(&self, name: &str) -> bool {
		self.cookie(name).is_some()
	}

	pub fn content_type(&self) -> Option<&str> {
		self.header("Content-Type")
	}

	pub fn is_json(&self) -> bool {
		self.content_type()
			.map(|ct| ct.contains("application/json"))
			.unwrap_or(false)
	}

	pub fn is_form(&self) -> bool {
		self.content_type()
			.map(|ct| ct.contains("application/x-www-form-urlencoded"))
			.unwrap_or(false)
	}

	pub fn is_multipart(&self) -> bool {
		self.content_type()
			.map(|ct| ct.contains("multipart/form-data"))
			.unwrap_or(false)
	}

	pub fn authorization(&self) -> Option<&str> {
		self.header("Authorization")
	}

	pub fn bearer_token(&self) -> Option<&str> {
		self.authorization()
			.and_then(|auth| auth.strip_prefix("Bearer "))
	}

	pub fn basic_auth(&self) -> Option<(String, String)> {
		self.authorization()
			.and_then(|auth| auth.strip_prefix("Basic "))
			.and_then(|encoded| {
				let decoded = MurCodec::base64_decode(encoded).ok()?;
				let decoded_str = String::from_utf8(decoded).ok()?;
				let mut parts = decoded_str.splitn(2, ':');
				let username = parts.next()?.to_string();
				let password = parts.next()?.to_string();
				Some((username, password))
			})
	}

	pub fn user_agent(&self) -> Option<&str> {
		self.header("User-Agent")
	}

	pub fn accept(&self) -> Option<&str> {
		self.header("Accept")
	}

	pub fn accepts_json(&self) -> bool {
		self.accept()
			.map(|a| a.contains("application/json") || a.contains("*/*"))
			.unwrap_or(true)
	}

	pub fn method(&self) -> &http::Method {
		&self.parts.method
	}

	pub fn uri(&self) -> &http::Uri {
		&self.parts.uri
	}

	pub fn is_get(&self) -> bool {
		self.parts.method == http::Method::GET
	}

	pub fn is_post(&self) -> bool {
		self.parts.method == http::Method::POST
	}

	pub fn is_put(&self) -> bool {
		self.parts.method == http::Method::PUT
	}

	pub fn is_delete(&self) -> bool {
		self.parts.method == http::Method::DELETE
	}

	pub fn is_patch(&self) -> bool {
		self.parts.method == http::Method::PATCH
	}

	pub fn json<T: DeserializeOwned>(&self) -> Result<T, MurError> {
		let body = self
			.body
			.as_ref()
			.ok_or_else(|| MurError::BadRequest("Missing request body".to_string()))?;

		serde_json::from_slice(body)
			.map_err(|e| MurError::BadRequest(format!("Invalid JSON: {}", e)))
	}

	pub fn body_bytes(&self) -> Option<&Bytes> {
		self.body.as_ref()
	}

	pub fn body_string(&self) -> Result<String, MurError> {
		let body = self
			.body
			.as_ref()
			.ok_or_else(|| MurError::BadRequest("Missing request body".to_string()))?;

		String::from_utf8(body.to_vec())
			.map_err(|e| MurError::BadRequest(format!("Invalid UTF-8 in body: {}", e)))
	}

	pub fn form<T: DeserializeOwned>(&self) -> Result<T, MurError> {
		let body = self.body_string()?;
		serde_urlencoded::from_str(&body)
			.map_err(|e| MurError::BadRequest(format!("Invalid form data: {}", e)))
	}

	pub fn has_body(&self) -> bool {
		self.body.is_some()
	}

	pub fn content_length(&self) -> Option<usize> {
		self.header("Content-Length").and_then(|s| s.parse().ok())
	}

	pub fn client_ip(&self) -> Option<&str> {
		self.header("X-Forwarded-For")
			.and_then(|s| s.split(',').next())
			.map(|s| s.trim())
			.or_else(|| self.header("X-Real-IP"))
	}

	pub fn host(&self) -> Option<&str> {
		self.header("Host")
	}

	pub fn origin(&self) -> Option<&str> {
		self.header("Origin")
	}

	pub fn referer(&self) -> Option<&str> {
		self.header("Referer")
	}

	pub fn header_all(&self, name: &str) -> Vec<&str> {
		self.parts
			.headers
			.get_all(name)
			.iter()
			.filter_map(|v| v.to_str().ok())
			.collect()
	}

	pub fn typed_query<T: DeserializeOwned>(&self) -> Result<T, MurError> {
		let query = self.parts.uri.query().unwrap_or("");
		serde_urlencoded::from_str(query)
			.map_err(|e| MurError::BadRequest(format!("Failed to parse query params: {}", e)))
	}

	pub fn with_access_control(mut self, access_control: MurRouteAccessControl) -> Self {
		self.access_control = Some(access_control);
		self
	}

	pub fn is_public_route(&self) -> bool {
		self.access_control
			.as_ref()
			.map(|ac| ac.is_public)
			.unwrap_or(false)
	}

	pub fn allowed_roles(&self) -> Option<&HashSet<String>> {
		self.access_control.as_ref().map(|ac| &ac.allowed_roles)
	}

	pub fn has_allowed_role(&self, role: &str) -> bool {
		self.access_control
			.as_ref()
			.map(|ac| ac.allowed_roles.is_empty() || ac.allowed_roles.contains(role))
			.unwrap_or(true)
	}
}

impl std::fmt::Debug for MurRequestContext {
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
		f.debug_struct("MurRequestContext")
			.field("method", &self.parts.method)
			.field("uri", &self.parts.uri)
			.field("path_params", &self.path_params)
			.field("has_body", &self.body.is_some())
			.finish()
	}
}