Browse Source

Merge pull request 'Refactor `JsonPointer` into separate (im)mutable versions' (#10) from feature_immutable_json_pointer into develop

Reviewed-on: #10
pull/12/head
dkanus 2 years ago
parent
commit
4cf66442cb
  1. 24
      sources/Data/Collections/Collection.uc
  2. 47
      sources/Data/Database/Connection/DBCache.uc
  3. 31
      sources/Data/Database/Connection/DBConnection.uc
  4. 35
      sources/Data/Database/DBAPI.uc
  5. 14
      sources/Data/Database/Database.uc
  6. 33
      sources/Data/Database/Local/DBRecord.uc
  7. 16
      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. 528
      sources/Text/JSON/JSONPointer.uc
  12. 207
      sources/Text/JSON/MutableJsonPointer.uc
  13. 7
      sources/Text/MutableText.uc
  14. 103
      sources/Text/Tests/TEST_JSON.uc
  15. 28
      sources/Users/PersistentData/PersistentDataManager.uc
  16. 4
      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 * 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: * Acedia provides two collections:
* 1. `ArrayList` is treated as a JSON array in the context of * 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 * @return An item `jsonPointer` is referring to (according to the above
* stated rules). `none` if such item does not exist. * 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 int segmentIndex;
local Text nextSegment; local Text nextSegment;
@ -648,7 +648,7 @@ public final function ArrayList GetArrayListBy(BaseText jsonPointerAsText)
* is missing or has a different type. * is missing or has a different type.
*/ */
public final function bool GetBoolByJSON( public final function bool GetBoolByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional bool defaultValue) optional bool defaultValue)
{ {
local bool result; local bool result;
@ -689,7 +689,7 @@ public final function bool GetBoolByJSON(
* is missing or has a different type. * is missing or has a different type.
*/ */
public final function byte GetByteByJSON( public final function byte GetByteByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional byte defaultValue) optional byte defaultValue)
{ {
local byte result; local byte result;
@ -734,7 +734,7 @@ public final function byte GetByteByJSON(
* is missing or has a different type. * is missing or has a different type.
*/ */
public final function int GetIntByJSON( public final function int GetIntByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional int defaultValue) optional int defaultValue)
{ {
local int result; local int result;
@ -789,7 +789,7 @@ public final function int GetIntByJSON(
* is missing or has a different type. * is missing or has a different type.
*/ */
public final function float GetFloatByJSON( public final function float GetFloatByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional float defaultValue) optional float defaultValue)
{ {
local float result; local float result;
@ -840,7 +840,7 @@ public final function float GetFloatByJSON(
* is missing or has a different type. * is missing or has a different type.
*/ */
public final function Vector GetVectorByJSON( public final function Vector GetVectorByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional Vector defaultValue) optional Vector defaultValue)
{ {
local Vector result; local Vector result;
@ -881,7 +881,7 @@ public final function Vector GetVectorByJSON(
* if it is missing or has a different type. * if it is missing or has a different type.
*/ */
public final function string GetStringByJSON( public final function string GetStringByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional string defaultValue) optional string defaultValue)
{ {
local AcediaObject result; local AcediaObject result;
@ -915,7 +915,7 @@ public final function string GetStringByJSON(
* `defaultValue` if it is missing or has a different type. * `defaultValue` if it is missing or has a different type.
*/ */
public final function string GetFormattedStringByJSON( public final function string GetFormattedStringByJSON(
JSONPointer jsonPointer, BaseJSONPointer jsonPointer,
optional string defaultValue) optional string defaultValue)
{ {
local AcediaObject result; local AcediaObject result;
@ -972,7 +972,7 @@ public final function Text GetTextByJSON(JSONPointer jsonPointer)
* `none` if it is missing or has a different type. * `none` if it is missing or has a different type.
*/ */
public final function Collection GetCollectionByJSON( public final function Collection GetCollectionByJSON(
JSONPointer jsonPointer) BaseJSONPointer jsonPointer)
{ {
local AcediaObject result; local AcediaObject result;
local Collection asCollection; local Collection asCollection;
@ -999,7 +999,7 @@ public final function Collection GetCollectionByJSON(
* `none` if it is missing or has a different type. * `none` if it is missing or has a different type.
*/ */
public final function HashTable GetHashTableByJSON( public final function HashTable GetHashTableByJSON(
JSONPointer jsonPointer) BaseJSONPointer jsonPointer)
{ {
local AcediaObject result; local AcediaObject result;
local HashTable asHashTable; local HashTable asHashTable;
@ -1026,7 +1026,7 @@ public final function HashTable GetHashTableByJSON(
* `none` if it is missing or has a different type. * `none` if it is missing or has a different type.
*/ */
public final function ArrayList GetArrayListByJSON( public final function ArrayList GetArrayListByJSON(
JSONPointer jsonPointer) BaseJSONPointer jsonPointer)
{ {
local AcediaObject result; local AcediaObject result;
local ArrayList asArrayList; local ArrayList asArrayList;

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

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

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

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

35
sources/Data/Database/DBAPI.uc

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

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

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

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

@ -150,7 +150,7 @@ private final function DBTask MakeNewTask(class<DBTask> newTaskClass)
} }
private function bool ValidatePointer( private function bool ValidatePointer(
JSONPointer pointer, BaseJSONPointer pointer,
DBTask relevantTask, DBTask relevantTask,
int requestID) int requestID)
{ {
@ -171,7 +171,7 @@ private function bool ValidateRootRecord(DBTask relevantTask, int requestID)
} }
public function DBReadTask ReadData( public function DBReadTask ReadData(
JSONPointer pointer, BaseJSONPointer pointer,
optional bool makeMutable, optional bool makeMutable,
optional int requestID) optional int requestID)
{ {
@ -195,7 +195,7 @@ public function DBReadTask ReadData(
} }
public function DBWriteTask WriteData( public function DBWriteTask WriteData(
JSONPointer pointer, BaseJSONPointer pointer,
AcediaObject data, AcediaObject data,
optional int requestID) optional int requestID)
{ {
@ -229,7 +229,7 @@ public function DBWriteTask WriteData(
} }
public function DBRemoveTask RemoveData( public function DBRemoveTask RemoveData(
JSONPointer pointer, BaseJSONPointer pointer,
optional int requestID) optional int requestID)
{ {
local DBRemoveTask removeTask; local DBRemoveTask removeTask;
@ -255,7 +255,7 @@ public function DBRemoveTask RemoveData(
} }
public function DBCheckTask CheckDataType( public function DBCheckTask CheckDataType(
JSONPointer pointer, BaseJSONPointer pointer,
optional int requestID) optional int requestID)
{ {
local DBCheckTask checkTask; local DBCheckTask checkTask;
@ -269,7 +269,7 @@ public function DBCheckTask CheckDataType(
} }
public function DBSizeTask GetDataSize( public function DBSizeTask GetDataSize(
JSONPointer pointer, BaseJSONPointer pointer,
optional int requestID) optional int requestID)
{ {
local DBSizeTask sizeTask; local DBSizeTask sizeTask;
@ -283,7 +283,7 @@ public function DBSizeTask GetDataSize(
} }
public function DBKeysTask GetDataKeys( public function DBKeysTask GetDataKeys(
JSONPointer pointer, BaseJSONPointer pointer,
optional int requestID) optional int requestID)
{ {
local ArrayList keys; local ArrayList keys;
@ -304,7 +304,7 @@ public function DBKeysTask GetDataKeys(
} }
public function DBIncrementTask IncrementData( public function DBIncrementTask IncrementData(
JSONPointer pointer, BaseJSONPointer pointer,
AcediaObject increment, AcediaObject increment,
optional int requestID) 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`. * @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) public final function JSONPointer Pointer(optional BaseText pointerAsText)
{ {
return JSONPointer(_.memory.Allocate(class'JSONPointer')) local MutableJSONPointer result;
.Set(pointerAsText);
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;
} }
/** /**

528
sources/Text/JSON/JSONPointer.uc

@ -1,11 +1,8 @@
/** /**
* Class for representing a JSON pointer (see * Author: dkanus
* https://tools.ietf.org/html/rfc6901). * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* Allows quick and simple access to components of it's path: * License: GPL
* Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c", * Copyright 2023 Anton Tarasenko
* 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
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
* *
@ -22,513 +19,48 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. * 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 '/'). //! Immutable representation of a Json pointer as defined in
struct Component //! [RFC6901](https://tools.ietf.org/html/rfc6901).
{
// 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;
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH; public function JsonPointer IntoJsonPointer() {
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; 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;
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;
} }
/** public function bool IsEqual(Object other) {
* Returns a component of the path by it's index, starting from `0`. local JsonPointer otherJsonPointer;
*
* @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 otherJsonPointer = JsonPointer(other);
// numeric one if (otherJsonPointer == none) return false;
if (components[index].asText == none) { if (components.length != otherJsonPointer.components.length) return false;
components[index].asText = _.text.FromIntM(components[index].asNumber); if (!StartsWith(otherJsonPointer)) return false;
}
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; 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;
} }
/** protected function int CalculateHashCode() {
* 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 int i;
local Text nextComponent; local int accumulator, nextValue;
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;
}
/** if (components.length == 0) {
* Amount of path components in the caller `JSONPointer` that do not directly return 32894237;
* 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);
}
/**
* 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;
if (maxLength <= 0) {
maxLength = components.length - startIndex;
}
endIndex = startIndex + maxLength;
if (endIndex <= 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
} }
startIndex = Max(startIndex, 0); if (components[0].textRepresentation != none) {
endIndex = Min(endIndex, components.length); accumulator = components[0].textRepresentation.GetHashCode();
for (i = startIndex; i < endIndex; i += 1) } else {
{ accumulator = components[0].numericRepresentation;
newComponents[newComponents.length] = components[i];
if (components[i].asText != none)
{
newComponents[newComponents.length - 1].asText =
components[i].asText.MutableCopy();
} }
for (i = 1; i < components.length; i += 1) {
if (components[i].textRepresentation != none) {
nextValue = components[i].textRepresentation.GetHashCode();
} else {
nextValue = components[i].numericRepresentation;
} }
newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer')); accumulator = CombineHash(accumulator, nextValue);
newPointer.components = newComponents;
return newPointer;
}
/**
* 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;
if (other == none) {
return self;
}
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]; return accumulator;
}
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;
}
}
return true;
} }
defaultproperties defaultproperties {
{
TSLASH = 0
stringConstants(0) = "/"
TJSON_ESCAPE = 1
stringConstants(1) = "~"
TJSON_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
} }

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() public function Text IntoText()
{ {
local Text immutableVersion; local Text immutableVersion;
immutableVersion = Copy(); immutableVersion = Copy();
FreeSelf(); FreeSelf();
return immutableVersion; return immutableVersion;
@ -31,7 +32,11 @@ public function Text IntoText()
public function MutableText IntoMutableText() public function MutableText IntoMutableText()
{ {
return self; local MutableText mutableVersion;
mutableVersion = MutableCopy();
FreeSelf();
return mutableVersion;
} }
/** /**

103
sources/Text/Tests/TEST_JSON.uc

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

28
sources/Users/PersistentData/PersistentDataManager.uc

@ -56,7 +56,7 @@ class PersistentDataManager extends AcediaObject;
var private bool initialized; var private bool initialized;
var private Database database; var private Database database;
var private JSONPointer rootPointer; var private MutableJSONPointer rootPointer;
var private HashTable userToConnection, connectionToUser; var private HashTable userToConnection, connectionToUser;
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal; 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 * @return `true` if setup was successful (requires both arguments to be not
* `none`) and `false` otherwise. * `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 (db == none) return false;
if (location == none) return false; if (location == none) return false;
@ -109,7 +109,7 @@ public final function bool Setup(Database db, JSONPointer location)
Reset(); Reset();
database = db; database = db;
database.NewRef(); database.NewRef();
rootPointer = location.Copy(); rootPointer = location.MutableCopy();
userToConnection = _.collections.EmptyHashTable(); userToConnection = _.collections.EmptyHashTable();
connectionToUser = _.collections.EmptyHashTable(); connectionToUser = _.collections.EmptyHashTable();
// Using `userToConnection` as an empty hash table, not related to its // Using `userToConnection` as an empty hash table, not related to its
@ -143,7 +143,7 @@ public final function AcediaObject GetPersistentData(
{ {
local AcediaObject result; local AcediaObject result;
local Text textID; local Text textID;
local JSONPointer location; local MutableJSONPointer location;
local DBConnection relevantConnection; local DBConnection relevantConnection;
if (!initialized) return none; if (!initialized) return none;
@ -155,7 +155,7 @@ public final function AcediaObject GetPersistentData(
textID.FreeSelf(); textID.FreeSelf();
if (relevantConnection != none) if (relevantConnection != none)
{ {
location = _.json.Pointer(); location = _.json.MutablePointer();
location.Push(groupName); location.Push(groupName);
if (dataName != none) { if (dataName != none) {
location.Push(dataName); location.Push(dataName);
@ -193,7 +193,7 @@ public final function bool WritePersistentData(
{ {
local bool result; local bool result;
local Text textID; local Text textID;
local JSONPointer location; local MutableJSONPointer location;
local DBConnection relevantConnection; local DBConnection relevantConnection;
local HashTable emptyObject; local HashTable emptyObject;
@ -208,7 +208,7 @@ public final function bool WritePersistentData(
if (relevantConnection != none) if (relevantConnection != none)
{ {
emptyObject = _.collections.EmptyHashTable(); emptyObject = _.collections.EmptyHashTable();
location = _.json.Pointer(); location = _.json.MutablePointer();
location.Push(groupName); location.Push(groupName);
relevantConnection.IncrementDataByJSON(location, emptyObject); relevantConnection.IncrementDataByJSON(location, emptyObject);
location.Push(dataName); location.Push(dataName);
@ -246,7 +246,7 @@ public final function bool IncrementPersistentData(
{ {
local bool result; local bool result;
local Text textID; local Text textID;
local JSONPointer location; local MutableJSONPointer location;
local DBConnection relevantConnection; local DBConnection relevantConnection;
if (!initialized) return false; if (!initialized) return false;
@ -259,8 +259,9 @@ public final function bool IncrementPersistentData(
textID.FreeSelf(); textID.FreeSelf();
if (relevantConnection != none) if (relevantConnection != none)
{ {
location = _.json.Pointer(); location = _.json.MutablePointer();
location.Push(groupName).Push(dataName); location.Push(groupName);
location.Push(dataName);
result = relevantConnection.IncrementDataByJSON(location, data); result = relevantConnection.IncrementDataByJSON(location, data);
relevantConnection.FreeSelf(); relevantConnection.FreeSelf();
location.FreeSelf(); location.FreeSelf();
@ -291,7 +292,7 @@ public final function bool RemovePersistentData(
{ {
local bool result; local bool result;
local Text textID; local Text textID;
local JSONPointer location; local MutableJSONPointer location;
local DBConnection relevantConnection; local DBConnection relevantConnection;
if (!initialized) return false; if (!initialized) return false;
@ -304,8 +305,9 @@ public final function bool RemovePersistentData(
textID.FreeSelf(); textID.FreeSelf();
if (relevantConnection != none) if (relevantConnection != none)
{ {
location = _.json.Pointer(); location = _.json.MutablePointer();
location.Push(groupName).Push(dataName); location.Push(groupName);
location.Push(dataName);
result = relevantConnection.RemoveDataByJSON(location); result = relevantConnection.RemoveDataByJSON(location);
relevantConnection.FreeSelf(); relevantConnection.FreeSelf();
location.FreeSelf(); location.FreeSelf();

4
sources/Users/Users_Feature.uc

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

Loading…
Cancel
Save