UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1390 lines
52 KiB

/**
* Provides convenient access to JSON-related functions.
* Printing method produce correct JSON.
* Parsing methods do not provide validity checks guarantees and will parse
* both valid and invalid JSON. However only correctly parsing valid JSON
* is guaranteed. This means that you should not rely on these methods to parse
* any JSON extensions or validate JSON for you.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JSONAPI extends AcediaObject
config(AcediaSystem);
var private bool formattingInitialized;
// Variables used in json pretty printing for defining used colors;
// Colors are taken from `ColorAPI`.
var private Text.Formatting jPropertyName, jObjectBraces, jArrayBraces, jComma;
var private Text.Formatting jColon, jNumber, jBoolean, jString, jNull;
var const int TNULL, TTRUE, TFALSE, TDOT, TEXPONENT;
var const int TOPEN_BRACKET, TCLOSE_BRACKET, TOPEN_BRACE, TCLOSE_BRACE;
var const int TCOMMA, TCOLON, TQUOTE, TJSON_INDENT, TSPACE, TCOLON_SPACE;
var const int CODEPOINT_BACKSPACE, CODEPOINT_TAB, CODEPOINT_LINE_FEED;
var const int CODEPOINT_FORM_FEED, CODEPOINT_CARRIAGE_RETURN;
var const int CODEPOINT_QUOTATION_MARK, CODEPOINT_SOLIDUS;
var const int CODEPOINT_REVERSE_SOLIDUS, CODEPOINT_SMALL_B, CODEPOINT_SMALL_F;
var const int CODEPOINT_SMALL_N, CODEPOINT_SMALL_R, CODEPOINT_SMALL_T;
// Max precision that will be used when outputting JSON values as a string.
// Hardcoded to force this value between 0 and 10, inclusively.
var private const config int MAX_FLOAT_PRECISION;
// Method for initializing json formatting variables
private final function InitFormatting()
{
if (formattingInitialized) {
return;
}
formattingInitialized = true;
jPropertyName = _.text.FormattingFromColor(_.color.jPropertyName);
jObjectBraces = _.text.FormattingFromColor(_.color.jObjectBraces);
jArrayBraces = _.text.FormattingFromColor(_.color.jArrayBraces);
jComma = _.text.FormattingFromColor(_.color.jComma);
jColon = _.text.FormattingFromColor(_.color.jColon);
jNumber = _.text.FormattingFromColor(_.color.jNumber);
jBoolean = _.text.FormattingFromColor(_.color.jBoolean);
jString = _.text.FormattingFromColor(_.color.jString);
jNull = _.text.FormattingFromColor(_.color.jNull);
}
/**
* Creates new `JSONPointer`, corresponding to a given path in
* JSON pointer format (https://tools.ietf.org/html/rfc6901).
*
* If provided `Text` value is an incorrect pointer, then it will be
* treated like an empty pointer.
* However, if given pointer can be fixed by prepending "/" - it will be
* done automatically. This means that "foo/bar" is treated like "/foo/bar",
* "path" like "/path", but empty `Text` "" is treated like itself.
*
* @param pointerAsText `Text` representation of the JSON pointer.
* @return New `JSONPointer`, corresponding to the given `pointerAsText`.
* Guaranteed to not be `none`. If provided `pointerAsText` is
* an incorrect JSON pointer or `none`, - empty `JSONPointer` will be
* returned.
*/
public final function JSONPointer Pointer(Text pointerAsText)
{
return JSONPointer(_.memory.Allocate(class'JSONPointer'))
.Set(pointerAsText);
}
/**
* Checks whether passed `AcediaObject` can be converted into JSON by this API.
*
* Compatible objects are `none` and any object that has one of the following
* classes: `BoolBox`, `BoolRef`, `ByteBox`, `ByteRef`, `IntBox`, `IntRef`,
* `FloatBox`, `FloatRef`, `Text`, `MutableText`, `DynamicArray`,
* `AssociativeArray`.
*
* This method does not check whether objects stored inside `DynamicArray`,
* `AssociativeArray` are compatible. If they are not, they will normally be
* defaulted to JSON null upon any conversion.
*/
public function bool IsCompatible(AcediaObject data)
{
local class<AcediaObject> dataClass;
if (data == none) {
return true;
}
dataClass = data.class;
return dataClass == class'BoolBox' || dataClass == class'BoolRef'
|| dataClass == class'ByteBox' || dataClass == class'ByteRef'
|| dataClass == class'IntBox' || dataClass == class'IntRef'
|| dataClass == class'FloatBox' || dataClass == class'FloatRef'
|| dataClass == class'Text' || dataClass == class'MutableText'
|| dataClass == class'DynamicArray'
|| dataClass == class'AssociativeArray';
}
/**
* Uses given parser to parse a null JSON value ("null" in arbitrary case).
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON null term (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value from
* it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
*/
public final function TryNullWith(Parser parser)
{
if (parser != none) {
parser.Match(T(default.TNULL), SCASE_INSENSITIVE);
}
}
/**
* Tries to parse null JSON value ("null" in arbitrary case) and reports
* whether parsing succeeded.
*
* `source` must contain precisely a null JSON value and nothing else for this
* method to succeed. For example, even having leading/trailing whitespace
* symbols (" null" or "null ") is enough to fail parsing.
*
* @param source `Text` instance to parse JSON null value from.
* @return `true` if parsing succeeded and `false` otherwise.
*/
public final function bool IsNull(Text source)
{
local bool parsingSucceeded;
local Parser parser;
if (source == none) return false;
parser = _.text.Parse(source);
parser.Match(T(default.TNULL), SCASE_INSENSITIVE);
parsingSucceeded = parser.Ok() && parser.HasFinished();
parser.FreeSelf();
return parsingSucceeded;
}
/**
* Uses given parser to parse a JSON boolean ("true" or "false" with
* arbitrary case).
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON boolean (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value from
* it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @return Parsed boolean value if parsing was successful and
* `false` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function bool ParseBooleanVariableWith(Parser parser)
{
local Parser.ParserState initState;
if (parser == none) return false;
if (!parser.Ok()) return false;
initState = parser.GetCurrentState();
// Check if we should return `true`
if (parser.Match(T(default.TTRUE), SCASE_INSENSITIVE).Ok()) {
return true;
}
// We need to try parsing "false", so that we can use `parser`'s state
// to report about success of parsing; but we return `false` anyway.
parser.RestoreState(initState).Match(T(default.TFALSE), SCASE_INSENSITIVE);
return false;
}
/**
* Uses given parser to parse a JSON boolean ("true" or "false"
* with arbitrary case) into either `BoolBox` or `BoolRef`.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON boolean (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`BoolRef`) and `false` if immutable (`BoolBox`).
* @return Parsed boolean value as an `AcediaObject` if parsing was successful
* and `none` otherwise. If parsing succeeded, it is guaranteed to
* be not `none` and have correct class, determined by
* `parseAsMutable` parameter.
* Returns `none` iff parsing has failed.
*/
public final function AcediaObject ParseBooleanWith(
Parser parser,
optional bool parseAsMutable)
{
local bool result;
if (parser == none) return none;
result = ParseBooleanVariableWith(parser);
if (!parser.Ok()) {
return none;
}
if (parseAsMutable) {
return _.ref.bool(result);
}
else {
return _.box.bool(result);
}
}
/**
* Parses a JSON boolean ("true" or "false" with arbitrary case) from
* a given `source` into either `BoolBox` or `BoolRef`.
*
* `source` must contain precisely a boolean value and nothing else for this
* method to succeed. For example, even having leading/trailing whitespace
* symbols (" true" or "false ") is enough to fail parsing.
*
* @param source `Text` instance to parse JSON boolean value from.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`BoolRef`) and `false` if immutable (`BoolBox`).
* @return Parsed boolean value as an `AcediaObject` if parsing was successful
* and `none` otherwise. If parsing succeeded, it is guaranteed to
* be not `none` and have correct class, determined by
* `parseAsMutable` parameter.
* Returns `none` iff parsing has failed.
*/
public final function AcediaObject ParseBoolean(
Text source,
optional bool parseAsMutable)
{
local bool result;
local bool parsingFailed;
local Parser parser;
if (source == none) return none;
parser = _.text.Parse(source);
result = ParseBooleanVariableWith(parser);
parsingFailed = !parser.Ok() || !parser.HasFinished();
parser.FreeSelf();
if (parsingFailed) {
return none;
}
if (parseAsMutable) {
return _.ref.bool(result);
}
else {
return _.box.bool(result);
}
}
/**
* Uses given parser to parse a JSON number into an integer.
*
* If number is written in an "integer form" (not dot "." or exponent "e"),
* then it will be directly be parsed as an `int`. Otherwise it will be
* parsed as a `float` and the converted into `int`, with appropriate loss
* of precision.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse a JSON number
* (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* To parse a JSON number into a `float` use `ParseFloatVariableWith()` method.
*
* @param parser Parser that method would use to parse JSON value from
* it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param integerOnly Setting this parameter to `true` will prevent method
* from parsing number as a `float` (and possibly losing precision):
* in case it is written in a `float`, parsing will be considered failed.
* @return Parsed integer value if parsing was successful and
* `0` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function int ParseIntegerVariableWith(
Parser parser,
optional bool integerOnly)
{
local int integerValue;
local bool isInFloatForm;
local float floatValue;
local Parser.ParserState initState, integerParsedState;
if (parser == none) return 0;
initState = parser.GetCurrentState();
if (!parser.MInteger(integerValue, 10).Ok()) {
return 0;
}
// `integerParsedState` is guaranteed to be a successful state
integerParsedState = parser.GetCurrentState();
// JSON number recorded as float form will have either dot or exponent
// after the integer part.
isInFloatForm = parser.Match(T(default.TDOT)).Ok();
parser.RestoreState(integerParsedState);
if (parser.Match(T(default.TEXPONENT), SCASE_INSENSITIVE).Ok()) {
isInFloatForm = true;
}
// For a number check if it can be parsed as a float specifically.
// If not - use parsed integer.
parser.RestoreState(initState);
if (isInFloatForm && parser.MNumber(floatValue).Ok())
{
if (integerOnly)
{
parser.Fail();
return 0;
}
return int(floatValue);
}
parser.RestoreState(integerParsedState);
return integerValue;
}
/**
* Uses given parser to parse a JSON number.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON number (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* To parse a JSON number into an `int` use `ParseIntegerVariableWith()` method.
*
* @param parser Parser that method would use to parse JSON value from
* it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @return Parsed number value if parsing was successful and
* `0.0` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function float ParseFloatVariableWith(Parser parser)
{
local float floatValue;
if (parser == none) return 0.0;
if (!parser.MNumber(floatValue).Ok()) return 0.0;
return floatValue;
}
/**
* Uses given parser to parse a JSON number into one of the following
* object classes: `IntBox`, `IntRef`, `FloatBox`, `FloatRef`, depending on
* parameters and how numeric value is recorded.
*
* To improve precision, this method will try to parse JSON number as
* an integer (`IntBox` or `IntRef`) if possible (if number does not include
* fractional or exponent parts: "." or "e").
* Otherwise it will parse number as a floating point value
* (`FloatBox` or `FloatRef`).
* The choice between box and reference is made depending on the method's
* parameter `parseAsMutable`.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON number (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`IntRef` or `FloatRef`) and `false` if immutable
* (`IntBox` or `FloatBox`).
* @return Parsed number value as an `AcediaObject` if parsing was successful
* and `none` otherwise. If parsing succeeded, it is guaranteed to
* be not `none` and have correct class, determined partly by
* `parseAsMutable` parameter.
* Returns `none` iff parsing has failed.
*/
public final function AcediaObject ParseNumberWith(
Parser parser,
optional bool parseAsMutable)
{
local int integerResult;
local float floatResult;
local Parser.ParserState initState;
if (parser == none) return none;
initState = parser.GetCurrentState();
// Try parsing into `int`;
// this will fail if number recorded in floating format.
integerResult = ParseIntegerVariableWith(parser, true);
if (parser.Ok())
{
if (parseAsMutable) {
return _.ref.int(integerResult);
}
else {
return _.box.int(integerResult);
}
}
// If simple integer does not work - try to parse it as `float`
floatResult = ParseFloatVariableWith(parser.RestoreState(initState));
if (!parser.Ok()) {
return none;
}
if (parseAsMutable) {
return _.ref.float(floatResult);
}
else {
return _.box.float(floatResult);
}
}
/**
* Parses a JSON number from `source` into one of the following
* object classes: `IntBox`, `IntRef`, `FloatBox`, `FloatRef`, depending on
* parameters and how numeric value is recorded.
*
* To improve precision, this method will try to parse JSON number as
* an integer (`IntBox` or `IntRef`) if possible (if number does not include
* fractional or exponent parts: "." or "e").
* Otherwise it will parse number as a floating point value
* (`FloatBox` or `FloatRef`).
* The choice between box and reference is made depending on the method's
* parameter `parseAsMutable`.
*
* `source` must contain precisely a numeric value and nothing else for this
* method to succeed. For example, even having leading/trailing whitespace
* symbols (" 75.3" or "9 ") is enough to fail parsing.
*
* @param source `Text` instance to parse JSON number from.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`IntRef` or `FloatRef`) and `false` if immutable
* (`IntBox` or `FloatBox`).
* @return Parsed number value as an `AcediaObject` if parsing was successful
* and `none` otherwise. If parsing succeeded, it is guaranteed to
* be not `none` and have correct class, determined partly by
* `parseAsMutable` parameter.
* Returns `none` iff parsing has failed.
*/
public final function AcediaObject ParseNumber(
Text source,
optional bool parseAsMutable)
{
local int integerResult;
local float floatResult;
local Parser parser;
parser = _.text.Parse(source);
// Try parsing into `int`;
// this will fail if number recorded in floating format.
integerResult = ParseIntegerVariableWith(parser, true);
if (parser.Ok() && parser.HasFinished())
{
parser.FreeSelf();
if (parseAsMutable) {
return _.ref.int(integerResult);
}
else {
return _.box.int(integerResult);
}
}
// If simple integer does not work - try to parse it as `float`
floatResult = ParseFloatVariableWith(parser.R());
if (!parser.Ok() || !parser.HasFinished())
{
parser.FreeSelf();
return none;
}
parser.FreeSelf();
if (parseAsMutable) {
return _.ref.float(floatResult);
}
else {
return _.box.float(floatResult);
}
}
/**
* Uses given parser to parse a JSON string.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON string (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`MutableText`) and `false` if immutable (`Text`).
* @return Parsed string value as `Text` or `MutableText` (depending on
* `parseAsMutable` parameter) if parsing was successful and
* `none` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function Text ParseStringWith(
Parser parser,
optional bool parseAsMutable)
{
local Text immutableTextValue;
local MutableText mutableTextValue;
if (parser == none) {
return none;
}
parser.MStringLiteral(mutableTextValue);
if (!parser.Ok()) {
mutableTextValue.FreeSelf();
return none;
}
if (parseAsMutable) {
return mutableTextValue;
}
immutableTextValue = mutableTextValue.Copy();
mutableTextValue.FreeSelf();
return immutableTextValue;
}
/**
* Parses a JSON string from `source` into either `Text` or `MutableText`,
* depending on parameters.
*
* `source` must contain precisely a JSON string value and nothing else for
* this method to succeed. For example, even having leading/trailing whitespace
* symbols (" \"string!\"" or "\"another!\" ") is enough to fail parsing.
*
* @param source `Text` instance to parse JSON string from.
* @param parseAsMutable `true` if you want this method to return mutable
* object (`MutableText`) and `false` if immutable (`Text`).
* @return Parsed string value as `Text` or `MutableText` (depending on
* `parseAsMutable` parameter) if parsing was successful and
* `none` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function Text ParseString(
Text source,
optional bool parseAsMutable)
{
local bool parsingSuccessful;
local Parser parser;
local Text immutableTextValue;
local MutableText mutableTextValue;
parser = _.text.Parse(source);
parsingSuccessful =
parser.MStringLiteral(mutableTextValue).Ok() && parser.HasFinished();
parser.FreeSelf();
if (!parsingSuccessful) {
mutableTextValue.FreeSelf();
return none;
}
if (parseAsMutable) {
return mutableTextValue;
}
immutableTextValue = mutableTextValue.Copy();
mutableTextValue.FreeSelf();
return immutableTextValue;
}
/**
* Uses given parser to parse a JSON array.
*
* This method will parse JSON values that are contained in parsed JSON array
* according to description given for `ParseWith()` method.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON array (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON array
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to parse array's
* items as mutable values and `false` otherwise (as immutable ones).
* @return Parsed JSON array as `DynamicArray` if parsing was successful and
* `none` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public final function DynamicArray ParseArrayWith(
Parser parser,
optional bool parseAsMutable)
{
local bool parsingSucceeded;
local Parser.ParserState confirmedState;
local AcediaObject nextValue;
local array<AcediaObject> parsedValues;
if (parser == none) return none;
confirmedState =
parser.Skip().Match(T(default.TOPEN_BRACKET)).GetCurrentState();
while (parser.Ok() && !parser.HasFinished())
{
confirmedState = parser.Skip().GetCurrentState();
// Check for JSON array ending and ONLY THEN declare parsing
// is successful, not encountering '}' implies bad JSON format.
if (parser.Match(T(default.TCLOSE_BRACKET)).Ok()) {
parsingSucceeded = true;
break;
}
parser.RestoreState(confirmedState);
// Look for comma after each element
if (parsedValues.length > 0)
{
if (!parser.Match(T(default.TCOMMA)).Skip().Ok()) {
break;
}
confirmedState = parser.GetCurrentState();
}
// Parse next value
nextValue = ParseWith(parser, parseAsMutable);
parsedValues[parsedValues.length] = nextValue;
if (!parser.Ok()) {
break;
}
}
if (parsingSucceeded) {
return _.collections.NewDynamicArray(parsedValues, true);
}
_.memory.FreeMany(parsedValues);
parser.Fail();
return none;
}
/**
* Uses given parser to parse a JSON object.
*
* This method will parse JSON values that are contained in parsed JSON object
* according to description given for `ParseWith()` method.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON object (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON object
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to parse object's
* items as mutable values and `false` otherwise (as immutable ones).
* @return Parsed JSON object as `AssociativeArray` if parsing was successful
* and `none` otherwise. To check for parsing success check the state of
* the `parser`.
*/
public function AssociativeArray ParseObjectWith(
Parser parser,
optional bool parseAsMutable)
{
local bool parsingSucceeded;
local Parser.ParserState confirmedState;
local array<AssociativeArray.Entry> parsedEntries;
if (parser == none) return none;
// Ensure that parser starts pointing at what looks like a JSON object
confirmedState =
parser.Skip().Match(T(default.TOPEN_BRACE)).GetCurrentState();
if (!parser.Ok()) {
return none;
}
while (parser.Ok() && !parser.HasFinished())
{
confirmedState = parser.Skip().GetCurrentState();
// Check for JSON object ending and ONLY THEN declare parsing
// is successful, not encountering '}' implies bad JSON format.
if (parser.Match(T(default.TCLOSE_BRACE)).Ok())
{
parsingSucceeded = true;
break;
}
parser.RestoreState(confirmedState);
// Look for comma after each key-value pair
if (parsedEntries.length > 0)
{
if (!parser.Match(T(default.TCOMMA)).Skip().Ok()) {
break;
}
confirmedState = parser.GetCurrentState();
}
// Parse property
parsedEntries[parsedEntries.length] =
ParseProperty(parser, parseAsMutable);
if (!parser.Ok()) {
break;
}
}
if (parsingSucceeded) {
return _.collections.NewAssociativeArray(parsedEntries, true);
}
FreeEntries(parsedEntries);
parser.Fail();
return none;
}
// Parses a JSON key-value pair (there must not be any leading spaces).
private function AssociativeArray.Entry ParseProperty(
Parser parser,
bool parseAsMutable)
{
local MutableText nextKey;
local AssociativeArray.Entry entry;
parser.MStringLiteral(nextKey).Skip().Match(T(default.TCOLON)).Skip();
entry.key = nextKey.Copy();
nextKey.FreeSelf();
entry.value = ParseWith(parser, parseAsMutable);
return entry;
}
// Auxiliary method for deallocating unneeded objects in entry pairs.
private function FreeEntries(array<AssociativeArray.Entry> entries)
{
local int i;
for (i = 0; i < entries.length; i += 1)
{
_.memory.Free(entries[i].key);
_.memory.Free(entries[i].value);
}
}
/**
* Uses given parser to parse a JSON value.
*
* This method will parse JSON values that are contained in parsed JSON object
* according to description given for `ParseWith()` method.
*
* Rules for determining types into which JSON value will be parsed:
* 1. Null values will be returned as `none`;
* 2. Number values will be return as an `IntBox`/`IntRef` if they consist
* of only digits (and optionally a sign) and `FloatBox`/`FloatRef`
* otherwise. Choice between box and ref is made based on
* `parseAsMutable` parameter (boxes are immutable, refs are mutable);
* 3. String values will be parsed as `Text`/`MutableText`, based on
* `parseAsMutable` parameter;
* 4. Array values will be parsed as a `DynamicArray`, it's items parsed
* according to these rules (`parseAsMutable` parameter is propagated).
* 5. Object values will be parsed as a `AssociativeArray`, it's items
* parsed according to these rules (`parseAsMutable` parameter is
* propagated) and recorded under the keys parsed into `Text`.
*
* It does not matter what content follows parsed value in the `parser`,
* method will be successful as long as it manages to parse correct
* JSON value (from the current `parser`'s position).
*
* To check whether parsing have failed, simply check if `parser` is in
* a failed state after the method call.
*
* @param parser Parser that method would use to parse JSON value
* from it's current position. It's confirmed state will not be changed.
* If parsing was successful it will point at the next available character.
* Parser will be in a failed state after this method iff
* parsing has failed.
* @param parseAsMutable `true` if you want this method to parse value
* (and it's sub-items, if applicable) as mutable values and
* `false` if you want them to be immutable.
* @return Parsed JSON value as `AcediaObject` that has one of the classes
* described in parsing rules, `none` otherwise. To check for parsing
* success check the state of the `parser`. Note that method can also
* return `none` if parsed JSON value was "null".
*/
public final function AcediaObject ParseWith(
Parser parser,
optional bool parseAsMutable)
{
local AcediaObject result;
local Parser.ParserState initState;
if (parser == none) return none;
if (!parser.Ok()) return none;
initState = parser.GetCurrentState();
TryNullWith(parser);
if (parser.Ok()) {
return none;
}
result = ParseBooleanWith(parser.RestoreState(initState), parseAsMutable);
if (parser.Ok()) {
return result;
}
result = ParseNumberWith(parser.RestoreState(initState), parseAsMutable);
if (parser.Ok()) {
return result;
}
result = ParseStringWith(parser.RestoreState(initState), parseAsMutable);
if (parser.Ok()) {
return result;
}
result = ParseArrayWith(parser.RestoreState(initState), parseAsMutable);
if (parser.Ok()) {
return result;
}
result = ParseObjectWith(parser.RestoreState(initState), parseAsMutable);
if (parser.Ok()) {
return result;
}
return none;
}
/**
* "Prints" given `AcediaObject` value, saving it in JSON format.
*
* "Prints" given `AcediaObject` in a minimal way, for a human-readable output
* use `PrettyPrint()` method.
*
* Only certain classes (the same as the ones that can be parsed from JSON
* via this API) are supported:
* 1. `none` is printed into "null";
* 2. Boolean types (`BoolBox`/`BoolRef`) are printed into JSON bool value;
* 3. Integer (`IntBox`/`IntRef`) and float (`FloatBox`/`FloatRef`) types
* are printed into JSON number value;
* 4. `Text` and `MutableText` are printed into JSON string value;
* 5. `DynamicArray` is printed into JSON array with `Print()` method
* applied to each of it's items. If some of them have not printable
* types - "none" will be used as a fallback.
* 6. `AssociativeArray` is printed into JSON object with `Print()` method
* applied to each of it's items. Only items with `Text` keys are
* printed, the rest is omitted. If some of them have not printable
* types - "none" will be used as a fallback.
*
* @param toPrint Object to "print" into `MutableText`.
* @return Text version of given `toDisplay`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText Print(AcediaObject toPrint)
{
if (toPrint == none) {
return T(default.TNULL).MutableCopy();
}
if (toPrint.class == class'IntBox') {
return _.text.FromIntM(IntBox(toPrint).Get());
}
if (toPrint.class == class'IntRef') {
return _.text.FromIntM(IntRef(toPrint).Get());
}
if (toPrint.class == class'BoolBox') {
return _.text.FromBoolM(BoolBox(toPrint).Get());
}
if (toPrint.class == class'BoolRef') {
return _.text.FromBoolM(BoolRef(toPrint).Get());
}
if (toPrint.class == class'FloatBox') {
return _.text.FromFloatM(FloatBox(toPrint).Get(), MAX_FLOAT_PRECISION);
}
if (toPrint.class == class'FloatRef') {
return _.text.FromFloatM(FloatRef(toPrint).Get(), MAX_FLOAT_PRECISION);
}
if ( toPrint.class == class'Text'
|| toPrint.class == class'MutableText') {
return DisplayText(Text(toPrint));
}
if (toPrint.class == class'DynamicArray') {
return PrintArray(DynamicArray(toPrint));
}
if (toPrint.class == class'AssociativeArray') {
return PrintObject(AssociativeArray(toPrint));
}
return none;
}
/**
* "Prints" given `DynamicArray` value, saving it as a JSON array in
* `MutableText`.
*
* "Prints" given `DynamicArray` in a minimal way, for a human-readable output
* use `PrettyPrintArray()` method.
*
* It's items must either be equal to `none` or have one of the following
* classes: `BoolBox`, `BoolRef`, `IntBox`, `IntRef`, `FloatBox`, `FloatRef`,
* `Text`, `MutableText`, `DynamicArray`, `AssociativeArray`.
* Otherwise items will be printed as "null" values.
* Also see `Print()` method.
*
* @param toPrint Array to "print" into `MutableText`.
* @return Text version of given `toPrint`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText PrintArray(DynamicArray toPrint)
{
local int i, length;
local MutableText result, printedItem;
if (toPrint == none) return none;
length = toPrint.GetLength();
result = T(default.TOPEN_BRACKET).MutableCopy();
for (i = 0; i < length; i += 1)
{
if (i > 0) {
result.Append(T(default.TCOMMA));
}
printedItem = Print(toPrint.GetItem(i));
if (printedItem != none)
{
result.Append(printedItem);
printedItem.FreeSelf();
}
else {
result.Append(T(default.TNULL));
}
}
result.Append(T(default.TCLOSE_BRACKET));
return result;
}
/**
* "Prints" given `AssociativeArray` value, saving it as a JSON object in
* `MutableText`.
*
* "Prints" given `AssociativeArray` in a minimal way, for
* a human-readable output use `PrettyPrintObject()` method.
*
* Only prints items recorded with `Text` key, the rest is omitted.
*
* It's items must either be equal to `none` or have one of the following
* classes: `BoolBox`, `BoolRef`, `IntBox`, `IntRef`, `FloatBox`, `FloatRef`,
* `Text`, `MutableText`, `DynamicArray`, `AssociativeArray`.
* Otherwise items will be printed as "null" values.
* Also see `Print()` method.
*
* @param toPrint Array to "print" into `MutableText`.
* @return Text version of given `toPrint`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText PrintObject(AssociativeArray toPrint)
{
local bool printedKeyValuePair;
local Iter iter;
local Text nextKey;
local AcediaObject nextValue;
local MutableText result, printedKey, printedValue;
if (toPrint == none) return none;
result = T(default.TOPEN_BRACE).MutableCopy();
iter = toPrint.Iterate();
for (iter = toPrint.Iterate(); !iter.HasFinished(); iter.Next())
{
if (printedKeyValuePair) {
result.Append(T(default.TCOMMA));
}
nextKey = Text(iter.GetKey());
nextValue = iter.Get();
if (nextKey == none) continue;
if (nextKey.class != class'Text') continue;
printedKey = DisplayText(nextKey);
printedValue = Print(nextValue);
result.Append(printedKey).Append(T(default.TCOLON));
printedKey.FreeSelf();
if (printedValue != none)
{
result.Append(printedValue);
printedValue.FreeSelf();
}
else {
result.Append(T(default.TNULL));
}
printedKeyValuePair = true;
}
iter.FreeSelf();
result.Append(T(default.TCLOSE_BRACE));
return result;
}
/**
* "Prints" given `AcediaObject` value, saving it in JSON format.
*
* "Prints" given `AcediaObject` in a human-readable way. For a minimal output
* use `Print()` method.
*
* Only certain classes (the same as the ones that can be parsed from JSON
* via this API) are supported:
* 1. `none` is printed into "null";
* 2. Boolean types (`BoolBox`/`BoolRef`) are printed into JSON bool value;
* 3. Integer (`IntBox`/`IntRef`) and float (`FloatBox`/`FloatRef`) types
* are printed into JSON number value;
* 4. `Text` and `MutableText` are printed into JSON string value;
* 5. `DynamicArray` is printed into JSON array with `Print()` method
* applied to each of it's items. If some of them have not printable
* types - "none" will be used as a fallback.
* 6. `AssociativeArray` is printed into JSON object with `Print()` method
* applied to each of it's items. Only items with `Text` keys are
* printed, the rest is omitted. If some of them have not printable
* types - "none" will be used as a fallback.
*
* @param toPrint Object to "print" into `MutableText`.
* @return Text version of given `toDisplay`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText PrettyPrint(AcediaObject toPrint)
{
local MutableText result;
local MutableText accumulatedIndent;
InitFormatting();
accumulatedIndent = _.text.Empty();
result = PrettyPrintWithIndent(toPrint, accumulatedIndent);
accumulatedIndent.FreeSelf();
return result;
}
/**
* "Prints" given `DynamicArray` value, saving it as a JSON array in
* `MutableText`.
*
* "Prints" given `DynamicArray` in human-readable way, for minimal output
* use `PrintArray()` method.
*
* It's items must either be equal to `none` or have one of the following
* classes: `BoolBox`, `BoolRef`, `IntBox`, `IntRef`, `FloatBox`, `FloatRef`,
* `Text`, `MutableText`, `DynamicArray`, `AssociativeArray`.
* Otherwise items will be printed as "null" values.
* Also see `Print()` method.
*
* @param toPrint Array to "print" into `MutableText`.
* @return Text version of given `toPrint`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText PrettyPrintArray(DynamicArray toPrint)
{
local MutableText result;
local MutableText accumulatedIndent;
InitFormatting();
accumulatedIndent = _.text.Empty();
result = PrettyPrintArrayWithIndent(toPrint, accumulatedIndent);
accumulatedIndent.FreeSelf();
return result;
}
/**
* "Prints" given `AssociativeArray` value, saving it as a JSON object in
* `MutableText`.
*
* "Prints" given `AssociativeArray` in a human readable way, for
* a minimal output use `PrintObject()` method.
*
* Only prints items recorded with `Text` key, the rest is omitted.
*
* It's items must either be equal to `none` or have one of the following
* classes: `BoolBox`, `BoolRef`, `IntBox`, `IntRef`, `FloatBox`, `FloatRef`,
* `Text`, `MutableText`, `DynamicArray`, `AssociativeArray`.
* Otherwise items will be printed as "null" values.
* Also see `Print()` method.
*
* @param toPrint Array to "print" into `MutableText`.
* @return Text version of given `toPrint`, if it has one of the printable
* classes. Otherwise returns `none`.
* Note that `none` is considered printable and will produce "null".
*/
public final function MutableText PrettyPrintObject(AssociativeArray toPrint)
{
local MutableText result;
local MutableText accumulatedIndent;
InitFormatting();
accumulatedIndent = _.text.Empty();
result = PrettyPrintObjectWithIndent(toPrint, accumulatedIndent);
accumulatedIndent.FreeSelf();
return result;
}
// Does the actual job for `PrettyPrint()` method.
// Separated to hide `accumulatedIndent` parameter that is necessary for
// pretty printing.
// Assumes `InitFormatting()` was made and json formatting variables are
// initialized.
private final function MutableText PrettyPrintWithIndent(
AcediaObject toPrint,
optional MutableText accumulatedIndent)
{
if (toPrint == none) {
return T(default.TNULL).MutableCopy().ChangeFormatting(jNull);
}
if (toPrint.class == class'IntBox') {
return _.text.FromIntM(IntBox(toPrint).Get()).ChangeFormatting(jNumber);
}
if (toPrint.class == class'IntRef') {
return _.text.FromIntM(IntRef(toPrint).Get()).ChangeFormatting(jNumber);
}
if (toPrint.class == class'BoolBox')
{
return _.text.FromBoolM(BoolBox(toPrint).Get())
.ChangeFormatting(jBoolean);
}
if (toPrint.class == class'BoolRef')
{
return _.text.FromBoolM(BoolRef(toPrint).Get())
.ChangeFormatting(jBoolean);
}
if (toPrint.class == class'FloatBox')
{
return _.text.FromFloatM(FloatBox(toPrint).Get(), MAX_FLOAT_PRECISION)
.ChangeFormatting(jNumber);
}
if (toPrint.class == class'FloatRef')
{
return _.text.FromFloatM(FloatRef(toPrint).Get(), MAX_FLOAT_PRECISION)
.ChangeFormatting(jNumber);
}
if ( toPrint.class == class'Text'
|| toPrint.class == class'MutableText')
{
return DisplayText(Text(toPrint)).ChangeFormatting(jString);
}
if (toPrint.class == class'DynamicArray')
{
return PrettyPrintArrayWithIndent( DynamicArray(toPrint),
accumulatedIndent);
}
if (toPrint.class == class'AssociativeArray') {
return PrettyPrintObjectWithIndent( AssociativeArray(toPrint),
accumulatedIndent);
}
return none;
}
// Does the actual job for `PrettyPrintArray()` method.
// Separated to hide `accumulatedIndent` parameter that is necessary for
// pretty printing.
// Assumes `InitFormatting()` was made and json formatting variables are
// initialized.
private final function MutableText PrettyPrintArrayWithIndent(
DynamicArray toPrint,
MutableText accumulatedIndent)
{
local int i, length;
local MutableText extendedIndent;
local MutableText result, printedItem;
if (toPrint == none) {
return none;
}
length = toPrint.GetLength();
extendedIndent = accumulatedIndent.MutableCopy().Append(T(TJSON_INDENT));
result = T(default.TOPEN_BRACKET).MutableCopy()
.ChangeFormatting(jArrayBraces);
for (i = 0; i < length; i += 1)
{
if (i > 0) {
result.Append(T(default.TCOMMA), jComma);
}
printedItem = PrettyPrintWithIndent(toPrint.GetItem(i), extendedIndent);
if (printedItem != none)
{
result.AppendLineBreak().Append(extendedIndent).Append(printedItem);
printedItem.FreeSelf();
}
else {
result.Append(T(default.TNULL), jNull);
}
}
if (i > 0) {
result.AppendLineBreak().Append(accumulatedIndent);
}
result.Append(T(default.TCLOSE_BRACKET), jArrayBraces);
extendedIndent.FreeSelf();
return result;
}
// Does the actual job for `PrettyPrintObject()` method.
// Separated to hide `accumulatedIndent` parameter that is necessary for
// pretty printing.
// Assumes `InitFormatting()` was made and json formatting variables are
// initialized.
private final function MutableText PrettyPrintObjectWithIndent(
AssociativeArray toPrint,
MutableText accumulatedIndent)
{
local bool printedKeyValuePair;
local Iter iter;
local Text nextKey;
local AcediaObject nextValue;
local MutableText extendedIndent;
local MutableText result;
if (toPrint == none) {
return none;
}
extendedIndent = accumulatedIndent.MutableCopy().Append(T(TJSON_INDENT));
result = T(default.TOPEN_BRACE).MutableCopy()
.ChangeFormatting(jObjectBraces);
iter = toPrint.Iterate();
for (iter = toPrint.Iterate(); !iter.HasFinished(); iter.Next())
{
if (printedKeyValuePair) {
result.Append(T(default.TCOMMA), jComma);
}
nextKey = Text(iter.GetKey());
nextValue = iter.Get();
if (nextKey == none) continue;
if (nextKey.class != class'Text') continue;
PrettyPrintKeyValue(result, nextKey, nextValue, extendedIndent);
printedKeyValuePair = true;
}
if (printedKeyValuePair) {
result.AppendLineBreak().Append(accumulatedIndent);
}
iter.FreeSelf();
result.Append(T(default.TCLOSE_BRACE), jObjectBraces);
extendedIndent.FreeSelf();
return result;
}
// Auxiliary method for printing key-value pair into the `builder`.
// `accumulatedIndent` is necessary in case passed `value` is
// an object or array.
// Assumes `InitFormatting()` was made and json formatting variables are
// initialized.
private final function PrettyPrintKeyValue(
MutableText builder,
Text nextKey,
AcediaObject nextValue,
MutableText accumulatedIndent)
{
local MutableText printedKey, printedValue;
printedKey = DisplayText(nextKey).ChangeFormatting(jPropertyName);
printedValue = PrettyPrintWithIndent(nextValue, accumulatedIndent);
builder.AppendLineBreak()
.Append(accumulatedIndent)
.Append(printedKey)
.Append(T(default.TCOLON_SPACE), jColon);
printedKey.FreeSelf();
if (printedValue != none)
{
builder.Append(printedValue);
printedValue.FreeSelf();
}
else {
builder.Append(T(default.TNULL), jNull);
}
}
// Auxiliary method to convert `Text` into it's JSON "string"
// representation.
// We can't just dump `original`'s contents into JSON output as is,
// since we have to replace several special characters with escaped sequences.
private final function MutableText DisplayText(Text original)
{
local int i, length;
local MutableText result;
local Text.Character nextCharacter;
local Text.Character reverseSolidus;
reverseSolidus = _.text.CharacterFromCodePoint(CODEPOINT_REVERSE_SOLIDUS);
result = T(TQUOTE).MutableCopy();
length = original.GetLength();
for (i = 0; i < length; i += 1)
{
nextCharacter = original.GetCharacter(i);
if (DoesNeedEscaping(nextCharacter.codePoint))
{
result.AppendCharacter(reverseSolidus);
nextCharacter.codePoint =
GetEscapedVersion(nextCharacter.codePoint);
result.AppendCharacter(nextCharacter);
}
else {
result.AppendCharacter(nextCharacter);
}
}
result.Append(T(TQUOTE));
return result;
}
// Checks whether a certain character (code point) needs to be replaced for
// JSON printing.
private final function bool DoesNeedEscaping(int codePoint)
{
if (codePoint == CODEPOINT_REVERSE_SOLIDUS) return true;
if (codePoint == CODEPOINT_CARRIAGE_RETURN) return true;
if (codePoint == CODEPOINT_QUOTATION_MARK) return true;
if (codePoint == CODEPOINT_BACKSPACE) return true;
if (codePoint == CODEPOINT_FORM_FEED) return true;
if (codePoint == CODEPOINT_LINE_FEED) return true;
if (codePoint == CODEPOINT_SOLIDUS) return true;
if (codePoint == CODEPOINT_TAB) return true;
return false;
}
// Replaces code point with it's escaped letter.
// When printing text into JSON some characters need to be escaped
// (see `DoesNeedEscaping()`), but while some can use themselves in escaped
// sequence ("\""), some need to be replaced with a different character ("\n").
private final function int GetEscapedVersion(int codePoint)
{
if (codePoint == CODEPOINT_BACKSPACE) {
return CODEPOINT_SMALL_B;
}
else if (codePoint == CODEPOINT_FORM_FEED) {
return CODEPOINT_SMALL_F;
}
else if (codePoint == CODEPOINT_LINE_FEED) {
return CODEPOINT_SMALL_N;
}
else if (codePoint == CODEPOINT_CARRIAGE_RETURN) {
return CODEPOINT_SMALL_R;
}
else if (codePoint == CODEPOINT_TAB) {
return CODEPOINT_SMALL_T;
}
return codePoint;
}
defaultproperties
{
MAX_FLOAT_PRECISION = 4
TNULL = 0
stringConstants(0) = "null"
TTRUE = 1
stringConstants(1) = "true"
TFALSE = 2
stringConstants(2) = "false"
TDOT = 3
stringConstants(3) = "."
TEXPONENT = 4
stringConstants(4) = "e"
TOPEN_BRACKET = 5
stringConstants(5) = "["
TCLOSE_BRACKET = 6
stringConstants(6) = "]"
TOPEN_BRACE = 7
stringConstants(7) = "&{"
TCLOSE_BRACE = 8
stringConstants(8) = "&}"
TCOMMA = 9
stringConstants(9) = ","
TCOLON = 10
stringConstants(10) = ":"
TQUOTE = 11
stringConstants(11) = "\""
TJSON_INDENT = 12
stringConstants(12) = " "
TCOLON_SPACE = 13
stringConstants(13) = ": "
CODEPOINT_BACKSPACE = 8
CODEPOINT_TAB = 9
CODEPOINT_LINE_FEED = 10
CODEPOINT_FORM_FEED = 12
CODEPOINT_CARRIAGE_RETURN = 13
CODEPOINT_QUOTATION_MARK = 34
CODEPOINT_SOLIDUS = 47
CODEPOINT_REVERSE_SOLIDUS = 92
CODEPOINT_SMALL_B = 98
CODEPOINT_SMALL_F = 102
CODEPOINT_SMALL_N = 110
CODEPOINT_SMALL_R = 114
CODEPOINT_SMALL_T = 116
}