[lua] Add native API to decode/encode JSON text (fix #3233)

New json.decode(jsonText) and json.encode(luaTable) functions.

In this way we don't depend on third-party libraries to decode/encode
JSON text which is a quite common task (in tests and export scripts).
This commit is contained in:
David Capello 2023-07-16 19:44:58 -03:00
parent 00b75a76a8
commit 86a50e2e9a
10 changed files with 420 additions and 41 deletions

3
.gitmodules vendored
View File

@ -1,6 +1,3 @@
[submodule "tests/third_party/json"]
path = tests/third_party/json
url = https://github.com/aseprite/json.lua
[submodule "third_party/pixman"]
path = third_party/pixman
url = https://github.com/aseprite/pixman.git

View File

@ -184,6 +184,7 @@ if(ENABLE_SCRIPTING)
script/image_iterator_class.cpp
script/image_spec_class.cpp
script/images_class.cpp
script/json_class.cpp
script/keys.cpp
script/layer_class.cpp
script/layers_class.cpp

View File

@ -154,6 +154,7 @@ void register_app_pixel_color_object(lua_State* L);
void register_app_fs_object(lua_State* L);
void register_app_command_object(lua_State* L);
void register_app_preferences_object(lua_State* L);
void register_json_object(lua_State* L);
void register_brush_class(lua_State* L);
void register_cel_class(lua_State* L);
@ -255,12 +256,13 @@ Engine::Engine()
// Generic code used by metatables
run_mt_index_code(L);
// Register global app object
// Register global objects (app, json)
register_app_object(L);
register_app_pixel_color_object(L);
register_app_fs_object(L);
register_app_command_object(L);
register_app_preferences_object(L);
register_json_object(L);
// Register constants
lua_newtable(L);

View File

@ -0,0 +1,314 @@
// Aseprite
// Copyright (C) 2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "app/script/luacpp.h"
#include "app/script/values.h"
#include <cstring>
#include "json11.hpp"
namespace app {
namespace script {
namespace {
struct Json { };
using JsonObj = json11::Json;
using JsonArrayIterator = JsonObj::array::const_iterator;
using JsonObjectIterator = JsonObj::object::const_iterator;
void push_json_value(lua_State* L, const JsonObj& value)
{
switch (value.type()) {
case json11::Json::NUL:
lua_pushnil(L);
break;
case json11::Json::NUMBER:
lua_pushnumber(L, value.number_value());
break;
case json11::Json::BOOL:
lua_pushboolean(L, value.bool_value());
break;
case json11::Json::STRING:
lua_pushstring(L, value.string_value().c_str());
break;
case json11::Json::ARRAY:
case json11::Json::OBJECT:
push_obj(L, value);
break;
}
}
JsonObj get_json_value(lua_State* L, int index)
{
switch (lua_type(L, index)) {
case LUA_TNONE:
case LUA_TNIL:
return JsonObj();
case LUA_TBOOLEAN:
return JsonObj(lua_toboolean(L, index) ? true: false);
case LUA_TNUMBER:
return JsonObj(lua_tonumber(L, index));
case LUA_TSTRING:
return JsonObj(lua_tostring(L, index));
case LUA_TTABLE:
if (is_array_table(L, index)) {
JsonObj::array items;
if (index < 0)
--index;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
items.push_back(get_json_value(L, -1));
lua_pop(L, 1); // pop the value lua_next(), leave the key in the stack
}
return JsonObj(items);
}
else {
JsonObj::object items;
lua_pushnil(L);
if (index < 0)
--index;
while (lua_next(L, index) != 0) {
if (const char* k = lua_tostring(L, -2)) {
items[k] = get_json_value(L, -1);
}
lua_pop(L, 1); // pop the value lua_next(), leave the key in the stack
}
return JsonObj(items);
}
break;
case LUA_TUSERDATA:
// TODO convert rectangles, point, size, uuids?
break;
}
return JsonObj();
}
int JsonObj_gc(lua_State* L)
{
get_obj<JsonObj>(L, 1)->~JsonObj();
return 0;
}
int JsonObj_eq(lua_State* L)
{
auto a = get_obj<JsonObj>(L, 1);
auto b = get_obj<JsonObj>(L, 2);
return (*a == *b);
}
int JsonObj_len(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
switch (obj->type()) {
case json11::Json::STRING:
lua_pushinteger(L, obj->string_value().size());
break;
case json11::Json::ARRAY:
lua_pushinteger(L, obj->array_items().size());
break;
case json11::Json::OBJECT:
lua_pushinteger(L, obj->object_items().size());
break;
case json11::Json::NUL:
lua_pushnil(L);
break;
default:
case json11::Json::NUMBER:
case json11::Json::BOOL:
lua_pushinteger(L, 1);
break;
}
return 1;
}
int JsonObj_index(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
if (obj->type() == json11::Json::OBJECT) {
if (auto key = lua_tostring(L, 2)) {
push_json_value(L, (*obj)[key]);
return 1;
}
}
else if (obj->type() == json11::Json::ARRAY) {
auto i = lua_tointeger(L, 2) - 1; // Adjust to 0-based index
push_json_value(L, (*obj)[i]);
return 1;
}
return 0;
}
int JsonObj_newindex(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
if (obj->type() == json11::Json::OBJECT) {
if (auto key = lua_tostring(L, 2)) {
// TODO ugly hack, but it works
const_cast<std::map<std::string, json11::Json>&>
(obj->object_items())[key] = get_json_value(L, 3);
}
}
else if (obj->type() == json11::Json::ARRAY) {
auto i = lua_tointeger(L, 2) - 1; // Adjust to 0-based index
const_cast<std::vector<json11::Json>&>
(obj->array_items())[i] = get_json_value(L, 3);
}
return 0;
}
int JsonObj_pairs_next(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
auto& it = *get_obj<JsonObjectIterator>(L, lua_upvalueindex(1));
if (it == obj->object_items().end())
return 0;
lua_pushstring(L, (*it).first.c_str());
push_json_value(L, (*it).second);
++it;
return 2;
}
int JsonObj_ipairs_next(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
auto& it = *get_obj<JsonArrayIterator>(L, lua_upvalueindex(1));
if (it == obj->array_items().end())
return 0;
lua_pushinteger(L, (it - obj->array_items().begin() + 1));
push_json_value(L, (*it));
++it;
return 0;
}
int JsonObj_pairs(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
if (obj->type() == json11::Json::OBJECT) {
push_obj(L, obj->object_items().begin());
lua_pushcclosure(L, JsonObj_pairs_next, 1);
lua_pushvalue(L, 1); // Copy the same obj as the second return value
return 2;
}
else if (obj->type() == json11::Json::ARRAY) {
push_obj(L, obj->array_items().begin());
lua_pushcclosure(L, JsonObj_ipairs_next, 1);
lua_pushvalue(L, 1); // Copy the same obj as the second return value
return 2;
}
return 0;
}
int JsonObj_tostring(lua_State* L)
{
auto obj = get_obj<JsonObj>(L, 1);
lua_pushstring(L, obj->dump().c_str());
return 1;
}
int JsonObjectIterator_gc(lua_State* L)
{
get_obj<JsonObjectIterator>(L, 1)->~JsonObjectIterator();
return 0;
}
int JsonArrayIterator_gc(lua_State* L)
{
get_obj<JsonArrayIterator>(L, 1)->~JsonArrayIterator();
return 0;
}
int Json_decode(lua_State* L)
{
if (const char* s = lua_tostring(L, 1)) {
std::string err;
auto json = json11::Json::parse(s, std::strlen(s), err);
if (!err.empty())
return luaL_error(L, err.c_str());
push_obj(L, json);
return 1;
}
return 0;
}
int Json_encode(lua_State* L)
{
// Encode a JsonObj, we deep copy it (create a deep copy)
if (auto obj = may_get_obj<JsonObj>(L, 1)) {
lua_pushstring(L, obj->dump().c_str());
return 1;
}
// Encode a Lua table
else if (lua_istable(L, 1)) {
lua_pushstring(L, get_json_value(L, 1).dump().c_str());
return 1;
}
return 0;
}
const luaL_Reg JsonObj_methods[] = {
{ "__gc", JsonObj_gc },
{ "__eq", JsonObj_eq },
{ "__len", JsonObj_len },
{ "__index", JsonObj_index },
{ "__newindex", JsonObj_newindex },
{ "__pairs", JsonObj_pairs },
{ "__tostring", JsonObj_tostring },
{ nullptr, nullptr }
};
const luaL_Reg JsonObjectIterator_methods[] = {
{ "__gc", JsonObjectIterator_gc },
{ nullptr, nullptr }
};
const luaL_Reg JsonArrayIterator_methods[] = {
{ "__gc", JsonArrayIterator_gc },
{ nullptr, nullptr }
};
const luaL_Reg Json_methods[] = {
{ "decode", Json_decode },
{ "encode", Json_encode },
{ nullptr, nullptr }
};
} // anonymous namespace
DEF_MTNAME(Json);
DEF_MTNAME(JsonObj);
DEF_MTNAME(JsonObjectIterator);
DEF_MTNAME(JsonArrayIterator);
void register_json_object(lua_State* L)
{
REG_CLASS(L, JsonObj);
REG_CLASS(L, JsonObjectIterator);
REG_CLASS(L, JsonArrayIterator);
REG_CLASS(L, Json);
lua_newtable(L); // Create a table which will be the "json" object
lua_pushvalue(L, -1);
luaL_getmetatable(L, get_mtname<Json>());
lua_setmetatable(L, -2);
lua_setglobal(L, "json");
lua_pop(L, 1); // Pop json table
}
} // namespace script
} // namespace app

