Browse Source

Refactor JSON pointer into a separate class

pull/8/head
Anton Tarasenko 4 years ago
parent
commit
44fb393f7a
  1. 12
      sources/Data/Collections/AssociativeArray.uc
  2. 117
      sources/Data/Collections/Collection.uc
  3. 2
      sources/Data/Collections/DynamicArray.uc
  4. 20
      sources/Text/JSON/JSONAPI.uc
  5. 136
      sources/Text/JSON/JSONPointer.uc
  6. 31
      sources/Text/Tests/TEST_JSON.uc

12
sources/Data/Collections/AssociativeArray.uc

@ -544,17 +544,9 @@ public final function Entry GetEntryByIndex(Index index)
return hashTable[index.bucketIndex].entries[index.entryIndex]; return hashTable[index.bucketIndex].entries[index.entryIndex];
} }
protected function AcediaObject GetByText(MutableText key) protected function AcediaObject GetByText(Text key)
{ {
local Text immutableKey; return GetItem(key);
local AcediaObject result;
if (key == none) {
return none;
}
immutableKey = key.Copy();
result = GetItem(immutableKey);
immutableKey.FreeSelf();
return result;
} }
/** /**

117
sources/Data/Collections/Collection.uc

@ -22,40 +22,21 @@
class Collection extends AcediaObject class Collection extends AcediaObject
abstract; 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 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. * Method that must be overloaded for `GetItemByPointer()` to properly work.
* *
* This method must return an item that `key` refers to with it's * This method must return an item that `key` refers to with it's
* textual content (not as an object itself). * textual content (not as an object itself).
* For example, `DynamicArray` parses it into unsigned number, while * For example, `DynamicArray` parses it into unsigned number, while
* `AssociativeArray` converts it into an immutable `Text` key, whose hash code * `AssociativeArray` uses it as a key directly.
* depends on the contents.
* *
* There is no requirement that all stored values must be reachable by * There is no requirement that all stored values must be reachable by
* this method (i.e. `AssociativeArray` only lets you access values with * this method (i.e. `AssociativeArray` only lets you access values with
* `Text` keys). * `Text` keys).
*/ */
protected function AcediaObject GetByText(MutableText key); protected function AcediaObject GetByText(Text key);
/** /**
* Creates an `Iterator` instance to iterate over stored items. * Creates an `Iterator` instance to iterate over stored items.
@ -79,61 +60,6 @@ public final function Iter Iterate()
return newIterator; 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 * Returns stored `AcediaObject` from the caller storage
* (or from it's sub-storages) via given `Text` path. * (or from it's sub-storages) via given `Text` path.
@ -173,27 +99,34 @@ private final function FreePointer(out JSONPointer ptr)
*/ */
public final function AcediaObject GetItemByPointer(Text jsonPointerAsText) public final function AcediaObject GetItemByPointer(Text jsonPointerAsText)
{ {
local int segmentIndex;
local Text nextSegment;
local AcediaObject result; local AcediaObject result;
local JSONPointer ptr; local JSONPointer pointer;
local Collection nextCollection; local Collection nextCollection;
if (jsonPointerAsText == none) return none; if (jsonPointerAsText == none) return none;
if (jsonPointerAsText.IsEmpty()) return self; if (jsonPointerAsText.IsEmpty()) return self;
pointer = _.json.Pointer(jsonPointerAsText);
if (jsonPointerAsText.GetLength() < 1) return self;
if (!MakePointer(jsonPointerAsText, ptr)) {
return none;
}
nextCollection = self; nextCollection = self;
while (!IsFinalPointerKey(ptr)) while (segmentIndex < pointer.GetLength() - 1)
{
nextCollection = Collection(nextCollection.GetByText(PopJSONKey(ptr)));
if (nextCollection == none)
{ {
FreePointer(ptr); nextSegment = pointer.GetSegment(segmentIndex);
return none; nextCollection = Collection(nextCollection.GetByText(nextSegment));
_.memory.Free(nextSegment);
if (nextCollection == none) {
break;
}
segmentIndex += 1;
} }
if (nextCollection != none)
{
nextSegment = pointer.GetSegment(segmentIndex);
result = nextCollection.GetByText(nextSegment);
_.memory.Free(nextSegment);
} }
result = nextCollection.GetByText(PopJSONKey(ptr)); _.memory.Free(pointer);
FreePointer(ptr);
return result; return result;
} }
@ -409,12 +342,4 @@ public final function DynamicArray GetDynamicArrayByPointer(
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"
} }

2
sources/Data/Collections/DynamicArray.uc

@ -474,7 +474,7 @@ public final function int Find(AcediaObject item)
return -1; return -1;
} }
protected function AcediaObject GetByText(MutableText key) protected function AcediaObject GetByText(Text key)
{ {
local int index, consumed; local int index, consumed;
local Parser parser; local Parser parser;

20
sources/Text/JSON/JSONAPI.uc

@ -63,6 +63,26 @@ private final function InitFormatting()
jNull = _.text.FormattingFromColor(_.color.jNull); jNull = _.text.FormattingFromColor(_.color.jNull);
} }
/**
* Creates new `JSONPointer` from a given text representation `pointerAsText`.
*
* @param pointerAsText 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 (without resolving
* escaped sequences "~0" and "~1").
* @return `JSONPointer` if passed `Text` was not `none`. `none` otherwise.
*/
public final function JSONPointer Pointer(Text pointerAsText)
{
local JSONPointer pointer;
pointer = JSONPointer(_.memory.Allocate(class'JSONPointer'));
if (pointer.Initialize(pointerAsText)) {
return pointer;
}
pointer.FreeSelf();
return none;
}
/** /**
* Uses given parser to parse a null JSON value ("null" in arbitrary case). * Uses given parser to parse a null JSON value ("null" in arbitrary case).
* *

136
sources/Text/JSON/JSONPointer.uc

@ -0,0 +1,136 @@
/**
* Class for representing a JSON pointer (see
* https://tools.ietf.org/html/rfc6901).
* Allows quick and simple access to parts/segments of it's path.
* Objects of this class should only be used after initialization.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class JSONPointer extends AcediaObject;
var private bool initialized;
// Segments of the path this JSON pointer was initialized with
var private array<MutableText> keys;
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE;
protected function Finalizer()
{
_.memory.FreeMany(keys);
keys.length = 0;
initialized = false;
}
/**
* Initializes caller `JSONPointer` with a given path.
*
* @param pointerAsText 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 (without resolving
* escaped sequences "~0" and "~1").
* @return `true` if caller `JSONPointer` was correctly initialized with this
* call. `false` otherwise: can happen if `none` was passed as a parameter
* or caller `JSONPointer` was already initialized.
*/
public final function bool Initialize(Text pointerAsText)
{
local int i;
local bool hasEscapedSequences;
if (initialized) return false;
if (pointerAsText == none) return false;
initialized = true;
if (!pointerAsText.StartsWith(T(TSLASH)) && !pointerAsText.IsEmpty()) {
keys[0] = pointerAsText.MutableCopy();
}
else
{
hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0);
keys = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0));
// First elements of the array will be empty, so throw it away
_.memory.Free(keys[0]);
keys.Remove(0, 1);
}
if (!hasEscapedSequences) {
return true;
}
// 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 < keys.length; i += 1)
{
keys[i].Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH));
keys[i].Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE));
}
return true;
}
/**
* Returns a segment of the path by it's index.
*
* For path "/a/b/c":
* `GetSegment(0) == "a"`
* `GetSegment(1) == "b"`
* `GetSegment(2) == "c"`
* `GetSegment(3) == none`
* For path "/":
* `GetSegment(0) == ""`
* `GetSegment(1) == none`
* For path "":
* `GetSegment(0) == none`
* For path "abc":
* `GetSegment(0) == "abc"`
* `GetSegment(1) == none`
*
* @param index Index of the segment to return. Must be inside
* `[0; GetLength() - 1]` segment.
* @return Path's segment as a `Text`. If passed `index` is outside of
* `[0; GetLength() - 1]` segment - returns `none`.
*/
public final function Text GetSegment(int index)
{
if (index < 0) return none;
if (index >= keys.length) return none;
if (keys[index] == none) return none;
return keys[index].Copy();
}
/**
* Amount of path segments in this JSON pointer.
*
* For more details see `GetSegment()`.
*
* @return Amount of segments in the caller `JSONPointer`.
*/
public final function int GetLength()
{
return keys.length;
}
defaultproperties
{
TSLASH = 0
stringConstants(0) = "/"
TJSON_ESCAPE = 1
stringConstants(1) = "~"
TJSON_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
}

31
sources/Text/Tests/TEST_JSON.uc

@ -24,10 +24,41 @@ var string simpleJSONObject, complexJSONObject;
protected static function TESTS() protected static function TESTS()
{ {
Test_Pointer();
Test_Print(); Test_Print();
Test_Parse(); Test_Parse();
} }
protected static function Test_Pointer()
{
local JSONPointer pointer;
Context("Testing JSON pointer.");
Issue("\"Empty\" JSON pointers are not handled correctly.");
pointer = __().json.Pointer(P(""));
TEST_ExpectTrue(pointer.GetLength() == 0);
TEST_ExpectNone(pointer.GetSegment(0));
pointer = __().json.Pointer(P("/"));
TEST_ExpectTrue(pointer.GetLength() == 1);
TEST_ExpectNotNone(pointer.GetSegment(0));
TEST_ExpectTrue(pointer.GetSegment(0).IsEmpty());
Issue("Normal JSON pointers are not handled correctly.");
pointer = __().json.Pointer(P("/a~1b/c%d/e^f/g|h/i\\j/m~0n"));
TEST_ExpectTrue(pointer.GetLength() == 6);
TEST_ExpectTrue(pointer.GetSegment(0).ToPlainString() == "a/b");
TEST_ExpectTrue(pointer.GetSegment(1).ToPlainString() == "c%d");
TEST_ExpectTrue(pointer.GetSegment(2).ToPlainString() == "e^f");
TEST_ExpectTrue(pointer.GetSegment(3).ToPlainString() == "g|h");
TEST_ExpectTrue(pointer.GetSegment(4).ToPlainString() == "i\\j");
TEST_ExpectTrue(pointer.GetSegment(5).ToPlainString() == "m~n");
Issue("Non-JSON pointers `Text` constants are not handled correctly.");
pointer = __().json.Pointer(P("huh/send~0/pics~1"));
TEST_ExpectTrue(pointer.GetLength() == 1);
TEST_ExpectTrue( pointer.GetSegment(0).ToPlainString()
== "huh/send~0/pics~1");
}
protected static function Test_Print() protected static function Test_Print()
{ {
Context("Testing printing simple JSON values."); Context("Testing printing simple JSON values.");

Loading…
Cancel
Save