use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "v2-query")]
use type_bridge_contract::query_plan::{QueryInvocation, QueryPlan};
use type_bridge_core_lib::ast::Clause;
use super::crud_info::CrudInfo;
#[derive(Clone)]
pub struct RequestContext {
pub request_id: String,
pub client_id: String,
pub database: String,
pub transaction_type: String,
pub metadata: HashMap<String, serde_json::Value>,
pub timestamp: chrono::DateTime<chrono::Utc>,
pub crud_info: Option<CrudInfo>,
}
impl fmt::Debug for RequestContext {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let metadata_keys = self.metadata.keys().collect::<BTreeSet<_>>();
formatter
.debug_struct("RequestContext")
.field("request_id", &self.request_id)
.field("client_id", &self.client_id)
.field("database", &self.database)
.field("transaction_type", &self.transaction_type)
.field("metadata_keys", &metadata_keys)
.field("timestamp", &self.timestamp)
.field("crud_info", &self.crud_info)
.finish()
}
}
#[cfg(feature = "v2-query")]
pub struct V2PolicyRequest<'a> {
plan: &'a QueryPlan,
invocation: &'a QueryInvocation,
}
#[cfg(feature = "v2-query")]
impl<'a> V2PolicyRequest<'a> {
pub(crate) const fn new(plan: &'a QueryPlan, invocation: &'a QueryInvocation) -> Self {
Self { plan, invocation }
}
#[must_use]
pub const fn plan(&self) -> &'a QueryPlan {
self.plan
}
#[must_use]
pub const fn invocation(&self) -> &'a QueryInvocation {
self.invocation
}
}
#[cfg(feature = "v2-query")]
pub struct V2PolicyOutcome<'a> {
success: bool,
code: &'a str,
response_bytes: usize,
}
#[cfg(feature = "v2-query")]
impl<'a> V2PolicyOutcome<'a> {
#[must_use]
pub const fn new(success: bool, code: &'a str, response_bytes: usize) -> Self {
Self {
success,
code,
response_bytes,
}
}
#[must_use]
pub const fn success(&self) -> bool {
self.success
}
#[must_use]
pub const fn code(&self) -> &'a str {
self.code
}
#[must_use]
pub const fn response_bytes(&self) -> usize {
self.response_bytes
}
}
#[derive(Debug, thiserror::Error)]
#[allow(dead_code)] pub enum InterceptError {
#[error("Access denied: {reason}")]
AccessDenied {
reason: String,
},
#[error("Rate limited: {reason}")]
RateLimited {
reason: String,
},
#[error("Validation failed: {reason}")]
ValidationFailed {
reason: String,
},
#[error("Internal error: {0}")]
Internal(String),
}
pub trait Interceptor: Send + Sync {
fn name(&self) -> &str;
fn on_request<'a>(
&'a self,
clauses: Vec<Clause>,
ctx: &'a mut RequestContext,
) -> Pin<Box<dyn Future<Output = Result<Vec<Clause>, InterceptError>> + Send + 'a>>;
#[cfg(feature = "v2-query")]
fn supports_v2(&self) -> bool {
false
}
#[cfg(feature = "v2-query")]
fn on_v2_transport<'a>(
&'a self,
_ctx: &'a mut RequestContext,
) -> Pin<Box<dyn Future<Output = Result<(), InterceptError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
#[cfg(feature = "v2-query")]
fn on_v2_request<'a>(
&'a self,
_request: &'a V2PolicyRequest<'a>,
_ctx: &'a mut RequestContext,
) -> Pin<Box<dyn Future<Output = Result<(), InterceptError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
#[cfg(feature = "v2-query")]
fn on_v2_response<'a>(
&'a self,
_outcome: &'a V2PolicyOutcome<'a>,
_ctx: &'a RequestContext,
) -> Pin<Box<dyn Future<Output = Result<(), InterceptError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
fn on_response<'a>(
&'a self,
_result: &'a serde_json::Value,
_ctx: &'a RequestContext,
) -> Pin<Box<dyn Future<Output = Result<(), InterceptError>> + Send + 'a>> {
Box::pin(async { Ok(()) })
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn intercept_error_access_denied_display() {
let e = InterceptError::AccessDenied {
reason: "no permission".into(),
};
assert_eq!(e.to_string(), "Access denied: no permission");
}
#[test]
fn intercept_error_rate_limited_display() {
let e = InterceptError::RateLimited {
reason: "too many requests".into(),
};
assert_eq!(e.to_string(), "Rate limited: too many requests");
}
#[test]
fn intercept_error_validation_failed_display() {
let e = InterceptError::ValidationFailed {
reason: "bad input".into(),
};
assert_eq!(e.to_string(), "Validation failed: bad input");
}
#[test]
fn intercept_error_internal_display() {
let e = InterceptError::Internal("something broke".into());
assert_eq!(e.to_string(), "Internal error: something broke");
}
#[test]
fn request_context_clone() {
let ctx = RequestContext {
request_id: "req-1".into(),
client_id: "client-1".into(),
database: "db".into(),
transaction_type: "read".into(),
metadata: HashMap::new(),
timestamp: chrono::Utc::now(),
crud_info: None,
};
let cloned = ctx.clone();
assert_eq!(cloned.request_id, "req-1");
assert_eq!(cloned.database, "db");
}
#[test]
fn request_context_debug() {
let metadata = HashMap::from([
(
"http_headers".to_owned(),
serde_json::json!({
"authorization": ["Bearer debug-secret"],
"cookie": ["session=debug-secret"],
}),
),
("tenant".to_owned(), serde_json::json!("private-tenant")),
]);
let ctx = RequestContext {
request_id: "req-1".into(),
client_id: "client-1".into(),
database: "db".into(),
transaction_type: "read".into(),
metadata,
timestamp: chrono::Utc::now(),
crud_info: None,
};
let debug = format!("{:?}", ctx);
assert!(debug.contains("req-1"));
assert!(debug.contains("http_headers"));
assert!(debug.contains("tenant"));
assert!(!debug.contains("debug-secret"));
assert!(!debug.contains("private-tenant"));
assert!(!debug.contains("authorization"));
assert!(!debug.contains("cookie"));
}
#[tokio::test]
async fn default_on_response_returns_ok() {
struct MinimalInterceptor;
impl Interceptor for MinimalInterceptor {
fn name(&self) -> &str {
"minimal"
}
fn on_request<'a>(
&'a self,
clauses: Vec<Clause>,
_ctx: &'a mut RequestContext,
) -> Pin<Box<dyn Future<Output = Result<Vec<Clause>, InterceptError>> + Send + 'a>>
{
Box::pin(async move { Ok(clauses) })
}
}
let interceptor = MinimalInterceptor;
assert_eq!(interceptor.name(), "minimal");
let mut ctx = RequestContext {
request_id: "req-1".into(),
client_id: "client-1".into(),
database: "db".into(),
transaction_type: "read".into(),
metadata: HashMap::new(),
timestamp: chrono::Utc::now(),
crud_info: None,
};
let req_result = interceptor.on_request(vec![], &mut ctx).await;
assert!(req_result.is_ok());
let result = interceptor.on_response(&serde_json::json!({}), &ctx).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn interceptor_trait_object_safety() {
struct DummyInterceptor;
impl Interceptor for DummyInterceptor {
fn name(&self) -> &str {
"dummy"
}
fn on_request<'a>(
&'a self,
clauses: Vec<Clause>,
_ctx: &'a mut RequestContext,
) -> Pin<Box<dyn Future<Output = Result<Vec<Clause>, InterceptError>> + Send + 'a>>
{
Box::pin(async move { Ok(clauses) })
}
}
let boxed: Box<dyn Interceptor> = Box::new(DummyInterceptor);
assert_eq!(boxed.name(), "dummy");
let mut ctx = RequestContext {
request_id: "req-1".into(),
client_id: "client-1".into(),
database: "db".into(),
transaction_type: "read".into(),
metadata: HashMap::new(),
timestamp: chrono::Utc::now(),
crud_info: None,
};
let result = boxed.on_request(vec![], &mut ctx).await;
assert!(result.unwrap().is_empty());
}
}