use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use http::{HeaderMap, Method, StatusCode, Uri};
use crate::error::Result;
#[derive(Debug, Clone)]
pub struct RequestContext {
pub method: Method,
pub uri: Uri,
pub headers: HeaderMap,
pub body: Option<Bytes>,
pub started_at: Instant,
pub metadata: Metadata,
}
impl RequestContext {
pub fn new(method: Method, uri: Uri) -> Self {
Self {
method,
uri,
headers: HeaderMap::new(),
body: None,
started_at: Instant::now(),
metadata: Metadata::new(),
}
}
pub fn elapsed(&self) -> Duration {
self.started_at.elapsed()
}
}
#[derive(Debug, Clone)]
pub struct ResponseContext {
pub status: StatusCode,
pub headers: HeaderMap,
pub body_size: Option<usize>,
pub duration: Duration,
pub request: RequestContext,
}
#[derive(Debug)]
pub struct ErrorContext {
pub error: crate::error::Error,
pub request: RequestContext,
pub duration: Duration,
pub should_retry: bool,
}
#[derive(Debug, Clone, Default)]
pub struct Metadata {
entries: Vec<(String, String)>,
}
impl Metadata {
pub fn new() -> Self {
Self::default()
}
pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
let key = key.into();
if let Some(entry) = self.entries.iter_mut().find(|(k, _)| k == &key) {
entry.1 = value.into();
} else {
self.entries.push((key, value.into()));
}
}
pub fn get(&self, key: &str) -> Option<&str> {
self.entries
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v.as_str())
}
pub fn contains(&self, key: &str) -> bool {
self.entries.iter().any(|(k, _)| k == key)
}
pub fn remove(&mut self, key: &str) -> Option<String> {
if let Some(idx) = self.entries.iter().position(|(k, _)| k == key) {
Some(self.entries.remove(idx).1)
} else {
None
}
}
}
pub type HookFuture<T> = Pin<Box<dyn Future<Output = T> + Send + 'static>>;
pub trait PreRequestHook: Send + Sync {
fn on_request(&self, ctx: &mut RequestContext) -> Result<()>;
}
pub trait PostResponseHook: Send + Sync {
fn on_response(&self, ctx: &ResponseContext);
}
pub trait ErrorHook: Send + Sync {
fn on_error(&self, ctx: &mut ErrorContext);
}
pub trait AsyncPreRequestHook: Send + Sync {
fn on_request(&self, ctx: &mut RequestContext) -> HookFuture<Result<()>>;
}
#[derive(Default)]
pub struct Hooks {
pre_request: Vec<Arc<dyn PreRequestHook>>,
post_response: Vec<Arc<dyn PostResponseHook>>,
error: Vec<Arc<dyn ErrorHook>>,
}
impl Hooks {
pub fn new() -> Self {
Self::default()
}
pub fn add_pre_request(&mut self, hook: impl PreRequestHook + 'static) {
self.pre_request.push(Arc::new(hook));
}
pub fn add_post_response(&mut self, hook: impl PostResponseHook + 'static) {
self.post_response.push(Arc::new(hook));
}
pub fn add_error(&mut self, hook: impl ErrorHook + 'static) {
self.error.push(Arc::new(hook));
}
pub fn run_pre_request(&self, ctx: &mut RequestContext) -> Result<()> {
for hook in &self.pre_request {
hook.on_request(ctx)?;
}
Ok(())
}
pub fn run_post_response(&self, ctx: &ResponseContext) {
for hook in &self.post_response {
hook.on_response(ctx);
}
}
pub fn run_error(&self, ctx: &mut ErrorContext) {
for hook in &self.error {
hook.on_error(ctx);
}
}
pub fn is_empty(&self) -> bool {
self.pre_request.is_empty() && self.post_response.is_empty() && self.error.is_empty()
}
}
#[derive(Debug, Default)]
pub struct LoggingHook {
log_headers: bool,
log_body: bool,
}
impl LoggingHook {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_headers(mut self) -> Self {
self.log_headers = true;
self
}
#[must_use]
pub fn with_body(mut self) -> Self {
self.log_body = true;
self
}
}
impl PreRequestHook for LoggingHook {
fn on_request(&self, ctx: &mut RequestContext) -> Result<()> {
tracing::info!(
method = %ctx.method,
uri = %ctx.uri,
"Starting request"
);
if self.log_headers {
tracing::debug!(headers = ?ctx.headers, "Request headers");
}
Ok(())
}
}
impl PostResponseHook for LoggingHook {
fn on_response(&self, ctx: &ResponseContext) {
tracing::info!(
method = %ctx.request.method,
uri = %ctx.request.uri,
status = %ctx.status,
duration_ms = ctx.duration.as_millis(),
"Request completed"
);
}
}
impl ErrorHook for LoggingHook {
fn on_error(&self, ctx: &mut ErrorContext) {
tracing::error!(
method = %ctx.request.method,
uri = %ctx.request.uri,
error = %ctx.error,
duration_ms = ctx.duration.as_millis(),
"Request failed"
);
}
}
#[derive(Debug, Clone)]
pub struct HeaderInjector {
headers: HeaderMap,
}
impl HeaderInjector {
pub fn new() -> Self {
Self {
headers: HeaderMap::new(),
}
}
pub fn add(mut self, name: http::header::HeaderName, value: http::header::HeaderValue) -> Self {
self.headers.insert(name, value);
self
}
}
impl Default for HeaderInjector {
fn default() -> Self {
Self::new()
}
}
impl PreRequestHook for HeaderInjector {
fn on_request(&self, ctx: &mut RequestContext) -> Result<()> {
for (name, value) in &self.headers {
ctx.headers.insert(name.clone(), value.clone());
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct MetricsHook {
}
impl MetricsHook {
pub fn new() -> Self {
Self::default()
}
}
impl PostResponseHook for MetricsHook {
fn on_response(&self, ctx: &ResponseContext) {
let labels = [
("method", ctx.request.method.as_str()),
("status", ctx.status.as_str()),
];
tracing::trace!(
labels = ?labels,
duration_ms = ctx.duration.as_millis(),
body_size = ctx.body_size,
"Recording metrics"
);
}
}
impl ErrorHook for MetricsHook {
fn on_error(&self, ctx: &mut ErrorContext) {
tracing::trace!(
method = %ctx.request.method,
error = %ctx.error,
"Recording error metric"
);
}
}
#[derive(Debug)]
pub struct RateLimitHook {
domain_pattern: Option<String>,
}
impl RateLimitHook {
pub fn new() -> Self {
Self {
domain_pattern: None,
}
}
pub fn for_domain(pattern: impl Into<String>) -> Self {
Self {
domain_pattern: Some(pattern.into()),
}
}
fn matches_domain(&self, uri: &Uri) -> bool {
match (&self.domain_pattern, uri.host()) {
(Some(pattern), Some(host)) => host.contains(pattern.as_str()),
(None, _) => true,
(_, None) => false,
}
}
}
impl Default for RateLimitHook {
fn default() -> Self {
Self::new()
}
}
impl PreRequestHook for RateLimitHook {
fn on_request(&self, ctx: &mut RequestContext) -> Result<()> {
if self.matches_domain(&ctx.uri) {
ctx.metadata.set("rate_limited_check", "true");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use super::*;
use http::Method;
#[test]
fn test_metadata() {
let mut meta = Metadata::new();
meta.set("key1", "value1");
meta.set("key2", "value2");
assert_eq!(meta.get("key1"), Some("value1"));
assert!(meta.contains("key2"));
assert!(!meta.contains("key3"));
meta.remove("key1");
assert!(!meta.contains("key1"));
}
#[test]
fn test_request_context() {
let ctx = RequestContext::new(Method::GET, "https://example.com".parse().unwrap());
assert_eq!(ctx.method, Method::GET);
assert!(ctx.elapsed() < Duration::from_secs(1));
}
#[test]
fn test_hooks_chain() {
let mut hooks = Hooks::new();
hooks.add_pre_request(LoggingHook::new());
hooks.add_pre_request(HeaderInjector::new());
let mut ctx = RequestContext::new(Method::GET, "https://example.com".parse().unwrap());
hooks.run_pre_request(&mut ctx).unwrap();
}
#[test]
fn test_header_injector() {
let injector = HeaderInjector::new().add(
http::header::ACCEPT,
http::header::HeaderValue::from_static("application/json"),
);
let mut ctx = RequestContext::new(Method::GET, "https://example.com".parse().unwrap());
injector.on_request(&mut ctx).unwrap();
assert!(ctx.headers.contains_key(http::header::ACCEPT));
}
}