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 + + true false 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; /// /// Represents a failure state caused by a cancellation, including the reason for the cancellation. @@ -33,7 +33,7 @@ public sealed record Failed(TFailure Reason) : Failure; /// for the cancellation through the property. It is a specialized type of . /// The reason for the cancellation. - public sealed record Canceled(CancelReason CancelReason) : Failure; + public sealed partial record Canceled(CancelReason CancelReason) : Failure; /// /// Represents a failure that occurred due to an exception. @@ -42,7 +42,7 @@ public sealed record Canceled(CancelReason CancelReason) : Failure; /// caused by exceptions in a structured manner. It is typically used in scenarios where exceptions need to be /// propagated or logged as part of a failure result. /// The exception. - public sealed record ExceptionOccured(Exception Exception) : Failure; + public sealed partial record ExceptionOccured(Exception Exception) : Failure; } /// @@ -58,7 +58,7 @@ public abstract partial record Failure /// for the cancellation through the property. It is a specialized type of . /// The reason for the cancellation. - public sealed record Canceled(CancelReason CancelReason) : Failure; + public sealed partial record Canceled(CancelReason CancelReason) : Failure; /// /// Represents a failure that occurred due to an exception. @@ -67,5 +67,5 @@ public sealed record Canceled(CancelReason CancelReason) : Failure; /// caused by exceptions in a structured manner. It is typically used in scenarios where exceptions need to be /// propagated or logged as part of a failure result. /// The exception. - public sealed record ExceptionOccured(Exception Exception) : Failure; + public sealed partial record ExceptionOccured(Exception Exception) : Failure; } \ No newline at end of file diff --git a/Source/Sundew.Base.Primitives/ResultExtensions.cs b/Source/Sundew.Base.Primitives/ResultExtensions.cs new file mode 100644 index 0000000..8d552d3 --- /dev/null +++ b/Source/Sundew.Base.Primitives/ResultExtensions.cs @@ -0,0 +1,172 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +using System; + +/// +/// Extensions for and . +/// +public static class ResultExtensions +{ +#pragma warning disable SA1101 + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The error type. + /// The result. + /// The otherResultFunc result. + /// A new result. + public static R And(this R result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return result; + } + + return new R(false, default, otherResult.Error); + } + + return result; + } + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The success 2 type. + /// The error type. + /// The result. + /// The otherResultFunc result. + /// A new result. + public static R<(TSuccess1 Value1, TSuccess2 Value2), TError> And(this R result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return new R<(TSuccess1, TSuccess2), TError>(true, (result.Value, otherResult.Value), otherResult.Error); + } + + return new R<(TSuccess1, TSuccess2), TError>(false, default, otherResult.Error); + } + + return new R<(TSuccess1, TSuccess2), TError>(false, default, result.Error); + } + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The success 2 type. + /// The error type. + /// The result. + /// The other result func. + /// A new result. + public static R<(TSuccess1 Value1, TSuccess2 Value2), TError> And(this R<(TSuccess1 Value1, TSuccess2 Value2), TError> result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return result; + } + + return new R<(TSuccess1 Value1, TSuccess2 Value2), TError>(false, default, otherResult.Error); + } + + return result; + } + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The success 2 type. + /// The success 3 type. + /// The error type. + /// The result. + /// The otherResultFunc result. + /// A new result. + public static R<(TSuccess1 Value1, TSuccess2 Value2, TSuccess3 Value3), TError> And(this R<(TSuccess1 Value1, TSuccess2 Value2), TError> result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return new R<(TSuccess1, TSuccess2, TSuccess3), TError>(true, (result.Value.Value1, result.Value.Value2, otherResult.Value), default); + } + + return new R<(TSuccess1, TSuccess2, TSuccess3), TError>(false, default, otherResult.Error); + } + + return new R<(TSuccess1, TSuccess2, TSuccess3), TError>(false, default, result.Error); + } + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The success 2 type. + /// The success 3 type. + /// The error type. + /// The result. + /// The other result func. + /// A new result. + public static R<(TSuccess1 Value1, TSuccess2 Value2, TSuccess3 Value3), TError> And(this R<(TSuccess1 Value1, TSuccess2 Value2, TSuccess3 Value3), TError> result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return result; + } + + return new R<(TSuccess1, TSuccess2, TSuccess3), TError>(false, default, otherResult.Error); + } + + return result; + } + + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The success 1 type. + /// The success 2 type. + /// The success 3 type. + /// The success 4 type. + /// The error type. + /// The result. + /// The other result func. + /// A new result. + public static R<(TSuccess1 Value1, TSuccess2 Value2, TSuccess3 Value3, TSuccess4 Value4), TError> And(this R<(TSuccess1 Value1, TSuccess2 Value2, TSuccess3 Value3), TError> result, Func> otherResultFunc) + { + if (result.IsSuccess) + { + var otherResult = otherResultFunc(); + if (otherResult.IsSuccess) + { + return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(true, (result.Value.Value1, result.Value.Value2, result.Value.Value3, otherResult.Value), default); + } + + return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(false, default, otherResult.Error); + } + + return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(false, default, result.Error); + } +#pragma warning restore SA1101 +} \ No newline at end of file diff --git a/Source/Sundew.Base.Primitives/ResultOfErrorExtensions.cs b/Source/Sundew.Base.Primitives/ResultOfErrorExtensions.cs new file mode 100644 index 0000000..e34f034 --- /dev/null +++ b/Source/Sundew.Base.Primitives/ResultOfErrorExtensions.cs @@ -0,0 +1,40 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +using System; + +/// +/// Extensions for and . +/// +public static class ResultOfErrorExtensions +{ + /// + /// Extensions for . + /// + /// The success type. + /// The error type. + extension(RoE result) + { + /// + /// Gets a new result that is successful, if both results were a success. + /// + /// The other result. + /// A new result. + public R And(Func> other) + { +#pragma warning disable SA1101 + if (result.IsSuccess) + { + return other(); + } + + return result.Map(default(TSuccess)!); + } + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Primitives/RoE{TError}.cs b/Source/Sundew.Base.Primitives/RoE{TError}.cs index 062f871..8617344 100644 --- a/Source/Sundew.Base.Primitives/RoE{TError}.cs +++ b/Source/Sundew.Base.Primitives/RoE{TError}.cs @@ -129,6 +129,26 @@ public static implicit operator Task>(RoE error) return !(left == right); } + /// + /// Implements the true operator. + /// + /// the result. + /// true if the result is successful, otherwise false. + public static bool operator true(RoE result) + { + return result.IsSuccess; + } + + /// + /// Implements the false operator. + /// + /// the result. + /// true if the result is erroneous, otherwise false. + public static bool operator false(RoE result) + { + return result.IsError; + } + /// /// Checks if the result is an error and passes the error through the out parameter. /// diff --git a/Source/Sundew.Base.Primitives/R{TSuccess,TError}.cs b/Source/Sundew.Base.Primitives/R{TSuccess,TError}.cs index 047357a..f5d1b18 100644 --- a/Source/Sundew.Base.Primitives/R{TSuccess,TError}.cs +++ b/Source/Sundew.Base.Primitives/R{TSuccess,TError}.cs @@ -77,7 +77,7 @@ internal R(bool isSuccess, TSuccess? value, TError? error) /// public TError? Error { get; } - /// + /*/// /// Gets the result's success property. /// /// The result. @@ -88,7 +88,7 @@ internal R(bool isSuccess, TSuccess? value, TError? error) public static implicit operator bool(R r) { return r.IsSuccess; - } + }*/ /// /// Gets the result's success property. @@ -242,6 +242,26 @@ public static implicit operator Task>(R r) return !(left == right); } + /// + /// Implements the true operator. + /// + /// the result. + /// true if the result is successful, otherwise false. + public static bool operator true(R result) + { + return result.IsSuccess; + } + + /// + /// Implements the false operator. + /// + /// the result. + /// true if the result is erroneous, otherwise false. + public static bool operator false(R result) + { + return result.IsError; + } + /// /// Checks if the result is a success and passes the value through the out parameter. /// diff --git a/Source/Sundew.Base.Primitives/R{TSuccess}.cs b/Source/Sundew.Base.Primitives/R{TSuccess}.cs index 5e83c3a..06949d8 100644 --- a/Source/Sundew.Base.Primitives/R{TSuccess}.cs +++ b/Source/Sundew.Base.Primitives/R{TSuccess}.cs @@ -132,6 +132,26 @@ public static implicit operator Task>(R r) return !(left == right); } + /// + /// Implements the true operator. + /// + /// the result. + /// true if the result is successful, otherwise false. + public static bool operator true(R result) + { + return result.IsSuccess; + } + + /// + /// Implements the false operator. + /// + /// the result. + /// true if the result is erroneous, otherwise false. + public static bool operator false(R result) + { + return result.IsError; + } + /// /// Evaluates the result into a single value. /// diff --git a/Source/Sundew.Base.Primitives/Sundew.Base.Primitives.csproj b/Source/Sundew.Base.Primitives/Sundew.Base.Primitives.csproj index 364cda1..6f0eefb 100644 --- a/Source/Sundew.Base.Primitives/Sundew.Base.Primitives.csproj +++ b/Source/Sundew.Base.Primitives/Sundew.Base.Primitives.csproj @@ -7,11 +7,11 @@ - + - + diff --git a/Source/Sundew.Base.Threading/PostSubmitAction.cs b/Source/Sundew.Base.Threading/PostSubmitAction.cs index 879808d..8e3d894 100644 --- a/Source/Sundew.Base.Threading/PostSubmitAction.cs +++ b/Source/Sundew.Base.Threading/PostSubmitAction.cs @@ -20,18 +20,18 @@ public abstract partial record PostSubmitAction /// /// Represents a post apply action that performs no operation. /// - public sealed record None : PostSubmitAction; + public sealed partial record None : PostSubmitAction; /// /// Represents an action that sets a value during post-processing. /// /// The value to be set by this action. - public sealed record SetValue(TValue Value) : PostSubmitAction; + public sealed partial record SetValue(TValue Value) : PostSubmitAction; /// /// Represents a post apply action that triggers a refresh of the value, carrying associated information. /// - public sealed record Refresh(TParameter Parameter) : PostSubmitAction; + public sealed partial record Refresh(TParameter Parameter) : PostSubmitAction; /// /// Represents a post-apply action that refreshes the target when it becomes idle. @@ -40,5 +40,5 @@ public sealed record Refresh(TParameter Parameter) : PostSubmitAction - public sealed record RefreshOnIdle(TParameter Parameter) : PostSubmitAction; + public sealed partial record RefreshOnIdle(TParameter Parameter) : PostSubmitAction; } \ No newline at end of file diff --git a/Source/Sundew.Base.Threading/ValueSynchronizer.cs b/Source/Sundew.Base.Threading/ValueSynchronizer.cs index 3261f6d..0c190f4 100644 --- a/Source/Sundew.Base.Threading/ValueSynchronizer.cs +++ b/Source/Sundew.Base.Threading/ValueSynchronizer.cs @@ -237,7 +237,6 @@ private async Task HandleCancellation(object submissionId, Enabler enabler) #else enabler.Cancel(); #endif - //// using (await this.@lock.LockAsync(CancellationToken.None).ConfigureAwait(false)) lock (this.@lock) { this.pendingSubmissions.Remove(submissionId, out var _); diff --git a/Source/Sundew.Base.slnx b/Source/Sundew.Base.slnx index 9484eef..f6c6a61 100644 --- a/Source/Sundew.Base.slnx +++ b/Source/Sundew.Base.slnx @@ -36,6 +36,7 @@ + @@ -44,6 +45,7 @@ +