Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

invoke getmetatable(val).__tojson() if present #14

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 174 additions & 4 deletions lua_cjson.c
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@
#define CJSON_VERSION "2.1devel"
#endif

// convention is to use a pointer to anything in our C module for unique lightuserdata
#define CJSON_LIGHTUSERDATA ((void*)json_token_type_name)

/* Workaround for Solaris platforms missing isinf() */
#if !defined(isinf) && (defined(USE_INTERNAL_ISINF) || defined(MISSING_ISINF))
#define isinf(x) (!isnan(x) && isnan((x) - (x)))
Expand All @@ -68,6 +71,9 @@
#define DEFAULT_DECODE_INVALID_NUMBERS 1
#define DEFAULT_ENCODE_KEEP_BUFFER 1
#define DEFAULT_ENCODE_NUMBER_PRECISION 14
#define DEFAULT_ENCODE_SORT_KEYS 0

#define CJSON_REGISTRY_INVALID_TYPE_ENCODER 1

#ifdef DISABLE_INVALID_NUMBERS
#undef DEFAULT_DECODE_INVALID_NUMBERS
Expand Down Expand Up @@ -124,6 +130,7 @@ typedef struct {
int encode_invalid_numbers; /* 2 => Encode as "null" */
int encode_number_precision;
int encode_keep_buffer;
int encode_sort_keys;

int decode_invalid_numbers;
int decode_max_depth;
Expand Down Expand Up @@ -345,6 +352,13 @@ static int json_cfg_encode_invalid_numbers(lua_State *l)
return 1;
}

static int json_cfg_encode_sort_keys(lua_State *l)
{
json_config_t *cfg = json_arg_init(l, 1);
json_enum_option(l, 1, &cfg->encode_sort_keys, NULL, 1);
return 0;
}

static int json_cfg_decode_invalid_numbers(lua_State *l)
{
json_config_t *cfg = json_arg_init(l, 1);
Expand All @@ -356,6 +370,38 @@ static int json_cfg_decode_invalid_numbers(lua_State *l)
return 1;
}

static void json_push_registry_table(lua_State *l)
{
lua_pushlightuserdata(l, CJSON_LIGHTUSERDATA);
lua_rawget(l, LUA_REGISTRYINDEX);
}

static int json_cfg_set_invalid_type_encoder(lua_State *l)
{
luaL_checktype(l, 1, LUA_TFUNCTION); // [func]
lua_pushnumber(l, CJSON_REGISTRY_INVALID_TYPE_ENCODER); // [func, idx]
json_push_registry_table(l); // [func, idx, cjson_registry]
lua_insert(l, -3); // [cjson_registry, func, idx]
lua_insert(l, -2); // [cjson_registry, idx, func]
lua_rawset(l, -3); // [cjson_registry]
lua_pop(l, 1);
return 0;
}

static int json_push_invalid_type_encoder(lua_State *l)
{
json_push_registry_table(l); // [cjson_registry]
lua_pushnumber(l, CJSON_REGISTRY_INVALID_TYPE_ENCODER); // [cjson_registry, idx]
lua_rawget(l, -2); // [cjson_registry, invalid_type_encoder]
lua_remove(l, -2); // [invalid_type_encoder]
if(lua_type(l,-1) == LUA_TNIL) {
lua_pop(l, 1);
return 0;
} else {
return 1;
}
}

