You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
534 lines
18 KiB
534 lines
18 KiB
/** |
|
* 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-2023 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 <https://www.gnu.org/licenses/>. |
|
*/ |
|
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<Component> 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<BaseText> 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; |
|
} |
|
|
|
/** |
|
* Checks whether component at given index can be used to index array. |
|
* |
|
* This method accepts numeric components plus component equal to "-", that can |
|
* be used to point at the element after the last on in the `JSONArray`. |
|
* |
|
* @param index Index of the component to check. |
|
* @param `true` if component with given index exists and it either positive |
|
* number or "-". |
|
*/ |
|
public final function bool IsComponentArrayApplicable(int index) |
|
{ |
|
local bool isAddElementAlias; |
|
local Text component; |
|
|
|
if (GetNumericComponent(index) >= 0) { |
|
return true; |
|
} |
|
component = GetComponent(index); |
|
isAddElementAlias = P("-").IsEqual(component); |
|
_.memory.Free(component); |
|
return isAddElementAlias; |
|
} |
|
|
|
/** |
|
* 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<Component> 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; |
|
} |
|
|
|
/** |
|
* Appends path, contained in JSON pointer `other` to the caller JSON pointer. |
|
* Appending "/A/B/7/C" to "/object/hey/1/there/" produces |
|
* "/object/hey/1/there//A/B/7/C". |
|
* |
|
* @param other Pointer to append. If `none` - caller `JSONPointer` will |
|
* not change. |
|
* @return Reference to the caller `JSONPointer` to allow for method chaining. |
|
*/ |
|
public final function JSONPointer Append(JSONPointer other) |
|
{ |
|
local int i; |
|
local array<Component> otherComponents; |
|
|
|
if (other == none) { |
|
return self; |
|
} |
|
otherComponents = other.components; |
|
for (i = 0; i < otherComponents.length; i += 1) |
|
{ |
|
if (otherComponents[i].asText != none) { |
|
otherComponents[i].asText = otherComponents[i].asText.MutableCopy(); |
|
} |
|
components[components.length] = otherComponents[i]; |
|
} |
|
return self; |
|
} |
|
|
|
/** |
|
* Checks if given pointer corresponds with the beginning of the caller one. |
|
* |
|
* Pointer starts with another one if it includes all of its fields from |
|
* the beginning and in order |
|
* E.g. "/A/B/C" starts with "/A/B", but not with "/A/B/C/D", "/D/A/B/C" or |
|
* "/A/B/CD". |
|
* |
|
* @param other Candidate into being caller pointer's prefix. |
|
* @return `true` if `other` is prefix and `false` otherwise. `none` is |
|
* considered to be an empty pointer and, therefore, prefix to any other |
|
* pointer. |
|
*/ |
|
public final function bool StartsWith(JSONPointer other) |
|
{ |
|
local int i; |
|
local array<Component> otherComponents; |
|
|
|
// `none` is same as empty |
|
if (other == none) return true; |
|
otherComponents = other.components; |
|
// Not enough length |
|
if (components.length < otherComponents.length) return false; |
|
|
|
for (i = 0; i < otherComponents.length; i += 1) |
|
{ |
|
// Compare numeric components if at least one is such |
|
if ( components[i].testedForBeingNumeric |
|
|| otherComponents[i].testedForBeingNumeric) |
|
{ |
|
if (GetNumericComponent(i) != other.GetNumericComponent(i)) { |
|
return false; |
|
} |
|
// End this iteration for numeric component, but continue for |
|
// text ones |
|
if (GetNumericComponent(i) >= 0) { |
|
continue; |
|
} |
|
} |
|
// We can reach here if: |
|
// 1. Neither components have `testedForBeingNumeric` set to |
|
// `true`, neither `asText` fields are `none` by the invariant; |
|
// 2. At least one had `testedForBeingNumeric`, but they tested |
|
// negative for being numeric. |
|
if (!components[i].asText.Compare(otherComponents[i].asText)) { |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
defaultproperties |
|
{ |
|
TSLASH = 0 |
|
stringConstants(0) = "/" |
|
TJSON_ESCAPE = 1 |
|
stringConstants(1) = "~" |
|
TJSON_ESCAPED_SLASH = 2 |
|
stringConstants(2) = "~1" |
|
TJSON_ESCAPED_ESCAPE = 3 |
|
stringConstants(3) = "~0" |
|
} |