diff --git a/modules/meson.build b/modules/meson.build index 85f4d25f..4e004f10 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -155,6 +155,7 @@ shared_library( [ 'module-lua-scripting.c', 'module-lua-scripting/pod.c', + 'module-lua-scripting/json.c', 'module-lua-scripting/api.c', 'module-lua-scripting/config.c', m_lua_scripting_resources, diff --git a/modules/module-lua-scripting/api.c b/modules/module-lua-scripting/api.c index 876b4f6f..041f1366 100644 --- a/modules/module-lua-scripting/api.c +++ b/modules/module-lua-scripting/api.c @@ -14,6 +14,7 @@ #define URI_API "resource:///org/freedesktop/pipewire/wireplumber/m-lua-scripting/api.lua" void wp_lua_scripting_pod_init (lua_State *L); +void wp_lua_scripting_json_init (lua_State *L); /* helpers */ @@ -1363,6 +1364,7 @@ wp_lua_scripting_api_init (lua_State *L) lua_setglobal (L, "WpPlugin"); wp_lua_scripting_pod_init (L); + wp_lua_scripting_json_init (L); wplua_register_type_methods (L, G_TYPE_SOURCE, NULL, source_methods); diff --git a/modules/module-lua-scripting/api.lua b/modules/module-lua-scripting/api.lua index 41488b4f..38bdfbc6 100644 --- a/modules/module-lua-scripting/api.lua +++ b/modules/module-lua-scripting/api.lua @@ -205,6 +205,7 @@ SANDBOX_EXPORT = { LocalNode = WpImplNode_new, Link = WpLink_new, Pod = WpSpaPod, + Json = WpSpaJson, State = WpState_new, LocalModule = WpImplModule_new, } diff --git a/modules/module-lua-scripting/json.c b/modules/module-lua-scripting/json.c new file mode 100644 index 00000000..78534e24 --- /dev/null +++ b/modules/module-lua-scripting/json.c @@ -0,0 +1,333 @@ +/* WirePlumber + * + * Copyright © 2022 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include +#include + +/* API */ + +static int +spa_json_get_data (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushstring (L, wp_spa_json_get_data (json)); + return 1; +} + +static int +spa_json_get_size (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushinteger (L, wp_spa_json_get_size (json)); + return 1; +} + +static int +spa_json_is_null (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_null (json)); + return 1; +} + +static int +spa_json_is_boolean (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_boolean (json)); + return 1; +} + +static int +spa_json_is_int (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_int (json)); + return 1; +} + +static int +spa_json_is_float (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_float (json)); + return 1; +} + +static int +spa_json_is_string (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_string (json)); + return 1; +} + +static int +spa_json_is_array (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_array (json)); + return 1; +} + +static int +spa_json_is_object (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + lua_pushboolean (L, wp_spa_json_is_object (json)); + return 1; +} + +static void +push_luajson (lua_State *L, WpSpaJson *json) +{ + /* Null */ + if (wp_spa_json_is_null (json)) { + lua_pushnil (L); + } + + /* Boolean */ + else if (wp_spa_json_is_boolean (json)) { + gboolean value = FALSE; + g_warn_if_fail (wp_spa_json_parse_boolean (json, &value)); + lua_pushboolean (L, value); + } + + /* Int */ + else if (wp_spa_json_is_int (json)) { + gint value = 0; + g_warn_if_fail (wp_spa_json_parse_int (json, &value)); + lua_pushinteger (L, value); + } + + /* Float */ + else if (wp_spa_json_is_float (json)) { + float value = 0; + g_warn_if_fail (wp_spa_json_parse_float (json, &value)); + lua_pushnumber (L, value); + } + + /* String */ + else if (wp_spa_json_is_string (json)) { + g_autofree gchar *value = wp_spa_json_parse_string (json); + g_warn_if_fail (value); + lua_pushstring (L, value); + } + + /* Array */ + else if (wp_spa_json_is_array (json)) { + g_auto (GValue) item = G_VALUE_INIT; + g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json); + guint i = 1; + lua_newtable (L); + for (; wp_iterator_next (it, &item); g_value_unset (&item)) { + WpSpaJson *j = g_value_get_boxed (&item); + push_luajson (L, j); + lua_rawseti (L, -2, i++); + } + } + + /* Object */ + else if (wp_spa_json_is_object (json)) { + g_auto (GValue) item = G_VALUE_INIT; + g_autoptr (WpIterator) it = wp_spa_json_new_iterator (json); + lua_newtable (L); + for (; wp_iterator_next (it, &item); g_value_unset (&item)) { + WpSpaJson *key = g_value_get_boxed (&item); + g_autofree gchar *key_str = NULL; + WpSpaJson *value = NULL; + g_warn_if_fail (wp_spa_json_is_string (key)); + key_str = wp_spa_json_parse_string (key); + g_warn_if_fail (key_str); + g_value_unset (&item); + wp_iterator_next (it, &item); + value = g_value_get_boxed (&item); + push_luajson (L, value); + lua_setfield (L, -2, key_str); + } + } +} + +static int +spa_json_parse (lua_State *L) +{ + WpSpaJson *json = wplua_checkboxed (L, 1, WP_TYPE_SPA_JSON); + push_luajson (L, json); + return 1; +} + +/* None */ + +static int +spa_json_null_new (lua_State *L) +{ + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_null ()); + return 1; +} + +/* Boolean */ + +static int +spa_json_boolean_new (lua_State *L) +{ + gboolean value = lua_toboolean (L, 1); + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_boolean (value)); + return 1; +} + +/* Int */ + +static int +spa_json_int_new (lua_State *L) +{ + gint64 value = lua_tointeger (L, 1); + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_int (value)); + return 1; +} + +/* Float */ + +static int +spa_json_float_new (lua_State *L) +{ + float value = lua_tonumber (L, 1); + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_float (value)); + return 1; +} + +/* String */ + +static int +spa_json_string_new (lua_State *L) +{ + const gchar *value = lua_tostring (L, 1); + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_new_string (value)); + return 1; +} + +/* Array */ + +static int +spa_json_array_new (lua_State *L) +{ + g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_array (); + + luaL_checktype (L, 1, LUA_TTABLE); + + lua_pushnil (L); + while (lua_next (L, 1)) { + switch (lua_type (L, -1)) { + case LUA_TBOOLEAN: + wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1)); + break; + case LUA_TNUMBER: + if (lua_isinteger (L, -1)) + wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1)); + else + wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1)); + break; + case LUA_TSTRING: + wp_spa_json_builder_add_string (builder, lua_tostring (L, -1)); + break; + case LUA_TUSERDATA: { + WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON); + wp_spa_json_builder_add_json (builder, json); + break; + } + default: + luaL_error (L, "Json does not support lua type ", + lua_typename(L, lua_type(L, -1))); + break; + } + + lua_pop (L, 1); + } + + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder)); + return 1; +} + +/* Object */ + +static int +spa_json_object_new (lua_State *L) +{ + g_autoptr (WpSpaJsonBuilder) builder = wp_spa_json_builder_new_object (); + + luaL_checktype (L, 1, LUA_TTABLE); + + lua_pushnil (L); + while (lua_next (L, -2)) { + wp_spa_json_builder_add_property (builder, lua_tostring (L, -2)); + + switch (lua_type (L, -1)) { + case LUA_TBOOLEAN: + wp_spa_json_builder_add_boolean (builder, lua_toboolean (L, -1)); + break; + case LUA_TNUMBER: + if (lua_isinteger (L, -1)) + wp_spa_json_builder_add_int (builder, lua_tointeger (L, -1)); + else + wp_spa_json_builder_add_float (builder, lua_tonumber (L, -1)); + break; + case LUA_TSTRING: + wp_spa_json_builder_add_string (builder, lua_tostring (L, -1)); + break; + case LUA_TUSERDATA: { + WpSpaJson *json = wplua_checkboxed (L, -1, WP_TYPE_SPA_JSON); + wp_spa_json_builder_add_json (builder, json); + break; + } + default: + luaL_error (L, "Json does not support lua type ", + lua_typename(L, lua_type(L, -1))); + break; + } + + lua_pop (L, 1); + } + + wplua_pushboxed (L, WP_TYPE_SPA_JSON, wp_spa_json_builder_end (builder)); + return 1; +} + +/* Init */ + +static const luaL_Reg spa_json_methods[] = { + { "get_data", spa_json_get_data }, + { "get_size", spa_json_get_size }, + { "is_null", spa_json_is_null }, + { "is_boolean", spa_json_is_boolean }, + { "is_int", spa_json_is_int }, + { "is_float", spa_json_is_float }, + { "is_string", spa_json_is_string }, + { "is_array", spa_json_is_array }, + { "is_object", spa_json_is_object }, + { "parse", spa_json_parse }, + { NULL, NULL } +}; + +static const luaL_Reg spa_json_constructors[] = { + { "Null", spa_json_null_new }, + { "Boolean", spa_json_boolean_new }, + { "Int", spa_json_int_new }, + { "Float", spa_json_float_new }, + { "String", spa_json_string_new }, + { "Array", spa_json_array_new }, + { "Object", spa_json_object_new }, + { NULL, NULL } +}; + +void +wp_lua_scripting_json_init (lua_State *L) +{ + luaL_newlib (L, spa_json_constructors); + lua_setglobal (L, "WpSpaJson"); + + wplua_register_type_methods (L, WP_TYPE_SPA_JSON, NULL, spa_json_methods); +} diff --git a/tests/wplua/meson.build b/tests/wplua/meson.build index 128cc309..a7ff033f 100644 --- a/tests/wplua/meson.build +++ b/tests/wplua/meson.build @@ -21,6 +21,12 @@ test( args: ['pod.lua'], env: common_env, ) +test( + 'test-lua-json', + script_tester, + args: ['json.lua'], + env: common_env, +) test( 'test-lua-monitor-rules', script_tester, diff --git a/tests/wplua/scripts/json.lua b/tests/wplua/scripts/json.lua new file mode 100644 index 00000000..270afa98 --- /dev/null +++ b/tests/wplua/scripts/json.lua @@ -0,0 +1,123 @@ +-- Null +json = Json.Null () +assert (json:is_null()) +assert (json:parse() == nil) +assert (json:get_data() == "null") +assert (json:get_size() == 4) + +-- Boolean +json = Json.Boolean (true) +assert (json:is_boolean()) +assert (json:parse()) +assert (json:get_data() == "true") +assert (json:get_size() == 4) +json = Json.Boolean (false) +assert (json:is_boolean()) +assert (not json:parse()) +assert (json:get_data() == "false") +assert (json:get_size() == 5) + +-- Int +json = Json.Int (3) +assert (json:is_int()) +assert (json:parse() == 3) +assert (json:get_data() == "3") +assert (json:get_size() == 1) + +-- Float +json = Json.Float(3.14) +assert (json:is_float()) +val = json:parse () +assert (val > 3.13 and val < 3.15) + +-- String +json = Json.String ("wireplumber") +assert (json:is_string()) +assert (json:parse() == "wireplumber") +assert (json:get_data() == "\"wireplumber\"") +assert (json:get_size() == 13) + +-- Array +json = Json.Array { Json.Null (), Json.Null () } +assert (json:is_array()) +val = json:parse () +assert (val[1] == nil) +assert (val[2] == nil) +assert (json:get_data() == "[null, null]") +assert (json:get_size() == 12) + +json = Json.Array { true, false } +assert (json:is_array()) +val = json:parse () +assert (val[1]) +assert (not val[2]) +assert (json:get_data() == "[true, false]") +assert (json:get_size() == 13) + +json = Json.Array {1, 2, 3} +assert (json:is_array()) +val = json:parse () +assert (val[1] == 1) +assert (val[2] == 2) +assert (val[3] == 3) +assert (json:get_data() == "[1, 2, 3]") +assert (json:get_size() == 9) + +json = Json.Array {1.11, 2.22, 3.33} +assert (json:is_array()) +val = json:parse () +assert (val[1] > 1.10 and val[1] < 1.12) +assert (val[2] > 2.21 and val[2] < 2.23) +assert (val[3] > 3.32 and val[3] < 3.34) + +json = Json.Array {"lua", "spa", "json"} +assert (json:is_array()) +val = json:parse () +assert (val[1] == "lua") +assert (val[2] == "spa") +assert (val[3] == "json") +assert (json:get_data() == "[\"lua\", \"spa\", \"json\"]") +assert (json:get_size() == 22) + +json = Json.Array { + Json.Array { + Json.Object { + key1 = 1 + }, + Json.Object { + key2 = 2 + }, + } +} +assert (json:is_array()) +assert (json:get_data() == "[[{\"key1\":1}, {\"key2\":2}]]") + +-- Object +json = Json.Object { + key1 = Json.Null(), + key2 = true, + key3 = 3, + key4 = 4.44, + key5 = "foo", + key6 = Json.Array {5, 6, 7}, + key7 = Json.Object { + key_nested1 = "nested", + key_nested2 = 8, + key_nested3 = Json.Array {false, true, false} + } +} +assert (json:is_object()) +val = json:parse () +assert (val.key1 == nil) +assert (val.key2 == true) +assert (val.key3 == 3) +assert (val.key4 > 4.43 and val.key4 < 4.45) +assert (val.key5 == "foo") +assert (val.key6[1] == 5) +assert (val.key6[2] == 6) +assert (val.key6[3] == 7) +assert (val.key7.key_nested1 == "nested") +assert (val.key7.key_nested2 == 8) +assert (not val.key7.key_nested3[1]) +assert (val.key7.key_nested3[2]) +assert (not val.key7.key_nested3[3])