use actix_web::body::MessageBody;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::http::header::{HeaderName, HeaderValue};
use actix_web::{Error, http::header};
use futures::future::{LocalBoxFuture, Ready, ready};
use std::sync::LazyLock;
use uuid::Uuid;
pub const INSTANCE_HEADER: &str = "x-ocs-instance";
static INSTANCE_ID: LazyLock<String> = LazyLock::new(|| Uuid::new_v4().to_string());
#[must_use]
pub fn instance_id() -> &'static str {
&INSTANCE_ID
}
pub struct InstanceHeader;
impl<S, B> Transform<S, ServiceRequest> for InstanceHeader
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = InstanceHeaderService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(InstanceHeaderService { service }))
}
}
pub struct InstanceHeaderService<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for InstanceHeaderService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_web::dev::forward_ready!(service);
fn call(&self, request: ServiceRequest) -> Self::Future {
let future = self.service.call(request);
Box::pin(async move {
let mut response = future.await?;
if let Ok(value) = HeaderValue::from_str(instance_id()) {
response
.headers_mut()
.insert(HeaderName::from_static(INSTANCE_HEADER), value);
}
let _ = header::CONTENT_TYPE;
Ok(response)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{App, HttpResponse, test, web};
#[actix_web::test]
async fn test_every_response_names_the_instance() {
let app = test::init_service(App::new().wrap(InstanceHeader).route(
"/probe",
web::get().to(|| async { HttpResponse::Ok().finish() }),
))
.await;
let first =
test::call_service(&app, test::TestRequest::get().uri("/probe").to_request()).await;
let second =
test::call_service(&app, test::TestRequest::get().uri("/probe").to_request()).await;
let read = |response: &ServiceResponse<_>| -> String {
response
.headers()
.get(INSTANCE_HEADER)
.and_then(|value| value.to_str().ok())
.unwrap_or_default()
.to_string()
};
let one = read(&first);
assert!(!one.is_empty(), "every response must name its instance");
assert_eq!(one, read(&second), "the identity is fixed for the process");
assert_eq!(one, instance_id(), "and it is the one the handlers report");
}
#[actix_web::test]
async fn test_the_identity_is_opaque() {
let id = instance_id();
assert!(
Uuid::parse_str(id).is_ok(),
"the identity must be an opaque uuid, it was {id}"
);
}
}