Skip to content

Commit 3ba3314

Browse files
committed
Add http feature for basic http support
Add http/headers module
1 parent 4ecddf6 commit 3ba3314

File tree

6 files changed

+380
-8
lines changed

6 files changed

+380
-8
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ vendored = ["mlua/vendored"]
1919
json = ["mlua/serde", "dep:ouroboros", "dep:serde", "dep:serde_json"]
2020
regex = ["dep:regex", "dep:ouroboros", "dep:quick_cache"]
2121
yaml = ["mlua/serde", "dep:ouroboros", "dep:serde", "dep:serde_yaml"]
22+
http = ["dep:http"]
2223

2324
[dependencies]
2425
mlua = { version = "0.11" }
@@ -29,3 +30,6 @@ serde_yaml = { version = "0.9", optional = true }
2930
owo-colors = "4"
3031
regex = { version = "1.0", optional = true }
3132
quick_cache = { version = "0.6", optional = true }
33+
34+
# http
35+
http = { version = "1.3", optional = true }

src/http/headers.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
use http::header::{HeaderMap, HeaderName, HeaderValue};
2+
use mlua::{
3+
Either, ExternalError, ExternalResult, FromLua, Lua, MetaMethod, Result, String as LuaString, Table,
4+
UserData, UserDataMethods, Value,
5+
};
6+
7+
#[derive(Clone)]
8+
pub(crate) struct Headers(HeaderMap);
9+
10+
impl UserData for Headers {
11+
fn register(registry: &mut mlua::UserDataRegistry<Self>) {
12+
registry.add_function("new", |lua, arg: Option<Table>| match arg {
13+
None => Ok(Headers(HeaderMap::new())),
14+
Some(t) => Headers::from_lua(Value::Table(t), lua),
15+
});
16+
17+
registry.add_method("get", |lua, this, name: LuaString| {
18+
let name = name.to_str()?;
19+
(this.0.get(&*name))
20+
.map(|v| lua.create_string(v.as_ref()))
21+
.transpose()
22+
});
23+
24+
registry.add_method("get_all", |lua, this, name: LuaString| {
25+
let name = name.to_str()?;
26+
(this.0.get_all(&*name))
27+
.iter()
28+
.map(|v| lua.create_string(v.as_ref()))
29+
.collect::<Result<Vec<_>>>()
30+
});
31+
32+
registry.add_method("get_count", |_, this, name: LuaString| {
33+
let name = name.to_str()?;
34+
Ok(this.0.get_all(&*name).iter().count())
35+
});
36+
37+
registry.add_method_mut("set", |_, this, (name, value): (LuaString, LuaString)| {
38+
let name = HeaderName::from_lua(name)?;
39+
let value = HeaderValue::from_lua(value)?;
40+
this.0.insert(name, value);
41+
Ok(())
42+
});
43+
44+
registry.add_method_mut("add", |_, this, (name, value): (LuaString, LuaString)| {
45+
let name = HeaderName::from_lua(name)?;
46+
let value = HeaderValue::from_lua(value)?;
47+
Ok(this.0.append(name, value))
48+
});
49+
50+
registry.add_method_mut("remove", |_, this, name: LuaString| {
51+
let name = HeaderName::from_lua(name)?;
52+
this.0.remove(&name);
53+
Ok(())
54+
});
55+
56+
registry.add_method("count", |_, this, ()| Ok(this.0.len()));
57+
58+
registry.add_method_mut("clear", |_, this, ()| {
59+
this.0.clear();
60+
Ok(())
61+
});
62+
63+
registry.add_method("keys", |lua, this, ()| {
64+
(this.0.keys())
65+
.map(|n| lua.create_string(n.as_str()))
66+
.collect::<Result<Vec<_>>>()
67+
});
68+
69+
registry.add_method("clone", |_, this, ()| Ok(Headers(this.0.clone())));
70+
71+
// Convert headers map to a Lua table
72+
registry.add_method("to_table", |lua, this, ()| {
73+
let table = lua.create_table_with_capacity(0, this.0.keys_len())?;
74+
for key in this.0.keys() {
75+
let name = lua.create_string(&key)?;
76+
let mut iter = this.0.get_all(key).iter().enumerate().peekable();
77+
let mut values = None;
78+
while let Some((i, value)) = iter.next() {
79+
let value = lua.create_string(value.as_ref())?;
80+
if i == 0 && iter.peek().is_none() {
81+
table.raw_set(name, value)?;
82+
break;
83+
}
84+
// There are multiple values for this header, store them in a sub-table
85+
if i == 0 {
86+
values = Some(lua.create_table()?);
87+
values.as_ref().unwrap().raw_set(i + 1, value)?;
88+
table.raw_set(name.clone(), values.as_ref().unwrap())?;
89+
} else {
90+
values.as_ref().unwrap().raw_set(i + 1, value)?;
91+
}
92+
}
93+
}
94+
set_headers_metatable(lua, &table)?;
95+
Ok(table)
96+
});
97+
98+
// Index
99+
registry.add_meta_method(MetaMethod::Index, |lua, this, key: LuaString| {
100+
let key = key.to_str()?;
101+
match this.0.get(&*key) {
102+
Some(value) => Ok(Some(lua.create_string(value.as_ref())?)),
103+
None => Ok(None),
104+
}
105+
});
106+
107+
// NewIndex
108+
registry.add_meta_method_mut(
109+
MetaMethod::NewIndex,
110+
|_, this, (key, value): (LuaString, Either<Option<LuaString>, Table>)| {
111+
let key = HeaderName::from_lua(key)?;
112+
match value {
113+
Either::Left(None) => {
114+
this.0.remove(&key);
115+
}
116+
Either::Left(Some(v)) => {
117+
let value = HeaderValue::from_lua(v)?;
118+
this.0.insert(key, value);
119+
}
120+
Either::Right(t) => {
121+
this.0.remove(&key);
122+
for (i, v) in t.sequence_values::<LuaString>().enumerate() {
123+
let value = HeaderValue::from_lua(v?)?;
124+
if i == 0 {
125+
this.0.insert(key.clone(), value);
126+
continue;
127+
}
128+
this.0.append(key.clone(), value);
129+
}
130+
}
131+
}
132+
Ok(())
133+
},
134+
);
135+
}
136+
}
137+
138+
impl FromLua for Headers {
139+
fn from_lua(value: Value, lua: &Lua) -> Result<Self> {
140+
match value {
141+
Value::Table(table) => {
142+
let mut headers = HeaderMap::new();
143+
table.for_each::<LuaString, Value>(|key, value| {
144+
let name = HeaderName::from_lua(key)?;
145+
// Maybe `value` is a list of values
146+
if let Value::Table(values) = value {
147+
for value in values.sequence_values::<LuaString>() {
148+
headers.append(name.clone(), HeaderValue::from_lua(value?)?);
149+
}
150+
} else {
151+
let value = lua.unpack::<LuaString>(value)?;
152+
headers.append(name, HeaderValue::from_lua(value)?);
153+
}
154+
Ok(())
155+
})?;
156+
Ok(Headers(headers))
157+
}
158+
Value::UserData(ud) if ud.is::<Self>() => ud.borrow::<Self>().map(|hdrs| hdrs.clone()),
159+
val => {
160+
let type_name = val.type_name();
161+
let msg = format!("cannot make headers from {type_name}");
162+
Err(msg.into_lua_err())
163+
}
164+
}
165+
}
166+
}
167+
168+
/// Sets a metatable for the given headers table to handle case-insensitive keys
169+
/// and normalize underscores to dashes.
170+
fn set_headers_metatable(lua: &Lua, headers: &Table) -> Result<()> {
171+
if let Ok(Some(mt)) = lua.named_registry_value::<Option<Table>>("__headers_metatable") {
172+
return headers.set_metatable(Some(mt));
173+
}
174+
175+
// Create a new metatable
176+
let metatable = lua
177+
.load(
178+
r#"
179+
return {
180+
__index = function(self, key)
181+
local normalized_key = string.lower(key)
182+
return rawget(self, normalized_key)
183+
end,
184+
185+
__newindex = function(self, key, value)
186+
local normalized_key = string.lower(key)
187+
rawset(self, normalized_key, value)
188+
end
189+
}
190+
"#,
191+
)
192+
.eval::<Table>()?;
193+
194+
// Cache it in the Lua registry
195+
lua.set_named_registry_value("__headers_metatable", &metatable)?;
196+
headers.set_metatable(Some(metatable))
197+
}
198+
199+
pub(crate) trait LuaHeaderExt {
200+
fn from_lua(value: LuaString) -> Result<Self>
201+
where
202+
Self: Sized;
203+
}
204+
205+
impl LuaHeaderExt for HeaderName {
206+
#[inline]
207+
fn from_lua(value: LuaString) -> Result<Self> {
208+
HeaderName::from_bytes(&value.as_bytes()).into_lua_err()
209+
}
210+
}
211+
212+
impl LuaHeaderExt for HeaderValue {
213+
#[inline]
214+
fn from_lua(value: LuaString) -> Result<Self> {
215+
HeaderValue::from_bytes(&value.as_bytes()).into_lua_err()
216+
}
217+
}

src/http/mod.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use mlua::{Lua, Result, Table};
2+
3+
/// A loader for the `http` module.
4+
fn loader(lua: &Lua) -> Result<Table> {
5+
let t = lua.create_table()?;
6+
t.set("Headers", lua.create_proxy::<headers::Headers>()?)?;
7+
Ok(t)
8+
}
9+
10+
/// Registers the `http` module in the given Lua state.
11+
pub fn register(lua: &Lua, name: Option<&str>) -> Result<Table> {
12+
let name = name.unwrap_or("@http");
13+
let value = loader(lua)?;
14+
lua.register_module(name, &value)?;
15+
Ok(value)
16+
}
17+
18+
mod headers;

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ pub mod json;
2020
pub mod regex;
2121
#[cfg(feature = "yaml")]
2222
pub mod yaml;
23+
24+
pub mod http;

tests/lib.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ fn run_file(modname: &str) -> Result<()> {
1818
mlua_stdlib::yaml::register(&lua, None)?;
1919
#[cfg(feature = "regex")]
2020
mlua_stdlib::regex::register(&lua, None)?;
21+
#[cfg(feature = "http")]
22+
mlua_stdlib::http::register(&lua, None)?;
2123

2224
// Add `testing` global variable (an instance of the testing framework)
2325
let testing = testing.call_function::<Table>("new", modname)?;
@@ -38,14 +40,32 @@ fn run_file(modname: &str) -> Result<()> {
3840

3941
// Helper macro to generate Rust test functions for Lua test modules.
4042
macro_rules! include_tests {
41-
($( $(#[$meta:meta])? $name:ident $(,)? )*) => {
42-
$(
43-
$(#[$meta])*
44-
#[test]
45-
fn $name() -> Result<()> {
46-
run_file(stringify!($name))
47-
}
48-
)*
43+
() => {};
44+
45+
// Grouped tests
46+
($(#[$meta:meta])? $group:ident { $($item:ident),* $(,)? }, $($rest:tt)*) => {
47+
$(#[$meta])*
48+
mod $group {
49+
use super::*;
50+
$(
51+
#[test]
52+
fn $item() -> Result<()> {
53+
run_file(&format!("{}/{}", stringify!($group), stringify!($item)))
54+
}
55+
)*
56+
}
57+
58+
include_tests!( $($rest)* );
59+
};
60+
61+
($(#[$meta:meta])? $name:ident, $($rest:tt)*) => {
62+
$(#[$meta])*
63+
#[test]
64+
fn $name() -> Result<()> {
65+
run_file(stringify!($name))
66+
}
67+
68+
include_tests!( $($rest)* );
4969
};
5070
}
5171

@@ -55,4 +75,9 @@ include_tests! {
5575
#[cfg(feature = "json")] json,
5676
#[cfg(feature = "regex")] regex,
5777
#[cfg(feature = "yaml")] yaml,
78+
79+
#[cfg(feature = "http")]
80+
http {
81+
headers,
82+
},
5883
}

0 commit comments

Comments
 (0)