-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1272 from BaerMitUmlaut/json-parser
Add JSON parse and encode function
- Loading branch information
Showing
6 changed files
with
502 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 + "}" | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.