mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 13:30:38 +00:00
Add support for multiple query & header values in net request
This commit is contained in:
parent
cd78fea1f5
commit
c9ce29741b
5 changed files with 159 additions and 62 deletions
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -10,6 +10,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for multiple values for a single query, and multiple values for a single header, in `net.request`. This is a part of the HTTP specification that is not widely used but that may be useful in certain cases. To clarify:
|
||||||
|
|
||||||
|
- Single values remain unchanged and will work exactly the same as before. <br/>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- https://example.com/?foo=bar&baz=qux
|
||||||
|
local net = require("@lune/net")
|
||||||
|
net.request({
|
||||||
|
url = "example.com",
|
||||||
|
query = {
|
||||||
|
foo = "bar",
|
||||||
|
baz = "qux",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- Multiple values _on a single query / header_ are represented as an ordered array of strings. <br/>
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- https://example.com/?foo=first&foo=second&foo=third&bar=baz
|
||||||
|
local net = require("@lune/net")
|
||||||
|
net.request({
|
||||||
|
url = "example.com",
|
||||||
|
query = {
|
||||||
|
foo = { "first", "second", "third" },
|
||||||
|
bar = "baz",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Update to Luau version `0.606`.
|
- Update to Luau version `0.606`.
|
||||||
|
|
|
@ -4,6 +4,8 @@ use mlua::prelude::*;
|
||||||
|
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
|
|
||||||
|
use super::util::table_to_hash_map;
|
||||||
|
|
||||||
// Net request config
|
// Net request config
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -45,17 +47,17 @@ impl<'lua> FromLua<'lua> for RequestConfigOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RequestConfig<'a> {
|
pub struct RequestConfig {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub method: Method,
|
pub method: Method,
|
||||||
pub query: HashMap<LuaString<'a>, LuaString<'a>>,
|
pub query: HashMap<String, Vec<String>>,
|
||||||
pub headers: HashMap<LuaString<'a>, LuaString<'a>>,
|
pub headers: HashMap<String, Vec<String>>,
|
||||||
pub body: Option<Vec<u8>>,
|
pub body: Option<Vec<u8>>,
|
||||||
pub options: RequestConfigOptions,
|
pub options: RequestConfigOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
|
impl FromLua<'_> for RequestConfig {
|
||||||
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
|
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 we just got a string we assume its a GET request to a given url
|
||||||
if let LuaValue::String(s) = value {
|
if let LuaValue::String(s) = value {
|
||||||
return Ok(Self {
|
return Ok(Self {
|
||||||
|
@ -72,9 +74,7 @@ impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
|
||||||
// Extract url
|
// Extract url
|
||||||
let url = match tab.raw_get::<_, LuaString>("url") {
|
let url = match tab.raw_get::<_, LuaString>("url") {
|
||||||
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
|
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
|
||||||
Err(_) => Err(LuaError::RuntimeError(
|
Err(_) => Err(LuaError::runtime("Missing 'url' in request config")),
|
||||||
"Missing 'url' in request config".to_string(),
|
|
||||||
)),
|
|
||||||
}?;
|
}?;
|
||||||
// Extract method
|
// Extract method
|
||||||
let method = match tab.raw_get::<_, LuaString>("method") {
|
let method = match tab.raw_get::<_, LuaString>("method") {
|
||||||
|
@ -83,26 +83,12 @@ impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
|
||||||
};
|
};
|
||||||
// Extract query
|
// Extract query
|
||||||
let query = match tab.raw_get::<_, LuaTable>("query") {
|
let query = match tab.raw_get::<_, LuaTable>("query") {
|
||||||
Ok(config_headers) => {
|
Ok(tab) => table_to_hash_map(tab, "query")?,
|
||||||
let mut lua_headers = HashMap::new();
|
|
||||||
for pair in config_headers.pairs::<LuaString, LuaString>() {
|
|
||||||
let (key, value) = pair?.to_owned();
|
|
||||||
lua_headers.insert(key, value);
|
|
||||||
}
|
|
||||||
lua_headers
|
|
||||||
}
|
|
||||||
Err(_) => HashMap::new(),
|
Err(_) => HashMap::new(),
|
||||||
};
|
};
|
||||||
// Extract headers
|
// Extract headers
|
||||||
let headers = match tab.raw_get::<_, LuaTable>("headers") {
|
let headers = match tab.raw_get::<_, LuaTable>("headers") {
|
||||||
Ok(config_headers) => {
|
Ok(tab) => table_to_hash_map(tab, "headers")?,
|
||||||
let mut lua_headers = HashMap::new();
|
|
||||||
for pair in config_headers.pairs::<LuaString, LuaString>() {
|
|
||||||
let (key, value) = pair?.to_owned();
|
|
||||||
lua_headers.insert(key, value);
|
|
||||||
}
|
|
||||||
lua_headers
|
|
||||||
}
|
|
||||||
Err(_) => HashMap::new(),
|
Err(_) => HashMap::new(),
|
||||||
};
|
};
|
||||||
// Extract body
|
// Extract body
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use hyper::header::{CONTENT_ENCODING, CONTENT_LENGTH};
|
use hyper::header::CONTENT_ENCODING;
|
||||||
|
|
||||||
use crate::lune::{scheduler::Scheduler, util::TableBuilder};
|
use crate::lune::{scheduler::Scheduler, util::TableBuilder};
|
||||||
|
|
||||||
use self::server::create_server;
|
use self::{server::create_server, util::header_map_to_table};
|
||||||
|
|
||||||
use super::serde::{
|
use super::serde::{
|
||||||
compress_decompress::{decompress, CompressDecompressFormat},
|
compress_decompress::{decompress, CompressDecompressFormat},
|
||||||
|
@ -18,6 +16,7 @@ mod config;
|
||||||
mod processing;
|
mod processing;
|
||||||
mod response;
|
mod response;
|
||||||
mod server;
|
mod server;
|
||||||
|
mod util;
|
||||||
mod websocket;
|
mod websocket;
|
||||||
|
|
||||||
use client::{NetClient, NetClientBuilder};
|
use client::{NetClient, NetClientBuilder};
|
||||||
|
@ -61,18 +60,25 @@ fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult<Lua
|
||||||
EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
|
EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn net_request<'lua>(lua: &'lua Lua, config: RequestConfig<'lua>) -> LuaResult<LuaTable<'lua>>
|
async fn net_request<'lua>(lua: &'lua Lua, config: RequestConfig) -> LuaResult<LuaTable<'lua>>
|
||||||
where
|
where
|
||||||
'lua: 'static, // FIXME: Get rid of static lifetime bound here
|
'lua: 'static, // FIXME: Get rid of static lifetime bound here
|
||||||
{
|
{
|
||||||
// Create and send the request
|
// Create and send the request
|
||||||
let client = NetClient::from_registry(lua);
|
let client = NetClient::from_registry(lua);
|
||||||
let mut request = client.request(config.method, &config.url);
|
let mut request = client.request(config.method, &config.url);
|
||||||
for (query, value) in config.query {
|
for (query, values) in config.query {
|
||||||
request = request.query(&[(query.to_str()?, value.to_str()?)]);
|
request = request.query(
|
||||||
|
&values
|
||||||
|
.iter()
|
||||||
|
.map(|v| (query.as_str(), v))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for (header, value) in config.headers {
|
for (header, values) in config.headers {
|
||||||
request = request.header(header.to_str()?, value.to_str()?);
|
for value in values {
|
||||||
|
request = request.header(header.as_str(), value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let res = request
|
let res = request
|
||||||
.body(config.body.unwrap_or_default())
|
.body(config.body.unwrap_or_default())
|
||||||
|
@ -82,44 +88,32 @@ where
|
||||||
// Extract status, headers
|
// Extract status, headers
|
||||||
let res_status = res.status().as_u16();
|
let res_status = res.status().as_u16();
|
||||||
let res_status_text = res.status().canonical_reason();
|
let res_status_text = res.status().canonical_reason();
|
||||||
let mut res_headers = res
|
let res_headers = res.headers().clone();
|
||||||
.headers()
|
|
||||||
.iter()
|
|
||||||
.map(|(name, value)| {
|
|
||||||
(
|
|
||||||
name.as_str().to_string(),
|
|
||||||
value.to_str().unwrap().to_owned(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<HashMap<String, String>>();
|
|
||||||
// Read response bytes
|
// Read response bytes
|
||||||
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
|
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
|
||||||
|
let mut res_decompressed = false;
|
||||||
// Check for extra options, decompression
|
// Check for extra options, decompression
|
||||||
if config.options.decompress {
|
if config.options.decompress {
|
||||||
// NOTE: Header names are guaranteed to be lowercase because of the above
|
let decompress_format = res_headers
|
||||||
// transformations of them into the hashmap, so we can compare directly
|
.iter()
|
||||||
let format = res_headers.iter().find_map(|(name, val)| {
|
.find(|(name, _)| {
|
||||||
if name == CONTENT_ENCODING.as_str() {
|
name.as_str()
|
||||||
CompressDecompressFormat::detect_from_header_str(val)
|
.eq_ignore_ascii_case(CONTENT_ENCODING.as_str())
|
||||||
} else {
|
})
|
||||||
None
|
.and_then(|(_, value)| value.to_str().ok())
|
||||||
}
|
.and_then(CompressDecompressFormat::detect_from_header_str);
|
||||||
});
|
if let Some(format) = decompress_format {
|
||||||
if let Some(format) = format {
|
|
||||||
res_bytes = decompress(format, res_bytes).await?;
|
res_bytes = decompress(format, res_bytes).await?;
|
||||||
let content_encoding_header_str = CONTENT_ENCODING.as_str();
|
res_decompressed = true;
|
||||||
let content_length_header_str = CONTENT_LENGTH.as_str();
|
|
||||||
res_headers.retain(|name, _| {
|
|
||||||
name != content_encoding_header_str && name != content_length_header_str
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Construct and return a readonly lua table with results
|
// Construct and return a readonly lua table with results
|
||||||
|
let res_headers_lua = header_map_to_table(lua, res_headers, res_decompressed)?;
|
||||||
TableBuilder::new(lua)?
|
TableBuilder::new(lua)?
|
||||||
.with_value("ok", (200..300).contains(&res_status))?
|
.with_value("ok", (200..300).contains(&res_status))?
|
||||||
.with_value("statusCode", res_status)?
|
.with_value("statusCode", res_status)?
|
||||||
.with_value("statusMessage", res_status_text)?
|
.with_value("statusMessage", res_status_text)?
|
||||||
.with_value("headers", res_headers)?
|
.with_value("headers", res_headers_lua)?
|
||||||
.with_value("body", lua.create_string(&res_bytes)?)?
|
.with_value("body", lua.create_string(&res_bytes)?)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
81
src/lune/builtins/net/util.rs
Normal file
81
src/lune/builtins/net/util.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use hyper::{
|
||||||
|
header::{CONTENT_ENCODING, CONTENT_LENGTH},
|
||||||
|
HeaderMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
use crate::lune::util::TableBuilder;
|
||||||
|
|
||||||
|
pub fn header_map_to_table(
|
||||||
|
lua: &Lua,
|
||||||
|
headers: HeaderMap,
|
||||||
|
remove_content_headers: bool,
|
||||||
|
) -> LuaResult<LuaTable> {
|
||||||
|
let mut res_headers: HashMap<String, Vec<String>> = HashMap::new();
|
||||||
|
for (name, value) in headers.iter() {
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = TableBuilder::new(lua)?;
|
||||||
|
for (name, mut values) in res_headers {
|
||||||
|
if values.len() == 1 {
|
||||||
|
let value = values.pop().unwrap().into_lua(lua)?;
|
||||||
|
builder = builder.with_value(name, value)?;
|
||||||
|
} else {
|
||||||
|
let values = TableBuilder::new(lua)?
|
||||||
|
.with_sequential_values(values)?
|
||||||
|
.build_readonly()?
|
||||||
|
.into_lua(lua)?;
|
||||||
|
builder = builder.with_value(name, values)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"
|
export type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH"
|
||||||
|
|
||||||
|
type HttpQueryOrHeaderMap = { [string]: string | { string } }
|
||||||
|
export type HttpQueryMap = HttpQueryOrHeaderMap
|
||||||
|
export type HttpHeaderMap = HttpQueryOrHeaderMap
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface FetchParamsOptions
|
@interface FetchParamsOptions
|
||||||
@within Net
|
@within Net
|
||||||
|
@ -33,8 +37,8 @@ export type FetchParams = {
|
||||||
url: string,
|
url: string,
|
||||||
method: HttpMethod?,
|
method: HttpMethod?,
|
||||||
body: string?,
|
body: string?,
|
||||||
query: { [string]: string }?,
|
query: HttpQueryMap?,
|
||||||
headers: { [string]: string }?,
|
headers: HttpHeaderMap?,
|
||||||
options: FetchParamsOptions?,
|
options: FetchParamsOptions?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +60,7 @@ export type FetchResponse = {
|
||||||
ok: boolean,
|
ok: boolean,
|
||||||
statusCode: number,
|
statusCode: number,
|
||||||
statusMessage: string,
|
statusMessage: string,
|
||||||
headers: { [string]: string },
|
headers: HttpHeaderMap,
|
||||||
body: string,
|
body: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue