diff --git a/modules/module-lua-scripting/api/api.c b/modules/module-lua-scripting/api/api.c index 64bc7e00..35248679 100644 --- a/modules/module-lua-scripting/api/api.c +++ b/modules/module-lua-scripting/api/api.c @@ -1713,6 +1713,86 @@ static const luaL_Reg conf_methods[] = { { NULL, NULL } }; +/* JsonUtils */ + +static gboolean +json_utils_match_rules_cb (gpointer data, const gchar * action, + WpSpaJson * value, GError ** error) +{ + lua_State *L = data; + g_autoptr (GError) pcall_err = NULL; + gboolean ret = TRUE; + int top = lua_gettop (L); + + /* this callback is called within the context of json_utils_match_rules() + * and the Lua callback function is at the top of the stack at this point */ + + /* re-push the function because lua_call pops it; then, push arguments */ + lua_pushvalue (L, -1); + lua_pushstring (L, action); + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_ref (value)); + + lua_call (L, 2, 2); + + ret = lua_toboolean (L, -2); + if (!ret) { + g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED, + "%s", lua_tostring (L, -1)); + } + + lua_settop (L, top); + return ret; +} + +static int +json_utils_match_rules (lua_State *L) +{ + g_autoptr (WpProperties) properties = NULL; + g_autoptr (GError) error = NULL; + WpSpaJson *json; + gboolean res; + + json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + luaL_checktype (L, 2, LUA_TTABLE); + luaL_checktype (L, 3, LUA_TFUNCTION); + + properties = wplua_table_to_properties (L, 2); + + res = wp_json_utils_match_rules (json, properties, json_utils_match_rules_cb, + L, &error); + + lua_pushboolean (L, res); + if (error) + lua_pushstring (L, error->message); + else + lua_pushnil (L); + return 2; +} + +static int +json_utils_match_rules_update_properties (lua_State *L) +{ + g_autoptr (WpProperties) properties = NULL; + WpSpaJson *json; + int count; + + json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + luaL_checktype (L, 2, LUA_TTABLE); + properties = wplua_table_to_properties (L, 2); + + count = wp_json_utils_match_rules_update_properties (json, properties); + + lua_pushinteger (L, count); + wplua_properties_to_table (L, properties); + return 2; +} + +static const luaL_Reg json_utils_funcs[] = { + { "match_rules", json_utils_match_rules }, + { "match_rules_update_properties", json_utils_match_rules_update_properties }, + { NULL, NULL } +}; + /* WpSettings */ static int @@ -2390,6 +2470,9 @@ wp_lua_scripting_api_init (lua_State *L) luaL_newlib (L, conf_methods); lua_setglobal (L, "WpConf"); + luaL_newlib (L, json_utils_funcs); + lua_setglobal (L, "JsonUtils"); + luaL_newlib (L, settings_methods); lua_setglobal (L, "WpSettings"); diff --git a/modules/module-lua-scripting/api/api.lua b/modules/module-lua-scripting/api/api.lua index 7fe5e85f..a000d96c 100644 --- a/modules/module-lua-scripting/api/api.lua +++ b/modules/module-lua-scripting/api/api.lua @@ -212,6 +212,7 @@ SANDBOX_EXPORT = { ImplMetadata = WpImplMetadata_new, Settings = WpSettings, Conf = WpConf, + JsonUtils = JsonUtils, SimpleEventHook = WpSimpleEventHook_new, AsyncEventHook = WpAsyncEventHook_new, } diff --git a/tests/wplua/meson.build b/tests/wplua/meson.build index 616fca21..18ba1293 100644 --- a/tests/wplua/meson.build +++ b/tests/wplua/meson.build @@ -30,6 +30,12 @@ test( args: ['lua-api-tests', 'json.lua'], env: common_env, ) +test( + 'test-lua-json-utils', + script_tester, + args: ['lua-api-tests', 'json-utils.lua'], + env: common_env, +) test( 'test-lua-monitor-rules', script_tester, diff --git a/tests/wplua/scripts/json-utils.lua b/tests/wplua/scripts/json-utils.lua new file mode 100644 index 00000000..b22c8b16 --- /dev/null +++ b/tests/wplua/scripts/json-utils.lua @@ -0,0 +1,146 @@ +rules_json_str = [[ +[ + { + matches = [ + { + device.name = "~alsa_card.*" + } + ] + actions = { + update-props = { + api.alsa.use-acp = true + api.acp.auto-port = false + } + } + } + { + matches = [ + { + node.name = "alsa_output.0.my-alsa-device" + } + ] + actions = { + update-props = { + audio.rate = 96000 + node.description = "My ALSA Node" + media.class = null + } + } + } +] +]] + +match_props = { ["device.name"] = "unmatched-device-name" } +ret, ret_props = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props) +assert (ret == 0) +assert (ret_props["device.name"] == match_props["device.name"]) + +match_props = { ["device.name"] = "alsa_card_0.my-alsa-device" } +ret, ret_props = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props) +assert (ret == 2) +assert (ret_props["device.name"] == "alsa_card_0.my-alsa-device") +assert (ret_props["api.alsa.use-acp"] == "true") +assert (ret_props["api.acp.auto-port"] == "false") + +match_props = { ["node.name"] = "alsa_output.0.my-alsa-device" } +ret, ret_props = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props) +assert (ret == 2) +assert (ret_props["node.name"] == "alsa_output.0.my-alsa-device") +assert (ret_props["audio.rate"] == "96000") +assert (ret_props["node.description"] == "My ALSA Node") +assert (ret_props["media.class"] == nil) + +match_props = { + ["node.name"] = "alsa_output.0.my-alsa-device", + ["media.class"] = "Audio/Sink", + ["audio.rate"] = "48000", + ["node.description"] = "Test", +} +ret, ret_props = JsonUtils.match_rules_update_properties (Json.Raw (rules_json_str), match_props) +assert (ret == 3) +assert (ret_props["node.name"] == "alsa_output.0.my-alsa-device") +assert (ret_props["audio.rate"] == "96000") +assert (ret_props["node.description"] == "My ALSA Node") +assert (ret_props["media.class"] == nil) + +rules_json_str = [[ +[ + { + matches = [ + { + device.name = "~alsa_card.*" + } + ] + actions = { + update-props = { + api.acp.auto-port = false + } + set-answer = 42 + } + } + { + matches = [ + { + test.error = true + } + ] + actions = { + generate-error = "test.error is true" + } + } + { + matches = [ + { + device.name = "alsa_card.1" + } + ] + actions = { + set-description = "My ALSA Device" + } + } +] +]] + +function match_rules_callback (action, value) + if action == "update-props" then + local updates = value:parse () + for k,v in pairs (updates) do + match_props[k] = tostring (v) + end + elseif action == "set-answer" then + local v = value:parse () + match_props["answer.universe"] = tostring (v) + elseif action == "generate-error" then + local err = value:parse () + return false, tostring (err) + elseif action == "set-description" then + local str = value:parse () + match_props["device.description"] = tostring (str) + end + + return true +end + +match_props = { + ["device.name"] = "alsa_card.1", + ["test.error"] = "false", +} +ret, err = JsonUtils.match_rules (Json.Raw (rules_json_str), match_props, match_rules_callback) +assert (ret == true) +assert (err == nil) +assert (match_props["device.name"] == "alsa_card.1") +assert (match_props["api.acp.auto-port"] == "false") +assert (match_props["answer.universe"] == "42") +assert (match_props["device.description"] == "My ALSA Device") + +match_props = { + ["device.name"] = "alsa_card.1", + ["test.error"] = "true", +} +ret, err = JsonUtils.match_rules (Json.Raw (rules_json_str), match_props, match_rules_callback) +assert (ret == false) +assert (err == "test.error is true") +assert (match_props["device.name"] == "alsa_card.1") +assert (match_props["api.acp.auto-port"] == "false") +assert (match_props["answer.universe"] == "42") +assert (match_props["device.description"] == nil)