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
|
|
@ -40,12 +40,144 @@ internal sealed class PropertyPathTests
|
||||||
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"members[1].name","severity":0,"message":"Member name is required.","code":null,"attemptedValue":null}""");
|
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"members[1].name","severity":0,"message":"Member name is required.","code":null,"attemptedValue":null}""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_serialize_paths_with_multiple_indexers_using_the_json_naming_policy()
|
||||||
|
{
|
||||||
|
var problem = new Problem
|
||||||
|
{
|
||||||
|
PropertyPath = "Matrix[1][2].Value",
|
||||||
|
Message = "Value is required.",
|
||||||
|
};
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(problem, CamelCaseJsonOptions);
|
||||||
|
|
||||||
|
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ """{"propertyPath":"matrix[1][2].value","severity":0,"message":"Value is required.","code":null,"attemptedValue":null}""");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_iterate_and_index_segments()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Members[1].Name";
|
||||||
|
var segments = (IReadOnlyList<string>)propertyPath;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(propertyPath.IsEmpty).IsFalse();
|
||||||
|
await Assert.That(segments.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments[0]).IsEqualTo("Members[1]");
|
||||||
|
await Assert.That(segments[1]).IsEqualTo("Name");
|
||||||
|
await Assert.That(propertyPath[0].ToString()).IsEqualTo("Members[1]");
|
||||||
|
await Assert.That(propertyPath[1].ToString()).IsEqualTo("Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_treat_multiple_indexers_as_a_single_segment()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Matrix[1][2].Value";
|
||||||
|
var segments = (IReadOnlyList<string>)propertyPath;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments[0]).IsEqualTo("Matrix[1][2]");
|
||||||
|
await Assert.That(segments[1]).IsEqualTo("Value");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_do_not_split_dots_inside_brackets()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Items[foo.bar].Name";
|
||||||
|
var segments = (IReadOnlyList<string>)propertyPath;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments[0]).IsEqualTo("Items[foo.bar]");
|
||||||
|
await Assert.That(segments[1]).IsEqualTo("Name");
|
||||||
|
await Assert.That(propertyPath[0].ToString()).IsEqualTo("Items[foo.bar]");
|
||||||
|
await Assert.That(propertyPath[1].ToString()).IsEqualTo("Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_expose_empty_segments_for_malformed_paths()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Address..Street";
|
||||||
|
var segments = (IReadOnlyList<string>)propertyPath;
|
||||||
|
|
||||||
|
await Assert.That(propertyPath.Count).IsEqualTo(3);
|
||||||
|
await Assert.That(propertyPath.IsEmpty).IsFalse();
|
||||||
|
await Assert.That(segments.Count).IsEqualTo(3);
|
||||||
|
await Assert.That(segments[0]).IsEqualTo("Address");
|
||||||
|
await Assert.That(segments[1]).IsEqualTo(string.Empty);
|
||||||
|
await Assert.That(segments[2]).IsEqualTo("Street");
|
||||||
|
await Assert.That(propertyPath[1].ToString()).IsEqualTo(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_treat_an_empty_path_as_having_no_segments()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"";
|
||||||
|
|
||||||
|
await Assert.That(propertyPath.Count).IsEqualTo(0);
|
||||||
|
await Assert.That(propertyPath.IsEmpty).IsTrue();
|
||||||
|
// ReSharper disable once RedundantCast
|
||||||
|
await Assert.That(((IReadOnlyList<string>)propertyPath).Count).IsEqualTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_access_segments_through_the_explicit_read_only_list_interface()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Members[1].Name";
|
||||||
|
var segments = (IReadOnlyList<string>)propertyPath;
|
||||||
|
|
||||||
|
await Assert.That(segments.Count).IsEqualTo(2);
|
||||||
|
await Assert.That(segments[0]).IsEqualTo("Members[1]");
|
||||||
|
await Assert.That(segments[1]).IsEqualTo("Name");
|
||||||
|
await Assert.That(string.Join("|", segments)).IsEqualTo("Members[1]|Name");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_see_it_throw_for_out_of_range_segment_access()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Name";
|
||||||
|
|
||||||
|
await Assert.That(() => propertyPath[1].ToString())
|
||||||
|
.Throws<ArgumentOutOfRangeException>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_compare_a_property_path_to_a_string()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Address.Street";
|
||||||
|
|
||||||
|
using (Assert.Multiple())
|
||||||
|
{
|
||||||
|
await Assert.That(propertyPath.Equals("Address.Street")).IsTrue();
|
||||||
|
await Assert.That(propertyPath.Equals("Address.Street")).IsTrue();
|
||||||
|
await Assert.That(propertyPath.Equals("address.street")).IsFalse();
|
||||||
|
await Assert.That(propertyPath.Equals((string?)null)).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_compare_a_property_path_to_a_string_through_object_equality()
|
||||||
|
{
|
||||||
|
var propertyPath = (PropertyPath)"Address.Street";
|
||||||
|
|
||||||
|
using (Assert.Multiple())
|
||||||
|
{
|
||||||
|
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||||
|
await Assert.That(propertyPath.Equals((object?)"Address.Street")).IsTrue();
|
||||||
|
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||||
|
await Assert.That(propertyPath.Equals((object?)"address.street")).IsFalse();
|
||||||
|
// ReSharper disable once SuspiciousTypeConversion.Global
|
||||||
|
await Assert.That(propertyPath.Equals((object?)null)).IsFalse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_can_combine_property_paths_with_the_plus_operator()
|
public async Task I_can_combine_property_paths_with_the_plus_operator()
|
||||||
{
|
{
|
||||||
var propertyPath = "Address" + (PropertyPath)"Street";
|
var propertyPath = "Address" + (PropertyPath)"Street";
|
||||||
|
|
||||||
await Assert.That(propertyPath).IsEqualTo("Address.Street");
|
await Assert.That(propertyPath.ToString()).IsEqualTo("Address.Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -54,8 +186,8 @@ internal sealed class PropertyPathTests
|
||||||
var leftEmpty = "" + (PropertyPath)"Street";
|
var leftEmpty = "" + (PropertyPath)"Street";
|
||||||
var rightEmpty = (PropertyPath)"Address" + "";
|
var rightEmpty = (PropertyPath)"Address" + "";
|
||||||
|
|
||||||
await Assert.That(leftEmpty).IsEqualTo("Street");
|
await Assert.That(leftEmpty.ToString()).IsEqualTo("Street");
|
||||||
await Assert.That(rightEmpty).IsEqualTo("Address");
|
await Assert.That(rightEmpty.ToString()).IsEqualTo("Address");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -63,7 +195,7 @@ internal sealed class PropertyPathTests
|
||||||
{
|
{
|
||||||
var propertyPath = (PropertyPath)"Members" + 1;
|
var propertyPath = (PropertyPath)"Members" + 1;
|
||||||
|
|
||||||
await Assert.That(propertyPath).IsEqualTo("Members[1]");
|
await Assert.That(propertyPath.ToString()).IsEqualTo("Members[1]");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -71,6 +203,6 @@ internal sealed class PropertyPathTests
|
||||||
{
|
{
|
||||||
var propertyPath = (PropertyPath)"" + 1;
|
var propertyPath = (PropertyPath)"" + 1;
|
||||||
|
|
||||||
await Assert.That(propertyPath).IsEqualTo("[1]");
|
await Assert.That(propertyPath.ToString()).IsEqualTo("[1]");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ internal sealed class RuleBuilderExtensionsTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyPath).IsEqualTo(propertyPath);
|
await Assert.That(problem.PropertyPath.ToString()).IsEqualTo(propertyPath);
|
||||||
await Assert.That(problem.Message).IsEqualTo(message);
|
await Assert.That(problem.Message).IsEqualTo(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyPath).IsEqualTo(nameof(Person.Name));
|
await Assert.That(problem.PropertyPath.ToString()).IsEqualTo(nameof(Person.Name));
|
||||||
await Assert.That(problem.Message).IsEqualTo("Name is required.");
|
await Assert.That(problem.Message).IsEqualTo("Name is required.");
|
||||||
await Assert.That(problem.Code).IsEqualTo("NAME_REQUIRED");
|
await Assert.That(problem.Code).IsEqualTo("NAME_REQUIRED");
|
||||||
await Assert.That(problem.Severity).IsEqualTo(Severity.Warning);
|
await Assert.That(problem.Severity).IsEqualTo(Severity.Warning);
|
||||||
|
|
@ -58,7 +58,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Address.Street");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Address.Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -74,7 +74,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Street");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -103,7 +103,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Members[1].Name");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Members[1].Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -119,7 +119,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("People[1].Name");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("People[1].Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -137,7 +137,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyPath).IsEqualTo("Address.Street");
|
await Assert.That(problem.PropertyPath.ToString()).IsEqualTo("Address.Street");
|
||||||
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -156,7 +156,7 @@ internal sealed class ValidatorTests
|
||||||
|
|
||||||
using (Assert.Multiple())
|
using (Assert.Multiple())
|
||||||
{
|
{
|
||||||
await Assert.That(problem.PropertyPath).IsEqualTo(nameof(Person.Name));
|
await Assert.That(problem.PropertyPath.ToString()).IsEqualTo(nameof(Person.Name));
|
||||||
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
await Assert.That(problem.Message).IsEqualTo("Value is required.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -171,7 +171,7 @@ internal sealed class ValidatorTests
|
||||||
var result = validator.Validate(new Person { Name = "" });
|
var result = validator.Validate(new Person { Name = "" });
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Username");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Username");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -188,7 +188,7 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Location.Road");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Location.Road");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -270,8 +270,8 @@ internal sealed class ValidatorTests
|
||||||
});
|
});
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(2);
|
await Assert.That(result.Problems).Count().IsEqualTo(2);
|
||||||
await Assert.That(result.Problems[0].PropertyPath).IsEqualTo("Members[0].Name");
|
await Assert.That(result.Problems[0].PropertyPath.ToString()).IsEqualTo("Members[0].Name");
|
||||||
await Assert.That(result.Problems[1].PropertyPath).IsEqualTo("Members[2].Name");
|
await Assert.That(result.Problems[1].PropertyPath.ToString()).IsEqualTo("Members[2].Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
@ -292,7 +292,7 @@ internal sealed class ValidatorTests
|
||||||
var result = validator.Validate(context);
|
var result = validator.Validate(context);
|
||||||
|
|
||||||
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
await Assert.That(result.Problems).Count().IsEqualTo(1);
|
||||||
await Assert.That(result.Problems.Single().PropertyPath).IsEqualTo("Address.Street");
|
await Assert.That(result.Problems.Single().PropertyPath.ToString()).IsEqualTo("Address.Street");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Collections;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
@ -11,7 +12,7 @@ namespace Geekeey.Request.Validation;
|
||||||
/// Represents a property path used by validation problems.
|
/// Represents a property path used by validation problems.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(PropertyPathJsonConverter))]
|
[JsonConverter(typeof(PropertyPathJsonConverter))]
|
||||||
public readonly struct PropertyPath : IEquatable<PropertyPath>
|
public readonly struct PropertyPath : IEquatable<PropertyPath>, IEquatable<string>, IReadOnlyList<string>
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new property path.
|
/// Creates a new property path.
|
||||||
|
|
@ -27,6 +28,54 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Value { get; } = string.Empty;
|
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 />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
|
|
@ -55,10 +104,16 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
return string.Equals(Value, other.Value, StringComparison.Ordinal);
|
return string.Equals(Value, other.Value, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Equals(string? other)
|
||||||
|
{
|
||||||
|
return string.Equals(Value, other, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool Equals(object? obj)
|
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 />
|
/// <inheritdoc />
|
||||||
|
|
@ -109,9 +164,48 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
return propertyPath.Value is { Length: > 0 } ? $"{propertyPath}[{index}]" : $"[{index}]";
|
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)
|
internal string ToJsonName(JsonNamingPolicy? namingPolicy)
|
||||||
{
|
{
|
||||||
var value = Value ?? string.Empty;
|
var value = Value;
|
||||||
if (namingPolicy is null || value.Length is 0)
|
if (namingPolicy is null || value.Length is 0)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
|
|
@ -119,16 +213,15 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
|
|
||||||
var builder = new StringBuilder(value.Length);
|
var builder = new StringBuilder(value.Length);
|
||||||
var first = true;
|
var first = true;
|
||||||
|
var enumerator = new Enumerator(value);
|
||||||
foreach (var range in value.AsSpan().Split('.'))
|
while (enumerator.MoveNext())
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!first)
|
||||||
{
|
{
|
||||||
builder.Append('.');
|
builder.Append('.');
|
||||||
}
|
}
|
||||||
|
|
||||||
var segment = value.AsSpan()[range];
|
if (enumerator.Current is { IsEmpty: false } segment)
|
||||||
if (segment.Length is not 0)
|
|
||||||
{
|
{
|
||||||
if (segment.IndexOf('[') is >= 0 and var x)
|
if (segment.IndexOf('[') is >= 0 and var x)
|
||||||
{
|
{
|
||||||
|
|
@ -146,6 +239,64 @@ public readonly struct PropertyPath : IEquatable<PropertyPath>
|
||||||
|
|
||||||
return builder.ToString();
|
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>
|
internal sealed class PropertyPathJsonConverter : JsonConverter<PropertyPath>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue