mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Implement server response config and full symmetry between request and response methods
This commit is contained in:
parent
1e4b020d84
commit
a0f55bc1ec
5 changed files with 250 additions and 34 deletions
65
crates/lune-std-net/src/server/config.rs
Normal file
65
crates/lune-std-net/src/server/config.rs
Normal 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()
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
pub mod config;
|
|
@ -28,27 +28,50 @@ pub fn header_map_to_table(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
remove_content_headers: bool,
|
remove_content_headers: bool,
|
||||||
) -> LuaResult<LuaTable> {
|
) -> LuaResult<LuaTable> {
|
||||||
let mut res_headers = HashMap::<String, Vec<String>>::new();
|
let mut string_map = HashMap::<String, Vec<String>>::new();
|
||||||
for (name, value) in &headers {
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if remove_content_headers {
|
if remove_content_headers {
|
||||||
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
||||||
let content_length_header_str = CONTENT_LENGTH.as_str();
|
let content_length_header_str = CONTENT_LENGTH.as_str();
|
||||||
res_headers.retain(|name, _| {
|
if name == content_encoding_header_str || name == content_length_header_str {
|
||||||
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())?;
|
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 {
|
if values.len() == 1 {
|
||||||
let value = values.pop().unwrap().into_lua(lua)?;
|
let value = values.pop().unwrap().into_lua(lua)?;
|
||||||
builder = builder.with_value(name, value)?;
|
builder = builder.with_value(name, value)?;
|
||||||
|
|
|
@ -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 url::Url;
|
||||||
|
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::Bytes,
|
body::{Body as _, Bytes, Incoming},
|
||||||
header::{HeaderName, HeaderValue, USER_AGENT},
|
header::{HeaderName, HeaderValue, USER_AGENT},
|
||||||
HeaderMap, Request as HyperRequest,
|
HeaderMap, Method, Request as HyperRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request {
|
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
|
Returns the inner `hyper` request with its body
|
||||||
type modified to `Full<Bytes>` for sending.
|
type modified to `Full<Bytes>` for sending.
|
||||||
|
@ -82,10 +163,10 @@ impl Request {
|
||||||
.method(self.inner.method())
|
.method(self.inner.method())
|
||||||
.uri(self.inner.uri());
|
.uri(self.inner.uri());
|
||||||
|
|
||||||
let headers = builder.headers_mut().expect("request was valid");
|
builder
|
||||||
for (name, value) in self.inner.headers() {
|
.headers_mut()
|
||||||
headers.insert(name, value.clone());
|
.expect("request was valid")
|
||||||
}
|
.extend(self.inner.headers().clone());
|
||||||
|
|
||||||
let body = Full::new(self.inner.body().clone());
|
let body = Full::new(self.inner.body().clone());
|
||||||
builder.body(body).expect("request was valid")
|
builder.body(body).expect("request was valid")
|
||||||
|
@ -101,3 +182,17 @@ fn add_default_headers(lua: &Lua, headers: &mut HeaderMap) -> LuaResult<()> {
|
||||||
|
|
||||||
Ok(())
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,12 +3,13 @@ use http_body_util::{BodyStream, Full};
|
||||||
|
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{Body, Bytes, Incoming},
|
body::{Body, Bytes, Incoming},
|
||||||
|
header::{HeaderName, HeaderValue},
|
||||||
HeaderMap, Response as HyperResponse,
|
HeaderMap, Response as HyperResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Response {
|
pub struct Response {
|
||||||
|
@ -19,12 +20,42 @@ pub struct Response {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
Creates a new response from a raw incoming response.
|
||||||
*/
|
*/
|
||||||
pub async fn from_incoming(
|
pub async fn from_incoming(
|
||||||
incoming: HyperResponse<Incoming>,
|
incoming: HyperResponse<Incoming>,
|
||||||
decompressed: bool,
|
decompress: bool,
|
||||||
) -> LuaResult<Self> {
|
) -> LuaResult<Self> {
|
||||||
let (parts, body) = incoming.into_parts();
|
let (parts, body) = incoming.into_parts();
|
||||||
|
|
||||||
|
@ -40,12 +71,11 @@ impl Response {
|
||||||
.await
|
.await
|
||||||
.into_lua_err()?;
|
.into_lua_err()?;
|
||||||
|
|
||||||
let bytes = Bytes::from(body);
|
// TODO: Decompress body if decompress is true and headers are present
|
||||||
let inner = HyperResponse::from_parts(parts, bytes);
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
inner,
|
inner: HyperResponse::from_parts(parts, Bytes::from(body)),
|
||||||
decompressed,
|
decompressed: decompress,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,12 +119,14 @@ impl Response {
|
||||||
type modified to `Full<Bytes>` for sending.
|
type modified to `Full<Bytes>` for sending.
|
||||||
*/
|
*/
|
||||||
pub fn as_full(&self) -> HyperResponse<Full<Bytes>> {
|
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");
|
builder
|
||||||
for (name, value) in self.inner.headers() {
|
.headers_mut()
|
||||||
headers.insert(name, value.clone());
|
.expect("request was valid")
|
||||||
}
|
.extend(self.inner.headers().clone());
|
||||||
|
|
||||||
let body = Full::new(self.inner.body().clone());
|
let body = Full::new(self.inner.body().clone());
|
||||||
builder.body(body).expect("request was valid")
|
builder.body(body).expect("request was valid")
|
||||||
|
|
Loading…
Add table
Reference in a new issue