mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +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
|
||||
|
||||
We use the result from the above ping command and parse it
|
||||
to show the results it gave us in a nicer format, then we
|
||||
either exit successfully or with an error (exit code 1)
|
||||
We use the result from the above ping command and parse
|
||||
it to show the results it gave us in a nicer format, here we
|
||||
also exit with an error (exit code 1) if spawning the process failed
|
||||
]==]
|
||||
|
||||
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("Standard deviation: %.3fms", assert(tonumber(stddev))))
|
||||
else
|
||||
print("\nFailed to send ping to google!")
|
||||
print(result.stderr)
|
||||
process.exit(result.code)
|
||||
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! 🌙")
|
||||
|
|
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/),
|
||||
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
|
||||
|
||||
### Added
|
||||
|
|
20
README.md
20
README.md
|
@ -47,12 +47,24 @@ type fs = {
|
|||
}
|
||||
```
|
||||
|
||||
### **`json`** - JSON
|
||||
### **`net`** - Networking
|
||||
|
||||
```lua
|
||||
type json = {
|
||||
encode: (value: any, pretty: boolean?) -> string,
|
||||
decode: (encoded: string) -> any,
|
||||
type net = {
|
||||
request: (config: string | {
|
||||
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:
|
||||
args:
|
||||
- type: string
|
||||
# JSON
|
||||
json.encode:
|
||||
# Net (networking)
|
||||
net.jsonEncode:
|
||||
args:
|
||||
- type: any
|
||||
- required: false
|
||||
type: boolean
|
||||
json.decode:
|
||||
net.jsonDecode:
|
||||
args:
|
||||
- type: string
|
||||
net.request:
|
||||
args:
|
||||
- type: any
|
||||
# Process
|
||||
process.getEnvVars:
|
||||
process.getEnvVar:
|
||||
|
|
|
@ -11,9 +11,21 @@ declare fs: {
|
|||
isDir: (path: string) -> boolean,
|
||||
}
|
||||
|
||||
declare json: {
|
||||
encode: (value: any, pretty: boolean?) -> string,
|
||||
decode: (encoded: string) -> any,
|
||||
declare net: {
|
||||
request: (config: string | {
|
||||
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: {
|
||||
|
|
|
@ -7,7 +7,7 @@ use clap::{CommandFactory, Parser};
|
|||
use mlua::{Lua, MultiValue, Result, ToLua};
|
||||
|
||||
use crate::{
|
||||
lune::{fs::LuneFs, json::LuneJson, process::LuneProcess},
|
||||
lune::{fs::LuneFs, net::LuneNet, process::LuneProcess},
|
||||
utils::GithubClient,
|
||||
};
|
||||
|
||||
|
@ -102,8 +102,8 @@ impl Cli {
|
|||
let lua = Lua::new();
|
||||
let globals = lua.globals();
|
||||
globals.set("fs", LuneFs::new())?;
|
||||
globals.set("net", LuneNet::new())?;
|
||||
globals.set("process", LuneProcess::new())?;
|
||||
globals.set("json", LuneJson::new())?;
|
||||
lua.sandbox(true)?;
|
||||
// Load & call the file with the given args
|
||||
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 json;
|
||||
pub mod net;
|
||||
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 {
|
||||
pub fn new() -> Result<Self> {
|
||||
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
|
||||
.strip_prefix("https://github.com/")
|
||||
.unwrap()
|
||||
.split_once('/')
|
||||
.unwrap();
|
||||
let (github_owner, github_repo) = get_github_owner_and_repo();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"User-Agent",
|
||||
HeaderValue::from_str(&format!("{}-{}-cli", github_owner, github_repo))?,
|
||||
HeaderValue::from_str(&get_github_user_agent_header())?,
|
||||
);
|
||||
headers.insert(
|
||||
"Accept",
|
||||
|
@ -58,8 +54,8 @@ impl GithubClient {
|
|||
let client = Client::builder().default_headers(headers).build()?;
|
||||
Ok(Self {
|
||||
client,
|
||||
github_owner: github_owner.to_string(),
|
||||
github_repo: github_repo.to_string(),
|
||||
github_owner,
|
||||
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) {
|
||||
match e {
|
||||
mlua::Error::RuntimeError(e) => {
|
||||
|
|
Loading…
Reference in a new issue