View File

@ -467,37 +467,14 @@ doc::UserData::Variant get_value_from_lua(lua_State* L, int index)
v = std::string(lua_tostring(L, index));
break;
case LUA_TTABLE: {
int i = 0;
bool isArray = true;
if (index < 0)
--index;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
if (lua_isinteger(L, -2)) {
if (++i != lua_tointeger(L, -2)) {
isArray = false;
lua_pop(L, 2); // Pop value and key
break;
}
}
else {
isArray = false;
lua_pop(L, 2);
break;
}
lua_pop(L, 1); // Pop the value, leave the key for lua_next()
}
if (index < 0)
++index;
if (isArray) {
case LUA_TTABLE:
if (is_array_table(L, index)) {
v = get_value_from_lua<doc::UserData::Vector>(L, index);
}
else {
v = get_value_from_lua<doc::UserData::Properties>(L, index);
}
break;
}
case LUA_TUSERDATA: {
if (auto rect = may_get_obj<gfx::Rect>(L, index)) {
@ -579,5 +556,28 @@ doc::UserData::Vector get_value_from_lua(lua_State* L, int index)
return v;
}
bool is_array_table(lua_State* L, int index)
{
if (index < 0)
--index;
int i = 0;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
if (lua_isinteger(L, -2)) {
if (++i != lua_tointeger(L, -2)) {
lua_pop(L, 2); // Pop value and key
return false;
}
}
else {
lua_pop(L, 2);
return false;
}
lua_pop(L, 1); // Pop the value, leave the key for lua_next()
}
return true;
}
} // namespace script
} // namespace app

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019 Igara Studio S.A.
// Copyright (C) 2019-2023 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@ -21,6 +21,9 @@ T get_value_from_lua(lua_State* L, int index);
template<typename T>
void push_value_to_lua(lua_State* L, const T& value);
// Returns true if the given table is an array
bool is_array_table(lua_State* L, int index);
} // namespace script
} // namespace app

