From 4a4787ff41a7584b174dc8ad141245939c7c2627 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sun, 10 Jul 2022 09:30:54 -0700 Subject: [PATCH] add wezterm.json_parse and wezterm.json_encode --- Cargo.lock | 11 ++ docs/changelog.md | 1 + docs/config/lua/wezterm/json_encode.md | 10 ++ docs/config/lua/wezterm/json_parse.md | 12 ++ env-bootstrap/Cargo.toml | 1 + env-bootstrap/src/lib.rs | 1 + lua-api-crates/json/Cargo.toml | 12 ++ lua-api-crates/json/src/lib.rs | 166 +++++++++++++++++++++++++ 8 files changed, 214 insertions(+) create mode 100644 docs/config/lua/wezterm/json_encode.md create mode 100644 docs/config/lua/wezterm/json_parse.md create mode 100644 lua-api-crates/json/Cargo.toml create mode 100644 lua-api-crates/json/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e07e4ffb1..0dfb76c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1096,6 +1096,7 @@ dependencies = [ "dirs-next", "env_logger", "filesystem", + "json", "lazy_static", "libc", "log", @@ -1835,6 +1836,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json" +version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "luahelper", + "serde_json", +] + [[package]] name = "k9" version = "0.11.5" diff --git a/docs/changelog.md b/docs/changelog.md index c5ccb2337..2de5c8341 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -23,6 +23,7 @@ As features stabilize some brief notes about them will accumulate here. * You may now use [wezterm.format](config/lua/wezterm/format.md) (or otherwise use strings with escape sequences) in the labels of the [Launcher Menu](config/launch.md#the-launcher-menu). * You may now specify `assume_emoji_presentation = true` (or `false`) in [wezterm.font()](config/lua/wezterm/font.md) and [wezterm.font_with_fallback()](config/lua/wezterm/font_with_fallback.md) * Wayland: `zwp_text_input_v3` is now supported, which enables IME to work in wezterm if your compositor also implements this protocol. +* New [wezterm.json_parse()](config/lua/wezterm/json_parse.md) and [wezterm.json_encode()](config/lua/wezterm/json_encode.md) functions for working with JSON. #### Fixed * [ActivateKeyTable](config/lua/keyassignment/ActivateKeyTable.md)'s `replace_current` field was not actually optional. Made it optional. [#2179](https://github.com/wez/wezterm/issues/2179) diff --git a/docs/config/lua/wezterm/json_encode.md b/docs/config/lua/wezterm/json_encode.md new file mode 100644 index 000000000..9cfe85b2c --- /dev/null +++ b/docs/config/lua/wezterm/json_encode.md @@ -0,0 +1,10 @@ +# `wezterm.json_encode(value)` + +*Since: nightly builds only* + +Encodes the supplied lua value as json: + +``` +> wezterm.json_encode({foo = "bar"}) +"{\"foo\":\"bar\"}" +``` diff --git a/docs/config/lua/wezterm/json_parse.md b/docs/config/lua/wezterm/json_parse.md new file mode 100644 index 000000000..2f87f0e45 --- /dev/null +++ b/docs/config/lua/wezterm/json_parse.md @@ -0,0 +1,12 @@ +# `wezterm.json_parse(string)` + +*Since: nightly builds only* + +Parses the supplied string as json and returns the equivalent lua values: + +``` +> wezterm.json_parse('{"foo":"bar"}') +{ + "foo": "bar", +} +``` diff --git a/env-bootstrap/Cargo.toml b/env-bootstrap/Cargo.toml index 931e5e202..ae5c17a62 100644 --- a/env-bootstrap/Cargo.toml +++ b/env-bootstrap/Cargo.toml @@ -21,6 +21,7 @@ termwiz-funcs = { path = "../lua-api-crates/termwiz-funcs" } logging = { path = "../lua-api-crates/logging" } mux-lua = { path = "../lua-api-crates/mux" } filesystem = { path = "../lua-api-crates/filesystem" } +json = { path = "../lua-api-crates/json" } share-data = { path = "../lua-api-crates/share-data" } ssh-funcs = { path = "../lua-api-crates/ssh-funcs" } spawn-funcs = { path = "../lua-api-crates/spawn-funcs" } diff --git a/env-bootstrap/src/lib.rs b/env-bootstrap/src/lib.rs index ef864870b..bbe04d55a 100644 --- a/env-bootstrap/src/lib.rs +++ b/env-bootstrap/src/lib.rs @@ -165,6 +165,7 @@ fn register_lua_modules() { logging::register, mux_lua::register, filesystem::register, + json::register, ssh_funcs::register, spawn_funcs::register, share_data::register, diff --git a/lua-api-crates/json/Cargo.toml b/lua-api-crates/json/Cargo.toml new file mode 100644 index 000000000..2cfbd7322 --- /dev/null +++ b/lua-api-crates/json/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "json" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +config = { path = "../../config" } +luahelper = { path = "../../luahelper" } +serde_json = "1.0.82" diff --git a/lua-api-crates/json/src/lib.rs b/lua-api-crates/json/src/lib.rs new file mode 100644 index 000000000..5d4ddc4dd --- /dev/null +++ b/lua-api-crates/json/src/lib.rs @@ -0,0 +1,166 @@ +use config::lua::get_or_create_module; +use config::lua::mlua::{self, Lua, ToLua, Value as LuaValue}; +use serde_json::{Map, Value as JValue}; +use std::collections::HashSet; + +pub fn register(lua: &Lua) -> anyhow::Result<()> { + let wezterm_mod = get_or_create_module(lua, "wezterm")?; + wezterm_mod.set("json_parse", lua.create_function(json_parse)?)?; + wezterm_mod.set("json_encode", lua.create_function(json_encode)?)?; + Ok(()) +} + +fn json_value_to_lua_value<'lua>(lua: &'lua Lua, value: JValue) -> mlua::Result { + Ok(match value { + JValue::Null => LuaValue::Nil, + JValue::Bool(b) => LuaValue::Boolean(b), + JValue::Number(n) => match n.as_i64() { + Some(n) => LuaValue::Integer(n), + None => match n.as_f64() { + Some(n) => LuaValue::Number(n), + None => { + return Err(mlua::Error::external(format!( + "cannot represent {n:#?} as either i64 or f64" + ))) + } + }, + }, + JValue::String(s) => s.to_lua(lua)?, + JValue::Array(arr) => { + let tbl = lua.create_table_with_capacity(arr.len() as i32, 0)?; + for (idx, value) in arr.into_iter().enumerate() { + tbl.set(idx + 1, json_value_to_lua_value(lua, value)?)?; + } + LuaValue::Table(tbl) + } + JValue::Object(map) => { + let tbl = lua.create_table_with_capacity(0, map.len() as i32)?; + for (key, value) in map.into_iter() { + let key = key.to_lua(lua)?; + let value = json_value_to_lua_value(lua, value)?; + tbl.set(key, value)?; + } + LuaValue::Table(tbl) + } + }) +} + +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 lua_value_to_json_value(value: LuaValue, visited: &mut HashSet) -> mlua::Result { + let ptr = value.to_pointer() as usize; + if visited.contains(&ptr) { + // Skip this one, as we've seen it before. + // Treat it as a Null value. + return Ok(JValue::Null); + } + visited.insert(ptr); + Ok(match value { + LuaValue::Nil => JValue::Null, + LuaValue::String(s) => JValue::String(s.to_str()?.to_string()), + LuaValue::Boolean(b) => JValue::Bool(b), + LuaValue::Integer(i) => JValue::Number(i.into()), + LuaValue::Number(i) => { + if let Some(n) = serde_json::value::Number::from_f64(i) { + JValue::Number(n) + } else { + return Err(mlua::Error::FromLuaConversionError { + from: "number", + to: "JsonValue", + message: Some(format!("unable to represent {i} as json float")), + }); + } + } + // Handle our special Null userdata case and map it to Null + LuaValue::LightUserData(ud) if ud.0.is_null() => JValue::Null, + LuaValue::LightUserData(_) | LuaValue::UserData(_) => { + return Err(mlua::Error::FromLuaConversionError { + from: "userdata", + to: "JsonValue", + message: None, + }) + } + LuaValue::Function(_) => { + return Err(mlua::Error::FromLuaConversionError { + from: "function", + to: "JsonValue", + message: None, + }) + } + LuaValue::Thread(_) => { + return Err(mlua::Error::FromLuaConversionError { + from: "thread", + to: "JsonValue", + message: None, + }) + } + LuaValue::Error(e) => return Err(e), + LuaValue::Table(table) => { + if let Ok(true) = table.contains_key(1) { + let mut array = vec![]; + let pairs = table.clone(); + for value in table.sequence_values() { + array.push(lua_value_to_json_value(value?, visited)?); + } + + for pair in pairs.pairs::() { + let (key, _value) = pair?; + match &key { + LuaValue::Integer(n) if *n >= 1 && *n as usize <= array.len() => { + // Ok! + } + _ => { + let type_name = key.type_name(); + let key = luahelper::ValuePrinter(key); + return Err(mlua::Error::FromLuaConversionError { + from: type_name, + to: "numeric array index", + message: Some(format!( + "Unexpected key {key:?} for array style table" + )), + }); + } + } + } + + JValue::Array(array.into()) + } else { + let mut obj = Map::default(); + for pair in table.pairs::() { + let (key, value) = pair?; + let key_type = key.type_name(); + let key = match lua_value_to_json_value(key, visited)? { + JValue::String(s) => s, + _ => { + return Err(mlua::Error::FromLuaConversionError { + from: key_type, + to: "string", + message: Some("json object keys must be strings".to_string()), + }); + } + }; + let lua_type = value.type_name(); + let value = lua_value_to_json_value(value, visited).map_err(|e| { + mlua::Error::FromLuaConversionError { + from: lua_type, + to: "value", + message: Some(format!("while processing {key:?}: {}", e.to_string())), + } + })?; + obj.insert(key, value); + } + JValue::Object(obj.into()) + } + } + }) +} + +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:#}"))) +}