/**
* Class for representing a JSON pointer (see
* https://tools.ietf.org/html/rfc6901).
* 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.
*
* 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;
// 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 components;
var protected const int TSLASH, TJSON_ESCAPE, TJSON_ESCAPED_SLASH;
var protected const int TJSON_ESCAPED_ESCAPE;
protected function Finalizer()
{
Empty();
}
/**
* 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 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;
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(BaseText pointerAsText)
{
local int i;
local bool hasEscapedSequences;
local Component nextComponent;
local MutableText nextPart;
local array parts;
Empty();
if (pointerAsText == none) {
return self;
}
hasEscapedSequences = (pointerAsText.IndexOf(T(TJSON_ESCAPE)) >= 0);
parts = pointerAsText.SplitByCharacter(T(TSLASH).GetCharacter(0),, true);
// First element of the array is expected to be empty, so throw it away;
// If it is not empty - then `pointerAsText` does not start with "/" and
// we will pretend that we have already removed first element, thus
// "fixing" path (e.g. effectively turning "foo/bar" into "/foo/bar").
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 < parts.length; i += 1)
{
nextPart = MutableText(parts[i]);
nextPart.Replace(T(TJSON_ESCAPED_SLASH), T(TSLASH));
nextPart.Replace(T(TJSON_ESCAPED_ESCAPE), T(TJSON_ESCAPE));
}
}
for (i = 0; i < parts.length; i += 1)
{
nextComponent.asText = MutableText(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(BaseText 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.FromInt(components[lastIndex].asNumber);
}
else {
result = components[lastIndex].asText.Copy();
}
if (!doNotRemove)
{
_.memory.Free(components[lastIndex].asText);
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);
if (!doNotRemove)
{
_.memory.Free(components[lastIndex].asText);
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 component as a `Text`. If passed `index` is outside of
* `[0; GetLength() - 1]` segment - returns `none`.
*/
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)
{
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 components in the caller `JSONPointer`.
*
* Also see `GetFoldsAmount()` method.
*
* @return Amount of components in the caller `JSONPointer`.
*/
public final function int GetLength()
{
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`.
*
* Copies components in the range `[startIndex; startIndex + maxLength - 1]`
* If provided parameters `startIndex` and `maxLength` define a range that
* goes beyond `[0; self.GetLength() - 1]`, then intersection with a valid
* range will be used.
*
* @param startIndex Position of the first component to copy.
* By default `0`, corresponding to the very first component.
* @param maxLength Max length of the extracted JSON pointer (in amount of
* components). By default `0` - that and all negative values mean that
* method should extract all components to the right of `startIndex`.
* @return Copy of the specified range of the caller `JSONPointer`.
*/
public final function JSONPointer Copy(
optional int startIndex,
optional int maxLength)
{
local int i, endIndex;
local JSONPointer newPointer;
local array newComponents;
if (maxLength <= 0) {
maxLength = components.length - startIndex;
}
endIndex = startIndex + maxLength;
if (endIndex <= 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
}
startIndex = Max(startIndex, 0);
endIndex = Min(endIndex, components.length);
for (i = startIndex; i < endIndex; i += 1)
{
newComponents[newComponents.length] = components[i];
if (components[i].asText != none)
{
newComponents[newComponents.length - 1].asText =
components[i].asText.MutableCopy();
}
}
newPointer = JSONPointer(_.memory.Allocate(class'JSONPointer'));
newPointer.components = newComponents;
return newPointer;
}
defaultproperties
{
TSLASH = 0
stringConstants(0) = "/"
TJSON_ESCAPE = 1
stringConstants(1) = "~"
TJSON_ESCAPED_SLASH = 2
stringConstants(2) = "~1"
TJSON_ESCAPED_ESCAPE = 3
stringConstants(3) = "~0"
}