feat: add string segment api to PropertyPath
This resolves the api constraints defined in #5
This commit is contained in:
parent
a7ecf08efb
commit
b1d2b749a4
4 changed files with 308 additions and 25 deletions
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
|
@ -11,7 +12,7 @@ namespace Geekeey.Request.Validation;
|
|||
/// Represents a property path used by validation problems.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(PropertyPathJsonConverter))]
|
||||
public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||
public readonly struct PropertyPath : IEquatable<PropertyPath>, IEquatable<string>, IReadOnlyList<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new property path.
|
||||
|
|
@ -27,6 +28,54 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
|||
/// </summary>
|
||||
public string Value { get; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of path segments.
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
var count = 0;
|
||||
var enumerator = new Enumerator(Value);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the property path contains no segments.
|
||||
/// </summary>
|
||||
public bool IsEmpty => Value.Length is 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the segment at the specified index.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<char> this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
|
||||
var currentIndex = 0;
|
||||
var enumerator = new Enumerator(Value);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (currentIndex == index)
|
||||
{
|
||||
return enumerator.Current;
|
||||
}
|
||||
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(index), index, "The segment index must refer to an existing property path segment.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
@ -55,10 +104,16 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
|||
return string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(string? other)
|
||||
{
|
||||
return string.Equals(Value, other, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is PropertyPath other && Equals(other);
|
||||
return (obj is PropertyPath other && Equals(other)) || (obj is string s && Equals(s));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
|
@ -109,9 +164,48 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
|||
return propertyPath.Value is { Length: > 0 } ? $"{propertyPath}[{index}]" : $"[{index}]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an allocation-free enumerator over the path segments.
|
||||
/// </summary>
|
||||
public Enumerator GetEnumerator()
|
||||
{
|
||||
return new Enumerator(Value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
string IReadOnlyList<string>.this[int index] => this[index].ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator<string> IEnumerable<string>.GetEnumerator()
|
||||
{
|
||||
var enumerator = new Enumerator(Value);
|
||||
|
||||
List<string> segments = [];
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
segments.Add(enumerator.Current.ToString());
|
||||
}
|
||||
|
||||
return segments.GetEnumerator();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
var enumerator = new Enumerator(Value);
|
||||
|
||||
List<string> segments = [];
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
segments.Add(enumerator.Current.ToString());
|
||||
}
|
||||
|
||||
return segments.GetEnumerator();
|
||||
}
|
||||
|
||||
internal string ToJsonName(JsonNamingPolicy? namingPolicy)
|
||||
{
|
||||
var value = Value ?? string.Empty;
|
||||
var value = Value;
|
||||
if (namingPolicy is null || value.Length is 0)
|
||||
{
|
||||
return value;
|
||||
|
|
@ -119,16 +213,15 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
|||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
var first = true;
|
||||
|
||||
foreach (var range in value.AsSpan().Split('.'))
|
||||
var enumerator = new Enumerator(value);
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
builder.Append('.');
|
||||
}
|
||||
|
||||
var segment = value.AsSpan()[range];
|
||||
if (segment.Length is not 0)
|
||||
if (enumerator.Current is { IsEmpty: false } segment)
|
||||
{
|
||||
if (segment.IndexOf('[') is >= 0 and var x)
|
||||
{
|
||||
|
|
@ -146,6 +239,64 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
|||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates path segments without allocations.
|
||||
/// </summary>
|
||||
public ref struct Enumerator
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _value;
|
||||
private int _nextStart;
|
||||
|
||||
internal Enumerator(ReadOnlySpan<char> value)
|
||||
{
|
||||
_value = value;
|
||||
_nextStart = value.Length is 0 ? -1 : 0;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current path segment.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<char> Current { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Advances to the next path segment.
|
||||
/// </summary>
|
||||
public bool MoveNext()
|
||||
{
|
||||
if (_nextStart < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var start = _nextStart;
|
||||
var bracketDepth = 0;
|
||||
|
||||
for (var i = start; i < _value.Length; i++)
|
||||
{
|
||||
switch (_value[i])
|
||||
{
|
||||
case '[':
|
||||
bracketDepth++;
|
||||
break;
|
||||
case ']':
|
||||
bracketDepth = int.Max(bracketDepth - 1, 0);
|
||||
break;
|
||||
case '.' when bracketDepth is 0:
|
||||
Current = _value[start..i];
|
||||
_nextStart = i + 1;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Current = _value[start..];
|
||||
_nextStart = -1;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class PropertyPathJsonConverter : JsonConverter<PropertyPath>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue