use std::sync::Arc;
use std::time::Duration;
use http::{Method, StatusCode};
use crate::error::ErrorDetail;
#[derive(Clone, Debug)]
pub(crate) struct RequestInfo(Arc<RequestInfoInner>);
#[derive(Debug)]
struct RequestInfoInner {
method: Method,
path: Arc<str>,
route: Option<Arc<str>>,
request_id: Option<Arc<str>>,
}
impl RequestInfo {
pub(crate) fn new(
method: Method,
path: Arc<str>,
route: Option<Arc<str>>,
request_id: Option<Arc<str>>,
) -> Self {
Self(Arc::new(RequestInfoInner {
method,
path,
route,
request_id,
}))
}
pub(crate) fn method(&self) -> &Method {
&self.0.method
}
pub(crate) fn path(&self) -> &str {
self.0.path.as_ref()
}
pub(crate) fn route(&self) -> Option<&str> {
self.0.route.as_deref()
}
pub(crate) fn request_id(&self) -> Option<&str> {
self.0.request_id.as_deref()
}
}
macro_rules! shared_accessors {
($t:ty) => {
impl $t {
pub fn method(&self) -> &Method {
&self.info.0.method
}
pub fn path(&self) -> &str {
self.info.0.path.as_ref()
}
pub fn route(&self) -> Option<&str> {
self.info.0.route.as_deref()
}
pub fn request_id(&self) -> Option<&str> {
self.info.0.request_id.as_deref()
}
}
};
}
pub struct RequestEvent {
info: RequestInfo,
}
impl RequestEvent {
pub(crate) fn new(info: RequestInfo) -> Self {
Self { info }
}
}
shared_accessors!(RequestEvent);
pub struct ResponseEvent {
info: RequestInfo,
status: StatusCode,
elapsed: Duration,
}
impl ResponseEvent {
pub(crate) fn new(info: RequestInfo, status: StatusCode, elapsed: Duration) -> Self {
Self {
info,
status,
elapsed,
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn elapsed(&self) -> Duration {
self.elapsed
}
}
shared_accessors!(ResponseEvent);
pub struct ErrorEvent {
info: RequestInfo,
status: StatusCode,
code: &'static str,
message: String,
}
impl ErrorEvent {
pub(crate) fn new(
info: RequestInfo,
status: StatusCode,
code: &'static str,
message: String,
) -> Self {
Self {
info,
status,
code,
message,
}
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn code(&self) -> &str {
self.code
}
pub fn message(&self) -> &str {
&self.message
}
}
shared_accessors!(ErrorEvent);
pub struct ValidationErrorEvent {
info: RequestInfo,
details: Vec<ErrorDetail>,
}
impl ValidationErrorEvent {
pub(crate) fn new(info: RequestInfo, details: Vec<ErrorDetail>) -> Self {
Self { info, details }
}
pub fn details(&self) -> &[ErrorDetail] {
&self.details
}
}
shared_accessors!(ValidationErrorEvent);
pub struct PanicEvent {
info: RequestInfo,
message: String,
}
impl PanicEvent {
pub(crate) fn new(info: RequestInfo, message: String) -> Self {
Self { info, message }
}
pub fn message(&self) -> &str {
&self.message
}
}
shared_accessors!(PanicEvent);
pub struct ErrorContext {
info: RequestInfo,
}
impl ErrorContext {
pub(crate) fn new(info: RequestInfo) -> Self {
Self { info }
}
}
shared_accessors!(ErrorContext);
#[cfg(test)]
mod tests {
use super::*;
fn info() -> RequestInfo {
RequestInfo::new(
Method::GET,
Arc::from("/users/7"),
Some(Arc::from("/users/{id}")),
Some(Arc::from("req-1")),
)
}
#[test]
fn shared_accessors_expose_request_metadata() {
let event = RequestEvent::new(info());
assert_eq!(event.method(), Method::GET);
assert_eq!(event.path(), "/users/7");
assert_eq!(event.route(), Some("/users/{id}"));
assert_eq!(event.request_id(), Some("req-1"));
}
#[test]
fn response_event_carries_status_and_elapsed() {
let event = ResponseEvent::new(info(), StatusCode::OK, Duration::from_millis(5));
assert_eq!(event.status(), StatusCode::OK);
assert_eq!(event.elapsed(), Duration::from_millis(5));
}
#[test]
fn error_event_carries_status_code_and_message() {
let event = ErrorEvent::new(
info(),
StatusCode::NOT_FOUND,
"NOT_FOUND",
"missing".to_owned(),
);
assert_eq!(event.status(), StatusCode::NOT_FOUND);
assert_eq!(event.code(), "NOT_FOUND");
assert_eq!(event.message(), "missing");
}
#[test]
fn validation_event_carries_details() {
let event = ValidationErrorEvent::new(
info(),
vec![ErrorDetail::new("name", "TOO_SHORT", "too short")],
);
assert_eq!(event.details().len(), 1);
assert_eq!(event.details()[0].field, "name");
}
#[test]
fn panic_event_carries_message() {
let event = PanicEvent::new(info(), "boom".to_owned());
assert_eq!(event.message(), "boom");
}
#[test]
fn error_context_exposes_route() {
let ctx = ErrorContext::new(info());
assert_eq!(ctx.route(), Some("/users/{id}"));
}
}