Browse Source

Refactor `JSONPointer`

Renames `Segment` into `Component`, allows to "Pop" segments without
removing them from the caller `JSONPointer`.
pull/8/head
Anton Tarasenko 3 years ago
parent
commit
bdf2ca5ff8
  1. 20
      sources/Data/Collections/Collection.uc
  2. 1
      sources/Data/Collections/Tests/TEST_CollectionsMixed.uc
  3. 56
      sources/Text/JSON/JSONAPI.uc
  4. 385
      sources/Text/JSON/JSONPointer.uc
  5. 139
      sources/Text/Tests/TEST_JSON.uc

20
sources/Data/Collections/Collection.uc

@ -64,11 +64,13 @@ public final function Iter Iterate()
* 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.
* *
* Path is used in one of the two ways: * Path is treated like a [JSON pointer](https://tools.ietf.org/html/rfc6901).
* 1. If path is an empty `Text` or if it starts with "/" character, * If given path does not start with "/" character (like it is expected from
* it will be interpreted as * a json pointer) - it will be added automatically.
* a [JSON pointer](https://tools.ietf.org/html/rfc6901); * This means that "foo/bar" is treated like "/foo/bar" and
* 2. Otherwise it will be used as an argument's name. * "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: * Acedia provides two collections:
* 1. `DynamicArray` is treated as a JSON array in the context of * 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 * this method (i.e. `AssociativeArray` only lets you access values with
* `Text` keys). * `Text` keys).
* *
* @param jsonPointerAsText Treated as a JSON pointer if it starts with "/" * @param jsonPointerAsText Path, given by a JSON pointer.
* 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 * @return An item `jsonPointerAsText` 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.
*/ */
@ -112,7 +112,7 @@ public final function AcediaObject GetItemByPointer(Text jsonPointerAsText)
nextCollection = self; nextCollection = self;
while (segmentIndex < pointer.GetLength() - 1) while (segmentIndex < pointer.GetLength() - 1)
{ {
nextSegment = pointer.GetSegment(segmentIndex); nextSegment = pointer.GetComponent(segmentIndex);
nextCollection = Collection(nextCollection.GetByText(nextSegment)); nextCollection = Collection(nextCollection.GetByText(nextSegment));
_.memory.Free(nextSegment); _.memory.Free(nextSegment);
if (nextCollection == none) { if (nextCollection == none) {
@ -122,7 +122,7 @@ public final function AcediaObject GetItemByPointer(Text jsonPointerAsText)
} }
if (nextCollection != none) if (nextCollection != none)
{ {
nextSegment = pointer.GetSegment(segmentIndex); nextSegment = pointer.GetComponent(segmentIndex);
result = nextCollection.GetByText(nextSegment); result = nextCollection.GetByText(nextSegment);
_.memory.Free(nextSegment); _.memory.Free(nextSegment);
} }

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

@ -56,7 +56,6 @@ protected static function Test_GetByPointer()
DynamicArray(obj.GetItemByPointer(P("/innerObject/array")))); DynamicArray(obj.GetItemByPointer(P("/innerObject/array"))));
Issue("`GetItemByPointer()` does not return `none` for incorrect pointers"); 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("//")));
TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/5"))); TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/5")));
TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/-1"))); TEST_ExpectNone(obj.GetItemByPointer(P("/innerObject/array/-1")));

56
sources/Text/JSON/JSONAPI.uc

