/** * 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 . */ 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 BaseText.Formatting jPropertyName, jObjectBraces, jArrayBraces; var private BaseText.Formatting jComma, jColon, jNumber, jBoolean, jString; var private BaseText.Formatting 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(optional BaseText 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 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(BaseText 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( BaseText 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( BaseText 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 BaseText 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 BaseText ParseString( BaseText 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 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 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 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, BaseText 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(BaseText original) { local int i, length; local MutableText result; local BaseText.Character nextCharacter; local BaseText.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 }