|
|
|
@ -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<MutableText> keys; |
|
|
|
|
var private array<Component> 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() |
|
|
|
|
{ |
|
|
|
|
return components.length == 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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; |
|
|
|
|
local bool hasEscapedSequences; |
|
|
|
|
if (initialized) return false; |
|
|
|
|
if (pointerAsText == none) return false; |
|
|
|
|
for (i = 0; i < components.length; i += 1) { |
|
|
|
|
_.memory.Free(components[i].asText); |
|
|
|
|
} |
|
|
|
|
components.length = 0; |
|
|
|
|
return self; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
initialized = true; |
|
|
|
|
if (!pointerAsText.StartsWith(T(TSLASH)) && !pointerAsText.IsEmpty()) { |
|
|
|
|
keys[0] = pointerAsText.MutableCopy(); |
|
|
|
|
/** |
|
|
|
|
* 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; |
|
|
|
|
} |
|
|
|
|
else |
|
|
|
|
{ |
|
|
|
|
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 |
|
|
|
|
_.memory.Free(keys[0]); |
|
|
|
|
keys.Remove(0, 1); |
|
|
|
|
} |
|
|
|
|
if (!hasEscapedSequences) { |
|
|
|
|
return true; |
|
|
|
|
if (parts[0].IsEmpty()) |
|
|
|
|
{ |
|
|
|
|
_.memory.Free(parts[0]); |
|
|
|
|
parts.Remove(0, 1); |
|
|
|
|
} |
|
|
|
|
if (hasEscapedSequences) |
|
|
|
|
{ |
|
|
|
|
// 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) |
|
|
|
|
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)); |
|
|
|
|
keys[i].Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE)); |
|
|
|
|
nextComponent.asText = parts[i]; |
|
|
|
|
components[components.length] = nextComponent; |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
return self; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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 |
|
|
|
|
* 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 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. |
|
|
|
|
* @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 >= keys.length) return none; |
|
|
|
|
if (keys[index] == none) return none; |
|
|
|
|
return keys[index].Copy(); |
|
|
|
|
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) |
|
|
|
|
{ |
|
|
|
|
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() |
|
|
|
|
{ |
|
|
|
|
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 |
|
|
|
|