static int json_destroy_config(lua_State *l)
{
json_config_t *cfg;
Expand Down Expand Up @@ -390,6 +436,7 @@ static void json_create_config(lua_State *l)
cfg->decode_invalid_numbers = DEFAULT_DECODE_INVALID_NUMBERS;
cfg->encode_keep_buffer = DEFAULT_ENCODE_KEEP_BUFFER;
cfg->encode_number_precision = DEFAULT_ENCODE_NUMBER_PRECISION;
cfg->encode_sort_keys = DEFAULT_ENCODE_SORT_KEYS;

#if DEFAULT_ENCODE_KEEP_BUFFER > 0
strbuf_init(&cfg->encode_buf, 0);
Expand Down Expand Up @@ -662,6 +709,87 @@ static void json_append_object(lua_State *l, json_config_t *cfg,
strbuf_append_char(json, '}');
}

static void json_append_object_sortkeys(lua_State *l, json_config_t *cfg,
int current_depth, strbuf_t *json)
{
// [data]
lua_createtable(l, 1024, 0); // [data, keytable]

int n = 0;
lua_pushnil(l); // [data, keytable, nil]
while(lua_next(l, -3) != 0) {
// [data, keytable, key, value]
lua_pop(l, 1); // [data, keytable, key]
lua_pushvalue(l, -1); // [data, keytable, key, key]
lua_rawseti(l, -3, ++n); // [data, keytable, key]
}
// [data, keytable]

lua_pushvalue(l, -1); // [data, keytable, keytable]
lua_getglobal(l, "table"); // [data, keytable, keytable, _G.table]
lua_getfield(l, -1, "sort"); // [data, keytable, keytable, _G.table, _G.table.sort]
lua_insert(l, -3); // [data, keytable, _G.table.sort, keytable, _G.table]
lua_pop(l, 1); // [data, keytable, _G.table.sort, keytable]
lua_call(l, 1, 0); // [data, keytable]

int comma, keytype;

/* Object */
strbuf_append_char(json, '{');

lua_pushnil(l); // [data, keytable, nil]
comma = 0;
while (lua_next(l, -2) != 0) {
// [data, keytable, n, key]
if (comma)
strbuf_append_char(json, ',');
else
comma = 1;

keytype = lua_type(l, -1);
if (keytype == LUA_TNUMBER) {
strbuf_append_char(json, '"');
json_append_number(l, cfg, json, -1);
strbuf_append_mem(json, "\":", 2);
} else if (keytype == LUA_TSTRING) {
json_append_string(l, json, -1);
strbuf_append_char(json, ':');
} else {
json_encode_exception(l, cfg, json, -1, "table key must be a number or string");
/* never returns */
}

// [data, keytable, n, key]
lua_gettable(l, -4); // [data, keytable, n, data[key]]
json_append_data(l, cfg, current_depth, json);
lua_pop(l, 1); // [data, keytable, n]
}
// [data, keytable]
lua_pop(l, 1); // [data]

strbuf_append_char(json, '}');
}

static int json_append_tojson_invoke(lua_State* l, json_config_t *cfg, strbuf_t* json) {
// [data]
if(lua_getmetatable(l, -1)) { // [data, mt]
lua_getfield(l, -1, "__tojson"); // [data, mt, __tojson]
if(lua_type(l,-1) == LUA_TFUNCTION) {
lua_pushvalue(l, -3); // [data, mt, __tojson, data]
lua_call(l, 1, 1); // [data, mt, string]
if(lua_type(l,-1) != LUA_TSTRING) {
json_encode_exception(l, cfg, json, -1, "__tojson(v) did not return a string");
/* never returns */
}
strbuf_append_string(json, lua_tolstring(l, -1, 0));
lua_settop(l, -3);
return 1;
}
lua_settop(l, -3);
}
return 0;
}

/* Serialise Lua data into JSON string. */
static void json_append_data(lua_State *l, json_config_t *cfg,
int current_depth, strbuf_t *json)
Expand All @@ -682,13 +810,20 @@ static void json_append_data(lua_State *l, json_config_t *cfg,
strbuf_append_mem(json, "false", 5);
break;
case LUA_TTABLE:
if(json_append_tojson_invoke(l, cfg, json)) { break; }

current_depth++;
json_check_encode_depth(l, cfg, current_depth, json);
len = lua_array_length(l, cfg, json);
if (len > 0)
json_append_array(l, cfg, current_depth, json, len);
else
json_append_object(l, cfg, current_depth, json);
else {
if(cfg->encode_sort_keys) {
json_append_object_sortkeys(l, cfg, current_depth, json);
} else {
json_append_object(l, cfg, current_depth, json);
}
}
break;
case LUA_TNIL:
strbuf_append_mem(json, "null", 4);
Expand All @@ -701,8 +836,20 @@ static void json_append_data(lua_State *l, json_config_t *cfg,
default:
/* Remaining types (LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD,
* and LUA_TLIGHTUSERDATA) cannot be serialised */
json_encode_exception(l, cfg, json, -1, "type not supported");
/* never returns */
if(json_push_invalid_type_encoder(l)) { // [data, invalid_type_encoder]
lua_pushvalue(l, -2); // [data, invalid_type_encoder, data]
lua_call(l, 1, 1); // [data, encoder_result]
if(lua_type(l,-1) != LUA_TSTRING) {
json_encode_exception(l, cfg, json, -1, "invalid_type_encoder(v) did not return a string");
/* never returns */
} else {
strbuf_append_string(json, lua_tolstring(l, -1, 0));
lua_pop(l, 1); // [data]
}
} else {
json_encode_exception(l, cfg, json, -1, "type not supported");
/* never returns */
}
}
}

Expand Down Expand Up @@ -1138,6 +1285,20 @@ static void json_decode_descend(lua_State *l, json_parse_t *json, int slots)
json->current_depth, json->ptr - json->data);
}

static void json_set_metatable_field_string(lua_State* l, char* k, char* v)
{
// [table]
if(!lua_getmetatable(l, -1)) {
lua_newtable(l); // [table, mt]
lua_pushvalue(l, -1); // [table, mt, mt]
lua_setmetatable(l, -3); // [table, mt]
}
// [table, mt]
lua_pushstring(l, v); // [table, mt, v]
lua_setfield(l, -2, k); // [table, mt]
lua_pop(l, 1); // [table]
}

