From bdf2ca5ff824ddd83bd6974fb22f68900b01f122 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 13 Jul 2021 05:16:53 +0700 Subject: [PATCH] Refactor `JSONPointer` Renames `Segment` into `Component`, allows to "Pop" segments without removing them from the caller `JSONPointer`. --- sources/Data/Collections/Collection.uc | 20 +- .../Tests/TEST_CollectionsMixed.uc | 1 - sources/Text/JSON/JSONAPI.uc | 56 ++- sources/Text/JSON/JSONPointer.uc | 401 +++++++++++++++--- sources/Text/Tests/TEST_JSON.uc | 139 +++++- 5 files changed, 507 insertions(+), 110 deletions(-) diff --git a/sources/Data/Collections/Collection.uc b/sources/Data/Collections/Collection.uc index 7262e9d..eea4e1c 100644 --- a/sources/Data/Collections/Collection.uc +++ b/sources/Data/Collections/Collection.uc @@ -64,11 +64,13 @@ public final function Iter Iterate() * 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. + * Path is treated like a [JSON pointer](https://tools.ietf.org/html/rfc6901). + * If given path does not start with "/" character (like it is expected from + * a json pointer) - it will be added automatically. + * This means that "foo/bar" is treated like "/foo/bar" and + * "path" like "/path". However, empty `Text` is treated like itself (""), + * since it constitutes a valid JSON pointer (it will point at a caller + * collection itself). * * Acedia provides two collections: * 1. `DynamicArray` is treated as a JSON array in the context of @@ -91,9 +93,7 @@ public final function Iter Iterate() * 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. + * @param jsonPointerAsText Path, given by a JSON pointer. * @return An item `jsonPointerAsText` is referring to (according to the above * stated rules). `none` if such item does not exist. */ @@ -112,7 +112,7 @@ public final function AcediaObject GetItemByPointer(Text jsonPointerAsText) nextCollection = self; while (segmentIndex < pointer.GetLength() - 1) { - nextSegment = pointer.GetSegment(segmentIndex); + nextSegment = pointer.GetComponent(segmentIndex); nextCollection = Collection(nextCollection.GetByText(nextSegment)); _.memory.Free(nextSegment); if (nextCollection == none) { @@ -122,7 +122,7 @@ public final function AcediaObject GetItemByPointer(Text jsonPointerAsText) } if (nextCollection != none) { - nextSegment = pointer.GetSegment(segmentIndex); + nextSegment = pointer.GetComponent(segmentIndex); result = nextCollection.GetByText(nextSegment); _.memory.Free(nextSegment); } diff --git a/sources/Data/Collections/Tests/TEST_CollectionsMixed.uc b/sources/Data/Collections/Tests/TEST_CollectionsMixed.uc index 18ea1b9..6af13a6 100644 --- a/sources/Data/Collections/Tests/TEST_CollectionsMixed.uc +++ b/sources/Data/Collections/Tests/TEST_CollectionsMixed.uc @@ -56,7 +56,6 @@ protected static function Test_GetByPointer() 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"))); diff --git a/sources/Text/JSON/JSONAPI.uc b/sources/Text/JSON/JSONAPI.uc index 2a84e47..4555721 100644 --- a/sources/Text/JSON/JSONAPI.uc +++ b/sources/Text/JSON/JSONAPI.uc @@ -64,23 +64,53 @@ private final function InitFormatting() } /** - * 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. + * 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 JSONPointer Pointer(Text pointerAsText) { - local JSONPointer pointer; - pointer = JSONPointer(_.memory.Allocate(class'JSONPointer')); - if (pointer.Initialize(pointerAsText)) { - return pointer; + return JSONPointer(_.memory.Allocate(class'JSONPointer')) + .Set(pointerAsText); +} + +/** + * Checks whether passed `AcediaObject` can be converted into JSON by this API. + * + * Compatible objects are `none` and any object that has one of the following + * classes: `BoolBox`, `BoolRef`, `ByteBox`, `ByteRef`, `IntBox`, `IntRef`, + * `FloatBox`, `FloatRef`, `Text`, `MutableText`, `DynamicArray`, + * `AssociativeArray`. + * + * This method does not check whether objects stored inside `DynamicArray`, + * `AssociativeArray` are compatible. If they are not, they will normally be + * defaulted to JSON null upon any conversion. + */ +public function bool IsCompatible(AcediaObject data) +{ + local class dataClass; + if (data == none) { + return true; } - pointer.FreeSelf(); - return none; + dataClass = data.class; + return dataClass == class'BoolBox' || dataClass == class'BoolRef' + || dataClass == class'ByteBox' || dataClass == class'ByteRef' + || dataClass == class'IntBox' || dataClass == class'IntRef' + || dataClass == class'FloatBox' || dataClass == class'FloatRef' + || dataClass == class'Text' || dataClass == class'MutableText' + || dataClass == class'DynamicArray' + || dataClass == class'AssociativeArray'; } /** diff --git a/sources/Text/JSON/JSONPointer.uc b/sources/Text/JSON/JSONPointer.uc index 6c4d14e..93ecc3c 100644 --- a/sources/Text/JSON/JSONPointer.uc +++ b/sources/Text/JSON/JSONPointer.uc @@ -1,8 +1,10 @@ /** * 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. + * Allows quick and simple access to components of it's path: + * Path "/a/b/c" will be stored as a sequence of components "a", "b" and "c", + * path "/" will be stored as a singular empty component "" + * and empty path "" would mean that there is not components at all. * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. @@ -22,105 +24,366 @@ */ class JSONPointer extends AcediaObject; -var private bool initialized; +// Component of the pointer (the part, separated by slash character '/'). +struct Component +{ + // For arrays, a component is specified by a numeric index; + // To avoid parsing `asText` multiple times we record whether we + // have already done so. + var bool testedForBeingNumeric; + // Numeric index, represented by `asText`; + // `-1` if it was already tested and found to be equal to not be a number + // (valid index values are always `>= 0`). + var int asNumber; + // `Text` representation of the component. + // Can be equal to `none` only if this component was specified via + // numeric index (guarantees `testedForBeingNumeric == true`). + var MutableText asText; +}; // Segments of the path this JSON pointer was initialized with -var private array keys; +var private array components; 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; + Empty(); } /** - * 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. + * 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 Initialize(Text pointerAsText) +public final function bool IsEmpty() { - local int i; - local bool hasEscapedSequences; - if (initialized) return false; - if (pointerAsText == none) return false; + return components.length == 0; +} + - initialized = true; - if (!pointerAsText.StartsWith(T(TSLASH)) && !pointerAsText.IsEmpty()) { - keys[0] = pointerAsText.MutableCopy(); +/** + * 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(Text pointerAsText) +{ + local int i; + local bool hasEscapedSequences; + local Component nextComponent; + local array parts; + Empty(); + if (pointerAsText == none) { + return self; } - else + hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0); + parts = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0)); + // First elements of the array will be empty, so throw it away + if (parts[0].IsEmpty()) { - 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) + _.memory.Free(parts[0]); + parts.Remove(0, 1); + } + if (hasEscapedSequences) { - keys[i].Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH)); - keys[i].Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE)); + // 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) + { + parts[i].Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH)); + parts[i].Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE)); + } + } + for (i = 0; i < parts.length; i += 1) + { + nextComponent.asText = 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(Text newComponent) +{ + local Component newComponentRecord; + if (newComponent == none) { + return self; + } + newComponentRecord.asText = newComponent.MutableCopy(); + components[components.length] = newComponentRecord; + return self; +} + +/** + * Adds new numeric component to the caller `JSONPointer`. + * + * Adding component `7` to the pointer representing path "/a/b/c" would + * result in it representing a path "/a/b/c/7". + * + * @param newComponent Numeric component to add. If passed negative value - + * no changes will be made at all. + * @return Reference to the caller `JSONPointer` to allow for method chaining. + */ +public final function JSONPointer PushNumeric(int newComponent) +{ + local Component newComponentRecord; + if (newComponent < 0) { + return self; + } + newComponentRecord.asNumber = newComponent; + // Obviously this component is going to be numeric + newComponentRecord.testedForBeingNumeric = true; + components[components.length] = newComponentRecord; + 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.FromIntM(components[lastIndex].asNumber); + } + else { + result = components[lastIndex].asText; + } + if (!doNotRemove) { + 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); + _.memory.Free(components[lastIndex].asText); + if (!doNotRemove) { + components.length = components.length - 1; } - return true; + return result; } /** - * 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 + * Returns a component of the path by it's index, starting from `0`. + * + * @param index Index of the component to return. Must be inside * `[0; GetLength() - 1]` segment. - * @return Path's segment as a `Text`. If passed `index` is outside of + * @return Path's component as a `Text`. If passed `index` is outside of * `[0; GetLength() - 1]` segment - returns `none`. */ -public final function Text GetSegment(int index) +public final function Text GetComponent(int index) +{ + if (index < 0) return none; + if (index >= components.length) return none; + + // `asText` will store `none` only if we have added this component as + // numeric one + if (components[index].asText == none) { + components[index].asText = _.text.FromIntM(components[index].asNumber); + } + return components[index].asText.Copy(); +} + +/** + * Returns a numeric component of the path by it's index, starting from `0`. + * + * @param index Index of the component to return. Must be inside + * `[0; GetLength() - 1]` segment and correspond to numeric comonent. + * @return Path's component as a `Text`. If passed `index` is outside of + * `[0; GetLength() - 1]` segment or does not correspond to + * a numeric component - returns `-1`. + */ +public final function int GetNumericComponent(int index) { - if (index < 0) return none; - if (index >= keys.length) return none; - if (keys[index] == none) return none; - return keys[index].Copy(); + 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; } /** - * Amount of path segments in this JSON pointer. + * Converts caller `JSONPointer` into it's `Text` representation. * - * For more details see `GetSegment()`. + * For the method, but returning `MutableText` see `ToTextM()`. * - * @return Amount of segments in the caller `JSONPointer`. + * @return `Text` that represents caller `JSONPointer`. + */ +public final function Text ToText() +{ + local Text result; + local MutableText builder; + builder = ToTextM(); + result = builder.Copy(); + builder.FreeSelf(); + return result; +} + +/** + * Converts caller `JSONPointer` into it's `MutableText` representation. + * + * For the method, but returning `Text` see `ToTextM()`. + * + * @return `MutableText` that represents caller `JSONPointer`. + */ +public final function MutableText ToTextM() +{ + local int i; + local Text nextComponent; + local MutableText nextMutableComponent; + local MutableText result; + result = _.text.Empty(); + if (GetLength() <= 0) { + return result; + } + for (i = 0; i < GetLength(); i += 1) + { + nextComponent = GetComponent(i); + nextMutableComponent = nextComponent.MutableCopy(); + // Replace (order is important) + nextMutableComponent.Replace(T(TJSON_ESCAPE), T(TJSON_ESCAPED_ESCAPE)); + nextMutableComponent.Replace(T(TSLASH), T(TJSON_ESCAPED_SLASH)); + result.Append(T(TSLASH)).Append(nextMutableComponent); + // Get rid of temporary values + nextMutableComponent.FreeSelf(); + nextComponent.FreeSelf(); + } + return result; +} + +/** + * Amount of path components in the caller `JSONPointer`. + * + * Also see `GetFoldsAmount()` method. + * + * @return Amount of components in the caller `JSONPointer`. */ public final function int GetLength() { - return keys.length; + return components.length; +} + +/** + * Amount of path components in the caller `JSONPointer` that do not directly + * correspond to a pointed value. + * + * Equal to the `Max(0, GetLength() - 1)`. + * + * For example, path "/user/Ivan/records/5/count" refers to the value named + * "value" that is _folded_ inside `4` objects named "users", "Ivan", + * "records" and "5". Therefore it's folds amount if `4`. + * + * @return Amount of components in the caller `JSONPointer` that do not + * directly correspond to a pointed value. + */ +public final function int GetFoldsAmount() +{ + return Max(0, components.length - 1); +} + +/** + * Makes an exact copy of the caller `JSONPointer`. + * + * @return Copy of the caller `JSONPointer`. + */ +public final function JSONPointer Copy() +{ + local int i; + local JSONPointer newPointer; + local array newComponents; + newComponents = components; + for (i = 0; i < newComponents.length; i += 1) + { + if (newComponents[i].asText != none) { + newComponents[i].asText = newComponents[i].asText.MutableCopy(); + } + } + newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer')); + newPointer.components = newComponents; + return newPointer; } defaultproperties diff --git a/sources/Text/Tests/TEST_JSON.uc b/sources/Text/Tests/TEST_JSON.uc index c6af9c8..d26e577 100644 --- a/sources/Text/Tests/TEST_JSON.uc +++ b/sources/Text/Tests/TEST_JSON.uc @@ -30,33 +30,138 @@ protected static function TESTS() } protected static function Test_Pointer() +{ + Context("Testing method for working with JSON pointers."); + SubTest_PointerCreate(); + SubTest_PointerToText(); + SubTest_PointerPushPop(); + SubTest_PointerNumeric(); +} + +protected static function SubTest_PointerCreate() { 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)); + TEST_ExpectNone(pointer.GetComponent(0)); pointer = __().json.Pointer(P("/")); TEST_ExpectTrue(pointer.GetLength() == 1); - TEST_ExpectNotNone(pointer.GetSegment(0)); - TEST_ExpectTrue(pointer.GetSegment(0).IsEmpty()); + TEST_ExpectNotNone(pointer.GetComponent(0)); + TEST_ExpectTrue(pointer.GetComponent(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("/a~1b/c%d/e^f//g|h/i\\j/m~0n/")); + TEST_ExpectTrue(pointer.GetLength() == 8); + TEST_ExpectTrue(pointer.GetComponent(0).ToPlainString() == "a/b"); + TEST_ExpectTrue(pointer.GetComponent(1).ToPlainString() == "c%d"); + TEST_ExpectTrue(pointer.GetComponent(2).ToPlainString() == "e^f"); + TEST_ExpectTrue(pointer.GetComponent(3).ToPlainString() == ""); + TEST_ExpectTrue(pointer.GetComponent(4).ToPlainString() == "g|h"); + TEST_ExpectTrue(pointer.GetComponent(5).ToPlainString() == "i\\j"); + TEST_ExpectTrue(pointer.GetComponent(6).ToPlainString() == "m~n"); + TEST_ExpectTrue(pointer.GetComponent(7).ToPlainString() == ""); + + Issue("Initializing JSON pointers with values, not starting with \"/\"," + @ "is 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"); + TEST_ExpectTrue(pointer.GetLength() == 3); + TEST_ExpectTrue(pointer.GetComponent(0).ToPlainString() == "huh"); + TEST_ExpectTrue(pointer.GetComponent(1).ToPlainString() == "send~"); + TEST_ExpectTrue(pointer.GetComponent(2).ToPlainString() == "pics/"); +} + +protected static function SubTest_PointerToText() +{ + local JSONPointer pointer; + Issue("`JSONPointer` is not converted to `Text` correctly."); + pointer = __().json.Pointer(P("")); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == ""); + TEST_ExpectTrue(pointer.ToTextM().ToPlainString() == ""); + pointer = __().json.Pointer(P("///")); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "///"); + TEST_ExpectTrue(pointer.ToTextM().ToPlainString() == "///"); + pointer = __().json.Pointer(P("/a~1b/c%d/e^f//g|h/i\\j/m~0n/")); + TEST_ExpectTrue( pointer.ToText().ToPlainString() + == "/a~1b/c%d/e^f//g|h/i\\j/m~0n/"); + TEST_ExpectTrue( pointer.ToTextM().ToPlainString() + == "/a~1b/c%d/e^f//g|h/i\\j/m~0n/"); + + pointer = __().json.Pointer(P("/a/b/c")); + Issue("Result of `ToText()` has a wrong class."); + TEST_ExpectTrue(pointer.ToText().class == class'Text'); + + Issue("Result of `ToTextM()` has a wrong class."); + TEST_ExpectTrue(pointer.ToTextM().class == class'MutableText'); +} + +protected static function SubTest_PointerPushPop() +{ + local JSONPointer pointer; + local Text value0, value1, value2, value3, value4, value5, value6; + Issue("`Push()`/`PushNumeric()` incorrectly affect `JSONPointer`."); + pointer = __().json.Pointer(P("//lets/go")); + pointer.Push(P("one")).PushNumeric(404).Push(P("More")); + TEST_ExpectTrue( pointer.ToText().ToPlainString() + == "//lets/go/one/404/More"); + + Issue("`Pop()` incorrectly affects `JSONPointer`."); + value6 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "//lets/go/one/404"); + value5 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "//lets/go/one"); + value4 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "//lets/go"); + value3 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "//lets"); + value2 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == "/"); + value1 = pointer.Pop(); + TEST_ExpectTrue(pointer.ToText().ToPlainString() == ""); + value0 = pointer.Pop(); + + Issue("`Pop()` returns incorrect value."); + TEST_ExpectTrue(value6.ToPlainString() == "More"); + TEST_ExpectTrue(value5.ToPlainString() == "404"); + TEST_ExpectTrue(value4.ToPlainString() == "one"); + TEST_ExpectTrue(value3.ToPlainString() == "go"); + TEST_ExpectTrue(value2.ToPlainString() == "lets"); + TEST_ExpectTrue(value1.ToPlainString() == ""); + TEST_ExpectNone(value0); +} + +protected static function SubTest_PointerNumeric() +{ + local JSONPointer pointer; + local string correct, incorrect; + correct = "`GetNumericComponent()`/`PopNumeric()` cannot correctly retrieve" + @ "`JSONPointer`'s numeric components."; + incorrect = "`GetNumericComponent()`/`PopNumeric()` do not return negative" + @ "values for non-numeric components `JSONPointer`'s" + @ "numeric components."; + Issue(correct); + pointer = __().json.Pointer(P("/lets//404/8./6/11/d/0")); + pointer.PushNumeric(-2).PushNumeric(13); + TEST_ExpectTrue(pointer.GetNumericComponent(8) == 13); + Issue(incorrect); + TEST_ExpectTrue(pointer.GetNumericComponent(6) < 0); + Issue(correct); + TEST_ExpectTrue(pointer.PopNumeric() == 13); + TEST_ExpectTrue(pointer.PopNumeric() == 0); + Issue(incorrect); + TEST_ExpectTrue(pointer.PopNumeric() < 0); + Issue(correct); + TEST_ExpectTrue(pointer.PopNumeric() == 11); + TEST_ExpectTrue(pointer.PopNumeric() == 6); + Issue(incorrect); + TEST_ExpectTrue(pointer.PopNumeric() < 0); + Issue(correct); + TEST_ExpectTrue(pointer.PopNumeric() == 404); + Issue(incorrect); + TEST_ExpectTrue(pointer.PopNumeric() < 0); + TEST_ExpectTrue(pointer.PopNumeric() < 0); + TEST_ExpectTrue(pointer.PopNumeric() < 0); + TEST_ExpectTrue(pointer.PopNumeric() < 0); } protected static function Test_Print()