use mlua::{Function, Lua, Table, UserData, Value as LuaValue};
use serde_json::Value as JsonValue;
use crate::scripting::conversion::lua_to_json_value;
#[derive(Debug, Clone)]
pub enum ResponseBody {
Json(JsonValue),
Raw {
content_type: String,
bytes: Vec<u8>,
},
File {
key: String,
filename: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct ScriptResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: ResponseBody,
}
impl UserData for ScriptResponse {}
#[derive(Debug, Clone, Default)]
pub struct ResponseOverrides {
pub status: Option<u16>,
pub headers: Vec<(String, String)>,
}
pub fn reset_overrides(lua: &Lua) {
lua.set_app_data(ResponseOverrides::default());
}
pub fn take_overrides(lua: &Lua) -> ResponseOverrides {
lua.remove_app_data::<ResponseOverrides>()
.unwrap_or_default()
}
fn valid_status(code: u16) -> Result<u16, mlua::Error> {
if (100..=599).contains(&code) {
Ok(code)
} else {
Err(mlua::Error::RuntimeError(format!(
"invalid HTTP status {}",
code
)))
}
}
fn headers_from(lua: &Lua, value: Option<LuaValue>) -> Result<Vec<(String, String)>, mlua::Error> {
let Some(value) = value else {
return Ok(Vec::new());
};
let LuaValue::Table(t) = value else {
return Err(mlua::Error::RuntimeError(
"headers must be a table of name = value".to_string(),
));
};
let mut out = Vec::new();
for pair in t.pairs::<String, LuaValue>() {
let (k, v) = pair?;
let v = match v {
LuaValue::String(s) => s.to_str()?.to_string(),
other => lua_to_json_value(lua, other)?.to_string(),
};
out.push((k, v));
}
Ok(out)
}
pub fn create_status_function(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, code: u16| {
let code = valid_status(code)?;
let mut ov = lua
.app_data_mut::<ResponseOverrides>()
.ok_or_else(|| mlua::Error::RuntimeError("no request in progress".to_string()))?;
ov.status = Some(code);
Ok(())
})
}
pub fn create_header_function(lua: &Lua) -> mlua::Result<Function> {
lua.create_function(|lua, (name, value): (String, String)| {
let mut ov = lua
.app_data_mut::<ResponseOverrides>()
.ok_or_else(|| mlua::Error::RuntimeError("no request in progress".to_string()))?;
ov.headers.push((name, value));
Ok(())
})
}
pub fn create_response_table(lua: &Lua) -> mlua::Result<Table> {
let response = lua.create_table()?;
response.set(
"json",
lua.create_function(
|lua, (data, status, headers): (LuaValue, Option<u16>, Option<LuaValue>)| {
let body = lua_to_json_value(lua, data)?;
lua.create_userdata(ScriptResponse {
status: status.map(valid_status).transpose()?.unwrap_or(200),
headers: headers_from(lua, headers)?,
body: ResponseBody::Json(body),
})
},
)?,
)?;
response.set(
"html",
lua.create_function(|lua, (content, status): (String, Option<u16>)| {
lua.create_userdata(ScriptResponse {
status: status.map(valid_status).transpose()?.unwrap_or(200),
headers: Vec::new(),
body: ResponseBody::Raw {
content_type: "text/html; charset=utf-8".to_string(),
bytes: content.into_bytes(),
},
})
})?,
)?;
response.set(
"redirect",
lua.create_function(|lua, (url, status): (String, Option<u16>)| {
lua.create_userdata(ScriptResponse {
status: status.map(valid_status).transpose()?.unwrap_or(302),
headers: vec![("Location".to_string(), url)],
body: ResponseBody::Raw {
content_type: "text/plain; charset=utf-8".to_string(),
bytes: Vec::new(),
},
})
})?,
)?;
response.set(
"file",
lua.create_function(|lua, (key, filename): (String, Option<String>)| {
lua.create_userdata(ScriptResponse {
status: 200,
headers: Vec::new(),
body: ResponseBody::File { key, filename },
})
})?,
)?;
response.set(
"cors",
lua.create_function(|lua, (data, options): (LuaValue, Option<Table>)| {
let body = lua_to_json_value(lua, data)?;
let get = |k: &str| -> Result<Option<String>, mlua::Error> {
match &options {
Some(t) => match t.get::<LuaValue>(k)? {
LuaValue::Nil => Ok(None),
LuaValue::String(s) => Ok(Some(s.to_str()?.to_string())),
LuaValue::Boolean(b) => Ok(Some(b.to_string())),
LuaValue::Integer(i) => Ok(Some(i.to_string())),
other => Ok(Some(lua_to_json_value(lua, other)?.to_string())),
},
None => Ok(None),
}
};
let mut headers = vec![(
"Access-Control-Allow-Origin".to_string(),
get("origin")?.unwrap_or_else(|| "*".to_string()),
)];
headers.push((
"Access-Control-Allow-Methods".to_string(),
get("methods")?.unwrap_or_else(|| "GET, POST, PUT, DELETE, OPTIONS".to_string()),
));
if let Some(h) = get("headers")? {
headers.push(("Access-Control-Allow-Headers".to_string(), h));
}
if get("credentials")?.as_deref() == Some("true") {
headers.push((
"Access-Control-Allow-Credentials".to_string(),
"true".to_string(),
));
}
if let Some(age) = get("max_age")? {
headers.push(("Access-Control-Max-Age".to_string(), age));
}
lua.create_userdata(ScriptResponse {
status: 200,
headers,
body: ResponseBody::Json(body),
})
})?,
)?;
Ok(response)
}