Add networking functions & move json apis

This commit is contained in:
Filip Tibell 2023-01-19 17:56:12 -05:00
parent 07e3b1fc5e
commit 3ccb31d918
No known key found for this signature in database
10 changed files with 278 additions and 52 deletions

View file

@ -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! 🌙")

View file

@ -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

View file

@ -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,
} }
``` ```

View file

@ -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:

View file

@ -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: {

View file

@ -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

View file

@ -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)
}

View file

@ -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
View 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))
}

View file

@ -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) => {