@ -64,23 +64,53 @@ private final function InitFormatting()
} }
/** /**
* Creates new `JSONPointer` from a given text representation `pointerAsText`. * Creates new `JSONPointer`, corresponding to a given path in
* * JSON pointer format (https://tools.ietf.org/html/rfc6901).
* @param pointerAsText Treated as a JSON pointer if it starts with "/" *
* character or is an empty `Text`, otherwise treated as an item's * If provided `Text` value is an incorrect pointer, then it will be
* name / identificator inside the caller collection (without resolving * treated like an empty pointer.
* escaped sequences "~0" and "~1"). * However, if given pointer can be fixed by prepending "/" - it will be
* @return `JSONPointer` if passed `Text` was not `none`. `none` otherwise. * 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) public final function JSONPointer Pointer(Text pointerAsText)
{ {
local JSONPointer pointer; return JSONPointer(_.memory.Allocate(class'JSONPointer'))
pointer = JSONPointer(_.memory.Allocate(class'JSONPointer')); .Set(pointerAsText);
if (pointer.Initialize(pointerAsText)) {
return pointer;
} }
pointer.FreeSelf();
return none; /**
* 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<AcediaObject> dataClass;
if (data == none) {
return true;
}
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';
} }
/** /**

385
sources/Text/JSON/JSONPointer.uc

@ -1,8 +1,10 @@
/** /**
* Class for representing a JSON pointer (see * Class for representing a JSON pointer (see
* https://tools.ietf.org/html/rfc6901). * https://tools.ietf.org/html/rfc6901).
* Allows quick and simple access to parts/segments of it's path. * Allows quick and simple access to components of it's path:
* Objects of this class should only be used after initialization. * 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 * Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------ *------------------------------------------------------------------------------
* This file is part of Acedia. * This file is part of Acedia.
@ -22,105 +24,366 @@
*/ */
class JSONPointer extends AcediaObject; 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 // Segments of the path this JSON pointer was initialized with
var private array<MutableText> keys; var private array<Component> components;
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH; var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE; var protected const int TJSON_ESCAPED_ESCAPE;
protected function Finalizer() protected function Finalizer()
{ {
_.memory.FreeMany(keys); Empty();
keys.length = 0;
initialized = false;
} }
/** /**
* Initializes caller `JSONPointer` with a given path. * Checks whether caller `JSONPointer` is empty (points at the root value).
* *
* @param pointerAsText Treated as a JSON pointer if it starts with "/" * @return `true` iff caller `JSONPointer` points at the root value.
* 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) public final function bool IsEmpty()
{ {
local int i; return components.length == 0;
local bool hasEscapedSequences; }
if (initialized) return false;
if (pointerAsText == none) return false;
initialized = true; /**
if (!pointerAsText.StartsWith(T(TSLASH)) && !pointerAsText.IsEmpty()) { * Resets caller `JSONPointer`, erasing all of it's components.
keys[0] = pointerAsText.MutableCopy(); *
* @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;
} }
else
/**
* 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<MutableText> parts;
Empty();
if (pointerAsText == none) {
return self;
}
hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0); hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0);
keys = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0)); parts = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0));
// First elements of the array will be empty, so throw it away // First elements of the array will be empty, so throw it away
_.memory.Free(keys[0]); if (parts[0].IsEmpty())
keys.Remove(0, 1); {
} _.memory.Free(parts[0]);
if (!hasEscapedSequences) { parts.Remove(0, 1);
return true;
} }
if (hasEscapedSequences)
{
// Replace escaped sequences "~0" and "~1". // Replace escaped sequences "~0" and "~1".
// Order is specific, necessity of which is explained in // Order is specific, necessity of which is explained in
// JSON Pointer's documentation: // JSON Pointer's documentation:
// https://tools.ietf.org/html/rfc6901 // https://tools.ietf.org/html/rfc6901
for (i = 0; i < keys.length; i += 1) 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)
{ {
keys[i].Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH)); nextComponent.asText = parts[i];
keys[i].Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE)); components[components.length] = nextComponent;
} }
return true; return self;
} }
/** /**
* Returns a segment of the path by it's index. * Adds new component to the caller `JSONPointer`.
* *
* For path "/a/b/c": * Adding component "new" to the pointer representing path "/a/b/c" would
* `GetSegment(0) == "a"` * result in it representing a path "/a/b/c/new".
* `GetSegment(1) == "b"` *
* `GetSegment(2) == "c"` * Although this method can be used to add numeric components, `PushNumeric()`
* `GetSegment(3) == none` * should be used for that if possible.
* For path "/": *
* `GetSegment(0) == ""` * @param newComponent Component to add. If passed `none` value -
* `GetSegment(1) == none` * no changes will be made at all.
* For path "": * @return Reference to the caller `JSONPointer` to allow for method chaining.
* `GetSegment(0) == none` */
* For path "abc": public final function JSONPointer Push(Text newComponent)
* `GetSegment(0) == "abc"` {
* `GetSegment(1) == none` local Component newComponentRecord;
* if (newComponent == none) {
* @param index Index of the segment to return. Must be inside 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 result;
}
/**
* 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. * `[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`. * `[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 < 0) return none;
if (index >= keys.length) return none; if (index >= components.length) return none;
if (keys[index] == none) return none;
return keys[index].Copy(); // `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)
{
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;
}
/**
* 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;
}
/**
* 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 segments in this JSON pointer. * Amount of path components in the caller `JSONPointer`.
* *
* For more details see `GetSegment()`. * Also see `GetFoldsAmount()` method.
* *
* @return Amount of segments in the caller `JSONPointer`. * @return Amount of components in the caller `JSONPointer`.
*/ */
public final function int GetLength() 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<Component> 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 defaultproperties

139
sources/Text/Tests/TEST_JSON.uc

@ -30,33 +30,138 @@ protected static function TESTS()
} }
protected static function Test_Pointer() 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; local JSONPointer pointer;
Context("Testing JSON pointer.");
Issue("\"Empty\" JSON pointers are not handled correctly."); Issue("\"Empty\" JSON pointers are not handled correctly.");
pointer = __().json.Pointer(P("")); pointer = __().json.Pointer(P(""));
TEST_ExpectTrue(pointer.GetLength() == 0); TEST_ExpectTrue(pointer.GetLength() == 0);
TEST_ExpectNone(pointer.GetSegment(0)); TEST_ExpectNone(pointer.GetComponent(0));
pointer = __().json.Pointer(P("/")); pointer = __().json.Pointer(P("/"));
TEST_ExpectTrue(pointer.GetLength() == 1); TEST_ExpectTrue(pointer.GetLength() == 1);
TEST_ExpectNotNone(pointer.GetSegment(0)); TEST_ExpectNotNone(pointer.GetComponent(0));
TEST_ExpectTrue(pointer.GetSegment(0).IsEmpty()); TEST_ExpectTrue(pointer.GetComponent(0).IsEmpty());
Issue("Normal JSON pointers are not handled correctly."); Issue("Normal JSON pointers are not handled correctly.");
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.GetLength() == 6); TEST_ExpectTrue(pointer.GetLength() == 8);
TEST_ExpectTrue(pointer.GetSegment(0).ToPlainString() == "a/b"); TEST_ExpectTrue(pointer.GetComponent(0).ToPlainString() == "a/b");
TEST_ExpectTrue(pointer.GetSegment(1).ToPlainString() == "c%d"); TEST_ExpectTrue(pointer.GetComponent(1).ToPlainString() == "c%d");
TEST_ExpectTrue(pointer.GetSegment(2).ToPlainString() == "e^f"); TEST_ExpectTrue(pointer.GetComponent(2).ToPlainString() == "e^f");
TEST_ExpectTrue(pointer.GetSegment(3).ToPlainString() == "g|h"); TEST_ExpectTrue(pointer.GetComponent(3).ToPlainString() == "");
TEST_ExpectTrue(pointer.GetSegment(4).ToPlainString() == "i\\j"); TEST_ExpectTrue(pointer.GetComponent(4).ToPlainString() == "g|h");
TEST_ExpectTrue(pointer.GetSegment(5).ToPlainString() == "m~n"); TEST_ExpectTrue(pointer.GetComponent(5).ToPlainString() == "i\\j");
TEST_ExpectTrue(pointer.GetComponent(6).ToPlainString() == "m~n");
Issue("Non-JSON pointers `Text` constants are not handled correctly."); 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")); pointer = __().json.Pointer(P("huh/send~0/pics~1"));
TEST_ExpectTrue(pointer.GetLength() == 1); TEST_ExpectTrue(pointer.GetLength() == 3);
TEST_ExpectTrue( pointer.GetSegment(0).ToPlainString() TEST_ExpectTrue(pointer.GetComponent(0).ToPlainString() == "huh");
== "huh/send~0/pics~1"); 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() protected static function Test_Print()

Loading…
Cancel
Save