From 6321368665a61392d5ef570bea4aaae293ed337c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 2 Apr 2023 04:18:38 +0700 Subject: [PATCH] Refactor `JsonPointer` into separate (im)mutable versions --- sources/Data/Collections/Collection.uc | 24 +- sources/Data/Database/Connection/DBCache.uc | 65 +-- .../Data/Database/Connection/DBConnection.uc | 33 +- sources/Data/Database/DBAPI.uc | 35 ++ sources/Data/Database/Database.uc | 14 +- sources/Data/Database/Local/DBRecord.uc | 37 +- .../Database/Local/LocalDatabaseInstance.uc | 20 +- sources/Text/BaseText.uc | 2 +- sources/Text/JSON/BaseJsonPointer.uc | 389 +++++++++++++ sources/Text/JSON/JSONAPI.uc | 32 +- sources/Text/JSON/JSONPointer.uc | 532 ++---------------- sources/Text/JSON/MutableJsonPointer.uc | 207 +++++++ sources/Text/MutableText.uc | 7 +- sources/Text/Tests/TEST_JSON.uc | 107 ++-- .../PersistentData/PersistentDataManager.uc | 60 +- sources/Users/Users_Feature.uc | 10 +- 16 files changed, 901 insertions(+), 673 deletions(-) create mode 100644 sources/Text/JSON/BaseJsonPointer.uc create mode 100644 sources/Text/JSON/MutableJsonPointer.uc diff --git a/sources/Data/Collections/Collection.uc b/sources/Data/Collections/Collection.uc index bc76b95..c194fd3 100644 --- a/sources/Data/Collections/Collection.uc +++ b/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; diff --git a/sources/Data/Database/Connection/DBCache.uc b/sources/Data/Database/Connection/DBCache.uc index 22ee9b8..f59f1d9 100644 --- a/sources/Data/Database/Connection/DBCache.uc +++ b/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 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) { diff --git a/sources/Data/Database/Connection/DBConnection.uc b/sources/Data/Database/Connection/DBConnection.uc index 3e35a18..7ebcd4f 100644 --- a/sources/Data/Database/Connection/DBConnection.uc +++ b/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); } /** diff --git a/sources/Data/Database/DBAPI.uc b/sources/Data/Database/DBAPI.uc index ed8d7cd..b12aaa8 100644 --- a/sources/Data/Database/DBAPI.uc +++ b/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 ":" (or, optionally, "[]:"), + * followed by the JSON pointer (possibly empty one) to the object inside it. + * "" can be either "local" or "remote" and is necessary only when both + * local and remote database have the same name (which should be avoided). + * "" 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`. * diff --git a/sources/Data/Database/Database.uc b/sources/Data/Database/Database.uc index a296020..3b4efe5 100644 --- a/sources/Data/Database/Database.uc +++ b/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) { diff --git a/sources/Data/Database/Local/DBRecord.uc b/sources/Data/Database/Local/DBRecord.uc index 8032001..6d486ab 100644 --- a/sources/Data/Database/Local/DBRecord.uc +++ b/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(); } diff --git a/sources/Data/Database/Local/LocalDatabaseInstance.uc b/sources/Data/Database/Local/LocalDatabaseInstance.uc index 7ccd624..2556b67 100644 --- a/sources/Data/Database/Local/LocalDatabaseInstance.uc +++ b/sources/Data/Database/Local/LocalDatabaseInstance.uc @@ -150,9 +150,9 @@ private final function DBTask MakeNewTask(class 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) { diff --git a/sources/Text/BaseText.uc b/sources/Text/BaseText.uc index 218788e..f15d905 100644 --- a/sources/Text/BaseText.uc +++ b/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`. */ diff --git a/sources/Text/JSON/BaseJsonPointer.uc b/sources/Text/JSON/BaseJsonPointer.uc new file mode 100644 index 0000000..3e5573b --- /dev/null +++ b/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 . + */ +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 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 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 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" +} \ No newline at end of file diff --git a/sources/Text/JSON/JSONAPI.uc b/sources/Text/JSON/JSONAPI.uc index 04a2674..1515eaf 100644 --- a/sources/Text/JSON/JSONAPI.uc +++ b/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; } /** diff --git a/sources/Text/JSON/JSONPointer.uc b/sources/Text/JSON/JSONPointer.uc index 63ed173..f170847 100644 --- a/sources/Text/JSON/JSONPointer.uc +++ b/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 . */ -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 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 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 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 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 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 { } \ No newline at end of file diff --git a/sources/Text/JSON/MutableJsonPointer.uc b/sources/Text/JSON/MutableJsonPointer.uc new file mode 100644 index 0000000..cc2f138 --- /dev/null +++ b/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 . + */ +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 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 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 { +} \ No newline at end of file diff --git a/sources/Text/MutableText.uc b/sources/Text/MutableText.uc index d809106..bd66576 100644 --- a/sources/Text/MutableText.uc +++ b/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; } /** diff --git a/sources/Text/Tests/TEST_JSON.uc b/sources/Text/Tests/TEST_JSON.uc index 8e45da9..ea8fc4a 100644 --- a/sources/Text/Tests/TEST_JSON.uc +++ b/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."); diff --git a/sources/Users/PersistentData/PersistentDataManager.uc b/sources/Users/PersistentData/PersistentDataManager.uc index c6ec518..c58fe10 100644 --- a/sources/Users/PersistentData/PersistentDataManager.uc +++ b/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(); diff --git a/sources/Users/Users_Feature.uc b/sources/Users/Users_Feature.uc index e785eb5..9431038 100644 --- a/sources/Users/Users_Feature.uc +++ b/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, -- 2.20.1