use crate::status;
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::io::{self, Write};
#[derive(Debug, Default)]
pub struct FcgiContext {
pub(crate) method: String,
pub(crate) path: String,
pub(crate) query: BTreeMap<String, String>,
pub(crate) incoming_headers: BTreeMap<String, String>,
pub(crate) incoming_body: Vec<u8>,
pub(crate) outgoing_headers: BTreeMap<String, String>,
pub(crate) outgoing_body: Vec<u8>,
pub(crate) typemap: BTreeMap<TypeId, Box<dyn Any>>,
pub(crate) halted: bool,
}
impl FcgiContext {
pub fn add_data<T: Any + 'static>(&mut self, value: T) {
self.typemap.insert(value.type_id(), Box::new(value));
}
pub fn get_data<T: Any + 'static>(&self) -> Option<&T> {
self.typemap
.get(&TypeId::of::<T>())
.and_then(|b| b.downcast_ref::<T>())
}
pub fn get_mut_data<T: Any + 'static>(&mut self) -> Option<&mut T> {
self.typemap
.get_mut(&TypeId::of::<T>())
.and_then(|b| b.downcast_mut::<T>())
}
pub fn method(&self) -> &str {
self.method.as_str()
}
pub fn path(&self) -> &str {
self.path.as_str()
}
pub fn query_value(&self, key: &str) -> Option<&str> {
self.query.get(key).map(String::as_str)
}
pub fn get_header(&self, header: &str) -> Option<&str> {
self.incoming_headers.get(header).map(String::as_str)
}
pub fn body(&self) -> &[u8] {
self.incoming_body.as_slice()
}
pub fn halt(mut self) -> Self {
self.halted = true;
self
}
pub fn with_content_type<S: Into<String>>(self, content_type: S) -> Self {
self.with_header("Content-Type", content_type)
}
pub fn with_status(self, code: u16) -> Self {
self.with_header("Status", code.to_string())
}
pub fn with_location<S: Into<String>>(self, location: S) -> Self {
self.with_header("Location", location)
}
pub fn with_body<S: Into<String>>(mut self, body: S) -> Self {
self.outgoing_body = body.into().into_bytes();
self
}
pub fn with_raw_body<S: Into<Vec<u8>>>(mut self, body: S) -> Self {
self.outgoing_body = body.into();
self
}
pub fn with_html_body<S: Into<String>>(self, html: S) -> Self {
self.with_content_type("text/html").with_body(html)
}
pub fn with_json_body<S: Into<String>>(self, json: S) -> Self {
self.with_content_type("application/json").with_body(json)
}
pub fn with_permanent_redirect<S: Into<String>>(self, path: S) -> Self {
self.with_status(status::PERMANENT_REDIRECT)
.with_location(path)
}
pub fn with_temporary_redirect<S: Into<String>>(self, path: S) -> Self {
self.with_status(status::TEMPORARY_REDIRECT)
.with_location(path)
}
pub fn with_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.outgoing_headers.insert(key.into(), value.into());
self
}
}
impl FcgiContext {
pub(crate) fn write_stdout_bytes<W: Write>(&self, writer: &mut W) -> Result<(), io::Error> {
for (key, value) in self.outgoing_headers.iter() {
writeln!(writer, "{key}: {value}")?;
}
writeln!(writer)?;
writer.write_all(&self.outgoing_body)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn assert_serialized(ctx: FcgiContext, expected: &str) {
let mut buf = vec![];
ctx.write_stdout_bytes(&mut buf).unwrap();
assert_eq!(String::from_utf8_lossy(&buf), expected);
}
#[test]
fn setting_the_status() {
assert_serialized(FcgiContext::default().with_status(400), "Status: 400\n\n");
}
#[test]
fn setting_the_location() {
assert_serialized(
FcgiContext::default().with_location("/path"),
"Location: /path\n\n",
)
}
#[test]
fn setting_redirects() {
assert_serialized(
FcgiContext::default().with_temporary_redirect("/path"),
"Location: /path\nStatus: 307\n\n",
);
assert_serialized(
FcgiContext::default().with_permanent_redirect("/path"),
"Location: /path\nStatus: 308\n\n",
);
}
#[test]
fn setting_the_content_type() {
assert_serialized(
FcgiContext::default().with_content_type("text/pre"),
"Content-Type: text/pre\n\n",
);
}
#[test]
fn setting_body() {
assert_serialized(FcgiContext::default().with_body("hello"), "\nhello")
}
#[test]
fn setting_html_body() {
assert_serialized(
FcgiContext::default().with_html_body("<div></div>"),
"Content-Type: text/html\n\n<div></div>",
)
}
#[test]
fn setting_json_body() {
assert_serialized(
FcgiContext::default().with_json_body("{}"),
"Content-Type: application/json\n\n{}",
);
}
}