diff --git a/Cargo.lock b/Cargo.lock index 3f8ed7d1940..0e9642c1074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1555,7 +1555,6 @@ dependencies = [ "dirs-next", "env_logger 0.10.2", "filesystem", - "json", "lazy_static", "libc", "log", @@ -1564,6 +1563,7 @@ dependencies = [ "objc", "plugin", "procinfo-funcs", + "serde-funcs", "share-data", "spawn-funcs", "ssh-funcs", @@ -2776,17 +2776,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.1.0" -dependencies = [ - "anyhow", - "config", - "luahelper", - "serde_json", - "wezterm-dynamic", -] - [[package]] name = "k9" version = "0.11.6" @@ -4766,6 +4755,19 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-funcs" +version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "luahelper", + "serde_json", + "serde_yaml", + "toml 0.8.9", + "wezterm-dynamic", +] + [[package]] name = "serde_cbor" version = "0.11.2" diff --git a/ci/generate-docs.py b/ci/generate-docs.py index a4d15fc96e4..a30c237ec01 100644 --- a/ci/generate-docs.py +++ b/ci/generate-docs.py @@ -394,6 +394,10 @@ def render(self, output, depth=0, mode="mdbook"): "module: wezterm.procinfo", "config/lua/wezterm.procinfo", ), + Gen( + "module: wezterm.serde", + "config/lua/wezterm.serde", + ), Gen( "module: wezterm.time", "config/lua/wezterm.time", diff --git a/docs/config/lua/wezterm.serde/index.markdown b/docs/config/lua/wezterm.serde/index.markdown new file mode 100644 index 00000000000..7ff61a8d8bd --- /dev/null +++ b/docs/config/lua/wezterm.serde/index.markdown @@ -0,0 +1,8 @@ +# `wezterm.serde` module + +{{since('nightly')}} + +The `wezterm.serde` module provides functions for parsing the given string as +`json`, `yaml`, or `toml`, returning the corresponding `Lua` values, and vice versa. + +## Available functions diff --git a/docs/config/lua/wezterm.serde/json_decode.md b/docs/config/lua/wezterm.serde/json_decode.md new file mode 100644 index 00000000000..9d8457e06a4 --- /dev/null +++ b/docs/config/lua/wezterm.serde/json_decode.md @@ -0,0 +1,12 @@ +# `wezterm.serde.json_decode(string)` + +{{since('nightly')}} + +Parses the supplied string as `json` and returns the equivalent `lua` values: + +``` +> wezterm.serde.json_decode('{"foo":"bar"}') +{ + "foo": "bar", +} +``` diff --git a/docs/config/lua/wezterm.serde/json_encode.md b/docs/config/lua/wezterm.serde/json_encode.md new file mode 100644 index 00000000000..c6d931025a7 --- /dev/null +++ b/docs/config/lua/wezterm.serde/json_encode.md @@ -0,0 +1,10 @@ +# `wezterm.serde.json_encode(value)` + +{{since('nightly')}} + +Encodes the supplied `lua` value as `json`: + +``` +> wezterm.serde.json_encode({foo = "bar"}) +"{\"foo\":\"bar\"}" +``` diff --git a/docs/config/lua/wezterm.serde/json_encode_pretty.md b/docs/config/lua/wezterm.serde/json_encode_pretty.md new file mode 100644 index 00000000000..d305362218c --- /dev/null +++ b/docs/config/lua/wezterm.serde/json_encode_pretty.md @@ -0,0 +1,10 @@ +# `wezterm.serde.json_encode_pretty(value)` + +{{since('nightly')}} + +Encodes the supplied `lua` value as a pretty-printed string of `json`: + +``` +> wezterm.serde.json_encode_pretty({foo = "bar"}) +"{\n \"foo\": \"bar\"\n}" +``` diff --git a/docs/config/lua/wezterm.serde/toml_decode.md b/docs/config/lua/wezterm.serde/toml_decode.md new file mode 100644 index 00000000000..db8f3e5084d --- /dev/null +++ b/docs/config/lua/wezterm.serde/toml_decode.md @@ -0,0 +1,12 @@ +# `wezterm.serde.toml_decode(string)` + +{{since('nightly')}} + +Parses the supplied string as `toml` and returns the equivalent `lua` values: + +``` +> wezterm.serde.toml_decode('foo = "bar"') +{ + "foo": "bar", +} +``` diff --git a/docs/config/lua/wezterm.serde/toml_encode.md b/docs/config/lua/wezterm.serde/toml_encode.md new file mode 100644 index 00000000000..0b3dcf440b1 --- /dev/null +++ b/docs/config/lua/wezterm.serde/toml_encode.md @@ -0,0 +1,10 @@ +# `wezterm.serde.toml_encode(value)` + +{{since('nightly')}} + +Encodes the supplied `lua` value as `toml`: + +``` +> wezterm.serde.toml_encode({foo = { "bar", "baz", "qux" } }) +"foo = [\"bar\", \"baz\", \"qux\"]\n" +``` diff --git a/docs/config/lua/wezterm.serde/toml_encode_pretty.md b/docs/config/lua/wezterm.serde/toml_encode_pretty.md new file mode 100644 index 00000000000..16fec3d4355 --- /dev/null +++ b/docs/config/lua/wezterm.serde/toml_encode_pretty.md @@ -0,0 +1,10 @@ +# `wezterm.serde.toml_encode_pretty(value)` + +{{since('nightly')}} + +Encodes the supplied `lua` value as a pretty-printed string of `toml`: + +``` +> wezterm.serde.toml_encode_pretty({foo = { "bar", "baz", "qux" } }) +"foo = [\n \"bar\",\n \"baz\",\n \"qux\",\n]\n" +``` diff --git a/docs/config/lua/wezterm.serde/yaml_decode.md b/docs/config/lua/wezterm.serde/yaml_decode.md new file mode 100644 index 00000000000..af48db865c5 --- /dev/null +++ b/docs/config/lua/wezterm.serde/yaml_decode.md @@ -0,0 +1,12 @@ +# `wezterm.serde.yaml_decode(string)` + +{{since('nightly')}} + +Parses the supplied string as `yaml` and returns the equivalent `lua` values: + +``` +> wezterm.serde.yaml_decode('---\n# comment\nfoo: "bar"') +{ + "foo": "bar", +} +``` diff --git a/docs/config/lua/wezterm.serde/yaml_encode.md b/docs/config/lua/wezterm.serde/yaml_encode.md new file mode 100644 index 00000000000..7be9378972a --- /dev/null +++ b/docs/config/lua/wezterm.serde/yaml_encode.md @@ -0,0 +1,10 @@ +# `wezterm.serde.yaml_encode(value)` + +{{since('nightly')}} + +Encodes the supplied `lua` value as `yaml`: + +``` +> wezterm.serde.yaml_encode({foo = "bar"}) +"foo: bar\n" +``` diff --git a/env-bootstrap/Cargo.toml b/env-bootstrap/Cargo.toml index 8dce2d87be5..d48e7df055a 100644 --- a/env-bootstrap/Cargo.toml +++ b/env-bootstrap/Cargo.toml @@ -23,7 +23,7 @@ logging = { path = "../lua-api-crates/logging" } mux-lua = { path = "../lua-api-crates/mux" } procinfo-funcs = { path = "../lua-api-crates/procinfo-funcs" } filesystem = { path = "../lua-api-crates/filesystem" } -json = { path = "../lua-api-crates/json" } +serde-funcs = { path = "../lua-api-crates/serde-funcs" } plugin = { path = "../lua-api-crates/plugin" } share-data = { path = "../lua-api-crates/share-data" } ssh-funcs = { path = "../lua-api-crates/ssh-funcs" } diff --git a/env-bootstrap/src/lib.rs b/env-bootstrap/src/lib.rs index d43cb0e9cff..c6eb5c619e0 100644 --- a/env-bootstrap/src/lib.rs +++ b/env-bootstrap/src/lib.rs @@ -199,7 +199,7 @@ fn register_lua_modules() { mux_lua::register, procinfo_funcs::register, filesystem::register, - json::register, + serde_funcs::register, plugin::register, ssh_funcs::register, spawn_funcs::register, diff --git a/lua-api-crates/json/Cargo.toml b/lua-api-crates/serde-funcs/Cargo.toml similarity index 84% rename from lua-api-crates/json/Cargo.toml rename to lua-api-crates/serde-funcs/Cargo.toml index f120ea2545f..0b624b097bd 100644 --- a/lua-api-crates/json/Cargo.toml +++ b/lua-api-crates/serde-funcs/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "json" +name = "serde-funcs" version = "0.1.0" edition = "2021" @@ -11,3 +11,5 @@ config = { path = "../../config" } luahelper = { path = "../../luahelper" } wezterm-dynamic = { path = "../../wezterm-dynamic" } serde_json = "1.0.82" +serde_yaml = "0.9.31" +toml = "0.8.9" diff --git a/lua-api-crates/json/src/lib.rs b/lua-api-crates/serde-funcs/src/lib.rs similarity index 60% rename from lua-api-crates/json/src/lib.rs rename to lua-api-crates/serde-funcs/src/lib.rs index b3dc5570d13..f3c5b841da4 100644 --- a/lua-api-crates/json/src/lib.rs +++ b/lua-api-crates/serde-funcs/src/lib.rs @@ -1,17 +1,85 @@ -use config::lua::get_or_create_module; use config::lua::mlua::{self, IntoLua, Lua, Value as LuaValue}; +use config::lua::{get_or_create_module, get_or_create_sub_module}; use luahelper::lua_value_to_dynamic; use serde_json::{Map, Value as JValue}; use std::collections::HashSet; use wezterm_dynamic::{FromDynamic, Value as DynValue}; pub fn register(lua: &Lua) -> anyhow::Result<()> { + let serde_mod = get_or_create_sub_module(lua, "serde")?; + + // Decoders: + serde_mod.set("json_decode", lua.create_function(json_decode)?)?; + serde_mod.set("yaml_decode", lua.create_function(yaml_decode)?)?; + serde_mod.set("toml_decode", lua.create_function(toml_decode)?)?; + + // Encoders: + serde_mod.set("json_encode", lua.create_function(json_encode)?)?; + serde_mod.set("yaml_encode", lua.create_function(yaml_encode)?)?; + serde_mod.set("toml_encode", lua.create_function(toml_encode)?)?; + // Pretty ones: + serde_mod.set( + "json_encode_pretty", + lua.create_function(json_encode_pretty)?, + )?; + serde_mod.set( + "toml_encode_pretty", + lua.create_function(toml_encode_pretty)?, + )?; + // Note there is no pretty encoder for yaml, because the default one is pretty already. + // See https://github.com/dtolnay/serde-yaml/issues/226 + + // For backward compatibility. let wezterm_mod = get_or_create_module(lua, "wezterm")?; - wezterm_mod.set("json_parse", lua.create_function(json_parse)?)?; + wezterm_mod.set("json_parse", lua.create_function(json_decode)?)?; wezterm_mod.set("json_encode", lua.create_function(json_encode)?)?; + Ok(()) } +fn json_encode(_: &Lua, value: LuaValue) -> mlua::Result { + let json = lua_value_to_json_value(value, &mut HashSet::new())?; + serde_json::to_string(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +} + +fn json_encode_pretty(_: &Lua, value: LuaValue) -> mlua::Result { + let json = lua_value_to_json_value(value, &mut HashSet::new())?; + serde_json::to_string_pretty(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +} + +fn yaml_encode(_: &Lua, value: LuaValue) -> mlua::Result { + let json = lua_value_to_json_value(value, &mut HashSet::new())?; + serde_yaml::to_string(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +} + +fn toml_encode(_: &Lua, value: LuaValue) -> mlua::Result { + let json = lua_value_to_json_value(value, &mut HashSet::new())?; + toml::to_string(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +} + +fn toml_encode_pretty(_: &Lua, value: LuaValue) -> mlua::Result { + let json = lua_value_to_json_value(value, &mut HashSet::new())?; + toml::to_string_pretty(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +} + +fn json_decode(lua: &Lua, text: String) -> mlua::Result { + let value = + serde_json::from_str(&text).map_err(|err| mlua::Error::external(format!("{err:#}")))?; + json_value_to_lua_value(lua, value) +} + +fn yaml_decode(lua: &Lua, text: String) -> mlua::Result { + let value: JValue = + serde_yaml::from_str(&text).map_err(|err| mlua::Error::external(format!("{err:#}")))?; + json_value_to_lua_value(lua, value) +} + +fn toml_decode(lua: &Lua, text: String) -> mlua::Result { + let value: JValue = + toml::from_str(&text).map_err(|err| mlua::Error::external(format!("{err:#}")))?; + json_value_to_lua_value(lua, value) +} + fn json_value_to_lua_value<'lua>(lua: &'lua Lua, value: JValue) -> mlua::Result { Ok(match value { JValue::Null => LuaValue::Nil, @@ -47,12 +115,6 @@ fn json_value_to_lua_value<'lua>(lua: &'lua Lua, value: JValue) -> mlua::Result< }) } -fn json_parse<'lua>(lua: &'lua Lua, text: String) -> mlua::Result { - let value = - serde_json::from_str(&text).map_err(|err| mlua::Error::external(format!("{err:#}")))?; - json_value_to_lua_value(lua, value) -} - fn dyn_to_json(value: DynValue) -> anyhow::Result { Ok(match value { DynValue::Null => JValue::Null, @@ -227,8 +289,78 @@ fn lua_value_to_json_value(value: LuaValue, visited: &mut HashSet) -> mlu }) } -fn json_encode<'lua>(_: &'lua Lua, value: LuaValue) -> mlua::Result { - let mut visited = HashSet::new(); - let json = lua_value_to_json_value(value, &mut visited)?; - serde_json::to_string(&json).map_err(|err| mlua::Error::external(format!("{err:#}"))) +#[cfg(test)] +mod test { + use super::*; + use serde_json::json; + + #[test] + fn test_json_encode_decode() { + // We use the json Value from serde_json crate as input, and convert the LuaValue, + // propagate through the encode and decode processes, and convert it back to the json Value for result checking. + let j0 = json!({ + "key2str": "value1", "key2int": 4, "key2float": 4.5, + "key2arr": vec![2, 3], "key2dict": {"a": "a_value", "b": 3}}); + + let lua = Lua::new(); + let v0 = json_value_to_lua_value(&lua, j0.clone()).unwrap(); + let s = json_encode(&lua, v0.clone()).unwrap(); + let j1: JValue = serde_json::from_str(&s).unwrap(); + assert_eq!(j0, j1); + let v1 = json_decode(&lua, s).unwrap(); + let j1 = lua_value_to_json_value(v1, &mut HashSet::new()).unwrap(); + assert_eq!(j0, j1); + + // Do it again with the pretty variant. + let s = json_encode_pretty(&lua, v0.clone()).unwrap(); + let j1: JValue = serde_json::from_str(&s).unwrap(); + assert_eq!(j0, j1); + let v1 = json_decode(&lua, s).unwrap(); + let j1 = lua_value_to_json_value(v1, &mut HashSet::new()).unwrap(); + assert_eq!(j0, j1); + } + + #[test] + fn test_yaml_encode_decode() { + // We use the json Value from serde_json crate as input, and convert the LuaValue, + // propagate through the encode and decode processes, and convert it back to the json Value for result checking. + let j0 = json!({ + "key2str": "value1", "key2int": 4, "key2float": 4.5, + "key2arr": vec![2, 3], "key2dict": {"a": "a_value", "b": 3}}); + + let lua = Lua::new(); + let v0 = json_value_to_lua_value(&lua, j0.clone()).unwrap(); + let s = yaml_encode(&lua, v0.clone()).unwrap(); + let j1: JValue = serde_yaml::from_str(&s).unwrap(); + assert_eq!(j0, j1); + let v1 = yaml_decode(&lua, s).unwrap(); + let j1 = lua_value_to_json_value(v1, &mut HashSet::new()).unwrap(); + assert_eq!(j0, j1); + } + + #[test] + fn test_toml_encode_decode() { + // We use the json Value from serde_json crate as input, and convert the LuaValue, + // propagate through the encode and decode processes, and convert it back to the json Value for result checking. + let j0 = json!({ + "key2str": "value1", "key2int": 4, "key2float": 4.5, + "key2arr": vec![2, 3], "key2dict": {"a": "a_value", "b": 3}}); + + let lua = Lua::new(); + let v0 = json_value_to_lua_value(&lua, j0.clone()).unwrap(); + let s = toml_encode(&lua, v0.clone()).unwrap(); + let j1: JValue = toml::from_str(&s).unwrap(); + assert_eq!(j0, j1); + let v1 = toml_decode(&lua, s).unwrap(); + let j1 = lua_value_to_json_value(v1, &mut HashSet::new()).unwrap(); + assert_eq!(j0, j1); + + // Do it again with the pretty variant. + let s = toml_encode_pretty(&lua, v0.clone()).unwrap(); + let j1: JValue = toml::from_str(&s).unwrap(); + assert_eq!(j0, j1); + let v1 = toml_decode(&lua, s).unwrap(); + let j1 = lua_value_to_json_value(v1, &mut HashSet::new()).unwrap(); + assert_eq!(j0, j1); + } }