View File

@ -17,7 +17,6 @@ if ! $ASEPRITE -b sprites/1empty3.aseprite --sheet "$d/sheet.png" > "$d/stdout.j
exit 1
fi
cat >$d/compare.lua <<EOF
local json = dofile('third_party/json/json.lua')
local data = json.decode(io.open('$d/stdout.json'):read('a'))
local frames = { data.frames['1empty3 0.aseprite'],
data.frames['1empty3 1.aseprite'],
@ -93,7 +92,6 @@ $ASEPRITE -b --split-layers sprites/1empty3.aseprite \
--sheet "$d/sheet.png" \
--data "$d/sheet.json" || exit 1
cat >$d/compare.lua <<EOF
local json = dofile('third_party/json/json.lua')
local data = json.decode(io.open('$d/sheet.json'):read('a'))
assert(data.meta.size.w == 96)
assert(data.meta.size.h == 64)
@ -159,7 +157,6 @@ $ASEPRITE -b \
-data "$d/sheet2.json" || exit 1
cat >$d/check.lua <<EOF
local json = dofile('third_party/json/json.lua')
local sheet1 = json.decode(io.open('$d/sheet1.json'):read('a'))
local sheet2 = json.decode(io.open('$d/sheet2.json'):read('a'))
assert(#sheet1.frames == 12)
@ -258,7 +255,6 @@ for layer in a b ; do
-data "$d/data2-$layer.json" \
-format json-array -sheet "$d/sheet2-$layer.png" || exit 1
cat >$d/compare.lua <<EOF
local json = dofile('third_party/json/json.lua')
local data1 = json.decode(io.open('$d/data1-$layer.json'):read('a'))
local data2 = json.decode(io.open('$d/data2-$layer.json'):read('a'))
assert(#data1.frames == #data2.frames)
@ -289,7 +285,6 @@ $ASEPRITE -b \
-data "$d/data2.json" \
-format json-array -sheet-pack -sheet "$d/sheet2.png" || exit 1
cat >$d/compare.lua <<EOF
local json = dofile('third_party/json/json.lua')
local data1 = json.decode(io.open('$d/data1.json'):read('a'))
local data2 = json.decode(io.open('$d/data2.json'):read('a'))
assert(#data1.frames == #data2.frames)
@ -308,7 +303,6 @@ d=$t/issue-2380
$ASEPRITE -b -trim -all-layers "sprites/groups3abc.aseprite" -data "$d/sheet1.json" -format json-array -sheet "$d/sheet1.png" -list-layers
$ASEPRITE -b -trim -all-layers -split-layers "sprites/groups3abc.aseprite" -data "$d/sheet2.json" -format json-array -sheet "$d/sheet2.png" -list-layers
cat >$d/check.lua <<EOF
local json = dofile('third_party/json/json.lua')
local sheet1 = json.decode(io.open('$d/sheet1.json'):read('a'))
local sheet2 = json.decode(io.open('$d/sheet2.json'):read('a'))
assert(#sheet1.meta.layers == 12)
@ -323,7 +317,6 @@ d=$t/issue-2432
$ASEPRITE -b -trim -ignore-layer "c" -all-layers "sprites/groups3abc.aseprite" -data "$d/sheet1.json" -format json-array -sheet "$d/sheet1.png" -list-layers
$ASEPRITE -b -trim -ignore-layer "c" -all-layers -split-layers "sprites/groups3abc.aseprite" -data "$d/sheet2.json" -format json-array -sheet "$d/sheet2.png" -list-layers
cat >$d/check.lua <<EOF
local json = dofile('third_party/json/json.lua')
local sheet1 = json.decode(io.open('$d/sheet1.json'):read('a'))
local sheet2 = json.decode(io.open('$d/sheet2.json'):read('a'))
assert(#sheet1.meta.layers == 8)
@ -337,7 +330,6 @@ $ASEPRITE -b -script "$d/check.lua" || exit 1
d=$t/issue-2600
$ASEPRITE -b -list-layers -format json-array -trim -merge-duplicates -split-layers -all-layers "sprites/link.aseprite" -data "$d/sheet.json" -sheet "$d/sheet.png"
cat >$d/check.lua <<EOF
local json = dofile('third_party/json/json.lua')
local sheet = json.decode(io.open('$d/sheet.json'):read('a'))
local restoredSprite = Sprite(sheet.frames[1].sourceSize.w, sheet.frames[1].sourceSize.h, ColorMode.RGB)
local spriteSheet = Image{ fromFile="$d/sheet.png" }
@ -409,7 +401,6 @@ $ASEPRITE -b "sprites/1empty3.aseprite" "sprites/tags3.aseprite" \
-sheet "$d/atlas.png" \
-list-tags -tagname-format="{title}-{tag}" || exit 1
cat >$d/compare.lua <<EOF
local json = dofile('third_party/json/json.lua')
local data = json.decode(io.open('$d/atlas.json'):read('a'))
assert(#data.meta.frameTags == 5)

View File

@ -9,7 +9,6 @@ if file1 == nil or file2 == nil then
return 0
end
local json = dofile('../third_party/json/json.lua')
local data1 = json.decode(io.open(file1):read('a'))
local data2 = json.decode(io.open(file2):read('a'))

73
tests/scripts/json.lua Normal file
View File

@ -0,0 +1,73 @@
-- Copyright (C) 2023 Igara Studio S.A.
--
-- This file is released under the terms of the MIT license.
-- Read LICENSE.txt for more information.
-- Basic decode + iterators
do
local o = json.decode('{"a":true,"b":5,"c":[1,3,9]}')
assert(#o == 3)
assert(o.a == true)
assert(o.b == 5)
assert(#o.c == 3)
assert(o.c[1] == 1)
assert(o.c[2] == 3)
assert(o.c[3] == 9)
-- Iterate json data
for k,v in pairs(o) do
if k == "a" then assert(v == true) end
if k == "b" then assert(v == 5) end
if k == "c" then
assert(v[1] == 1)
assert(v[2] == 3)
assert(v[3] == 9)
end
end
for i,v in ipairs(o.c) do
if i == 1 then assert(v == 1) end
if i == 2 then assert(v == 3) end
if i == 3 then assert(v == 9) end
end
end
-- Modify values
do
local o = json.decode('{"obj":{"a":3,"b":5},"arr":[0,"hi",{"bye":true}]}')
assert(o.obj.a == 3)
assert(o.obj.b == 5)
assert(o.arr[1] == 0)
assert(o.arr[2] == "hi")
assert(o.arr[3].bye == true)
local u = json.decode(json.encode(o)) -- Creates a copy
assert(o == o)
assert(o == u)
o.obj.b = 6
o.arr[1] = "name"
assert(tostring(o.obj) == '{"a": 3, "b": 6}')
assert(tostring(o.arr) == '["name", "hi", {"bye": true}]')
-- Check that "u" is a copy and wasn't modify (only "o" was modified)
assert(tostring(u.obj) == '{"a": 3, "b": 5}')
assert(tostring(u.arr) == '[0, "hi", {"bye": true}]')
end
-- Encode Lua tables
do
local arrStr = json.encode({ 4, "hi", true })
assert(arrStr == '[4, "hi", true]')
local arr = json.decode(arrStr)
assert(arr[1] == 4)
assert(arr[2] == "hi")
assert(arr[3] == true)
local obj = json.decode(json.encode({ a=4, b=true, c="name", d={1,8,{a=2}} }))
assert(obj.a == 4)
assert(obj.b == true)
assert(obj.c == "name")
assert(obj.d[1] == 1)
assert(obj.d[2] == 8)
assert(obj.d[3].a == 2)
end

@ -1 +0,0 @@
Subproject commit dbf4b2dd2eb7c23be2773c89eb059dadd6436f94