use std::sync::Arc;
use crate::{server::ApiErrorV1, service::ResourceController, utils::sanitize_path};
use actix_web::{Scope, web::JsonConfig};
use mime;
use tracing::{instrument, trace};
#[derive(Clone)]
pub struct ApiService {
path: String,
json_config: JsonConfig,
controllers: Vec<Arc<dyn ResourceController + 'static>>,
}
impl ApiService {
pub fn new<P: Into<String>>(path: P) -> Self {
Self {
path: sanitize_path(path.into()),
json_config: JsonConfig::default()
.limit(5 * 1024)
.content_type(|mime| mime.subtype() == mime::JSON)
.error_handler(|err, _| ApiErrorV1::from(err).into()),
controllers: Vec::new(),
}
}
pub fn with_json_config(mut self, json_config: JsonConfig) -> Self {
self.json_config = json_config;
self
}
#[instrument(skip_all, name = "SERVICE_SETUP", level = "debug")]
pub fn register_controller(mut self, controller: impl ResourceController + 'static) -> Self {
trace!("Registering controller for: {}", controller.path());
self.controllers.push(Arc::new(controller));
self
}
pub fn build(&self) -> Scope {
let mut api_scope = Scope::new(&self.path).app_data(self.json_config.clone());
api_scope = self
.controllers
.clone()
.into_iter()
.fold(api_scope, |api_scope, controller| {
api_scope.configure(|cfg| controller.controller_config(cfg))
});
api_scope
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{
App, HttpResponse,
http::StatusCode,
middleware::ErrorHandlers,
test::{self, TestRequest},
web::{self, Json, to},
};
use serde_json::Value;
#[actix_web::test]
async fn json_extractor_configuration() {
let app = test::init_service(
App::new()
.app_data(
JsonConfig::default()
.limit(5)
.content_type_required(true)
.content_type(|c| c == mime::TEXT_PLAIN),
)
.route(
"/foo/bar",
to(|data: Json<String>| async move { format!("Hi {data}") }),
),
)
.await;
let req = TestRequest::post()
.set_json("Hi")
.insert_header(("content-type", "application/json "))
.uri("/foo/bar");
let res = test::call_service(&app, req.to_request()).await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::default().uri("/foo/bar");
let res = test::call_service(
&app,
req.insert_header(("content-type", "application/json"))
.to_request(),
)
.await;
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::post().uri("/foo/bar");
let res = test::call_service(&app, req.set_json("Too large").to_request()).await;
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
assert!(
res.response()
.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap()
.starts_with("text/plain")
);
}
#[actix_web::test]
async fn json_service_errors() {
let test_server = test::init_service(
App::new()
.wrap(ErrorHandlers::new().default_handler(crate::server::to_json_error_response))
.route("/t/notfound", web::to(HttpResponse::NotFound))
.route(
"/t/internalerror",
web::to(HttpResponse::InternalServerError),
)
.route("/t/paymentrequired", web::to(HttpResponse::PaymentRequired)),
)
.await;
let notfound = test::TestRequest::get().uri("/t/notfound").to_request();
let internalerror = test::TestRequest::get()
.uri("/t/internalerror")
.to_request();
let paymentrequired = test::TestRequest::get()
.uri("/t/paymentrequired")
.to_request();
let notfound_response: Value = test::call_and_read_body_json(&test_server, notfound).await;
let paymentrequired_response: Value =
test::call_and_read_body_json(&test_server, paymentrequired).await;
let internalerror_response: Value =
test::call_and_read_body_json(&test_server, internalerror).await;
let notfound_json: serde_json::Value =
serde_json::from_str(r#"{"status": 404,"message":"Not Found"}"#).unwrap();
let internalerror_json: serde_json::Value =
serde_json::from_str(r#"{"status": 500,"message":"Internal Server Error"}"#).unwrap();
let paymentrequired_json: serde_json::Value =
serde_json::from_str(r#"{"status": 402,"message":"Payment Required"}"#).unwrap();
assert_eq!(notfound_json, notfound_response);
assert_eq!(internalerror_json, internalerror_response);
assert_eq!(paymentrequired_json, paymentrequired_response);
}
}