diff --git a/Source/Directory.Build.targets b/Source/Directory.Build.targets
index 48253f3..6d260a2 100644
--- a/Source/Directory.Build.targets
+++ b/Source/Directory.Build.targets
@@ -23,6 +23,10 @@
+
+ true
+
+
truefalse
diff --git a/Source/Directory.Packages.props b/Source/Directory.Packages.props
index 99ee5ef..88f430b 100644
--- a/Source/Directory.Packages.props
+++ b/Source/Directory.Packages.props
@@ -3,32 +3,32 @@
true
-
+
-
+
-
+
-
-
-
-
+
+
+
+
-
+
-
+
-
+
-
+
diff --git a/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs b/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs
index f9e3fc4..028966b 100644
--- a/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs
+++ b/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs
@@ -160,12 +160,12 @@ IEnumerator IEnumerable.GetEnumerator()
/// The hashcode.
public override int GetHashCode()
{
- if (this.inner.HasValue)
+ if (this.Count == 0)
{
- return StructuralComparisons.StructuralEqualityComparer.GetHashCode(this.inner);
+ return 0;
}
- return 0;
+ return StructuralComparisons.StructuralEqualityComparer.GetHashCode(this.inner!);
}
///
@@ -175,6 +175,11 @@ public override int GetHashCode()
/// true, if the values are equal otherwise false.
public bool Equals(ValueArray other)
{
+ if (this.Count == 0 && other.Count == 0)
+ {
+ return true;
+ }
+
return StructuralComparisons.StructuralEqualityComparer.Equals(this.inner, other.inner);
}
diff --git a/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs b/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs
index 953f29d..3e1d243 100644
--- a/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs
+++ b/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs
@@ -162,13 +162,13 @@ public IEnumerator> GetEnumerator()
public override int GetHashCode()
{
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
- if (this.inner == null)
+ if (this.Count == 0)
{
return 0;
}
var hashCode = default(HashCode);
- foreach (var pair in this.inner)
+ foreach (var pair in this.inner!)
{
hashCode.Add(pair.Key);
hashCode.Add(pair.Value);
@@ -186,7 +186,7 @@ static int CombineHashCode(int hashCode1, int hashcode2)
}
}
- return this.inner == default ? 0 : Equality.Equality.GetItemsHashCode(this.inner.Select(x => CombineHashCode(x.Key?.GetHashCode() ?? 0, x.Value?.GetHashCode() ?? 0)));
+ return this.Count == 0 ? 0 : Equality.Equality.GetItemsHashCode(this.inner!.Select(x => CombineHashCode(x.Key?.GetHashCode() ?? 0, x.Value?.GetHashCode() ?? 0)));
#endif
}
@@ -198,6 +198,11 @@ static int CombineHashCode(int hashCode1, int hashcode2)
/// true, if the values are equal otherwise false.
public bool Equals(ValueDictionary other)
{
+ if (this.Count == 0 && other.Count == 0)
+ {
+ return true;
+ }
+
if (this.inner == null)
{
if (other.inner == null)
diff --git a/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs b/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs
index bf15185..74ea8ca 100644
--- a/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs
+++ b/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs
@@ -133,20 +133,20 @@ public IEnumerator GetEnumerator()
public override int GetHashCode()
{
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
- if (this.inner == null)
+ if (this.Count == 0)
{
return 0;
}
var hashCode = default(HashCode);
- foreach (var item in this.inner)
+ foreach (var item in this.inner!)
{
hashCode.Add(item?.GetHashCode() ?? 0);
}
return hashCode.ToHashCode();
#else
- return this.inner == default ? 0 : Equality.Equality.GetItemsHashCode(this.inner.Select(x => x?.GetHashCode() ?? 0));
+ return this.Count == 0 ? 0 : Equality.Equality.GetItemsHashCode(this.inner!.Select(x => x?.GetHashCode() ?? 0));
#endif
}
@@ -158,6 +158,11 @@ public override int GetHashCode()
/// true, if the values are equal otherwise false.
public bool Equals(ValueList other)
{
+ if (this.Count == 0 && other.Count == 0)
+ {
+ return true;
+ }
+
if (this.inner == null)
{
if (other.inner == null)
diff --git a/Source/Sundew.Base.Collections.Linq/Empty.cs b/Source/Sundew.Base.Collections.Linq/Empty.cs
index 1e57214..15a80e9 100644
--- a/Source/Sundew.Base.Collections.Linq/Empty.cs
+++ b/Source/Sundew.Base.Collections.Linq/Empty.cs
@@ -14,9 +14,9 @@ namespace Sundew.Base.Collections.Linq;
///
/// The item type.
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
-public sealed record Empty : ListCardinality
+public sealed partial record Empty : ListCardinality
#else
-public sealed class Empty : ListCardinality
+public sealed partial class Empty : ListCardinality
#endif
{
}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Collections.Linq/Item.cs b/Source/Sundew.Base.Collections.Linq/Item.cs
index d89f02d..edd3df1 100644
--- a/Source/Sundew.Base.Collections.Linq/Item.cs
+++ b/Source/Sundew.Base.Collections.Linq/Item.cs
@@ -14,6 +14,18 @@ namespace Sundew.Base.Collections.Linq;
///
public static class Item
{
+ ///
+ /// Converts the item into an item result.
+ ///
+ /// The success type.
+ /// The result.
+ /// An Item result.
+ [MethodImpl((MethodImplOptions)0x300)]
+ public static Item PassIfSuccess(R result)
+ {
+ return result.ToItem();
+ }
+
///
/// Converts the item into an item result.
///
@@ -167,6 +179,18 @@ public static Item ToItem(this TValue? option)
return new Item(option, option.HasValue);
}
+ ///
+ /// Converts the option to an item.
+ ///
+ /// The value type.
+ /// The result.
+ /// The new item.
+ [MethodImpl((MethodImplOptions)0x300)]
+ public static Item ToItem(this R result)
+ {
+ return new Item(result.Value, result.IsSuccess);
+ }
+
///
/// Converts the option to an item.
///
diff --git a/Source/Sundew.Base.Collections.Linq/Multiple.cs b/Source/Sundew.Base.Collections.Linq/Multiple.cs
index 37142fe..ed1f0f3 100644
--- a/Source/Sundew.Base.Collections.Linq/Multiple.cs
+++ b/Source/Sundew.Base.Collections.Linq/Multiple.cs
@@ -15,9 +15,9 @@ namespace Sundew.Base.Collections.Linq;
///
/// The item type.
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
-public sealed record Multiple : ListCardinality, IEnumerable
+public sealed partial record Multiple : ListCardinality, IEnumerable
#else
-public sealed class Multiple : ListCardinality, IEnumerable
+public sealed partial class Multiple : ListCardinality, IEnumerable
#endif
{
///
diff --git a/Source/Sundew.Base.Collections.Linq/Single.cs b/Source/Sundew.Base.Collections.Linq/Single.cs
index 4acb454..4485541 100644
--- a/Source/Sundew.Base.Collections.Linq/Single.cs
+++ b/Source/Sundew.Base.Collections.Linq/Single.cs
@@ -12,7 +12,7 @@ namespace Sundew.Base.Collections.Linq;
///
/// The item type.
#if NETSTANDARD2_0_OR_GREATER || NET6_0_OR_GREATER
-public sealed record Single : ListCardinality
+public sealed partial record Single : ListCardinality
#else
public sealed class Single : ListCardinality
#endif
diff --git a/Source/Sundew.Base.Development.Tests/Collections/ValueArrayTests.cs b/Source/Sundew.Base.Development.Tests/Collections/ValueArrayTests.cs
index 56a67fd..7579c97 100644
--- a/Source/Sundew.Base.Development.Tests/Collections/ValueArrayTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Collections/ValueArrayTests.cs
@@ -22,6 +22,15 @@ public void Equals_When_IsDefault_Then_LhsAndRhsShouldBeEqual()
((object)lhs).Should().Be(rhs);
}
+ [Test]
+ public void Equals_When_OneIsDefaultAndTheOtherIsEmpty_Then_LhsAndRhsShouldBeEqual()
+ {
+ ValueArray lhs = ValueArray.Empty;
+ ValueArray rhs = default;
+
+ ((object)lhs).Should().Be(rhs);
+ }
+
[Test]
public void Equals_When_UsedWithInt_Then_LhsAndRhsShouldBeEqual()
{
diff --git a/Source/Sundew.Base.Development.Tests/Collections/ValueDictionaryTests.cs b/Source/Sundew.Base.Development.Tests/Collections/ValueDictionaryTests.cs
index ba84ef4..61bcdb1 100644
--- a/Source/Sundew.Base.Development.Tests/Collections/ValueDictionaryTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Collections/ValueDictionaryTests.cs
@@ -13,6 +13,15 @@ namespace Sundew.Base.Development.Tests.Collections;
public class ValueDictionaryTests
{
+ [Test]
+ public void Equals_When_OneIsDefaultAndTheOtherIsEmpty_Then_LhsAndRhsShouldBeEqual()
+ {
+ ValueDictionary lhs = ValueDictionary.Empty;
+ ValueDictionary rhs = default;
+
+ ((object)lhs).Should().Be(rhs);
+ }
+
[Test]
public void Equals_When_UsedWithInt_Then_LhsAndRhsShouldBeEqual()
{
diff --git a/Source/Sundew.Base.Development.Tests/Collections/ValueListTests.cs b/Source/Sundew.Base.Development.Tests/Collections/ValueListTests.cs
index b11781c..93646a6 100644
--- a/Source/Sundew.Base.Development.Tests/Collections/ValueListTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Collections/ValueListTests.cs
@@ -13,6 +13,15 @@ namespace Sundew.Base.Development.Tests.Collections;
public class ValueListTests
{
+ [Test]
+ public void Equals_When_OneIsDefaultAndTheOtherIsEmpty_Then_LhsAndRhsShouldBeEqual()
+ {
+ ValueList lhs = ValueList.Empty;
+ ValueList rhs = default;
+
+ ((object)lhs).Should().Be(rhs);
+ }
+
[Test]
public void Equals_When_UsedWithInt_Then_LhsAndRhsShouldBeEqual()
{
diff --git a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs
new file mode 100644
index 0000000..59530c8
--- /dev/null
+++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs
@@ -0,0 +1,366 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Development.Tests.Identification;
+
+using System;
+using System.Globalization;
+using AwesomeAssertions;
+using AwesomeAssertions.Execution;
+using Sundew.Base.Identification;
+
+public class IdTests
+{
+ [Test]
+ public void ToUri_Then_ResultShouldBeExpectedResult()
+ {
+ const string expected =
+ "appid://username@localhost:80/IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=6&Y=4))";
+ var id = Id.From(x => x.NavigateTo(new Position(6, 4)));
+
+ var result = id.ToUri("appid", "username", "localhost", 80);
+
+ result.ToString().Should().Be(expected);
+ }
+
+ [Test]
+ public void ToUriWithScheme_Then_ResultShouldBeExpectedResult()
+ {
+ const string expected =
+ "appid:///IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=6&Y=4))";
+ var id = Id.From(x => x.NavigateTo(new Position(6, 4)));
+
+ var result = id.ToUriWithScheme("appid");
+
+ result.ToString().Should().Be(expected);
+ }
+
+ [Test]
+ public void FromUri_Then_ResultShouldBeExpectedResult()
+ {
+ var id = Id.From(x => x.NavigateTo(new Position(6, 4)));
+
+ var result = Id.From(id.ToUriWithScheme("appid"));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.IsSuccess.Should().BeTrue();
+ result.Value.Should().Be(id);
+ }
+ }
+
+ [Test]
+ [Arguments("Name+Nested~Name.Space$Assembly")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Path")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Path?1")]
+ [Arguments("Name+Nested~Namespace$Assembly/Path?Name=John&LastName=Doe")]
+ [Arguments("Name+Nested~Namespace$Assembly/Some/Path?Name=John&LastName=Doe")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find?Person=(Address=Home&Number=15)&Description=(Eyes=Blue)")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find?Person=(Address=Home&Number=15)&Colors=[Blue,Green]")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find((Address=Home&Number=15)&Colors=[Blue,Green])")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find(Query!Name~Name.Space$Assembly=(Address=Home&Number=15)&Colors=[Blue,Green])")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find(!Name~Name.Space$Assembly=(Address=Home&Number=15)&Colors=[Blue,Green])")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find(!Name~Name.Space$Assembly=(Address=Home&Number=15)&Colors=[!Colors~Assembly=Blue,Green])")]
+ [Arguments("Name+Nested~Name.Space$Assembly/Find(!Name~Name.Space$Assembly=(Address=Home&Number=15)&Colors=[!Colors~Namespace$Assembly=Blue,Green])")]
+ [Arguments("IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=null)")]
+ public void Parse_Then_ResultShouldNotBeNull(string input)
+ {
+ var result = Id.Parse(input, CultureInfo.InvariantCulture);
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().NotBeNull();
+ result.ToString().Should().Be(input);
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()";
+ var result = Id.From(x => x.GoBack());
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ var expected = Id.Parse(result.ToString(), CultureInfo.InvariantCulture);
+ result.Should().Be(expected);
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([]);
+ result.TryGetResultType().Value.Should().Be(typeof(void));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsMethodWith1ParameterAsNull_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=^null)";
+ var result = Id.From(x => x.NavigateTo(null!));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture));
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Position)]);
+ result.TryGetResultType().Value.Should().Be(typeof(void));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=6&Y=4))";
+ var result = Id.From(x => x.NavigateTo(new Position(6, 4)));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture));
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Position)]);
+ result.TryGetResultType().Value.Should().Be(typeof(void));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo(position!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=^null&addToHistory=False)";
+ var result = Id.From(x => x.NavigateTo(null!, default));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture));
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Position), typeof(bool)]);
+ result.TryGetResultType().Value.Should().Be(typeof(void));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull()
+ {
+ const string expectedResult =
+ "IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X";
+ var result = Id.From(x => x.X);
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture));
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(int)]);
+ result.TryGetResultType().Value.Should().Be(typeof(int));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(Position));
+ }
+ }
+
+ [Test]
+ public void ToValue_Then_ResultShouldBeExpectedResult()
+ {
+ const string expectedResult =
+ "!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=5)";
+ var position = new Position(4, 5);
+
+ var valueId = position.Id;
+ var result = valueId.ToValue(new Position(0, 0));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ valueId.ToString().Should().Be(expectedResult);
+ result.Should().Be(position);
+ }
+ }
+
+ [Test]
+ public void ToValue_When_UsingNestedType_Then_ResultShouldBeExpectedResult()
+ {
+ const string expectedResult =
+ "!IdTests+Position3D~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Position=(X=4&Y=5)&Z=6)";
+ var position = new Position3D(new Position(4, 5), 6);
+
+ var valueId = position.Id;
+ var result = valueId.ToValue(new Position3D(new Position(0, 0), 0));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ valueId.ToString().Should().Be(expectedResult);
+ result.Should().Be(position);
+ }
+ }
+
+ [Test]
+ public void ToValue_When_UsingNestedTypeWithNull_Then_ResultShouldBeExpectedResult()
+ {
+ const string expectedResult =
+ "!IdTests+Position3D~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Position=^null&Z=6)";
+ var position = new Position3D(null!, 6);
+
+ var valueId = position.Id;
+ var result = valueId.ToValue(new Position3D(null!, 0));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ valueId.ToString().Should().Be(expectedResult);
+ result.Should().Be(position);
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsMethodWith1Parameters_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate/Execute(parameter!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6))";
+ var result = Id.From(x => x.Navigate.Execute(Id.Argument()), new Position(4, 6));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture));
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Position)]);
+ result.TryGetResultType().Value.Should().Be(typeof(void));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(ICommand));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult =
+ "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate?!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6)";
+ var result = Id.From(x => x.Navigate, new Position(4, 6));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ var expected = Id.Parse(result.ToString(), CultureInfo.InvariantCulture);
+ expected.Should().Be(result);
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Position)]);
+ result.TryGetResultType().Value.Should().Be(typeof(ICommand));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_TargetIsPropertyAndPassingArgumentByReferenceId_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Description?^1";
+ var result = Id.From(x => x.Description, new PointOfInterest("Home"));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ var expected = Id.Parse(result.ToString(), CultureInfo.InvariantCulture);
+ expected.Should().Be(result);
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal(null);
+ result.TryGetResultType().Value.Should().Be(typeof(ICommand));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+ [Test]
+ public void From_When_PassingArgumentsWithReservedCharacters_Then_ResultShouldBeExpected()
+ {
+ const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Search(query!IdTests+Query~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Name=with%20%28%20%29%20%7B%20%7D%20%2F%20%2C%20%3A%20%21%20%24%20~%20%3D%20%3C%20%3E%20%26%20%5B%20%5D%20%25%20%5E))";
+ var result = Id.From(x => x.Search(new Query("with ( ) { } / , : ! $ ~ = < > & [ ] % ^")));
+
+ using (var scope = new AssertionScope())
+ {
+ scope.FormattingOptions.MaxDepth = 20;
+ var expected = Id.Parse(result.ToString(), CultureInfo.InvariantCulture);
+ expected.Should().Be(result);
+ result.ToString().Should().Be(expectedResult);
+ result.TryGetInputTypes().Value.Should().Equal([typeof(Query)]);
+ result.TryGetResultType().Value.Should().Be(typeof(Position));
+ result.TryGetTargetContainingType().Value.Should().Be(typeof(INavigator));
+ }
+ }
+
+#pragma warning disable SA1201
+ public interface INavigator
+#pragma warning restore SA1201
+ {
+ ICommand Navigate { get; }
+
+ ICommand Description { get; }
+
+ void GoBack();
+
+ void NavigateTo(Position position);
+
+ void NavigateTo(Position position, bool addToHistory);
+
+ Position Search(Query query);
+ }
+
+ public interface ICommand
+ {
+ void Execute(TParameter parameter);
+ }
+
+ public interface ICommand
+ {
+ TResult Execute(TParameter parameter);
+ }
+
+ public record Position(int X, int Y) : IValueIdentifiable
+ {
+ public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.X).Add(value.Y));
+
+ public static Position From(Position position, ValueId valueId, IFormatProvider? formatProvider)
+ {
+ return new Position(
+ valueId.GetScalar(position.X, formatProvider),
+ valueId.GetScalar(position.Y, formatProvider));
+ }
+ }
+
+ public record Position3D(Position Position, int Z) : IValueIdentifiable
+ {
+ public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.Position).Add(value.Z));
+
+ public static Position3D From(Position3D value, ValueId valueId, IFormatProvider? formatProvider)
+ {
+ return new Position3D(
+ valueId.GetValue(value.Position, formatProvider),
+ valueId.GetScalar(value.Z, formatProvider));
+ }
+ }
+
+ public record Query(string Name) : IValueIdentifiable
+ {
+ public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.Name));
+
+ public static Query From(Query value, ValueId valueId, IFormatProvider? formatProvider)
+ {
+ return new Query(valueId.GetScalar(value.Name, formatProvider));
+ }
+ }
+
+ public record PointOfInterest(string Name) : IIdentifiable
+ {
+ public InstanceId Id { get; } = InstanceId.Next();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs
new file mode 100644
index 0000000..d946e4f
--- /dev/null
+++ b/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs
@@ -0,0 +1,60 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Development.Tests.Identification;
+
+using AwesomeAssertions;
+using Sundew.Base.Identification;
+
+[NotInParallel]
+public class RevisionIdTests
+{
+ [Test]
+ public void Next_WhenCalledASecondTime_Then_TheTwoShouldNotBeEqual()
+ {
+ var id1 = RevisionId.Next();
+ var id2 = RevisionId.Next();
+
+ id1.Should().NotBe(id2);
+ }
+
+ [Test]
+ public void Next_WhenCalledASecondTime_Then_TheFirstShouldNotBeNewer()
+ {
+ var id1 = RevisionId.Next();
+ var id2 = RevisionId.Next();
+
+ var result = id1.IsNewer(id2);
+
+ result.Should().BeFalse();
+ }
+
+ [Test]
+ public void Next_WhenCalledASecondTime_Then_TheSecondShouldBeNewer()
+ {
+ var id1 = RevisionId.Next();
+ var id2 = RevisionId.Next();
+
+ var result = id2.IsNewer(id1);
+
+ result.Should().BeTrue();
+ }
+
+ [Test]
+ public void Next_WhenCalledASecondTimeAndOverflows_Then_TheSecondShouldBeNewer()
+ {
+ var currentId = SequenceId.CurrentId;
+ SequenceId.CurrentId = uint.MaxValue - 1;
+ var id1 = RevisionId.Next();
+ var id2 = RevisionId.Next();
+ SequenceId.CurrentId = currentId;
+
+ var result = id2.IsNewer(id1);
+
+ result.Should().BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Development.Tests/Migrations/DiscriminatedUnionMigrationsTests.cs b/Source/Sundew.Base.Development.Tests/Migrations/DiscriminatedUnionMigrationsTests.cs
index 7d925f1..596c693 100644
--- a/Source/Sundew.Base.Development.Tests/Migrations/DiscriminatedUnionMigrationsTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Migrations/DiscriminatedUnionMigrationsTests.cs
@@ -47,12 +47,12 @@ public abstract partial record SingleVersionDto
{
}
-public sealed record SingleVersion(string Name) : SingleVersionDto;
+public sealed partial record SingleVersion(string Name) : SingleVersionDto;
[DiscriminatedUnion]
public abstract partial record TwoVersionDto
{
- public sealed record V1(string Name) : TwoVersionDto;
+ public sealed partial record V1(string Name) : TwoVersionDto;
}
-public sealed record TwoVersion(string Name, int Number) : TwoVersionDto;
+public sealed partial record TwoVersion(string Name, int Number) : TwoVersionDto;
diff --git a/Source/Sundew.Base.Development.Tests/Migrations/MigrationCancellationTests.cs b/Source/Sundew.Base.Development.Tests/Migrations/MigrationCancellationTests.cs
index 795921e..9a571eb 100644
--- a/Source/Sundew.Base.Development.Tests/Migrations/MigrationCancellationTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Migrations/MigrationCancellationTests.cs
@@ -75,11 +75,11 @@ public async Task Migrate_When_IsListAndCancelling_Then_ResultShouldBeErrorIndic
[DiscriminatedUnion]
public abstract partial record CancelledPerson : IMigratable
{
- public sealed record V1(string Name) : CancelledPerson;
+ public sealed partial record V1(string Name) : CancelledPerson;
- public sealed record V2(string Name, string LastName) : CancelledPerson;
+ public sealed partial record V2(string Name, string LastName) : CancelledPerson;
- public sealed record V3(string Name, string LastName, int Age) : CancelledPerson;
+ public sealed partial record V3(string Name, string LastName, int Age) : CancelledPerson;
public static async ValueTask> Migrate(CancelledPerson person, __ valueProvider, CancellationToken cancellationToken)
{
diff --git a/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/DeserializationTests.cs b/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/DeserializationTests.cs
index 7c1beab..68a1d2d 100644
--- a/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/DeserializationTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/DeserializationTests.cs
@@ -61,4 +61,4 @@ public static ValueTask> Migrate(PersonPastDto pe
public static IReadOnlyCollection GetMigrationInfo() => DiscriminatedUnionMigrations.FromVersionNamedUnion();
}
-public sealed record PersonPast(string Name) : PersonPastDto;
\ No newline at end of file
+public sealed partial record PersonPast(string Name) : PersonPastDto;
\ No newline at end of file
diff --git a/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/MigrationTests.cs b/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/MigrationTests.cs
index 7f23c18..ac4d501 100644
--- a/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/MigrationTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Migrations/System_Text_Json/MigrationTests.cs
@@ -103,9 +103,9 @@ public async Task Migrate_When_MigratingList_Then_ResultShouldBeValidListOfV3()
[DiscriminatedUnion]
public abstract partial record PersonDto : IMigratable
{
- public sealed record V1(string Name) : PersonDto;
+ public sealed partial record V1(string Name) : PersonDto;
- public sealed record V2(string Name, string LastName, Address.V1? Address) : PersonDto;
+ public sealed partial record V2(string Name, string LastName, Address.V1? Address) : PersonDto;
public static async ValueTask> Migrate(PersonDto personDto, IPersonValueProvider valueProvider, CancellationToken cancellationToken)
{
@@ -120,14 +120,14 @@ public static async ValueTask> Migrate(PersonDto pers
public static IReadOnlyCollection GetMigrationInfo() => DiscriminatedUnionMigrations.FromVersionNamedUnion();
}
-public sealed record Person(string Name, string LastName, int Age, Address.V2? Address) : PersonDto;
+public sealed partial record Person(string Name, string LastName, int Age, Address.V2? Address) : PersonDto;
[DiscriminatedUnion]
public abstract record Address
{
- public sealed record V1(string Street, string Number, string PostalCode, string City) : Address;
+ public sealed partial record V1(string Street, string Number, string PostalCode, string City) : Address;
- public sealed record V2(string Street, string Number, string PostalCode, string City, string? Apartment, string? SecondLine) : Address;
+ public sealed partial record V2(string Street, string Number, string PostalCode, string City, string? Apartment, string? SecondLine) : Address;
}
public interface IPersonValueProvider
diff --git a/Source/Sundew.Base.Development.Tests/Primitives/QuestTests.cs b/Source/Sundew.Base.Development.Tests/Primitives/QuestTests.cs
index cf61cee..b33a156 100644
--- a/Source/Sundew.Base.Development.Tests/Primitives/QuestTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Primitives/QuestTests.cs
@@ -76,9 +76,10 @@ public async Task Start_When_UnstartedAndCanceled_Then_IsCanceledShouldBeTrue()
using var cancellationTokenSource = new CancellationTokenSource();
var quest = Quest.Create(
__._,
- _ =>
+ token =>
{
Thread.Sleep(1000);
+ token.ThrowIfCancellationRequested();
return Task.CompletedTask;
},
cancellationTokenSource.Token);
diff --git a/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj b/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj
index 21ae90d..f15cadf 100644
--- a/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj
+++ b/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj
@@ -24,6 +24,7 @@
+
@@ -38,4 +39,11 @@
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs b/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs
index 8c35407..de1f79c 100644
--- a/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs
+++ b/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs
@@ -111,7 +111,7 @@ public async Task WaitAsync_When_SetAndResetBeforeAndSetAfter_Then_ResultShouldB
{
this.testee.Set();
this.testee.Reset();
- var waitTask = Task.Run(async () => await this.testee.WaitAsync(TimeSpan.FromMilliseconds(1000)));
+ var waitTask = Task.Run(async () => await this.testee.WaitAsync());
await Task.Delay(100);
this.testee.Set();
@@ -230,8 +230,8 @@ public async Task WaitAsync_When_SetAndResetAndTimedout_Then_ResultShouldBeFalse
[Test]
public async Task WaitAsync_When_Set_Then_AllWaitersShouldBeNotified()
{
- var waitTask1 = Task.Run(async () => await this.testee.WaitAsync(TimeSpan.FromMilliseconds(1000)));
- var waitTask2 = Task.Run(async () => await this.testee.WaitAsync(TimeSpan.FromMilliseconds(1000)));
+ var waitTask1 = Task.Run(async () => await this.testee.WaitAsync());
+ var waitTask2 = Task.Run(async () => await this.testee.WaitAsync());
await Task.Delay(10);
this.testee.Set();
diff --git a/Source/Sundew.Base.Identification/AppendOptions.cs b/Source/Sundew.Base.Identification/AppendOptions.cs
new file mode 100644
index 0000000..c17b6da
--- /dev/null
+++ b/Source/Sundew.Base.Identification/AppendOptions.cs
@@ -0,0 +1,14 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+///
+/// Represents options that configure how data is appended in a specific context.
+///
+/// Indicates whether grouping should be avoided when appending data. If set to true, data will be appended without grouping, otherwise, it may be grouped based on the context.
+public readonly record struct AppendOptions(bool IsRoot);
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Argument.cs b/Source/Sundew.Base.Identification/Argument.cs
new file mode 100644
index 0000000..1751244
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Argument.cs
@@ -0,0 +1,35 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Text;
+
+///
+/// Represents an argument.
+///
+/// The Name.
+/// The value id.
+public sealed record Argument(string? Name, ValueId ValueId)
+{
+ ///
+ /// Appends this instance into the specified string builder.
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ if (this.Name.HasValue)
+ {
+ stringBuilder.Append(this.Name);
+ }
+
+ this.ValueId.AppendInto(stringBuilder, formatProvider, appendOptions, this.Name.HasValue);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Arguments.cs b/Source/Sundew.Base.Identification/Arguments.cs
new file mode 100644
index 0000000..5dd897f
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Arguments.cs
@@ -0,0 +1,32 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Text;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Identification.Parsing;
+using Sundew.Base.Text;
+
+///
+/// Represents a list of arguments.
+///
+/// The arguments.
+public sealed record Arguments(ValueArray Items)
+{
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ stringBuilder.AppendItems(this.Items, (builder, argument) => argument.AppendInto(builder, formatProvider, appendOptions), Grammar.ArgumentSeparator);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ArrayValue.cs b/Source/Sundew.Base.Identification/ArrayValue.cs
new file mode 100644
index 0000000..53a4f7e
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ArrayValue.cs
@@ -0,0 +1,49 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Globalization;
+using System.Text;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Identification.Parsing;
+using Sundew.Base.Text;
+
+///
+/// Represents arguments for an .
+///
+/// The value ids.
+public sealed partial record ArrayValue(ValueArray Items) : IValue
+{
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ stringBuilder.Append(Grammar.ArrayStart);
+ stringBuilder.AppendItems(
+ this.Items,
+ (stringBuilder, valueId) => valueId.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }, false),
+ Grammar.ArrayElementSeparator);
+ stringBuilder.Append(Grammar.ArrayEnd);
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true));
+ return stringBuilder.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ComplexValue.cs b/Source/Sundew.Base.Identification/ComplexValue.cs
new file mode 100644
index 0000000..7f594cd
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ComplexValue.cs
@@ -0,0 +1,49 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Globalization;
+using System.Text;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Identification.Parsing;
+using Sundew.Base.Text;
+
+///
+/// Represents arguments for an .
+///
+/// The value ids.
+public sealed partial record ComplexValue(ValueArray Items) : IValue
+{
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ stringBuilder.AppendItems(
+ this.Items,
+ (stringBuilder) => stringBuilder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupStart)),
+ (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }),
+ (stringBuilder) => stringBuilder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupEnd)),
+ Grammar.ArgumentSeparator);
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true));
+ return stringBuilder.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs
new file mode 100644
index 0000000..267cdd2
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs
@@ -0,0 +1,138 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Sundew.Base.Collections.Immutable;
+
+///
+/// Provides static methods for evaluating mathematical and logical expressions represented as strings.
+///
+internal static class ExpressionEvaluator
+{
+ ///
+ /// Gets a for the specified expression.
+ ///
+ /// The path expression.
+ /// The value id.
+ /// A new .
+ public static (Source Source, Path Path, Arguments? Arguments) From(LambdaExpression pathExpression, ValueId? valueId = null)
+ {
+ var isUsed = false;
+ var segments = ImmutableArray.CreateBuilder();
+ var source = Source.FromType(pathExpression.Parameters.First().Type);
+ var valueIds = ImmutableArray.CreateBuilder();
+ EvaluateToPath(pathExpression);
+
+ return (source, new Path(segments.ToImmutable()), !valueId.HasValue || isUsed || !valueId.HasValue ? null : new Arguments(ValueArray.Empty.Add(new Argument(null, valueId))));
+
+ void EvaluateToPath(Expression expression)
+ {
+ switch (expression)
+ {
+ case LambdaExpression lambdaExpression:
+ EvaluateToPath(lambdaExpression.Body);
+ break;
+ case MethodCallExpression methodCallExpression:
+ if (methodCallExpression.Object.HasValue)
+ {
+ EvaluateToPath(methodCallExpression.Object);
+ }
+
+ valueIds = ImmutableArray.CreateBuilder();
+ var parameterInfos = methodCallExpression.Method.GetParameters();
+ foreach (var argument in methodCallExpression.Arguments.Zip(parameterInfos))
+ {
+ if (argument.First is MethodCallExpression argumentMethodCallExpression && argumentMethodCallExpression.Method.DeclaringType == typeof(Id) && argumentMethodCallExpression.Method.Name == nameof(Id.Argument) && valueId.HasValue)
+ {
+ valueIds.Add(new Argument(argument.Second.Name, valueId));
+ isUsed = true;
+ }
+ else
+ {
+ GetArgument(argument.First, argument.Second, valueIds);
+ }
+ }
+
+ segments.Add(new Segment(methodCallExpression.Method.Name, new Arguments(valueIds.ToValueArray())));
+
+ break;
+ case MemberExpression memberExpression:
+ if (memberExpression.Expression.HasValue)
+ {
+ EvaluateToPath(memberExpression.Expression);
+ }
+
+ segments.Add(new Segment(memberExpression.Member.Name));
+ break;
+ case UnaryExpression unaryExpression:
+ EvaluateToPath(unaryExpression.Operand);
+ break;
+ }
+ }
+ }
+
+ private static void GetArgument(Expression argument, ParameterInfo parameterInfo, ImmutableArray.Builder builder)
+ {
+ switch (argument)
+ {
+ case ConstantExpression constantExpression:
+ var valueIdValue = GetValueIdValue(constantExpression.Value);
+ builder.Add(new Argument(parameterInfo.Name, new ValueId(GetMetadata(argument.Type), valueIdValue)));
+ break;
+ case MemberExpression memberExpression:
+ if (memberExpression.Expression is ConstantExpression constantExpression2)
+ {
+ var container = constantExpression2.Value;
+ if (memberExpression.Member is FieldInfo fieldInfo)
+ {
+ var value = fieldInfo.GetValue(container);
+ builder.Add(new Argument(fieldInfo.Name, new ValueId(GetMetadata(fieldInfo.FieldType), GetValueIdValue(value))));
+ }
+
+ if (memberExpression.Member is PropertyInfo propertyInfo)
+ {
+ var value = propertyInfo.GetValue(container);
+ builder.Add(new Argument(propertyInfo.Name, new ValueId(GetMetadata(propertyInfo.PropertyType), GetValueIdValue(value))));
+ }
+ }
+
+ break;
+ case NewExpression newExpression:
+ ImmutableArray.Builder newBuilder = ImmutableArray.CreateBuilder();
+ if (newExpression.Constructor.HasValue)
+ {
+ foreach (var valueTuple in newExpression.Arguments.Zip(newExpression.Constructor.GetParameters()))
+ {
+ GetArgument(valueTuple.First, valueTuple.Second, newBuilder);
+ }
+
+ builder.Add(new Argument(parameterInfo.Name, new ValueId(GetMetadata(argument.Type), new ComplexValue(newBuilder.ToImmutable()))));
+ }
+
+ break;
+ }
+ }
+
+ private static IValue GetValueIdValue(object? value)
+ {
+ const string @null = "null";
+ var valueString = value?.ToString() ?? @null;
+ var valueIdValue = value != null ? IValue.ScalarValue(valueString) : IValue.LiteralValue(valueString);
+ return valueIdValue;
+ }
+
+ private static string? GetMetadata(Type argumentType)
+ {
+ return TargetEvaluator.IsKnownType(argumentType) ? null : Source.FromType(argumentType).ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/IIdentifiable.cs b/Source/Sundew.Base.Identification/IIdentifiable.cs
new file mode 100644
index 0000000..cf25f68
--- /dev/null
+++ b/Source/Sundew.Base.Identification/IIdentifiable.cs
@@ -0,0 +1,24 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+
+///
+/// Interface for implementing an identifiable.
+///
+/// The type of the value id.
+public interface IIdentifiable
+ where TId : IEquatable
+{
+ ///
+ /// Gets the value id.
+ ///
+ /// The value id.
+ TId Id { get; }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ISequenceId.cs b/Source/Sundew.Base.Identification/ISequenceId.cs
new file mode 100644
index 0000000..d2b0ece
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ISequenceId.cs
@@ -0,0 +1,28 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+///
+/// Interface for implementing an incremental id.
+///
+/// The id type.
+public interface ISequenceId
+ where TId : ISequenceId
+{
+ ///
+ /// Gets the code.
+ ///
+ uint Number { get; }
+
+ ///
+ /// Creates an Id.
+ ///
+ /// The id.
+ /// The new id.
+ static abstract TId Create(uint id);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/IValue.cs b/Source/Sundew.Base.Identification/IValue.cs
new file mode 100644
index 0000000..f6448ad
--- /dev/null
+++ b/Source/Sundew.Base.Identification/IValue.cs
@@ -0,0 +1,27 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Text;
+using Sundew.DiscriminatedUnions;
+
+///
+/// Represents a value.
+///
+[DiscriminatedUnion]
+public partial interface IValue
+{
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/IValueIdentifiable.cs b/Source/Sundew.Base.Identification/IValueIdentifiable.cs
new file mode 100644
index 0000000..0eca3d0
--- /dev/null
+++ b/Source/Sundew.Base.Identification/IValueIdentifiable.cs
@@ -0,0 +1,26 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+
+///
+/// Interface for implementing a value identifiable.
+///
+/// The type of the value.
+public interface IValueIdentifiable : IIdentifiable
+{
+ ///
+ /// Creates a value from the value id.
+ ///
+ /// The initial value.
+ /// The value id.
+ /// The format provider.
+ /// The created value.
+ static abstract TValue From(TValue value, ValueId valueId, IFormatProvider? formatProvider);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Id.cs b/Source/Sundew.Base.Identification/Id.cs
new file mode 100644
index 0000000..955d1a8
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Id.cs
@@ -0,0 +1,315 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq.Expressions;
+using System.Text;
+using Sundew.Base.Identification.Parsing;
+
+///
+/// Represents any Id.
+///
+public sealed record Id(Source Source, Path? Path, Arguments? Arguments = null, Arguments? Fragment = null) : IParsable
+{
+ ///
+ /// Creates an Uri from this .
+ ///
+ /// A new .
+ public Uri ToUri()
+ {
+ return this.ToUriWithScheme(null);
+ }
+
+ ///
+ /// Creates an Uri from this .
+ ///
+ /// The scheme.
+ /// A new .
+ public Uri ToUriWithScheme(string? scheme)
+ {
+ return this.ToUri(scheme, null, null, 0);
+ }
+
+ ///
+ /// Creates an Uri from this .
+ ///
+ /// The host.
+ /// A new .
+ public Uri ToUriWithHost(string? host)
+ {
+ return this.ToUri(null, null, host, 0);
+ }
+
+ ///
+ /// Create an Uri from this .
+ ///
+ /// The host.
+ /// The port.
+ /// A new .
+ public Uri ToUri(string? host, int port)
+ {
+ return this.ToUri(null, null, host, port);
+ }
+
+ ///
+ /// Create an Uri from this .
+ ///
+ /// The scheme.
+ /// The host.
+ /// A new .
+ public Uri ToUri(string? scheme, string? host)
+ {
+ return this.ToUri(scheme, null, host, 0);
+ }
+
+ ///
+ /// Create an Uri from this .
+ ///
+ /// The scheme.
+ /// The host.
+ /// The port.
+ /// A new .
+ public Uri ToUri(string? scheme, string? host, int port)
+ {
+ return this.ToUri(scheme, null, host, port);
+ }
+
+ ///
+ /// Create an Uri from this .
+ ///
+ /// The scheme.
+ /// The user info.
+ /// The host.
+ /// The port.
+ /// A new .
+ public Uri ToUri(string? scheme, string? userInfo, string? host, int port)
+ {
+ var pathPrefix = string.Empty;
+ if (string.IsNullOrEmpty(host))
+ {
+ pathPrefix = @"///";
+ }
+
+ var uriBuilder = new UriBuilder(scheme, host, port, pathPrefix + this.ToString())
+ {
+ UserName = userInfo,
+ };
+
+ return uriBuilder.Uri;
+ }
+
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static Id Parse(string inputId, IFormatProvider? provider)
+ {
+ if (TryParse(inputId, provider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputId} is not a valid {nameof(Id)}");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out Id result)
+ {
+ return IdRouteParser.ParseId(inputId, formatProvider).TryGet(out result, out _);
+ }
+
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider)
+ {
+ this.Source.AppendInto(stringBuilder, formatProvider);
+ if (this.Path.HasValue)
+ {
+ stringBuilder.Append(Path.Separator);
+ this.Path.AppendInto(stringBuilder, formatProvider);
+ }
+
+ if (this.Arguments.HasValue)
+ {
+ stringBuilder.Append(Grammar.ArgumentsSeparator);
+ this.Arguments.AppendInto(stringBuilder, formatProvider, new AppendOptions(true));
+ }
+
+ if (this.Fragment.HasValue)
+ {
+ stringBuilder.Append(Grammar.FragmentSeparator);
+ this.Fragment.AppendInto(stringBuilder, formatProvider, new AppendOptions(true));
+ }
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture);
+ return stringBuilder.ToString();
+ }
+
+ ///
+ /// Tries to get the source type.
+ ///
+ /// A result containing the source type if successful.
+ public R TryGetSourceType()
+ {
+ return this.Source.TryGetType();
+ }
+
+ ///
+ /// Tries to get the result type.
+ ///
+ /// A result containing the result type if successful.
+ public R TryGetResultType()
+ {
+ return TargetEvaluator.GetResultType(this.Source, this.Path);
+ }
+
+ ///
+ /// Tries to get the input types.
+ ///
+ /// A result containing the input types if successful.
+ public R> TryGetInputTypes()
+ {
+ return TargetEvaluator.GetInputTypes(this.Source, this.Path, this.Arguments);
+ }
+
+ ///
+ /// Tries to get the target containing type.
+ ///
+ /// A result containing the containing type if successful.
+ public R TryGetTargetContainingType()
+ {
+ return TargetEvaluator.GetDeclaringType(this.Source, this.Path);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The uri.
+ /// The format provider.
+ /// A new .
+ public static R From(Uri uri, IFormatProvider? formatProvider = null)
+ {
+ var pathAndQuery = uri.PathAndQuery;
+ if (pathAndQuery.StartsWith('/'))
+ {
+ pathAndQuery = pathAndQuery.Substring(1);
+ }
+
+ return IdRouteParser.ParseId(pathAndQuery, formatProvider);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// A new .
+ public static Id From(Expression> targetExpression)
+ {
+ var (source, path, arguments) = ExpressionEvaluator.From(targetExpression);
+ return new Id(source, path, arguments);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// A new .
+ public static Id From(Expression> targetExpression)
+ {
+ var target = ExpressionEvaluator.From(targetExpression);
+ return new Id(target.Source, target.Path);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// The value.
+ /// A new .
+ public static Id From(Expression> targetExpression, IIdentifiable value)
+ {
+ var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, new ValueId(null, new LiteralValue(value.Id.Number.ToString())));
+ return new Id(source, path, valueId, null);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// The value.
+ /// A new .
+ public static Id From(Expression> targetExpression, IIdentifiable value)
+ {
+ var target = ExpressionEvaluator.From(targetExpression, new ValueId(null, new LiteralValue(value.Id.Number.ToString())));
+ return new Id(target.Source, target.Path, target.Arguments);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// The value.
+ /// A new .
+ public static Id From(Expression> targetExpression, IIdentifiable value)
+ {
+ var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value?.Id);
+ return new Id(source, path, valueId);
+ }
+
+ ///
+ /// Gets an from the specified source and expression.
+ ///
+ /// The source type.
+ /// The target expression.
+ /// The value.
+ /// A new .
+ public static Id From(Expression> targetExpression, IIdentifiable value)
+ {
+ var target = ExpressionEvaluator.From(targetExpression, value?.Id);
+ return new Id(target.Source, target.Path, target.Arguments);
+ }
+
+ ///
+ /// Indicates an argument placeholder.
+ ///
+ /// The argument type.
+ /// The default value.
+ public static TArgument Argument()
+ {
+ return default!;
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/IdRoute.cs b/Source/Sundew.Base.Identification/IdRoute.cs
new file mode 100644
index 0000000..a423751
--- /dev/null
+++ b/Source/Sundew.Base.Identification/IdRoute.cs
@@ -0,0 +1,48 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Identification.Parsing;
+
+///
+/// Represents a route consisting of a path of .
+///
+public sealed record IdRoute(ValueList Path) : IParsable
+{
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static IdRoute Parse(string inputIdRoute, IFormatProvider? provider)
+ {
+ if (TryParse(inputIdRoute, provider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputIdRoute} is not a valid {nameof(Id)}");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputIdRoute, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out IdRoute result)
+ {
+ return IdRouteParser.ParseIdRoute(inputIdRoute, formatProvider).TryGet(out result, out _);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/InstanceId.cs b/Source/Sundew.Base.Identification/InstanceId.cs
new file mode 100644
index 0000000..d75d610
--- /dev/null
+++ b/Source/Sundew.Base.Identification/InstanceId.cs
@@ -0,0 +1,34 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+///
+/// Default implementation of into an instance id.
+///
+/// The number.
+public readonly record struct InstanceId(uint Number) : ISequenceId
+{
+ ///
+ /// Creates a new instance id.
+ ///
+ /// The number.
+ /// The new instance id.
+ public static InstanceId Create(uint number)
+ {
+ return new InstanceId(number);
+ }
+
+ ///
+ /// Returns a string that represents this instance.
+ ///
+ /// The id as a string.
+ public override string ToString()
+ {
+ return '#' + this.Number.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/LiteralValue.cs b/Source/Sundew.Base.Identification/LiteralValue.cs
new file mode 100644
index 0000000..bd7dd13
--- /dev/null
+++ b/Source/Sundew.Base.Identification/LiteralValue.cs
@@ -0,0 +1,37 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Text;
+using Sundew.Base.Identification.Parsing;
+
+///
+/// Represents a literal value.
+///
+/// The literal value.
+public sealed partial record LiteralValue(string Value) : IValue
+{
+ ///
+ /// None value.
+ ///
+ public const string Null = "null";
+
+ ///
+ /// Appends the content of the literal value into the string builder.
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ stringBuilder
+ .Append(Grammar.LiteralSeparator)
+ .Append(Uri.EscapeDataString(this.Value));
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Parsing/Grammar.cs b/Source/Sundew.Base.Identification/Parsing/Grammar.cs
new file mode 100644
index 0000000..f701a68
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Parsing/Grammar.cs
@@ -0,0 +1,56 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification.Parsing;
+
+internal static class Grammar
+{
+ /// The Source name/path separator.
+ public const char SourceNamePathSeparator = '~';
+
+ /// The Source path/origin separator.
+ public const char SourcePathOriginSeparator = '$';
+
+ /// The path segment separator.
+ public const char PathSegmentSeparator = '/';
+
+ /// The arguments separator.
+ public const char ArgumentsSeparator = '?';
+
+ /// The literal separator.
+ public const char LiteralSeparator = '^';
+
+ /// The fragment separator.
+ public const char FragmentSeparator = '#';
+
+ /// Metadata separator.
+ public const char NameMetadataSeparator = '!';
+
+ /// Key Value separator.
+ public const char KeyValueSeparator = '=';
+
+ /// The array element separator.
+ public const char ArrayElementSeparator = ',';
+
+ /// The start of and array.
+ public const char ArrayStart = '[';
+
+ /// The end of an array.
+ public const char ArrayEnd = ']';
+
+ /// The argument separator.
+ public const char ArgumentSeparator = '&';
+
+ /// The group start.
+ public const char GroupStart = '(';
+
+ /// The group end.
+ public const char GroupEnd = ')';
+
+ /// The Id separator.
+ public const char IdSeparator = '>';
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Parsing/IParserError.cs b/Source/Sundew.Base.Identification/Parsing/IParserError.cs
new file mode 100644
index 0000000..f37bb5a
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Parsing/IParserError.cs
@@ -0,0 +1,133 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+namespace Sundew.Base.Identification;
+
+using Sundew.Base.Parsing;
+using Sundew.DiscriminatedUnions;
+
+///
+/// Interface for implementing a parser error.
+///
+public interface IParserError;
+
+#pragma warning disable SA1402
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IIdRouteError : IParserError
+{
+ public sealed partial record IdRouteIdError(IIdError Error) : IIdRouteError;
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IIdError : IParserError
+{
+ public sealed partial record IdSourceError(ISourceError Error) : IIdError;
+
+ public sealed partial record IdPathError(IPathError Error) : IIdError;
+
+ public sealed partial record IdValueIdError(IArgumentsError Error) : IIdError;
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface ISourceError : IParserError
+{
+ public sealed partial record SourceError(object Cause, LexerError Error) : ISourceError;
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IPathError : IParserError
+{
+ public sealed partial record SegmentNameError(LexerError? Error) : IPathError;
+
+ public sealed partial record PathValueIdError(IArgumentsError Inner) : IPathError;
+
+ public sealed partial record PathEndError(object Cause, LexerError? Error) : IPathError;
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IArgumentsError : IParserError
+{
+ public sealed partial record ValueIdError(IValueIdError Error) : IArgumentsError;
+
+ public sealed partial record ValueError(IValueError Error) : IArgumentsError;
+
+ public sealed partial record GroupValueIdError(object Cause, LexerError? Error) : IArgumentsError;
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IParseValueIdError : IParserError
+{
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IValueIdError : IParserError
+{
+}
+
+///
+/// Union for errors.
+///
+[DiscriminatedUnion]
+public partial interface IValueError : IParserError
+{
+ /*public sealed partial record LiteralError(char Expected, LexerError Error) : IValueError;
+
+ public sealed partial record GroupError(char Expected, LexerError Error) : IValueError;
+
+ public sealed partial record ArrayError(char Expected, LexerError Error) : IValueError;*/
+
+ public sealed partial record ArgumentsError(IArgumentsError Error) : IValueError;
+
+ public sealed partial record ValueIdError(IValueIdError Error) : IValueError;
+
+ public sealed partial record ValueError(LexerError? Error) : IValueError;
+}
+
+public sealed partial record ValueIdError(object Cause, LexerError? Error) : IValueIdError, IParseValueIdError;
+
+public sealed partial record ValueIdValueError(IValueError Error) : IValueIdError, IParseValueIdError;
+
+///
+/// Represents an error when there was still input to process.
+///
+public sealed partial record NotAtEndError() : IIdError, IIdRouteError, IParseValueIdError;
+
+///
+/// Represents an error when the input is empty or null.
+///
+public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError, IParseValueIdError;
+
+///
+/// Represents a lexer error.
+///
+/// The cause.
+/// The lexer error.
+public sealed partial record ExpectedCharacterError(object Cause, LexerError LexerError) : IArgumentsError, IValueIdError, IValueError;
+#pragma warning restore SA1402
diff --git a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs
new file mode 100644
index 0000000..febcbb9
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs
@@ -0,0 +1,375 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification.Parsing;
+
+using System;
+using System.Collections.Immutable;
+using System.Text.RegularExpressions;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Parsing;
+
+internal static class IdRouteParser
+{
+ private static readonly Lexer IdRouteLexer;
+ private static readonly Lexer IdLexer;
+
+ static IdRouteParser()
+ {
+ var sourceNameLexerRule = new RegexLexerRule(Tokens.SourceName, new Regex("[^~]+", RegexOptions.Compiled));
+ var sourcePathLexerRule = new RegexLexerRule(Tokens.SourcePath, new Regex("[^$~]+", RegexOptions.Compiled));
+ var sourceOriginLexerRule = new RegexLexerRule(Tokens.SourceOrigin, new Regex("[^$~//>]*", RegexOptions.Compiled));
+ var segmentNameLexerRule = new RegexLexerRule(Tokens.PathSegmentName, new Regex("[^$~//?(>]+", RegexOptions.Compiled));
+ var argumentNameLexerRule = new RegexLexerRule(Tokens.ArgumentName, new Regex(@"\G[^!=(]+", RegexOptions.Compiled));
+ var valueIdMetadataLexerRule = new RegexLexerRule(Tokens.ValueIdMetadata, new Regex("[^=]+", RegexOptions.Compiled));
+ var valueIdValueLexerRule = new RegexLexerRule(Tokens.ValueIdValue, new Regex(@"[^),\]&\#]+", RegexOptions.Compiled));
+ var fragmentLexerRule = new RegexLexerRule(Tokens.Fragment, new Regex(@"[^),\]&]+", RegexOptions.Compiled));
+ IdRouteLexer = new Lexer(
+ [sourceNameLexerRule,
+ sourcePathLexerRule,
+ sourceOriginLexerRule,
+ segmentNameLexerRule,
+ argumentNameLexerRule,
+ valueIdMetadataLexerRule,
+ valueIdValueLexerRule,
+ fragmentLexerRule]);
+ IdLexer = new Lexer(
+ [sourceNameLexerRule,
+ sourcePathLexerRule,
+ sourceOriginLexerRule,
+ segmentNameLexerRule,
+ argumentNameLexerRule,
+ valueIdMetadataLexerRule,
+ valueIdValueLexerRule,
+ fragmentLexerRule]);
+ }
+
+ public static R ParseIdRoute(string? input, IFormatProvider? formatProvider)
+ {
+ if (!input.HasValue)
+ {
+ return R.Error(IIdRouteError.EmptyOrNullError);
+ }
+
+ var parser = new Parser(IdRouteLexer, input, formatProvider);
+ var idRouteResult = IdRoute(parser);
+ if (idRouteResult.IsSuccess && parser.IsEnd())
+ {
+ return idRouteResult;
+ }
+
+ return R.Error(IIdRouteError.NotAtEndError);
+ }
+
+ public static R ParseId(string? input, IFormatProvider? formatProvider)
+ {
+ if (!input.HasValue)
+ {
+ return R.Error(IIdError.EmptyOrNullError);
+ }
+
+ var parser = new Parser(IdLexer, input, formatProvider);
+ var idResult = Id(parser);
+ if (idResult.IsSuccess && parser.IsEnd())
+ {
+ return idResult;
+ }
+
+ return R.Error(IIdError.NotAtEndError);
+ }
+
+ public static R ParseValueId(string? input, IFormatProvider? formatProvider)
+ {
+ if (!input.HasValue)
+ {
+ return R.Error(IParseValueIdError.EmptyOrNullError);
+ }
+
+ var parser = new Parser(IdLexer, input, formatProvider);
+ var valueIdResult = ValueId(parser);
+ if (valueIdResult.IsSuccess && parser.IsEnd())
+ {
+ return valueIdResult.MapError(x => (IParseValueIdError)x);
+ }
+
+ return R.Error(IParseValueIdError.NotAtEndError);
+ }
+
+ private static R IdRoute(Parser parser)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+ do
+ {
+ var idResult = Id(parser);
+ if (idResult.IsSuccess)
+ {
+ builder.Add(idResult.Value);
+ }
+ else
+ {
+ return R.Error(IIdRouteError._IdRouteIdError(idResult.Error));
+ }
+ }
+ while (parser.TryAccept(Grammar.IdSeparator));
+ return R.Success(new IdRoute(builder.ToValueList()));
+ }
+
+ private static R Id(Parser parser)
+ {
+ var sourceResult = Source(parser);
+ if (!sourceResult.IsSuccess)
+ {
+ return R.Error(IIdError._IdSourceError(sourceResult.Error));
+ }
+
+ Path? path = null;
+ if (parser.TryAccept(Grammar.PathSegmentSeparator))
+ {
+ var pathResult = Path(parser);
+ if (pathResult.IsSuccess)
+ {
+ path = pathResult.Value;
+ }
+ else
+ {
+ return R.Error(IIdError._IdPathError(pathResult.Error));
+ }
+ }
+
+ Arguments? arguments = null;
+ if (parser.TryAccept(Grammar.ArgumentsSeparator))
+ {
+ var valueIdsResult = Arguments(parser);
+ if (valueIdsResult.IsSuccess)
+ {
+ arguments = valueIdsResult.Value;
+ }
+ else
+ {
+ return R.Error(IIdError._IdValueIdError(valueIdsResult.Error));
+ }
+ }
+
+ Arguments? fragment = null;
+ if (parser.TryAccept(Grammar.FragmentSeparator))
+ {
+ var valueIdsResult = Arguments(parser);
+ if (valueIdsResult.IsSuccess)
+ {
+ fragment = valueIdsResult.Value;
+ }
+ else
+ {
+ return R.Error(IIdError._IdValueIdError(valueIdsResult.Error));
+ }
+ }
+
+ return R.Success(new Id(sourceResult.Value, path, arguments, fragment));
+ }
+
+ private static R Source(Parser parser)
+ {
+ return parser.Accept(Tokens.SourceName).MapError(lexerError => ISourceError._SourceError(Tokens.SourceName, lexerError))
+ .And(() => parser.Accept(Grammar.SourceNamePathSeparator).Map(lexerError => ISourceError._SourceError(Grammar.SourceNamePathSeparator, lexerError)))
+ .And(() => parser.Accept(Tokens.SourcePath).MapError(lexerError => ISourceError._SourceError(Tokens.SourcePath, lexerError)))
+ .And(() => parser.Accept(Grammar.SourcePathOriginSeparator).Map(lexerError => ISourceError._SourceError(Grammar.SourcePathOriginSeparator, lexerError)))
+ .And(() => parser.Accept(Tokens.SourceOrigin).MapError(lexerError => ISourceError._SourceError(Tokens.SourceOrigin, lexerError)))
+ .Map(x => new Source(x.Value3, x.Value2, x.Value1));
+ }
+
+ private static R Path(Parser parser)
+ {
+ var segments = ImmutableArray.CreateBuilder();
+ while (!parser.IsNext(Grammar.ArgumentsSeparator) && !parser.IsEnd())
+ {
+ var segmentResult = parser.Accept(Tokens.PathSegmentName).MapError(IPathError._SegmentNameError);
+ if (segmentResult.IsSuccess)
+ {
+ if (parser.TryAccept(Grammar.GroupStart))
+ {
+ if (parser.TryAccept(Grammar.GroupEnd))
+ {
+ segments.Add(new Segment(segmentResult.Value, new Arguments(ValueArray.Empty)));
+ }
+ else
+ {
+ var argumentsResult = Arguments(parser).MapError(IPathError._PathValueIdError)
+ .And(() => parser.Accept(Grammar.GroupEnd).Map(le => IPathError._PathEndError(Grammar.GroupStart, le)));
+ if (!argumentsResult.IsSuccess)
+ {
+ return R.Error(argumentsResult.Error);
+ }
+
+ segments.Add(new Segment(segmentResult.Value, argumentsResult.Value));
+ }
+ }
+ else
+ {
+ segments.Add(new Segment(segmentResult.Value, null));
+ }
+ }
+ else
+ {
+ return R.Error(segmentResult.Error);
+ }
+
+ if (!parser.TryAccept(Grammar.PathSegmentSeparator))
+ {
+ break;
+ }
+ }
+
+ if (segments.Count == 0)
+ {
+ return R.Error(IPathError._SegmentNameError(null));
+ }
+
+ return R.Success(new Path(segments.ToValueArray()));
+ }
+
+ private static R Arguments(Parser parser)
+ {
+ var valueIds = ImmutableArray.CreateBuilder();
+ while (!parser.IsEnd())
+ {
+ var argumentResult = Argument(parser);
+ if (argumentResult.IsSuccess)
+ {
+ valueIds.Add(argumentResult.Value);
+ if (!parser.Accept(Grammar.ArgumentSeparator))
+ {
+ break;
+ }
+ }
+ else
+ {
+ return R.Error(argumentResult.Error);
+ }
+ }
+
+ return R.Success(new Arguments(valueIds.ToValueArray()));
+ }
+
+ private static R Argument(Parser parser)
+ {
+ var argumentNameOption = parser.TryAccept(Tokens.ArgumentName);
+ var argumentResult = ValueId(parser).MapError(IArgumentsError._ValueIdError)
+ .Map(x => new Argument(argumentNameOption, x));
+ if (argumentResult.IsSuccess)
+ {
+ return argumentResult;
+ }
+
+ argumentResult = parser.TryAccept(
+ Grammar.KeyValueSeparator,
+ result => result.MapError(x => IArgumentsError.ExpectedCharacterError(Grammar.KeyValueSeparator, x)))
+ .And(() => Value(parser).MapError(IArgumentsError._ValueError))
+ .Map(x => new Argument(argumentNameOption, new ValueId(null, x.Value2)));
+ if (argumentResult.IsSuccess)
+ {
+ return argumentResult;
+ }
+
+ var valueIdResult = Value(parser);
+ if (valueIdResult.IsSuccess)
+ {
+ return R.Success(new Argument(argumentNameOption, new ValueId(null, valueIdResult.Value)));
+ }
+
+ parser.Undo();
+ valueIdResult = Value(parser);
+ if (valueIdResult.IsSuccess)
+ {
+ return R.Success(new Argument(null, new ValueId(null, valueIdResult.Value)));
+ }
+
+ return R.Error(IArgumentsError._ValueError(valueIdResult.Error));
+ }
+
+ private static R ValueId(Parser parser)
+ {
+ var metadataResult = parser.TryAccept(
+ Grammar.NameMetadataSeparator,
+ result => result.MapError(lexerError => IValueIdError.ExpectedCharacterError(Grammar.NameMetadataSeparator, lexerError))
+ .And(() => parser.Accept(Tokens.ValueIdMetadata).MapError(lexerError => IValueIdError.ValueIdError(Tokens.ValueIdMetadata, lexerError))));
+
+ var valueIdResult = parser.TryAccept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError.ValueIdError(Grammar.KeyValueSeparator, lexerError))
+ .And(() => Value(parser).MapError(IValueIdError.ValueIdValueError))
+ .Map(x => new ValueId(metadataResult.Value.Value2, x));
+ if (valueIdResult.IsSuccess)
+ {
+ return valueIdResult;
+ }
+
+ return Value(parser).Map(x => new ValueId(null, x), IValueIdError.ValueIdValueError);
+ }
+
+ private static R Value(Parser parser)
+ {
+ var valueResult = parser.TryAccept(
+ Grammar.GroupStart,
+ result =>
+ {
+ return result.MapError(lexerError => IValueError.ExpectedCharacterError(Grammar.GroupStart, lexerError))
+ .And(() => Arguments(parser).MapError(x => IValueError._ArgumentsError(x)))
+ .And(() => parser.Accept(Grammar.GroupEnd).Map(lexerError => IValueError.ExpectedCharacterError(Grammar.GroupEnd, lexerError)))
+ .Map(x => x.Value2);
+ }).Map(x => IValue.ComplexValue(x.Items));
+ if (valueResult.IsSuccess)
+ {
+ return valueResult;
+ }
+
+ valueResult = parser.TryAccept(
+ Grammar.ArrayStart,
+ result =>
+ {
+ return result.MapError(lexerError => IValueError.ExpectedCharacterError(Grammar.ArrayStart, lexerError))
+ .And(() =>
+ {
+ var valueIds = ImmutableArray.CreateBuilder();
+ while (!parser.IsNext(Grammar.ArrayEnd))
+ {
+ var singleValueIdResult = ValueId(parser);
+ if (singleValueIdResult.IsSuccess)
+ {
+ valueIds.Add(singleValueIdResult.Value);
+ if (!parser.Accept(Grammar.ArrayElementSeparator))
+ {
+ break;
+ }
+ }
+ else
+ {
+ return R.Error(IValueError._ValueIdError(singleValueIdResult.Error));
+ }
+ }
+
+ return R.Success(new ArrayValue(valueIds.ToValueArray())).Omits();
+ })
+ .And(() => parser.Accept(Grammar.ArrayEnd)
+ .Map(lexerError => IValueError.ExpectedCharacterError(Grammar.ArrayEnd, lexerError)))
+ .Map(x => x.Value2);
+ });
+ if (valueResult.IsSuccess)
+ {
+ return valueResult;
+ }
+
+ var literalValueResult = parser.TryAccept(
+ Grammar.LiteralSeparator,
+ result => result.MapError(lexerError => IValueError.ExpectedCharacterError(Grammar.LiteralSeparator, lexerError))
+ .And(() => parser.Accept(Tokens.ValueIdValue).Map(x => IValue.LiteralValue(x), IValueError._ValueError)))
+ .Map(x => x.Value2);
+ if (literalValueResult.IsSuccess)
+ {
+ return literalValueResult;
+ }
+
+ return parser.Accept(Tokens.ValueIdValue).Map(x => IValue.ScalarValue(Uri.UnescapeDataString(x)), IValueError._ValueError);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Parsing/Tokens.cs b/Source/Sundew.Base.Identification/Parsing/Tokens.cs
new file mode 100644
index 0000000..7465c17
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Parsing/Tokens.cs
@@ -0,0 +1,27 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification.Parsing;
+
+internal enum Tokens
+{
+ SourceName,
+
+ SourcePath,
+
+ SourceOrigin,
+
+ PathSegmentName,
+
+ ArgumentName,
+
+ ValueIdMetadata,
+
+ ValueIdValue,
+
+ Fragment,
+}
diff --git a/Source/Sundew.Base.Identification/Path.cs b/Source/Sundew.Base.Identification/Path.cs
new file mode 100644
index 0000000..ed946d1
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Path.cs
@@ -0,0 +1,96 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Text;
+
+///
+/// Represents a path composed of multiple segments separated by a forward slash ('/').
+///
+/// The collection of segments that make up the path, in order from root to leaf.
+public sealed record Path(ValueArray Segments)
+{
+ /// The path separator.
+ public const char Separator = '/';
+ /*
+ ///
+ /// GetScalar the from input path.
+ ///
+ /// The input path.
+ /// The path.
+ public static Path From(string inputPath)
+ {
+ return new Path(inputPath.Split(Separator));
+ }
+
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static Path Parse(string inputPath, IFormatProvider? formatProvider)
+ {
+ if (TryParse(inputPath, formatProvider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputPath} is not a valid {nameof(Path)}.");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputPath, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out Path result)
+ {
+ if (inputPath.HasValue)
+ {
+ var parts = inputPath.Split(Separator, StringSplitOptions.RemoveEmptyEntries);
+ if (parts.Length > 0)
+ {
+ result = new Path(parts);
+ return true;
+ }
+ }
+
+ result = null;
+ return false;
+ }*/
+
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider)
+ {
+ stringBuilder.AppendItems(this.Segments, (builder, segment) => segment.AppendInto(builder, formatProvider), Separator);
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture);
+ return stringBuilder.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Properties/AssemblyInfo.cs b/Source/Sundew.Base.Identification/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..d84e6b9
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Properties/AssemblyInfo.cs
@@ -0,0 +1,10 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Sundew.Base.Development.Tests")]
diff --git a/Source/Sundew.Base.Identification/RevisionId.cs b/Source/Sundew.Base.Identification/RevisionId.cs
new file mode 100644
index 0000000..eac39f2
--- /dev/null
+++ b/Source/Sundew.Base.Identification/RevisionId.cs
@@ -0,0 +1,25 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+///
+/// Default implementation of into a revision id.
+///
+/// The code.
+public readonly record struct RevisionId(uint Number) : ISequenceId
+{
+ ///
+ /// Creates a new revision id.
+ ///
+ /// The number.
+ /// The new revision id.
+ public static RevisionId Create(uint number)
+ {
+ return new RevisionId(number);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ScalarValue.cs b/Source/Sundew.Base.Identification/ScalarValue.cs
new file mode 100644
index 0000000..01f67e3
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ScalarValue.cs
@@ -0,0 +1,41 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Globalization;
+using System.Text;
+
+///
+/// Represents an argument in a .
+///
+/// The value.
+public sealed partial record ScalarValue(string Value) : IValue
+{
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions)
+ {
+ stringBuilder.Append(Uri.EscapeDataString(this.Value));
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true));
+ return stringBuilder.ToString();
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Segment.cs b/Source/Sundew.Base.Identification/Segment.cs
new file mode 100644
index 0000000..083b529
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Segment.cs
@@ -0,0 +1,39 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Text;
+
+///
+/// Represents a segment with a specified name and optional associated value identifiers.
+///
+/// The name of the segment, which serves as its identifier.
+/// A value for the segment.
+public sealed record Segment(string Name, Arguments? Arguments = null)
+{
+ ///
+ /// Appends the name of the current instance to the specified StringBuilder, followed by parentheses.
+ ///
+ /// The StringBuilder instance to which the name will be appended. This parameter cannot be null.
+ /// The format provider.
+ /// The updated StringBuilder instance containing the appended name.
+ public StringBuilder AppendInto(StringBuilder builder, IFormatProvider formatProvider)
+ {
+ builder.Append(this.Name);
+
+ if (this.Arguments.HasValue)
+ {
+ builder.Append('(');
+ this.Arguments.AppendInto(builder, formatProvider, new AppendOptions(true));
+ builder.Append(')');
+ }
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/SequenceId.cs b/Source/Sundew.Base.Identification/SequenceId.cs
new file mode 100644
index 0000000..91f706b
--- /dev/null
+++ b/Source/Sundew.Base.Identification/SequenceId.cs
@@ -0,0 +1,16 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+internal static class SequenceId
+ where TId : ISequenceId
+{
+#pragma warning disable SA1401
+ internal static uint CurrentId = 0;
+#pragma warning restore SA1401
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/SequenceIdExtensions.cs b/Source/Sundew.Base.Identification/SequenceIdExtensions.cs
new file mode 100644
index 0000000..6b26e82
--- /dev/null
+++ b/Source/Sundew.Base.Identification/SequenceIdExtensions.cs
@@ -0,0 +1,42 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+///
+/// Extends with easy to use methods.
+///
+public static class SequenceIdExtensions
+{
+ extension(TId id)
+ where TId : ISequenceId
+ {
+ ///
+ /// Creates the next id.
+ ///
+ /// The next id.
+ public static TId Next()
+ {
+ return TId.Create(unchecked((uint)Interlocked.Increment(ref Unsafe.As(ref SequenceId.CurrentId))));
+ }
+
+ ///
+ /// Check whether this instance is newer than the other.
+ ///
+ /// The other.
+ /// true, when this instance is newer.
+ public bool IsNewer(TId other)
+ {
+#pragma warning disable SA1101
+ return (int)(id.Number - other.Number) > 0;
+#pragma warning restore SA1101
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Source.cs b/Source/Sundew.Base.Identification/Source.cs
new file mode 100644
index 0000000..81fcc0b
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Source.cs
@@ -0,0 +1,138 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+
+///
+/// Represents a source for an .
+///
+/// The Origin.
+/// The Path.
+/// The Name.
+public sealed record Source(string Origin, string Path, string Name) : IParsable
+{
+ /// The origin separator.
+ public const char OriginSeparator = '$';
+
+ /// The name separator.
+ public const char NameSeparator = '~';
+
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static Source Parse(string inputSource, IFormatProvider? formatProvider)
+ {
+ if (TryParse(inputSource, formatProvider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputSource} is not a valid {nameof(Source)}");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputSource, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out Source result)
+ {
+ if (string.IsNullOrEmpty(inputSource))
+ {
+ result = null;
+ return false;
+ }
+
+ var originStartIndex = inputSource.IndexOf(OriginSeparator);
+ var nameEndIndex = inputSource.IndexOf(NameSeparator);
+ if (originStartIndex > -1 && nameEndIndex > -1)
+ {
+ var origin = inputSource.Substring(originStartIndex + 1);
+ var name = inputSource.Substring(0, nameEndIndex);
+ result = new Source(origin, inputSource.Substring(nameEndIndex + 1, originStartIndex - nameEndIndex - 1), name);
+ return true;
+ }
+
+ result = new Source(string.Empty, string.Empty, inputSource);
+ return true;
+ }
+
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider)
+ {
+ stringBuilder.Append(this.Name);
+ if (!string.IsNullOrEmpty(this.Path))
+ {
+ stringBuilder.Append(NameSeparator);
+ stringBuilder.Append(this.Path);
+ }
+
+ if (!string.IsNullOrEmpty(this.Origin))
+ {
+ stringBuilder.Append(OriginSeparator);
+ stringBuilder.Append(this.Origin);
+ }
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture);
+ return stringBuilder.ToString();
+ }
+
+ ///
+ /// Creates an for the specified type.
+ ///
+ /// The type.
+ /// A new >.
+ public static Source FromType(Type type)
+ {
+ var stringBuilder = new StringBuilder();
+ if (TargetEvaluator.GetTypeName(type, stringBuilder))
+ {
+ return new Source(string.Empty, string.Empty, stringBuilder.ToString());
+ }
+
+ return new Source(type.Assembly.GetName().Name ?? string.Empty, type.Namespace ?? string.Empty, stringBuilder.ToString());
+ }
+
+ ///
+ /// Tries to get the type for the .
+ ///
+ /// A result containing the type if successful.
+ public R TryGetType()
+ {
+ var knownType = TargetEvaluator.TryGetKnownType(this.Name);
+ if (knownType.IsSuccess)
+ {
+ return knownType;
+ }
+
+ var type = Type.GetType($"{this.Path}.{this.Name}, {this.Origin}");
+ return R.From(type);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj
new file mode 100644
index 0000000..2d507d5
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0;net9.0;net8.0
+ The AIdRoute, AId and ValueId for generic identification.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/Target.cs b/Source/Sundew.Base.Identification/Target.cs
new file mode 100644
index 0000000..974d576
--- /dev/null
+++ b/Source/Sundew.Base.Identification/Target.cs
@@ -0,0 +1,134 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Text;
+
+///
+/// Represents a composed of a source and an optional path.
+///
+/// The source component of the target.
+/// The path associated with the target.
+public sealed record Target(Source Source, Path? Path) : IParsable
+{
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format formatProvider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static Target Parse(string inputTarget, IFormatProvider? formatProvider)
+ {
+ if (TryParse(inputTarget, formatProvider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputTarget} is not a valid {nameof(Target)}");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputTarget, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out Target result)
+ {
+ if (inputTarget.HasValue)
+ {
+ var argumentsSeparatorIndex = inputTarget.IndexOf(Path.Separator);
+ if (argumentsSeparatorIndex > -1)
+ {
+ var targetString = inputTarget.Substring(0, argumentsSeparatorIndex);
+ var argumentsString = inputTarget.Substring(argumentsSeparatorIndex + 1);
+ if (Source.TryParse(targetString, formatProvider, out var entry) /* && Path.TryParse(argumentsString, formatProvider, out var path)*/)
+ {
+ result = new Target(entry, null);
+ return true;
+ }
+ }
+ else if (Source.TryParse(inputTarget, formatProvider, out var entry))
+ {
+ result = new Target(entry, null);
+ return true;
+ }
+ }
+
+ result = null;
+ return false;
+ }
+
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider)
+ {
+ this.Source.AppendInto(stringBuilder, formatProvider);
+ if (this.Path.HasValue)
+ {
+ stringBuilder.Append(Path.Separator);
+ this.Path.AppendInto(stringBuilder, formatProvider);
+ }
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture);
+ return stringBuilder.ToString();
+ }
+
+ ///
+ /// Tries to get the source type.
+ ///
+ /// A result containing the source type if successful.
+ public R TryGetSourceType()
+ {
+ return this.Source.TryGetType();
+ }
+
+ ///
+ /// Tries to get the result type.
+ ///
+ /// A result containing the source type if successful.
+ public R TryGetResultType()
+ {
+ return TargetEvaluator.GetResultType(this.Source, this.Path);
+ }
+
+ ///
+ /// Tries to get the input types.
+ ///
+ /// A result containing the input types if successful.
+ public R> TryGetInputTypes()
+ {
+ return TargetEvaluator.GetInputTypes(this.Source, this.Path, null);
+ }
+
+ ///
+ /// Tries to get the target type.
+ ///
+ /// A result containing the source type if successful.
+ public R TryGetContainingType()
+ {
+ return TargetEvaluator.GetDeclaringType(this.Source, this.Path);
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/TargetEvaluator.cs b/Source/Sundew.Base.Identification/TargetEvaluator.cs
new file mode 100644
index 0000000..eb17635
--- /dev/null
+++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs
@@ -0,0 +1,299 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Collections.Linq;
+using Sundew.Base.Identification.Parsing;
+using Sundew.Base.Text;
+
+internal static class TargetEvaluator
+{
+ private static readonly Dictionary PrimitiveAliases = new()
+ {
+ [typeof(bool)] = "bool",
+ [typeof(byte)] = "byte",
+ [typeof(sbyte)] = "sbyte",
+ [typeof(char)] = "char",
+ [typeof(decimal)] = "decimal",
+ [typeof(double)] = "double",
+ [typeof(float)] = "float",
+ [typeof(int)] = "int",
+ [typeof(uint)] = "uint",
+ [typeof(long)] = "long",
+ [typeof(ulong)] = "ulong",
+ [typeof(short)] = "short",
+ [typeof(ushort)] = "ushort",
+ [typeof(string)] = "string",
+ [typeof(object)] = "object",
+ [typeof(void)] = "void",
+ };
+
+ public static R GetResultType(Source source, Path? path)
+ {
+ var sourceType = source.TryGetType();
+ if (sourceType.IsError)
+ {
+ return R.Error();
+ }
+
+ if (!path.HasValue)
+ {
+ return sourceType;
+ }
+
+ var memberInfo = GetTargetMemberInfo(sourceType.Value, path);
+ if (memberInfo.HasValue)
+ {
+ switch (memberInfo)
+ {
+ case MethodInfo methodInfo:
+ return R.Success(methodInfo.ReturnType);
+ case PropertyInfo propertyInfo:
+ return R.Success(propertyInfo.PropertyType);
+ }
+ }
+
+ return R.Error();
+ }
+
+ public static R> GetInputTypes(Source source, Path? path, Arguments? arguments)
+ {
+ var sourceType = source.TryGetType();
+ if (sourceType.IsError)
+ {
+ return R.Error();
+ }
+
+ if (arguments.HasValue)
+ {
+ var typesFromMetadata = arguments.Items.Select(x => x.ValueId.TryGetType()).AllOrFailed(x => x.ToItem());
+ if (typesFromMetadata.IsSuccess)
+ {
+ return typesFromMetadata.Map(x => (IReadOnlyList)x.Items);
+ }
+
+ // Fall back to member signature when some arguments lack metadata (e.g. primitives/defaults)
+ if (path.HasValue)
+ {
+ var fallbackMemberInfo = GetTargetMemberInfo(sourceType.Value, path);
+ if (fallbackMemberInfo is MethodInfo fallbackMethodInfo)
+ {
+ var parameters = fallbackMethodInfo.GetParameters();
+ if (parameters.Length == arguments.Items.Count)
+ {
+ var types = new Type[parameters.Length];
+ for (var i = 0; i < parameters.Length; i++)
+ {
+ var argType = arguments.Items[i].ValueId.TryGetType();
+ types[i] = argType.IsSuccess ? argType.Value : parameters[i].ParameterType;
+ }
+
+ return R.Success>(types);
+ }
+ }
+ }
+
+ return R.Error();
+ }
+
+ if (!path.HasValue)
+ {
+ return R.Success>([sourceType.Value]);
+ }
+
+ var memberInfo = GetTargetMemberInfo(sourceType.Value, path);
+ if (memberInfo.HasValue)
+ {
+ switch (memberInfo)
+ {
+ case MethodInfo methodInfo:
+ return R.Success>(methodInfo.GetParameters().Select(x => x.ParameterType).ToArray());
+ case PropertyInfo propertyInfo:
+ return R.Success>([propertyInfo.PropertyType]);
+ }
+ }
+
+ return R.Error();
+ }
+
+ public static R GetDeclaringType(Source source, Path? path)
+ {
+ var sourceType = source.TryGetType();
+ if (sourceType.IsError)
+ {
+ return R.Error();
+ }
+
+ if (!path.HasValue)
+ {
+ return sourceType;
+ }
+
+ var memberInfo = GetTargetMemberInfo(sourceType.Value, path);
+ if (memberInfo.HasValue)
+ {
+ return R.From(memberInfo.DeclaringType);
+ }
+
+ return R.Error();
+ }
+
+ public static bool IsKnownType(Type type)
+ {
+ return PrimitiveAliases.ContainsKey(type);
+ }
+
+ internal static R TryGetKnownType(string? inputSource)
+ {
+ return R.From(PrimitiveAliases.FirstOrDefault(x => x.Value == inputSource).Key);
+ }
+
+ internal static bool GetTypeName(Type type, StringBuilder stringBuilder)
+ {
+ if (PrimitiveAliases.TryGetValue(type, out var alias))
+ {
+ stringBuilder.Append(alias);
+ return true;
+ }
+
+ if (type.IsArray)
+ {
+ var elementType = type.GetElementType()!;
+ var rank = type.GetArrayRank();
+ var commas = rank > 1 ? new string(',', rank - 1) : string.Empty;
+ GetTypeName(elementType, stringBuilder);
+ stringBuilder.Append($"[{commas}]");
+ return false;
+ }
+
+ if (type.IsGenericType)
+ {
+ var genericDef = type.GetGenericTypeDefinition();
+ var baseName = genericDef.Name;
+ var backtickIndex = baseName.IndexOf('`');
+ if (backtickIndex > 0)
+ {
+ baseName = baseName[..backtickIndex];
+ }
+
+ stringBuilder
+ .Append(baseName)
+ .Append(Grammar.ArrayStart)
+ .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), Grammar.ArrayElementSeparator)
+ .Append(Grammar.ArrayEnd);
+
+ return false;
+ }
+
+ // Nested types: strip the declaring type prefix, keep the + separator
+ // e.g. "MyType+MyNestedType" from FullName "MyNameSpace.MyType+MyNestedType"
+ if (type.IsNested)
+ {
+ // Walk up to build "OuterType+InnerType" without namespace
+ BuildNestedName(type, stringBuilder);
+ return false;
+ }
+
+ // Regular type: just the simple name
+ stringBuilder.Append(type.Name);
+ return false;
+ }
+
+ private static void BuildNestedName(Type type, StringBuilder stringBuilder)
+ {
+ if (!type.DeclaringType.HasValue)
+ {
+ stringBuilder.Append(type.Name);
+ return;
+ }
+
+ BuildNestedName(type.DeclaringType, stringBuilder);
+ stringBuilder.Append('+');
+ stringBuilder.Append(type.Name);
+ }
+
+ private static MemberInfo? GetTargetMemberInfo(Type sourceType, Path path)
+ {
+ var currentType = sourceType;
+ MemberInfo? memberInfo = null;
+ foreach (var segment in path.Segments)
+ {
+ var memberInfos = currentType.GetMember(segment.Name, MemberTypes.Method | MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
+ var cardinality = memberInfos.ByCardinality();
+ switch (cardinality)
+ {
+ case Empty empty:
+ break;
+ case Multiple multiple:
+ var valueIds = segment.Arguments?.Items ?? [];
+ var methodInfo = multiple.Items.OfType()
+ .Select(methodInfo => (methodInfo, parameters: methodInfo.GetParameters()))
+ .Where(x => x.parameters.Length == valueIds.Count)
+ .FirstOrDefault(x => IsMatch(x.parameters, valueIds)).methodInfo;
+ memberInfo = methodInfo;
+ if (methodInfo.HasValue)
+ {
+ currentType = methodInfo.ReturnType;
+ }
+
+ break;
+ case Single single:
+ if (single.Item is MethodInfo singleMethodInfo)
+ {
+ memberInfo = singleMethodInfo;
+ currentType = singleMethodInfo.ReturnType;
+ }
+ else if (single.Item is PropertyInfo propertyInfo)
+ {
+ memberInfo = propertyInfo;
+ currentType = propertyInfo.PropertyType;
+ }
+
+ break;
+ }
+ }
+
+ return memberInfo;
+ }
+
+ private static bool IsMatch(ParameterInfo[] parameterInfos, ValueArray arguments)
+ {
+ if (arguments.IsEmpty)
+ {
+ return parameterInfos.Length == 0;
+ }
+
+ return parameterInfos.Zip(arguments).All(x =>
+ {
+ var argumentType = x.Second.ValueId.Metadata.HasValue
+ ? Source.Parse(x.Second.ValueId.Metadata, CultureInfo.InvariantCulture).TryGetType().Value
+ : GetTypeFromArgument(x.First.ParameterType, x.Second.ValueId.Value);
+ return x.First.ParameterType.IsAssignableFrom(argumentType);
+ });
+ }
+
+ private static Type? GetTypeFromArgument(Type firstParameterType, IValue value)
+ {
+ const string parseName = "Parse";
+ var parseMethod = firstParameterType.GetMethod(parseName, BindingFlags.Public | BindingFlags.Static, [typeof(string), typeof(IFormatProvider)]);
+ if (parseMethod.HasValue)
+ {
+ return parseMethod.Invoke(null, [value.ToString(), CultureInfo.InvariantCulture])?.GetType();
+ }
+
+ parseMethod = firstParameterType.GetMethod(parseName, BindingFlags.Public | BindingFlags.Static, [typeof(string)]);
+ return parseMethod?.Invoke(null, [value.ToString()])?.GetType();
+ }
+}
diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs
new file mode 100644
index 0000000..35a693e
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ValueId.cs
@@ -0,0 +1,241 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text;
+using Sundew.Base.Identification.Parsing;
+
+///
+/// Represents a value id for an argument.
+///
+/// The metadata.
+/// The value.
+public sealed record ValueId(string? Metadata, IValue Value)
+{
+ ///
+ /// Gets the type of the source.
+ ///
+ /// A result containing the type is successful.
+ public R TryGetType()
+ {
+ if (Source.TryParse(this.Metadata, CultureInfo.InvariantCulture, out var source))
+ {
+ return source.TryGetType();
+ }
+
+ return R.Error();
+ }
+
+ ///
+ /// Creates a string representation of the .
+ ///
+ /// A string.
+ public override string ToString()
+ {
+ var stringBuilder = new StringBuilder();
+ this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true), false);
+ return stringBuilder.ToString();
+ }
+
+ ///
+ /// Appends this to the specified .
+ ///
+ /// The string builder.
+ /// The format provider.
+ /// The append options.
+ /// Indicates whether the value id has a name.
+ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions, bool requiresKeySeparation)
+ {
+ bool TryAppendMetadata()
+ {
+ if (!string.IsNullOrEmpty(this.Metadata))
+ {
+ stringBuilder.Append(Grammar.NameMetadataSeparator);
+ stringBuilder.Append(this.Metadata);
+ return true;
+ }
+
+ return false;
+ }
+
+ if (TryAppendMetadata() || requiresKeySeparation)
+ {
+ stringBuilder.Append(Grammar.KeyValueSeparator);
+ }
+
+ this.Value.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false });
+ }
+
+ ///
+ /// Creates an from the specified builder func.
+ ///
+ /// The type of the value.
+ /// The value.
+ /// The value id func.
+ /// A new .
+ public static ValueId From(TValue value, Action valueIdFunc)
+ {
+ var valueIdBuilder = new ValueIdBuilder(value?.GetType() ?? typeof(TValue));
+ valueIdFunc(value, valueIdBuilder);
+ return valueIdBuilder.Build();
+ }
+
+ ///
+ /// Parses the specified input string into an instance of the type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// An instance of ValueId that represents the parsed value from the input string.
+ /// Thrown if the input string is not in a valid format for the > type.
+ public static ValueId Parse(string inputArg, IFormatProvider? provider)
+ {
+ if (TryParse(inputArg, provider, out var result))
+ {
+ return result;
+ }
+
+ throw new FormatException($"The string: {inputArg} is not a valid {nameof(ValueId)}.");
+ }
+
+ ///
+ /// Tries to parse the specified input string into an instance of the > type.
+ ///
+ /// The string representation of the argument to be parsed. This value must be a valid format for the > type.
+ /// The format provider.
+ /// The result.
+ /// true if parsing was successful, otherwise false.
+ public static bool TryParse([NotNullWhen(true)] string? inputArg, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out ValueId result)
+ {
+ var valueIdResult = IdRouteParser.ParseValueId(inputArg, formatProvider);
+ result = valueIdResult.Value;
+ return valueIdResult.IsSuccess;
+ }
+
+ ///
+ /// Converts the specified initial value to a value of the specified type, using the current instance as context.
+ ///
+ /// The type of the value to convert. Must implement the interface.
+ /// The default value to be converted. Must be of type TValue.
+ /// The format provider.
+ /// A value of type TValue that is derived from the initial value and the current instance.
+ public TValue ToValue(TValue defaultValue, IFormatProvider formatProvider)
+ where TValue : IValueIdentifiable
+ {
+ return TValue.From(defaultValue, this, formatProvider);
+ }
+
+ ///
+ /// Converts the specified initial value to a value of the specified type, using the current instance as context.
+ ///
+ /// The type of the value to convert. Must implement the interface.
+ /// The default value to be converted. Must be of type TValue.
+ /// A value of type TValue that is derived from the initial value and the current instance.
+ public TValue ToValue(TValue defaultValue)
+ where TValue : IValueIdentifiable
+ {
+ return TValue.From(defaultValue, this, CultureInfo.CurrentCulture);
+ }
+
+ ///
+ /// Gets the value from the arguments.
+ ///
+ /// The type of the value.
+ /// The default value.
+ /// The format provider.
+ /// The argument name.
+ /// The retrieved value or the default value.
+ public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProvider, [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null)
+ where TValue : IParsable
+ {
+ if (!referenceName.HasValue)
+ {
+ throw new NotSupportedException("ReferenceName should be filled by compiler.");
+ }
+
+ if (this.Value is not ComplexValue complexValue)
+ {
+ return defaultValue;
+ }
+
+ static string GetRawString(IValue value) =>
+ value is ScalarValue scalarValue ? scalarValue.Value : value.ToString() ?? string.Empty;
+
+ var argument = complexValue.Items.FirstOrDefault(x => x.Name == referenceName);
+ if (argument.HasValue)
+ {
+ return TValue.Parse(GetRawString(argument.ValueId.Value), formatProvider);
+ }
+
+ var firstDotIndex = referenceName.IndexOf('.');
+ var fallback = firstDotIndex > -1
+ ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1)
+ : null;
+ argument = complexValue.Items.FirstOrDefault(x => x.Name == fallback);
+ if (argument.HasValue)
+ {
+ return TValue.Parse(GetRawString(argument.ValueId.Value), formatProvider);
+ }
+
+ return defaultValue;
+ }
+
+ ///
+ /// Gets the value from the arguments.
+ ///
+ /// The type of the value.
+ /// The default value.
+ /// The format provider.
+ /// The argument name.
+ /// The retrieved value or the default value.
+ public TValue GetValue(TValue defaultValue, IFormatProvider? formatProvider, [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null)
+ where TValue : IValueIdentifiable
+ {
+ if (!referenceName.HasValue)
+ {
+ throw new NotSupportedException("ReferenceName should be filled by compiler.");
+ }
+
+ if (this.Value is not ComplexValue complexValue)
+ {
+ return defaultValue;
+ }
+
+ var argument = complexValue.Items.FirstOrDefault(x => x.Name == referenceName);
+ if (argument.HasValue)
+ {
+ if (argument.ValueId.Value is LiteralValue { Value: LiteralValue.Null })
+ {
+ return defaultValue;
+ }
+
+ return TValue.From(defaultValue, argument.ValueId, formatProvider);
+ }
+
+ var firstDotIndex = referenceName.IndexOf('.');
+ var fallback = firstDotIndex > -1
+ ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1)
+ : null;
+ argument = complexValue.Items.FirstOrDefault(x => x.Name == fallback);
+ if (argument.HasValue)
+ {
+ if (argument.ValueId.Value is LiteralValue { Value: LiteralValue.Null })
+ {
+ return defaultValue;
+ }
+
+ return TValue.From(defaultValue, argument.ValueId, formatProvider);
+ }
+
+ return defaultValue;
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs
new file mode 100644
index 0000000..8577cb8
--- /dev/null
+++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs
@@ -0,0 +1,104 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Identification;
+
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Sundew.Base.Collections.Immutable;
+using Sundew.Base.Collections.Linq;
+
+///
+/// Builder for constructing for dynamic construction of identifiers.
+///
+/// The .
+public sealed class ValueIdBuilder(Type type)
+{
+ private readonly List values = new();
+
+ ///
+ /// Adds a value to the builder for dynamic construction of identifiers.
+ ///
+ /// The type of the value.
+ /// The value to be added to the builder. This can be any object representing an identifier component.
+ /// The name of the value being added.
+ /// The current instance of the builder, enabling method chaining.
+ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameof(value))] string? name = null)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ throw new NotSupportedException($"{nameof(name)} should be filled by compiler!");
+ }
+
+ if (char.IsLower(name[0]))
+ {
+ var dotIndex = name.IndexOf('.');
+ if (dotIndex > -1)
+ {
+ name = name.Substring(dotIndex + 1);
+ }
+ }
+
+ if (value != null && value is IValueIdentifiable valueIdentifiable)
+ {
+ var valueId = valueIdentifiable.Id;
+ this.values.Add(new Argument(name, valueId with { Metadata = GetMetadata(value.GetType(), typeof(TValue), false) }));
+ }
+ else if (value != null && value is IIdentifiable instanceIdentifiable)
+ {
+ var instanceId = instanceIdentifiable.Id;
+ this.values.Add(new Argument(name, new ValueId(GetMetadata(value.GetType(), typeof(TValue), false), new LiteralValue(instanceId.Number.ToString()))));
+ }
+ else if (value != null)
+ {
+ var stringValue = value.ToString();
+ if (stringValue.HasValue)
+ {
+ this.values.Add(new Argument(name, new ValueId(GetMetadata(value.GetType(), typeof(TValue), false), new ScalarValue(stringValue))));
+ }
+ }
+ else
+ {
+ this.values.Add(new Argument(name, new ValueId(null, new LiteralValue(LiteralValue.Null))));
+ }
+
+ return this;
+ }
+
+ ///
+ /// Builds the instance based on the values added to the builder. Each value is converted to an with its name and string representation of the value. The resulting .
+ ///
+ /// A new .
+ public ValueId Build()
+ {
+ var cardinality = this.values.ByCardinality();
+ var metadata = Source.FromType(type).ToString();
+ const string @null = "null";
+ return cardinality switch
+ {
+ Empty empty => new ValueId(metadata, new LiteralValue(@null)),
+ Multiple valueIds => new ValueId(metadata, new ComplexValue(valueIds.Items.ToValueArray())),
+ Single single => new ValueId(metadata, new ComplexValue(new[] { single.Item }.ToValueArray())),
+ };
+ }
+
+ private static string? GetMetadata(Type actualType, Type knownType, bool isRoot)
+ {
+ if (!isRoot && IsKnownType(actualType, knownType))
+ {
+ return null;
+ }
+
+ return Source.FromType(actualType).ToString();
+ }
+
+ private static bool IsKnownType(Type type, Type knownType)
+ {
+ return type == knownType || type.IsValueType;
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/ILexer.cs b/Source/Sundew.Base.Parsing/ILexer.cs
new file mode 100644
index 0000000..28b103b
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/ILexer.cs
@@ -0,0 +1,29 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+///
+/// Defines a contract for lexers that extract lexemes from input strings based on tokens.
+///
+/// Specifies the type of tokens that the lexer processes.
+public interface ILexer
+ where TToken : notnull
+{
+ ///
+ /// Attempts to extract the lexeme associated with the specified token from the input string.
+ ///
+ /// The token for which to retrieve the corresponding lexeme.
+ /// The input string from which the lexeme is to be extracted.
+ /// The current parser state, which may influence how the lexeme is determined.
+ /// When this method returns , contains the extracted lexeme for the specified token;
+ /// otherwise, the value is undefined.
+ /// When this method returns , contains the number of characters consumed from the input to
+ /// extract the lexeme; otherwise, the value is undefined.
+ /// if the lexeme was successfully extracted; otherwise, .
+ bool TryGetLexeme(TToken token, string input, Parser.State state, out string lexeme, out int consumedLength);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/ILexerRule.cs b/Source/Sundew.Base.Parsing/ILexerRule.cs
new file mode 100644
index 0000000..0931278
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/ILexerRule.cs
@@ -0,0 +1,32 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+///
+/// Defines a contract for a lexer rule that identifies and extracts tokens from input text based on the current parser
+/// state.
+///
+/// The type of token produced by this lexer rule.
+public interface ILexerRule
+ where TToken : notnull
+{
+ ///
+ /// Gets the token associated with this instance.
+ ///
+ TToken Token { get; }
+
+ ///
+ /// Attempts to extract a lexeme from the specified input string based on the current parser state.
+ ///
+ /// The input string from which to extract the lexeme. This parameter must not be null or empty.
+ /// The current parser state that determines how the lexeme is identified. This parameter must be a valid and
+ /// initialized state.
+ /// A result containing a tuple with the extracted lexeme and the number of characters consumed from the input. If
+ /// extraction fails, returns an error describing the reason.
+ R<(string Lexeme, int ConsumedLength), LexerError> TryGetLexeme(string input, Parser.State state);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/Lexer.cs b/Source/Sundew.Base.Parsing/Lexer.cs
new file mode 100644
index 0000000..381b214
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/Lexer.cs
@@ -0,0 +1,65 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System.Collections.Generic;
+using System.Linq;
+
+///
+/// Provides functionality to tokenize input strings based on defined lexer rules.
+///
+/// Specifies the type of tokens that the lexer will recognize. This type must be a non-nullable type.
+public class Lexer : ILexer
+ where TToken : notnull
+{
+ private readonly Dictionary> lexerRules;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// An enumerable collection of lexer rules that define how tokens are recognized. Each rule must specify a unique
+ /// token.
+ public Lexer(IEnumerable> lexerRules)
+ {
+ this.lexerRules = lexerRules.ToDictionary(x => x.Token);
+ }
+
+ ///
+ /// Attempts to extract the lexeme corresponding to the specified token from the input string, using the current
+ /// parser state.
+ ///
+ /// The token for which to retrieve the associated lexeme.
+ /// The input string from which the lexeme is to be extracted.
+ /// The current parser state, which may influence how the lexeme is determined.
+ /// When this method returns, contains the extracted lexeme if successful; otherwise, an empty string.
+ /// When this method returns, contains the number of characters consumed from the input if successful; otherwise,
+ /// zero.
+ /// true if the lexeme was successfully extracted for the specified token; otherwise, false.
+ public bool TryGetLexeme(
+ TToken token,
+ string input,
+ Parser.State state,
+ out string lexeme,
+ out int consumedLength)
+ {
+ if (this.lexerRules.TryGetValue(token, out var lexerRule))
+ {
+ var result = lexerRule.TryGetLexeme(input, state);
+ if (result.IsSuccess)
+ {
+ lexeme = result.Value.Lexeme;
+ consumedLength = result.Value.ConsumedLength;
+ return true;
+ }
+ }
+
+ lexeme = string.Empty;
+ consumedLength = 0;
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/LexerError.cs b/Source/Sundew.Base.Parsing/LexerError.cs
new file mode 100644
index 0000000..1f8acac
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/LexerError.cs
@@ -0,0 +1,102 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System.Collections.Generic;
+using Sundew.DiscriminatedUnions;
+
+///
+/// Represents a lexer error.
+///
+[DiscriminatedUnion]
+public abstract partial record LexerError
+{
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The error message.
+ ///
+ public abstract string GetMessage();
+
+ ///
+ /// Represents a lexical analysis error that includes details about the invalid input segment and its location within the source text.
+ ///
+ /// The token type.
+ /// The zero-based index in the input string where the error was detected.
+ /// The length of the invalid segment in the input string that triggered the error.
+ public sealed partial record TokenTypeError(object TokenType, int Position, int Length) : LexerError
+ {
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The error message.
+ ///
+ public override string GetMessage()
+ {
+ return $"Invalid token type: {this.TokenType} at position: {this.Position}";
+ }
+ }
+
+ ///
+ /// Represents a lexical analysis error that includes details about the invalid input segment and its location within the source text.
+ ///
+ /// The input string that contains the invalid segment that caused the lexical error.
+ /// The zero-based index in the input string where the error was detected.
+ /// The length of the invalid segment in the input string that triggered the error.
+ public sealed partial record TokenError(string Token, int Position, int Length) : LexerError
+ {
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The error message.
+ ///
+ public override string GetMessage()
+ {
+ return $"Invalid input: {this.Token} at position: {this.Position}";
+ }
+ }
+
+ ///
+ /// Represents a lexical analysis error when the end was expected, but the input still contained more characters.
+ ///
+ /// The zero-based index in the input string where the error was detected.
+ public sealed partial record End(int Position) : LexerError
+ {
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The error message.
+ ///
+ public override string GetMessage()
+ {
+ return $"Expected end at position: {this.Position}";
+ }
+ }
+
+ ///
+ /// Represents a collection of lexer errors that occurred during input processing.
+ ///
+ /// The collection of lexer errors encountered, providing context for the invalid input.
+ public sealed partial record Multiple(IEnumerable Errors) : LexerError
+ {
+ ///
+ /// Gets the message.
+ ///
+ ///
+ /// The error message.
+ ///
+ public override string GetMessage()
+ {
+ return $"Invalid input:";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/ParseSettings.cs b/Source/Sundew.Base.Parsing/ParseSettings.cs
new file mode 100644
index 0000000..4904fa6
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/ParseSettings.cs
@@ -0,0 +1,75 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System.Globalization;
+
+///
+/// Parameter class for specifying parse settings.
+///
+public class ParseSettings
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The culture information.
+ /// if set to true [assert end].
+ /// if set to true [throw on error].
+ public ParseSettings(CultureInfo cultureInfo, bool assertEnd, bool throwOnError)
+ {
+ this.CultureInfo = cultureInfo;
+ this.AssertEnd = assertEnd;
+ this.ThrowOnError = throwOnError;
+ }
+
+ ///
+ /// Gets the default.
+ ///
+ ///
+ /// The default.
+ ///
+ public static ParseSettings DefaultInvariantCulture { get; } = new(
+ CultureInfo.InvariantCulture,
+ true,
+ true);
+
+ ///
+ /// Gets the default current culture.
+ ///
+ ///
+ /// The default current culture.
+ ///
+ public static ParseSettings DefaultCurrentCulture { get; } = new(
+ CultureInfo.CurrentCulture,
+ true,
+ true);
+
+ ///
+ /// Gets the culture information.
+ ///
+ ///
+ /// The culture information.
+ ///
+ public CultureInfo CultureInfo { get; }
+
+ ///
+ /// Gets a value indicating whether parsing should assert the ending.
+ ///
+ ///
+ /// true if parse should assert the ending; otherwise, false.
+ ///
+ public bool AssertEnd { get; }
+
+ ///
+ /// Gets a value indicating whether parsing should throw an expcetion on error.
+ ///
+ ///
+ /// true if an exception should be thrown; otherwise, false.
+ ///
+ public bool ThrowOnError { get; }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/Parser.cs b/Source/Sundew.Base.Parsing/Parser.cs
new file mode 100644
index 0000000..2964e41
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/Parser.cs
@@ -0,0 +1,319 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+
+///
+/// Provides functionality to parse a sequence of tokens from an input string using a specified lexer.
+///
+/// The type of tokens produced by the lexer.
+[DebuggerTypeProxy(typeof(ParserDebugView<>))]
+[DebuggerDisplay("{Current}")]
+public class Parser
+ where TToken : notnull
+{
+ private readonly ILexer lexer;
+ private readonly Stack stateStack = new Stack();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The lexer used to tokenize the input string. This parameter must not be null.
+ /// The input string to be parsed. This parameter cannot be null or empty.
+ /// The format provider.
+ public Parser(ILexer lexer, string input, IFormatProvider? formatProvider)
+ {
+ this.lexer = lexer;
+ this.Input = input;
+ this.FormatProvider = formatProvider ?? CultureInfo.CurrentCulture;
+ this.stateStack.Push(new State(0));
+ }
+
+ ///
+ /// Gets the input being processed.
+ ///
+ public string Input { get; }
+
+ ///
+ /// Gets the format provider used to control formatting operations for values such as numbers and dates.
+ ///
+ public IFormatProvider FormatProvider { get; }
+
+ internal string Current => this.Input.Substring(this.stateStack.Peek().Position);
+
+ ///
+ /// Determines whether the specified token is accepted at the current position and retrieves the corresponding
+ /// lexeme if accepted.
+ ///
+ /// The token to evaluate for acceptance at the current input position.
+ /// When this method returns, contains the lexeme associated with the accepted token if the token is accepted;
+ /// otherwise, an empty string.
+ /// true if the token is accepted at the current position; otherwise, false.
+ public bool Accept(TToken token, out string lexeme)
+ {
+ var state = this.stateStack.Peek();
+ if (this.lexer.TryGetLexeme(token, this.Input, state, out lexeme, out var consumedLength))
+ {
+ this.stateStack.Push(new State(state.Position + consumedLength));
+ return true;
+ }
+
+ lexeme = string.Empty;
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified token is accepted at the current position and retrieves the corresponding
+ /// lexeme if accepted.
+ ///
+ /// The token to evaluate for acceptance at the current input position.
+ /// true if the token is accepted at the current position; otherwise, false.
+ public R Accept(TToken token)
+ {
+ var state = this.stateStack.Peek();
+ if (this.lexer.TryGetLexeme(token, this.Input, state, out var lexeme, out var consumedLength))
+ {
+ this.stateStack.Push(new State(state.Position + consumedLength));
+ return R.Success(lexeme);
+ }
+
+ return R.Error(LexerError._TokenTypeError(token, state.Position, 0));
+ }
+
+ ///
+ /// Determines whether the specified token is accepted at the current position and retrieves the corresponding
+ /// lexeme if accepted.
+ ///
+ /// The token to evaluate for acceptance at the current input position.
+ /// true if the token is accepted at the current position; otherwise, false.
+ public string? TryAccept(TToken token)
+ {
+ var state = this.stateStack.Peek();
+ if (this.lexer.TryGetLexeme(token, this.Input, state, out var lexeme, out var consumedLength))
+ {
+ this.stateStack.Push(new State(state.Position + consumedLength));
+ return lexeme;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Determines whether the specified token is accepted at the current position and retrieves the corresponding
+ /// lexeme if accepted.
+ ///
+ /// The type of the first output.
+ /// The type of the error.
+ /// The token to evaluate for acceptance at the current input position.
+ /// The next match func.
+ /// true if the token is accepted at the current position; otherwise, false.
+ public R TryAccept(TToken token, Func, R> matchNextFunc)
+ {
+ var state = this.stateStack.Peek();
+ if (this.lexer.TryGetLexeme(token, this.Input, state, out var lexeme, out var consumedLength))
+ {
+ this.stateStack.Push(new State(state.Position + consumedLength));
+ var result = matchNextFunc(R.Success(lexeme));
+ if (result.IsSuccess)
+ {
+ return result;
+ }
+
+ this.stateStack.Pop();
+ return R.Error(result.Error);
+ }
+
+ return matchNextFunc(R.Error(LexerError._TokenTypeError(token, state.Position, 0)));
+ }
+
+ ///
+ /// Determines whether the specified token is accepted at the current position and retrieves the corresponding
+ /// lexeme if accepted.
+ ///
+ /// The type of the first output.
+ /// The type of the error.
+ /// The character to evaluate against the expected input for the current parsing state.
+ /// The next match func.
+ /// true if the token is accepted at the current position; otherwise, false.
+ public R TryAccept(char input, Func, R> matchNextFunc)
+ {
+ var state = this.stateStack.Peek();
+ if (this.IsNext(input))
+ {
+ this.stateStack.Push(new State(state.Position + 1));
+ var result = matchNextFunc(R.Success(input.ToString()));
+ if (result.IsSuccess)
+ {
+ return result;
+ }
+
+ this.stateStack.Pop();
+ return R.Error(result.Error);
+ }
+
+ return matchNextFunc(R.Error(LexerError._TokenError(input.ToString(), state.Position, 0)));
+ }
+
+ ///
+ /// Determines whether the specified character matches the expected input for the current parsing state and advances
+ /// the state if a match is found.
+ ///
+ /// If the input character matches the expected value, the parsing state is updated to the next
+ /// position. Otherwise, the state remains unchanged.
+ /// The character to evaluate against the expected input for the current parsing state.
+ /// true if the input character matches the expected input and the state is advanced; otherwise, false.
+ public RoE TryAccept(char input)
+ {
+ var state = this.stateStack.Peek();
+ if (this.IsNext(input))
+ {
+ this.stateStack.Push(new State(state.Position + 1));
+ return R.Success();
+ }
+
+ return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1));
+ }
+
+ ///
+ /// Determines whether the specified character matches the expected input for the current parsing state and advances
+ /// the state if a match is found.
+ ///
+ /// If the input character matches the expected value, the parsing state is updated to the next
+ /// position. Otherwise, the state remains unchanged.
+ /// The character to evaluate against the expected input for the current parsing state.
+ /// true if the input character matches the expected input and the state is advanced; otherwise, false.
+ public RoE Accept(char input)
+ {
+ var state = this.stateStack.Peek();
+ if (this.IsNext(input))
+ {
+ this.stateStack.Push(new State(state.Position + 1));
+ return R.Success();
+ }
+
+ return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1));
+ }
+
+ ///
+ /// Determines whether the specified input matches the expected next input and advances the internal state if the
+ /// match is successful.
+ ///
+ /// The input string to evaluate against the expected next input. Cannot be null.
+ /// true if the input matches the expected next input and the state is advanced; otherwise, false.
+ public RoE Accept(string input)
+ {
+ var state = this.stateStack.Peek();
+ if (this.IsNext(input))
+ {
+ this.stateStack.Push(new State(state.Position + input.Length));
+ return R.Success();
+ }
+
+ return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1));
+ }
+
+ ///
+ /// Determines whether the specified character matches the character at the current position in the input.
+ ///
+ /// The character to compare with the character at the current input position.
+ /// true if the specified character matches the current character in the input; otherwise, false.
+ public bool IsNext(char input)
+ {
+ var state = this.stateStack.Peek();
+ if (this.Input.Length > state.Position && this.Input[state.Position] == input)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the specified string matches the input sequence at the current position.
+ ///
+ /// The string to compare with the input sequence at the current position. The length of this string must not exceed
+ /// the number of remaining characters in the input sequence.
+ /// true if the specified string matches the input sequence at the current position; otherwise, false.
+ public bool IsNext(string input)
+ {
+ var state = this.stateStack.Peek();
+ if (this.Input.Length > state.Position + input.Length - 1 &&
+ this.Input.AsSpan(state.Position, input.Length).SequenceEqual(input.AsSpan()))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines whether the current position in the input has reached the end of the input sequence.
+ ///
+ /// true if the current position is at the end of the input; otherwise, false.
+ public RoE AcceptEnd()
+ {
+ var state = this.stateStack.Peek();
+ return R.FromError(state.Position == this.Input.Length, () => LexerError._End(state.Position));
+ }
+
+ ///
+ /// Determines whether the current position in the input has reached the end of the input sequence.
+ ///
+ /// true if the current position is at the end of the input; otherwise, false.
+ public RoE IsEnd()
+ {
+ var state = this.stateStack.Peek();
+ return R.FromError(state.Position == this.Input.Length, () => LexerError._End(state.Position));
+ }
+
+ ///
+ /// Undoes the last accepted change.
+ ///
+ /// A value indicating whether the undo was successful.
+ public bool Undo()
+ {
+ if (this.stateStack.Count > 1)
+ {
+ this.stateStack.Pop();
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Gets the current state of the parser.
+ ///
+ /// The current state represented as a instance.
+ public State CurrentState()
+ {
+ return this.stateStack.Peek();
+ }
+
+ ///
+ /// Represents the current state of an item within its parent collection, including its position.
+ ///
+ public readonly record struct State
+ {
+ internal State(int position)
+ {
+ this.Position = position;
+ }
+
+ ///
+ /// Gets the zero-based index that indicates the current position of the item within its parent collection.
+ ///
+ /// The position reflects the item's index in the collection, where the first item has a
+ /// position of 0. This property is read-only and updates automatically as the collection changes.
+ public int Position { get; }
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/ParserDebugView.cs b/Source/Sundew.Base.Parsing/ParserDebugView.cs
new file mode 100644
index 0000000..f82e8b3
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/ParserDebugView.cs
@@ -0,0 +1,24 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System.Diagnostics;
+
+internal class ParserDebugView
+ where TToken : notnull
+{
+ private readonly Parser parser;
+
+ public ParserDebugView(Parser parser)
+ {
+ this.parser = parser;
+ }
+
+ [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
+ public string State => this.parser.Input.Substring(this.parser.CurrentState().Position);
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/RegexLexerRule.cs b/Source/Sundew.Base.Parsing/RegexLexerRule.cs
new file mode 100644
index 0000000..af78175
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/RegexLexerRule.cs
@@ -0,0 +1,57 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Copyright (c) Sundews. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Sundew.Base.Parsing;
+
+using System.Text.RegularExpressions;
+
+///
+/// Represents a lexer rule that matches input text using a specified regular expression and produces a corresponding
+/// token when a match is found.
+///
+/// The type of token produced by the lexer rule.
+/// The token to associate with input that matches the regular expression.
+/// The regular expression used to identify matching lexemes in the input text.
+public class RegexLexerRule(TToken token, Regex regex) : ILexerRule
+ where TToken : notnull
+{
+ private const string TokenGroupName = "TOKEN";
+
+ ///
+ /// Gets the token associated with the current instance.
+ ///
+ public TToken Token { get; } = token;
+
+ ///
+ /// Attempts to extract a lexeme from the specified input string, starting at the position indicated by the parser
+ /// state.
+ ///
+ /// The input string from which to extract the lexeme. Cannot be null or empty.
+ /// The current parser state, which specifies the position in the input string at which to begin matching.
+ /// An R containing a tuple with the extracted lexeme and the number of characters consumed if a match is found;
+ /// otherwise, an error indicating the failure to retrieve a lexeme.
+ public R<(string Lexeme, int ConsumedLength), LexerError> TryGetLexeme(string input, Parser.State state)
+ {
+ var match = regex.Match(input, state.Position);
+ if (match.Success && match.Index == state.Position)
+ {
+ if (match.Groups.TryGetValue(TokenGroupName, out var matchingGroup))
+ {
+ if (matchingGroup.Success)
+ {
+ return R.Success((matchingGroup.Value, match.Length));
+ }
+
+ return R.Error(LexerError._TokenError(input, state.Position, -1));
+ }
+
+ return R.Success((match.Value, match.Length));
+ }
+
+ return R.Error(LexerError._TokenError(input, state.Position, -1));
+ }
+}
\ No newline at end of file
diff --git a/Source/Sundew.Base.Parsing/Sundew.Base.Parsing.csproj b/Source/Sundew.Base.Parsing/Sundew.Base.Parsing.csproj
new file mode 100644
index 0000000..1d55835
--- /dev/null
+++ b/Source/Sundew.Base.Parsing/Sundew.Base.Parsing.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0;net9.0;net8.0
+ The common concepts for implementing a parser.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Source/Sundew.Base.Primitives/Failure.cs b/Source/Sundew.Base.Primitives/Failure.cs
index edf97eb..0bbf61c 100644
--- a/Source/Sundew.Base.Primitives/Failure.cs
+++ b/Source/Sundew.Base.Primitives/Failure.cs
@@ -24,7 +24,7 @@ public abstract partial record Failure
/// the failure. It is commonly used in result-based patterns to distinguish between success and failure
/// outcomes.
/// The reason.
- public sealed record Failed(TFailure Reason) : Failure;
+ public sealed partial record Failed(TFailure Reason) : Failure;
///