Implement server response config and full symmetry between request and response methods

This commit is contained in:
Filip Tibell 2025-04-27 12:38:27 +02:00
parent 1e4b020d84
commit a0f55bc1ec
5 changed files with 250 additions and 34 deletions

View file

@ -0,0 +1,65 @@
use std::collections::HashMap;
use bstr::{BString, ByteSlice};
use hyper::{header::CONTENT_TYPE, StatusCode};
use mlua::prelude::*;
use crate::shared::headers::table_to_hash_map;
#[derive(Debug, Clone)]
pub struct ResponseConfig {
pub status: StatusCode,
pub headers: HashMap<String, Vec<String>>,
pub body: Option<Vec<u8>>,
}
impl FromLua for ResponseConfig {
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
// If we just got a string we assume its a plaintext 200 response
if let LuaValue::String(s) = value {
Ok(Self {
status: StatusCode::OK,
headers: HashMap::from([(
CONTENT_TYPE.to_string(),
vec!["text/plain".to_string()],
)]),
body: Some(s.as_bytes().to_owned()),
})
} else if let LuaValue::Table(tab) = value {
// If we got a table we are able to configure the entire response
// Extract url
let status = match tab.get::<u16>("status") {
Ok(status) => Ok(StatusCode::from_u16(status).into_lua_err()?),
Err(_) => Err(LuaError::runtime("Missing 'status' in response config")),
}?;
// Extract headers
let headers = match tab.get::<LuaTable>("headers") {
Ok(tab) => table_to_hash_map(tab, "headers")?,
Err(_) => HashMap::new(),
};
// Extract body
let body = match tab.get::<BString>("body") {
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
Err(_) => None,
};
// All good, validated and we got what we need
Ok(Self {
status,
headers,
body,
})
} else {
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ResponseConfig".to_string(),
message: Some(format!(
"Invalid response config - expected string or table, got {}",
value.type_name()
)),
})
}
}
}

View file

@ -0,0 +1 @@
pub mod config;

View file

