Browse Source

Add JSON pointers support for Acedia `Collection`s

pull/8/head
Anton Tarasenko 4 years ago
parent
commit
c371bd89c5
  1. 15
      sources/Data/Collections/AssociativeArray.uc
  2. 386
      sources/Data/Collections/Collection.uc
  3. 17
      sources/Data/Collections/DynamicArray.uc
  4. 105
      sources/Data/Collections/Tests/TEST_CollectionsMixed.uc
  5. 9
      sources/Manifest.uc

15
sources/Data/Collections/AssociativeArray.uc

@ -6,7 +6,7 @@
* stores generic `AcediaObject` keys and values. `Text` can be used instead of
* typical `string` keys and primitive values can be added in their boxed form
* (either as actual `<Type>Box` or as it's reference counterpart).
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -530,6 +530,19 @@ public final function Entry GetEntryByIndex(Index index)
return hashTable[index.bucketIndex].entries[index.entryIndex];
}
protected function AcediaObject GetByText(MutableText key)
{
local Text immutableKey;
local AcediaObject result;
if (key == none) {
return none;
}
immutableKey = key.Copy();
result = GetItem(immutableKey);
immutableKey.FreeSelf();
return result;
}
defaultproperties
{
iteratorClass = class'AssociativeArrayIterator'

386
sources/Data/Collections/Collection.uc

@ -2,7 +2,7 @@
* Acedia provides a small set of collections for easier data storage.
* This is their base class that provides a simple interface for
* common methods.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -22,8 +22,41 @@
class Collection extends AcediaObject
abstract;
// A private struct for `Collection` that disassembles a
// [JSON pointer](https://tools.ietf.org/html/rfc6901) into the path parts,
// separated by "/".
// It is used to simplify the code working with them.
struct JSONPointer
{
// Records whether JSON pointer had it's escape sequences ("~0" and "~1");
// This is used to determine if we need to waste our time replacing them.
var private bool hasEscapedSequences;
// Parts of the path that were separated by "/" character.
var private array<MutableText> keys;
// Points at a part in `keys` to be used next.
var private int nextIndex;
};
var class<Iter> iteratorClass;
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE;
/**
* Method that must be overloaded for `GetItemByPointer()` to properly work.
*
* This method must return an item that `key` refers to with it's
* textual content (not as an object itself).
* For example, `DynamicArray` parses it into unsigned number, while
* `AssociativeArray` converts it into an immutable `Text` key, whose hash code
* depends on the contents.
*
* There is no requirement that all stored values must be reachable by
* this method (i.e. `AssociativeArray` only lets you access values with
* `Text` keys).
*/
protected function AcediaObject GetByText(MutableText key);
/**
* Creates an `Iterator` instance to iterate over stored items.
*
@ -46,6 +79,357 @@ public final function Iter Iterate()
return newIterator;
}
// Created `JSONPointer` structure (inside `ptr` out argument), based on
// it's textual representation `pointerAsText`. Returns whether it's succeeded.
// Deviates from JSON pointer specification in also allowing non-empty
// arguments not starting with "/" by treating them as a whole variable name.
private final function bool MakePointer(Text pointerAsText, out JSONPointer ptr)
{
if (pointerAsText == none) {
return false;
}
FreePointer(ptr); // Clean up, in case we were given used pointer
ptr.hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0);
if (!pointerAsText.StartsWith(T(TSLASH)))
{
ptr.nextIndex = 0;
ptr.keys[0] = pointerAsText.MutableCopy();
return true;
}
ptr.keys = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0));
// First elements of the array will be empty, so throw it away
_.memory.Free(ptr.keys[0]);
ptr.nextIndex = 1;
return true;
}
private final function bool IsFinalPointerKey(JSONPointer ptr)
{
return ((ptr.nextIndex + 1) == ptr.keys.length);
}
private final function MutableText PopJSONKey(out JSONPointer ptr)
{
local MutableText result;
if (ptr.nextIndex >= ptr.keys.length) {
return none;
}
ptr.nextIndex += 1;
result = ptr.keys[ptr.nextIndex - 1];
if (ptr.hasEscapedSequences)
{
// Order is specific, necessity of which is explained in
// JSON Pointer's documentation:
// https://tools.ietf.org/html/rfc6901
result.Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH));
result.Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE));
}
return result;
}
// Frees all memory used up by the `JSONPointer`
private final function FreePointer(out JSONPointer ptr)
{
_.memory.FreeMany(ptr.keys);
}
/**
* Returns stored `AcediaObject` from the caller storage
* (or from it's sub-storages) via given `Text` path.
*
* Path is used in one of the two ways:
* 1. If path is an empty `Text` or if it starts with "/" character,
* it will be interpreted as
* a [JSON pointer](https://tools.ietf.org/html/rfc6901);
* 2. Otherwise it will be used as an argument's name.
*
* Acedia provides two collections:
* 1. `DynamicArray` is treated as a JSON array in the context of
* JSON pointers and passed variable names are treated as a `Text`
* representation of it's integer indices;
* 2. `AssociativeArray` is treated as a JSON object in the context of
* JSON pointers and passed variable names are treated as it's
* `Text` keys (to refer to an element with an empty key, use "/",
* since "" is treated as a JSON pointer and refers to
* the array itself).
* It is also possible to define your own collection type that will also be
* integrated with this method by making it a sub-class of `Collection` and
* appropriately defining `GetByText()` protected method.
*
* Making only getter available (without setters or `Take...()` methods that
* also remove returned element) is a deliberate choice made to reduce amount
* of possible errors when working with collections.
*
* There is no requirement that all stored values must be reachable by
* this method (i.e. `AssociativeArray` only lets you access values with
* `Text` keys).
*
* @param jsonPointerAsText Treated as a JSON pointer if it starts with "/"
* character or is an empty `Text`, otherwise treated as an item's
* name / identificator inside the caller collection.
* @return An item `jsonPointerAsText` is referring to (according to the above
* stated rules). `none` if such item does not exist.
*/
public final function AcediaObject GetItemByPointer(Text jsonPointerAsText)
{
local AcediaObject result;
local JSONPointer ptr;
local Collection nextCollection;
if (jsonPointerAsText == none) return none;
if (jsonPointerAsText.IsEmpty()) return self;
if (!MakePointer(jsonPointerAsText, ptr)) {
return none;
}
nextCollection = self;
while (!IsFinalPointerKey(ptr))
{
nextCollection = Collection(nextCollection.GetByText(PopJSONKey(ptr)));
if (nextCollection == none)
{
FreePointer(ptr);
return none;
}
}
result = nextCollection.GetByText(PopJSONKey(ptr));
FreePointer(ptr);
return result;
}
/**
* Returns a `bool` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `BoolBox` or `BoolRef`
* (or one of their sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `bool` value.
* @param defaultValue Value to return in case `jsonPointerAsText`
* does not point at any existing value or if that value does not have
* appropriate type.
* @return `bool` value, stored at `jsonPointerAsText` or `defaultValue` if it
* is missing or has a different type.
*/
public final function bool GetBoolByPointer(
Text jsonPointerAsText,
optional bool defaultValue)
{
local AcediaObject result;
local BoolBox asBox;
local BoolRef asRef;
result = GetItemByPointer(jsonPointerAsText);
if (result == none) {
return defaultValue;
}
asBox = BoolBox(result);
if (asBox != none) {
return asBox.Get();
}
asRef = BoolRef(result);
if (asRef != none) {
return asRef.Get();
}
return defaultValue;
}
/**
* Returns a `byte` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `ByteBox` or `ByteRef`
* (or one of their sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `byte` value.
* @param defaultValue Value to return in case `jsonPointerAsText`
* does not point at any existing value or if that value does not have
* appropriate type.
* @return `byte` value, stored at `jsonPointerAsText` or `defaultValue` if it
* is missing or has a different type.
*/
public final function byte GetByteByPointer(
Text jsonPointerAsText,
optional byte defaultValue)
{
local AcediaObject result;
local ByteBox asBox;
local ByteRef asRef;
result = GetItemByPointer(jsonPointerAsText);
if (result == none) {
return defaultValue;
}
asBox = ByteBox(result);
if (asBox != none) {
return asBox.Get();
}
asRef = ByteRef(result);
if (asRef != none) {
return asRef.Get();
}
return defaultValue;
}
/**
* Returns a `int` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `IntBox` or `IntRef`
* (or one of their sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `int` value.
* @param defaultValue Value to return in case `jsonPointerAsText`
* does not point at any existing value or if that value does not have
* appropriate type.
* @return `int` value, stored at `jsonPointerAsText` or `defaultValue` if it
* is missing or has a different type.
*/
public final function int GetIntByPointer(
Text jsonPointerAsText,
optional int defaultValue)
{
local AcediaObject result;
local IntBox asBox;
local IntRef asRef;
result = GetItemByPointer(jsonPointerAsText);
if (result == none) {
return defaultValue;
}
asBox = IntBox(result);
if (asBox != none) {
return asBox.Get();
}
asRef = IntRef(result);
if (asRef != none) {
return asRef.Get();
}
return defaultValue;
}
/**
* Returns a `float` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `FloatBox` or `FloatRef`
* (or one of their sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `float` value.
* @param defaultValue Value to return in case `jsonPointerAsText`
* does not point at any existing value or if that value does not have
* appropriate type.
* @return `float` value, stored at `jsonPointerAsText` or `defaultValue` if it
* is missing or has a different type.
*/
public final function float GetFloatByPointer(
Text jsonPointerAsText,
optional float defaultValue)
{
local AcediaObject result;
local FloatBox asBox;
local FloatRef asRef;
result = GetItemByPointer(jsonPointerAsText);
if (result == none) {
return defaultValue;
}
asBox = FloatBox(result);
if (asBox != none) {
return asBox.Get();
}
asRef = FloatRef(result);
if (asRef != none) {
return asRef.Get();
}
return defaultValue;
}
/**
* Returns a `Text` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `Text` or `MutableText`
* (or one of their sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the `Text` value.
* @return `Text` value, stored at `jsonPointerAsText` or `none` if it
* is missing or has a different type.
*/
public final function Text GetTextByPointer(Text jsonPointerAsText)
{
local AcediaObject result;
local MutableText asMutable;
local Text asImmutable;
result = GetItemByPointer(jsonPointerAsText);
if (result == none) {
return none;
}
asMutable = MutableText(result);
if (asMutable != none) {
return asMutable;
}
asImmutable = Text(result);
if (asImmutable != none) {
return asImmutable;
}
return none;
}
/**
* Returns an `AssociativeArray` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `AssociativeArray`
* (or one of it's sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the
* `AssociativeArray` value.
* @return `AssociativeArray` value, stored at `jsonPointerAsText` or
* `none` if it is missing or has a different type.
*/
public final function AssociativeArray GetAssociativeArrayByPointer(
Text jsonPointerAsText)
{
return AssociativeArray(GetItemByPointer(jsonPointerAsText));
}
/**
* Returns an `DynamicArray` value stored (in the caller `Collection` or
* one of it's sub-collections) pointed by
* [JSON pointer](https://tools.ietf.org/html/rfc6901).
* See `GetItemByPointer()` for more information.
*
* Referred value must be stored as `DynamicArray`
* (or one of it's sub-classes) for this method to work.
*
* @param jsonPointerAsText Description of a path to the
* `DynamicArray` value.
* @return `DynamicArray` value, stored at `jsonPointerAsText` or
* `none` if it is missing or has a different type.
*/
public final function DynamicArray GetDynamicArrayByPointer(
Text jsonPointerAsText)
{
return DynamicArray(GetItemByPointer(jsonPointerAsText));
}
defaultproperties
{
TSLASH = 0
stringConstants(0) = "/"
TJSON_ESCAPE = 1
stringConstants(1) = "~"
TJSON_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
}

17
sources/Data/Collections/DynamicArray.uc

@ -5,7 +5,7 @@
* `AcediaObject`s.
* Appropriate classes and APIs for their construction are provided for
* main primitive types and can be extended to any custom `struct`.
* Copyright 2020 Anton Tarasenko
* Copyright 2020 - 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -474,6 +474,21 @@ public final function int Find(AcediaObject item)
return -1;
}
protected function AcediaObject GetByText(MutableText key)
{
local int index, consumed;
local Parser parser;
parser = _.text.Parse(key);
parser.MUnsignedInteger(index,,, consumed);
if (!parser.Ok())
{
parser.FreeSelf();
return none;
}
parser.FreeSelf();
return GetItem(index);
}
defaultproperties
{
iteratorClass = class'DynamicArrayIterator'

105
sources/Data/Collections/Tests/TEST_CollectionsMixed.uc

@ -0,0 +1,105 @@
/**
* Set of tests for `AssociativeArray` class.
* Copyright 2020 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 TEST_CollectionsMixed extends TestCase
abstract;
var protected const string complexJSONObject;
protected static function TESTS()
{
Context("Testing accessing collections by JSON pointers.");
Test_GetByPointer();
Test_GetTypeByPointer();
}
protected static function Test_GetByPointer()
{
local AcediaObject result;
local AssociativeArray obj;
Issue("`GetItemByPointer()` does not return correct objects.");
obj = __().json.ParseObjectWith(
__().text.ParseString(default.complexJSONObject));
TEST_ExpectTrue(obj.GetItemByPointer(P("")) == obj);
result = obj.GetItemByPointer(P("/innerObject/array/1"));
TEST_ExpectNotNone(BoolBox(result));
TEST_ExpectTrue(BoolBox(result).Get() == false);
result = obj.GetItemByPointer(P("/innerObject/array/3/maybe"));
TEST_ExpectNotNone(FloatBox(result));
TEST_ExpectTrue(FloatBox(result).Get() == 0.003);
Issue("`GetItemByPointer()` does not return correct objects when using"
@ "'~'-escaped sequences.");
result = obj.GetItemByPointer(P("/another~01var"));
TEST_ExpectNotNone(Text(result));
TEST_ExpectTrue(Text(result).ToPlainString() == "aye!");
result = obj.GetItemByPointer(P("/innerObject/one more/no~1pe"));
TEST_ExpectNotNone(IntBox(result));
TEST_ExpectTrue(IntBox(result).Get() == 324532);
TEST_ExpectNotNone(
DynamicArray(obj.GetItemByPointer(P("/innerObject/array"))));
Issue("`GetItemByPointer()` does not return `none` for incorrect pointers");
TEST_ExpectNone(obj.GetItemByPointer(P("innerObject/array")));
TEST_ExpectNone(obj.GetItemByPointer(P("//")));
TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/5")));
TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/-1")));
TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/")));
}
protected static function Test_GetTypeByPointer()
{
local AssociativeArray obj;
obj = __().json.ParseObjectWith(
__().text.ParseString(default.complexJSONObject));
obj.SetItem(P("byte"), __().ref.byte(56));
Issue("`Get<Type>ByPointer()` methods do not return correct"
@ "existing values.");
TEST_ExpectTrue(obj.GetAssociativeArrayByPointer(P("")) == obj);
TEST_ExpectNotNone(obj.GetDynamicArrayByPointer(P("/innerObject/array")));
TEST_ExpectTrue(
obj.GetBoolByPointer(P("/innerObject/array/1"), true)
== false);
TEST_ExpectTrue(obj.GetByteByPointer(P("/byte"), 128) == 56);
TEST_ExpectTrue(obj.GetIntByPointer(P("/innerObject/my_int")) == -9823452);
TEST_ExpectTrue(obj
.GetFloatByPointer(P("/innerObject/array/4"), 2.34)
== 56.6);
TEST_ExpectTrue(obj
.GetTextByPointer(P("/innerObject/one more/o rly?")).ToPlainString()
== "ya rly");
Issue("`Get<Type>ByPointer()` methods do not return default value for"
@ "incorrect pointers.");
TEST_ExpectTrue(
obj.GetBoolByPointer(P("/innerObject/array/20"), true)
== true);
TEST_ExpectTrue(obj.GetByteByPointer(P("/byte/"), 128) == 128);
TEST_ExpectTrue(obj.GetIntByPointer(P("/innerObject/my int")) == 0);
TEST_ExpectTrue(obj
.GetFloatByPointer(P("/innerObject/array"), 2.34)
== 2.34);
TEST_ExpectNone(obj.GetTextByPointer(P("")));
}
defaultproperties
{
caseGroup = "Collections"
caseName = "Common methods"
complexJSONObject = "{\"innerObject\":{\"my_bool\":true,\"array\":[\"Engine.Actor\",false,null,{\"something \\\"here\\\"\":\"yes\",\"maybe\":0.003},56.6],\"one more\":{\"no/pe\":324532,\"whatever\":false,\"o rly?\":\"ya rly\"},\"my_int\":-9823452},\"some_var\":-7.32,\"another~1var\":\"aye!\"}"
}

9
sources/Manifest.uc

@ -46,8 +46,9 @@ defaultproperties
testCases(12) = class'TEST_Memory'
testCases(13) = class'TEST_DynamicArray'
testCases(14) = class'TEST_AssociativeArray'
testCases(15) = class'TEST_Iterator'
testCases(16) = class'TEST_Command'
testCases(17) = class'TEST_CommandDataBuilder'
testCases(18) = class'TEST_LogMessage'
testCases(15) = class'TEST_CollectionsMixed'
testCases(16) = class'TEST_Iterator'
testCases(17) = class'TEST_Command'
testCases(18) = class'TEST_CommandDataBuilder'
testCases(19) = class'TEST_LogMessage'
}
Loading…
Cancel
Save