static void json_parse_object_context(lua_State *l, json_parse_t *json)
{
json_token_t token;
Expand All @@ -1147,6 +1308,7 @@ static void json_parse_object_context(lua_State *l, json_parse_t *json)
json_decode_descend(l, json, 3);

lua_newtable(l);
json_set_metatable_field_string(l, "__jsontype", "object");

json_next_token(json, &token);

Expand Down Expand Up @@ -1199,6 +1361,7 @@ static void json_parse_array_context(lua_State *l, json_parse_t *json)
json_decode_descend(l, json, 2);

lua_newtable(l);
json_set_metatable_field_string(l, "__jsontype", "array");

json_next_token(json, &token);

Expand Down Expand Up @@ -1357,8 +1520,10 @@ static int lua_cjson_new(lua_State *l)
{ "decode_max_depth", json_cfg_decode_max_depth },
{ "encode_number_precision", json_cfg_encode_number_precision },
{ "encode_keep_buffer", json_cfg_encode_keep_buffer },
{ "encode_sort_keys", json_cfg_encode_sort_keys },
{ "encode_invalid_numbers", json_cfg_encode_invalid_numbers },
{ "decode_invalid_numbers", json_cfg_decode_invalid_numbers },
{ "set_invalid_type_encoder", json_cfg_set_invalid_type_encoder },
{ "new", lua_cjson_new },
{ NULL, NULL }
};
Expand All @@ -1383,6 +1548,11 @@ static int lua_cjson_new(lua_State *l)
lua_pushliteral(l, CJSON_VERSION);
lua_setfield(l, -2, "_VERSION");

/* Create our registry table */
lua_pushlightuserdata(l, CJSON_LIGHTUSERDATA);
lua_newtable(l);
lua_rawset(l, LUA_REGISTRYINDEX);

return 1;
}

Expand Down
45 changes: 45 additions & 0 deletions tests/test.lua
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@ local cjson_tests = {
json.decode, { string.rep("[", 1100) .. '1100' .. string.rep("]", 1100)},
false, { "Found too many nested data structures (1001) at character 1001" } },

{ "Decode array syntax sets mt.__jsontype = 'array'",
function()
local t = json.decode("[]")
return getmetatable(t).__jsontype == 'array'
end, { },
true, { true } },

-- Test encoding nested tables
{ "Set encode_max_depth(5)",
json.encode_max_depth, { 5 }, true, { 5 } },
Expand All @@ -199,6 +206,29 @@ local cjson_tests = {
json.encode, { testdata.deeply_nested_data },
false, { "Cannot serialise, excessive nesting (1001)" } },

-- Test encoding with metatable.__tojson
{ "Encode a table that has a __tojson",
function()
local mt = {__tojson=function() return "asdf" end}
local t = {}
setmetatable(t, mt)
return json.encode(t)
end, {}, true, { "asdf" } },
{ "Encode multiple tables that have __tojson",
function()
local mt = {__tojson=function() return "asdf" end}
local t = {}
setmetatable(t, mt)
return json.encode({c={t,t,t}})
end, {}, true, { '{"c":[asdf,asdf,asdf]}' } },
{ "Encode a table that has a metatable without a __tojson",
function()
local mt = {a=1}
local t = {b=1}
setmetatable(t, mt)
return json.encode(t)
end, {}, true, { '{"b":1}' } },

-- Test encoding simple types
{ "Encode null",
json.encode, { json.null }, true, { 'null' } },
Expand Down Expand Up @@ -402,6 +432,21 @@ local cjson_tests = {
{ "Decode (safe) error generation after new()",
function(...) return json_safe.new().decode(...) end, { "Oops" },
true, { nil, "Expected value but found invalid token at character 1" } },

-- Test encoding invalid type callback
{ "set_invalid_type_encoder(...)",
json.set_invalid_type_encoder, { function() return "FOO" end }, true, { } },
{ "Encode Lua function with invalid_type_encoder",
json.encode, { function () end },
true, { "FOO" } },

-- Test sorted keys
{ "encode_sort_keys(true)",
json.encode_sort_keys, { true }, true, { } },
{ "test sorted encode",
json.encode, { {a=1,b=1,c=1,d=1,e=1,f=1,g=1,h=1,i=1,j=1,k=1,l=1,m=1,n=1,o=1} },
true, { '{"a":1,"b":1,"c":1,"d":1,"e":1,"f":1,"g":1,"h":1,"i":1,"j":1,"k":1,"l":1,"m":1,"n":1,"o":1}' } },

}

print(("==> Testing Lua CJSON version %s\n"):format(json._VERSION))
Expand Down