#[cfg(all(feature = "actix", not(feature = "simulator")))]
use std::sync::{Arc, Mutex};
#[cfg(all(feature = "actix", not(feature = "simulator")))]
use ::actix_test::{TestServer, start};
#[cfg(all(feature = "actix", not(feature = "simulator")))]
pub struct FlattenedRoute {
pub full_path: String,
pub method: crate::Method,
pub handler: std::sync::Arc<crate::RouteHandler>,
}
impl FlattenedRoute {
#[cfg(all(feature = "actix", not(feature = "simulator")))]
fn new(
full_path: String,
method: crate::Method,
handler: std::sync::Arc<crate::RouteHandler>,
) -> Self {
Self {
full_path,
method,
handler,
}
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
impl std::fmt::Debug for FlattenedRoute {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlattenedRoute")
.field("full_path", &self.full_path)
.field("method", &self.method)
.field("handler", &"<RouteHandler>")
.finish()
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
#[must_use]
pub fn flatten_scope_tree(scopes: &[crate::Scope]) -> Vec<FlattenedRoute> {
let mut flattened_routes = Vec::new();
for scope in scopes {
flatten_scope_recursive(scope, "", &mut flattened_routes);
}
flattened_routes
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
fn normalize_scope_path(path: &str) -> String {
match path {
"" | "/" => String::new(), p if p.starts_with("//") => p[1..].to_string(), p if p.ends_with('/') && p.len() > 1 => p[..p.len() - 1].to_string(), p => p.to_string(),
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
fn normalize_route_path(path: &str) -> String {
match path {
"" | "/" => "/".to_string(), p if p.starts_with("//") => p[1..].to_string(), p if !p.starts_with('/') => format!("/{p}"), p => p.to_string(),
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
fn convert_scope_to_actix(scope: &crate::Scope) -> actix_web::Scope {
let normalized_scope_path = normalize_scope_path(&scope.path);
let mut actix_scope = actix_web::web::scope(&normalized_scope_path);
for route in &scope.routes {
let normalized_route_path = normalize_route_path(&route.path);
let handler = std::sync::Arc::clone(&route.handler);
let method = route.method;
let actix_handler = move |req: actix_web::HttpRequest| {
let handler = handler.clone();
async move {
let our_request = crate::HttpRequest::from(&req);
let result = handler(our_request).await;
result.map(|resp| {
let mut actix_resp = actix_web::HttpResponseBuilder::new(
actix_web::http::StatusCode::from_u16(resp.status_code.into())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR),
);
for (name, value) in resp.headers {
actix_resp.insert_header((name, value));
}
if let Some(location) = resp.location {
actix_resp.insert_header((actix_http::header::LOCATION, location));
}
match resp.body {
Some(crate::HttpResponseBody::Bytes(bytes)) => actix_resp.body(bytes),
None => actix_resp.finish(),
}
})
}
};
actix_scope = match method {
crate::Method::Get => actix_scope.route(
&normalized_route_path,
actix_web::web::get().to(actix_handler),
),
crate::Method::Post => actix_scope.route(
&normalized_route_path,
actix_web::web::post().to(actix_handler),
),
crate::Method::Put => actix_scope.route(
&normalized_route_path,
actix_web::web::put().to(actix_handler),
),
crate::Method::Delete => actix_scope.route(
&normalized_route_path,
actix_web::web::delete().to(actix_handler),
),
crate::Method::Patch => actix_scope.route(
&normalized_route_path,
actix_web::web::patch().to(actix_handler),
),
crate::Method::Head => actix_scope.route(
&normalized_route_path,
actix_web::web::head().to(actix_handler),
),
crate::Method::Options => actix_scope.route(
&normalized_route_path,
actix_web::web::route()
.method(actix_web::http::Method::OPTIONS)
.to(actix_handler),
),
crate::Method::Trace => actix_scope.route(
&normalized_route_path,
actix_web::web::route()
.method(actix_web::http::Method::TRACE)
.to(actix_handler),
),
crate::Method::Connect => actix_scope.route(
&normalized_route_path,
actix_web::web::route()
.method(actix_web::http::Method::CONNECT)
.to(actix_handler),
),
};
}
for nested_scope in &scope.scopes {
let nested_actix_scope = convert_scope_to_actix(nested_scope);
actix_scope = actix_scope.service(nested_actix_scope);
}
actix_scope
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
fn join_paths(left: &str, right: &str) -> String {
match (left.is_empty(), right.is_empty()) {
(true, true) => String::new(),
(true, false) => right.to_string(),
(false, true) => left.to_string(),
(false, false) => {
if left == "/" {
if right.starts_with('/') {
right.to_string()
} else {
format!("/{right}")
}
} else if left.ends_with('/') || right.starts_with('/') {
format!("{left}{right}")
} else {
format!("{left}/{right}")
}
}
}
}
fn flatten_scope_recursive(
scope: &crate::Scope,
parent_prefix: &str,
results: &mut Vec<FlattenedRoute>,
) {
let full_prefix = join_paths(parent_prefix, &scope.path);
for route in &scope.routes {
let full_path = join_paths(&full_prefix, &route.path);
let flattened_route = FlattenedRoute::new(
full_path,
route.method,
std::sync::Arc::clone(&route.handler),
);
results.push(flattened_route);
}
for nested_scope in &scope.scopes {
flatten_scope_recursive(nested_scope, &full_prefix, results);
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
pub struct ActixWebServer {
test_server: Arc<Mutex<TestServer>>,
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
impl ActixWebServer {
#[must_use]
pub fn new(scopes: Vec<crate::Scope>) -> Self {
Self::new_with_native_nesting(scopes)
}
#[must_use]
pub fn new_with_flattening(scopes: Vec<crate::Scope>) -> Self {
let app = move || {
let mut app = actix_web::App::new();
let flattened_routes = flatten_scope_tree(&scopes);
for flattened_route in flattened_routes {
let path = flattened_route.full_path;
let handler = flattened_route.handler;
let method = flattened_route.method;
let actix_handler = move |req: actix_web::HttpRequest| {
let handler = handler.clone();
async move {
let our_request = crate::HttpRequest::from(&req);
let result = handler(our_request).await;
result.map(|resp| {
let mut actix_resp = actix_web::HttpResponseBuilder::new(
actix_web::http::StatusCode::from_u16(resp.status_code.into())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR),
);
for (name, value) in resp.headers {
actix_resp.insert_header((name, value));
}
if let Some(location) = resp.location {
actix_resp.insert_header((actix_http::header::LOCATION, location));
}
match resp.body {
Some(crate::HttpResponseBody::Bytes(bytes)) => {
actix_resp.body(bytes)
}
None => actix_resp.finish(),
}
})
}
};
app = match method {
crate::Method::Get => app.route(&path, actix_web::web::get().to(actix_handler)),
crate::Method::Post => {
app.route(&path, actix_web::web::post().to(actix_handler))
}
crate::Method::Put => app.route(&path, actix_web::web::put().to(actix_handler)),
crate::Method::Delete => {
app.route(&path, actix_web::web::delete().to(actix_handler))
}
crate::Method::Patch => {
app.route(&path, actix_web::web::patch().to(actix_handler))
}
crate::Method::Head => {
app.route(&path, actix_web::web::head().to(actix_handler))
}
crate::Method::Options => app.route(
&path,
actix_web::web::route()
.method(actix_web::http::Method::OPTIONS)
.to(actix_handler),
),
crate::Method::Trace => app.route(
&path,
actix_web::web::route()
.method(actix_web::http::Method::TRACE)
.to(actix_handler),
),
crate::Method::Connect => app.route(
&path,
actix_web::web::route()
.method(actix_web::http::Method::CONNECT)
.to(actix_handler),
),
};
}
app
};
let test_server = start(app);
#[allow(clippy::arc_with_non_send_sync)]
let wrapped_server = Arc::new(Mutex::new(test_server));
Self {
test_server: wrapped_server,
}
}
#[must_use]
pub fn new_with_native_nesting(scopes: Vec<crate::Scope>) -> Self {
let app = move || {
let mut app = actix_web::App::new();
for scope in &scopes {
let actix_scope = convert_scope_to_actix(scope);
app = app.service(actix_scope);
}
app
};
let test_server = start(app);
#[allow(clippy::arc_with_non_send_sync)]
let wrapped_server = Arc::new(Mutex::new(test_server));
Self {
test_server: wrapped_server,
}
}
#[must_use]
pub fn url(&self) -> String {
let addr = {
let server = self.test_server.lock().unwrap();
server.addr()
}; format!("http://{addr}")
}
#[must_use]
pub fn addr(&self) -> std::net::SocketAddr {
let server = self.test_server.lock().unwrap();
server.addr()
}
#[must_use]
pub fn port(&self) -> u16 {
let addr = {
let server = self.test_server.lock().unwrap();
server.addr()
}; addr.port()
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
#[derive(Debug, Default)]
pub struct ActixWebServerBuilder {
scopes: Vec<crate::Scope>,
addr: Option<String>,
port: Option<u16>,
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
impl ActixWebServerBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_scope(mut self, scope: crate::Scope) -> Self {
self.scopes.push(scope);
self
}
#[must_use]
pub fn with_scopes(mut self, scopes: impl IntoIterator<Item = crate::Scope>) -> Self {
self.scopes.extend(scopes);
self
}
#[must_use]
pub fn with_addr(mut self, addr: impl Into<String>) -> Self {
self.addr = Some(addr.into());
self
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn with_port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
#[must_use]
pub fn build(self) -> ActixWebServer {
ActixWebServer::new(self.scopes)
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
impl ActixWebServer {
#[must_use]
pub fn with_test_routes() -> Self {
let scope = crate::Scope::new("")
.route(crate::Method::Get, "/test", |_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_content_type("application/json")
.with_body(crate::HttpResponseBody::from(
r#"{"message":"Hello from test route!"}"#,
)))
})
})
.route(crate::Method::Get, "/health", |_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_content_type("application/json")
.with_body(crate::HttpResponseBody::from(r#"{"status":"ok"}"#)))
})
});
Self::new(vec![scope])
}
#[must_use]
pub fn with_api_routes() -> Self {
let scope = crate::Scope::new("/api")
.route(crate::Method::Get, "/status", |_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_content_type("application/json")
.with_body(crate::HttpResponseBody::from(r#"{"service":"running"}"#)))
})
})
.route(crate::Method::Post, "/echo", |_req| {
Box::pin(async {
Ok(crate::HttpResponse::ok()
.with_content_type("application/json")
.with_body(crate::HttpResponseBody::from(r#"{"echoed":"data"}"#)))
})
});
Self::new(vec![scope])
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
pub struct ActixTestClient {
_server: ActixWebServer,
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
impl ActixTestClient {
#[must_use]
pub const fn new(server: ActixWebServer) -> Self {
Self { _server: server }
}
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
#[derive(Debug, thiserror::Error)]
pub enum ActixTestClientError {
#[error("Actix backend has thread-safety limitations. Use simulator backend instead.")]
ThreadSafetyLimitation,
#[error("Invalid HTTP method: {0}")]
InvalidMethod(String),
}
#[cfg(not(feature = "actix"))]
pub struct ActixTestClient;
#[cfg(not(feature = "actix"))]
impl ActixTestClient {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[cfg(not(feature = "actix"))]
impl Default for ActixTestClient {
fn default() -> Self {
Self::new()
}
}