Browse Source

Refactor `JsonPointer` into separate (im)mutable versions

pull/10/head
Anton Tarasenko 2 years ago
parent
commit
6321368665
  1. 24
      sources/Data/Collections/Collection.uc
  2. 65
      sources/Data/Database/Connection/DBCache.uc
  3. 33
      sources/Data/Database/Connection/DBConnection.uc
  4. 35
      sources/Data/Database/DBAPI.uc
  5. 14
      sources/Data/Database/Database.uc
  6. 37
      sources/Data/Database/Local/DBRecord.uc
  7. 20
      sources/Data/Database/Local/LocalDatabaseInstance.uc
  8. 2
      sources/Text/BaseText.uc
  9. 389
      sources/Text/JSON/BaseJsonPointer.uc
  10. 32
      sources/Text/JSON/JSONAPI.uc
  11. 532
      sources/Text/JSON/JSONPointer.uc
  12. 207
      sources/Text/JSON/MutableJsonPointer.uc
  13. 7
      sources/Text/MutableText.uc
  14. 107
      sources/Text/Tests/TEST_JSON.uc
  15. 60
      sources/Users/PersistentData/PersistentDataManager.uc
  16. 10
      sources/Users/Users_Feature.uc

24
sources/Data/Collections/Collection.uc

@ -97,7 +97,7 @@ public function Empty() {}
/**
* Returns stored `AcediaObject` from the caller storage
* (or from it's sub-storages) via given `JSONPointer` path.
* (or from it's sub-storages) via given `BaseJSONPointer` path.
*
* Acedia provides two collections:
* 1. `ArrayList` is treated as a JSON array in the context of
@ -119,7 +119,7 @@ public function Empty() {}
* @return An item `jsonPointer` is referring to (according to the above
* stated rules). `none` if such item does not exist.
*/
public final function AcediaObject GetItemByJSON(JSONPointer jsonPointer)
public final function AcediaObject GetItemByJSON(BaseJSONPointer jsonPointer)
{
local int segmentIndex;
local Text nextSegment;
@ -648,7 +648,7 @@ public final function ArrayList GetArrayListBy(BaseText jsonPointerAsText)
* is missing or has a different type.
*/
public final function bool GetBoolByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional bool defaultValue)
{
local bool result;
@ -689,7 +689,7 @@ public final function bool GetBoolByJSON(
* is missing or has a different type.
*/
public final function byte GetByteByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional byte defaultValue)
{
local byte result;
@ -734,7 +734,7 @@ public final function byte GetByteByJSON(
* is missing or has a different type.
*/
public final function int GetIntByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional int defaultValue)
{
local int result;
@ -789,7 +789,7 @@ public final function int GetIntByJSON(
* is missing or has a different type.
*/
public final function float GetFloatByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional float defaultValue)
{
local float result;
@ -840,7 +840,7 @@ public final function float GetFloatByJSON(
* is missing or has a different type.
*/
public final function Vector GetVectorByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional Vector defaultValue)
{
local Vector result;
@ -881,7 +881,7 @@ public final function Vector GetVectorByJSON(
* if it is missing or has a different type.
*/
public final function string GetStringByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional string defaultValue)
{
local AcediaObject result;
@ -915,7 +915,7 @@ public final function string GetStringByJSON(
* `defaultValue` if it is missing or has a different type.
*/
public final function string GetFormattedStringByJSON(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
optional string defaultValue)
{
local AcediaObject result;
@ -972,7 +972,7 @@ public final function Text GetTextByJSON(JSONPointer jsonPointer)
* `none` if it is missing or has a different type.
*/
public final function Collection GetCollectionByJSON(
JSONPointer jsonPointer)
BaseJSONPointer jsonPointer)
{
local AcediaObject result;
local Collection asCollection;
@ -999,7 +999,7 @@ public final function Collection GetCollectionByJSON(
* `none` if it is missing or has a different type.
*/
public final function HashTable GetHashTableByJSON(
JSONPointer jsonPointer)
BaseJSONPointer jsonPointer)
{
local AcediaObject result;
local HashTable asHashTable;
@ -1026,7 +1026,7 @@ public final function HashTable GetHashTableByJSON(
* `none` if it is missing or has a different type.
*/
public final function ArrayList GetArrayListByJSON(
JSONPointer jsonPointer)
BaseJSONPointer jsonPointer)
{
local AcediaObject result;
local ArrayList asArrayList;

65
sources/Data/Database/Connection/DBCache.uc

@ -32,8 +32,8 @@ class DBCache extends AcediaObject;
*
* ## Usage
*
* You can simply read and write JSON data with `Read(JSONPointer)` and
* `Write(JSONPointer, AcediaObject)` right after `DBCache`'s creation.
* You can simply read and write JSON data with `Read(BaseJSONPointer)` and
* `Write(BaseJSONPointer, AcediaObject)` right after `DBCache`'s creation.
* Once real database's data has arrived, you can set it with `SetRealData()`.
* Data recorded before the `SetRealData()` call is an *approximation* and
* might not function as a real JSON value/database. Because `DBCache` doesn't
@ -47,7 +47,7 @@ class DBCache extends AcediaObject;
*
* ```unrealscript
* local DBCache cache;
* local JSONPointer dataLocation,;
* local JSONPointer dataLocation;
* local HashTable emptyObject;
*
* cache = DBCache(_.memory.Allocate(class'DBCache'));
@ -214,7 +214,7 @@ protected function Finalizer()
* @return Data stored at location given by `location`, `none` if nothing is
* stored there.
*/
public final function AcediaObject Read(JSONPointer location)
public final function AcediaObject Read(BaseJSONPointer location)
{
local Collection cachedCollection;
@ -262,7 +262,7 @@ public final function AcediaObject Read(JSONPointer location)
* but then rejected after the real data is set if they're incompatible
* with its structure (@see `SetRealData()` for more information).
*/
public final function bool Write(JSONPointer location, AcediaObject data)
public final function bool Write(BaseJSONPointer location, AcediaObject data)
{
local Collection cachedCollection;
@ -317,7 +317,7 @@ public final function bool Write(JSONPointer location, AcediaObject data)
* but then rejected after the real data is set if they're incompatible
* with its structure (@see `SetRealData()` for more information).
*/
public final function bool Remove(JSONPointer location)
public final function bool Remove(BaseJSONPointer location)
{
local Collection cachedCollection;
@ -377,7 +377,7 @@ public final function bool Remove(JSONPointer location)
* but then rejected after the real data is set if they're incompatible
* with its structure (@see `SetRealData()` for more information).
*/
public final function bool Increment(JSONPointer location, AcediaObject data)
public final function bool Increment(BaseJSONPointer location, AcediaObject data)
{
local AcediaObject incrementedRoot;
local Collection cachedCollection;
@ -405,14 +405,7 @@ public final function bool Increment(JSONPointer location, AcediaObject data)
}
return false;
}
/*INC 2
Cache inc 1 {} /test True
Cache inc 2
Cache inc 3
Cache inc 4
INC 5
WriteDataByJSON #1
WriteDataByJSON #2 */
/**
* Checks whether `SetRealData()` was called.
*
@ -494,7 +487,7 @@ public final function array<PendingEdit> SetRealData(AcediaObject realData)
}
// For reading data when before the `SetRealData()` call.
private final function AcediaObject ReadPending(JSONPointer location)
private final function AcediaObject ReadPending(BaseJSONPointer location)
{
local int nextEditIndex;
local int newestOverridingEdit;
@ -534,8 +527,8 @@ private final function AcediaObject ReadPending(JSONPointer location)
// Assumes `location` is not `none`.
// Assumes `pendingEdits[editIndex].location` is prefix of `location`.
private final function AcediaObject ReconstructFromEdit(
JSONPointer location,
int editIndex)
BaseJSONPointer location,
int editIndex)
{
local int startIndex;
local AcediaObject result;
@ -584,12 +577,12 @@ private final function AcediaObject ReconstructFromEdit(
// Assumes `locationToCorrect` in not `none`.
private final function ApplyCorrectingWrites(
out AcediaObject target,
JSONPointer locationToCorrect,
BaseJSONPointer locationToCorrect,
int startIndex)
{
local int i;
local Collection targetAsCollection;
local JSONPointer subLocation, nextLocation;;
local JSONPointer subLocation, nextLocation;
if (target == none) {
return;
@ -660,17 +653,17 @@ private final function bool EditJSONSimpleValue(
// Assumes that `target` isn't `none`.
private final function bool EditJSONCollection(
Collection target,
JSONPointer location,
BaseJSONPointer location,
AcediaObject value,
DBCacheEditType editType)
{
local bool success;
local Text key;
local ArrayList arrayCollection;
local HashTable objectCollection;
local Collection innerCollection;
local JSONPointer poppedLocation;
local AcediaObject valueCopy;
local bool success;
local Text key;
local ArrayList arrayCollection;
local HashTable objectCollection;
local Collection innerCollection;
local MutableJSONPointer poppedLocation;
local AcediaObject valueCopy;
// Empty pointer is only allowed if we're incrementing;
if (location.IsEmpty())
@ -682,7 +675,7 @@ private final function bool EditJSONCollection(
// (which is data pointed by `location` without the last segment).
// Last segment will serve as a key in that `Collection`, so also
// keep it.
poppedLocation = location.Copy();
poppedLocation = location.MutableCopy();
key = poppedLocation.Pop();
innerCollection = target.GetCollectionByJSON(poppedLocation);
// Then, depending on the collection, get the actual data
@ -696,7 +689,7 @@ private final function bool EditJSONCollection(
key,
value,
editType,
location.PopNumeric(true));
location.PeekNumeric());
}
if (objectCollection != none) {
success = EditHashTable(objectCollection, key, value, editType);
@ -887,7 +880,7 @@ private final function bool IncrementCollection(
// For writing data when before the `SetRealData()` call.
// Assumes `location` isn't `none`.
private final function bool AddPendingEdit(
JSONPointer location,
BaseJSONPointer location,
AcediaObject data,
DBCacheEditType type)
{
@ -983,7 +976,7 @@ private final function bool AddPendingEdit(
// Checks if `pointer`'s last component is "-", which denotes appending
// new item to the JSON array.
// Assumes `pointer != none`.
private final function bool IsPointerAppendingToArray(JSONPointer pointer)
private final function bool IsPointerAppendingToArray(BaseJSONPointer pointer)
{
local int lastComponentIndex;
@ -998,9 +991,9 @@ private final function bool IsPointerAppendingToArray(JSONPointer pointer)
// To avoid unnecessary copying the key is specified as a component of
// JSON pointer `path` with index `keyIndex`.
private final function bool IsKeyAcceptable(
Collection target,
JSONPointer path,
int keyIndex)
Collection target,
BaseJSONPointer path,
int keyIndex)
{
local ArrayList arrayCollection;
@ -1022,7 +1015,7 @@ private final function bool IsKeyAcceptable(
// `from` will contain index `2` of the component "c".
private final function AcediaObject ApplyPointer(
AcediaObject data,
JSONPointer pointer,
BaseJSONPointer pointer,
out int from,
int to)
{

33
sources/Data/Database/Connection/DBConnection.uc

@ -277,8 +277,8 @@ protected function Finalizer()
* @return `true` if initialization was successful and `false` otherwise.
*/
public final function bool Initialize(
Database initDatabase,
optional JSONPointer initRootPointer)
Database initDatabase,
optional BaseJSONPointer initRootPointer)
{
if (IsInitialized()) return false;
if (initDatabase == none) return false;
@ -310,7 +310,7 @@ public final function bool Initialize(
* @param pointer Location from which to read the data.
* @return Data recorded for the given `JSONPointer`. `none` if it is missing.
*/
public final function AcediaObject ReadDataByJSON(JSONPointer pointer)
public final function AcediaObject ReadDataByJSON(BaseJSONPointer pointer)
{
return localCache.Read(pointer);
}
@ -342,7 +342,7 @@ public final function AcediaObject ReadDataByJSON(JSONPointer pointer)
* the writing database request to be made.
*/
public final function bool WriteDataByJSON(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
@ -383,7 +383,7 @@ public final function bool WriteDataByJSON(
* the incrementing database request to be made.
*/
public final function bool IncrementDataByJSON(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
@ -420,7 +420,7 @@ public final function bool IncrementDataByJSON(
* @return `true` on success and `false` on failure. `true` is required for
* the removal database request to be made.
*/
public final function bool RemoveDataByJSON(JSONPointer pointer)
public final function bool RemoveDataByJSON(BaseJSONPointer pointer)
{
if (pointer == none) {
return false;
@ -434,16 +434,16 @@ public final function bool RemoveDataByJSON(JSONPointer pointer)
}
private final function ModifyDataInDatabase(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject data,
bool increment)
{
local JSONPointer dataPointer;
local MutableJSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer = rootPointer.MutableCopy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
if (increment)
@ -452,30 +452,33 @@ private final function ModifyDataInDatabase(
.IncrementData(
dataPointer,
data,
RegisterNextRequestID(dataPointer))
RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
else
{
dbInstance
.WriteData(dataPointer, data, RegisterNextRequestID(dataPointer))
.WriteData(dataPointer, data, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
}
private final function RemoveDataInDatabase(JSONPointer pointer)
private final function RemoveDataInDatabase(BaseJSONPointer pointer)
{
local JSONPointer dataPointer;
local MutableJSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.Copy();
dataPointer = rootPointer.MutableCopy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
dbInstance
.RemoveData(dataPointer, RegisterNextRequestID(dataPointer))
.RemoveData(dataPointer, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
/**

35
sources/Data/Database/DBAPI.uc

@ -76,6 +76,41 @@ public final function Database Load(BaseText databaseLink)
return result;
}
/**
* Extracts `MutableJSONPointer` from the database path, given by `databaseLink`.
*
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "<type>" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "<db_name>" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
* This method returns `MutableJSONPointer` that comes after type-name pair.
*
* @param Link from which to extract `MutableJSONPointer`.
* @return `MutableJSONPointer` from the database link.
* Guaranteed to not be `none` if provided argument `databaseLink`
* is not `none`.
*/
public final function MutableJsonPointer GetMutablePointer(BaseText databaseLink)
{
local int slashIndex;
local Text textPointer;
local MutableJsonPointer result;
if (databaseLink == none) {
return none;
}
slashIndex = databaseLink.IndexOf(P(":"));
if (slashIndex < 0) {
return MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
}
textPointer = databaseLink.Copy(slashIndex + 1);
result = _.json.MutablePointer(textPointer);
textPointer.FreeSelf();
return result;
}
/**
* Extracts `JSONPointer` from the database path, given by `databaseLink`.
*

14
sources/Data/Database/Database.uc

@ -101,7 +101,7 @@ enum DBQueryResult
* it does not point at any existing value inside the caller database.
*/
public function DBReadTask ReadData(
JSONPointer pointer,
BaseJSONPointer pointer,
optional bool makeMutable,
optional int requestID)
{
@ -148,7 +148,7 @@ public function DBReadTask ReadData(
* "sub-object" does not exist.
*/
public function DBWriteTask WriteData(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject data,
optional int requestID)
{
@ -183,7 +183,7 @@ public function DBWriteTask WriteData(
* it does not point at any existing value inside the caller database.
*/
public function DBRemoveTask RemoveData(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
return none;
@ -224,7 +224,7 @@ public function DBRemoveTask RemoveData(
* `result == DBR_Success`.
*/
public function DBCheckTask CheckDataType(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
return none;
@ -268,7 +268,7 @@ public function DBCheckTask CheckDataType(
* caller database.
*/
public function DBSizeTask GetDataSize(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
return none;
@ -314,7 +314,7 @@ public function DBSizeTask GetDataSize(
* (value can either not exist at all or have some other type).
*/
public function DBKeysTask GetDataKeys(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
return none;
@ -378,7 +378,7 @@ public function DBKeysTask GetDataKeys(
* operation) with `increment` parameter.
*/
public function DBIncrementTask IncrementData(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject increment,
optional int requestID)
{

37
sources/Data/Database/Local/DBRecord.uc

@ -185,7 +185,7 @@ private final function DBRecordPointer MakeRecordPointer(
}
// Converts `JSONPointer` into our internal representation.
private final function DBRecordPointer ConvertPointer(JSONPointer jsonPointer)
private final function DBRecordPointer ConvertPointer(BaseJSONPointer jsonPointer)
{
if (jsonPointer == none) {
return MakeRecordPointer(none);
@ -195,8 +195,7 @@ private final function DBRecordPointer ConvertPointer(JSONPointer jsonPointer)
// Produced out internal pointer representation `DBRecordPointer` to
// the container that stores object, referred to by a given `JSONPointer`.
private final function DBRecordPointer ConvertContainerPointer(
JSONPointer jsonPointer)
private final function DBRecordPointer ConvertContainerPointer(BaseJSONPointer jsonPointer)
{
local DBRecordPointer pointer;
if (jsonPointer == none) {
@ -212,9 +211,9 @@ private final function DBRecordPointer ConvertContainerPointer(
// Converts `JSONPointer` into internal `DBRecordPointer`.
// Only uses sub-pointer: components from `startIndex` to `endIndex`.
private final function DBRecordPointer ConvertPointerPath(
JSONPointer pointer,
int startIndex,
int endIndex)
BaseJSONPointer pointer,
int startIndex,
int endIndex)
{
local int index;
local StorageItem nextElement;
@ -371,7 +370,7 @@ private final static function string GetRandomLetter()
* (either does not point at any existing value or is equal to `none`).
*/
public final function bool LoadObject(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
out AcediaObject result,
bool makeMutable)
{
@ -409,7 +408,7 @@ public final function bool LoadObject(
* (either missing some necessary segments or is equal to `none`).
*/
public final function bool SaveObject(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
AcediaObject newItem)
{
local int index;
@ -437,10 +436,10 @@ public final function bool SaveObject(
return false;
}
directContainer = pointer.record;
itemKey = __().text.IntoString(jsonPointer.Pop(true));
itemKey = __().text.IntoString(jsonPointer.Peek());
if (directContainer.isJSONArray)
{
index = jsonPointer.PopNumeric(true);
index = jsonPointer.PeekNumeric();
if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
index = directContainer.GetStorageLength();
}
@ -465,7 +464,7 @@ public final function bool SaveObject(
* `false` otherwise. Failure can happen if passed `pointer` is invalid
* (either does not point at any existing value or equal to `none`).
*/
public final function bool RemoveObject(JSONPointer jsonPointer)
public final function bool RemoveObject(BaseJSONPointer jsonPointer)
{
local int itemIndex;
local string itemKey;
@ -477,11 +476,11 @@ public final function bool RemoveObject(JSONPointer jsonPointer)
directContainer = containerPointer.record;
if (directContainer.isJSONArray) {
itemIndex = jsonPointer.PopNumeric(true);
itemIndex = jsonPointer.PeekNumeric();
}
else
{
itemKey = __().text.IntoString(jsonPointer.Pop(true));
itemKey = __().text.IntoString(jsonPointer.Peek());
itemIndex = directContainer.FindItem(itemKey);
}
if (itemIndex >= 0)
@ -502,7 +501,7 @@ public final function bool RemoveObject(JSONPointer jsonPointer)
* `JSON_Undefined` if value is missing or passed pointer is invalid.
*/
public final function LocalDatabaseInstance.DataType GetObjectType(
JSONPointer jsonPointer)
BaseJSONPointer jsonPointer)
{
local DBRecord directContainer;
local DBRecordPointer pointer;
@ -550,7 +549,7 @@ public final function LocalDatabaseInstance.DataType GetObjectType(
* @return If `pointer` refers to the JSON array or object - amount of it's
* elements is returned. Otherwise returns `-1`.
*/
public final function int GetObjectSize(JSONPointer jsonPointer)
public final function int GetObjectSize(BaseJSONPointer jsonPointer)
{
local DBRecordPointer pointer;
if (jsonPointer == none) {
@ -572,7 +571,7 @@ public final function int GetObjectSize(JSONPointer jsonPointer)
* @return If `pointer` refers to the JSON object - all available keys.
* `none` otherwise (including case of JSON arrays).
*/
public final function ArrayList GetObjectKeys(JSONPointer jsonPointer)
public final function ArrayList GetObjectKeys(BaseJSONPointer jsonPointer)
{
local int i;
local ArrayList resultKeys;
@ -610,7 +609,7 @@ public final function ArrayList GetObjectKeys(JSONPointer jsonPointer)
* according to `Database.IncrementData()` specification.
*/
public final function Database.DBQueryResult IncrementObject(
JSONPointer jsonPointer,
BaseJSONPointer jsonPointer,
AcediaObject object)
{
local int index;
@ -639,10 +638,10 @@ public final function Database.DBQueryResult IncrementObject(
return DBR_InvalidPointer;
}
directContainer = pointer.record;
itemKey = __().text.IntoString(jsonPointer.Pop(true));
itemKey = __().text.IntoString(jsonPointer.Peek());
if (directContainer.isJSONArray)
{
index = jsonPointer.PopNumeric(true);
index = jsonPointer.PeekNumeric();
if (index < 0 && itemKey == JSONPOINTER_NEW_ARRAY_ELEMENT) {
index = directContainer.GetStorageLength();
}

20
sources/Data/Database/Local/LocalDatabaseInstance.uc

@ -150,9 +150,9 @@ private final function DBTask MakeNewTask(class<DBTask> newTaskClass)
}
private function bool ValidatePointer(
JSONPointer pointer,
DBTask relevantTask,
int requestID)
BaseJSONPointer pointer,
DBTask relevantTask,
int requestID)
{
if (pointer != none) {
return true;
@ -171,7 +171,7 @@ private function bool ValidateRootRecord(DBTask relevantTask, int requestID)
}
public function DBReadTask ReadData(
JSONPointer pointer,
BaseJSONPointer pointer,
optional bool makeMutable,
optional int requestID)
{
@ -195,7 +195,7 @@ public function DBReadTask ReadData(
}
public function DBWriteTask WriteData(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject data,
optional int requestID)
{
@ -229,7 +229,7 @@ public function DBWriteTask WriteData(
}
public function DBRemoveTask RemoveData(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
local DBRemoveTask removeTask;
@ -255,7 +255,7 @@ public function DBRemoveTask RemoveData(
}
public function DBCheckTask CheckDataType(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
local DBCheckTask checkTask;
@ -269,7 +269,7 @@ public function DBCheckTask CheckDataType(
}
public function DBSizeTask GetDataSize(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
local DBSizeTask sizeTask;
@ -283,7 +283,7 @@ public function DBSizeTask GetDataSize(
}
public function DBKeysTask GetDataKeys(
JSONPointer pointer,
BaseJSONPointer pointer,
optional int requestID)
{
local ArrayList keys;
@ -304,7 +304,7 @@ public function DBKeysTask GetDataKeys(
}
public function DBIncrementTask IncrementData(
JSONPointer pointer,
BaseJSONPointer pointer,
AcediaObject increment,
optional int requestID)
{

2
sources/Text/BaseText.uc

@ -1663,7 +1663,7 @@ public function Text IntoText()
}
/**
* This method frees caller `Text` and returns immutable `Text` copy instead.
* This method frees caller `Text` and returns mutable `MutableText` copy instead.
*
* @return Immutable `Text` copy of the caller `MutableText`.
*/

389
sources/Text/JSON/BaseJsonPointer.uc

@ -0,0 +1,389 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 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 BaseJsonPointer extends AcediaObject
abstract;
//! A base class for representing a JSON pointer as defined in
//! [RFC6901](https://tools.ietf.org/html/rfc6901).
//!
//! A JSON pointer is a string of tokens separated by the "/" character.
//! Each token represents a reference to an object's key or an array's index.
//! For example, the pointer "/foo/1/bar" corresponds to the key "bar" of the array element at
//! index 1 of the object with key "foo".
//!
//! This class provides a simple way to represent and access the components of a JSON pointer.
//! The Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c", the path "/"
//! will be stored as a singular empty component "" and an empty path "" would mean that there are
//! no components at all.
/// A component of a Json pointer, which is a part of the pointer separated by
/// the slash character '/'
struct Component {
// For arrays, a component is specified by a numeric index.
// To avoid parsing the [`textRepresentation`] property multiple times, we record whether we
// have already done so.
var bool testedForBeingNumeric;
// Numeric index represented by asText.
// It is set to `-1` if it was already tested and found not to be a number.
// Valid index values are always >= 0.
var int numericRepresentation;
// [`Text`] representation of the component.
// Can be equal to `none` only if this component was specified via a numeric index.
// This guarantees that [`testedForBeingNumeric`] is `true`.
var Text textRepresentation;
};
// An array of components that make up the path for this Json pointer.
// Each component represents a part of the path separated by the slash character '/'.
// The array contains the sequence of components that this Json pointer was initialized with.
var protected array<Component> components;
var protected const int TSLASH, TJson_ESCAPE, TJson_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE;
protected function Finalizer() {
local int i;
for (i = 0; i < components.length; i += 1) {
_.memory.Free(components[i].textRepresentation);
}
components.length = 0;
}
/// Checks whether this [`BaseJsonPointer`] instance is empty, meaning it points at the root value.
///
/// Returns `true` if it is empty; otherwise, returns `false`.
public final function bool IsEmpty() {
return components.length == 0;
}
/// Returns the component of the path specified by the given index, starting from 0.
///
/// Returns the path's component as a [`Text`] unless the specified index is outside of the range of
/// `[0, GetLength() - 1]`. In that case, it returns `none`.
public final function Text GetComponent(int index) {
local MutableText result;
result = GetMutableComponent(index);
if (result != none) {
return result.IntoText();
}
return none;
}
/// Returns the component of the path specified by the given index, starting from 0.
///
/// Returns the path's component as a [`MutableText`] unless the specified index is outside of the
/// range of `[0, GetLength() - 1]`. In that case, it returns `none`.
public final function MutableText GetMutableComponent(int index) {
if (index < 0) return none;
if (index >= components.length) return none;
// [`asText`] will store `none` only if we have added this component as numeric one
if (components[index].textRepresentation == none) {
components[index].textRepresentation =
_.text.FromInt(components[index].numericRepresentation);
}
return components[index].textRepresentation.MutableCopy();
}
/// Returns the numeric component of the path specified by the given index, starting from `0`.
///
/// Returns the path's component as a non-negative `int` unless the specified index is outside of
/// the range of [0, GetLength() - 1]. In that case, it returns `-1`.
public final function int GetNumericComponent(int index) {
local Parser parser;
if (index < 0) return -1;
if (index >= components.length) return -1;
if (!components[index].testedForBeingNumeric)
{
components[index].testedForBeingNumeric = true;
parser = _.text.Parse(components[index].textRepresentation);
parser.MUnsignedInteger(components[index].numericRepresentation);
if (!parser.Ok() || !parser.HasFinished()) {
components[index].numericRepresentation = -1;
}
parser.FreeSelf();
}
return components[index].numericRepresentation;
}
/// Checks if the component at the given index can be used to index an array.
///
/// This method accepts numeric components and the "-" component, which can be used to point to
/// the element after the last one in a [`JsonArray`].
///
/// Returns `true` if a component with the given index exists and is either
/// a positive number or "-".
public final function bool IsComponentArrayApplicable(int index) {
local bool appendElementAlias;
local Text component;
if (GetNumericComponent(index) >= 0) {
return true;
}
component = GetComponent(index);
appendElementAlias = P("-").IsEqual(component);
_.memory.Free(component);
return appendElementAlias;
}
/// Converts caller [`JsonPointer`] into it's [`Text`] representation.
public final function Text ToText() {
return ToMutableText().IntoText();
}
/// Converts caller [`JsonPointer`] into it's [`MutableText`] representation.
public final function MutableText ToMutableText() {
local int i;
local Text nextComponent;
local MutableText nextMutableComponent;
local MutableText result;
result = _.text.Empty();
if (GetLength() <= 0) {
return result;
}
for (i = 0; i < GetLength(); i += 1) {
nextComponent = GetComponent(i);
nextMutableComponent = nextComponent.MutableCopy();
// Replace (order is important)
nextMutableComponent.Replace(T(TJson_ESCAPE), T(TJSON_ESCAPED_ESCAPE));
nextMutableComponent.Replace(T(TSLASH), T(TJson_ESCAPED_SLASH));
result.Append(T(TSLASH)).Append(nextMutableComponent);
// Get rid of temporary values
nextMutableComponent.FreeSelf();
nextComponent.FreeSelf();
}
return result;
}
/// Returns the number of path components in the caller JsonPointer.
public final function int GetLength() {
return components.length;
}
/// Returns the number of intermediate path components in the caller JsonPointer
/// that do not directly correspond to a pointed value.
///
/// This number is calculated as Max(0, GetLength() - 1). For example, if the
/// Json pointer is "/user/Ivan/records/5/count", it refers to the value named
/// "count" that is nested inside 4 objects named "user", "Ivan", "records",
/// and "5". Therefore, the number of intermediate path components or "folds"
/// is equal to 4.
public final function int GetFoldsAmount() {
return Max(0, components.length - 1);
}
/// Creates an immutable copy of the caller [`BaseJsonPointer`].
///
/// Copies components in the range `[startIndex; startIndex + maxLength - 1]`.
/// If the provided parameters `startIndex` and `maxLength` define a range that goes beyond
/// `[0; self.GetLength() - 1]`, then the intersection with a valid range will be used.
///
/// If [`maxLength`] is `0` (default value) or a negative value, the method extracts all components
/// to the right of `startIndex`.
public final function JsonPointer Copy(optional int startIndex, optional int maxLength) {
local JsonPointer newPointer;
newPointer = JsonPointer(_.memory.Allocate(class'JsonPointer'));
_copyInto(newPointer, startIndex, maxLength);
return newPointer;
}
/// Creates an mutable copy of the caller [`BaseJsonPointer`].
///
/// Copies components in the range `[startIndex; startIndex + maxLength - 1]`.
/// If the provided parameters `startIndex` and `maxLength` define a range that goes beyond
/// `[0; self.GetLength() - 1]`, then the intersection with a valid range will be used.
///
/// If [`maxLength`] is `0` (default value) or a negative value, the method extracts all components
/// to the right of `startIndex`.
public final function MutableJsonPointer MutableCopy(
optional int startIndex,
optional int maxLength
) {
local MutableJsonPointer newPointer;
newPointer = MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
_copyInto(newPointer, startIndex, maxLength);
return newPointer;
}
/// Determines whether the given pointer corresponds to the beginning of the caller one.
///
/// A pointer starts with another one if it includes all of its fields from the beginning and in order.
/// For example, "/A/B/C" starts with "/A/B", but not with "/A/B/C/D", "/D/A/B/C" or "/A/B/CD".
///
/// Returns `true` if [`other`] is a prefix and `false` otherwise.
/// `none` is considered to be an empty pointer and therefore a prefix to any other pointer.
public final function bool StartsWith(BaseJsonPointer other) {
local int i;
local array<Component> otherComponents;
// `none` is same as empty
if (other == none) return true;
otherComponents = other.components;
// Not enough length
if (components.length < otherComponents.length) return false;
for (i = 0; i < otherComponents.length; i += 1) {
// Compare numeric components if at least one is such
if (components[i].testedForBeingNumeric || otherComponents[i].testedForBeingNumeric) {
if (GetNumericComponent(i) != other.GetNumericComponent(i)) {
return false;
}
// End this iteration for numeric component, but continue for text ones
if (GetNumericComponent(i) >= 0) {
continue;
}
}
// We can reach here if:
//
// 1. Neither components have `testedForBeingNumeric` set to `true`,
// neither `asText` fields are `none` by the invariant;
// 2. At least one had `testedForBeingNumeric`, but they tested negative for being numeric.
if (!components[i].textRepresentation.Compare(otherComponents[i].textRepresentation)) {
return false;
}
}
return true;
}
/// Returns the last component of the caller [`MutableJsonPointer`].
///
/// For example, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/d", this method
/// would return "d".
///
/// If the caller [`MutableJsonPointer`] is empty, the returned last component is `none`.
public final function Text Peek() {
local MutableText mutableResult;
mutableResult = PeekMutable();
if (mutableResult != none) {
return mutableResult.IntoText();
}
return none;
}
/// Returns the last component of the caller [`MutableJsonPointer`].
///
/// For example, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/d", this method
/// would return "d".
///
/// If the caller [`MutableJsonPointer`] is empty, the returned last component is `none`.
public final function MutableText PeekMutable() {
local int lastIndex;
local MutableText result;
if (components.length <= 0) {
return none;
}
lastIndex = components.length - 1;
// Do not use `GetComponent()` to avoid unnecessary `Text` copying
if (components[lastIndex].textRepresentation == none) {
result = _.text.FromIntM(components[lastIndex].numericRepresentation);
} else {
result = components[lastIndex].textRepresentation.MutableCopy();
}
return result;
}
/// Returns the last numeric component of the caller [`MutableJsonPointer`].
///
/// For instance, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/1", this method
/// would return `1`.
///
/// If the caller [`MutableJsonPointer`] does not end with a numeric component or is empty,
/// the returned value is `-1`.
public final function int PeekNumeric() {
local int lastIndex;
local int result;
if (components.length <= 0) {
return -1;
}
lastIndex = components.length - 1;
result = GetNumericComponent(lastIndex);
return result;
}
/// This method releases caller json pointer and returns immutable [`JsonPointer`] copy instead.
public function JsonPointer IntoJsonPointer() {
local JsonPointer result;
result = Copy();
FreeSelf();
return result;
}
/// This method releases caller json pointer and returns mutable [`MutableJsonPointer`] copy
/// instead.
public function MutableJsonPointer IntoMutableJsonPointer() {
local MutableJsonPointer result;
result = MutableCopy();
FreeSelf();
return result;
}
// Copies contents of caller into `copy` pointer.
// Assumes `copy` is freshly made: not `none`, but also no need to deallocate its components
private final function _copyInto(
BaseJsonPointer copy,
optional int startIndex,
optional int maxLength
) {
local int i, endIndex;
local array<Component> newComponents;
if (maxLength <= 0) {
maxLength = components.length - startIndex;
}
endIndex = startIndex + maxLength;
if (endIndex <= 0) {
return;
}
startIndex = Max(startIndex, 0);
endIndex = Min(endIndex, components.length);
for (i = startIndex; i < endIndex; i += 1) {
newComponents[newComponents.length] = components[i];
if (components[i].textRepresentation != none) {
newComponents[newComponents.length - 1].textRepresentation =
components[i].textRepresentation.Copy();
}
}
copy.components = newComponents;
}
defaultproperties {
TSLASH = 0
stringConstants(0) = "/"
TJson_ESCAPE = 1
stringConstants(1) = "~"
TJson_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
}

32
sources/Text/JSON/JSONAPI.uc

@ -82,8 +82,36 @@ private final function InitFormatting()
*/
public final function JSONPointer Pointer(optional BaseText pointerAsText)
{
return JSONPointer(_.memory.Allocate(class'JSONPointer'))
.Set(pointerAsText);
local MutableJSONPointer result;
result = MutableJSONPointer(_.memory.Allocate(class'MutableJSONPointer'));
result.Set(pointerAsText);
return result.IntoJsonPointer();
}
/**
* 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 MutableJSONPointer MutablePointer(optional BaseText pointerAsText)
{
local MutableJSONPointer result;
result = MutableJSONPointer(_.memory.Allocate(class'MutableJSONPointer'));
result.Set(pointerAsText);
return result;
}
/**

532
sources/Text/JSON/JSONPointer.uc

@ -1,11 +1,8 @@
/**
* Class for representing a JSON pointer (see
* https://tools.ietf.org/html/rfc6901).
* Allows quick and simple access to components of it's path:
* Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c",
* path "/" will be stored as a singular empty component ""
* and empty path "" would mean that there is not components at all.
* Copyright 2021-2023 Anton Tarasenko
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,513 +19,48 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JSONPointer extends AcediaObject;
class JsonPointer extends BaseJsonPointer;
// Component of the pointer (the part, separated by slash character '/').
struct Component
{
// For arrays, a component is specified by a numeric index;
// To avoid parsing `asText` multiple times we record whether we
// have already done so.
var bool testedForBeingNumeric;
// Numeric index, represented by `asText`;
// `-1` if it was already tested and found to be equal to not be a number
// (valid index values are always `>= 0`).
var int asNumber;
// `Text` representation of the component.
// Can be equal to `none` only if this component was specified via
// numeric index (guarantees `testedForBeingNumeric == true`).
var MutableText asText;
};
// Segments of the path this JSON pointer was initialized with
var private array<Component> components;
//! Immutable representation of a Json pointer as defined in
//! [RFC6901](https://tools.ietf.org/html/rfc6901).
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE;
protected function Finalizer()
{
Empty();
}
/**
* Checks whether caller `JSONPointer` is empty (points at the root value).
*
* @return `true` iff caller `JSONPointer` points at the root value.
*/
public final function bool IsEmpty()
{
return components.length == 0;
}
/**
* Resets caller `JSONPointer`, erasing all of it's components.
*
* @return Caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer Empty()
{
local int i;
for (i = 0; i < components.length; i += 1) {
_.memory.Free(components[i].asText);
}
components.length = 0;
return self;
}
/**
* Sets caller `JSONPointer` to correspond 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 Reference to the caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer Set(BaseText pointerAsText)
{
local int i;
local bool hasEscapedSequences;
local Component nextComponent;
local MutableText nextPart;
local array<BaseText> parts;
Empty();
if (pointerAsText == none) {
return self;
}
hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0);
parts = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0),, true);
// First element of the array is expected to be empty, so throw it away;
// If it is not empty - then `pointerAsText` does not start with "/" and
// we will pretend that we have already removed first element, thus
// "fixing" path (e.g. effectively turning "foo/bar" into "/foo/bar").
if (parts[0].IsEmpty())
{
_.memory.Free(parts[0]);
parts.Remove(0, 1);
}
if (hasEscapedSequences)
{
// Replace escaped sequences "~0" and "~1".
// Order is specific, necessity of which is explained in
// JSON Pointer's documentation:
// https://tools.ietf.org/html/rfc6901
for (i = 0; i < parts.length; i += 1)
{
nextPart = MutableText(parts[i]);
nextPart.Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH));
nextPart.Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE));
}
}
for (i = 0; i < parts.length; i += 1)
{
nextComponent.asText = MutableText(parts[i]);
components[components.length] = nextComponent;
}
return self;
}
/**
* Adds new component to the caller `JSONPointer`.
*
* Adding component "new" to the pointer representing path "/a/b/c" would
* result in it representing a path "/a/b/c/new".
*
* Although this method can be used to add numeric components, `PushNumeric()`
* should be used for that if possible.
*
* @param newComponent Component to add. If passed `none` value -
* no changes will be made at all.
* @return Reference to the caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer Push(BaseText newComponent)
{
local Component newComponentRecord;
if (newComponent == none) {
return self;
}
newComponentRecord.asText = newComponent.MutableCopy();
components[components.length] = newComponentRecord;
return self;
}
/**
* Adds new numeric component to the caller `JSONPointer`.
*
* Adding component `7` to the pointer representing path "/a/b/c" would
* result in it representing a path "/a/b/c/7".
*
* @param newComponent Numeric component to add. If passed negative value -
* no changes will be made at all.
* @return Reference to the caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer PushNumeric(int newComponent)
{
local Component newComponentRecord;
if (newComponent < 0) {
return self;
}
newComponentRecord.asNumber = newComponent;
// Obviously this component is going to be numeric
newComponentRecord.testedForBeingNumeric = true;
components[components.length] = newComponentRecord;
public function JsonPointer IntoJsonPointer() {
return self;
}
/**
* Removes and returns last component from the caller `JSONPointer`.
*
* In `JSONPointer` corresponding to "/ab/c/d" this method would return "d"
* and leave caller `JSONPointer` to correspond to "/ab/c".
*
* @param doNotRemove Set this to `true` if you want to return last component
* without changing caller pointer.
* @return Last component of the caller `JSONPointer`.
* `none` iff caller `JSONPointer` is empty.
*/
public final function Text Pop(optional bool doNotRemove)
{
local int lastIndex;
local Text result;
if (components.length <= 0) {
return none;
}
lastIndex = components.length - 1;
// Do not use `GetComponent()` to avoid unnecessary `Text` copying
if (components[lastIndex].asText == none) {
result = _.text.FromInt(components[lastIndex].asNumber);
}
else {
result = components[lastIndex].asText.Copy();
}
if (!doNotRemove)
{
_.memory.Free(components[lastIndex].asText);
components.length = components.length - 1;
}
return result;
}
/**
* Removes and returns last numeric component from the caller `JSONPointer`.
*
* In `JSONPointer` corresponding to "/ab/c/7" this method would return `7`
* and leave caller `JSONPointer` to correspond to "/ab/c".
*
* Component is removed regardless of whether it was actually numeric.
*
* @param doNotRemove Set this to `true` if you want to return last component
* without changing caller pointer.
* @return Last component of the caller `JSONPointer`.
* `-1` iff caller `JSONPointer` is empty or last component is not numeric.
*/
public final function int PopNumeric(optional bool doNotRemove)
{
local int lastIndex;
local int result;
if (components.length <= 0) {
return -1;
}
lastIndex = components.length - 1;
result = GetNumericComponent(lastIndex);
if (!doNotRemove)
{
_.memory.Free(components[lastIndex].asText);
components.length = components.length - 1;
}
return result;
}
/**
* Returns a component of the path by it's index, starting from `0`.
*
* @param index Index of the component to return. Must be inside
* `[0; GetLength() - 1]` segment.
* @return Path's component as a `Text`. If passed `index` is outside of
* `[0; GetLength() - 1]` segment - returns `none`.
*/
public final function Text GetComponent(int index)
{
if (index < 0) return none;
if (index >= components.length) return none;
// `asText` will store `none` only if we have added this component as
// numeric one
if (components[index].asText == none) {
components[index].asText = _.text.FromIntM(components[index].asNumber);
}
return components[index].asText.Copy();
}
/**
* Returns a numeric component of the path by it's index, starting from `0`.
*
* @param index Index of the component to return. Must be inside
* `[0; GetLength() - 1]` segment and correspond to numeric comonent.
* @return Path's component as a `Text`. If passed `index` is outside of
* `[0; GetLength() - 1]` segment or does not correspond to
* a numeric component - returns `-1`.
*/
public final function int GetNumericComponent(int index)
{
local Parser parser;
if (index < 0) return -1;
if (index >= components.length) return -1;
if (!components[index].testedForBeingNumeric)
{
components[index].testedForBeingNumeric = true;
parser = _.text.Parse(components[index].asText);
parser.MUnsignedInteger(components[index].asNumber);
if (!parser.Ok() || !parser.HasFinished()) {
components[index].asNumber = -1;
}
parser.FreeSelf();
}
return components[index].asNumber;
}
/**
* Checks whether component at given index can be used to index array.
*
* This method accepts numeric components plus component equal to "-", that can
* be used to point at the element after the last on in the `JSONArray`.
*
* @param index Index of the component to check.
* @param `true` if component with given index exists and it either positive
* number or "-".
*/
public final function bool IsComponentArrayApplicable(int index)
{
local bool isAddElementAlias;
local Text component;
if (GetNumericComponent(index) >= 0) {
return true;
}
component = GetComponent(index);
isAddElementAlias = P("-").IsEqual(component);
_.memory.Free(component);
return isAddElementAlias;
}
/**
* Converts caller `JSONPointer` into it's `Text` representation.
*
* For the method, but returning `MutableText` see `ToTextM()`.
*
* @return `Text` that represents caller `JSONPointer`.
*/
public final function Text ToText()
{
local Text result;
local MutableText builder;
builder = ToTextM();
result = builder.Copy();
builder.FreeSelf();
return result;
}
/**
* Converts caller `JSONPointer` into it's `MutableText` representation.
*
* For the method, but returning `Text` see `ToTextM()`.
*
* @return `MutableText` that represents caller `JSONPointer`.
*/
public final function MutableText ToTextM()
{
local int i;
local Text nextComponent;
local MutableText nextMutableComponent;
local MutableText result;
result = _.text.Empty();
if (GetLength() <= 0) {
return result;
}
for (i = 0; i < GetLength(); i += 1)
{
nextComponent = GetComponent(i);
nextMutableComponent = nextComponent.MutableCopy();
// Replace (order is important)
nextMutableComponent.Replace(T(TJSON_ESCAPE), T(TJSON_ESCAPED_ESCAPE));
nextMutableComponent.Replace(T(TSLASH), T(TJSON_ESCAPED_SLASH));
result.Append(T(TSLASH)).Append(nextMutableComponent);
// Get rid of temporary values
nextMutableComponent.FreeSelf();
nextComponent.FreeSelf();
}
return result;
}
/**
* Amount of path components in the caller `JSONPointer`.
*
* Also see `GetFoldsAmount()` method.
*
* @return Amount of components in the caller `JSONPointer`.
*/
public final function int GetLength()
{
return components.length;
}
/**
* Amount of path components in the caller `JSONPointer` that do not directly
* correspond to a pointed value.
*
* Equal to the `Max(0, GetLength() - 1)`.
*
* For example, path "/user/Ivan/records/5/count" refers to the value named
* "value" that is _folded_ inside `4` objects named "users", "Ivan",
* "records" and "5". Therefore it's folds amount if `4`.
*
* @return Amount of components in the caller `JSONPointer` that do not
* directly correspond to a pointed value.
*/
public final function int GetFoldsAmount()
{
return Max(0, components.length - 1);
}
public function bool IsEqual(Object other) {
local JsonPointer otherJsonPointer;
/**
* Makes an exact copy of the caller `JSONPointer`.
*
* Copies components in the range `[startIndex; startIndex + maxLength - 1]`
* If provided parameters `startIndex` and `maxLength` define a range that
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
* range will be used.
*
* @param startIndex Position of the first component to copy.
* By default `0`, corresponding to the very first component.
* @param maxLength Max length of the extracted JSON pointer (in amount of
* components). By default `0` - that and all negative values mean that
* method should extract all components to the right of `startIndex`.
* @return Copy of the specified range of the caller `JSONPointer`.
*/
public final function JSONPointer Copy(
optional int startIndex,
optional int maxLength)
{
local int i, endIndex;
local JSONPointer newPointer;
local array<Component> newComponents;
otherJsonPointer = JsonPointer(other);
if (otherJsonPointer == none) return false;
if (components.length != otherJsonPointer.components.length) return false;
if (!StartsWith(otherJsonPointer)) return false;
if (maxLength <= 0) {
maxLength = components.length - startIndex;
}
endIndex = startIndex + maxLength;
if (endIndex <= 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
}
startIndex = Max(startIndex, 0);
endIndex = Min(endIndex, components.length);
for (i = startIndex; i < endIndex; i += 1)
{
newComponents[newComponents.length] = components[i];
if (components[i].asText != none)
{
newComponents[newComponents.length - 1].asText =
components[i].asText.MutableCopy();
}
}
newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer'));
newPointer.components = newComponents;
return newPointer;
return true;
}
/**
* Appends path, contained in JSON pointer `other` to the caller JSON pointer.
* Appending "/A/B/7/C" to "/object/hey/1/there/" produces
* "/object/hey/1/there//A/B/7/C".
*
* @param other Pointer to append. If `none` - caller `JSONPointer` will
* not change.
* @return Reference to the caller `JSONPointer` to allow for method chaining.
*/
public final function JSONPointer Append(JSONPointer other)
{
local int i;
local array<Component> otherComponents;
protected function int CalculateHashCode() {
local int i;
local int accumulator, nextValue;
if (other == none) {
return self;
if (components.length == 0) {
return 32894237;
}
otherComponents = other.components;
for (i = 0; i < otherComponents.length; i += 1)
{
if (otherComponents[i].asText != none) {
otherComponents[i].asText = otherComponents[i].asText.MutableCopy();
}
components[components.length] = otherComponents[i];
if (components[0].textRepresentation != none) {
accumulator = components[0].textRepresentation.GetHashCode();
} else {
accumulator = components[0].numericRepresentation;
}
return self;
}
/**
* Checks if given pointer corresponds with the beginning of the caller one.
*
* Pointer starts with another one if it includes all of its fields from
* the beginning and in order
* E.g. "/A/B/C" starts with "/A/B", but not with "/A/B/C/D", "/D/A/B/C" or
* "/A/B/CD".
*
* @param other Candidate into being caller pointer's prefix.
* @return `true` if `other` is prefix and `false` otherwise. `none` is
* considered to be an empty pointer and, therefore, prefix to any other
* pointer.
*/
public final function bool StartsWith(JSONPointer other)
{
local int i;
local array<Component> otherComponents;
// `none` is same as empty
if (other == none) return true;
otherComponents = other.components;
// Not enough length
if (components.length < otherComponents.length) return false;
for (i = 0; i < otherComponents.length; i += 1)
{
// Compare numeric components if at least one is such
if ( components[i].testedForBeingNumeric
|| otherComponents[i].testedForBeingNumeric)
{
if (GetNumericComponent(i) != other.GetNumericComponent(i)) {
return false;
}
// End this iteration for numeric component, but continue for
// text ones
if (GetNumericComponent(i) >= 0) {
continue;
}
}
// We can reach here if:
// 1. Neither components have `testedForBeingNumeric` set to
// `true`, neither `asText` fields are `none` by the invariant;
// 2. At least one had `testedForBeingNumeric`, but they tested
// negative for being numeric.
if (!components[i].asText.Compare(otherComponents[i].asText)) {
return false;
for (i = 1; i < components.length; i += 1) {
if (components[i].textRepresentation != none) {
nextValue = components[i].textRepresentation.GetHashCode();
} else {
nextValue = components[i].numericRepresentation;
}
accumulator = CombineHash(accumulator, nextValue);
}
return true;
return accumulator;
}
defaultproperties
{
TSLASH = 0
stringConstants(0) = "/"
TJSON_ESCAPE = 1
stringConstants(1) = "~"
TJSON_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
defaultproperties {
}

207
sources/Text/JSON/MutableJsonPointer.uc

@ -0,0 +1,207 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 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 MutableJsonPointer extends BaseJsonPointer;
//! Mutable representation of a Json pointer as defined in
//! [RFC6901](https://tools.ietf.org/html/rfc6901).
/// Resets the caller [`JsonPointer`] into an empty path, erasing all of its components.
public final function Empty() {
local int i;
for (i = 0; i < components.length; i += 1) {
_.memory.Free(components[i].textRepresentation);
}
components.length = 0;
}
/// Sets the caller [`JsonPointer`] to correspond to a given path in JSON pointer format
/// [RFC6901](https://tools.ietf.org/html/rfc6901).
///
/// If the provided [`BaseText`] value is not a valid pointer, it will be treated as
/// an empty pointer.
///
/// If the given pointer can be fixed by prepending "/", it will be done automatically.
/// For example, "foo/bar" is treated like "/foo/bar", "path" like "/path", but
/// an empty [`BaseText`] "" is treated as itself.
public final function Set(BaseText pointerAsText) {
local int i;
local bool hasEscapedSequences;
local Component nextComponent;
local MutableText nextPart;
local array<BaseText> parts;
Empty();
if (pointerAsText == none) {
return;
}
hasEscapedSequences = (pointerAsText.IndexOf(T(TJson_ESCAPE)) >= 0);
parts = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0),, true);
// The first element of the parts array is expected to be empty,
// because the [`BaseText::SplitByCharacter()`] method always returns an array with an empty
// first element when the input string starts with the delimiter.
//
// Therefore, we need to remove the first element to discard the empty string.
// If the first element is not empty, it means that the input string does not start with "/",
// so we pretend that we have already removed the first element, effectively "fixing"
// the path (e.g. turning "foo/bar" into "/foo/bar").
if (parts[0].IsEmpty()) {
_.memory.Free(parts[0]);
parts.Remove(0, 1);
}
if (hasEscapedSequences) {
// Replace escaped sequences "~0" and "~1".
// Order is specific, necessity of which is explained in Json Pointer's documentation:
// https://tools.ietf.org/html/rfc6901
for (i = 0; i < parts.length; i += 1) {
nextPart = MutableText(parts[i]);
nextPart.Replace(T(TJson_ESCAPED_SLASH), T(TSLASH));
nextPart.Replace(T(TJSON_ESCAPED_ESCAPE), T(TJson_ESCAPE));
}
}
for (i = 0; i < parts.length; i += 1) {
nextComponent.textRepresentation = parts[i].IntoText();
components[components.length] = nextComponent;
}
}
/// This function appends a new component to the end of the caller [`JsonPointer`].
///
/// For instance, if the caller pointer represents the path "/a/b/c", adding
/// the component "new" would result in it representing the path "/a/b/c/new".
///
/// While this method can be used to add numeric components, it's recommended to use
/// PushNumeric() if possible for better clarity and efficiency.
public final function Push(BaseText newComponent) {
local Component newComponentRecord;
if (newComponent != none) {
newComponentRecord.textRepresentation = newComponent.Copy();
components[components.length] = newComponentRecord;
}
}
/// This function adds a new numeric component to the end of the caller [`JsonPointer`].
///
/// For example, if the caller pointer represents the path "/a/b/c", adding the numeric
/// component "1" would result in it representing the path "/a/b/c/1".
public final function PushNumeric(int newComponent) {
local Component newComponentRecord;
if (newComponent >= 0) {
newComponentRecord.numericRepresentation = newComponent;
// Obviously this component is going to be numeric
newComponentRecord.testedForBeingNumeric = true;
components[components.length] = newComponentRecord;
}
}
/// Removes and returns the last component of the caller [`MutableJsonPointer`].
///
/// For example, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/d", this method
/// would return "d" and modify the caller [`MutableJsonPointer`] to correspond to "/ab/c".
///
/// If the caller [`MutableJsonPointer`] is empty, the returned last component is `none`.
public final function Text Pop() {
local MutableText mutableResult;
mutableResult = PopMutable();
if (mutableResult != none) {
return mutableResult.IntoText();
}
return none;
}
/// Removes and returns the last component of the caller [`MutableJsonPointer`].
///
/// For example, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/d", this method
/// would return "d" and modify the caller [`MutableJsonPointer`] to correspond to "/ab/c".
///
/// If the caller [`MutableJsonPointer`] is empty, the returned last component is `none`.
public final function MutableText PopMutable() {
local int lastIndex;
local MutableText result;
if (components.length <= 0) {
return none;
}
lastIndex = components.length - 1;
// Do not use `GetComponent()` to avoid unnecessary `Text` copying
result = PeekMutable();
_.memory.Free(components[lastIndex].textRepresentation);
components.length = components.length - 1;
return result;
}
/// Removes and returns the last numeric component of the caller [`MutableJsonPointer`].
///
/// For instance, if the caller [`MutableJsonPointer`] corresponds to "/ab/c/1", this method
/// would return `1` and modify the caller [`MutableJsonPointer`] to correspond to "/ab/c".
///
/// If the caller [`MutableJsonPointer`] does not end with a numeric component or is empty,
/// the returned value is `none`.
/// Value still gets removed.
public final function int PopNumeric() {
local int lastIndex;
local int result;
if (components.length <= 0) {
return -1;
}
lastIndex = components.length - 1;
result = GetNumericComponent(lastIndex);
_.memory.Free(components[lastIndex].textRepresentation);
components.length = components.length - 1;
return result;
}
/// Appends path, contained in Json pointer [`other`] to the caller Json pointer.
///
/// Appending "/A/B/7/C" to "/object/hey/1/there/" produces
/// "/object/hey/1/there//A/B/7/C".
///
/// Method will do nothing if `none` is passed as an argument.
/// Appends the path contained in the argument to the caller [`MutableJsonPointer`].
///
/// For example, appending "/A/B/7/C" to "/object/hey/1/there/" would result in
/// "/object/hey/1/there//A/B/7/C".
///
/// If the [`BaseJsonPointer`] argument is `non`, this method will do nothing.
public final function Append(BaseJsonPointer other) {
local int i;
local array<Component> otherComponents;
if (other == none) {
return;
}
otherComponents = other.components;
for (i = 0; i < otherComponents.length; i += 1) {
if (otherComponents[i].textRepresentation != none) {
otherComponents[i].textRepresentation = otherComponents[i].textRepresentation.Copy();
}
components[components.length] = otherComponents[i];
}
}
defaultproperties {
}

7
sources/Text/MutableText.uc

@ -24,6 +24,7 @@ var private int CODEPOINT_NEWLINE;
public function Text IntoText()
{
local Text immutableVersion;
immutableVersion = Copy();
FreeSelf();
return immutableVersion;
@ -31,7 +32,11 @@ public function Text IntoText()
public function MutableText IntoMutableText()
{
return self;
local MutableText mutableVersion;
mutableVersion = MutableCopy();
FreeSelf();
return mutableVersion;
}
/**

107
sources/Text/Tests/TEST_JSON.uc

@ -48,6 +48,7 @@ protected static function Test_Pointer()
SubText_Copy();
SubTest_StartsWith();
SubTest_IsComponentArrayApplicable();
SubTest_Hash();
}
protected static function SubTest_PointerCreate()
@ -89,31 +90,33 @@ protected static function SubTest_PointerToText()
Issue("`JSONPointer` is not converted to `Text` correctly.");
pointer = __().json.Pointer(P(""));
TEST_ExpectTrue(pointer.ToText().ToString() == "");
TEST_ExpectTrue(pointer.ToTextM().ToString() == "");
TEST_ExpectTrue(pointer.ToMutableText().ToString() == "");
pointer = __().json.Pointer(P("///"));
TEST_ExpectTrue(pointer.ToText().ToString() == "///");
TEST_ExpectTrue(pointer.ToTextM().ToString() == "///");
TEST_ExpectTrue(pointer.ToMutableText().ToString() == "///");
pointer = __().json.Pointer(P("/a~1b/c%d/e^f//g|h/i\\j/m~0n/"));
TEST_ExpectTrue( pointer.ToText().ToString()
== "/a~1b/c%d/e^f//g|h/i\\j/m~0n/");
TEST_ExpectTrue( pointer.ToTextM().ToString()
TEST_ExpectTrue( pointer.ToMutableText().ToString()
== "/a~1b/c%d/e^f//g|h/i\\j/m~0n/");
pointer = __().json.Pointer(P("/a/b/c"));
Issue("Result of `ToText()` has a wrong class.");
TEST_ExpectTrue(pointer.ToText().class == class'Text');
Issue("Result of `ToTextM()` has a wrong class.");
TEST_ExpectTrue(pointer.ToTextM().class == class'MutableText');
Issue("Result of `ToMutableText()` has a wrong class.");
TEST_ExpectTrue(pointer.ToMutableText().class == class'MutableText');
}
protected static function SubTest_PointerPushPop()
{
local JSONPointer pointer;
local MutableJSONPointer pointer;
local Text value0, value1, value2, value3, value4, value5, value6;
Issue("`Push()`/`PushNumeric()` incorrectly affect `JSONPointer`.");
pointer = __().json.Pointer(P("//lets/go"));
pointer.Push(P("one")).PushNumeric(404).Push(P("More"));
pointer = __().json.MutablePointer(P("//lets/go"));
pointer.Push(P("one"));
pointer.PushNumeric(404);
pointer.Push(P("More"));
TEST_ExpectTrue( pointer.ToText().ToString()
== "//lets/go/one/404/More");
@ -144,16 +147,17 @@ protected static function SubTest_PointerPushPop()
protected static function SubTest_PointerNumeric()
{
local JSONPointer pointer;
local string correct, incorrect;
local MutableJSONPointer pointer;
local string correct, incorrect;
correct = "`GetNumericComponent()`/`PopNumeric()` cannot correctly retrieve"
@ "`JSONPointer`'s numeric components.";
incorrect = "`GetNumericComponent()`/`PopNumeric()` do not return negative"
@ "values for non-numeric components `JSONPointer`'s"
@ "numeric components.";
Issue(correct);
pointer = __().json.Pointer(P("/lets//404/8./6/11/d/0"));
pointer.PushNumeric(-2).PushNumeric(13);
pointer = __().json.MutablePointer(P("/lets//404/8./6/11/d/0"));
pointer.PushNumeric(-2);
pointer.PushNumeric(13);
TEST_ExpectTrue(pointer.GetNumericComponent(8) == 13);
Issue(incorrect);
TEST_ExpectTrue(pointer.GetNumericComponent(6) < 0);
@ -178,45 +182,45 @@ protected static function SubTest_PointerNumeric()
protected static function SubTest_PopWithoutRemoving()
{
local Text component;
local JSONPointer pointer;
Issue("`Pop(true)` removes the value from the pointer.");
pointer = __().json.Pointer(P("/just/a/simple/test"));
TEST_ExpectTrue(pointer.Pop(true).ToString() == "test");
TEST_ExpectTrue(pointer.Pop(true).ToString() == "test");
Issue("`Pop(true)` returns actually stored value instead of a copy.");
pointer.Pop(true).FreeSelf();
TEST_ExpectTrue(pointer.Pop(true).ToString() == "test");
local Text component;
local MutableJSONPointer pointer;
Issue("`Peek()` removes the value from the pointer.");
pointer = __().json.MutablePointer(P("/just/a/simple/test"));
TEST_ExpectTrue(pointer.Peek().ToString() == "test");
TEST_ExpectTrue(pointer.Peek().ToString() == "test");
Issue("`Peek()` returns actually stored value instead of a copy.");
pointer.Peek().FreeSelf();
TEST_ExpectTrue(pointer.Peek().ToString() == "test");
component = pointer.Pop();
TEST_ExpectNotNone(component);
TEST_ExpectTrue(component.ToString() == "test");
TEST_ExpectTrue(component.IsAllocated());
Issue("`Pop(true)` breaks after regular `Pop()` call.");
TEST_ExpectTrue(pointer.Pop(true).ToString() == "simple");
TEST_ExpectTrue(pointer.Pop(true).ToString() == "simple");
Issue("`Peek()` breaks after regular `Pop()` call.");
TEST_ExpectTrue(pointer.Peek().ToString() == "simple");
TEST_ExpectTrue(pointer.Peek().ToString() == "simple");
}
protected static function SubTest_Append()
{
local JSONPointer pointer, append;
local MutableJSONPointer pointer, append;
Issue("Appending another JSON pointer is not working correctly.");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
append = __().json.Pointer(P("/A/B/7/C"));
pointer = __().json.MutablePointer(P("/object/hey/1/there/"));
append = __().json.MutablePointer(P("/A/B/7/C"));
pointer.Append(append);
TEST_ExpectTrue(
pointer.ToText().ToString()
== "/object/hey/1/there//A/B/7/C");
pointer = __().json.Pointer(P(""));
append = __().json.Pointer(P("/A/B/7/C"));
pointer = __().json.MutablePointer(P(""));
append = __().json.MutablePointer(P("/A/B/7/C"));
pointer.Append(append);
TEST_ExpectTrue(pointer.ToText().ToString() == "/A/B/7/C");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
append = __().json.Pointer(P(""));
pointer = __().json.MutablePointer(P("/object/hey/1/there/"));
append = __().json.MutablePointer(P(""));
pointer.Append(append);
TEST_ExpectTrue(pointer.ToText().ToString() == "/object/hey/1/there/");
pointer = __().json.Pointer(P("/object/hey/1/there/"));
pointer = __().json.MutablePointer(P("/object/hey/1/there/"));
pointer.Append(none);
TEST_ExpectTrue(pointer.ToText().ToString() == "/object/hey/1/there/");
}
@ -256,7 +260,7 @@ protected static function SubText_Copy()
protected static function SubTest_StartsWith()
{
local JSONPointer pointer;
local MutableJSONPointer pointer;
Issue("Any pointers start with `none` JSON pointer.");
TEST_ExpectTrue(__().json.Pointer(P("/A/B/C")).StartsWith(none));
@ -268,7 +272,10 @@ protected static function SubTest_StartsWith()
TEST_ExpectTrue(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7/C"))));
// Same, but constructed manually to handle components added as numeric
pointer = __().json.Pointer().Push(P("A")).PushNumeric(7).Push(P("C"));
pointer = __().json.MutablePointer();
pointer.Push(P("A"));
pointer.PushNumeric(7);
pointer.Push(P("C"));
TEST_ExpectTrue(pointer.StartsWith(__().json.Pointer(P("/A/7/C"))));
TEST_ExpectTrue(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7"))));
@ -282,7 +289,10 @@ protected static function SubTest_StartsWith()
TEST_ExpectFalse(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/3/C"))));
// Constructed manually to handle components added as numeric
pointer = __().json.Pointer().Push(P("A")).PushNumeric(8).Push(P("C"));
pointer = __().json.MutablePointer();
pointer.Push(P("A"));
pointer.PushNumeric(8);
pointer.Push(P("C"));
TEST_ExpectFalse(pointer.StartsWith(__().json.Pointer(P("/A/3/C"))));
TEST_ExpectFalse(__().json.Pointer(P("/A/7/C"))
.StartsWith(__().json.Pointer(P("/A/7/"))));
@ -306,6 +316,31 @@ protected static function SubTest_IsComponentArrayApplicable()
__().json.Pointer(P("/A/7/C")).IsComponentArrayApplicable(10));
}
protected static function SubTest_Hash()
{
Issue("`JsonPointer`'s hash doesn't generate unique values based on the pointer's contents.");
TEST_ExpectTrue(__().json.Pointer(P("")).GetHashCode()
== __().json.Pointer(P("")).GetHashCode());
TEST_ExpectTrue(__().json.Pointer(P("/")).GetHashCode()
== __().json.Pointer(P("/")).GetHashCode());
TEST_ExpectTrue(__().json.Pointer(P("/test//subObject/3/hey")).GetHashCode()
== __().json.Pointer(P("/test//subObject/3/hey")).GetHashCode());
TEST_ExpectTrue(__().json.Pointer(P("/test//subObject/3/hey")).GetHashCode()
== __().json.Pointer(P("/test//subObject/3/hey")).GetHashCode());
Issue("`JsonPointer`'s hash depends on the pointer's contents.");
// Technically these have a chance to get precisely the same hash code...
// but that's almost impossible
TEST_ExpectFalse(__().json.MutablePointer(P("")).GetHashCode()
== __().json.MutablePointer(P("")).GetHashCode());
TEST_ExpectFalse(__().json.MutablePointer(P("/")).GetHashCode()
== __().json.MutablePointer(P("/")).GetHashCode());
TEST_ExpectFalse(__().json.MutablePointer(P("/test//subObject/3/hey")).GetHashCode()
== __().json.MutablePointer(P("/test//subObject/3/hey")).GetHashCode());
TEST_ExpectFalse(__().json.MutablePointer(P("/test//subObject/3/hey")).GetHashCode()
== __().json.MutablePointer(P("/test//subObject/3/hey")).GetHashCode());
}
protected static function Test_Print()
{
Context("Testing printing simple JSON values.");

60
sources/Users/PersistentData/PersistentDataManager.uc

@ -54,10 +54,10 @@ class PersistentDataManager extends AcediaObject;
* have updated.
*/
var private bool initialized;
var private Database database;
var private JSONPointer rootPointer;
var private HashTable userToConnection, connectionToUser;
var private bool initialized;
var private Database database;
var private MutableJSONPointer rootPointer;
var private HashTable userToConnection, connectionToUser;
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal;
@ -101,7 +101,7 @@ private final function Reset()
* @return `true` if setup was successful (requires both arguments to be not
* `none`) and `false` otherwise.
*/
public final function bool Setup(Database db, JSONPointer location)
public final function bool Setup(Database db, BaseJSONPointer location)
{
if (db == none) return false;
if (location == none) return false;
@ -109,7 +109,7 @@ public final function bool Setup(Database db, JSONPointer location)
Reset();
database = db;
database.NewRef();
rootPointer = location.Copy();
rootPointer = location.MutableCopy();
userToConnection = _.collections.EmptyHashTable();
connectionToUser = _.collections.EmptyHashTable();
// Using `userToConnection` as an empty hash table, not related to its
@ -141,10 +141,10 @@ public final function AcediaObject GetPersistentData(
BaseText groupName,
optional BaseText dataName)
{
local AcediaObject result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
local AcediaObject result;
local Text textID;
local MutableJSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return none;
if (id == none) return none;
@ -155,7 +155,7 @@ public final function AcediaObject GetPersistentData(
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location = _.json.MutablePointer();
location.Push(groupName);
if (dataName != none) {
location.Push(dataName);
@ -191,11 +191,11 @@ public final function bool WritePersistentData(
BaseText dataName,
AcediaObject data)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
local HashTable emptyObject;
local bool result;
local Text textID;
local MutableJSONPointer location;
local DBConnection relevantConnection;
local HashTable emptyObject;
if (!initialized) return false;
if (id == none) return false;
@ -208,7 +208,7 @@ public final function bool WritePersistentData(
if (relevantConnection != none)
{
emptyObject = _.collections.EmptyHashTable();
location = _.json.Pointer();
location = _.json.MutablePointer();
location.Push(groupName);
relevantConnection.IncrementDataByJSON(location, emptyObject);
location.Push(dataName);
@ -244,10 +244,10 @@ public final function bool IncrementPersistentData(
BaseText dataName,
AcediaObject data)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
local bool result;
local Text textID;
local MutableJSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return false;
if (id == none) return false;
@ -259,8 +259,9 @@ public final function bool IncrementPersistentData(
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location.Push(groupName).Push(dataName);
location = _.json.MutablePointer();
location.Push(groupName);
location.Push(dataName);
result = relevantConnection.IncrementDataByJSON(location, data);
relevantConnection.FreeSelf();
location.FreeSelf();
@ -289,10 +290,10 @@ public final function bool RemovePersistentData(
BaseText groupName,
BaseText dataName)
{
local bool result;
local Text textID;
local JSONPointer location;
local DBConnection relevantConnection;
local bool result;
local Text textID;
local MutableJSONPointer location;
local DBConnection relevantConnection;
if (!initialized) return false;
if (id == none) return false;
@ -304,8 +305,9 @@ public final function bool RemovePersistentData(
textID.FreeSelf();
if (relevantConnection != none)
{
location = _.json.Pointer();
location.Push(groupName).Push(dataName);
location = _.json.MutablePointer();
location.Push(groupName);
location.Push(dataName);
result = relevantConnection.RemoveDataByJSON(location);
relevantConnection.FreeSelf();
location.FreeSelf();

10
sources/Users/Users_Feature.uc

@ -48,10 +48,10 @@ struct IDAnnotationPair
var Text id, annotation;
};
var private bool userGroupsDataLoaded;
var private Database usersGroupsDatabase;
var private JSONPointer userGroupsRootPointer;
var private int stackedDBReadingRequests;
var private bool userGroupsDataLoaded;
var private Database usersGroupsDatabase;
var private MutableJSONPointer userGroupsRootPointer;
var private int stackedDBReadingRequests;
var private PersistentDataManager currentPersistentDataManager;
@ -191,7 +191,7 @@ private final function LoadUserData()
}
else
{
userGroupsRootPointer = _server.db.GetPointer(databaseLinkAsText);
userGroupsRootPointer = _server.db.GetMutablePointer(databaseLinkAsText);
emptyHashTable = _.collections.EmptyHashTable();
usersGroupsDatabase.IncrementData(
userGroupsRootPointer,

Loading…
Cancel
Save