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

Add JSON parse and encode function #1272

Merged
merged 12 commits into from
Jan 3, 2020
2 changes: 2 additions & 0 deletions addons/hashes/CfgFunctions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class CfgFunctions {
PATHTO_FNC(parseYAML);
PATHTO_FNC(serializeNamespace);
PATHTO_FNC(deserializeNamespace);
PATHTO_FNC(encodeJSON);
PATHTO_FNC(parseJSON);
};
};
};
94 changes: 94 additions & 0 deletions addons/hashes/fnc_encodeJSON.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "script_component.hpp"
/* ----------------------------------------------------------------------------
Function: CBA_fnc_encodeJSON

Description:
Serializes input to a JSON string. Can handle
- ARRAY
- BOOL
- CONTROL
- GROUP
- LOCATION
- NAMESPACE
- NIL (ANY)
- NUMBER
- OBJECT
- STRING
- TASK
- TEAM_MEMBER
- Everything else will simply be stringified.

Parameters:
_object - Object to serialize. <ARRAY, ...>

Returns:
_json - JSON string containing serialized object.

Examples:
(begin example)
private _settings = call CBA_fnc_createNamespace;
_settings setVariable ["enabled", true];
private _json = [_settings] call CBA_fnc_encodeJSON;
(end)

Author:
BaerMitUmlaut
---------------------------------------------------------------------------- */
SCRIPT(encodeJSON);
params ["_object"];

if (isNil "_object") exitWith { "null" };

switch (typeName _object) do {
case "SCALAR";
case "BOOL": {
commy2 marked this conversation as resolved.
Show resolved Hide resolved
str _object;
};

case "STRING": {
{
_object = [_object, _x#0, _x#1] call CBA_fnc_replace;
} forEach [
["\", "\\"],
["""", "\"""],
[toString [8], "\b"],
[toString [12], "\f"],
[endl, "\n"],
[toString [10], "\n"],
[toString [13], "\r"],
[toString [9], "\t"]
];
"""" + _object + """"
BaerMitUmlaut marked this conversation as resolved.
Show resolved Hide resolved
};

case "ARRAY": {
if ([_object] call CBA_fnc_isHash) then {
private _json = (([_object] call CBA_fnc_hashKeys) apply {
private _name = _x;
private _value = [_object, _name] call CBA_fnc_hashGet;

format ["%1: %2", [_name] call CBA_fnc_encodeJSON, [_value] call CBA_fnc_encodeJSON]
}) joinString ", ";
"{" + _json + "}"
} else {
private _json = (_object apply {[_x] call CBA_fnc_encodeJSON}) joinString ", ";
"[" + _json + "]"
};
};

default {
if !(typeName _object in (supportInfo "u:allVariables*" apply {_x splitString " " select 1})) exitWith {
[str _object] call CBA_fnc_encodeJSON
};

if (isNull _object) exitWith { "null" };
commy2 marked this conversation as resolved.
Show resolved Hide resolved

private _json = ((allVariables _object) apply {
private _name = _x;
private _value = _object getVariable _name;

format ["%1: %2", [_name] call CBA_fnc_encodeJSON, [_value] call CBA_fnc_encodeJSON]
}) joinString ", ";
"{" + _json + "}"
};
};
255 changes: 255 additions & 0 deletions addons/hashes/fnc_parseJSON.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
#include "script_component.hpp"
/* ----------------------------------------------------------------------------
Function: CBA_fnc_parseJSON

Description:
Deserializes a JSON string.

Parameters:
_json - String containing valid JSON. <STRING>
_useHashes - Output CBA hashes instead of namespaces
(optional, default: false) <BOOLEAN>

Returns:
_object - The deserialized JSON object or nil if JSON is invalid.
<LOCATION, ARRAY, STRING, NUMBER, BOOL, NIL>

Examples:
(begin example)
private _json = "{ ""enabled"": true }";
private _settings = [_json] call CBA_fnc_parseJSON;
private _enabled = _settings getVariable "enabled";

loadFile "data\config.json" call CBA_fnc_parseJSON
[preprocessFile "data\config.json", true] call CBA_fnc_parseJSON
commy2 marked this conversation as resolved.
Show resolved Hide resolved
(end)

Author:
BaerMitUmlaut
---------------------------------------------------------------------------- */
SCRIPT(parseJSON);
params ["_json", ["_useHashes", false]];

// Wrappers for creating "objects" and setting values on them
private ["_objectSet", "_createObject"];
if (_useHashes) then {
_createObject = CBA_fnc_hashCreate;
_objectSet = CBA_fnc_hashSet;
} else {
_createObject = CBA_fnc_createNamespace;
_objectSet = {
params ["_obj", "_key", "_val"];
_obj setVariable [_key, _val];
};
};

// Handles escaped characters, except for unicode escapes (\uXXXX)
private _unescape = {
params ["_char"];

switch (_char) do {
case """": { """" };
case "\": { "\" };
case "/": { "/" };
case "b": { toString [8] };
case "f": { toString [12] };
case "n": { endl };
case "r": { toString [13] };
case "t": { toString [9] };
default { "" };
};
};

// Splits the input string into tokens
// Tokens can be numbers, strings, null, true, false and symbols
// Strings are prefixed with $ to distinguish them from symbols
private _tokenize = {
params ["_input"];

// Split string into chars, works with unicode unlike splitString
_input = toArray _input apply {toString [_x]};

private _tokens = [];
private _numeric = "+-.0123456789e" splitString "";
private _symbols = "{}[]:," splitString "";
private _consts = "tfn" splitString "";

while {count _input > 0} do {
private _c = _input deleteAt 0;

switch (true) do {
// Symbols ({}[]:,) are passed directly into the tokens
case (_c in _symbols): {
_tokens pushBack _c;
};

// Number parsing
// This can fail with some invalid JSON numbers, like e10
// Those would require some additional logic or regex
// Valid numbers are all parsed correctly though
case (_c in _numeric): {
private _numStr = _c;
while { _c = _input deleteAt 0; !isNil "_c" && {_c in _numeric} } do {
_numStr = _numStr + _c;
};
_tokens pushBack parseNumber _numStr;

if (!isNil "_c") then {
_input = [_c] + _input;
};
};

// true, false and null
// Only check first char and assume JSON is valid
case (_c in _consts): {
switch (_c) do {
case "t": {
_input deleteRange [0, 3];
_tokens pushBack true;
};
case "f": {
_input deleteRange [0, 4];
_tokens pushBack false;
};
case "n": {
_input deleteRange [0, 3];
_tokens pushBack objNull;
};
};
};

// String parsing
case (_c == """"): {
private _str = "$";

while {true} do {
_c = _input deleteAt 0;

if (_c == """") exitWith {};

if (_c == "\") then {
_str = _str + ((_input deleteAt 0) call _unescape);
} else {
_str = _str + _c;
};
};

_tokens pushBack _str;
};
};
};

_tokens
};

// Appends the next token to the parsing stack
// Returns true unless no more tokens left
private _shift = {
params ["_parseStack", "_tokens"];

if (count _tokens > 0) then {
_parseStack pushBack (_tokens deleteAt 0);
true
} else {
false
};
};

// Tries to reduce the current parsing stack (collect arrays or objects)
// Returns true if parsing stack could be reduced
private _reduce = {
params ["_parseStack", "_tokens"];

// Nothing to reduce
if (count _parseStack == 0) exitWith { false };

// Check top of stack
switch (_parseStack#(count _parseStack - 1)) do {

// Reached end of array, time to collect elements
case "]": {
private _array = [];

// Empty arrays need special handling
if !(_parseStack#(count _parseStack - 2) isEqualTo "[") then {
// Get next token, if [ beginning is reached, otherwise assume
// valid JSON and that the token is a comma
while {_parseStack deleteAt (count _parseStack - 1) != "["} do {
private _element = _parseStack deleteAt (count _parseStack - 1);

// Remove $ prefix from string
if (_element isEqualType "") then {
_element = _element select [1];
};

_array pushBack _element;
};

reverse _array;
} else {
_parseStack resize (count _parseStack - 2);
};

_parseStack pushBack _array;
true
};

// Reached end of array, time to collect elements
// Works very similar to arrays
case "}": {
private _object = [] call _createObject;

// Empty objects need special handling
if !(_parseStack#(count _parseStack - 2) isEqualTo "{") then {
// Get next token, if { beginning is reached, otherwise assume
// valid JSON and that token is comma
while {_parseStack deleteAt (count _parseStack - 1) != "{"} do {
private _value = _parseStack deleteAt (count _parseStack - 1);
private _colon = _parseStack deleteAt (count _parseStack - 1);
private _name = _parseStack deleteAt (count _parseStack - 1);

// Remove $ prefix from strings
if (_value isEqualType "") then {
_value = _value select [1];
};
_name = _name select [1];

[_object, _name, _value] call _objectSet;
};
} else {
_parseStack resize (count _parseStack - 2);
};

_parseStack pushBack _object;
true
};

default {
false
};
};
};

// Simple shift-reduce parser
private _parse = {
params ["_tokens"];
private _parseStack = [];
private _params = [_parseStack, _tokens];

while { _params call _reduce || {_params call _shift} } do {};

if (count _parseStack != 1) then {
nil
} else {
private _object = _parseStack#0;

// If JSON is just a string, remove $ prefix from it
if (_object isEqualType "") then {
_object = _object select [1];
};

_object
};
};

[_json call _tokenize] call _parse
2 changes: 1 addition & 1 deletion addons/hashes/test.sqf
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#define DEBUG_MODE_FULL
#include "script_component.hpp"

#define TESTS ["hashEachPair", "hashes", "parseYaml", "hashFilter"]
#define TESTS ["hashEachPair", "hashes", "parseJSON", "parseYaml", "hashFilter"]

SCRIPT(test-hashes);

Expand Down
Loading