#![deny(missing_docs)]
use async_trait::async_trait;
use js_sys::{Function, Reflect};
use service_logging::{log, LogEntry, LogQueue, Logger, Severity};
use std::cell::RefCell;
use std::fmt;
use wasm_bindgen::JsValue;
mod error;
pub use error::Error;
mod method;
pub use method::Method;
mod request;
pub use request::Request;
mod response;
pub use response::{Body, Response};
mod media_type;
pub use media_type::media_type;
pub use url::Url;
mod context;
pub use context::Context;
mod httpdate;
pub(crate) mod js_values;
pub use httpdate::HttpDate;
#[derive(Debug)]
pub struct RunContext {
pub log_queue: RefCell<LogQueue>,
}
unsafe impl Sync for RunContext {}
impl RunContext {
pub fn log(&self, entry: LogEntry) {
self.log_queue.borrow_mut().log(entry);
}
}
#[async_trait]
pub trait Runnable {
async fn run(&self, ctx: &RunContext);
}
#[derive(Clone, Debug)]
pub struct HandlerReturn {
pub status: u16,
pub text: String,
}
impl fmt::Display for HandlerReturn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({},{})", self.status, self.text)
}
}
pub fn handler_return(status: u16, text: &str) -> HandlerReturn {
HandlerReturn {
status,
text: text.to_string(),
}
}
impl Default for HandlerReturn {
fn default() -> Self {
Self {
status: 200,
text: String::default(),
}
}
}
#[async_trait(?Send)]
pub trait Handler {
async fn handle(&self, req: &Request, ctx: &mut Context) -> Result<(), HandlerReturn>;
}
pub struct ServiceConfig {
pub logger: Box<dyn Logger>,
pub handlers: Vec<Box<dyn Handler>>,
pub internal_error_handler: fn(req: &Request, ctx: &mut Context),
pub not_found_handler: fn(req: &Request, ctx: &mut Context),
}
impl Default for ServiceConfig {
fn default() -> ServiceConfig {
ServiceConfig {
logger: service_logging::silent_logger(),
handlers: Vec::new(),
internal_error_handler: default_internal_error_handler,
not_found_handler: default_not_found_handler,
}
}
}
struct DeferredData {
tasks: Vec<Box<dyn Runnable + std::panic::UnwindSafe>>,
logs: Vec<LogEntry>,
logger: Box<dyn Logger>,
}
pub async fn service_request(req: JsValue, config: ServiceConfig) -> Result<JsValue, JsValue> {
let mut is_err = false;
let map = js_sys::Map::from(req);
let req = Request::from_js(&map)?;
let mut ctx = Context::default();
let mut handler_result = Ok(());
for handler in config.handlers.iter() {
handler_result = handler.handle(&req, &mut ctx).await;
if ctx.is_internal_error().is_some() {
(config.internal_error_handler)(&req, &mut ctx);
is_err = true;
break;
}
if handler_result.is_err() || !ctx.response().is_unset() {
break;
}
}
if let Err(result) = handler_result {
ctx.response().status(result.status).text(result.text);
} else if ctx.response().is_unset() {
(config.not_found_handler)(&req, &mut ctx);
}
let response = ctx.take_response();
if response.get_status() < 200 || response.get_status() > 307 {
is_err = true;
}
let severity = if response.get_status() == 404 {
Severity::Warning
} else if is_err {
Severity::Error
} else {
Severity::Info
};
log!(ctx, severity, _:"service", method: req.method(), url: req.url(), status: response.get_status());
if is_err {
let _ = config
.logger
.send("http", ctx.take_logs())
.await
.map_err(|e| {
ctx.response()
.header("X-service-log-err-ret", e.to_string())
.unwrap()
});
} else {
let js_event =
js_sys::Object::from(check_defined(map.get(&"event".into()), "missing event")?);
let wait_func = Function::from(
Reflect::get(&js_event, &JsValue::from_str("waitUntil"))
.map_err(|_| "event without waitUntil")?,
);
let promise = deferred_promise(Box::new(DeferredData {
tasks: ctx.take_tasks(),
logs: ctx.take_logs(),
logger: config.logger,
}));
let _ = wait_func.call1(&js_event, &promise); }
Ok(response.into_js())
}
fn default_internal_error_handler(req: &Request, ctx: &mut Context) {
let error = ctx.is_internal_error();
log!(ctx, Severity::Error, _:"InternalError", url: req.url(),
error: error.map(|e| e.to_string()).unwrap_or_else(|| String::from("none")));
ctx.response()
.status(200)
.content_type(mime::TEXT_PLAIN_UTF_8)
.unwrap()
.text("Sorry, an internal error has occurred. It has been logged.");
}
pub fn default_not_found_handler(req: &Request, ctx: &mut Context) {
log!(ctx, Severity::Info, _:"NotFound", url: req.url());
ctx.response()
.status(404)
.content_type(mime::TEXT_PLAIN_UTF_8)
.unwrap()
.text("Not Found");
}
fn deferred_promise(args: Box<DeferredData>) -> js_sys::Promise {
wasm_bindgen_futures::future_to_promise(async move {
if let Err(e) = args.logger.send("http", args.logs).await {
log_log_error(e);
}
let log_queue = RefCell::new(LogQueue::default());
let run_ctx = RunContext { log_queue };
for t in args.tasks.iter() {
t.run(&run_ctx).await;
}
let logs = run_ctx.log_queue.borrow_mut().take();
if let Err(e) = args.logger.send("http", logs).await {
log_log_error(e);
}
Ok(JsValue::undefined())
})
}
fn check_defined(v: JsValue, msg: &str) -> Result<JsValue, JsValue> {
if v.is_undefined() {
return Err(JsValue::from_str(msg));
}
Ok(v)
}
fn log_log_error(e: Box<dyn std::error::Error>) {
web_sys::console::log_1(&wasm_bindgen::JsValue::from_str(&format!(
"Error sending logs: {:?}",
e
)))
}