diff --git a/CHANGELOG.md b/CHANGELOG.md index c7600fe..bc6bec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,37 @@ 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). +## `0.5.1` - February 25th, 2023 + +### Added + +- Added `net.encode` and `net.decode` which are equivalent to `net.jsonEncode` and `net.jsonDecode`, but with support for more formats. + + **_WARNING: Unstable API_** + + _This API is unstable and may change or be removed in the next major version of Lune. The purpose of making a new release with these functions is to gather feedback from the community, and potentially replace the JSON-specific encoding and decoding utilities._ + + Example usage: + + ```lua + local toml = net.decode("toml", [[ + [package] + name = "my-cool-toml-package" + version = "0.1.0" + + [values] + epic = true + ]]) + + assert(toml.package.name == "my-cool-toml-package") + assert(toml.package.version == "0.1.0") + assert(toml.values.epic == true) + ``` + +### Fixed + +- Fixed indentation of closing curly bracket when printing tables + ## `0.5.0` - February 23rd, 2023 ### Added diff --git a/Cargo.lock b/Cargo.lock index 3bf7faf..d4dd4e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -797,6 +797,7 @@ dependencies = [ "serde_yaml", "tokio", "tokio-tungstenite", + "toml", ] [[package]] @@ -1276,6 +1277,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1510,6 +1520,41 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -1858,6 +1903,15 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +[[package]] +name = "winnow" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c06e7dbfe731192c512aa707249279d720876a868eb08f21ea93eb9de1eca9" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" diff --git a/Cargo.toml b/Cargo.toml index 22bfbf7..62f417c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ members = ["packages/cli", "packages/lib"] default-members = ["packages/cli"] +# Package config values shared across all packages, +# such as version, license, and other metadata [workspace.package] version = "0.5.0" edition = "2021" @@ -12,23 +14,32 @@ readme = "README.md" keywords = ["cli", "lua", "luau", "scripts"] categories = ["command-line-interface"] +# Shared dependencies that are used across 2 or more packages +# These are declared here to ensure consistent versioning [workspace.dependencies] console = "0.15" futures-util = "0.3" lazy_static = "1.4" +# Serde dependencies, supporting user-facing formats: json, yaml, toml serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } serde_yaml = "0.9" +toml = { version = "0.7", features = ["preserve_order"] } + +# Tokio runtime & async clients tokio = { version = "1.24", features = ["full"] } +reqwest = { version = "0.11", default-features = false, features = [ + "rustls-tls", +] } -[workspace.dependencies.reqwest] -version = "0.11" -default-features = false -features = ["rustls-tls"] - +# Profile for building the release binary, with the following options set: +# 1. Optimize for size +# 2. Automatically strip symbols from the binary +# 3. Enable link-time optimization +# 4. Remove extra panic info [profile.release] -strip = true # Automatically strip symbols from the binary. -opt-level = "z" # Optimize for size. -lto = true # Enable link-time optimization -panic = "abort" # Remove extra panic info +opt-level = "z" +strip = true +lto = true +panic = "abort" diff --git a/docs/luneTypes.d.luau b/docs/luneTypes.d.luau index cb5a785..4b3359c 100644 --- a/docs/luneTypes.d.luau +++ b/docs/luneTypes.d.luau @@ -14,7 +14,7 @@ export type FsWriteOptions = { --[=[ @class FS - + Filesystem ]=] declare fs: { @@ -40,7 +40,7 @@ declare fs: { @must_use Reads entries in a directory at `path`. - + An error will be thrown in the following situations: * `path` does not point to an existing directory. @@ -146,9 +146,9 @@ declare fs: { Throws an error if a file or directory already exists at the target path. This can be bypassed by passing `true` as the third argument, or a dictionary of options. Refer to the documentation for `FsWriteOptions` for specific option keys and their values. - + An error will be thrown in the following situations: - + * The current process lacks permissions to read at `from` or write at `to`. * The new path exists on a different mount point. * Some other I/O error occurred. @@ -162,6 +162,8 @@ declare fs: { type NetMethod = "GET" | "POST" | "PUT" | "DELETE" | "HEAD" | "OPTIONS" | "PATCH" +type NetEncodeDecodeFormat = "json" | "yaml" | "toml" + --[=[ @type NetFetchParams @within Net @@ -231,7 +233,7 @@ export type NetRequest = { --[=[ @type NetRequest @within Net - + Response type for requests in `net.serve`. This is a dictionary that may contain one or more of the following values: @@ -280,7 +282,7 @@ export type NetServeHandle = { @within Net A reference to a web socket connection. - + The web socket may be in either an "open" or a "closed" state, changing its current behavior. When open: @@ -311,7 +313,7 @@ declare net: { @within Net Sends an HTTP request using the given url and / or parameters, and returns a dictionary that describes the response received. - + Only throws an error if a miscellaneous network or I/O error occurs, never for unsuccessful status codes. @param config The URL or request config to use @@ -323,7 +325,7 @@ declare net: { @must_use Connects to a web socket at the given URL. - + Throws an error if the server at the given URL does not support web sockets, or if a miscellaneous network or I/O error occurs. @@ -364,6 +366,37 @@ declare net: { @return The decoded lua value ]=] jsonDecode: (encoded: string) -> any, + --[=[ + @within Net + @must_use + + ***WARNING:** Unstable API* + + *This API is unstable and may change or be removed in the next major version of Lune.* + + Encodes the given value using the given format. + + @param format The format to use + @param value The value to encode + @param pretty If the encoded string should be human-readable, including things such as newlines and spaces. Only supported for json and toml formats, and defaults to false + @return The encoded string + ]=] + encode: (format: NetEncodeDecodeFormat, value: any, pretty: boolean?) -> string, + --[=[ + @within Net + @must_use + + ***WARNING:** Unstable API* + + *This API is unstable and may change or be removed in the next major version of Lune.* + + Decodes the given string using the given format into a lua value. + + @param format The format to use + @param encoded The JSON string to decode + @return The decoded lua value + ]=] + decode: (format: NetEncodeDecodeFormat, encoded: string) -> any, } type ProcessSpawnOptionsStdio = "inherit" | "default" @@ -415,9 +448,9 @@ declare process: { --[=[ @within Process @read_only - + The current operating system being used. - + Possible values: * `"linux"` @@ -428,9 +461,9 @@ declare process: { --[=[ @within Process @read_only - + The architecture of the processor currently being used. - + Possible values: * `"x86_64"` @@ -468,13 +501,13 @@ declare process: { Exit code 0 is treated as a successful exit, any other value is treated as an error. Setting the exit code using this function will override any otherwise automatic exit code. - + @param code The exit code to set ]=] exit: (code: number?) -> (), --[=[ @within Process - + Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process. The second argument, `params`, can be passed as a list of string parameters to give to the program. @@ -497,7 +530,7 @@ declare process: { --[=[ @class Stdio - Standard input / output & utility functions + Standard input / output & utility functions ]=] declare stdio: { --[=[ @@ -507,9 +540,9 @@ declare stdio: { Return an ANSI string that can be used to modify the persistent output color. Pass `"reset"` to get a string that can reset the persistent output color. - + ### Example usage - + ```lua stdio.write(stdio.color("red")) print("This text will be red") @@ -528,9 +561,9 @@ declare stdio: { Return an ANSI string that can be used to modify the persistent output style. Pass `"reset"` to get a string that can reset the persistent output style. - + ### Example usage - + ```lua stdio.write(stdio.style("bold")) print("This text will be bold") @@ -629,7 +662,7 @@ declare task: { @within Task Instantly runs a thread or function. - + If the spawned task yields, the thread that spawned the task will resume, letting the spawned task run in the background. diff --git a/packages/lib/Cargo.toml b/packages/lib/Cargo.toml index 8ea0f9b..0b41db5 100644 --- a/packages/lib/Cargo.toml +++ b/packages/lib/Cargo.toml @@ -21,6 +21,7 @@ lazy_static.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true +toml.workspace = true tokio.workspace = true reqwest.workspace = true diff --git a/packages/lib/src/globals/net.rs b/packages/lib/src/globals/net.rs index 24de41e..767d49f 100644 --- a/packages/lib/src/globals/net.rs +++ b/packages/lib/src/globals/net.rs @@ -8,8 +8,8 @@ use tokio::{sync::mpsc, task}; use crate::lua::{ net::{ - NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig, - ServeConfig, + EncodeDecodeConfig, EncodeDecodeFormat, NetClient, NetClientBuilder, NetLocalExec, + NetService, NetWebSocket, RequestConfig, ServeConfig, }, table::TableBuilder, task::{TaskScheduler, TaskSchedulerAsyncExt}, @@ -25,6 +25,8 @@ pub fn create(lua: &'static Lua) -> LuaResult { lua.set_named_registry_value("net.client", client)?; // Create the global table for net TableBuilder::new(lua)? + .with_function("encode", net_encode)? + .with_function("decode", net_decode)? .with_function("jsonEncode", net_json_encode)? .with_function("jsonDecode", net_json_decode)? .with_async_function("request", net_request)? @@ -42,17 +44,32 @@ fn create_user_agent_header() -> String { format!("{github_owner}-{github_repo}-cli") } -fn net_json_encode(_: &'static Lua, (val, pretty): (LuaValue, Option)) -> LuaResult { - if let Some(true) = pretty { - serde_json::to_string_pretty(&val).map_err(LuaError::external) - } else { - serde_json::to_string(&val).map_err(LuaError::external) - } +fn net_encode<'a>( + lua: &'static Lua, + (format, val, pretty): (EncodeDecodeFormat, LuaValue<'a>, Option), +) -> LuaResult> { + let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default())); + config.serialize_to_string(lua, val) } -fn net_json_decode(lua: &'static Lua, json: String) -> LuaResult { - let json: serde_json::Value = serde_json::from_str(&json).map_err(LuaError::external)?; - lua.to_value(&json) +fn net_decode<'a>( + lua: &'static Lua, + (format, str): (EncodeDecodeFormat, LuaString<'a>), +) -> LuaResult> { + let config = EncodeDecodeConfig::from(format); + config.deserialize_from_string(lua, str) +} + +fn net_json_encode<'a>( + lua: &'static Lua, + (val, pretty): (LuaValue<'a>, Option), +) -> LuaResult> { + EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default())) + .serialize_to_string(lua, val) +} + +fn net_json_decode<'a>(lua: &'static Lua, json: LuaString<'a>) -> LuaResult> { + EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json) } async fn net_request<'a>(lua: &'static Lua, config: RequestConfig<'a>) -> LuaResult> { diff --git a/packages/lib/src/lua/net/mod.rs b/packages/lib/src/lua/net/mod.rs index c52b780..604b1d3 100644 --- a/packages/lib/src/lua/net/mod.rs +++ b/packages/lib/src/lua/net/mod.rs @@ -1,9 +1,11 @@ mod client; mod config; mod response; +mod serde; mod server; mod websocket; +pub use self::serde::{EncodeDecodeConfig, EncodeDecodeFormat}; pub use client::{NetClient, NetClientBuilder}; pub use config::{RequestConfig, ServeConfig}; pub use response::{NetServeResponse, NetServeResponseKind}; diff --git a/packages/lib/src/lua/net/serde.rs b/packages/lib/src/lua/net/serde.rs new file mode 100644 index 0000000..5caeccf --- /dev/null +++ b/packages/lib/src/lua/net/serde.rs @@ -0,0 +1,123 @@ +use mlua::prelude::*; + +use serde_json::Value as JsonValue; +use serde_yaml::Value as YamlValue; +use toml::Value as TomlValue; + +// Serde config + +#[derive(Debug, Clone, Copy)] +pub enum EncodeDecodeFormat { + Json, + Yaml, + Toml, +} + +impl<'lua> FromLua<'lua> for EncodeDecodeFormat { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + if let LuaValue::String(s) = &value { + match s.to_string_lossy().to_ascii_lowercase().trim() { + "json" => Ok(Self::Json), + "yaml" => Ok(Self::Yaml), + "toml" => Ok(Self::Toml), + kind => Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "EncodeDecodeFormat", + message: Some(format!( + "Invalid format '{kind}', valid formats are: json, yaml, toml" + )), + }), + } + } else { + Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "EncodeDecodeFormat", + message: None, + }) + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct EncodeDecodeConfig { + pub format: EncodeDecodeFormat, + pub pretty: bool, +} + +impl EncodeDecodeConfig { + pub fn serialize_to_string<'lua>( + self, + lua: &'lua Lua, + value: LuaValue<'lua>, + ) -> LuaResult> { + let bytes = match self.format { + EncodeDecodeFormat::Json => { + if self.pretty { + serde_json::to_vec_pretty(&value).map_err(LuaError::external)? + } else { + serde_json::to_vec(&value).map_err(LuaError::external)? + } + } + EncodeDecodeFormat::Yaml => { + let mut writer = Vec::with_capacity(128); + serde_yaml::to_writer(&mut writer, &value).map_err(LuaError::external)?; + writer + } + EncodeDecodeFormat::Toml => { + let s = if self.pretty { + toml::to_string_pretty(&value).map_err(LuaError::external)? + } else { + toml::to_string(&value).map_err(LuaError::external)? + }; + s.as_bytes().to_vec() + } + }; + lua.create_string(&bytes) + } + + pub fn deserialize_from_string<'lua>( + self, + lua: &'lua Lua, + string: LuaString<'lua>, + ) -> LuaResult> { + let bytes = string.as_bytes(); + match self.format { + EncodeDecodeFormat::Json => { + let value: JsonValue = serde_json::from_slice(bytes).map_err(LuaError::external)?; + lua.to_value(&value) + } + EncodeDecodeFormat::Yaml => { + let value: YamlValue = serde_yaml::from_slice(bytes).map_err(LuaError::external)?; + lua.to_value(&value) + } + EncodeDecodeFormat::Toml => { + if let Ok(s) = string.to_str() { + let value: TomlValue = toml::from_str(s).map_err(LuaError::external)?; + lua.to_value(&value) + } else { + Err(LuaError::RuntimeError( + "TOML must be valid utf-8".to_string(), + )) + } + } + } + } +} + +impl From for EncodeDecodeConfig { + fn from(format: EncodeDecodeFormat) -> Self { + Self { + format, + pretty: false, + } + } +} + +impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig { + fn from(value: (EncodeDecodeFormat, bool)) -> Self { + Self { + format: value.0, + pretty: value.1, + } + } +} diff --git a/packages/lib/src/lua/stdio/formatting.rs b/packages/lib/src/lua/stdio/formatting.rs index f843488..d132c8a 100644 --- a/packages/lib/src/lua/stdio/formatting.rs +++ b/packages/lib/src/lua/stdio/formatting.rs @@ -156,7 +156,7 @@ pub fn pretty_format_value( if is_empty { write!(buffer, "{}", STYLE_DIM.apply_to(" }"))?; } else { - write!(buffer, "\n{}", STYLE_DIM.apply_to("}"))?; + write!(buffer, "\n{depth_indent}{}", STYLE_DIM.apply_to("}"))?; } } }