mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Make request and response implement FromLua directly instead of separate config
This commit is contained in:
parent
1915441ee6
commit
9f6a1532a6
11 changed files with 252 additions and 349 deletions
|
@ -3,7 +3,6 @@
|
||||||
|
|
||||||
local net = require("@lune/net")
|
local net = require("@lune/net")
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
local task = require("@lune/task")
|
|
||||||
|
|
||||||
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||||
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
then assert(tonumber(process.env.PORT), "Failed to parse port from env")
|
||||||
|
@ -11,6 +10,10 @@ local PORT = if process.env.PORT ~= nil and #process.env.PORT > 0
|
||||||
|
|
||||||
-- Create our responder functions
|
-- Create our responder functions
|
||||||
|
|
||||||
|
local function root(_request: net.ServeRequest): string
|
||||||
|
return `Hello from Lune server!`
|
||||||
|
end
|
||||||
|
|
||||||
local function pong(request: net.ServeRequest): string
|
local function pong(request: net.ServeRequest): string
|
||||||
return `Pong!\n{request.path}\n{request.body}`
|
return `Pong!\n{request.path}\n{request.body}`
|
||||||
end
|
end
|
||||||
|
@ -29,10 +32,12 @@ local function notFound(_request: net.ServeRequest): net.ServeResponse
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Run the server on port 8080
|
-- Run the server on the port forever
|
||||||
|
|
||||||
local handle = net.serve(PORT, function(request)
|
net.serve(PORT, function(request)
|
||||||
if string.sub(request.path, 1, 5) == "/ping" then
|
if request.path == "/" then
|
||||||
|
return root(request)
|
||||||
|
elseif string.sub(request.path, 1, 5) == "/ping" then
|
||||||
return pong(request)
|
return pong(request)
|
||||||
elseif string.sub(request.path, 1, 7) == "/teapot" then
|
elseif string.sub(request.path, 1, 7) == "/teapot" then
|
||||||
return teapot(request)
|
return teapot(request)
|
||||||
|
@ -42,12 +47,4 @@ local handle = net.serve(PORT, function(request)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
print(`Listening on port {PORT} 🚀`)
|
print(`Listening on port {PORT} 🚀`)
|
||||||
|
print("Press Ctrl+C to stop")
|
||||||
-- Exit our example after a small delay, if you copy this
|
|
||||||
-- example just remove this part to keep the server running
|
|
||||||
|
|
||||||
task.delay(2, function()
|
|
||||||
print("Shutting down...")
|
|
||||||
task.wait(1)
|
|
||||||
handle:stop()
|
|
||||||
end)
|
|
||||||
|
|
|
@ -1,154 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use hyper::{header::USER_AGENT, Method};
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
use crate::shared::headers::{create_user_agent_header, table_to_hash_map};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RequestConfigOptions {
|
|
||||||
pub decompress: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for RequestConfigOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { decompress: true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for RequestConfigOptions {
|
|
||||||
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
|
||||||
if let LuaValue::Nil = value {
|
|
||||||
// Nil means default options
|
|
||||||
Ok(Self::default())
|
|
||||||
} else if let LuaValue::Table(tab) = value {
|
|
||||||
// Table means custom options
|
|
||||||
let decompress = match tab.get::<Option<bool>>("decompress") {
|
|
||||||
Ok(decomp) => Ok(decomp.unwrap_or(true)),
|
|
||||||
Err(_) => Err(LuaError::RuntimeError(
|
|
||||||
"Invalid option value for 'decompress' in request config options".to_string(),
|
|
||||||
)),
|
|
||||||
}?;
|
|
||||||
Ok(Self { decompress })
|
|
||||||
} else {
|
|
||||||
// Anything else is invalid
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "RequestConfigOptions".to_string(),
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid request config options - expected table or nil, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RequestConfig {
|
|
||||||
pub url: String,
|
|
||||||
pub method: Method,
|
|
||||||
pub query: HashMap<String, Vec<String>>,
|
|
||||||
pub headers: HashMap<String, Vec<String>>,
|
|
||||||
pub body: Option<Vec<u8>>,
|
|
||||||
pub options: RequestConfigOptions,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromLua for RequestConfig {
|
|
||||||
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
|
||||||
// If we just got a string we assume its a GET request to a given url
|
|
||||||
if let LuaValue::String(s) = value {
|
|
||||||
Ok(Self {
|
|
||||||
url: s.to_string_lossy().to_string(),
|
|
||||||
method: Method::GET,
|
|
||||||
query: HashMap::new(),
|
|
||||||
headers: HashMap::new(),
|
|
||||||
body: None,
|
|
||||||
options: RequestConfigOptions::default(),
|
|
||||||
})
|
|
||||||
} else if let LuaValue::Table(tab) = value {
|
|
||||||
// If we got a table we are able to configure the entire request
|
|
||||||
|
|
||||||
// Extract url
|
|
||||||
let url = match tab.get::<LuaString>("url") {
|
|
||||||
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
|
|
||||||
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
|
|
||||||
}?;
|
|
||||||
// Extract method
|
|
||||||
let method = match tab.get::<LuaString>("method") {
|
|
||||||
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
|
|
||||||
Err(_) => "GET".to_string(),
|
|
||||||
};
|
|
||||||
// Extract query
|
|
||||||
let query = match tab.get::<LuaTable>("query") {
|
|
||||||
Ok(tab) => table_to_hash_map(tab, "query")?,
|
|
||||||
Err(_) => HashMap::new(),
|
|
||||||
};
|
|
||||||
// Extract headers
|
|
||||||
let mut 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,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert method string into proper enum
|
|
||||||
let method = method.trim().to_ascii_uppercase();
|
|
||||||
let method = match method.as_ref() {
|
|
||||||
"GET" => Ok(Method::GET),
|
|
||||||
"POST" => Ok(Method::POST),
|
|
||||||
"PUT" => Ok(Method::PUT),
|
|
||||||
"DELETE" => Ok(Method::DELETE),
|
|
||||||
"HEAD" => Ok(Method::HEAD),
|
|
||||||
"OPTIONS" => Ok(Method::OPTIONS),
|
|
||||||
"PATCH" => Ok(Method::PATCH),
|
|
||||||
_ => Err(LuaError::RuntimeError(format!(
|
|
||||||
"Invalid request config method '{}'",
|
|
||||||
&method
|
|
||||||
))),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
// Parse any extra options given
|
|
||||||
let options = match tab.get::<LuaValue>("options") {
|
|
||||||
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
|
|
||||||
Err(_) => RequestConfigOptions::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Finally, add any default headers, if applicable
|
|
||||||
add_default_headers(lua, &mut headers)?;
|
|
||||||
|
|
||||||
// All good, validated and we got what we need
|
|
||||||
Ok(Self {
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
query,
|
|
||||||
headers,
|
|
||||||
body,
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// Anything else is invalid
|
|
||||||
Err(LuaError::FromLuaConversionError {
|
|
||||||
from: value.type_name(),
|
|
||||||
to: "RequestConfig".to_string(),
|
|
||||||
message: Some(format!(
|
|
||||||
"Invalid request config - expected string or table, got {}",
|
|
||||||
value.type_name()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_default_headers(lua: &Lua, headers: &mut HashMap<String, Vec<String>>) -> LuaResult<()> {
|
|
||||||
if !headers.contains_key(USER_AGENT.as_str()) {
|
|
||||||
let ua = create_user_agent_header(lua)?;
|
|
||||||
headers.insert(USER_AGENT.to_string(), vec![ua]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -18,7 +18,6 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod config;
|
|
||||||
pub mod http_stream;
|
pub mod http_stream;
|
||||||
pub mod rustls;
|
pub mod rustls;
|
||||||
pub mod ws_stream;
|
pub mod ws_stream;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#![allow(clippy::cargo_common_metadata)]
|
#![allow(clippy::cargo_common_metadata)]
|
||||||
|
|
||||||
|
use hyper::header::{HeaderValue, USER_AGENT};
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::TableBuilder;
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
@ -9,9 +10,12 @@ pub(crate) mod shared;
|
||||||
pub(crate) mod url;
|
pub(crate) mod url;
|
||||||
|
|
||||||
use self::{
|
use self::{
|
||||||
client::{config::RequestConfig, ws_stream::WsStream},
|
client::ws_stream::WsStream,
|
||||||
server::{config::ServeConfig, handle::ServeHandle},
|
server::{config::ServeConfig, handle::ServeHandle},
|
||||||
shared::{request::Request, response::Response, websocket::Websocket},
|
shared::{
|
||||||
|
headers::create_user_agent_header, request::Request, response::Response,
|
||||||
|
websocket::Websocket,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
const TYPEDEFS: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/types.d.luau"));
|
||||||
|
@ -41,8 +45,13 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn net_request(lua: Lua, config: RequestConfig) -> LuaResult<Response> {
|
async fn net_request(lua: Lua, mut req: Request) -> LuaResult<Response> {
|
||||||
self::client::send_request(Request::try_from(config)?, lua).await
|
if !req.headers().contains_key(USER_AGENT.as_str()) {
|
||||||
|
let ua = create_user_agent_header(&lua)?;
|
||||||
|
let ua = HeaderValue::from_str(&ua).into_lua_err()?;
|
||||||
|
req.inner.headers_mut().insert(USER_AGENT, ua);
|
||||||
|
}
|
||||||
|
self::client::send_request(req, lua).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn net_socket(_: Lua, url: String) -> LuaResult<Websocket<WsStream>> {
|
async fn net_socket(_: Lua, url: String) -> LuaResult<Websocket<WsStream>> {
|
||||||
|
|
|
@ -1,14 +1,7 @@
|
||||||
use std::{
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
collections::HashMap,
|
|
||||||
net::{IpAddr, Ipv4Addr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bstr::{BString, ByteSlice};
|
|
||||||
use hyper::{header::CONTENT_TYPE, StatusCode};
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::shared::headers::table_to_hash_map;
|
|
||||||
|
|
||||||
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
const DEFAULT_IP_ADDRESS: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
|
||||||
|
|
||||||
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
|
const WEB_SOCKET_UPDGRADE_REQUEST_HANDLER: &str = r#"
|
||||||
|
@ -21,64 +14,6 @@ return {
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[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()
|
|
||||||
)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ServeConfig {
|
pub struct ServeConfig {
|
||||||
pub address: IpAddr,
|
pub address: IpAddr,
|
||||||
|
|
|
@ -13,7 +13,7 @@ use mlua_luau_scheduler::{LuaSchedulerExt, LuaSpawnExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
server::{
|
server::{
|
||||||
config::{ResponseConfig, ServeConfig},
|
config::ServeConfig,
|
||||||
upgrade::{is_upgrade_request, make_upgrade_response},
|
upgrade::{is_upgrade_request, make_upgrade_response},
|
||||||
},
|
},
|
||||||
shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket},
|
shared::{hyper::HyperIo, request::Request, response::Response, websocket::Websocket},
|
||||||
|
@ -96,8 +96,7 @@ async fn handle_request(
|
||||||
.get_thread_result(thread_id)
|
.get_thread_result(thread_id)
|
||||||
.expect("Missing handler thread result")?;
|
.expect("Missing handler thread result")?;
|
||||||
|
|
||||||
let config = ResponseConfig::from_lua_multi(thread_res, &lua)?;
|
let response = Response::from_lua_multi(thread_res, &lua)?;
|
||||||
let response = Response::try_from(config)?;
|
|
||||||
Ok(response.into_full())
|
Ok(response.into_full())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -86,33 +86,3 @@ pub fn hash_map_to_table(
|
||||||
|
|
||||||
builder.build_readonly()
|
builder.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn table_to_hash_map(
|
|
||||||
tab: LuaTable,
|
|
||||||
tab_origin_key: &'static str,
|
|
||||||
) -> LuaResult<HashMap<String, Vec<String>>> {
|
|
||||||
let mut map = HashMap::new();
|
|
||||||
|
|
||||||
for pair in tab.pairs::<String, LuaValue>() {
|
|
||||||
let (key, value) = pair?;
|
|
||||||
match value {
|
|
||||||
LuaValue::String(s) => {
|
|
||||||
map.insert(key, vec![s.to_str()?.to_owned()]);
|
|
||||||
}
|
|
||||||
LuaValue::Table(t) => {
|
|
||||||
let mut values = Vec::new();
|
|
||||||
for value in t.sequence_values::<LuaString>() {
|
|
||||||
values.push(value?.to_str()?.to_owned());
|
|
||||||
}
|
|
||||||
map.insert(key, values);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(LuaError::runtime(format!(
|
|
||||||
"Value for '{tab_origin_key}' must be a string or array of strings",
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(map)
|
|
||||||
}
|
|
||||||
|
|
57
crates/lune-std-net/src/shared/lua.rs
Normal file
57
crates/lune-std-net/src/shared/lua.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
use hyper::{
|
||||||
|
body::Bytes,
|
||||||
|
header::{HeaderName, HeaderValue},
|
||||||
|
HeaderMap, Method,
|
||||||
|
};
|
||||||
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
pub fn lua_value_to_bytes(value: &LuaValue) -> LuaResult<Bytes> {
|
||||||
|
match value {
|
||||||
|
LuaValue::Nil => Ok(Bytes::new()),
|
||||||
|
LuaValue::Buffer(buf) => Ok(Bytes::from(buf.to_vec())),
|
||||||
|
LuaValue::String(str) => Ok(Bytes::copy_from_slice(&str.as_bytes())),
|
||||||
|
v => Err(LuaError::FromLuaConversionError {
|
||||||
|
from: v.type_name(),
|
||||||
|
to: "Bytes".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Invalid body - expected string or buffer, got {}",
|
||||||
|
v.type_name()
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lua_value_to_method(value: &LuaValue) -> LuaResult<Method> {
|
||||||
|
match value {
|
||||||
|
LuaValue::Nil => Ok(Method::GET),
|
||||||
|
LuaValue::String(str) => {
|
||||||
|
let bytes = str.as_bytes().trim_ascii().to_ascii_uppercase();
|
||||||
|
Method::from_bytes(&bytes).into_lua_err()
|
||||||
|
}
|
||||||
|
LuaValue::Buffer(buf) => {
|
||||||
|
let bytes = buf.to_vec().trim_ascii().to_ascii_uppercase();
|
||||||
|
Method::from_bytes(&bytes).into_lua_err()
|
||||||
|
}
|
||||||
|
v => Err(LuaError::FromLuaConversionError {
|
||||||
|
from: v.type_name(),
|
||||||
|
to: "Method".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Invalid method - expected string or buffer, got {}",
|
||||||
|
v.type_name()
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lua_table_to_header_map(table: &LuaTable) -> LuaResult<HeaderMap> {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
|
||||||
|
for pair in table.pairs::<LuaString, LuaString>() {
|
||||||
|
let (key, val) = pair?;
|
||||||
|
let key = HeaderName::from_bytes(&key.as_bytes()).into_lua_err()?;
|
||||||
|
let val = HeaderValue::from_bytes(&val.as_bytes()).into_lua_err()?;
|
||||||
|
headers.insert(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(headers)
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ pub mod futures;
|
||||||
pub mod headers;
|
pub mod headers;
|
||||||
pub mod hyper;
|
pub mod hyper;
|
||||||
pub mod incoming;
|
pub mod incoming;
|
||||||
|
pub mod lua;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod response;
|
pub mod response;
|
||||||
pub mod websocket;
|
pub mod websocket;
|
||||||
|
|
|
@ -5,20 +5,56 @@ use url::Url;
|
||||||
|
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{Bytes, Incoming},
|
body::{Bytes, Incoming},
|
||||||
header::{HeaderName, HeaderValue},
|
|
||||||
HeaderMap, Method, Request as HyperRequest,
|
HeaderMap, Method, Request as HyperRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::shared::{
|
||||||
client::config::RequestConfig,
|
headers::{hash_map_to_table, header_map_to_table},
|
||||||
shared::{
|
incoming::handle_incoming_body,
|
||||||
headers::{hash_map_to_table, header_map_to_table},
|
lua::{lua_table_to_header_map, lua_value_to_bytes, lua_value_to_method},
|
||||||
incoming::handle_incoming_body,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RequestOptions {
|
||||||
|
pub decompress: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RequestOptions {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { decompress: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromLua for RequestOptions {
|
||||||
|
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||||
|
if let LuaValue::Nil = value {
|
||||||
|
// Nil means default options
|
||||||
|
Ok(Self::default())
|
||||||
|
} else if let LuaValue::Table(tab) = value {
|
||||||
|
// Table means custom options
|
||||||
|
let decompress = match tab.get::<Option<bool>>("decompress") {
|
||||||
|
Ok(decomp) => Ok(decomp.unwrap_or(true)),
|
||||||
|
Err(_) => Err(LuaError::RuntimeError(
|
||||||
|
"Invalid option value for 'decompress' in request options".to_string(),
|
||||||
|
)),
|
||||||
|
}?;
|
||||||
|
Ok(Self { decompress })
|
||||||
|
} else {
|
||||||
|
// Anything else is invalid
|
||||||
|
Err(LuaError::FromLuaConversionError {
|
||||||
|
from: value.type_name(),
|
||||||
|
to: "RequestOptions".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Invalid request options - expected table or nil, got {}",
|
||||||
|
value.type_name()
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
// NOTE: We use Bytes instead of Full<Bytes> to avoid
|
// NOTE: We use Bytes instead of Full<Bytes> to avoid
|
||||||
|
@ -135,52 +171,83 @@ impl Request {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<RequestConfig> for Request {
|
impl FromLua for Request {
|
||||||
type Error = LuaError;
|
fn from_lua(value: LuaValue, lua: &Lua) -> LuaResult<Self> {
|
||||||
fn try_from(config: RequestConfig) -> Result<Self, Self::Error> {
|
if let LuaValue::String(s) = value {
|
||||||
// 1. Parse the URL and make sure it is valid
|
// If we just got a string we assume
|
||||||
let mut url = Url::parse(&config.url).into_lua_err()?;
|
// its a GET request to a given url
|
||||||
|
let uri = s.to_str()?;
|
||||||
|
let uri = uri.parse().into_lua_err()?;
|
||||||
|
|
||||||
// 2. Append any query pairs passed as a table
|
let mut request = HyperRequest::new(Bytes::new());
|
||||||
{
|
*request.uri_mut() = uri;
|
||||||
let mut query = url.query_pairs_mut();
|
|
||||||
for (key, values) in config.query {
|
Ok(Self {
|
||||||
for value in values {
|
inner: request,
|
||||||
|
address: None,
|
||||||
|
redirects: None,
|
||||||
|
decompress: RequestOptions::default().decompress,
|
||||||
|
})
|
||||||
|
} else if let LuaValue::Table(tab) = value {
|
||||||
|
// If we got a table we are able to configure the
|
||||||
|
// entire request, maybe with extra options too
|
||||||
|
let options = match tab.get::<LuaValue>("options") {
|
||||||
|
Ok(opts) => RequestOptions::from_lua(opts, lua)?,
|
||||||
|
Err(_) => RequestOptions::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract url (required) + optional structured query params
|
||||||
|
let url = tab.get::<LuaString>("url")?;
|
||||||
|
let mut url = url.to_str()?.parse::<Url>().into_lua_err()?;
|
||||||
|
if let Some(t) = tab.get::<Option<LuaTable>>("query")? {
|
||||||
|
let mut query = url.query_pairs_mut();
|
||||||
|
for pair in t.pairs::<LuaString, LuaString>() {
|
||||||
|
let (key, value) = pair?;
|
||||||
|
let key = key.to_str()?;
|
||||||
|
let value = value.to_str()?;
|
||||||
query.append_pair(&key, &value);
|
query.append_pair(&key, &value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract method
|
||||||
|
let method = tab.get::<LuaValue>("method")?;
|
||||||
|
let method = lua_value_to_method(&method)?;
|
||||||
|
|
||||||
|
// Extract headers
|
||||||
|
let headers = tab.get::<Option<LuaTable>>("headers")?;
|
||||||
|
let headers = headers
|
||||||
|
.map(|t| lua_table_to_header_map(&t))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Extract body
|
||||||
|
let body = tab.get::<LuaValue>("body")?;
|
||||||
|
let body = lua_value_to_bytes(&body)?;
|
||||||
|
|
||||||
|
// Build the full request
|
||||||
|
let mut request = HyperRequest::new(body);
|
||||||
|
request.headers_mut().extend(headers);
|
||||||
|
*request.uri_mut() = url.to_string().parse().unwrap();
|
||||||
|
*request.method_mut() = method;
|
||||||
|
|
||||||
|
// All good, validated and we got what we need
|
||||||
|
Ok(Self {
|
||||||
|
inner: request,
|
||||||
|
address: None,
|
||||||
|
redirects: None,
|
||||||
|
decompress: options.decompress,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Anything else is invalid
|
||||||
|
Err(LuaError::FromLuaConversionError {
|
||||||
|
from: value.type_name(),
|
||||||
|
to: "Request".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Invalid request - expected string or table, got {}",
|
||||||
|
value.type_name()
|
||||||
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Create the inner request builder
|
|
||||||
let mut builder = HyperRequest::builder()
|
|
||||||
.method(config.method)
|
|
||||||
.uri(url.as_str());
|
|
||||||
|
|
||||||
// 4. 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Convert request 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();
|
|
||||||
|
|
||||||
// 6. Finally, attach the body, verifying that the request is valid
|
|
||||||
let inner = builder.body(body).into_lua_err()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
inner,
|
|
||||||
address: None,
|
|
||||||
redirects: None,
|
|
||||||
decompress: config.options.decompress,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,15 +2,16 @@ use http_body_util::Full;
|
||||||
|
|
||||||
use hyper::{
|
use hyper::{
|
||||||
body::{Bytes, Incoming},
|
body::{Bytes, Incoming},
|
||||||
header::{HeaderName, HeaderValue},
|
header::{HeaderValue, CONTENT_TYPE},
|
||||||
HeaderMap, Response as HyperResponse,
|
HeaderMap, Response as HyperResponse, StatusCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::shared::{
|
||||||
server::config::ResponseConfig,
|
headers::header_map_to_table,
|
||||||
shared::{headers::header_map_to_table, incoming::handle_incoming_body},
|
incoming::handle_incoming_body,
|
||||||
|
lua::{lua_table_to_header_map, lua_value_to_bytes},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -104,33 +105,55 @@ impl Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ResponseConfig> for Response {
|
impl FromLua for Response {
|
||||||
type Error = LuaError;
|
fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
|
||||||
fn try_from(config: ResponseConfig) -> Result<Self, Self::Error> {
|
if let Ok(body) = lua_value_to_bytes(&value) {
|
||||||
// 1. Create the inner response builder
|
// String or buffer is always a 200 text/plain response
|
||||||
let mut builder = HyperResponse::builder().status(config.status);
|
let mut response = HyperResponse::new(body);
|
||||||
|
response
|
||||||
|
.headers_mut()
|
||||||
|
.insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
|
||||||
|
Ok(Self {
|
||||||
|
inner: response,
|
||||||
|
decompressed: false,
|
||||||
|
})
|
||||||
|
} else if let LuaValue::Table(tab) = value {
|
||||||
|
// Extract status (required)
|
||||||
|
let status = tab.get::<u16>("status")?;
|
||||||
|
let status = StatusCode::from_u16(status).into_lua_err()?;
|
||||||
|
|
||||||
// 2. Append any headers passed as a table - builder
|
// Extract headers
|
||||||
// headers may be None if builder is already invalid
|
let headers = tab.get::<Option<LuaTable>>("headers")?;
|
||||||
if let Some(headers) = builder.headers_mut() {
|
let headers = headers
|
||||||
for (key, values) in config.headers {
|
.map(|t| lua_table_to_header_map(&t))
|
||||||
let key = HeaderName::from_bytes(key.as_bytes()).into_lua_err()?;
|
.transpose()?
|
||||||
for value in values {
|
.unwrap_or_default();
|
||||||
let value = HeaderValue::from_str(&value).into_lua_err()?;
|
|
||||||
headers.insert(key.clone(), value);
|
// Extract body
|
||||||
}
|
let body = tab.get::<LuaValue>("body")?;
|
||||||
}
|
let body = lua_value_to_bytes(&body)?;
|
||||||
|
|
||||||
|
// Build the full response
|
||||||
|
let mut response = HyperResponse::new(body);
|
||||||
|
response.headers_mut().extend(headers);
|
||||||
|
*response.status_mut() = status;
|
||||||
|
|
||||||
|
// All good, validated and we got what we need
|
||||||
|
Ok(Self {
|
||||||
|
inner: response,
|
||||||
|
decompressed: false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Anything else is invalid
|
||||||
|
Err(LuaError::FromLuaConversionError {
|
||||||
|
from: value.type_name(),
|
||||||
|
to: "Response".to_string(),
|
||||||
|
message: Some(format!(
|
||||||
|
"Invalid response - expected table/string/buffer, got {}",
|
||||||
|
value.type_name()
|
||||||
|
)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue