diff --git a/sources/Data/Collections/AssociativeArray.uc b/sources/Data/Collections/AssociativeArray.uc index 1515d64..d01ecad 100644 --- a/sources/Data/Collections/AssociativeArray.uc +++ b/sources/Data/Collections/AssociativeArray.uc @@ -544,17 +544,9 @@ public final function Entry GetEntryByIndex(Index index) return hashTable[index.bucketIndex].entries[index.entryIndex]; } -protected function AcediaObject GetByText(MutableText key) +protected function AcediaObject GetByText(Text key) { - local Text immutableKey; - local AcediaObject result; - if (key == none) { - return none; - } - immutableKey = key.Copy(); - result = GetItem(immutableKey); - immutableKey.FreeSelf(); - return result; + return GetItem(key); } /** diff --git a/sources/Data/Collections/Collection.uc b/sources/Data/Collections/Collection.uc index 470f7bf..7262e9d 100644 --- a/sources/Data/Collections/Collection.uc +++ b/sources/Data/Collections/Collection.uc @@ -22,40 +22,21 @@ 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 keys; - // Points at a part in `keys` to be used next. - var private int nextIndex; -}; - var class 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. + * `AssociativeArray` uses it as a key directly. * * 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); +protected function AcediaObject GetByText(Text key); /** * Creates an `Iterator` instance to iterate over stored items. @@ -79,61 +60,6 @@ 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. @@ -173,27 +99,34 @@ private final function FreePointer(out JSONPointer ptr) */ public final function AcediaObject GetItemByPointer(Text jsonPointerAsText) { + local int segmentIndex; + local Text nextSegment; local AcediaObject result; - local JSONPointer ptr; + local JSONPointer pointer; local Collection nextCollection; - if (jsonPointerAsText == none) return none; - if (jsonPointerAsText.IsEmpty()) return self; + if (jsonPointerAsText == none) return none; + if (jsonPointerAsText.IsEmpty()) return self; + pointer = _.json.Pointer(jsonPointerAsText); + if (jsonPointerAsText.GetLength() < 1) return self; - if (!MakePointer(jsonPointerAsText, ptr)) { - return none; - } nextCollection = self; - while (!IsFinalPointerKey(ptr)) + while (segmentIndex < pointer.GetLength() - 1) { - nextCollection = Collection(nextCollection.GetByText(PopJSONKey(ptr))); - if (nextCollection == none) - { - FreePointer(ptr); - return none; + nextSegment = pointer.GetSegment(segmentIndex); + 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)); - FreePointer(ptr); + _.memory.Free(pointer); return result; } @@ -409,12 +342,4 @@ public final function DynamicArray GetDynamicArrayByPointer( defaultproperties { - TSLASH = 0 - stringConstants(0) = "/" - TJSON_ESCAPE = 1 - stringConstants(1) = "~" - TJSON_ESCAPED_SLASH = 2 - stringConstants(2) = "~1" - TJSON_ESCAPED_ESCAPE = 3 - stringConstants(3) = "~0" } \ No newline at end of file diff --git a/sources/Data/Collections/DynamicArray.uc b/sources/Data/Collections/DynamicArray.uc index dbe2f15..34cb7d5 100644 --- a/sources/Data/Collections/DynamicArray.uc +++ b/sources/Data/Collections/DynamicArray.uc @@ -474,7 +474,7 @@ public final function int Find(AcediaObject item) return -1; } -protected function AcediaObject GetByText(MutableText key) +protected function AcediaObject GetByText(Text key) { local int index, consumed; local Parser parser; diff --git a/sources/Text/JSON/JSONAPI.uc b/sources/Text/JSON/JSONAPI.uc index f169f2e..2a84e47 100644 --- a/sources/Text/JSON/JSONAPI.uc +++ b/sources/Text/JSON/JSONAPI.uc @@ -63,6 +63,26 @@ private final function InitFormatting() 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). * diff --git a/sources/Text/JSON/JSONPointer.uc b/sources/Text/JSON/JSONPointer.uc new file mode 100644 index 0000000..6c4d14e --- /dev/null +++ b/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 . + */ +class JSONPointer extends AcediaObject; + +var private bool initialized; +// Segments of the path this JSON pointer was initialized with +var private array 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" +} \ No newline at end of file diff --git a/sources/Text/Tests/TEST_JSON.uc b/sources/Text/Tests/TEST_JSON.uc index b8450d5..c6af9c8 100644 --- a/sources/Text/Tests/TEST_JSON.uc +++ b/sources/Text/Tests/TEST_JSON.uc @@ -24,10 +24,41 @@ var string simpleJSONObject, complexJSONObject; protected static function TESTS() { + Test_Pointer(); Test_Print(); 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() { Context("Testing printing simple JSON values.");