1
0
mirror of https://github.com/VCMP-SqMod/SqMod.git synced 2024-11-08 08:47:17 +01:00
SqMod/module/Library/JSON.cpp

704 lines
24 KiB
C++
Raw Normal View History

2021-07-10 13:15:41 +02:00
// ------------------------------------------------------------------------------------------------
#include "Library/JSON.hpp"
// ------------------------------------------------------------------------------------------------
#include <sqratConst.h>
// ------------------------------------------------------------------------------------------------
#include <cstdio>
// ------------------------------------------------------------------------------------------------
namespace SqMod {
// ------------------------------------------------------------------------------------------------
SQMOD_DECL_TYPENAME(SqCtxJSON, _SC("SqCtxJSON"))
2021-07-13 19:07:07 +02:00
// ------------------------------------------------------------------------------------------------
static SQInteger SqFromJson_Push(HSQUIRRELVM vm, const sajson::value & node) noexcept // NOLINT(misc-no-recursion)
2021-07-13 19:07:07 +02:00
{
2021-07-16 19:42:34 +02:00
// Operation result
SQInteger r = SQ_OK;
// Identify element type
switch (node.get_type())
2021-07-13 19:07:07 +02:00
{
2021-07-16 19:42:34 +02:00
case sajson::TYPE_INTEGER: {
sq_pushinteger(vm, static_cast< SQInteger >(node.get_integer_value()));
} break;
case sajson::TYPE_DOUBLE: {
sq_pushfloat(vm, static_cast< SQFloat >(node.get_double_value()));
} break;
case sajson::TYPE_NULL: {
2021-07-13 19:07:07 +02:00
sq_pushnull(vm);
2021-07-16 19:42:34 +02:00
} break;
case sajson::TYPE_FALSE: {
sq_pushbool(vm, SQFalse);
} break;
case sajson::TYPE_TRUE: {
sq_pushbool(vm, SQTrue);
} break;
case sajson::TYPE_STRING: {
sq_pushstring(vm, node.as_cstring(), static_cast< SQInteger >(node.get_string_length()));
} break;
case sajson::TYPE_ARRAY: {
// Array length
const size_t n = node.get_length();
// Create a new array on the stack
sq_newarrayex(vm, static_cast< SQInteger >(n));
// Process array elements
for (size_t i = 0; i < n; ++i)
2021-07-13 19:07:07 +02:00
{
// Transform the value into a script object on the stack
2021-07-16 19:42:34 +02:00
r = SqFromJson_Push(vm, node.get_array_element(i));
2021-07-13 19:07:07 +02:00
// Did we fail?
if (SQ_FAILED(r))
{
2021-07-16 19:42:34 +02:00
break; // Abort
}
// At this point we have a value on the stack
r = sq_arrayappend(vm, -2);
// Did we fail?
if (SQ_FAILED(r))
{
// Discard the value
2021-07-13 19:07:07 +02:00
sq_poptop(vm);
// Abort
break;
}
}
2021-07-16 19:42:34 +02:00
// Anything bad happened?
2021-07-13 19:07:07 +02:00
if (SQ_FAILED(r))
{
2021-07-16 19:42:34 +02:00
sq_poptop(vm); // Discard the array
2021-07-13 19:07:07 +02:00
}
2021-07-16 19:42:34 +02:00
} break;
case sajson::TYPE_OBJECT: {
// Object length
const size_t n = node.get_length();
// Create a new table on the stack
sq_newtableex(vm, static_cast< SQInteger >(n));
//
for (size_t i = 0; i < n; ++i)
2021-07-13 19:07:07 +02:00
{
2021-07-16 19:42:34 +02:00
const auto k = node.get_object_key(i);
// Transform the key into a script object on the stack
sq_pushstring(vm, k.data(), static_cast< SQInteger >(k.length()));
// Transform the value into a script object on the stack
r = SqFromJson_Push(vm, node.get_object_value(i));
// Did we fail?
if (SQ_FAILED(r))
{
// Discard the key
sq_poptop(vm);
// Abort
break;
}
// At this point we have a key and a value on the stack
r = sq_newslot(vm, -3, SQFalse);
// Did we fail?
if (SQ_FAILED(r))
{
// Discard the key/value pair
sq_pop(vm, 2);
// Abort
break;
}
2021-07-13 19:07:07 +02:00
}
2021-07-16 19:42:34 +02:00
// Anything bad happened?
2021-07-13 19:07:07 +02:00
if (SQ_FAILED(r))
{
2021-07-16 19:42:34 +02:00
sq_poptop(vm); // Discard the table
2021-07-13 19:07:07 +02:00
}
2021-07-16 19:42:34 +02:00
} break;
default:
// Should never really get here because it should be sanitized by the JSON parser
// But doesn't hurt to have it here in case something out of our scope goes wrong
r = sq_throwerror(vm, _SC("Unrecognized JSON type"));
2021-07-13 19:07:07 +02:00
}
2021-07-16 19:42:34 +02:00
// Return the result
return r;
2021-07-13 19:07:07 +02:00
}
2021-07-10 13:15:41 +02:00
2021-07-13 19:07:07 +02:00
// ------------------------------------------------------------------------------------------------
static SQInteger SqFromJSON(HSQUIRRELVM vm) noexcept
{
// Remember the current stack size
const SQInteger top = sq_gettop(vm);
// Was the JSON string specified?
if (top < 2)
{
return sq_throwerror(vm, _SC("Please specify the JSON string to parse"));
}
// JSON string storage
StackStrF s(vm, 2);
// Attempt to retrieve the specified JSON string
if (SQ_FAILED(s.Proc(true)))
{
return s.mRes; // Propagate the error
}
2021-07-16 19:42:34 +02:00
// Attempt to parse the specified JSON string
const sajson::document & document = sajson::parse(sajson::dynamic_allocation(), sajson::string(s.mPtr, static_cast<size_t>(s.mLen)));
2021-07-13 19:07:07 +02:00
// See if there was an error
2021-07-16 19:42:34 +02:00
if (!document.is_valid())
2021-07-13 19:07:07 +02:00
{
2021-07-16 19:42:34 +02:00
return sq_throwerror(vm, document.get_error_message_as_cstring());
2021-07-13 19:07:07 +02:00
}
2021-07-16 19:42:34 +02:00
// Process the nodes that were parsed from the string
SQInteger r = SqFromJson_Push(vm, document.get_root());
2021-07-13 19:07:07 +02:00
// We either have a value to return or we propagate some error
2021-07-16 19:42:34 +02:00
return SQ_SUCCEEDED(r) ? 1 : r;
2021-07-13 19:07:07 +02:00
}
2021-07-10 13:15:41 +02:00
2022-09-17 22:25:16 +02:00
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::OpenArray()
{
// Add the array-begin character
mOutput.push_back('[');
// Go forward one level
Advance();
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::CloseArray()
{
// If the last character is a comma then replace it
if (mOutput.back() == ',')
{
mOutput.back() = ']';
}
// Append the array-end character
else
{
mOutput.push_back(']');
}
2022-09-17 22:41:43 +02:00
// Move the comma after the closing character
mOutput.push_back(',');
2022-09-17 22:25:16 +02:00
// Go back one level
Retreat();
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::OpenObject()
{
// Add the object-begin character
mOutput.push_back('{');
// Go forward one level
Advance();
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::CloseObject()
{
// If the last character is a comma then replace it
if (mOutput.back() == ',')
{
mOutput.back() = '}';
}
// Append the object-end character
else
{
mOutput.push_back('}');
}
2022-09-17 22:41:43 +02:00
// Move the comma after the closing character
mOutput.push_back(',');
2022-09-17 22:25:16 +02:00
// Go back one level
Retreat();
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::MakeKey()
{
// If the last character is a comma then replace it
if (mOutput.back() == ',')
{
mOutput.back() = ':';
}
// Append the array-end character
else
{
mOutput.push_back(':');
}
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
bool CtxJSON::CheckWeakRefWrap(HSQUIRRELVM vm, SQInteger idx) noexcept
{
SQRESULT r = sq_getweakrefval(vm, idx);
// Whether the type doesn't have to be wrapped
bool w = true;
// Attempt to grab the value pointed by the weak reference
if (SQ_SUCCEEDED(r))
{
// Attempt to serialize the actual value
w = sq_gettype(vm, -1) != OT_TABLE && sq_gettype(vm, -1) != OT_ARRAY && sq_gettype(vm, -1) == OT_INSTANCE;
// Pop the referenced value
sq_poptop(vm);
}
// Wrap the value by default
return w;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeParams(HSQUIRRELVM vm)
{
bool wrap_everything_in_array = false;
// Clear the output buffer if necessary
mOutput.clear();
mDepth = 0;
// Fetch the number of objects on the stack
const auto top = sq_gettop(vm);
// If there's more than one argument then they all get wrapped inside an array
// If there is one argument and is not an array, table or instance then do the same
if (top > 2 || (sq_gettype(vm, 2) != OT_TABLE &&
sq_gettype(vm, 2) != OT_ARRAY &&
sq_gettype(vm, 2) != OT_INSTANCE &&
CheckWeakRefWrap(vm, 2)))
{
wrap_everything_in_array = true;
// Open an array
OpenArray();
}
// Serialize every specified argument
for (SQInteger i = 2; i <= top; ++i)
{
if (SQRESULT r = SerializeAt(vm, i); SQ_FAILED(r))
{
return r; // Propagate the error
}
}
// Was everything wrapped inside an array?
if (wrap_everything_in_array)
{
CloseArray();
}
// Remove trailing separator, if any
if (mOutput.back() == ',')
2022-09-17 22:25:16 +02:00
{
mOutput.pop_back();
}
// Push the output string on the stack
sq_pushstring(vm, mOutput.c_str(), static_cast< SQInteger >(mOutput.size()));
// Specify that we have a value on the stack
return 1;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeAt(HSQUIRRELVM vm, SQInteger idx) // NOLINT(misc-no-recursion)
{
// Identify object type
switch (sq_gettype(vm, idx))
{
case OT_NULL: {
PushNull();
} break;
case OT_INTEGER: {
SQInteger v;
// Attempt to retrieve the value from the stack
if (SQRESULT r = sq_getinteger(vm, idx, &v); SQ_FAILED(r))
{
return r; // Propagate the error
}
// Write the value in the output
PushInteger(v);
} break;
case OT_FLOAT: {
SQFloat v;
// Attempt to retrieve the value from the stack
if (SQRESULT r = sq_getfloat(vm, idx, &v); SQ_FAILED(r))
{
return r; // Propagate the error
}
// Write the value in the output
#ifdef SQUSEDOUBLE
PushDouble(v);
#else
PushFloat(v);
#endif
} break;
case OT_BOOL: {
SQBool v;
// Attempt to retrieve the value from the stack
if (SQRESULT r = sq_getbool(vm, idx, &v); SQ_FAILED(r))
{
return r; // Propagate the error
}
// Write the value in the output
PushBool(v != SQFalse);
} break;
case OT_STRING: {
const SQChar * v = nullptr;
SQInteger n = 0;
// Attempt to retrieve and convert the string
if (SQRESULT r = sq_getstringandsize(vm, idx, &v, &n); SQ_FAILED(r))
{
return r; // Propagate the error
}
// Write the value in the output
PushString(v, static_cast< size_t >(n));
} break;
case OT_TABLE: {
if (SQRESULT r = SerializeTable(vm, idx); SQ_FAILED(r))
{
return r; // Propagate the error
}
} break;
case OT_ARRAY: {
if (SQRESULT r = SerializeArray(vm, idx); SQ_FAILED(r))
{
return r; // Propagate the error
}
} break;
case OT_INSTANCE: {
if (SQRESULT r = SerializeInstance(vm, idx); SQ_FAILED(r))
{
return r; // Propagate the error
}
} break;
case OT_WEAKREF: {
if (SQRESULT r = SerializeWeakRef(vm, idx); SQ_FAILED(r))
{
return r; // Propagate the error
}
} break;
case OT_USERDATA:
case OT_CLOSURE:
case OT_NATIVECLOSURE:
case OT_GENERATOR:
case OT_USERPOINTER:
case OT_THREAD:
case OT_FUNCPROTO:
case OT_CLASS:
case OT_OUTER:
return sq_throwerrorf(vm, _SC("Type (%s) is not serializable"), SqTypeName(sq_gettype(vm, 2)));
}
// Serialization was successful
return SQ_OK;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeArray(HSQUIRRELVM vm, SQInteger idx) // NOLINT(misc-no-recursion)
{
// Begin array scope
OpenArray();
// Push null to initiate iteration
sq_pushnull(vm);
// So we can use absolute stack indexes to avoid errors
const auto top = sq_gettop(vm);
// Start iterating the array at the specified position in the stack
for(SQRESULT r = SQ_OK; SQ_SUCCEEDED(sq_next(vm, idx));)
{
// Attempt serialization of the currently iterated value
r = SerializeAt(vm, top + 2);
// Check for failures
if (SQ_FAILED(r))
{
// Pop the null iterator, key and value from the stack
sq_pop(vm, 3);
// Propagate the error
return r;
}
// Pop the key and value from the stack (i.e. cleanup after `sq_next`)
sq_pop(vm, 2);
}
// Pop the null iterator
sq_poptop(vm);
// Close array scope
CloseArray();
// Serialization was successful
return SQ_OK;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeTable(HSQUIRRELVM vm, SQInteger idx) // NOLINT(misc-no-recursion)
{
// Begin object scope
OpenObject();
// Push null to initiate iteration
sq_pushnull(vm);
// So we can use absolute stack indexes to avoid errors
const auto top = sq_gettop(vm);
// Start iterating the object at the specified position in the stack
for(SQRESULT r = SQ_OK; SQ_SUCCEEDED(sq_next(vm, idx));)
{
if (sq_gettype(vm, -2) == OT_STRING)
{
// Attempt serialization of the currently iterated element key
r = SerializeAt(vm, top + 1);
// Can we proceed with the value?
if (SQ_SUCCEEDED(r))
{
// Mark the value above as the key of this element and
// attempt serialization of the currently iterated element value
r = MakeKey().SerializeAt(vm, top + 2);
}
}
else
{
r = sq_throwerror(vm, _SC("Only string values are accepted as object keys"));
}
// Check for failures
if (SQ_FAILED(r))
{
// Pop the null iterator, key and value from the stack
sq_pop(vm, 3);
// Propagate the error
return r;
}
// Pop the key and value from the stack (i.e. cleanup after `sq_next`)
sq_pop(vm, 2);
}
// Pop the null iterator
sq_poptop(vm);
// Close object scope
CloseObject();
// Serialization was successful
return SQ_OK;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeInstance(HSQUIRRELVM vm, SQInteger idx)
{
sq_pushstring(vm, _SC("_tojson"), 7);
// Attempt to retrieve the meta-method from the instance
if(SQRESULT r = sq_get(vm, idx); SQ_FAILED(r))
{
return r; // Propagate the error
}
// Make sure this is actually a closure/function that we can invoke
else if (const auto t = sq_gettype(vm, -1); t != OT_CLOSURE && t != OT_NATIVECLOSURE)
{
// Remove whatever is on the stack
sq_poptop(vm);
// Abort the operation as we can't do anything about it
return sq_throwerrorf(vm, _SC("`_tojson` meta-method is not a closure for type (%s)"), SqTypeName(vm, idx).c_str());
}
// Push the instance itself the stack (the environment)
sq_push(vm, idx);
// Push this instance on the stack (the json context)
ClassType< CtxJSON >::PushInstance(vm, this);
// Invoke the function to perform the conversion in this context
SQRESULT r = sq_call(vm, 2, SQFalse, SQFalse);
// Remove the closure from the stack
sq_poptop(vm);
// Propagate the result, whatever that is
return r;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::SerializeWeakRef(HSQUIRRELVM vm, SQInteger idx) // NOLINT(misc-no-recursion)
{
SQRESULT r = sq_getweakrefval(vm, idx);
// Attempt to grab the value pointed by the weak reference
if (SQ_SUCCEEDED(r))
{
// Attempt to serialize the actual value
r = SerializeAt(vm, sq_gettop(vm));
// Pop the referenced value
sq_poptop(vm);
}
// Propagate the error, if any
return r;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::PushValues(HSQUIRRELVM vm)
{
// Fetch the number of objects on the stack
const auto top = sq_gettop(vm);
// Do we have a value?
if (top < 2)
{
return sq_throwerror(vm, _SC("Must specify at least one value to be pushed"));
}
// Serialize every specified argument
for (SQInteger i = 2; i <= top; ++i)
{
if (SQRESULT r = SerializeAt(vm, i); SQ_FAILED(r))
{
return r; // Propagate the error
}
}
// Allow chaining
sq_push(vm, 1);
// Specify that a value was returned
return 1;
}
// ------------------------------------------------------------------------------------------------
SQRESULT CtxJSON::PushElement(HSQUIRRELVM vm)
{
// Fetch the number of objects on the stack
const auto top = sq_gettop(vm);
// Do we have a value?
if (top < 3)
{
return sq_throwerrorf(vm, _SC("Not enough parameters. Received %lld but %lld needed"), top-1, 2);
}
else if (sq_gettype(vm, 2) != OT_STRING)
{
return sq_throwerrorf(vm, _SC("Element key must be a string"));
}
// Attempt serialization of the currently iterated element key
if (SQRESULT r = SerializeAt(vm, 2); SQ_SUCCEEDED(r))
{
// Mark the value above as the key of this element and
// attempt serialization of the currently iterated element value
r = MakeKey().SerializeAt(vm, 3);
// Check for failures
if (SQ_FAILED(r))
{
return r; // Propagate the error
}
}
// Allow chaining
sq_push(vm, 1);
// Specify that a value was returned
return 1;
}
// ------------------------------------------------------------------------------------------------
CtxJSON & CtxJSON::PushKey(StackStrF & key)
{
// Validate the string value
if (key.mLen >= 0 && SQ_SUCCEEDED(key.mRes))
{
PushString(key.mPtr, static_cast< size_t >(key.mLen));
MakeKey();
}
else
{
STHROWF("Invalid object key");
}
// Allow chaining
return *this;
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushNull()
{
mOutput.append("null,");
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushInteger(SQInteger value)
{
fmt::format_int f(value);
// Append the formatted integer to the buffer
mOutput.append(f.data(), f.size());
mOutput.push_back(',');
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushFloat(float value)
{
fmt::format_to(std::back_inserter(mOutput), "{},", value);
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushDouble(double value)
{
fmt::format_to(std::back_inserter(mOutput), "{},", value);
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushBool(bool value)
{
if (value)
{
mOutput.append("true,", 5);
}
else
{
mOutput.append("false,", 6);
}
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushString(const SQChar * str)
{
mOutput.push_back('"');
mOutput.append(str);
mOutput.push_back('"');
mOutput.push_back(',');
}
// ------------------------------------------------------------------------------------------------
void CtxJSON::PushString(const SQChar * str, size_t length)
{
mOutput.push_back('"');
mOutput.append(str, length);
mOutput.push_back('"');
mOutput.push_back(',');
}
// ------------------------------------------------------------------------------------------------
static SQInteger SqToJSON(HSQUIRRELVM vm) noexcept
{
// Make sure the instance is cleaned up even in the case of exceptions
DeleteGuard< CtxJSON > sq_dg(new CtxJSON());
// Remember the instance, so we don't have to cast the script object back
auto ctx = sq_dg.Get();
// Turn it into a script object because it may be passed as a parameter to `_tojson` meta-methods
LightObj obj(sq_dg, vm);
// Proceed with the serialization
return ctx->SerializeParams(vm);
}
// ------------------------------------------------------------------------------------------------
static SQInteger SqToCompactJSON(HSQUIRRELVM vm) noexcept
{
// Make sure the instance is cleaned up even in the case of exceptions
DeleteGuard< CtxJSON > sq_dg(new CtxJSON(false));
// Remember the instance, so we don't have to cast the script object back
auto ctx = sq_dg.Get();
// Turn it into a script object because it may be passed as a parameter to `_tojson` meta-methods
LightObj obj(sq_dg, vm);
// Proceed with the serialization
return ctx->SerializeParams(vm);
}
2021-07-10 13:15:41 +02:00
// ================================================================================================
void Register_JSON(HSQUIRRELVM vm)
{
2021-07-13 19:07:07 +02:00
RootTable(vm).SquirrelFunc(_SC("SqToJSON"), SqToJSON);
RootTable(vm).SquirrelFunc(_SC("SqToCompactJSON"), SqToCompactJSON);
2021-07-13 19:07:07 +02:00
RootTable(vm).SquirrelFunc(_SC("SqFromJSON"), SqFromJSON);
// --------------------------------------------------------------------------------------------
RootTable(vm).Bind(_SC("SqCtxJSON"),
Class< CtxJSON, NoCopy< CtxJSON > >(vm, SqCtxJSON::Str)
// Constructors
.Ctor()
.Ctor< bool >()
// Meta-methods
.SquirrelFunc(_SC("_typename"), &SqCtxJSON::Fn)
// Properties
.Prop(_SC("Output"), &CtxJSON::GetOutput)
.Prop(_SC("Depth"), &CtxJSON::GetDepth)
.Prop(_SC("OOA"), &CtxJSON::GetObjectOverArray, &CtxJSON::SetObjectOverArray)
.Prop(_SC("ObjectOverArray"), &CtxJSON::GetObjectOverArray, &CtxJSON::SetObjectOverArray)
// Member Methods
.SquirrelMethod< CtxJSON, &CtxJSON::SerializeParams >(_SC("Serialize"))
.SquirrelMethod< CtxJSON, &CtxJSON::PushValues >(_SC("PushValues"))
.SquirrelMethod< CtxJSON, &CtxJSON::PushElement >(_SC("PushElement"))
.Func(_SC("OpenArray"), &CtxJSON::OpenArray)
.Func(_SC("CloseArray"), &CtxJSON::CloseArray)
.Func(_SC("OpenObject"), &CtxJSON::OpenObject)
.Func(_SC("CloseObject"), &CtxJSON::CloseObject)
.Func(_SC("MakeKey"), &CtxJSON::MakeKey)
.FmtFunc(_SC("PushKey"), &CtxJSON::PushKey)
.Func(_SC("SetOOA"), &CtxJSON::SetObjectOverArray)
.Func(_SC("SetObjectOverArray"), &CtxJSON::SetObjectOverArray)
);
2021-07-10 13:15:41 +02:00
}
} // Namespace:: SqMod