@ -28,27 +28,50 @@ pub fn header_map_to_table(
headers: HeaderMap,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut res_headers = HashMap::<String, Vec<String>>::new();
for (name, value) in &headers {
let name = name.as_str();
let value = value.to_str().unwrap().to_owned();
if let Some(existing) = res_headers.get_mut(name) {
existing.push(value);
} else {
res_headers.insert(name.to_owned(), vec![value]);
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, value) in headers {
if let Some(name) = name {
if let Ok(value) = value.to_str() {
string_map
.entry(name.to_string())
.or_default()
.push(value.to_owned());
}
}
}
if remove_content_headers {
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
res_headers.retain(|name, _| {
name != content_encoding_header_str && name != content_length_header_str
});
hash_map_to_table(lua, string_map, remove_content_headers)
}
pub fn hash_map_to_table(
lua: &Lua,
map: impl IntoIterator<Item = (String, Vec<String>)>,
remove_content_headers: bool,
) -> LuaResult<LuaTable> {
let mut string_map = HashMap::<String, Vec<String>>::new();
for (name, values) in map {
let name = name.as_str();
if remove_content_headers {
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
if name == content_encoding_header_str || name == content_length_header_str {
continue;
}
}
for value in values {
let value = value.as_str();
string_map
.entry(name.to_owned())
.or_default()
.push(value.to_owned());
}
}
let mut builder = TableBuilder::new(lua.clone())?;
for (name, mut values) in res_headers {
for (name, mut values) in string_map {
if values.len() == 1 {
let value = values.pop().unwrap().into_lua(lua)?;
builder = builder.with_value(name, value)?;

View file

@ -1,15 +1,21 @@
use http_body_util::Full;
use std::collections::HashMap;
use futures_lite::prelude::*;
use http_body_util::{BodyStream, Full};
use url::Url;
use hyper::{
body::Bytes,
body::{Body as _, Bytes, Incoming},
header::{HeaderName, HeaderValue, USER_AGENT},
HeaderMap, Request as HyperRequest,
HeaderMap, Method, Request as HyperRequest,
};
use mlua::prelude::*;
use crate::{client::config::RequestConfig, shared::headers::create_user_agent_header};
use crate::{
client::config::RequestConfig,
shared::headers::{create_user_agent_header, hash_map_to_table, header_map_to_table},
};
#[derive(Debug, Clone)]
pub struct Request {
@ -72,6 +78,81 @@ impl Request {
})
}
/**
Creates a new request from a raw incoming request.
*/
pub async fn from_incoming(
incoming: HyperRequest<Incoming>,
decompress: bool,
) -> LuaResult<Self> {
let (parts, body) = incoming.into_parts();
let size = body.size_hint().lower() as usize;
let buffer = Vec::<u8>::with_capacity(size);
let body = BodyStream::new(body)
.try_fold(buffer, |mut body, chunk| {
if let Some(chunk) = chunk.data_ref() {
body.extend_from_slice(chunk);
}
Ok(body)
})
.await
.into_lua_err()?;
// TODO: Decompress body if decompress is true and headers are present
Ok(Self {
inner: HyperRequest::from_parts(parts, Bytes::from(body)),
redirects: 0,
decompress,
})
}
/**
Returns the method of the request.
*/
pub fn method(&self) -> Method {
self.inner.method().clone()
}
/**
Returns the path of the request.
*/
pub fn path(&self) -> &str {
self.inner.uri().path()
}
/**
Returns the query parameters of the request.
*/
pub fn query(&self) -> HashMap<String, Vec<String>> {
let uri = self.inner.uri();
let url = uri.to_string().parse::<Url>().expect("uri is valid");
let mut result = HashMap::<String, Vec<String>>::new();
for (key, value) in url.query_pairs() {
result
.entry(key.into_owned())
.or_default()
.push(value.into_owned());
}
result
}
/**
Returns the headers of the request.
*/
pub fn headers(&self) -> &HeaderMap {
self.inner.headers()
}
/**
Returns the body of the request.
*/
pub fn body(&self) -> &[u8] {
self.inner.body()
}
/**
Returns the inner `hyper` request with its body
type modified to `Full<Bytes>` for sending.
@ -82,10 +163,10 @@ impl Request {
.method(self.inner.method())
.uri(self.inner.uri());
let headers = builder.headers_mut().expect("request was valid");
for (name, value) in self.inner.headers() {
headers.insert(name, value.clone());
}
builder
.headers_mut()
.expect("request was valid")
.extend(self.inner.headers().clone());
let body = Full::new(self.inner.body().clone());
builder.body(body).expect("request was valid")
@ -101,3 +182,17 @@ fn add_default_headers(lua: &Lua, headers: &mut HeaderMap) -> LuaResult<()> {
Ok(())
}
impl LuaUserData for Request {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("method", |_, this| Ok(this.method().to_string()));
fields.add_field_method_get("path", |_, this| Ok(this.path().to_string()));
fields.add_field_method_get("query", |lua, this| {
hash_map_to_table(lua, this.query(), false)
});
fields.add_field_method_get("headers", |lua, this| {
header_map_to_table(lua, this.headers().clone(), this.decompress)
});
fields.add_field_method_get("body", |lua, this| lua.create_string(this.body()));
}
}

View file

@ -3,12 +3,13 @@ use http_body_util::{BodyStream, Full};
use hyper::{
body::{Body, Bytes, Incoming},
header::{HeaderName, HeaderValue},
HeaderMap, Response as HyperResponse,
};
use mlua::prelude::*;
use crate::shared::headers::header_map_to_table;
use crate::{server::config::ResponseConfig, shared::headers::header_map_to_table};
#[derive(Debug, Clone)]
pub struct Response {
@ -19,12 +20,42 @@ pub struct Response {
}
impl Response {
/**
Creates a new response that is ready to be sent from a response configuration.
*/
pub fn from_config(config: ResponseConfig, _lua: Lua) -> LuaResult<Self> {
// 1. Create the inner response builder
let mut builder = HyperResponse::builder().status(config.status);
// 2. Append any headers passed as a table - builder
// headers may be None if builder is already invalid
if let Some(headers) = builder.headers_mut() {
for (key, values) in config.headers {
let key = HeaderName::from_bytes(key.as_bytes()).into_lua_err()?;
for value in values {
let value = HeaderValue::from_str(&value).into_lua_err()?;
headers.insert(key.clone(), value);
}
}
}
// 3. Convert response body bytes to the proper Body
// type that Hyper expects, if we got any bytes
let body = config.body.map(Bytes::from).unwrap_or_default();
// 4. Finally, attach the body, verifying that the response is valid
Ok(Self {
inner: builder.body(body).into_lua_err()?,
decompressed: false,
})
}
/**
Creates a new response from a raw incoming response.
*/
pub async fn from_incoming(
incoming: HyperResponse<Incoming>,
decompressed: bool,
decompress: bool,
) -> LuaResult<Self> {
let (parts, body) = incoming.into_parts();
@ -40,12 +71,11 @@ impl Response {
.await
.into_lua_err()?;
let bytes = Bytes::from(body);
let inner = HyperResponse::from_parts(parts, bytes);
// TODO: Decompress body if decompress is true and headers are present
Ok(Self {
inner,
decompressed,
inner: HyperResponse::from_parts(parts, Bytes::from(body)),
decompressed: decompress,
})
}
@ -89,12 +119,14 @@ impl Response {
type modified to `Full<Bytes>` for sending.
*/
pub fn as_full(&self) -> HyperResponse<Full<Bytes>> {
let mut builder = HyperResponse::builder().version(self.inner.version());
let mut builder = HyperResponse::builder()
.version(self.inner.version())
.status(self.inner.status());
let headers = builder.headers_mut().expect("request was valid");
for (name, value) in self.inner.headers() {
headers.insert(name, value.clone());
}
builder
.headers_mut()
.expect("request was valid")
.extend(self.inner.headers().clone());
let body = Full::new(self.inner.body().clone());
builder.body(body).expect("request was valid")