mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Add networking functions & move json apis
This commit is contained in:
parent
07e3b1fc5e
commit
3ccb31d918
10 changed files with 278 additions and 52 deletions
|
@ -110,9 +110,9 @@ local result = process.spawn("ping", {
|
||||||
|
|
||||||
Using the result of a spawned process, exiting the process
|
Using the result of a spawned process, exiting the process
|
||||||
|
|
||||||
We use the result from the above ping command and parse it
|
We use the result from the above ping command and parse
|
||||||
to show the results it gave us in a nicer format, then we
|
it to show the results it gave us in a nicer format, here we
|
||||||
either exit successfully or with an error (exit code 1)
|
also exit with an error (exit code 1) if spawning the process failed
|
||||||
]==]
|
]==]
|
||||||
|
|
||||||
if result.ok then
|
if result.ok then
|
||||||
|
@ -126,8 +126,52 @@ if result.ok then
|
||||||
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
print(string.format("Average ping time: %.3fms", assert(tonumber(avg))))
|
||||||
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
print(string.format("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
||||||
else
|
else
|
||||||
|
print("\nFailed to send ping to google!")
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
process.exit(result.code)
|
process.exit(result.code)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[==[
|
||||||
|
EXAMPLE #7
|
||||||
|
|
||||||
|
Using the built-in networking library
|
||||||
|
]==]
|
||||||
|
print("\nSending PATCH request to web API 📤")
|
||||||
|
local apiResult = net.request({
|
||||||
|
url = "https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
method = "PATCH",
|
||||||
|
headers = {
|
||||||
|
["Content-Type"] = "application/json",
|
||||||
|
},
|
||||||
|
body = net.jsonEncode({
|
||||||
|
title = "foo",
|
||||||
|
body = "bar",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not result.ok then
|
||||||
|
print("\nFailed to send network request!")
|
||||||
|
print(string.format("%d (%s)", apiResult.statusCode, apiResult.statusMessage))
|
||||||
|
print(apiResult.body)
|
||||||
|
process.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
type ApiResponse = {
|
||||||
|
id: number,
|
||||||
|
title: string,
|
||||||
|
body: string,
|
||||||
|
userId: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
local apiResponse: ApiResponse = net.jsonDecode(apiResult.body)
|
||||||
|
assert(apiResponse.title == "foo", "Invalid json response")
|
||||||
|
assert(apiResponse.body == "bar", "Invalid json response")
|
||||||
|
print("Got valid JSON response with changes applied")
|
||||||
|
|
||||||
|
--[==[
|
||||||
|
Example #8
|
||||||
|
|
||||||
|
Saying goodbye 😔
|
||||||
|
]==]
|
||||||
|
|
||||||
print("\nGoodbye, lune! 🌙")
|
print("\nGoodbye, lune! 🌙")
|
||||||
|
|
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added networking functions under `net`
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local apiResult = net.request({
|
||||||
|
url = "https://jsonplaceholder.typicode.com/posts/1",
|
||||||
|
method = "PATCH",
|
||||||
|
headers = {
|
||||||
|
["Content-Type"] = "application/json",
|
||||||
|
},
|
||||||
|
body = net.jsonEncode({
|
||||||
|
title = "foo",
|
||||||
|
body = "bar",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
local apiResponse = net.jsonDecode(apiResult.body)
|
||||||
|
assert(apiResponse.title == "foo", "Invalid json response")
|
||||||
|
assert(apiResponse.body == "bar", "Invalid json response")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The `json` api is now part of `net`
|
||||||
|
- `json.encode` becomes `net.jsonEncode`
|
||||||
|
- `json.decode` become `net.jsonDecode`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed JSON decode not working properly
|
||||||
|
|
||||||
## `0.0.2` - January 19th, 2023
|
## `0.0.2` - January 19th, 2023
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
20
README.md
20
README.md
|
@ -47,12 +47,24 @@ type fs = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### **`json`** - JSON
|
### **`net`** - Networking
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
type json = {
|
type net = {
|
||||||
encode: (value: any, pretty: boolean?) -> string,
|
request: (config: string | {
|
||||||
decode: (encoded: string) -> any,
|
url: string,
|
||||||
|
method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?,
|
||||||
|
headers: { [string]: string }?,
|
||||||
|
body: string?,
|
||||||
|
}) -> {
|
||||||
|
ok: boolean,
|
||||||
|
statusCode: number,
|
||||||
|
statusMessage: string,
|
||||||
|
headers: { [string]: string },
|
||||||
|
body: string,
|
||||||
|
},
|
||||||
|
jsonEncode: (value: any, pretty: boolean?) -> string,
|
||||||
|
jsonDecode: (encoded: string) -> any,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
9
lune.yml
9
lune.yml
|
@ -27,15 +27,18 @@ globals:
|
||||||
fs.isDir:
|
fs.isDir:
|
||||||
args:
|
args:
|
||||||
- type: string
|
- type: string
|
||||||
# JSON
|
# Net (networking)
|
||||||
json.encode:
|
net.jsonEncode:
|
||||||
args:
|
args:
|
||||||
- type: any
|
- type: any
|
||||||
- required: false
|
- required: false
|
||||||
type: boolean
|
type: boolean
|
||||||
json.decode:
|
net.jsonDecode:
|
||||||
args:
|
args:
|
||||||
- type: string
|
- type: string
|
||||||
|
net.request:
|
||||||
|
args:
|
||||||
|
- type: any
|
||||||
# Process
|
# Process
|
||||||
process.getEnvVars:
|
process.getEnvVars:
|
||||||
process.getEnvVar:
|
process.getEnvVar:
|
||||||
|
|
|
@ -11,9 +11,21 @@ declare fs: {
|
||||||
isDir: (path: string) -> boolean,
|
isDir: (path: string) -> boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
declare json: {
|
declare net: {
|
||||||
encode: (value: any, pretty: boolean?) -> string,
|
request: (config: string | {
|
||||||
decode: (encoded: string) -> any,
|
url: string,
|
||||||
|
method: ("GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH")?,
|
||||||
|
headers: { [string]: string }?,
|
||||||
|
body: string?,
|
||||||
|
}) -> {
|
||||||
|
ok: boolean,
|
||||||
|
statusCode: number,
|
||||||
|
statusMessage: string,
|
||||||
|
headers: { [string]: string },
|
||||||
|
body: string,
|
||||||
|
},
|
||||||
|
jsonEncode: (value: any, pretty: boolean?) -> string,
|
||||||
|
jsonDecode: (encoded: string) -> any,
|
||||||
}
|
}
|
||||||
|
|
||||||
declare process: {
|
declare process: {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use clap::{CommandFactory, Parser};
|
||||||
use mlua::{Lua, MultiValue, Result, ToLua};
|
use mlua::{Lua, MultiValue, Result, ToLua};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
lune::{fs::LuneFs, json::LuneJson, process::LuneProcess},
|
lune::{fs::LuneFs, net::LuneNet, process::LuneProcess},
|
||||||
utils::GithubClient,
|
utils::GithubClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ impl Cli {
|
||||||
let lua = Lua::new();
|
let lua = Lua::new();
|
||||||
let globals = lua.globals();
|
let globals = lua.globals();
|
||||||
globals.set("fs", LuneFs::new())?;
|
globals.set("fs", LuneFs::new())?;
|
||||||
|
globals.set("net", LuneNet::new())?;
|
||||||
globals.set("process", LuneProcess::new())?;
|
globals.set("process", LuneProcess::new())?;
|
||||||
globals.set("json", LuneJson::new())?;
|
|
||||||
lua.sandbox(true)?;
|
lua.sandbox(true)?;
|
||||||
// Load & call the file with the given args
|
// Load & call the file with the given args
|
||||||
let lua_args = self
|
let lua_args = self
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
use mlua::{Error, Lua, LuaSerdeExt, Result, UserData, UserDataMethods, Value};
|
|
||||||
|
|
||||||
pub struct LuneJson();
|
|
||||||
|
|
||||||
impl LuneJson {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserData for LuneJson {
|
|
||||||
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
|
|
||||||
methods.add_function("encode", json_encode);
|
|
||||||
methods.add_function("decode", json_decode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_encode(_: &Lua, (val, pretty): (Value, Option<bool>)) -> Result<String> {
|
|
||||||
if let Some(true) = pretty {
|
|
||||||
Ok(serde_json::to_string_pretty(&val).map_err(Error::external)?)
|
|
||||||
} else {
|
|
||||||
Ok(serde_json::to_string(&val).map_err(Error::external)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_decode(lua: &Lua, json: String) -> Result<Value> {
|
|
||||||
lua.to_value(&json)
|
|
||||||
}
|
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
pub mod json;
|
pub mod net;
|
||||||
pub mod process;
|
pub mod process;
|
||||||
|
|
137
src/lune/net.rs
Normal file
137
src/lune/net.rs
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
use std::{collections::HashMap, str::FromStr};
|
||||||
|
|
||||||
|
use mlua::{Error, Lua, LuaSerdeExt, Result, UserData, UserDataMethods, Value};
|
||||||
|
use reqwest::{
|
||||||
|
header::{HeaderMap, HeaderName, HeaderValue},
|
||||||
|
Method,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::utils::get_github_user_agent_header;
|
||||||
|
|
||||||
|
pub struct LuneNet();
|
||||||
|
|
||||||
|
impl LuneNet {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserData for LuneNet {
|
||||||
|
fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_function("jsonEncode", net_json_encode);
|
||||||
|
methods.add_function("jsonDecode", net_json_decode);
|
||||||
|
methods.add_async_function("request", net_request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn net_json_encode(_: &Lua, (val, pretty): (Value, Option<bool>)) -> Result<String> {
|
||||||
|
if let Some(true) = pretty {
|
||||||
|
serde_json::to_string_pretty(&val).map_err(Error::external)
|
||||||
|
} else {
|
||||||
|
serde_json::to_string(&val).map_err(Error::external)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn net_json_decode(lua: &Lua, json: String) -> Result<Value> {
|
||||||
|
let json: serde_json::Value = serde_json::from_str(&json).map_err(Error::external)?;
|
||||||
|
lua.to_value(&json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn net_request<'lua>(lua: &'lua Lua, config: Value<'lua>) -> Result<Value<'lua>> {
|
||||||
|
// Extract stuff from config and make sure its all valid
|
||||||
|
let (url, method, headers, body) = match config {
|
||||||
|
Value::String(s) => {
|
||||||
|
let url = s.to_string_lossy().to_string();
|
||||||
|
let method = "GET".to_string();
|
||||||
|
(url, method, None, None)
|
||||||
|
}
|
||||||
|
Value::Table(tab) => {
|
||||||
|
// Extract url
|
||||||
|
let url = match tab.raw_get::<&str, mlua::String>("url") {
|
||||||
|
Ok(config_url) => config_url.to_string_lossy().to_string(),
|
||||||
|
Err(_) => return Err(Error::RuntimeError("Missing 'url' in config".to_string())),
|
||||||
|
};
|
||||||
|
// Extract method
|
||||||
|
let method = match tab.raw_get::<&str, mlua::String>("method") {
|
||||||
|
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
|
||||||
|
Err(_) => "GET".to_string(),
|
||||||
|
};
|
||||||
|
// Extract headers
|
||||||
|
let headers = match tab.raw_get::<&str, mlua::Table>("headers") {
|
||||||
|
Ok(config_headers) => {
|
||||||
|
let mut lua_headers = HeaderMap::new();
|
||||||
|
for pair in config_headers.pairs::<mlua::String, mlua::String>() {
|
||||||
|
let (key, value) = pair?;
|
||||||
|
lua_headers.insert(
|
||||||
|
HeaderName::from_str(key.to_str()?).map_err(Error::external)?,
|
||||||
|
HeaderValue::from_str(value.to_str()?).map_err(Error::external)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(lua_headers)
|
||||||
|
}
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
// Extract body
|
||||||
|
let body = match tab.raw_get::<&str, mlua::String>("body") {
|
||||||
|
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
|
||||||
|
Err(_) => None,
|
||||||
|
};
|
||||||
|
(url, method, headers, body)
|
||||||
|
}
|
||||||
|
_ => return Err(Error::RuntimeError("Invalid config value".to_string())),
|
||||||
|
};
|
||||||
|
// Convert method string into proper enum
|
||||||
|
let method = match Method::from_str(&method) {
|
||||||
|
Ok(meth) => meth,
|
||||||
|
Err(_) => {
|
||||||
|
return Err(Error::RuntimeError(format!(
|
||||||
|
"Invalid config method '{}'",
|
||||||
|
&method
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Extract headers from config, force user agent
|
||||||
|
let mut header_map = if let Some(headers) = headers {
|
||||||
|
headers
|
||||||
|
} else {
|
||||||
|
HeaderMap::new()
|
||||||
|
};
|
||||||
|
header_map.insert(
|
||||||
|
"User-Agent",
|
||||||
|
HeaderValue::from_str(&get_github_user_agent_header()).map_err(Error::external)?,
|
||||||
|
);
|
||||||
|
// Create a client to send a request with
|
||||||
|
// FUTURE: Try to reuse this client
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.map_err(Error::external)?;
|
||||||
|
// Create and send the request
|
||||||
|
let mut request = client.request(method, url).headers(header_map);
|
||||||
|
if let Some(body) = body {
|
||||||
|
request = request.body(body)
|
||||||
|
}
|
||||||
|
let response = request.send().await.map_err(Error::external)?;
|
||||||
|
// Extract status, headers, body
|
||||||
|
let res_status = response.status();
|
||||||
|
let res_headers = response.headers().to_owned();
|
||||||
|
let res_bytes = response.bytes().await.map_err(Error::external)?;
|
||||||
|
// Construct and return a readonly lua table with results
|
||||||
|
let tab = lua.create_table()?;
|
||||||
|
tab.raw_set("ok", res_status.is_success())?;
|
||||||
|
tab.raw_set("statusCode", res_status.as_u16())?;
|
||||||
|
tab.raw_set(
|
||||||
|
"statusMessage",
|
||||||
|
res_status.canonical_reason().unwrap_or("?"),
|
||||||
|
)?;
|
||||||
|
tab.raw_set(
|
||||||
|
"headers",
|
||||||
|
res_headers
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, value)| value.to_str().is_ok())
|
||||||
|
.map(|(key, value)| (key.as_str(), value.to_str().unwrap()))
|
||||||
|
.collect::<HashMap<_, _>>(),
|
||||||
|
)?;
|
||||||
|
tab.raw_set("body", lua.create_string(&res_bytes)?)?;
|
||||||
|
tab.set_readonly(true);
|
||||||
|
Ok(Value::Table(tab))
|
||||||
|
}
|
26
src/utils.rs
26
src/utils.rs
|
@ -37,15 +37,11 @@ pub struct GithubClient {
|
||||||
|
|
||||||
impl GithubClient {
|
impl GithubClient {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
|
let (github_owner, github_repo) = get_github_owner_and_repo();
|
||||||
.strip_prefix("https://github.com/")
|
|
||||||
.unwrap()
|
|
||||||
.split_once('/')
|
|
||||||
.unwrap();
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"User-Agent",
|
"User-Agent",
|
||||||
HeaderValue::from_str(&format!("{}-{}-cli", github_owner, github_repo))?,
|
HeaderValue::from_str(&get_github_user_agent_header())?,
|
||||||
);
|
);
|
||||||
headers.insert(
|
headers.insert(
|
||||||
"Accept",
|
"Accept",
|
||||||
|
@ -58,8 +54,8 @@ impl GithubClient {
|
||||||
let client = Client::builder().default_headers(headers).build()?;
|
let client = Client::builder().default_headers(headers).build()?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
client,
|
client,
|
||||||
github_owner: github_owner.to_string(),
|
github_owner,
|
||||||
github_repo: github_repo.to_string(),
|
github_repo,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,6 +124,20 @@ impl GithubClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_github_owner_and_repo() -> (String, String) {
|
||||||
|
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
|
||||||
|
.strip_prefix("https://github.com/")
|
||||||
|
.unwrap()
|
||||||
|
.split_once('/')
|
||||||
|
.unwrap();
|
||||||
|
(github_owner.to_owned(), github_repo.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_github_user_agent_header() -> String {
|
||||||
|
let (github_owner, github_repo) = get_github_owner_and_repo();
|
||||||
|
format!("{}-{}-cli", github_owner, github_repo)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pretty_print_luau_error(e: &mlua::Error) {
|
pub fn pretty_print_luau_error(e: &mlua::Error) {
|
||||||
match e {
|
match e {
|
||||||
mlua::Error::RuntimeError(e) => {
|
mlua::Error::RuntimeError(e) => {
|
||||||
|
|
Loading…
Reference in a new issue