Skip to content

Commit

Permalink
Merge pull request #1272 from BaerMitUmlaut/json-parser
Browse files Browse the repository at this point in the history
Add JSON parse and encode function
  • Loading branch information
commy2 committed Jan 3, 2020
2 parents bd9f0c4 + d3b8f32 commit 4e6845e
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 1 deletion.
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);
};
};
};
95 changes: 95 additions & 0 deletions addons/hashes/fnc_encodeJSON.sqf
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#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": {
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"]
];
// Stringify without escaping inter string quote marks.
"""" + _object + """"
};

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" };

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
(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

0 comments on commit 4e6845e

Please sign in to comment.