From 22f4ebe06e7e9c1ace74f850448c9ad8a6b437e0 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Mon, 9 Mar 2026 00:13:16 +0100 Subject: [PATCH 01/20] Added Sundew.Base.Identification --- .../Identification/AIdTests.cs | 190 +++++++++++++++ .../Sundew.Base.Development.Tests.csproj | 1 + Source/Sundew.Base.Identification/AId.cs | 160 +++++++++++++ Source/Sundew.Base.Identification/Argument.cs | 128 +++++++++++ .../Sundew.Base.Identification/Arguments.cs | 217 ++++++++++++++++++ .../IValueIdentifiable.cs | 29 +++ Source/Sundew.Base.Identification/Path.cs | 147 ++++++++++++ Source/Sundew.Base.Identification/Source.cs | 132 +++++++++++ .../Sundew.Base.Identification.csproj | 13 ++ Source/Sundew.Base.Identification/Target.cs | 157 +++++++++++++ .../TargetEvaluator.cs | 208 +++++++++++++++++ .../ValueIdBuilder.cs | 65 ++++++ .../Sundew.Base.Primitives.csproj | 4 +- Source/Sundew.Base.slnx | 1 + 14 files changed, 1450 insertions(+), 2 deletions(-) create mode 100644 Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs create mode 100644 Source/Sundew.Base.Identification/AId.cs create mode 100644 Source/Sundew.Base.Identification/Argument.cs create mode 100644 Source/Sundew.Base.Identification/Arguments.cs create mode 100644 Source/Sundew.Base.Identification/IValueIdentifiable.cs create mode 100644 Source/Sundew.Base.Identification/Path.cs create mode 100644 Source/Sundew.Base.Identification/Source.cs create mode 100644 Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj create mode 100644 Source/Sundew.Base.Identification/Target.cs create mode 100644 Source/Sundew.Base.Identification/TargetEvaluator.cs create mode 100644 Source/Sundew.Base.Identification/ValueIdBuilder.cs diff --git a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs new file mode 100644 index 0000000..05ac7d4 --- /dev/null +++ b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs @@ -0,0 +1,190 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; +using static Sundew.Base.Development.Tests.Identification.AIdTests; + +public class AIdTests +{ + [Test] + [Obsolete("Obsolete")] + public void T() + { + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); + var t1 = Uri.EscapeUriString(uri6!.OriginalString); + var t2 = Uri.EscapeUriString(uri8!.OriginalString); + var t3 = Uri.EscapeUriString(uri9!.OriginalString); + } + + [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,Description)?person=(Address=Home,Number=15)&description=(Eyes=Blue)")] + [Arguments("Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]")] + public void Parse_Then_ResultShouldNotBeNull(string input) + { + var result = AId.Parse(input, CultureInfo.InvariantCulture); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.ToString().Should().Be(input); + } + } + + [Test] + public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; + var result = AId.From(x => x.GoBack()); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position)"; + var result = AId.From(x => x.Navigate(null!)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position,bool)"; + var result = AId.From(x => x.Navigate(null!, default)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 = "AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; + var result = AId.From(x => x.X); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 AsArguments_Then_ResultShouldBeExpectedResult() + { + const string expectedResult = "X=4&Y=5"; + var position = new Position(4, 5); + + var arguments = position.AsArguments(); + var result = Position.From(new Position(0, 0), arguments); + + using (new AssertionScope()) + { + arguments.ToString().Should().Be(expectedResult); + result.Should().Be(position); + } + } + + [Test] + public void AsArguments_Then_ResultShouldBeExpectedResult2() + { + const string expectedResult = "Position=(X=4&Y=5)&Z=6"; + var position = new Position3D(new Position(4, 5), 6); + + var arguments = position.AsArguments(); + var result = Position3D.From(new Position3D(new Position(0, 0), 0), arguments); + + using (new AssertionScope()) + { + arguments.ToString().Should().Be(expectedResult); + result.Should().Be(position); + } + } + +#pragma warning disable SA1201 + public interface INavigator +#pragma warning restore SA1201 + { + void GoBack(); + + void Navigate(Position position); + + void Navigate(Position position, bool addToHistory); + } + + public record Position(int X, int Y) : IValueIdentifiable + { + public Arguments AsArguments() => Arguments.From(builder => builder.Add(this.X).Add(this.Y)); + + public static Position From(Position position, Arguments arguments) + { + return new Position( + arguments.Get(position.X, CultureInfo.InvariantCulture), + arguments.Get(position.Y, CultureInfo.InvariantCulture)); + } + } + + public record Position3D(Position Position, int Z) : IValueIdentifiable + { + public Arguments AsArguments() + { + return Arguments.From(builder => builder.Add(this.Position).Add(this.Z)); + } + + public static Position3D From(Position3D value, Arguments arguments) + { + return new Position3D( + arguments.Get2(value.Position, CultureInfo.InvariantCulture), + arguments.Get(value.Z, CultureInfo.InvariantCulture)); + } + } +} \ No newline at end of file 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..993b86e 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 @@ + diff --git a/Source/Sundew.Base.Identification/AId.cs b/Source/Sundew.Base.Identification/AId.cs new file mode 100644 index 0000000..7133a02 --- /dev/null +++ b/Source/Sundew.Base.Identification/AId.cs @@ -0,0 +1,160 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +/// +/// Represents any Id. +/// +public record AId(Target Target, Arguments? Arguments) : IParsable +{ + /// The arguments separator. + public const char ArgumentsSeparator = '?'; + + /// + /// 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 Argument 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 AId Parse(string inputAId, IFormatProvider? provider) + { + if (TryParse(inputAId, provider, out var result)) + { + return result; + } + + throw new FormatException($"The string: {inputAId} is not a valid {nameof(AId)}"); + } + + /// + /// 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? inputAId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId result) + { + if (inputAId.HasValue) + { + var argumentsSeparatorIndex = inputAId.IndexOf(ArgumentsSeparator); + if (argumentsSeparatorIndex > -1) + { + var targetString = inputAId.Substring(0, argumentsSeparatorIndex); + var argumentsString = inputAId.Substring(argumentsSeparatorIndex + 1); + if (Target.TryParse(targetString, formatProvider, out var target) && Identification.Arguments.TryParse(argumentsString, formatProvider, out var args)) + { + result = new AId(target, args); + return true; + } + } + else if (Target.TryParse(inputAId, formatProvider, out var target)) + { + result = new AId(target, 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.Target.AppendInto(stringBuilder, formatProvider); + if (this.Arguments.HasValue) + { + stringBuilder.Append(ArgumentsSeparator); + this.Arguments.Value.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.Target.TryGetSourceType(); + } + + /// + /// Tries to get the result type. + /// + /// A result containing the result type if successful. + public R TryGetResultType() + { + return this.Target.TryGetResultType(); + } + + /// + /// Tries to get the input types. + /// + /// A result containing the input types if successful. + public R> TryGetInputTypes() + { + return this.Target.TryGetInputTypes(); + } + + /// + /// Tries to get the target containing type. + /// + /// A result containing the containing type if successful. + public R TryGetTargetContainingType() + { + return this.Target.TryGetContainingType(); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static AId From(Expression> targetExpression) + { + var target = Target.From(targetExpression); + return new AId(target, null); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static AId From(Expression> targetExpression) + { + var target = Target.From(targetExpression); + return new AId(target, null); + } +} \ 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..90f5638 --- /dev/null +++ b/Source/Sundew.Base.Identification/Argument.cs @@ -0,0 +1,128 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 an argument in a . +/// +/// The name. +/// The value. +public record Argument(string? Name, string Value) : IParsable +{ + /// Key Value separator. + public const char KeyValueSeparator = '='; + + /// + /// 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 Argument 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 Argument 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(Argument)}."); + } + + /// + /// 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 Argument result) + { + if (inputArg.HasValue) + { + string key = string.Empty; + var level = 0; + var index = 0; + while (index < inputArg.Length) + { + var character = inputArg[index++]; + if (character == Arguments.GroupStartSeparator) + { + level++; + } + + if (character == Arguments.GroupEndSeparator) + { + level--; + } + + if (character == KeyValueSeparator && level == 0) + { + result = new Argument(inputArg.Substring(0, index - 1), inputArg.Substring(index)); + return true; + } + } + + result = new Argument(null, inputArg); + return true; + } + + result = null; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + if (!string.IsNullOrEmpty(this.Name)) + { + stringBuilder.Append(this.Name).Append(KeyValueSeparator); + } + + stringBuilder.Append(this.Value); + } + + /// + /// Creates a string representation of the . + /// + /// A string. + public override string ToString() + { + var stringBuilder = new StringBuilder(); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + return stringBuilder.ToString(); + } + + /// + /// Attempts to parse the current value into an instance of the Arguments class. + /// + /// A result containing the parsed Arguments if successful. + public R TryGetValueArguments() + { + return this.TryGetValueArguments(CultureInfo.CurrentCulture); + } + + /// + /// Attempts to parse the current value into an instance of the Arguments class. + /// + /// The format provider. + /// A result containing the parsed Arguments if successful. + public R TryGetValueArguments(IFormatProvider formatProvider) + { + return R.From(Arguments.TryParse(this.Value, formatProvider, out var args), args); + } +} \ 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..038fe89 --- /dev/null +++ b/Source/Sundew.Base.Identification/Arguments.cs @@ -0,0 +1,217 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using Sundew.Base.Collections.Immutable; +using Sundew.Base.Text; + +#pragma warning disable CS8907, CS1591 +/// +/// Represents arguments for an . +/// +/// The arguments. +public readonly record struct Arguments(ValueArray Items) : IParsable +{ + /// The argument separator. + public const char ArgumentsSeparator = '&'; + + /// The group start separator. + public const char GroupStartSeparator = '('; + + /// The group end separator. + public const char GroupEndSeparator = ')'; + + /// + /// 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 Argument 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 Arguments Parse(string inputArgs, IFormatProvider? formatProvider) + { + if (TryParse(inputArgs, formatProvider, out var result)) + { + return result; + } + + throw new FormatException($"The string: {inputArgs} is not a valid {nameof(Arguments)}."); + } + + /// + /// 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? inputArguments, IFormatProvider? formatProvider, out Arguments result) + { + var args = ImmutableArray.CreateBuilder(); + if (inputArguments.HasValue) + { + var argStartIndex = 0; + var index = 0; + var level = 0; + while (index < inputArguments.Length) + { + var character = inputArguments[index++]; + if (character == GroupStartSeparator) + { + level++; + } + else if (character == GroupEndSeparator) + { + level--; + } + else if (character == ArgumentsSeparator && level == 0) + { + if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) + { + args.Add(arg); + argStartIndex = index; + } + else + { + result = default; + return false; + } + } + } + + if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) + { + args.Add(arg2); + } + + result = new Arguments(args.ToImmutable()); + return true; + } + + result = default; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + stringBuilder.AppendItems(this.Items, (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), Arguments.ArgumentsSeparator); + } + + /// + /// 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 from the specified builder func. + /// + /// The value id func. + /// A new . + public static Arguments From(Action valueIdFunc) + { + var valueIdBuilder = new ValueIdBuilder(); + valueIdFunc(valueIdBuilder); + return valueIdBuilder.Build(); + } + + /// + /// 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 Get(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + return TValue.Parse(argument.Value, formatProvider); + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + return TValue.Parse(argument.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 Get2(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + var innerArguments = argument.TryGetValueArguments(); + if (innerArguments.IsSuccess) + { + return TValue.From(defaultValue, innerArguments.Value); + } + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + var innerArguments = argument.TryGetValueArguments(); + if (innerArguments.IsSuccess) + { + return TValue.From(defaultValue, innerArguments.Value); + } + } + + return defaultValue; + } +} \ 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..4527b9c --- /dev/null +++ b/Source/Sundew.Base.Identification/IValueIdentifiable.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.Identification; + +/// +/// Interface for implementing a value identifiable. +/// +/// The type of the value. +public interface IValueIdentifiable +{ + /// + /// Gets the value id. + /// + /// The value id. + Arguments AsArguments(); + + /// + /// Creates a value from the value id. + /// + /// The initial value. + /// The arguments. + /// The created value. + static abstract TValue From(TValue value, Arguments arguments); +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Path.cs b/Source/Sundew.Base.Identification/Path.cs new file mode 100644 index 0000000..bb34e19 --- /dev/null +++ b/Source/Sundew.Base.Identification/Path.cs @@ -0,0 +1,147 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +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) : IParsable +{ + /// The path separator. + public const char Separator = '/'; + + /// The parameter separator. + public const char ParameterSeparator = ','; + + /// + /// Get 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 Argument 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.AppendJoin(Separator, this.Segments); + } + + /// + /// Creates a string representation of the . + /// + /// A string. + public override string ToString() + { + var stringBuilder = new StringBuilder(); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + return stringBuilder.ToString(); + } + + /// + /// Gets a for the specified expression. + /// + /// The path expression. + /// A new . + public static Path From(Expression pathExpression) + { + var segments = ImmutableArray.CreateBuilder(); + EvaluateToPath(pathExpression); + + return new Path(segments.ToImmutable()); + + void EvaluateToPath(Expression expression) + { + switch (expression) + { + case LambdaExpression lambdaExpression: + EvaluateToPath(lambdaExpression.Body); + break; + case MethodCallExpression methodCallExpression: + segments.Add($"{methodCallExpression.Method.Name}({GetParameters(methodCallExpression.Method.GetParameters())})"); + break; + case MemberExpression memberExpression: + if (memberExpression.Expression.HasValue) + { + EvaluateToPath(memberExpression.Expression); + } + + segments.Add(memberExpression.Member.Name); + break; + case UnaryExpression unaryExpression: + EvaluateToPath(unaryExpression.Operand); + break; + } + } + } + + private static string GetParameters(ParameterInfo[] parameterInfos) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendItems(parameterInfos, (stringBuilder, parameter) => stringBuilder.Append(TargetEvaluator.GetTypeName(parameter.ParameterType)), ParameterSeparator); + return stringBuilder.ToString(); + } +} \ 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..ae8551b --- /dev/null +++ b/Source/Sundew.Base.Identification/Source.cs @@ -0,0 +1,132 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 Argument 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 = null; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + stringBuilder.Append($"{this.Name}{NameSeparator}{this.Path}{OriginSeparator}{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(); + GetName(type, false); + + void GetName(Type type, bool isParent) + { + if (type.DeclaringType.HasValue) + { + GetName(type.DeclaringType, true); + } + + stringBuilder.Append(type.Name); + if (isParent) + { + stringBuilder.Append('+'); + } + } + + 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 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..d8b9110 --- /dev/null +++ b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0;net8.0 + The IReporter interface for assisting with implementation of reporter/loggers. + + + + + + + + \ 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..b19ccdc --- /dev/null +++ b/Source/Sundew.Base.Identification/Target.cs @@ -0,0 +1,157 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +/// +/// 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 Argument 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, path); + 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); + } + + /// + /// Tries to get the target type. + /// + /// A result containing the source type if successful. + public R TryGetContainingType() + { + return TargetEvaluator.GetDeclaringType(this.Source, this.Path); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static Target From(Expression> targetExpression) + { + return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static Target From(Expression> targetExpression) + { + return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); + } +} \ 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..2283b5d --- /dev/null +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -0,0 +1,208 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Linq; +using System.Reflection; + +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) + { + var sourceType = source.TryGetType(); + if (sourceType.IsError) + { + 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(); + } + + internal static string? GetTypeName(Type type) + { + if (type.IsArray) + { + var elementType = type.GetElementType()!; + var rank = type.GetArrayRank(); + var commas = rank > 1 ? new string(',', rank - 1) : string.Empty; + return $"{GetTypeName(elementType)}[{commas}]"; + } + + if (type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + var baseName = genericDef.Name; + var backtickIndex = baseName.IndexOf('`'); + if (backtickIndex > 0) + { + baseName = baseName[..backtickIndex]; + } + + var genericParameters = type.GetGenericArguments().Select(GetTypeName); + + return $"{baseName}[{string.Join(',', genericParameters)}]"; + } + + if (PrimitiveAliases.TryGetValue(type, out var alias)) + { + return alias; + } + + // 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 + return BuildNestedName(type); + } + + // Regular type: just the simple name + return type.Name; + } + + private static MemberInfo? GetTargetMemberInfo(Type sourceType, Path path) + { + var currentType = sourceType; + MemberInfo? memberInfo = null; + foreach (var segment in path.Segments) + { + var segmentType = GetSegment(segment); + var memberInfos = currentType.GetMember(segmentType.Name, MemberTypes.Method | MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (segmentType.IsProperty) + { + var propertyInfo = memberInfos.OfType().FirstOrDefault(); + memberInfo = propertyInfo; + if (propertyInfo.HasValue) + { + currentType = propertyInfo.PropertyType; + } + } + else + { + var methodInfo = memberInfos.OfType().FirstOrDefault(x => x.GetParameters().Select(x => GetTypeName(x.ParameterType)).SequenceEqual(segmentType.ParameterNames)); + memberInfo = methodInfo; + if (methodInfo.HasValue) + { + currentType = methodInfo.ReturnType; + } + } + } + + return memberInfo; + } + + private static (string Name, bool IsProperty, IReadOnlyList ParameterNames) GetSegment(string segment) + { + if (segment.EndsWith(')')) + { + var parametersStartIndex = segment.IndexOf('('); + return (segment.Substring(0, parametersStartIndex), false, segment.Substring(parametersStartIndex + 1, segment.Length - parametersStartIndex - 2).Split(',', StringSplitOptions.RemoveEmptyEntries)); + } + + return (segment, true, Array.Empty()); + } + + private static string BuildNestedName(Type type) + { + if (!type.DeclaringType.HasValue) + { + return type.Name; + } + + return $"{BuildNestedName(type.DeclaringType)}+{type.Name}"; + } +} diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs new file mode 100644 index 0000000..afe06f7 --- /dev/null +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.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.Identification; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Sundew.Base.Collections.Immutable; + +/// +/// Builder for constructing for dynamic construction of identifiers. +/// +public sealed class ValueIdBuilder +{ + private readonly List<(string Name, object Value)> 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 (!name.HasValue) + { + throw new NotSupportedException($"{nameof(name)} should be filled by compiler!"); + } + + name = name.Replace("this.", string.Empty); + if (value is IValueIdentifiable valueIdentifiable) + { + this.values.Add((name, $"{Arguments.GroupStartSeparator}{valueIdentifiable.AsArguments().ToString()}{Arguments.GroupEndSeparator}")); + } + else if (value != null) + { + var stringValue = value.ToString(); + if (stringValue.HasValue) + { + this.values.Add((name, stringValue)); + } + } + + 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 Arguments Build() + { + return new Arguments() + { + Items = this.values.Select(x => new Argument(x.Name, x.Value.ToString()!)).ToValueArray(), + }; + } +} \ No newline at end of file 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.slnx b/Source/Sundew.Base.slnx index 9484eef..d66c0ec 100644 --- a/Source/Sundew.Base.slnx +++ b/Source/Sundew.Base.slnx @@ -36,6 +36,7 @@ + From b36907d05795e8f6a82817f0880666d9e63b8e9f Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Mon, 9 Mar 2026 00:13:16 +0100 Subject: [PATCH 02/20] Added Sundew.Base.Identification --- .../Identification/AIdTests.cs | 190 +++++++++++++++ .../Sundew.Base.Development.Tests.csproj | 1 + Source/Sundew.Base.Identification/AId.cs | 160 +++++++++++++ Source/Sundew.Base.Identification/Argument.cs | 128 +++++++++++ .../Sundew.Base.Identification/Arguments.cs | 217 ++++++++++++++++++ .../IValueIdentifiable.cs | 29 +++ Source/Sundew.Base.Identification/Path.cs | 147 ++++++++++++ Source/Sundew.Base.Identification/Source.cs | 132 +++++++++++ .../Sundew.Base.Identification.csproj | 13 ++ Source/Sundew.Base.Identification/Target.cs | 157 +++++++++++++ .../TargetEvaluator.cs | 208 +++++++++++++++++ .../ValueIdBuilder.cs | 65 ++++++ .../Sundew.Base.Primitives.csproj | 4 +- Source/Sundew.Base.slnx | 1 + 14 files changed, 1450 insertions(+), 2 deletions(-) create mode 100644 Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs create mode 100644 Source/Sundew.Base.Identification/AId.cs create mode 100644 Source/Sundew.Base.Identification/Argument.cs create mode 100644 Source/Sundew.Base.Identification/Arguments.cs create mode 100644 Source/Sundew.Base.Identification/IValueIdentifiable.cs create mode 100644 Source/Sundew.Base.Identification/Path.cs create mode 100644 Source/Sundew.Base.Identification/Source.cs create mode 100644 Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj create mode 100644 Source/Sundew.Base.Identification/Target.cs create mode 100644 Source/Sundew.Base.Identification/TargetEvaluator.cs create mode 100644 Source/Sundew.Base.Identification/ValueIdBuilder.cs diff --git a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs new file mode 100644 index 0000000..05ac7d4 --- /dev/null +++ b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs @@ -0,0 +1,190 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; +using static Sundew.Base.Development.Tests.Identification.AIdTests; + +public class AIdTests +{ + [Test] + [Obsolete("Obsolete")] + public void T() + { + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); + var t1 = Uri.EscapeUriString(uri6!.OriginalString); + var t2 = Uri.EscapeUriString(uri8!.OriginalString); + var t3 = Uri.EscapeUriString(uri9!.OriginalString); + } + + [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,Description)?person=(Address=Home,Number=15)&description=(Eyes=Blue)")] + [Arguments("Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]")] + public void Parse_Then_ResultShouldNotBeNull(string input) + { + var result = AId.Parse(input, CultureInfo.InvariantCulture); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + result.ToString().Should().Be(input); + } + } + + [Test] + public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; + var result = AId.From(x => x.GoBack()); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position)"; + var result = AId.From(x => x.Navigate(null!)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position,bool)"; + var result = AId.From(x => x.Navigate(null!, default)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 = "AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; + var result = AId.From(x => x.X); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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 AsArguments_Then_ResultShouldBeExpectedResult() + { + const string expectedResult = "X=4&Y=5"; + var position = new Position(4, 5); + + var arguments = position.AsArguments(); + var result = Position.From(new Position(0, 0), arguments); + + using (new AssertionScope()) + { + arguments.ToString().Should().Be(expectedResult); + result.Should().Be(position); + } + } + + [Test] + public void AsArguments_Then_ResultShouldBeExpectedResult2() + { + const string expectedResult = "Position=(X=4&Y=5)&Z=6"; + var position = new Position3D(new Position(4, 5), 6); + + var arguments = position.AsArguments(); + var result = Position3D.From(new Position3D(new Position(0, 0), 0), arguments); + + using (new AssertionScope()) + { + arguments.ToString().Should().Be(expectedResult); + result.Should().Be(position); + } + } + +#pragma warning disable SA1201 + public interface INavigator +#pragma warning restore SA1201 + { + void GoBack(); + + void Navigate(Position position); + + void Navigate(Position position, bool addToHistory); + } + + public record Position(int X, int Y) : IValueIdentifiable + { + public Arguments AsArguments() => Arguments.From(builder => builder.Add(this.X).Add(this.Y)); + + public static Position From(Position position, Arguments arguments) + { + return new Position( + arguments.Get(position.X, CultureInfo.InvariantCulture), + arguments.Get(position.Y, CultureInfo.InvariantCulture)); + } + } + + public record Position3D(Position Position, int Z) : IValueIdentifiable + { + public Arguments AsArguments() + { + return Arguments.From(builder => builder.Add(this.Position).Add(this.Z)); + } + + public static Position3D From(Position3D value, Arguments arguments) + { + return new Position3D( + arguments.Get2(value.Position, CultureInfo.InvariantCulture), + arguments.Get(value.Z, CultureInfo.InvariantCulture)); + } + } +} \ No newline at end of file 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..993b86e 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 @@ + diff --git a/Source/Sundew.Base.Identification/AId.cs b/Source/Sundew.Base.Identification/AId.cs new file mode 100644 index 0000000..7133a02 --- /dev/null +++ b/Source/Sundew.Base.Identification/AId.cs @@ -0,0 +1,160 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +/// +/// Represents any Id. +/// +public record AId(Target Target, Arguments? Arguments) : IParsable +{ + /// The arguments separator. + public const char ArgumentsSeparator = '?'; + + /// + /// 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 Argument 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 AId Parse(string inputAId, IFormatProvider? provider) + { + if (TryParse(inputAId, provider, out var result)) + { + return result; + } + + throw new FormatException($"The string: {inputAId} is not a valid {nameof(AId)}"); + } + + /// + /// 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? inputAId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId result) + { + if (inputAId.HasValue) + { + var argumentsSeparatorIndex = inputAId.IndexOf(ArgumentsSeparator); + if (argumentsSeparatorIndex > -1) + { + var targetString = inputAId.Substring(0, argumentsSeparatorIndex); + var argumentsString = inputAId.Substring(argumentsSeparatorIndex + 1); + if (Target.TryParse(targetString, formatProvider, out var target) && Identification.Arguments.TryParse(argumentsString, formatProvider, out var args)) + { + result = new AId(target, args); + return true; + } + } + else if (Target.TryParse(inputAId, formatProvider, out var target)) + { + result = new AId(target, 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.Target.AppendInto(stringBuilder, formatProvider); + if (this.Arguments.HasValue) + { + stringBuilder.Append(ArgumentsSeparator); + this.Arguments.Value.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.Target.TryGetSourceType(); + } + + /// + /// Tries to get the result type. + /// + /// A result containing the result type if successful. + public R TryGetResultType() + { + return this.Target.TryGetResultType(); + } + + /// + /// Tries to get the input types. + /// + /// A result containing the input types if successful. + public R> TryGetInputTypes() + { + return this.Target.TryGetInputTypes(); + } + + /// + /// Tries to get the target containing type. + /// + /// A result containing the containing type if successful. + public R TryGetTargetContainingType() + { + return this.Target.TryGetContainingType(); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static AId From(Expression> targetExpression) + { + var target = Target.From(targetExpression); + return new AId(target, null); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static AId From(Expression> targetExpression) + { + var target = Target.From(targetExpression); + return new AId(target, null); + } +} \ 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..90f5638 --- /dev/null +++ b/Source/Sundew.Base.Identification/Argument.cs @@ -0,0 +1,128 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 an argument in a . +/// +/// The name. +/// The value. +public record Argument(string? Name, string Value) : IParsable +{ + /// Key Value separator. + public const char KeyValueSeparator = '='; + + /// + /// 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 Argument 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 Argument 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(Argument)}."); + } + + /// + /// 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 Argument result) + { + if (inputArg.HasValue) + { + string key = string.Empty; + var level = 0; + var index = 0; + while (index < inputArg.Length) + { + var character = inputArg[index++]; + if (character == Arguments.GroupStartSeparator) + { + level++; + } + + if (character == Arguments.GroupEndSeparator) + { + level--; + } + + if (character == KeyValueSeparator && level == 0) + { + result = new Argument(inputArg.Substring(0, index - 1), inputArg.Substring(index)); + return true; + } + } + + result = new Argument(null, inputArg); + return true; + } + + result = null; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + if (!string.IsNullOrEmpty(this.Name)) + { + stringBuilder.Append(this.Name).Append(KeyValueSeparator); + } + + stringBuilder.Append(this.Value); + } + + /// + /// Creates a string representation of the . + /// + /// A string. + public override string ToString() + { + var stringBuilder = new StringBuilder(); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + return stringBuilder.ToString(); + } + + /// + /// Attempts to parse the current value into an instance of the Arguments class. + /// + /// A result containing the parsed Arguments if successful. + public R TryGetValueArguments() + { + return this.TryGetValueArguments(CultureInfo.CurrentCulture); + } + + /// + /// Attempts to parse the current value into an instance of the Arguments class. + /// + /// The format provider. + /// A result containing the parsed Arguments if successful. + public R TryGetValueArguments(IFormatProvider formatProvider) + { + return R.From(Arguments.TryParse(this.Value, formatProvider, out var args), args); + } +} \ 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..038fe89 --- /dev/null +++ b/Source/Sundew.Base.Identification/Arguments.cs @@ -0,0 +1,217 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using Sundew.Base.Collections.Immutable; +using Sundew.Base.Text; + +#pragma warning disable CS8907, CS1591 +/// +/// Represents arguments for an . +/// +/// The arguments. +public readonly record struct Arguments(ValueArray Items) : IParsable +{ + /// The argument separator. + public const char ArgumentsSeparator = '&'; + + /// The group start separator. + public const char GroupStartSeparator = '('; + + /// The group end separator. + public const char GroupEndSeparator = ')'; + + /// + /// 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 Argument 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 Arguments Parse(string inputArgs, IFormatProvider? formatProvider) + { + if (TryParse(inputArgs, formatProvider, out var result)) + { + return result; + } + + throw new FormatException($"The string: {inputArgs} is not a valid {nameof(Arguments)}."); + } + + /// + /// 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? inputArguments, IFormatProvider? formatProvider, out Arguments result) + { + var args = ImmutableArray.CreateBuilder(); + if (inputArguments.HasValue) + { + var argStartIndex = 0; + var index = 0; + var level = 0; + while (index < inputArguments.Length) + { + var character = inputArguments[index++]; + if (character == GroupStartSeparator) + { + level++; + } + else if (character == GroupEndSeparator) + { + level--; + } + else if (character == ArgumentsSeparator && level == 0) + { + if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) + { + args.Add(arg); + argStartIndex = index; + } + else + { + result = default; + return false; + } + } + } + + if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) + { + args.Add(arg2); + } + + result = new Arguments(args.ToImmutable()); + return true; + } + + result = default; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + stringBuilder.AppendItems(this.Items, (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), Arguments.ArgumentsSeparator); + } + + /// + /// 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 from the specified builder func. + /// + /// The value id func. + /// A new . + public static Arguments From(Action valueIdFunc) + { + var valueIdBuilder = new ValueIdBuilder(); + valueIdFunc(valueIdBuilder); + return valueIdBuilder.Build(); + } + + /// + /// 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 Get(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + return TValue.Parse(argument.Value, formatProvider); + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + return TValue.Parse(argument.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 Get2(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + var innerArguments = argument.TryGetValueArguments(); + if (innerArguments.IsSuccess) + { + return TValue.From(defaultValue, innerArguments.Value); + } + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + var innerArguments = argument.TryGetValueArguments(); + if (innerArguments.IsSuccess) + { + return TValue.From(defaultValue, innerArguments.Value); + } + } + + return defaultValue; + } +} \ 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..4527b9c --- /dev/null +++ b/Source/Sundew.Base.Identification/IValueIdentifiable.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.Identification; + +/// +/// Interface for implementing a value identifiable. +/// +/// The type of the value. +public interface IValueIdentifiable +{ + /// + /// Gets the value id. + /// + /// The value id. + Arguments AsArguments(); + + /// + /// Creates a value from the value id. + /// + /// The initial value. + /// The arguments. + /// The created value. + static abstract TValue From(TValue value, Arguments arguments); +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Path.cs b/Source/Sundew.Base.Identification/Path.cs new file mode 100644 index 0000000..bb34e19 --- /dev/null +++ b/Source/Sundew.Base.Identification/Path.cs @@ -0,0 +1,147 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq.Expressions; +using System.Reflection; +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) : IParsable +{ + /// The path separator. + public const char Separator = '/'; + + /// The parameter separator. + public const char ParameterSeparator = ','; + + /// + /// Get 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 Argument 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.AppendJoin(Separator, this.Segments); + } + + /// + /// Creates a string representation of the . + /// + /// A string. + public override string ToString() + { + var stringBuilder = new StringBuilder(); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + return stringBuilder.ToString(); + } + + /// + /// Gets a for the specified expression. + /// + /// The path expression. + /// A new . + public static Path From(Expression pathExpression) + { + var segments = ImmutableArray.CreateBuilder(); + EvaluateToPath(pathExpression); + + return new Path(segments.ToImmutable()); + + void EvaluateToPath(Expression expression) + { + switch (expression) + { + case LambdaExpression lambdaExpression: + EvaluateToPath(lambdaExpression.Body); + break; + case MethodCallExpression methodCallExpression: + segments.Add($"{methodCallExpression.Method.Name}({GetParameters(methodCallExpression.Method.GetParameters())})"); + break; + case MemberExpression memberExpression: + if (memberExpression.Expression.HasValue) + { + EvaluateToPath(memberExpression.Expression); + } + + segments.Add(memberExpression.Member.Name); + break; + case UnaryExpression unaryExpression: + EvaluateToPath(unaryExpression.Operand); + break; + } + } + } + + private static string GetParameters(ParameterInfo[] parameterInfos) + { + var stringBuilder = new StringBuilder(); + stringBuilder.AppendItems(parameterInfos, (stringBuilder, parameter) => stringBuilder.Append(TargetEvaluator.GetTypeName(parameter.ParameterType)), ParameterSeparator); + return stringBuilder.ToString(); + } +} \ 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..ae8551b --- /dev/null +++ b/Source/Sundew.Base.Identification/Source.cs @@ -0,0 +1,132 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 Argument 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 = null; + return false; + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + stringBuilder.Append($"{this.Name}{NameSeparator}{this.Path}{OriginSeparator}{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(); + GetName(type, false); + + void GetName(Type type, bool isParent) + { + if (type.DeclaringType.HasValue) + { + GetName(type.DeclaringType, true); + } + + stringBuilder.Append(type.Name); + if (isParent) + { + stringBuilder.Append('+'); + } + } + + 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 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..d8b9110 --- /dev/null +++ b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj @@ -0,0 +1,13 @@ + + + + net10.0;net9.0;net8.0 + The IReporter interface for assisting with implementation of reporter/loggers. + + + + + + + + \ 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..b19ccdc --- /dev/null +++ b/Source/Sundew.Base.Identification/Target.cs @@ -0,0 +1,157 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +/// +/// 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 Argument 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, path); + 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); + } + + /// + /// Tries to get the target type. + /// + /// A result containing the source type if successful. + public R TryGetContainingType() + { + return TargetEvaluator.GetDeclaringType(this.Source, this.Path); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static Target From(Expression> targetExpression) + { + return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// A new . + public static Target From(Expression> targetExpression) + { + return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); + } +} \ 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..2283b5d --- /dev/null +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -0,0 +1,208 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Linq; +using System.Reflection; + +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) + { + var sourceType = source.TryGetType(); + if (sourceType.IsError) + { + 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(); + } + + internal static string? GetTypeName(Type type) + { + if (type.IsArray) + { + var elementType = type.GetElementType()!; + var rank = type.GetArrayRank(); + var commas = rank > 1 ? new string(',', rank - 1) : string.Empty; + return $"{GetTypeName(elementType)}[{commas}]"; + } + + if (type.IsGenericType) + { + var genericDef = type.GetGenericTypeDefinition(); + var baseName = genericDef.Name; + var backtickIndex = baseName.IndexOf('`'); + if (backtickIndex > 0) + { + baseName = baseName[..backtickIndex]; + } + + var genericParameters = type.GetGenericArguments().Select(GetTypeName); + + return $"{baseName}[{string.Join(',', genericParameters)}]"; + } + + if (PrimitiveAliases.TryGetValue(type, out var alias)) + { + return alias; + } + + // 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 + return BuildNestedName(type); + } + + // Regular type: just the simple name + return type.Name; + } + + private static MemberInfo? GetTargetMemberInfo(Type sourceType, Path path) + { + var currentType = sourceType; + MemberInfo? memberInfo = null; + foreach (var segment in path.Segments) + { + var segmentType = GetSegment(segment); + var memberInfos = currentType.GetMember(segmentType.Name, MemberTypes.Method | MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + if (segmentType.IsProperty) + { + var propertyInfo = memberInfos.OfType().FirstOrDefault(); + memberInfo = propertyInfo; + if (propertyInfo.HasValue) + { + currentType = propertyInfo.PropertyType; + } + } + else + { + var methodInfo = memberInfos.OfType().FirstOrDefault(x => x.GetParameters().Select(x => GetTypeName(x.ParameterType)).SequenceEqual(segmentType.ParameterNames)); + memberInfo = methodInfo; + if (methodInfo.HasValue) + { + currentType = methodInfo.ReturnType; + } + } + } + + return memberInfo; + } + + private static (string Name, bool IsProperty, IReadOnlyList ParameterNames) GetSegment(string segment) + { + if (segment.EndsWith(')')) + { + var parametersStartIndex = segment.IndexOf('('); + return (segment.Substring(0, parametersStartIndex), false, segment.Substring(parametersStartIndex + 1, segment.Length - parametersStartIndex - 2).Split(',', StringSplitOptions.RemoveEmptyEntries)); + } + + return (segment, true, Array.Empty()); + } + + private static string BuildNestedName(Type type) + { + if (!type.DeclaringType.HasValue) + { + return type.Name; + } + + return $"{BuildNestedName(type.DeclaringType)}+{type.Name}"; + } +} diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs new file mode 100644 index 0000000..afe06f7 --- /dev/null +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.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.Identification; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Sundew.Base.Collections.Immutable; + +/// +/// Builder for constructing for dynamic construction of identifiers. +/// +public sealed class ValueIdBuilder +{ + private readonly List<(string Name, object Value)> 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 (!name.HasValue) + { + throw new NotSupportedException($"{nameof(name)} should be filled by compiler!"); + } + + name = name.Replace("this.", string.Empty); + if (value is IValueIdentifiable valueIdentifiable) + { + this.values.Add((name, $"{Arguments.GroupStartSeparator}{valueIdentifiable.AsArguments().ToString()}{Arguments.GroupEndSeparator}")); + } + else if (value != null) + { + var stringValue = value.ToString(); + if (stringValue.HasValue) + { + this.values.Add((name, stringValue)); + } + } + + 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 Arguments Build() + { + return new Arguments() + { + Items = this.values.Select(x => new Argument(x.Name, x.Value.ToString()!)).ToValueArray(), + }; + } +} \ No newline at end of file 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.slnx b/Source/Sundew.Base.slnx index 9484eef..d66c0ec 100644 --- a/Source/Sundew.Base.slnx +++ b/Source/Sundew.Base.slnx @@ -36,6 +36,7 @@ + From 5895fd31d597b3b136dd5a68bf49e9a0e29b2e4d Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Mon, 23 Mar 2026 00:00:10 +0100 Subject: [PATCH 03/20] Starting implementinf AId parser --- .../Identification/AIdTests.cs | 113 +++++-- Source/Sundew.Base.Identification/AId.cs | 124 ++++++-- Source/Sundew.Base.Identification/AIdRoute.cs | 48 +++ Source/Sundew.Base.Identification/Argument.cs | 128 -------- .../ExpressionEvaluator.cs | 133 ++++++++ .../IIdentifiable.cs | 24 ++ Source/Sundew.Base.Identification/IValue.cs | 69 +++++ .../IValueIdentifiable.cs | 12 +- .../Parsing/AIdRouteParser.cs | 283 ++++++++++++++++++ .../Parsing/Tokens.cs | 25 ++ Source/Sundew.Base.Identification/Path.cs | 61 +--- Source/Sundew.Base.Identification/Segment.cs | 39 +++ .../Sundew.Base.Identification/SingleValue.cs | 69 +++++ Source/Sundew.Base.Identification/Source.cs | 40 +-- .../Sundew.Base.Identification.csproj | 3 +- Source/Sundew.Base.Identification/Target.cs | 35 +-- .../TargetEvaluator.cs | 142 ++++++--- Source/Sundew.Base.Identification/ValueId.cs | 183 +++++++++++ .../ValueIdBuilder.cs | 57 +++- .../{Arguments.cs => ValueIds.cs} | 96 +++--- Source/Sundew.Base.Parsing/ILexer.cs | 28 ++ Source/Sundew.Base.Parsing/ILexerRule.cs | 31 ++ Source/Sundew.Base.Parsing/Lexer.cs | 65 ++++ Source/Sundew.Base.Parsing/LexerError.cs | 25 ++ Source/Sundew.Base.Parsing/ParseSettings.cs | 75 +++++ Source/Sundew.Base.Parsing/Parser.cs | 165 ++++++++++ Source/Sundew.Base.Parsing/RegexLexerRule.cs | 56 ++++ .../Sundew.Base.Parsing.csproj | 13 + Source/Sundew.Base.slnx | 1 + 29 files changed, 1747 insertions(+), 396 deletions(-) create mode 100644 Source/Sundew.Base.Identification/AIdRoute.cs delete mode 100644 Source/Sundew.Base.Identification/Argument.cs create mode 100644 Source/Sundew.Base.Identification/ExpressionEvaluator.cs create mode 100644 Source/Sundew.Base.Identification/IIdentifiable.cs create mode 100644 Source/Sundew.Base.Identification/IValue.cs create mode 100644 Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs create mode 100644 Source/Sundew.Base.Identification/Parsing/Tokens.cs create mode 100644 Source/Sundew.Base.Identification/Segment.cs create mode 100644 Source/Sundew.Base.Identification/SingleValue.cs create mode 100644 Source/Sundew.Base.Identification/ValueId.cs rename Source/Sundew.Base.Identification/{Arguments.cs => ValueIds.cs} (62%) create mode 100644 Source/Sundew.Base.Parsing/ILexer.cs create mode 100644 Source/Sundew.Base.Parsing/ILexerRule.cs create mode 100644 Source/Sundew.Base.Parsing/Lexer.cs create mode 100644 Source/Sundew.Base.Parsing/LexerError.cs create mode 100644 Source/Sundew.Base.Parsing/ParseSettings.cs create mode 100644 Source/Sundew.Base.Parsing/Parser.cs create mode 100644 Source/Sundew.Base.Parsing/RegexLexerRule.cs create mode 100644 Source/Sundew.Base.Parsing/Sundew.Base.Parsing.csproj diff --git a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs index 05ac7d4..7cfd29e 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs @@ -28,10 +28,16 @@ public void T() Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,descriptions)?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri10); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(query,descriptions)?Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri11); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Start(!Name.Name.Space$Assembly=15)/Find?!Name.Name.Space$Assembly=(Address=Home,Number=15)&string[]=[Blue,Green]", UriKind.Absolute, out var uri12); var t1 = Uri.EscapeUriString(uri6!.OriginalString); var t2 = Uri.EscapeUriString(uri8!.OriginalString); var t3 = Uri.EscapeUriString(uri9!.OriginalString); + var t4 = Uri.EscapeUriString(uri10!.OriginalString); + var t5 = Uri.EscapeUriString(uri11!.OriginalString); + var t6 = Uri.EscapeUriString(uri12!.OriginalString); } [Test] @@ -42,7 +48,9 @@ public void T() [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,Description)?person=(Address=Home,Number=15)&description=(Eyes=Blue)")] - [Arguments("Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]")] + [Arguments("Name+Nested.Name.Space$Assembly/Find((Address=Home,Number=15)&descriptions=[Blue,Green])")] + [Arguments("Name+Nested.Name.Space$Assembly/Find(Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green])")] + [Arguments("Name+Nested.Name.Space$Assembly/Find(!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green])")] public void Parse_Then_ResultShouldNotBeNull(string input) { var result = AId.Parse(input, CultureInfo.InvariantCulture); @@ -73,8 +81,24 @@ public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position)"; - var result = AId.From(x => x.Navigate(null!)); + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=null))"; + var result = AId.From(x => x.NavigateTo(null!)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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_ResultShouldNotBeNull2() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=6&Y=4)))"; + var result = AId.From(x => x.NavigateTo(new Position(6, 4))); using (new AssertionScope()) { @@ -89,8 +113,8 @@ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position,bool)"; - var result = AId.From(x => x.Navigate(null!, default)); + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=null&addToHistory=False))"; + var result = AId.From(x => x.NavigateTo(null!, default)); using (new AssertionScope()) { @@ -121,15 +145,15 @@ public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() [Test] public void AsArguments_Then_ResultShouldBeExpectedResult() { - const string expectedResult = "X=4&Y=5"; + const string expectedResult = "!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=5)"; var position = new Position(4, 5); - var arguments = position.AsArguments(); - var result = Position.From(new Position(0, 0), arguments); + var valueId = position.GetValueId(true); + var result = valueId.ToValue(new Position(0, 0)); using (new AssertionScope()) { - arguments.ToString().Should().Be(expectedResult); + valueId.ToString().Should().Be(expectedResult); result.Should().Be(position); } } @@ -137,54 +161,93 @@ public void AsArguments_Then_ResultShouldBeExpectedResult() [Test] public void AsArguments_Then_ResultShouldBeExpectedResult2() { - const string expectedResult = "Position=(X=4&Y=5)&Z=6"; + const string expectedResult = "!AIdTests+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 arguments = position.AsArguments(); - var result = Position3D.From(new Position3D(new Position(0, 0), 0), arguments); + var valueId = position.GetValueId(true); + var result = valueId.ToValue(new Position3D(new Position(0, 0), 0)); using (new AssertionScope()) { - arguments.ToString().Should().Be(expectedResult); + valueId.ToString().Should().Be(expectedResult); result.Should().Be(position); } } + [Test] + public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull2() + { + const string expectedResult = "AIdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate/Execute((parameter!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6)))"; + var result = AId.From(x => x.Navigate.Execute(AId.Argument()), new Position(4, 6)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull3() + { + const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate?wAIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6)"; + var result = AId.From(x => x.Navigate, new Position(4, 6)); + + using (new AssertionScope()) + { + result.Should().NotBeNull(); + 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)); + } + } + #pragma warning disable SA1201 public interface INavigator #pragma warning restore SA1201 { + ICommand Navigate { get; } + void GoBack(); - void Navigate(Position position); + void NavigateTo(Position position); - void Navigate(Position position, bool addToHistory); + void NavigateTo(Position position, bool addToHistory); + } + + public interface ICommand + { + void Execute(TParameter parameter); } public record Position(int X, int Y) : IValueIdentifiable { - public Arguments AsArguments() => Arguments.From(builder => builder.Add(this.X).Add(this.Y)); + public ValueId GetValueId(bool isRoot) => ValueId.From(this, (value, builder) => builder.Add(value.X).Add(value.Y), isRoot); - public static Position From(Position position, Arguments arguments) + public static Position From(Position position, ValueId valueId) { return new Position( - arguments.Get(position.X, CultureInfo.InvariantCulture), - arguments.Get(position.Y, CultureInfo.InvariantCulture)); + valueId.Value.Get(position.X, CultureInfo.InvariantCulture), + valueId.Value.Get(position.Y, CultureInfo.InvariantCulture)); } } public record Position3D(Position Position, int Z) : IValueIdentifiable { - public Arguments AsArguments() + public ValueId GetValueId(bool isRoot) { - return Arguments.From(builder => builder.Add(this.Position).Add(this.Z)); + return ValueId.From(this, (value, builder) => builder.Add(value.Position).Add(value.Z), isRoot); } - public static Position3D From(Position3D value, Arguments arguments) + public static Position3D From(Position3D value, ValueId valueId) { return new Position3D( - arguments.Get2(value.Position, CultureInfo.InvariantCulture), - arguments.Get(value.Z, CultureInfo.InvariantCulture)); + valueId.Value.Get2(value.Position, CultureInfo.InvariantCulture), + valueId.Value.Get(value.Z, CultureInfo.InvariantCulture)); } } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/AId.cs b/Source/Sundew.Base.Identification/AId.cs index 7133a02..35bc9cc 100644 --- a/Source/Sundew.Base.Identification/AId.cs +++ b/Source/Sundew.Base.Identification/AId.cs @@ -13,21 +13,22 @@ namespace Sundew.Base.Identification; using System.Globalization; using System.Linq.Expressions; using System.Text; +using Sundew.Base.Identification.Parsing; /// /// Represents any Id. /// -public record AId(Target Target, Arguments? Arguments) : IParsable +public record AId(Source Source, Path? Path, ValueId? ValueId = null) : IParsable { - /// The arguments separator. - public const char ArgumentsSeparator = '?'; + /// The value ids separator. + public const char ValueIdsSeparator = '?'; /// /// 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 Argument that represents the parsed value from the input string. + /// 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 AId Parse(string inputAId, IFormatProvider? provider) { @@ -40,7 +41,7 @@ public static AId Parse(string inputAId, IFormatProvider? provider) } /// - /// Tries to parse the specified input string into an instance of the type. + /// 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. @@ -48,28 +49,30 @@ public static AId Parse(string inputAId, IFormatProvider? provider) /// true if parsing was successful, otherwise false. public static bool TryParse([NotNullWhen(true)] string? inputAId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId result) { + return AIdRouteParser.TryGetAId(inputAId, formatProvider, out result); + /* if (inputAId.HasValue) { - var argumentsSeparatorIndex = inputAId.IndexOf(ArgumentsSeparator); + var argumentsSeparatorIndex = inputAId.IndexOf(ValueIdsSeparator); if (argumentsSeparatorIndex > -1) { var targetString = inputAId.Substring(0, argumentsSeparatorIndex); var argumentsString = inputAId.Substring(argumentsSeparatorIndex + 1); - if (Target.TryParse(targetString, formatProvider, out var target) && Identification.Arguments.TryParse(argumentsString, formatProvider, out var args)) + if (TryParseTarget(targetString, formatProvider, out var target)) { - result = new AId(target, args); + result = new AId(target.Source, target.Path); return true; } } - else if (Target.TryParse(inputAId, formatProvider, out var target)) + else if (TryParseTarget(inputAId, formatProvider, out var target)) { - result = new AId(target, null); + result = new AId(target.Source, target.Path); return true; } } result = null; - return false; + return false;*/ } /// @@ -79,11 +82,17 @@ public static bool TryParse([NotNullWhen(true)] string? inputAId, IFormatProvide /// The format provider. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) { - this.Target.AppendInto(stringBuilder, formatProvider); - if (this.Arguments.HasValue) + this.Source.AppendInto(stringBuilder, formatProvider); + if (this.Path.HasValue) + { + stringBuilder.Append(Path.Separator); + this.Path.AppendInto(stringBuilder, formatProvider); + } + + if (this.ValueId.HasValue) { - stringBuilder.Append(ArgumentsSeparator); - this.Arguments.Value.AppendInto(stringBuilder, formatProvider); + stringBuilder.Append(ValueIdsSeparator); + this.ValueId.AppendInto(stringBuilder, formatProvider); } } @@ -104,7 +113,7 @@ public override string ToString() /// A result containing the source type if successful. public R TryGetSourceType() { - return this.Target.TryGetSourceType(); + return this.Source.TryGetType(); } /// @@ -113,7 +122,7 @@ public R TryGetSourceType() /// A result containing the result type if successful. public R TryGetResultType() { - return this.Target.TryGetResultType(); + return TargetEvaluator.GetResultType(this.Source, this.Path); } /// @@ -122,7 +131,7 @@ public R TryGetResultType() /// A result containing the input types if successful. public R> TryGetInputTypes() { - return this.Target.TryGetInputTypes(); + return TargetEvaluator.GetInputTypes(this.Source, this.Path, this.ValueId); } /// @@ -131,7 +140,7 @@ public R> TryGetInputTypes() /// A result containing the containing type if successful. public R TryGetTargetContainingType() { - return this.Target.TryGetContainingType(); + return TargetEvaluator.GetDeclaringType(this.Source, this.Path); } /// @@ -142,8 +151,8 @@ public R TryGetTargetContainingType() /// A new . public static AId From(Expression> targetExpression) { - var target = Target.From(targetExpression); - return new AId(target, null); + var (source, path, valueId) = ExpressionEvaluator.From(targetExpression); + return new AId(source, path, valueId); } /// @@ -154,7 +163,76 @@ public static AId From(Expression> targetExpression) /// A new . public static AId From(Expression> targetExpression) { - var target = Target.From(targetExpression); - return new AId(target, null); + var target = ExpressionEvaluator.From(targetExpression); + return new AId(target.Source, target.Path); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// The value. + /// A new . + public static AId From(Expression> targetExpression, IIdentifiable value) + { + var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value); + return new AId(source, path, valueId); + } + + /// + /// Gets an from the specified source and expression. + /// + /// The source type. + /// The target expression. + /// The value. + /// A new . + public static AId From(Expression> targetExpression, IIdentifiable value) + { + var target = ExpressionEvaluator.From(targetExpression); + return new AId(target.Source, target.Path, value.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 TryParseTarget([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; + } + + /// + /// 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/AIdRoute.cs b/Source/Sundew.Base.Identification/AIdRoute.cs new file mode 100644 index 0000000..fdd72bc --- /dev/null +++ b/Source/Sundew.Base.Identification/AIdRoute.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 AIdRoute(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 AIdRoute Parse(string inputAIdRoute, IFormatProvider? provider) + { + if (TryParse(inputAIdRoute, provider, out var result)) + { + return result; + } + + throw new FormatException($"The string: {inputAIdRoute} is not a valid {nameof(AId)}"); + } + + /// + /// 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? inputAidRoute, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AIdRoute result) + { + return AIdRouteParser.TryGetAIdRoute(inputAidRoute, formatProvider, out result); + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Argument.cs b/Source/Sundew.Base.Identification/Argument.cs deleted file mode 100644 index 90f5638..0000000 --- a/Source/Sundew.Base.Identification/Argument.cs +++ /dev/null @@ -1,128 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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 an argument in a . -/// -/// The name. -/// The value. -public record Argument(string? Name, string Value) : IParsable -{ - /// Key Value separator. - public const char KeyValueSeparator = '='; - - /// - /// 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 Argument 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 Argument 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(Argument)}."); - } - - /// - /// 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 Argument result) - { - if (inputArg.HasValue) - { - string key = string.Empty; - var level = 0; - var index = 0; - while (index < inputArg.Length) - { - var character = inputArg[index++]; - if (character == Arguments.GroupStartSeparator) - { - level++; - } - - if (character == Arguments.GroupEndSeparator) - { - level--; - } - - if (character == KeyValueSeparator && level == 0) - { - result = new Argument(inputArg.Substring(0, index - 1), inputArg.Substring(index)); - return true; - } - } - - result = new Argument(null, inputArg); - return true; - } - - result = null; - return false; - } - - /// - /// Appends this to the specified . - /// - /// The string builder. - /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) - { - if (!string.IsNullOrEmpty(this.Name)) - { - stringBuilder.Append(this.Name).Append(KeyValueSeparator); - } - - stringBuilder.Append(this.Value); - } - - /// - /// Creates a string representation of the . - /// - /// A string. - public override string ToString() - { - var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); - return stringBuilder.ToString(); - } - - /// - /// Attempts to parse the current value into an instance of the Arguments class. - /// - /// A result containing the parsed Arguments if successful. - public R TryGetValueArguments() - { - return this.TryGetValueArguments(CultureInfo.CurrentCulture); - } - - /// - /// Attempts to parse the current value into an instance of the Arguments class. - /// - /// The format provider. - /// A result containing the parsed Arguments if successful. - public R TryGetValueArguments(IFormatProvider formatProvider) - { - return R.From(Arguments.TryParse(this.Value, formatProvider, out var args), args); - } -} \ 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..1859e9c --- /dev/null +++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.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. +// +// -------------------------------------------------------------------------------------------------------------------- + +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 +{ + /// The argument separator. + public const char ArgumentSeparator = ','; + + /// + /// Gets a for the specified expression. + /// + /// The path expression. + /// The value. + /// A new . + public static (Source Source, Path Path, ValueId? ValueId) From(LambdaExpression pathExpression, IIdentifiable? value = null) + { + var valueId = value?.Id; + 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()), isUsed ? 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(AId) && argumentMethodCallExpression.Method.Name == nameof(AId.Argument) && valueId.HasValue) + { + valueIds.Add(valueId with { Name = argument.Second.Name + valueId.Name }); + isUsed = true; + } + else + { + GetArgument(argument.First, argument.Second, valueIds); + } + } + + segments.Add(new Segment(methodCallExpression.Method.Name, new ValueId(null, null, new ValueIds(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: + builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new SingleValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default")))); + 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 ValueId(null, null, new SingleValue(value?.ToString() ?? string.Empty))); + } + + if (memberExpression.Member is PropertyInfo propertyInfo) + { + var value = propertyInfo.GetValue(container); + builder.Add(new ValueId(null, null, new SingleValue(value?.ToString() ?? string.Empty))); + } + } + + 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 ValueId(parameterInfo.Name, GetMetadata(argument), new ValueIds(newBuilder.ToImmutable()))); + } + + break; + } + } + + private static string? GetMetadata(Expression argument) + { + return TargetEvaluator.IsKnownType(argument.Type) ? null : Source.FromType(argument.Type).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/IValue.cs b/Source/Sundew.Base.Identification/IValue.cs new file mode 100644 index 0000000..11a3e64 --- /dev/null +++ b/Source/Sundew.Base.Identification/IValue.cs @@ -0,0 +1,69 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 System.Text; + +/// +/// Represents a value. +/// +public interface IValue +{ + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider); + + /// + /// Converts the current instance to a collection of value identifiers. + /// + /// A object representing the value identifiers, or null if the instance does not correspond + /// to any value identifiers. + ValueIds ToValueIds() + { + return this switch + { + SingleValue singleValue => new ValueIds([new ValueId(null, null, singleValue)]), + ValueIds valueIds => valueIds, + _ => throw new NotSupportedException($"The type {this.GetType()} is not supported."), + }; + } + + /// + /// 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 Get( + TValue defaultValue, + IFormatProvider formatProvider, + [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) + where TValue : IParsable; + + /// + /// 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 Get2( + TValue defaultValue, + IFormatProvider formatProvider, + [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) + where TValue : IValueIdentifiable; +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/IValueIdentifiable.cs b/Source/Sundew.Base.Identification/IValueIdentifiable.cs index 4527b9c..ca9a44f 100644 --- a/Source/Sundew.Base.Identification/IValueIdentifiable.cs +++ b/Source/Sundew.Base.Identification/IValueIdentifiable.cs @@ -11,19 +11,13 @@ namespace Sundew.Base.Identification; /// Interface for implementing a value identifiable. /// /// The type of the value. -public interface IValueIdentifiable +public interface IValueIdentifiable : IIdentifiable { - /// - /// Gets the value id. - /// - /// The value id. - Arguments AsArguments(); - /// /// Creates a value from the value id. /// /// The initial value. - /// The arguments. + /// The valueId. /// The created value. - static abstract TValue From(TValue value, Arguments arguments); + static abstract TValue From(TValue value, ValueId valueId); } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs new file mode 100644 index 0000000..61a6a2e --- /dev/null +++ b/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs @@ -0,0 +1,283 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using Sundew.Base.Collections.Linq; +using Sundew.Base.Parsing; + +internal static class AIdRouteParser +{ + private static readonly Lexer AidLexer; + private static readonly Lexer AidRouteLexer; + + static AIdRouteParser() + { + var sourceNameTokenMatcher = new RegexLexerRule(Tokens.SourceName, new Regex("[^~]+", RegexOptions.Compiled)); + var sourcePathTokenMatcher = new RegexLexerRule(Tokens.SourcePath, new Regex("[^$~]+", RegexOptions.Compiled)); + var sourceOriginTokenMatcher = new RegexLexerRule(Tokens.SourceOrigin, new Regex("[^$~//>]*", RegexOptions.Compiled)); + var segmentNameTokenMatcher = new RegexLexerRule(Tokens.SegmentName, new Regex("[^$~//?(>]+", RegexOptions.Compiled)); + var valueIdNameTokenMatcher = new RegexLexerRule(Tokens.ValueIdName, new Regex("[^!=]", RegexOptions.Compiled)); + var valueIdMetadataTokenMatcher = new RegexLexerRule(Tokens.ValueIdMetadata, new Regex("[^=]+", RegexOptions.Compiled)); + var valueIdValueTokenMatcher = new RegexLexerRule(Tokens.ValueIdValue, new Regex("[^)&]+", RegexOptions.Compiled)); + AidRouteLexer = new Lexer( + [sourceNameTokenMatcher, + sourcePathTokenMatcher, + sourceOriginTokenMatcher, + segmentNameTokenMatcher, + valueIdNameTokenMatcher, + valueIdMetadataTokenMatcher, + valueIdValueTokenMatcher]); + AidLexer = new Lexer( + [sourceNameTokenMatcher, + sourcePathTokenMatcher, + sourceOriginTokenMatcher, + segmentNameTokenMatcher, + valueIdNameTokenMatcher, + valueIdMetadataTokenMatcher, + valueIdValueTokenMatcher]); + } + + public static bool TryGetAIdRoute( + string? input, + IFormatProvider? formatProvider, + [MaybeNullWhen(false)] out AIdRoute aIdRoute) + { + if (!input.HasValue) + { + aIdRoute = null; + return false; + } + + var parser = new Parser(AidRouteLexer, input); + if (TryGetAIdRoute(parser, out aIdRoute) && parser.AcceptEnd()) + { + return true; + } + + return false; + } + + public static bool TryGetAId(string? input, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId aId) + { + if (!input.HasValue) + { + aId = null; + return false; + } + + var parser = new Parser(AidLexer, input); + + if (TryGetAId(parser, out aId) && parser.AcceptEnd()) + { + return true; + } + + return false; + } + + private static bool TryGetAIdRoute(Parser parser, [MaybeNullWhen(false)] out AIdRoute aIdRoute) + { + var route = new List(); + while (TryGetAId(parser, out var aid)) + { + route.Add(aid); + if (!parser.Accept('>')) + { + break; + } + } + + if (route.Count > 0) + { + aIdRoute = new AIdRoute([..route]); + return true; + } + + aIdRoute = null; + return false; + } + + private static bool TryGetAId(Parser parser, [MaybeNullWhen(false)] out AId aid) + { + if (TryGetSource(parser, out var source)) + { + Path? path = null; + if (parser.Accept('/')) + { + if (TryGetPath(parser, out path)) + { + } + else + { + } + } + + ValueId? valueId = null; + if (parser.Accept('?')) + { + if (TryGetValueId(parser, out valueId)) + { + } + else + { + } + } + + aid = new AId(source, path, valueId); + return true; + } + + aid = null; + return false; + } + + private static bool TryGetSource(Parser parser, [MaybeNullWhen(false)] out Source source) + { + if (parser.Accept(Tokens.SourceName, out var sourceName) && parser.Accept('~') && + parser.Accept(Tokens.SourcePath, out var sourcePath) && parser.Accept('$') && + parser.Accept(Tokens.SourceOrigin, out var sourceOrigin)) + { + source = new Source(sourceName, sourcePath, sourceOrigin); + return true; + } + + source = null; + return false; + } + + private static bool TryGetPath(Parser parser, [MaybeNullWhen(false)] out Path path) + { + var segments = new List(); + while (!parser.IsNext('?') && !parser.AcceptEnd()) + { + if (parser.Accept(Tokens.SegmentName, out var segmentName)) + { + if (parser.Accept('(') && TryGetValueId(parser, out var valueId) && parser.Accept(')')) + { + segments.Add(new Segment(segmentName, valueId)); + } + else + { + segments.Add(new Segment(segmentName, null)); + } + } + } + + path = new Path([..segments]); + return true; + } + + private static bool TryGetValueId(Parser parser, [MaybeNullWhen(false)] out ValueId valueId) + { + if (parser.Accept('(')) + { + if (!TrySingleValue(parser, out valueId)) + { + valueId = null; + return false; + } + + if (parser.Accept(')')) + { + return true; + } + } + else + { + } + + valueId = null; + return false; + } + + private static bool TrySingleValue(Parser parser, [MaybeNullWhen(false)] out ValueId valueId) + { + string? name = null; + string? metadata = null; + IValue? value = null; + var lexemesState = parser.CurrentState(); + if (!parser.Accept(Tokens.ValueIdName, out name) || !parser.Accept('!') || + !parser.Accept(Tokens.ValueIdMetadata, out metadata) || !parser.Accept('=') || + !TryGetValue(parser, out value)) + { + name = null; + metadata = null; + parser.RestoreState(lexemesState); + if (!parser.Accept('!') || + !parser.Accept(Tokens.ValueIdMetadata, out metadata) || !parser.Accept('=') || + !TryGetValue(parser, out value)) + { + name = null; + metadata = null; + parser.RestoreState(lexemesState); + if (!parser.Accept(Tokens.ValueIdName, out name) || !parser.Accept('=') || + !TryGetValue(parser, out value)) + { + parser.RestoreState(lexemesState); + if (!TryGetValue(parser, out value)) + { + valueId = null; + return false; + } + } + } + } + + valueId = new ValueId(name, metadata, value); + return true; + } + + private static bool TryGetValue(Parser parser, [MaybeNullWhen(false)] out IValue value) + { + if (parser.Accept('(')) + { + var valueIds = new List(); + while (!parser.IsNext(')')) + { + if (TrySingleValue(parser, out var valueId) && parser.Accept(')')) + { + valueIds.Add(valueId); + if (!parser.Accept('&')) + { + break; + } + } + else + { + value = null; + return false; + } + } + + if (parser.Accept(')')) + { + value = valueIds.ByCardinality() switch + { + Empty empty => null, + Multiple multiple => new ValueIds([..valueIds]), + Single single => single.Item.Value, + }; + + return value != null; + } + } + + if (parser.Accept(Tokens.ValueIdValue, out var rawValue)) + { + value = new SingleValue(rawValue); + return true; + } + + value = null; + return false; + } +} \ 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..413c980 --- /dev/null +++ b/Source/Sundew.Base.Identification/Parsing/Tokens.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.Parsing; + +internal enum Tokens +{ + SourceName, + + SourcePath, + + SourceOrigin, + + SegmentName, + + ValueIdName, + + ValueIdMetadata, + + ValueIdValue, +} diff --git a/Source/Sundew.Base.Identification/Path.cs b/Source/Sundew.Base.Identification/Path.cs index bb34e19..1a5d71f 100644 --- a/Source/Sundew.Base.Identification/Path.cs +++ b/Source/Sundew.Base.Identification/Path.cs @@ -8,12 +8,8 @@ namespace Sundew.Base.Identification; using System; -using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq.Expressions; -using System.Reflection; using System.Text; using Sundew.Base.Collections.Immutable; using Sundew.Base.Text; @@ -22,14 +18,11 @@ namespace Sundew.Base.Identification; /// 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) : IParsable +public sealed record Path(ValueArray Segments) { /// The path separator. public const char Separator = '/'; - - /// The parameter separator. - public const char ParameterSeparator = ','; - + /* /// /// Get the from input path. /// @@ -45,7 +38,7 @@ public static Path From(string inputPath) /// /// 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 Argument that represents the parsed value from the input string. + /// 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) { @@ -78,7 +71,7 @@ public static bool TryParse([NotNullWhen(true)] string? inputPath, IFormatProvid result = null; return false; - } + }*/ /// /// Appends this to the specified . @@ -87,7 +80,7 @@ public static bool TryParse([NotNullWhen(true)] string? inputPath, IFormatProvid /// The format provider. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) { - stringBuilder.AppendJoin(Separator, this.Segments); + stringBuilder.AppendItems(this.Segments, (builder, segment) => segment.AppendInto(builder, formatProvider), Separator); } /// @@ -100,48 +93,4 @@ public override string ToString() this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); return stringBuilder.ToString(); } - - /// - /// Gets a for the specified expression. - /// - /// The path expression. - /// A new . - public static Path From(Expression pathExpression) - { - var segments = ImmutableArray.CreateBuilder(); - EvaluateToPath(pathExpression); - - return new Path(segments.ToImmutable()); - - void EvaluateToPath(Expression expression) - { - switch (expression) - { - case LambdaExpression lambdaExpression: - EvaluateToPath(lambdaExpression.Body); - break; - case MethodCallExpression methodCallExpression: - segments.Add($"{methodCallExpression.Method.Name}({GetParameters(methodCallExpression.Method.GetParameters())})"); - break; - case MemberExpression memberExpression: - if (memberExpression.Expression.HasValue) - { - EvaluateToPath(memberExpression.Expression); - } - - segments.Add(memberExpression.Member.Name); - break; - case UnaryExpression unaryExpression: - EvaluateToPath(unaryExpression.Operand); - break; - } - } - } - - private static string GetParameters(ParameterInfo[] parameterInfos) - { - var stringBuilder = new StringBuilder(); - stringBuilder.AppendItems(parameterInfos, (stringBuilder, parameter) => stringBuilder.Append(TargetEvaluator.GetTypeName(parameter.ParameterType)), ParameterSeparator); - 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..2754993 --- /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 id for the segment. +public sealed record Segment(string Name, ValueId? ValueId = 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.ValueId.HasValue) + { + builder.Append('('); + this.ValueId.AppendInto(builder, formatProvider); + builder.Append(')'); + } + + return builder; + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/SingleValue.cs b/Source/Sundew.Base.Identification/SingleValue.cs new file mode 100644 index 0000000..dbc6ae4 --- /dev/null +++ b/Source/Sundew.Base.Identification/SingleValue.cs @@ -0,0 +1,69 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Text; + +/// +/// Represents an argument in a . +/// +/// The value. +public sealed record SingleValue(string Value) : IValue +{ + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + stringBuilder.Append(this.Value); + } + + /// + /// 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 Get(TValue defaultValue, IFormatProvider formatProvider, string? referenceName = null) + where TValue : IParsable + { + 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 Get2(TValue defaultValue, IFormatProvider formatProvider, string? referenceName = null) + where TValue : IValueIdentifiable + { + return defaultValue; + } + + /// + /// 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/Source.cs b/Source/Sundew.Base.Identification/Source.cs index ae8551b..9804838 100644 --- a/Source/Sundew.Base.Identification/Source.cs +++ b/Source/Sundew.Base.Identification/Source.cs @@ -31,7 +31,7 @@ public sealed record Source(string Origin, string Path, string Name) : IParsable /// /// 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 Argument that represents the parsed value from the input string. + /// 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) { @@ -68,8 +68,8 @@ public static bool TryParse([NotNullWhen(true)] string? inputSource, IFormatProv return true; } - result = null; - return false; + result = new Source(string.Empty, string.Empty, inputSource); + return true; } /// @@ -79,7 +79,18 @@ public static bool TryParse([NotNullWhen(true)] string? inputSource, IFormatProv /// The format provider. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) { - stringBuilder.Append($"{this.Name}{NameSeparator}{this.Path}{OriginSeparator}{this.Origin}"); + 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); + } } /// @@ -101,20 +112,9 @@ public override string ToString() public static Source FromType(Type type) { var stringBuilder = new StringBuilder(); - GetName(type, false); - - void GetName(Type type, bool isParent) + if (TargetEvaluator.GetTypeName(type, stringBuilder)) { - if (type.DeclaringType.HasValue) - { - GetName(type.DeclaringType, true); - } - - stringBuilder.Append(type.Name); - if (isParent) - { - stringBuilder.Append('+'); - } + return new Source(string.Empty, string.Empty, stringBuilder.ToString()); } return new Source(type.Assembly.GetName().Name ?? string.Empty, type.Namespace ?? string.Empty, stringBuilder.ToString()); @@ -126,6 +126,12 @@ void GetName(Type type, bool isParent) /// 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); } diff --git a/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj index d8b9110..3573e16 100644 --- a/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj +++ b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj @@ -2,11 +2,12 @@ net10.0;net9.0;net8.0 - The IReporter interface for assisting with implementation of reporter/loggers. + The AIdRoute, AId and ValueId for generic identification. + diff --git a/Source/Sundew.Base.Identification/Target.cs b/Source/Sundew.Base.Identification/Target.cs index b19ccdc..362f103 100644 --- a/Source/Sundew.Base.Identification/Target.cs +++ b/Source/Sundew.Base.Identification/Target.cs @@ -11,7 +11,6 @@ namespace Sundew.Base.Identification; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq.Expressions; using System.Text; /// @@ -22,11 +21,11 @@ namespace Sundew.Base.Identification; public sealed record Target(Source Source, Path? Path) : IParsable { /// - /// Parses the specified input string into an instance of the type. + /// 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 Argument that represents the parsed value from the input string. + /// 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) { @@ -39,7 +38,7 @@ public static Target Parse(string inputTarget, IFormatProvider? formatProvider) } /// - /// Tries to parse the specified input string into an instance of the type. + /// 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. @@ -54,9 +53,9 @@ public static bool TryParse([NotNullWhen(true)] string? inputTarget, IFormatProv { 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)) + if (Source.TryParse(targetString, formatProvider, out var entry) /* && Path.TryParse(argumentsString, formatProvider, out var path)*/) { - result = new Target(entry, path); + result = new Target(entry, null); return true; } } @@ -121,7 +120,7 @@ public R TryGetResultType() /// A result containing the input types if successful. public R> TryGetInputTypes() { - return TargetEvaluator.GetInputTypes(this.Source, this.Path); + return TargetEvaluator.GetInputTypes(this.Source, this.Path, null); } /// @@ -132,26 +131,4 @@ public R TryGetContainingType() { return TargetEvaluator.GetDeclaringType(this.Source, this.Path); } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// A new . - public static Target From(Expression> targetExpression) - { - return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// A new . - public static Target From(Expression> targetExpression) - { - return new Target(Source.FromType(typeof(TSource)), Path.From(targetExpression)); - } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/TargetEvaluator.cs b/Source/Sundew.Base.Identification/TargetEvaluator.cs index 2283b5d..ae33dbf 100644 --- a/Source/Sundew.Base.Identification/TargetEvaluator.cs +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -9,8 +9,14 @@ namespace Sundew.Base.Identification; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using Sundew.Base.Collections; +using Sundew.Base.Collections.Linq; +using Sundew.Base.Text; internal static class TargetEvaluator { @@ -62,7 +68,7 @@ public static R GetResultType(Source source, Path? path) return R.Error(); } - public static R> GetInputTypes(Source source, Path? path) + public static R> GetInputTypes(Source source, Path? path, ValueId? valueId) { var sourceType = source.TryGetType(); if (sourceType.IsError) @@ -70,6 +76,11 @@ public static R> GetInputTypes(Source source, Path? path) return R.Error(); } + if (valueId.HasValue) + { + return valueId.TryGetType().Map(x => (IReadOnlyList)[x]); + } + if (!path.HasValue) { return R.Success>([sourceType.Value]); @@ -112,14 +123,32 @@ public static R GetDeclaringType(Source source, Path? path) return R.Error(); } - internal static string? GetTypeName(Type type) + 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; - return $"{GetTypeName(elementType)}[{commas}]"; + GetTypeName(elementType, stringBuilder); + stringBuilder.Append($"[{commas}]"); + return false; } if (type.IsGenericType) @@ -132,14 +161,13 @@ public static R GetDeclaringType(Source source, Path? path) baseName = baseName[..backtickIndex]; } - var genericParameters = type.GetGenericArguments().Select(GetTypeName); - - return $"{baseName}[{string.Join(',', genericParameters)}]"; - } + stringBuilder + .Append(baseName) + .Append('[') + .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), ExpressionEvaluator.ArgumentSeparator) + .Append(']'); - if (PrimitiveAliases.TryGetValue(type, out var alias)) - { - return alias; + return false; } // Nested types: strip the declaring type prefix, keep the + separator @@ -147,11 +175,26 @@ public static R GetDeclaringType(Source source, Path? path) if (type.IsNested) { // Walk up to build "OuterType+InnerType" without namespace - return BuildNestedName(type); + BuildNestedName(type, stringBuilder); + return false; } // Regular type: just the simple name - return type.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) @@ -160,49 +203,70 @@ public static R GetDeclaringType(Source source, Path? path) MemberInfo? memberInfo = null; foreach (var segment in path.Segments) { - var segmentType = GetSegment(segment); - var memberInfos = currentType.GetMember(segmentType.Name, MemberTypes.Method | MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - if (segmentType.IsProperty) - { - var propertyInfo = memberInfos.OfType().FirstOrDefault(); - memberInfo = propertyInfo; - if (propertyInfo.HasValue) - { - currentType = propertyInfo.PropertyType; - } - } - else + var memberInfos = currentType.GetMember(segment.Name, MemberTypes.Method | MemberTypes.Property, BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy); + var cardinality = memberInfos.ByCardinality(); + switch (cardinality) { - var methodInfo = memberInfos.OfType().FirstOrDefault(x => x.GetParameters().Select(x => GetTypeName(x.ParameterType)).SequenceEqual(segmentType.ParameterNames)); - memberInfo = methodInfo; - if (methodInfo.HasValue) - { - currentType = methodInfo.ReturnType; - } + case Empty empty: + break; + case Multiple multiple: + var valueIds = segment.ValueId?.Value.ToValueIds() ?? new ValueIds([]); + var methodInfo = multiple.Items.OfType() + .Select(methodInfo => (methodInfo, parameters: methodInfo.GetParameters())) + .Where(x => x.parameters.Length == valueIds.Items.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 (string Name, bool IsProperty, IReadOnlyList ParameterNames) GetSegment(string segment) + private static bool IsMatch(ParameterInfo[] parameterInfos, ValueIds valueIds) { - if (segment.EndsWith(')')) + if (!valueIds.HasValue) { - var parametersStartIndex = segment.IndexOf('('); - return (segment.Substring(0, parametersStartIndex), false, segment.Substring(parametersStartIndex + 1, segment.Length - parametersStartIndex - 2).Split(',', StringSplitOptions.RemoveEmptyEntries)); + return parameterInfos.Length == 0; } - return (segment, true, Array.Empty()); + return parameterInfos.Zip(valueIds.Items).All(x => + { + var argumentType = x.Second.Metadata.HasValue + ? Source.Parse(x.Second.Metadata, CultureInfo.InvariantCulture).TryGetType().Value + : GetTypeFromArgument(x.First.ParameterType, x.Second.Value); + return x.First.ParameterType.IsAssignableFrom(argumentType); + }); } - private static string BuildNestedName(Type type) + private static Type? GetTypeFromArgument(Type firstParameterType, IValue secondValue) { - if (!type.DeclaringType.HasValue) + const string parseName = "Parse"; + var parseMethod = firstParameterType.GetMethod(parseName, BindingFlags.Public | BindingFlags.Static, [typeof(string), typeof(IFormatProvider)]); + if (parseMethod.HasValue) { - return type.Name; + return parseMethod.Invoke(null, [secondValue.ToString(), CultureInfo.InvariantCulture])?.GetType(); } - return $"{BuildNestedName(type.DeclaringType)}+{type.Name}"; + parseMethod = firstParameterType.GetMethod(parseName, BindingFlags.Public | BindingFlags.Static, [typeof(string)]); + return parseMethod?.Invoke(null, [secondValue.ToString()])?.GetType(); } } diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs new file mode 100644 index 0000000..44c1d76 --- /dev/null +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -0,0 +1,183 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 value id for an argument. +/// +/// The name. +/// The metadata. +/// The value. +public sealed partial record ValueId(string? Name, string? Metadata, IValue Value) +{ + /// Key Value separator. + public const char KeyValueSeparator = '='; + + /// Metadata separator. + public const char MetadataSeparator = '!'; + + /// + /// 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); + return stringBuilder.ToString(); + } + + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + { + bool TryAppendMetadata() + { + if (!string.IsNullOrEmpty(this.Metadata)) + { + stringBuilder.Append(MetadataSeparator); + stringBuilder.Append(this.Metadata); + return true; + } + + return false; + } + + if (!string.IsNullOrEmpty(this.Name)) + { + stringBuilder.Append(this.Name); + TryAppendMetadata(); + stringBuilder.Append(KeyValueSeparator); + } + else + { + if (TryAppendMetadata()) + { + stringBuilder.Append(KeyValueSeparator); + } + } + + this.Value.AppendInto(stringBuilder, formatProvider); + } + + /// + /// Creates an from the specified builder func. + /// + /// The type of the value. + /// The value. + /// The value id func. + /// Indicated whether this is a root id. + /// A new . + public static ValueId From(TValue value, Action valueIdFunc, bool isRoot) + { + var valueIdBuilder = new ValueIdBuilder(value?.GetType() ?? typeof(TValue), isRoot); + 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) + { + if (inputArg.HasValue) + { + string key = string.Empty; + var level = 0; + var index = 0; + var metadataIndex = -1; + while (index < inputArg.Length) + { + var character = inputArg[index++]; + if (character == ValueIds.GroupStartSeparator) + { + level++; + } + + if (character == ValueIds.GroupEndSeparator) + { + level--; + } + + if (character == MetadataSeparator && level == 0) + { + metadataIndex = index; + } + + if (character == KeyValueSeparator && level == 0) + { + var (nameLength, metadataStart, metadataLength) = metadataIndex > -1 ? (metadataIndex - 1, metadataIndex, index - metadataIndex - 1) : (index - 1, 0, 0); + result = new ValueId(inputArg.Substring(0, nameLength), inputArg.Substring(metadataStart, metadataLength), new SingleValue(inputArg.Substring(index))); + return true; + } + } + + result = new ValueId(null, null, new SingleValue(inputArg)); + return true; + } + + result = null; + return false; + } + + /// + /// 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); + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs index afe06f7..54f1072 100644 --- a/Source/Sundew.Base.Identification/ValueIdBuilder.cs +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs @@ -12,13 +12,16 @@ namespace Sundew.Base.Identification; using System.Linq; using System.Runtime.CompilerServices; using Sundew.Base.Collections.Immutable; +using Sundew.Base.Collections.Linq; /// -/// Builder for constructing for dynamic construction of identifiers. +/// Builder for constructing for dynamic construction of identifiers. /// -public sealed class ValueIdBuilder +/// The . +/// Indicates whether the builder is for a root identifier. +public sealed class ValueIdBuilder(Type type, bool isRoot) { - private readonly List<(string Name, object Value)> values = new(); + private readonly List values = new(); /// /// Adds a value to the builder for dynamic construction of identifiers. @@ -29,22 +32,31 @@ public sealed class ValueIdBuilder /// The current instance of the builder, enabling method chaining. public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameof(value))] string? name = null) { - if (!name.HasValue) + if (string.IsNullOrEmpty(name)) { throw new NotSupportedException($"{nameof(name)} should be filled by compiler!"); } - name = name.Replace("this.", string.Empty); - if (value is IValueIdentifiable valueIdentifiable) + if (char.IsLower(name[0])) { - this.values.Add((name, $"{Arguments.GroupStartSeparator}{valueIdentifiable.AsArguments().ToString()}{Arguments.GroupEndSeparator}")); + var dotIndex = name.IndexOf('.'); + if (dotIndex > -1) + { + name = name.Substring(dotIndex + 1); + } + } + + if (value != null && value is IValueIdentifiable valueIdentifiable) + { + var valueId = valueIdentifiable.GetValueId(false); + this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), valueId.Value)); } else if (value != null) { var stringValue = value.ToString(); if (stringValue.HasValue) { - this.values.Add((name, stringValue)); + this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), new SingleValue(stringValue))); } } @@ -52,14 +64,33 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo } /// - /// 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 . + /// 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 Arguments Build() + /// A new . + public ValueId Build() { - return new Arguments() + var cardinality = this.values.ByCardinality(); + var metadata = isRoot ? Source.FromType(type).ToString() : null; + return cardinality switch { - Items = this.values.Select(x => new Argument(x.Name, x.Value.ToString()!)).ToValueArray(), + Empty empty => new ValueId(null, metadata, new SingleValue("null")), + Multiple valueIds => new ValueId(null, metadata, new ValueIds(this.values.ToValueArray())), + Single single => single.Item, }; } + + 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.Identification/Arguments.cs b/Source/Sundew.Base.Identification/ValueIds.cs similarity index 62% rename from Source/Sundew.Base.Identification/Arguments.cs rename to Source/Sundew.Base.Identification/ValueIds.cs index 038fe89..2aca7ad 100644 --- a/Source/Sundew.Base.Identification/Arguments.cs +++ b/Source/Sundew.Base.Identification/ValueIds.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -17,15 +17,14 @@ namespace Sundew.Base.Identification; using Sundew.Base.Collections.Immutable; using Sundew.Base.Text; -#pragma warning disable CS8907, CS1591 /// /// Represents arguments for an . /// -/// The arguments. -public readonly record struct Arguments(ValueArray Items) : IParsable +/// The value ids. +public sealed record ValueIds(ValueArray Items) : IValue { - /// The argument separator. - public const char ArgumentsSeparator = '&'; + /// The value id separator. + public const char ValueIdsSeparator = '&'; /// The group start separator. public const char GroupStartSeparator = '('; @@ -34,40 +33,40 @@ public readonly record struct Arguments(ValueArray Items) : IParsable< public const char GroupEndSeparator = ')'; /// - /// Parses the specified input string into an instance of the type. + /// 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 string representation of the argument to be parsed. This value must be a valid format for the > type. /// The format formatProvider. - /// An instance of Argument 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 Arguments Parse(string inputArgs, IFormatProvider? 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 ValueIds Parse(string inputValueId, IFormatProvider? formatProvider) { - if (TryParse(inputArgs, formatProvider, out var result)) + if (TryParse(inputValueId, formatProvider, out var result)) { return result; } - throw new FormatException($"The string: {inputArgs} is not a valid {nameof(Arguments)}."); + throw new FormatException($"The string: {inputValueId} is not a valid {nameof(ValueIds)}."); } /// - /// Tries to parse the specified input string into an instance of the type. + /// 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 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? inputArguments, IFormatProvider? formatProvider, out Arguments result) + public static bool TryParse([NotNullWhen(true)] string? inputValueId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out ValueIds result) { - var args = ImmutableArray.CreateBuilder(); - if (inputArguments.HasValue) + var args = ImmutableArray.CreateBuilder(); + if (inputValueId.HasValue) { - var argStartIndex = 0; + //// var argStartIndex = 0; var index = 0; var level = 0; - while (index < inputArguments.Length) + while (index < inputValueId.Length) { - var character = inputArguments[index++]; + var character = inputValueId[index++]; if (character == GroupStartSeparator) { level++; @@ -76,46 +75,51 @@ public static bool TryParse([NotNullWhen(true)] string? inputArguments, IFormatP { level--; } - else if (character == ArgumentsSeparator && level == 0) + else if (character == ValueIdsSeparator && level == 0) { - if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) + /*if (ValueId.TryParse(inputValueId.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) { args.Add(arg); argStartIndex = index; } else { - result = default; + result = null; return false; - } + }*/ } } - if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) + /*if (ValueId.TryParse(inputValueId.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) { args.Add(arg2); - } + }*/ - result = new Arguments(args.ToImmutable()); + result = new ValueIds(args.ToImmutable()); return true; } - result = default; + result = null; return false; } /// - /// Appends this to the specified . + /// Appends this to the specified . /// /// The string builder. /// The format provider. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) { - stringBuilder.AppendItems(this.Items, (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), Arguments.ArgumentsSeparator); + stringBuilder.AppendItems( + this.Items, + (builder) => builder.Append(GroupStartSeparator), + (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), + (builder) => builder.Append(GroupEndSeparator), + ValueIds.ValueIdsSeparator); } /// - /// Creates a string representation of the . + /// Creates a string representation of the . /// /// A string. public override string ToString() @@ -125,18 +129,6 @@ public override string ToString() return stringBuilder.ToString(); } - /// - /// Creates an from the specified builder func. - /// - /// The value id func. - /// A new . - public static Arguments From(Action valueIdFunc) - { - var valueIdBuilder = new ValueIdBuilder(); - valueIdFunc(valueIdBuilder); - return valueIdBuilder.Build(); - } - /// /// Gets the value from the arguments. /// @@ -156,7 +148,7 @@ public TValue Get(TValue defaultValue, IFormatProvider formatProvider, [ var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); if (argument.HasValue) { - return TValue.Parse(argument.Value, formatProvider); + return TValue.Parse(argument.Value.ToString() ?? string.Empty, formatProvider); } var firstDotIndex = referenceName.IndexOf('.'); @@ -166,7 +158,7 @@ public TValue Get(TValue defaultValue, IFormatProvider formatProvider, [ argument = this.Items.FirstOrDefault(x => x.Name == fallback); if (argument.HasValue) { - return TValue.Parse(argument.Value, formatProvider); + return TValue.Parse(argument.Value.ToString() ?? string.Empty, formatProvider); } return defaultValue; @@ -191,11 +183,7 @@ public TValue Get2(TValue defaultValue, IFormatProvider formatProvider, var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); if (argument.HasValue) { - var innerArguments = argument.TryGetValueArguments(); - if (innerArguments.IsSuccess) - { - return TValue.From(defaultValue, innerArguments.Value); - } + return TValue.From(defaultValue, argument); } var firstDotIndex = referenceName.IndexOf('.'); @@ -205,11 +193,7 @@ public TValue Get2(TValue defaultValue, IFormatProvider formatProvider, argument = this.Items.FirstOrDefault(x => x.Name == fallback); if (argument.HasValue) { - var innerArguments = argument.TryGetValueArguments(); - if (innerArguments.IsSuccess) - { - return TValue.From(defaultValue, innerArguments.Value); - } + return TValue.From(defaultValue, argument); } return defaultValue; diff --git a/Source/Sundew.Base.Parsing/ILexer.cs b/Source/Sundew.Base.Parsing/ILexer.cs new file mode 100644 index 0000000..3b7429e --- /dev/null +++ b/Source/Sundew.Base.Parsing/ILexer.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.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 +{ + /// + /// 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..d267b48 --- /dev/null +++ b/Source/Sundew.Base.Parsing/ILexerRule.cs @@ -0,0 +1,31 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 +{ + /// + /// 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..3e12587 --- /dev/null +++ b/Source/Sundew.Base.Parsing/LexerError.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.Parsing; + +/// +/// Represents a lexer error. +/// +public readonly record struct LexerError(string Input, int Position, int Length) +{ + /// + /// Gets the message. + /// + /// + /// The error message. + /// + public string GetMessage() + { + return $"Invalid input: {this.Input} at position: {this.Position}"; + } +} \ 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..f17458f --- /dev/null +++ b/Source/Sundew.Base.Parsing/Parser.cs @@ -0,0 +1,165 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +/// +/// Provides functionality to parse a sequence of tokens from an input string using a specified lexer. +/// +/// The type of tokens produced by the lexer. +public class Parser +{ + private readonly ILexer lexer; + private readonly string input; + private State state; + + /// + /// 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. + public Parser(ILexer lexer, string input) + { + this.lexer = lexer; + this.input = input; + this.state = new State(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. + /// 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) + { + if (this.lexer.TryGetLexeme(token, this.input, this.state, out lexeme, out var consumedLength)) + { + this.state = new State(this.state.Position + consumedLength); + return true; + } + + lexeme = string.Empty; + return false; + } + + /// + /// 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 bool Accept(char input) + { + if (this.IsNext(input)) + { + this.state = new State(this.state.Position + 1); + return true; + } + + return false; + } + + /// + /// 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 bool Accept(string input) + { + if (this.IsNext(input)) + { + this.state = new State(this.state.Position + input.Length); + return true; + } + + return false; + } + + /// + /// 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) + { + if (this.input[this.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) + { + if (this.input.AsSpan(this.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 bool AcceptEnd() + { + return this.state.Position == this.input.Length; + } + + /// + /// Gets the current state of the parser. + /// + /// The current state represented as a instance. + public State CurrentState() + { + return this.state; + } + + /// + /// Restores the object's state to the specified value. + /// + /// The state to restore. This parameter must not be null. + public void RestoreState(State state) + { + this.state = state; + } + + /// + /// 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/RegexLexerRule.cs b/Source/Sundew.Base.Parsing/RegexLexerRule.cs new file mode 100644 index 0000000..43eff2f --- /dev/null +++ b/Source/Sundew.Base.Parsing/RegexLexerRule.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.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 +{ + 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) + { + if (match.Groups.TryGetValue(TokenGroupName, out var matchingGroup)) + { + if (matchingGroup.Success) + { + return R.Success((matchingGroup.Value, match.Length)); + } + + return R.Error(new LexerError(input, state.Position, -1)); + } + + return R.Success((match.Value, match.Length)); + } + + return R.Error(new LexerError(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.slnx b/Source/Sundew.Base.slnx index d66c0ec..f6c6a61 100644 --- a/Source/Sundew.Base.slnx +++ b/Source/Sundew.Base.slnx @@ -45,6 +45,7 @@ + From a9cf41f2218454e85231b2d67c3956a122355fd2 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Fri, 10 Apr 2026 05:05:49 +0200 Subject: [PATCH 04/20] Continued work on Id parser --- Source/Directory.Build.targets | 4 + Source/Directory.Packages.props | 22 +- Source/Sundew.Base.Collections.Linq/Empty.cs | 4 +- Source/Sundew.Base.Collections.Linq/Item.cs | 24 ++ .../Sundew.Base.Collections.Linq/Multiple.cs | 4 +- Source/Sundew.Base.Collections.Linq/Single.cs | 2 +- .../{AIdTests.cs => IdTests.cs} | 115 +++--- .../DiscriminatedUnionMigrationsTests.cs | 6 +- .../Migrations/MigrationCancellationTests.cs | 6 +- .../System_Text_Json/DeserializationTests.cs | 2 +- .../System_Text_Json/MigrationTests.cs | 10 +- .../Sundew.Base.Development.Tests.csproj | 7 + Source/Sundew.Base.Identification/AId.cs | 238 ----------- .../AppendOptions.cs | 14 + .../Sundew.Base.Identification/ArrayValue.cs | 119 ++++++ .../{ValueIds.cs => ComplexValue.cs} | 104 +---- .../ExpressionEvaluator.cs | 15 +- .../Sundew.Base.Identification/IArguments.cs | 42 ++ .../IParserError.cs | 112 ++++++ Source/Sundew.Base.Identification/IValue.cs | 28 +- Source/Sundew.Base.Identification/Id.cs | 180 +++++++++ .../{AIdRoute.cs => IdRoute.cs} | 26 +- .../Parsing/AIdRouteParser.cs | 283 -------------- .../Parsing/Grammar.cs | 50 +++ .../Parsing/IdRouteParser.cs | 370 ++++++++++++++++++ .../Parsing/Tokens.cs | 2 +- .../{SingleValue.cs => ScalarValue.cs} | 12 +- Source/Sundew.Base.Identification/Segment.cs | 8 +- Source/Sundew.Base.Identification/Source.cs | 4 +- Source/Sundew.Base.Identification/Target.cs | 4 +- .../TargetEvaluator.cs | 29 +- Source/Sundew.Base.Identification/ValueId.cs | 58 ++- .../ValueIdBuilder.cs | 20 +- Source/Sundew.Base.Parsing/ILexer.cs | 1 + Source/Sundew.Base.Parsing/ILexerRule.cs | 1 + Source/Sundew.Base.Parsing/LexerError.cs | 83 +++- Source/Sundew.Base.Parsing/Parser.cs | 130 +++++- Source/Sundew.Base.Parsing/RegexLexerRule.cs | 5 +- Source/Sundew.Base.Primitives/Failure.cs | 10 +- .../ResultExtensions.cs | 172 ++++++++ .../ResultOfErrorExtensions.cs | 40 ++ Source/Sundew.Base.Primitives/RoE{TError}.cs | 20 + .../R{TSuccess,TError}.cs | 24 +- Source/Sundew.Base.Primitives/R{TSuccess}.cs | 20 + .../Sundew.Base.Threading/PostSubmitAction.cs | 8 +- .../ValueSynchronizer.cs | 1 - 46 files changed, 1595 insertions(+), 844 deletions(-) rename Source/Sundew.Base.Development.Tests/Identification/{AIdTests.cs => IdTests.cs} (58%) delete mode 100644 Source/Sundew.Base.Identification/AId.cs create mode 100644 Source/Sundew.Base.Identification/AppendOptions.cs create mode 100644 Source/Sundew.Base.Identification/ArrayValue.cs rename Source/Sundew.Base.Identification/{ValueIds.cs => ComplexValue.cs} (52%) create mode 100644 Source/Sundew.Base.Identification/IArguments.cs create mode 100644 Source/Sundew.Base.Identification/IParserError.cs create mode 100644 Source/Sundew.Base.Identification/Id.cs rename Source/Sundew.Base.Identification/{AIdRoute.cs => IdRoute.cs} (56%) delete mode 100644 Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs create mode 100644 Source/Sundew.Base.Identification/Parsing/Grammar.cs create mode 100644 Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs rename Source/Sundew.Base.Identification/{SingleValue.cs => ScalarValue.cs} (88%) create mode 100644 Source/Sundew.Base.Primitives/ResultExtensions.cs create mode 100644 Source/Sundew.Base.Primitives/ResultOfErrorExtensions.cs 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.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/Identification/AIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs similarity index 58% rename from Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs rename to Source/Sundew.Base.Development.Tests/Identification/IdTests.cs index 7cfd29e..0c9eecb 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -12,26 +12,26 @@ namespace Sundew.Base.Development.Tests.Identification; using AwesomeAssertions; using AwesomeAssertions.Execution; using Sundew.Base.Identification; -using static Sundew.Base.Development.Tests.Identification.AIdTests; +using static Sundew.Base.Development.Tests.Identification.IdTests; -public class AIdTests +public class IdTests { [Test] [Obsolete("Obsolete")] public void T() { - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,descriptions)?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri10); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(query,descriptions)?Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri11); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Start(!Name.Name.Space$Assembly=15)/Find?!Name.Name.Space$Assembly=(Address=Home,Number=15)&string[]=[Blue,Green]", UriKind.Absolute, out var uri12); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,descriptions)?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri10); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(query,descriptions)?Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri11); + Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Start(!Name~Name.Space$Assembly=15)/Find?!Name.Name.Space$Assembly=(Address=Home,Number=15)&string[]=[Blue,Green]", UriKind.Absolute, out var uri12); var t1 = Uri.EscapeUriString(uri6!.OriginalString); var t2 = Uri.EscapeUriString(uri8!.OriginalString); var t3 = Uri.EscapeUriString(uri9!.OriginalString); @@ -41,19 +41,22 @@ public void T() } [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,Description)?person=(Address=Home,Number=15)&description=(Eyes=Blue)")] - [Arguments("Name+Nested.Name.Space$Assembly/Find((Address=Home,Number=15)&descriptions=[Blue,Green])")] - [Arguments("Name+Nested.Name.Space$Assembly/Find(Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green])")] - [Arguments("Name+Nested.Name.Space$Assembly/Find(!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green])")] + [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 = AId.Parse(input, CultureInfo.InvariantCulture); + var result = Id.Parse(input, CultureInfo.InvariantCulture); using (new AssertionScope()) { @@ -65,12 +68,12 @@ public void Parse_Then_ResultShouldNotBeNull(string input) [Test] public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; - var result = AId.From(x => x.GoBack()); + const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; + var result = Id.From(x => x.GoBack()); using (new AssertionScope()) { - result.Should().NotBeNull(); + result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture)); result.ToString().Should().Be(expectedResult); result.TryGetInputTypes().Value.Should().Equal([]); result.TryGetResultType().Value.Should().Be(typeof(void)); @@ -81,12 +84,12 @@ public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=null))"; - var result = AId.From(x => x.NavigateTo(null!)); + 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 (new AssertionScope()) { - result.Should().NotBeNull(); + 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)); @@ -97,12 +100,12 @@ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull2() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=6&Y=4)))"; - var result = AId.From(x => x.NavigateTo(new Position(6, 4))); + 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 (new AssertionScope()) { - result.Should().NotBeNull(); + 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)); @@ -113,12 +116,12 @@ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull2() [Test] public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/NavigateTo((position!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=null&addToHistory=False))"; - var result = AId.From(x => x.NavigateTo(null!, default)); + 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 (new AssertionScope()) { - result.Should().NotBeNull(); + 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)); @@ -129,12 +132,12 @@ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() { - const string expectedResult = "AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; - var result = AId.From(x => x.X); + const string expectedResult = "IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; + var result = Id.From(x => x.X); using (new AssertionScope()) { - result.Should().NotBeNull(); + 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)); @@ -145,10 +148,10 @@ public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() [Test] public void AsArguments_Then_ResultShouldBeExpectedResult() { - const string expectedResult = "!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=5)"; + 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.GetValueId(true); + var valueId = position.Id; var result = valueId.ToValue(new Position(0, 0)); using (new AssertionScope()) @@ -161,10 +164,10 @@ public void AsArguments_Then_ResultShouldBeExpectedResult() [Test] public void AsArguments_Then_ResultShouldBeExpectedResult2() { - const string expectedResult = "!AIdTests+Position3D.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Position=(X=4&Y=5)&Z=6)"; + 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.GetValueId(true); + var valueId = position.Id; var result = valueId.ToValue(new Position3D(new Position(0, 0), 0)); using (new AssertionScope()) @@ -177,12 +180,12 @@ public void AsArguments_Then_ResultShouldBeExpectedResult2() [Test] public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull2() { - const string expectedResult = "AIdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate/Execute((parameter!AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6)))"; - var result = AId.From(x => x.Navigate.Execute(AId.Argument()), new Position(4, 6)); + 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 (new AssertionScope()) { - result.Should().NotBeNull(); + 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)); @@ -193,12 +196,13 @@ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull2( [Test] public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull3() { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate?wAIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=6)"; - var result = AId.From(x => x.Navigate, new Position(4, 6)); + 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 (new AssertionScope()) + using (var scope = new AssertionScope()) { - result.Should().NotBeNull(); + scope.FormattingOptions.MaxDepth = 20; + Id.Parse(result.ToString(), CultureInfo.InvariantCulture).Should().Be(result); result.ToString().Should().Be(expectedResult); result.TryGetInputTypes().Value.Should().Equal([typeof(Position)]); result.TryGetResultType().Value.Should().Be(typeof(ICommand)); @@ -226,7 +230,7 @@ public interface ICommand public record Position(int X, int Y) : IValueIdentifiable { - public ValueId GetValueId(bool isRoot) => ValueId.From(this, (value, builder) => builder.Add(value.X).Add(value.Y), isRoot); + public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.X).Add(value.Y)); public static Position From(Position position, ValueId valueId) { @@ -238,10 +242,7 @@ public static Position From(Position position, ValueId valueId) public record Position3D(Position Position, int Z) : IValueIdentifiable { - public ValueId GetValueId(bool isRoot) - { - return ValueId.From(this, (value, builder) => builder.Add(value.Position).Add(value.Z), isRoot); - } + public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.Position).Add(value.Z)); public static Position3D From(Position3D value, ValueId valueId) { 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/Sundew.Base.Development.Tests.csproj b/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj index 993b86e..f15cadf 100644 --- a/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj +++ b/Source/Sundew.Base.Development.Tests/Sundew.Base.Development.Tests.csproj @@ -39,4 +39,11 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/Source/Sundew.Base.Identification/AId.cs b/Source/Sundew.Base.Identification/AId.cs deleted file mode 100644 index 35bc9cc..0000000 --- a/Source/Sundew.Base.Identification/AId.cs +++ /dev/null @@ -1,238 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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 record AId(Source Source, Path? Path, ValueId? ValueId = null) : IParsable -{ - /// The value ids separator. - public const char ValueIdsSeparator = '?'; - - /// - /// 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 AId Parse(string inputAId, IFormatProvider? provider) - { - if (TryParse(inputAId, provider, out var result)) - { - return result; - } - - throw new FormatException($"The string: {inputAId} is not a valid {nameof(AId)}"); - } - - /// - /// 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? inputAId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId result) - { - return AIdRouteParser.TryGetAId(inputAId, formatProvider, out result); - /* - if (inputAId.HasValue) - { - var argumentsSeparatorIndex = inputAId.IndexOf(ValueIdsSeparator); - if (argumentsSeparatorIndex > -1) - { - var targetString = inputAId.Substring(0, argumentsSeparatorIndex); - var argumentsString = inputAId.Substring(argumentsSeparatorIndex + 1); - if (TryParseTarget(targetString, formatProvider, out var target)) - { - result = new AId(target.Source, target.Path); - return true; - } - } - else if (TryParseTarget(inputAId, formatProvider, out var target)) - { - result = new AId(target.Source, target.Path); - 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); - } - - if (this.ValueId.HasValue) - { - stringBuilder.Append(ValueIdsSeparator); - this.ValueId.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 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.ValueId); - } - - /// - /// 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 source type. - /// The target expression. - /// A new . - public static AId From(Expression> targetExpression) - { - var (source, path, valueId) = ExpressionEvaluator.From(targetExpression); - return new AId(source, path, valueId); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// A new . - public static AId From(Expression> targetExpression) - { - var target = ExpressionEvaluator.From(targetExpression); - return new AId(target.Source, target.Path); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// The value. - /// A new . - public static AId From(Expression> targetExpression, IIdentifiable value) - { - var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value); - return new AId(source, path, valueId); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// The value. - /// A new . - public static AId From(Expression> targetExpression, IIdentifiable value) - { - var target = ExpressionEvaluator.From(targetExpression); - return new AId(target.Source, target.Path, value.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 TryParseTarget([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; - } - - /// - /// 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/AppendOptions.cs b/Source/Sundew.Base.Identification/AppendOptions.cs new file mode 100644 index 0000000..25afe6c --- /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 sealed record AppendOptions(bool IsRoot); \ 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..ee0ab70 --- /dev/null +++ b/Source/Sundew.Base.Identification/ArrayValue.cs @@ -0,0 +1,119 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Linq; +using System.Runtime.CompilerServices; +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.AppendItems( + this.Items, + (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }), + Grammar.ValueIdsSeparator); + } + + /// + /// 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(); + } + + /// + /// 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 Get(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + return TValue.Parse(argument.Value.ToString() ?? string.Empty, formatProvider); + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + return TValue.Parse(argument.Value.ToString() ?? string.Empty, 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 Get2(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."); + } + + var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + return TValue.From(defaultValue, argument); + } + + var firstDotIndex = referenceName.IndexOf('.'); + var fallback = firstDotIndex > -1 + ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) + : null; + argument = this.Items.FirstOrDefault(x => x.Name == fallback); + if (argument.HasValue) + { + return TValue.From(defaultValue, argument); + } + + return defaultValue; + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ValueIds.cs b/Source/Sundew.Base.Identification/ComplexValue.cs similarity index 52% rename from Source/Sundew.Base.Identification/ValueIds.cs rename to Source/Sundew.Base.Identification/ComplexValue.cs index 2aca7ad..d54396b 100644 --- a/Source/Sundew.Base.Identification/ValueIds.cs +++ b/Source/Sundew.Base.Identification/ComplexValue.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -15,117 +15,39 @@ namespace Sundew.Base.Identification; using System.Runtime.CompilerServices; using System.Text; using Sundew.Base.Collections.Immutable; +using Sundew.Base.Identification.Parsing; using Sundew.Base.Text; /// -/// Represents arguments for an . +/// Represents arguments for an . /// /// The value ids. -public sealed record ValueIds(ValueArray Items) : IValue +public sealed partial record ComplexValue(ValueArray Items) : IValue { - /// The value id separator. - public const char ValueIdsSeparator = '&'; - - /// The group start separator. - public const char GroupStartSeparator = '('; - - /// The group end separator. - public const char GroupEndSeparator = ')'; - - /// - /// 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 ValueIds Parse(string inputValueId, IFormatProvider? formatProvider) - { - if (TryParse(inputValueId, formatProvider, out var result)) - { - return result; - } - - throw new FormatException($"The string: {inputValueId} is not a valid {nameof(ValueIds)}."); - } - - /// - /// 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? inputValueId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out ValueIds result) - { - var args = ImmutableArray.CreateBuilder(); - if (inputValueId.HasValue) - { - //// var argStartIndex = 0; - var index = 0; - var level = 0; - while (index < inputValueId.Length) - { - var character = inputValueId[index++]; - if (character == GroupStartSeparator) - { - level++; - } - else if (character == GroupEndSeparator) - { - level--; - } - else if (character == ValueIdsSeparator && level == 0) - { - /*if (ValueId.TryParse(inputValueId.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) - { - args.Add(arg); - argStartIndex = index; - } - else - { - result = null; - return false; - }*/ - } - } - - /*if (ValueId.TryParse(inputValueId.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) - { - args.Add(arg2); - }*/ - - result = new ValueIds(args.ToImmutable()); - return true; - } - - result = null; - return false; - } - /// - /// Appends this to the specified . + /// Appends this to the specified . /// /// The string builder. /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + /// The append options. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) { stringBuilder.AppendItems( this.Items, - (builder) => builder.Append(GroupStartSeparator), - (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), - (builder) => builder.Append(GroupEndSeparator), - ValueIds.ValueIdsSeparator); + (builder) => builder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupStart)), + (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }), + (builder) => builder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupEnd)), + Grammar.ValueIdsSeparator); } /// - /// Creates a string representation of the . + /// Creates a string representation of the . /// /// A string. public override string ToString() { var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true)); return stringBuilder.ToString(); } diff --git a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs index 1859e9c..78b8296 100644 --- a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs +++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs @@ -19,9 +19,6 @@ namespace Sundew.Base.Identification; /// internal static class ExpressionEvaluator { - /// The argument separator. - public const char ArgumentSeparator = ','; - /// /// Gets a for the specified expression. /// @@ -56,7 +53,7 @@ void EvaluateToPath(Expression expression) var parameterInfos = methodCallExpression.Method.GetParameters(); foreach (var argument in methodCallExpression.Arguments.Zip(parameterInfos)) { - if (argument.First is MethodCallExpression argumentMethodCallExpression && argumentMethodCallExpression.Method.DeclaringType == typeof(AId) && argumentMethodCallExpression.Method.Name == nameof(AId.Argument) && valueId.HasValue) + if (argument.First is MethodCallExpression argumentMethodCallExpression && argumentMethodCallExpression.Method.DeclaringType == typeof(Id) && argumentMethodCallExpression.Method.Name == nameof(Id.Argument) && valueId.HasValue) { valueIds.Add(valueId with { Name = argument.Second.Name + valueId.Name }); isUsed = true; @@ -67,7 +64,7 @@ void EvaluateToPath(Expression expression) } } - segments.Add(new Segment(methodCallExpression.Method.Name, new ValueId(null, null, new ValueIds(valueIds.ToValueArray())))); + segments.Add(new Segment(methodCallExpression.Method.Name, new ComplexValue(valueIds.ToValueArray()))); break; case MemberExpression memberExpression: @@ -90,7 +87,7 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo switch (argument) { case ConstantExpression constantExpression: - builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new SingleValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default")))); + builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new ScalarValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default")))); break; case MemberExpression memberExpression: if (memberExpression.Expression is ConstantExpression constantExpression2) @@ -99,13 +96,13 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo if (memberExpression.Member is FieldInfo fieldInfo) { var value = fieldInfo.GetValue(container); - builder.Add(new ValueId(null, null, new SingleValue(value?.ToString() ?? string.Empty))); + builder.Add(new ValueId(null, null, new ScalarValue(value?.ToString() ?? string.Empty))); } if (memberExpression.Member is PropertyInfo propertyInfo) { var value = propertyInfo.GetValue(container); - builder.Add(new ValueId(null, null, new SingleValue(value?.ToString() ?? string.Empty))); + builder.Add(new ValueId(null, null, new ScalarValue(value?.ToString() ?? string.Empty))); } } @@ -119,7 +116,7 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo GetArgument(valueTuple.First, valueTuple.Second, newBuilder); } - builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new ValueIds(newBuilder.ToImmutable()))); + builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new ComplexValue(newBuilder.ToImmutable()))); } break; diff --git a/Source/Sundew.Base.Identification/IArguments.cs b/Source/Sundew.Base.Identification/IArguments.cs new file mode 100644 index 0000000..1c0de3e --- /dev/null +++ b/Source/Sundew.Base.Identification/IArguments.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; +using System.Text; +using Sundew.DiscriminatedUnions; + +/// +/// Represents a value. +/// +[DiscriminatedUnion] +public partial interface IArguments +{ + /// + /// Appends this to the specified . + /// + /// The string builder. + /// The format provider. + /// The append options. + void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions); + + /// + /// Converts the current instance to a collection of value identifiers. + /// + /// A object representing the value identifiers, or null if the instance does not correspond + /// to any value identifiers. + ComplexValue ToValueIds() + { + return this switch + { + ArrayValue arrayValue => new ComplexValue([new ValueId(null, null, arrayValue)]), + ScalarValue singleValue => new ComplexValue([new ValueId(null, null, singleValue)]), + ComplexValue valueIds => valueIds, + }; + } +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/IParserError.cs b/Source/Sundew.Base.Identification/IParserError.cs new file mode 100644 index 0000000..4183c7c --- /dev/null +++ b/Source/Sundew.Base.Identification/IParserError.cs @@ -0,0 +1,112 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 GroupValueIdError(object Cause, LexerError? Error) : IArgumentsError; +} + +/// +/// Union for errors. +/// +[DiscriminatedUnion] +public partial interface IValueIdError : IParserError +{ + public sealed partial record ValueIdError(object Cause, LexerError? Error) : IValueIdError; + + public sealed partial record ValueIdValueError(IValueError Error) : IValueIdError; +} + +/// +/// Union for errors. +/// +[DiscriminatedUnion] +public partial interface IValueError : IParserError +{ + public sealed partial record GroupError(char Expected, LexerError Error) : IValueError; + + public sealed partial record ArrayError(char Expected, LexerError Error) : IValueError; + + public sealed partial record ValueIdError(IValueIdError Error) : IValueError; + + public sealed partial record ValueError(LexerError? Error) : IValueError; +} + +/// +/// Represents an error when there was still input to process. +/// +public sealed partial record NotAtEndError() : IIdError, IIdRouteError; + +/// +/// Represents an error when an Id is empty or null. +/// +public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError; + +#pragma warning restore SA1402 diff --git a/Source/Sundew.Base.Identification/IValue.cs b/Source/Sundew.Base.Identification/IValue.cs index 11a3e64..91d4b6d 100644 --- a/Source/Sundew.Base.Identification/IValue.cs +++ b/Source/Sundew.Base.Identification/IValue.cs @@ -8,37 +8,15 @@ namespace Sundew.Base.Identification; using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; +using Sundew.DiscriminatedUnions; /// /// Represents a value. /// -public interface IValue +[DiscriminatedUnion] +public partial interface IValue : IArguments { - /// - /// Appends this to the specified . - /// - /// The string builder. - /// The format provider. - void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider); - - /// - /// Converts the current instance to a collection of value identifiers. - /// - /// A object representing the value identifiers, or null if the instance does not correspond - /// to any value identifiers. - ValueIds ToValueIds() - { - return this switch - { - SingleValue singleValue => new ValueIds([new ValueId(null, null, singleValue)]), - ValueIds valueIds => valueIds, - _ => throw new NotSupportedException($"The type {this.GetType()} is not supported."), - }; - } - /// /// Gets the value from the arguments. /// diff --git a/Source/Sundew.Base.Identification/Id.cs b/Source/Sundew.Base.Identification/Id.cs new file mode 100644 index 0000000..cbcae02 --- /dev/null +++ b/Source/Sundew.Base.Identification/Id.cs @@ -0,0 +1,180 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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.Collections.Immutable; +using Sundew.Base.Identification.Parsing; + +/// +/// Represents any Id. +/// +public record Id(Source Source, Path? Path, IArguments? Arguments = null) : 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 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)); + } + } + + /// + /// 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 source type. + /// The target expression. + /// A new . + public static Id From(Expression> targetExpression) + { + var (source, path, valueId) = ExpressionEvaluator.From(targetExpression); + return new Id(source, path, valueId.HasValue ? new ComplexValue([valueId]) : null); + } + + /// + /// 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, value); + return new Id(source, path, valueId.HasValue ? new ComplexValue(ValueArray.Empty.Add(value.Id)) : 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, value); + return new Id(target.Source, target.Path, new ComplexValue(ValueArray.Empty.Add(value.Id))); + } + + /// + /// 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/AIdRoute.cs b/Source/Sundew.Base.Identification/IdRoute.cs similarity index 56% rename from Source/Sundew.Base.Identification/AIdRoute.cs rename to Source/Sundew.Base.Identification/IdRoute.cs index fdd72bc..a423751 100644 --- a/Source/Sundew.Base.Identification/AIdRoute.cs +++ b/Source/Sundew.Base.Identification/IdRoute.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -13,36 +13,36 @@ namespace Sundew.Base.Identification; using Sundew.Base.Identification.Parsing; /// -/// Represents a route consisting of a path of . +/// Represents a route consisting of a path of . /// -public sealed record AIdRoute(ValueList Path) : IParsable +public sealed record IdRoute(ValueList Path) : IParsable { /// - /// Parses the specified input string into an instance of the type. + /// 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 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 AIdRoute Parse(string inputAIdRoute, IFormatProvider? provider) + /// Thrown if the input string is not in a valid format for the > type. + public static IdRoute Parse(string inputIdRoute, IFormatProvider? provider) { - if (TryParse(inputAIdRoute, provider, out var result)) + if (TryParse(inputIdRoute, provider, out var result)) { return result; } - throw new FormatException($"The string: {inputAIdRoute} is not a valid {nameof(AId)}"); + 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. + /// 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 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? inputAidRoute, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AIdRoute result) + public static bool TryParse([NotNullWhen(true)] string? inputIdRoute, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out IdRoute result) { - return AIdRouteParser.TryGetAIdRoute(inputAidRoute, formatProvider, out result); + return IdRouteParser.ParseIdRoute(inputIdRoute, formatProvider).TryGet(out result, out _); } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs deleted file mode 100644 index 61a6a2e..0000000 --- a/Source/Sundew.Base.Identification/Parsing/AIdRouteParser.cs +++ /dev/null @@ -1,283 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -using Sundew.Base.Collections.Linq; -using Sundew.Base.Parsing; - -internal static class AIdRouteParser -{ - private static readonly Lexer AidLexer; - private static readonly Lexer AidRouteLexer; - - static AIdRouteParser() - { - var sourceNameTokenMatcher = new RegexLexerRule(Tokens.SourceName, new Regex("[^~]+", RegexOptions.Compiled)); - var sourcePathTokenMatcher = new RegexLexerRule(Tokens.SourcePath, new Regex("[^$~]+", RegexOptions.Compiled)); - var sourceOriginTokenMatcher = new RegexLexerRule(Tokens.SourceOrigin, new Regex("[^$~//>]*", RegexOptions.Compiled)); - var segmentNameTokenMatcher = new RegexLexerRule(Tokens.SegmentName, new Regex("[^$~//?(>]+", RegexOptions.Compiled)); - var valueIdNameTokenMatcher = new RegexLexerRule(Tokens.ValueIdName, new Regex("[^!=]", RegexOptions.Compiled)); - var valueIdMetadataTokenMatcher = new RegexLexerRule(Tokens.ValueIdMetadata, new Regex("[^=]+", RegexOptions.Compiled)); - var valueIdValueTokenMatcher = new RegexLexerRule(Tokens.ValueIdValue, new Regex("[^)&]+", RegexOptions.Compiled)); - AidRouteLexer = new Lexer( - [sourceNameTokenMatcher, - sourcePathTokenMatcher, - sourceOriginTokenMatcher, - segmentNameTokenMatcher, - valueIdNameTokenMatcher, - valueIdMetadataTokenMatcher, - valueIdValueTokenMatcher]); - AidLexer = new Lexer( - [sourceNameTokenMatcher, - sourcePathTokenMatcher, - sourceOriginTokenMatcher, - segmentNameTokenMatcher, - valueIdNameTokenMatcher, - valueIdMetadataTokenMatcher, - valueIdValueTokenMatcher]); - } - - public static bool TryGetAIdRoute( - string? input, - IFormatProvider? formatProvider, - [MaybeNullWhen(false)] out AIdRoute aIdRoute) - { - if (!input.HasValue) - { - aIdRoute = null; - return false; - } - - var parser = new Parser(AidRouteLexer, input); - if (TryGetAIdRoute(parser, out aIdRoute) && parser.AcceptEnd()) - { - return true; - } - - return false; - } - - public static bool TryGetAId(string? input, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId aId) - { - if (!input.HasValue) - { - aId = null; - return false; - } - - var parser = new Parser(AidLexer, input); - - if (TryGetAId(parser, out aId) && parser.AcceptEnd()) - { - return true; - } - - return false; - } - - private static bool TryGetAIdRoute(Parser parser, [MaybeNullWhen(false)] out AIdRoute aIdRoute) - { - var route = new List(); - while (TryGetAId(parser, out var aid)) - { - route.Add(aid); - if (!parser.Accept('>')) - { - break; - } - } - - if (route.Count > 0) - { - aIdRoute = new AIdRoute([..route]); - return true; - } - - aIdRoute = null; - return false; - } - - private static bool TryGetAId(Parser parser, [MaybeNullWhen(false)] out AId aid) - { - if (TryGetSource(parser, out var source)) - { - Path? path = null; - if (parser.Accept('/')) - { - if (TryGetPath(parser, out path)) - { - } - else - { - } - } - - ValueId? valueId = null; - if (parser.Accept('?')) - { - if (TryGetValueId(parser, out valueId)) - { - } - else - { - } - } - - aid = new AId(source, path, valueId); - return true; - } - - aid = null; - return false; - } - - private static bool TryGetSource(Parser parser, [MaybeNullWhen(false)] out Source source) - { - if (parser.Accept(Tokens.SourceName, out var sourceName) && parser.Accept('~') && - parser.Accept(Tokens.SourcePath, out var sourcePath) && parser.Accept('$') && - parser.Accept(Tokens.SourceOrigin, out var sourceOrigin)) - { - source = new Source(sourceName, sourcePath, sourceOrigin); - return true; - } - - source = null; - return false; - } - - private static bool TryGetPath(Parser parser, [MaybeNullWhen(false)] out Path path) - { - var segments = new List(); - while (!parser.IsNext('?') && !parser.AcceptEnd()) - { - if (parser.Accept(Tokens.SegmentName, out var segmentName)) - { - if (parser.Accept('(') && TryGetValueId(parser, out var valueId) && parser.Accept(')')) - { - segments.Add(new Segment(segmentName, valueId)); - } - else - { - segments.Add(new Segment(segmentName, null)); - } - } - } - - path = new Path([..segments]); - return true; - } - - private static bool TryGetValueId(Parser parser, [MaybeNullWhen(false)] out ValueId valueId) - { - if (parser.Accept('(')) - { - if (!TrySingleValue(parser, out valueId)) - { - valueId = null; - return false; - } - - if (parser.Accept(')')) - { - return true; - } - } - else - { - } - - valueId = null; - return false; - } - - private static bool TrySingleValue(Parser parser, [MaybeNullWhen(false)] out ValueId valueId) - { - string? name = null; - string? metadata = null; - IValue? value = null; - var lexemesState = parser.CurrentState(); - if (!parser.Accept(Tokens.ValueIdName, out name) || !parser.Accept('!') || - !parser.Accept(Tokens.ValueIdMetadata, out metadata) || !parser.Accept('=') || - !TryGetValue(parser, out value)) - { - name = null; - metadata = null; - parser.RestoreState(lexemesState); - if (!parser.Accept('!') || - !parser.Accept(Tokens.ValueIdMetadata, out metadata) || !parser.Accept('=') || - !TryGetValue(parser, out value)) - { - name = null; - metadata = null; - parser.RestoreState(lexemesState); - if (!parser.Accept(Tokens.ValueIdName, out name) || !parser.Accept('=') || - !TryGetValue(parser, out value)) - { - parser.RestoreState(lexemesState); - if (!TryGetValue(parser, out value)) - { - valueId = null; - return false; - } - } - } - } - - valueId = new ValueId(name, metadata, value); - return true; - } - - private static bool TryGetValue(Parser parser, [MaybeNullWhen(false)] out IValue value) - { - if (parser.Accept('(')) - { - var valueIds = new List(); - while (!parser.IsNext(')')) - { - if (TrySingleValue(parser, out var valueId) && parser.Accept(')')) - { - valueIds.Add(valueId); - if (!parser.Accept('&')) - { - break; - } - } - else - { - value = null; - return false; - } - } - - if (parser.Accept(')')) - { - value = valueIds.ByCardinality() switch - { - Empty empty => null, - Multiple multiple => new ValueIds([..valueIds]), - Single single => single.Item.Value, - }; - - return value != null; - } - } - - if (parser.Accept(Tokens.ValueIdValue, out var rawValue)) - { - value = new SingleValue(rawValue); - return true; - } - - value = null; - return false; - } -} \ 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..252acf4 --- /dev/null +++ b/Source/Sundew.Base.Identification/Parsing/Grammar.cs @@ -0,0 +1,50 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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. + public const char PathSegmentSeparator = '/'; + + /// The value ids separator. + public const char ArgumentsSeparator = '?'; + + /// Metadata separator. + public const char NameMetadataSeparator = '!'; + + /// Key Value separator. + public const char KeyValueSeparator = '='; + + /// The argument separator. + public const char ArgumentSeparator = ','; + + /// The start of and array. + public const char ArrayStart = '['; + + /// The end of an array. + public const char ArrayEnd = ']'; + + /// The value id separator. + public const char ValueIdsSeparator = '&'; + + /// 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/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs new file mode 100644 index 0000000..0bd8fd4 --- /dev/null +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -0,0 +1,370 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 IdLexer; + private static readonly Lexer IdRouteLexer; + + 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 valueIdNameLexerRule = new RegexLexerRule(Tokens.ValueIdName, new Regex("[^!=]+", RegexOptions.Compiled)); + var valueIdMetadataLexerRule = new RegexLexerRule(Tokens.ValueIdMetadata, new Regex("[^=]+", RegexOptions.Compiled)); + var valueIdValueLexerRule = new RegexLexerRule(Tokens.ValueIdValue, new Regex("[^)&]+", RegexOptions.Compiled)); + IdRouteLexer = new Lexer( + [sourceNameLexerRule, + sourcePathLexerRule, + sourceOriginLexerRule, + segmentNameLexerRule, + valueIdNameLexerRule, + valueIdMetadataLexerRule, + valueIdValueLexerRule]); + IdLexer = new Lexer( + [sourceNameLexerRule, + sourcePathLexerRule, + sourceOriginLexerRule, + segmentNameLexerRule, + valueIdNameLexerRule, + valueIdMetadataLexerRule, + valueIdValueLexerRule]); + } + + 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); + } + + 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)); + } + } + + IArguments? 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)); + } + } + + return R.Success(new Id(sourceResult.Value, path, arguments)); + } + + 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)) + { + 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 value = Value(parser); + if (value.IsSuccess) + { + valueIds.Add(new ValueId(null, null, value.Value)); + if (!parser.Accept(Grammar.ValueIdsSeparator)) + { + break; + } + + continue; + } + + var groupValueIdResult = parser.TryAccept( + Grammar.GroupStart, + r => r.MapError(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupStart, lexerError)) + .And(() => ValueId(parser).MapError(IArgumentsError._ValueIdError)) + .And(() => parser.Accept(Grammar.GroupEnd).Map(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupEnd, lexerError))) + .Map(x => x.Value2)); + + /*var groupValueIdResult = parser.TryAccept( + Grammar.GroupStart, + r => r.MapError(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupStart, lexerError)) + .And(() => ValueId(parser).MapError(IArgumentsError._ValueIdError)) + .And(() => parser.Accept(Grammar.GroupEnd).Map(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupEnd, lexerError))) + .Map(x => x.Value2));*/ + if (groupValueIdResult.IsSuccess) + { + valueIds.Add(groupValueIdResult.Value); + } + else if (parser.IsNext(Grammar.GroupEnd)) + { + } + else + { + var valueIdResult = ValueId(parser); + if (valueIdResult.IsSuccess) + { + valueIds.Add(valueIdResult.Value); + } + } + + if (groupValueIdResult.Error is IArgumentsError.GroupValueIdError { Cause: Grammar.GroupEnd }) + { + return R.Error(groupValueIdResult.Error); + } + + if (!parser.Accept(Grammar.ValueIdsSeparator)) + { + break; + } + } + + return R.Success(IArguments.ComplexValue(valueIds.ToValueArray())); + } + + private static R ValueId(Parser parser) + { + var fullValueIdResult = parser.TryAccept( + Tokens.ValueIdName, + result => result.MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdName, lexerError)) + .And(() => parser.Accept(Grammar.NameMetadataSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.NameMetadataSeparator, lexerError))) + .And(() => parser.Accept(Tokens.ValueIdMetadata).MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdMetadata, lexerError))) + .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) + .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) + .Map(x => new ValueId(x.Value1, x.Value2, x.Value3))); + if (fullValueIdResult.IsSuccess) + { + return fullValueIdResult; + } + + fullValueIdResult = parser.TryAccept( + Grammar.NameMetadataSeparator, + result => result.MapError(lexerError => IValueIdError._ValueIdError(Grammar.NameMetadataSeparator, lexerError)) + .And(() => parser.Accept(Tokens.ValueIdMetadata).MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdMetadata, lexerError))) + .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) + .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) + .Map(x => new ValueId(null, x.Value2, x.Value3))); + if (fullValueIdResult.IsSuccess) + { + return fullValueIdResult; + } + + fullValueIdResult = parser.TryAccept( + Tokens.ValueIdName, + result => result.MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdName, lexerError)) + .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) + .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) + .Map(x => new ValueId(x.Value1, null, x.Value2))); + if (fullValueIdResult.IsSuccess) + { + return fullValueIdResult; + } + + return Value(parser).Map(x => new ValueId(null, null, x), IValueIdError._ValueIdValueError); + } + + private static R Value(Parser parser) + { + var valueResult = parser.TryAccept( + Grammar.GroupStart, + result => + { + return result.MapError(lexerError => IValueError._GroupError(Grammar.GroupStart, lexerError)) + .And(() => + { + var valueIds = ImmutableArray.CreateBuilder(); + while (!parser.IsNext(Grammar.GroupStart)) + { + var valueIdResult = ValueId(parser); + if (valueIdResult.IsSuccess) + { + valueIds.Add(valueIdResult.Value); + if (!parser.Accept(Grammar.ValueIdsSeparator)) + { + break; + } + } + else + { + return R.Error(IValueError._ValueIdError(valueIdResult.Error)); + } + } + + return R.Success(new ComplexValue(valueIds.ToValueArray())).Omits(); + }) + .And(() => parser.Accept(Grammar.GroupEnd) + .Map(lexerError => IValueError._GroupError(Grammar.GroupEnd, lexerError))) + .Map(x => x.Value2); + }); + if (valueResult.IsSuccess) + { + return valueResult; + } + + valueResult = parser.TryAccept( + Grammar.ArrayStart, + result => + { + return result.MapError(lexerError => IValueError._ArrayError(Grammar.GroupStart, lexerError)) + .And(() => + { + var valueIds = ImmutableArray.CreateBuilder(); + while (!parser.IsNext(Grammar.GroupStart)) + { + var singleValueIdResult = ValueId(parser); + if (singleValueIdResult.IsSuccess) + { + valueIds.Add(singleValueIdResult.Value); + if (!parser.Accept(Grammar.ValueIdsSeparator)) + { + 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._ArrayError(Grammar.ArrayEnd, lexerError))) + .Map(x => x.Value2); + }); + if (valueResult.IsSuccess) + { + return valueResult; + } + + return parser.Accept(Tokens.ValueIdValue).Map(x => IValue.ScalarValue(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 index 413c980..6947265 100644 --- a/Source/Sundew.Base.Identification/Parsing/Tokens.cs +++ b/Source/Sundew.Base.Identification/Parsing/Tokens.cs @@ -15,7 +15,7 @@ internal enum Tokens SourceOrigin, - SegmentName, + PathSegmentName, ValueIdName, diff --git a/Source/Sundew.Base.Identification/SingleValue.cs b/Source/Sundew.Base.Identification/ScalarValue.cs similarity index 88% rename from Source/Sundew.Base.Identification/SingleValue.cs rename to Source/Sundew.Base.Identification/ScalarValue.cs index dbc6ae4..39dcfd4 100644 --- a/Source/Sundew.Base.Identification/SingleValue.cs +++ b/Source/Sundew.Base.Identification/ScalarValue.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -8,22 +8,22 @@ namespace Sundew.Base.Identification; using System; -using System.Collections.Generic; using System.Globalization; using System.Text; /// -/// Represents an argument in a . +/// Represents an argument in a . /// /// The value. -public sealed record SingleValue(string Value) : IValue +public sealed partial record ScalarValue(string Value) : IValue { /// /// Appends this to the specified . /// /// The string builder. /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + /// The append options. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) { stringBuilder.Append(this.Value); } @@ -63,7 +63,7 @@ public TValue Get2(TValue defaultValue, IFormatProvider formatProvider, public override string ToString() { var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + 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 index 2754993..95b1af2 100644 --- a/Source/Sundew.Base.Identification/Segment.cs +++ b/Source/Sundew.Base.Identification/Segment.cs @@ -14,8 +14,8 @@ namespace Sundew.Base.Identification; /// Represents a segment with a specified name and optional associated value identifiers. /// /// The name of the segment, which serves as its identifier. -/// A value id for the segment. -public sealed record Segment(string Name, ValueId? ValueId = null) +/// A value for the segment. +public sealed record Segment(string Name, IArguments? Arguments = null) { /// /// Appends the name of the current instance to the specified StringBuilder, followed by parentheses. @@ -27,10 +27,10 @@ public StringBuilder AppendInto(StringBuilder builder, IFormatProvider formatPro { builder.Append(this.Name); - if (this.ValueId.HasValue) + if (this.Arguments.HasValue) { builder.Append('('); - this.ValueId.AppendInto(builder, formatProvider); + this.Arguments.AppendInto(builder, formatProvider, new AppendOptions(true)); builder.Append(')'); } diff --git a/Source/Sundew.Base.Identification/Source.cs b/Source/Sundew.Base.Identification/Source.cs index 9804838..81fcc0b 100644 --- a/Source/Sundew.Base.Identification/Source.cs +++ b/Source/Sundew.Base.Identification/Source.cs @@ -13,7 +13,7 @@ namespace Sundew.Base.Identification; using System.Text; /// -/// Represents a source for an . +/// Represents a source for an . /// /// The Origin. /// The Path. @@ -24,7 +24,7 @@ public sealed record Source(string Origin, string Path, string Name) : IParsable public const char OriginSeparator = '$'; /// The name separator. - public const char NameSeparator = '.'; + public const char NameSeparator = '~'; /// /// Parses the specified input string into an instance of the type. diff --git a/Source/Sundew.Base.Identification/Target.cs b/Source/Sundew.Base.Identification/Target.cs index 362f103..974d576 100644 --- a/Source/Sundew.Base.Identification/Target.cs +++ b/Source/Sundew.Base.Identification/Target.cs @@ -21,7 +21,7 @@ namespace Sundew.Base.Identification; public sealed record Target(Source Source, Path? Path) : IParsable { /// - /// Parses the specified input string into an instance of the type. + /// 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. @@ -38,7 +38,7 @@ public static Target Parse(string inputTarget, IFormatProvider? formatProvider) } /// - /// Tries to parse the specified input string into an instance of the type. + /// 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. diff --git a/Source/Sundew.Base.Identification/TargetEvaluator.cs b/Source/Sundew.Base.Identification/TargetEvaluator.cs index ae33dbf..ca6a783 100644 --- a/Source/Sundew.Base.Identification/TargetEvaluator.cs +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -12,10 +12,9 @@ namespace Sundew.Base.Identification; using System.Globalization; using System.Linq; using System.Reflection; -using System.Runtime.CompilerServices; using System.Text; -using Sundew.Base.Collections; using Sundew.Base.Collections.Linq; +using Sundew.Base.Identification.Parsing; using Sundew.Base.Text; internal static class TargetEvaluator @@ -68,7 +67,7 @@ public static R GetResultType(Source source, Path? path) return R.Error(); } - public static R> GetInputTypes(Source source, Path? path, ValueId? valueId) + public static R> GetInputTypes(Source source, Path? path, IArguments? arguments) { var sourceType = source.TryGetType(); if (sourceType.IsError) @@ -76,9 +75,9 @@ public static R> GetInputTypes(Source source, Path? path, Va return R.Error(); } - if (valueId.HasValue) + if (arguments.HasValue) { - return valueId.TryGetType().Map(x => (IReadOnlyList)[x]); + return arguments.ToValueIds().Items.Select(x => x.TryGetType()).AllOrFailed(x => x.ToItem()).Map(x => (IReadOnlyList)x.Items); } if (!path.HasValue) @@ -163,9 +162,9 @@ internal static bool GetTypeName(Type type, StringBuilder stringBuilder) stringBuilder .Append(baseName) - .Append('[') - .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), ExpressionEvaluator.ArgumentSeparator) - .Append(']'); + .Append(Grammar.ArrayStart) + .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), Grammar.ArgumentSeparator) + .Append(Grammar.ArrayEnd); return false; } @@ -210,7 +209,7 @@ private static void BuildNestedName(Type type, StringBuilder stringBuilder) case Empty empty: break; case Multiple multiple: - var valueIds = segment.ValueId?.Value.ToValueIds() ?? new ValueIds([]); + var valueIds = segment.Arguments?.ToValueIds() ?? new ComplexValue([]); var methodInfo = multiple.Items.OfType() .Select(methodInfo => (methodInfo, parameters: methodInfo.GetParameters())) .Where(x => x.parameters.Length == valueIds.Items.Count) @@ -241,14 +240,14 @@ private static void BuildNestedName(Type type, StringBuilder stringBuilder) return memberInfo; } - private static bool IsMatch(ParameterInfo[] parameterInfos, ValueIds valueIds) + private static bool IsMatch(ParameterInfo[] parameterInfos, ComplexValue complexValue) { - if (!valueIds.HasValue) + if (!complexValue.HasValue) { return parameterInfos.Length == 0; } - return parameterInfos.Zip(valueIds.Items).All(x => + return parameterInfos.Zip(complexValue.Items).All(x => { var argumentType = x.Second.Metadata.HasValue ? Source.Parse(x.Second.Metadata, CultureInfo.InvariantCulture).TryGetType().Value @@ -257,16 +256,16 @@ private static bool IsMatch(ParameterInfo[] parameterInfos, ValueIds valueIds) }); } - private static Type? GetTypeFromArgument(Type firstParameterType, IValue secondValue) + 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, [secondValue.ToString(), CultureInfo.InvariantCulture])?.GetType(); + return parseMethod.Invoke(null, [value.ToString(), CultureInfo.InvariantCulture])?.GetType(); } parseMethod = firstParameterType.GetMethod(parseName, BindingFlags.Public | BindingFlags.Static, [typeof(string)]); - return parseMethod?.Invoke(null, [secondValue.ToString()])?.GetType(); + return parseMethod?.Invoke(null, [value.ToString()])?.GetType(); } } diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index 44c1d76..471ddbc 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -8,25 +8,19 @@ namespace Sundew.Base.Identification; using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text; +using Sundew.Base.Identification.Parsing; /// -/// Represents a value id for an argument. +/// Represents a value id for an argument. /// /// The name. /// The metadata. /// The value. public sealed partial record ValueId(string? Name, string? Metadata, IValue Value) { - /// Key Value separator. - public const char KeyValueSeparator = '='; - - /// Metadata separator. - public const char MetadataSeparator = '!'; - /// /// Gets the type of the source. /// @@ -42,28 +36,29 @@ public R TryGetType() } /// - /// Creates a string representation of the . + /// Creates a string representation of the . /// /// A string. public override string ToString() { var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true)); return stringBuilder.ToString(); } /// - /// Appends this to the specified . + /// Appends this to the specified . /// /// The string builder. /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) + /// The append options. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) { bool TryAppendMetadata() { if (!string.IsNullOrEmpty(this.Metadata)) { - stringBuilder.Append(MetadataSeparator); + stringBuilder.Append(Grammar.NameMetadataSeparator); stringBuilder.Append(this.Metadata); return true; } @@ -75,30 +70,31 @@ bool TryAppendMetadata() { stringBuilder.Append(this.Name); TryAppendMetadata(); - stringBuilder.Append(KeyValueSeparator); + stringBuilder.Append(Grammar.KeyValueSeparator); + + this.Value.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }); + + return; } - else + + if (TryAppendMetadata()) { - if (TryAppendMetadata()) - { - stringBuilder.Append(KeyValueSeparator); - } + stringBuilder.Append(Grammar.KeyValueSeparator); } - this.Value.AppendInto(stringBuilder, formatProvider); + this.Value.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }); } /// - /// Creates an from the specified builder func. + /// Creates an from the specified builder func. /// /// The type of the value. /// The value. /// The value id func. - /// Indicated whether this is a root id. - /// A new . - public static ValueId From(TValue value, Action valueIdFunc, bool isRoot) + /// A new . + public static ValueId From(TValue value, Action valueIdFunc) { - var valueIdBuilder = new ValueIdBuilder(value?.GetType() ?? typeof(TValue), isRoot); + var valueIdBuilder = new ValueIdBuilder(value?.GetType() ?? typeof(TValue)); valueIdFunc(value, valueIdBuilder); return valueIdBuilder.Build(); } @@ -138,30 +134,30 @@ public static bool TryParse([NotNullWhen(true)] string? inputArg, IFormatProvide while (index < inputArg.Length) { var character = inputArg[index++]; - if (character == ValueIds.GroupStartSeparator) + if (character == Grammar.GroupStart) { level++; } - if (character == ValueIds.GroupEndSeparator) + if (character == Grammar.GroupEnd) { level--; } - if (character == MetadataSeparator && level == 0) + if (character == Grammar.NameMetadataSeparator && level == 0) { metadataIndex = index; } - if (character == KeyValueSeparator && level == 0) + if (character == Grammar.KeyValueSeparator && level == 0) { var (nameLength, metadataStart, metadataLength) = metadataIndex > -1 ? (metadataIndex - 1, metadataIndex, index - metadataIndex - 1) : (index - 1, 0, 0); - result = new ValueId(inputArg.Substring(0, nameLength), inputArg.Substring(metadataStart, metadataLength), new SingleValue(inputArg.Substring(index))); + result = new ValueId(inputArg.Substring(0, nameLength), inputArg.Substring(metadataStart, metadataLength), new ScalarValue(inputArg.Substring(index))); return true; } } - result = new ValueId(null, null, new SingleValue(inputArg)); + result = new ValueId(null, null, new ScalarValue(inputArg)); return true; } diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs index 54f1072..362b7df 100644 --- a/Source/Sundew.Base.Identification/ValueIdBuilder.cs +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs @@ -9,17 +9,15 @@ namespace Sundew.Base.Identification; using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; using Sundew.Base.Collections.Immutable; using Sundew.Base.Collections.Linq; /// -/// Builder for constructing for dynamic construction of identifiers. +/// Builder for constructing for dynamic construction of identifiers. /// /// The . -/// Indicates whether the builder is for a root identifier. -public sealed class ValueIdBuilder(Type type, bool isRoot) +public sealed class ValueIdBuilder(Type type) { private readonly List values = new(); @@ -48,7 +46,7 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo if (value != null && value is IValueIdentifiable valueIdentifiable) { - var valueId = valueIdentifiable.GetValueId(false); + var valueId = valueIdentifiable.Id; this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), valueId.Value)); } else if (value != null) @@ -56,7 +54,7 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo var stringValue = value.ToString(); if (stringValue.HasValue) { - this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), new SingleValue(stringValue))); + this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), new ScalarValue(stringValue))); } } @@ -64,17 +62,17 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo } /// - /// 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 . + /// 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 . + /// A new . public ValueId Build() { var cardinality = this.values.ByCardinality(); - var metadata = isRoot ? Source.FromType(type).ToString() : null; + var metadata = Source.FromType(type).ToString(); return cardinality switch { - Empty empty => new ValueId(null, metadata, new SingleValue("null")), - Multiple valueIds => new ValueId(null, metadata, new ValueIds(this.values.ToValueArray())), + Empty empty => new ValueId(null, metadata, new ScalarValue("null")), + Multiple valueIds => new ValueId(null, metadata, new ComplexValue(valueIds.Items.ToValueArray())), Single single => single.Item, }; } diff --git a/Source/Sundew.Base.Parsing/ILexer.cs b/Source/Sundew.Base.Parsing/ILexer.cs index 3b7429e..28b103b 100644 --- a/Source/Sundew.Base.Parsing/ILexer.cs +++ b/Source/Sundew.Base.Parsing/ILexer.cs @@ -12,6 +12,7 @@ namespace Sundew.Base.Parsing; /// /// 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. diff --git a/Source/Sundew.Base.Parsing/ILexerRule.cs b/Source/Sundew.Base.Parsing/ILexerRule.cs index d267b48..0931278 100644 --- a/Source/Sundew.Base.Parsing/ILexerRule.cs +++ b/Source/Sundew.Base.Parsing/ILexerRule.cs @@ -13,6 +13,7 @@ namespace Sundew.Base.Parsing; /// /// The type of token produced by this lexer rule. public interface ILexerRule + where TToken : notnull { /// /// Gets the token associated with this instance. diff --git a/Source/Sundew.Base.Parsing/LexerError.cs b/Source/Sundew.Base.Parsing/LexerError.cs index 3e12587..1f8acac 100644 --- a/Source/Sundew.Base.Parsing/LexerError.cs +++ b/Source/Sundew.Base.Parsing/LexerError.cs @@ -7,10 +7,14 @@ namespace Sundew.Base.Parsing; +using System.Collections.Generic; +using Sundew.DiscriminatedUnions; + /// /// Represents a lexer error. /// -public readonly record struct LexerError(string Input, int Position, int Length) +[DiscriminatedUnion] +public abstract partial record LexerError { /// /// Gets the message. @@ -18,8 +22,81 @@ public readonly record struct LexerError(string Input, int Position, int Length) /// /// The error message. /// - public string GetMessage() + 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 { - return $"Invalid input: {this.Input} at position: {this.Position}"; + /// + /// 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/Parser.cs b/Source/Sundew.Base.Parsing/Parser.cs index f17458f..9a4e314 100644 --- a/Source/Sundew.Base.Parsing/Parser.cs +++ b/Source/Sundew.Base.Parsing/Parser.cs @@ -8,15 +8,18 @@ namespace Sundew.Base.Parsing; using System; +using System.Collections.Generic; +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. public class Parser + where TToken : notnull { private readonly ILexer lexer; - private readonly string input; + private readonly Stack stateStack = new Stack(); private State state; /// @@ -24,13 +27,25 @@ public class Parser /// /// 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. - public Parser(ILexer lexer, string input) + /// The format provider. + public Parser(ILexer lexer, string input, IFormatProvider? formatProvider) { this.lexer = lexer; - this.input = input; + this.Input = input; + this.FormatProvider = formatProvider ?? CultureInfo.CurrentCulture; this.state = 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; } + /// /// Determines whether the specified token is accepted at the current position and retrieves the corresponding /// lexeme if accepted. @@ -41,7 +56,7 @@ public Parser(ILexer lexer, string input) /// true if the token is accepted at the current position; otherwise, false. public bool Accept(TToken token, out string lexeme) { - if (this.lexer.TryGetLexeme(token, this.input, this.state, out lexeme, out var consumedLength)) + if (this.lexer.TryGetLexeme(token, this.Input, this.state, out lexeme, out var consumedLength)) { this.state = new State(this.state.Position + consumedLength); return true; @@ -51,6 +66,79 @@ public bool Accept(TToken token, out string lexeme) 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) + { + if (this.lexer.TryGetLexeme(token, this.Input, this.state, out var lexeme, out var consumedLength)) + { + this.state = new State(this.state.Position + consumedLength); + return R.Success(lexeme); + } + + return R.Error(LexerError._TokenTypeError(token, this.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 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 stateBefore = this.state; + if (this.lexer.TryGetLexeme(token, this.Input, this.state, out var lexeme, out var consumedLength)) + { + this.state = new State(this.state.Position + consumedLength); + var result = matchNextFunc(R.Success(lexeme)); + if (result.IsSuccess) + { + return result; + } + + this.state = stateBefore; + return R.Error(result.Error); + } + + return matchNextFunc(R.Error(LexerError._TokenTypeError(token, this.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 stateBefore = this.state; + if (this.IsNext(input)) + { + this.state = new State(this.state.Position + 1); + var result = matchNextFunc(R.Success(input.ToString())); + if (result.IsSuccess) + { + return result; + } + + this.state = stateBefore; + return R.Error(result.Error); + } + + return matchNextFunc(R.Error(LexerError._TokenError(input.ToString(), this.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. @@ -59,15 +147,34 @@ public bool Accept(TToken token, out string lexeme) /// 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 bool Accept(char input) + public RoE TryAccept(char input) { if (this.IsNext(input)) { this.state = new State(this.state.Position + 1); - return true; + return R.Success(); } - return false; + return R.Error(LexerError._TokenError(input.ToString(), this.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) + { + if (this.IsNext(input)) + { + this.state = new State(this.state.Position + 1); + return R.Success(); + } + + return R.Error(LexerError._TokenError(input.ToString(), this.state.Position, 1)); } /// @@ -94,7 +201,7 @@ public bool Accept(string input) /// true if the specified character matches the current character in the input; otherwise, false. public bool IsNext(char input) { - if (this.input[this.state.Position] == input) + if (this.Input.Length > this.state.Position && this.Input[this.state.Position] == input) { return true; } @@ -110,7 +217,8 @@ public bool IsNext(char input) /// true if the specified string matches the input sequence at the current position; otherwise, false. public bool IsNext(string input) { - if (this.input.AsSpan(this.state.Position, input.Length).SequenceEqual(input.AsSpan())) + if (this.Input.Length > this.state.Position + input.Length - 1 && + this.Input.AsSpan(this.state.Position, input.Length).SequenceEqual(input.AsSpan())) { return true; } @@ -122,9 +230,9 @@ public bool IsNext(string input) /// 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 bool AcceptEnd() + public RoE IsEnd() { - return this.state.Position == this.input.Length; + return R.FromError(this.state.Position == this.Input.Length, () => LexerError._End(this.state.Position)); } /// diff --git a/Source/Sundew.Base.Parsing/RegexLexerRule.cs b/Source/Sundew.Base.Parsing/RegexLexerRule.cs index 43eff2f..a6c3f35 100644 --- a/Source/Sundew.Base.Parsing/RegexLexerRule.cs +++ b/Source/Sundew.Base.Parsing/RegexLexerRule.cs @@ -17,6 +17,7 @@ namespace Sundew.Base.Parsing; /// 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"; @@ -45,12 +46,12 @@ public class RegexLexerRule(TToken token, Regex regex) : ILexerRule /// 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..a729b3f --- /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>(true, 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.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 _); From fddf54ff113591b8fd2cb23c99f1744ceb1cc364 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Fri, 10 Apr 2026 05:10:56 +0200 Subject: [PATCH 05/20] Fixed merge conflicts --- .../Identification/AIdTests.cs | 190 --------------- Source/Sundew.Base.Identification/AId.cs | 160 ------------- Source/Sundew.Base.Identification/Argument.cs | 128 ----------- .../Sundew.Base.Identification/Arguments.cs | 217 ------------------ 4 files changed, 695 deletions(-) delete mode 100644 Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs delete mode 100644 Source/Sundew.Base.Identification/AId.cs delete mode 100644 Source/Sundew.Base.Identification/Argument.cs delete mode 100644 Source/Sundew.Base.Identification/Arguments.cs diff --git a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs deleted file mode 100644 index 05ac7d4..0000000 --- a/Source/Sundew.Base.Development.Tests/Identification/AIdTests.cs +++ /dev/null @@ -1,190 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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; -using static Sundew.Base.Development.Tests.Identification.AIdTests; - -public class AIdTests -{ - [Test] - [Obsolete("Obsolete")] - public void T() - { - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); - var t1 = Uri.EscapeUriString(uri6!.OriginalString); - var t2 = Uri.EscapeUriString(uri8!.OriginalString); - var t3 = Uri.EscapeUriString(uri9!.OriginalString); - } - - [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,Description)?person=(Address=Home,Number=15)&description=(Eyes=Blue)")] - [Arguments("Name+Nested.Name.Space$Assembly/Find(Person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]")] - public void Parse_Then_ResultShouldNotBeNull(string input) - { - var result = AId.Parse(input, CultureInfo.InvariantCulture); - - using (new AssertionScope()) - { - result.Should().NotBeNull(); - result.ToString().Should().Be(input); - } - } - - [Test] - public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() - { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; - var result = AId.From(x => x.GoBack()); - - using (new AssertionScope()) - { - result.Should().NotBeNull(); - 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_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() - { - const string expectedResult = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position)"; - var result = AId.From(x => x.Navigate(null!)); - - using (new AssertionScope()) - { - result.Should().NotBeNull(); - 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 = "AIdTests+INavigator.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Navigate(AIdTests+Position,bool)"; - var result = AId.From(x => x.Navigate(null!, default)); - - using (new AssertionScope()) - { - result.Should().NotBeNull(); - 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 = "AIdTests+Position.Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; - var result = AId.From(x => x.X); - - using (new AssertionScope()) - { - result.Should().NotBeNull(); - 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 AsArguments_Then_ResultShouldBeExpectedResult() - { - const string expectedResult = "X=4&Y=5"; - var position = new Position(4, 5); - - var arguments = position.AsArguments(); - var result = Position.From(new Position(0, 0), arguments); - - using (new AssertionScope()) - { - arguments.ToString().Should().Be(expectedResult); - result.Should().Be(position); - } - } - - [Test] - public void AsArguments_Then_ResultShouldBeExpectedResult2() - { - const string expectedResult = "Position=(X=4&Y=5)&Z=6"; - var position = new Position3D(new Position(4, 5), 6); - - var arguments = position.AsArguments(); - var result = Position3D.From(new Position3D(new Position(0, 0), 0), arguments); - - using (new AssertionScope()) - { - arguments.ToString().Should().Be(expectedResult); - result.Should().Be(position); - } - } - -#pragma warning disable SA1201 - public interface INavigator -#pragma warning restore SA1201 - { - void GoBack(); - - void Navigate(Position position); - - void Navigate(Position position, bool addToHistory); - } - - public record Position(int X, int Y) : IValueIdentifiable - { - public Arguments AsArguments() => Arguments.From(builder => builder.Add(this.X).Add(this.Y)); - - public static Position From(Position position, Arguments arguments) - { - return new Position( - arguments.Get(position.X, CultureInfo.InvariantCulture), - arguments.Get(position.Y, CultureInfo.InvariantCulture)); - } - } - - public record Position3D(Position Position, int Z) : IValueIdentifiable - { - public Arguments AsArguments() - { - return Arguments.From(builder => builder.Add(this.Position).Add(this.Z)); - } - - public static Position3D From(Position3D value, Arguments arguments) - { - return new Position3D( - arguments.Get2(value.Position, CultureInfo.InvariantCulture), - arguments.Get(value.Z, CultureInfo.InvariantCulture)); - } - } -} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/AId.cs b/Source/Sundew.Base.Identification/AId.cs deleted file mode 100644 index 7133a02..0000000 --- a/Source/Sundew.Base.Identification/AId.cs +++ /dev/null @@ -1,160 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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; - -/// -/// Represents any Id. -/// -public record AId(Target Target, Arguments? Arguments) : IParsable -{ - /// The arguments separator. - public const char ArgumentsSeparator = '?'; - - /// - /// 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 Argument 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 AId Parse(string inputAId, IFormatProvider? provider) - { - if (TryParse(inputAId, provider, out var result)) - { - return result; - } - - throw new FormatException($"The string: {inputAId} is not a valid {nameof(AId)}"); - } - - /// - /// 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? inputAId, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out AId result) - { - if (inputAId.HasValue) - { - var argumentsSeparatorIndex = inputAId.IndexOf(ArgumentsSeparator); - if (argumentsSeparatorIndex > -1) - { - var targetString = inputAId.Substring(0, argumentsSeparatorIndex); - var argumentsString = inputAId.Substring(argumentsSeparatorIndex + 1); - if (Target.TryParse(targetString, formatProvider, out var target) && Identification.Arguments.TryParse(argumentsString, formatProvider, out var args)) - { - result = new AId(target, args); - return true; - } - } - else if (Target.TryParse(inputAId, formatProvider, out var target)) - { - result = new AId(target, 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.Target.AppendInto(stringBuilder, formatProvider); - if (this.Arguments.HasValue) - { - stringBuilder.Append(ArgumentsSeparator); - this.Arguments.Value.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.Target.TryGetSourceType(); - } - - /// - /// Tries to get the result type. - /// - /// A result containing the result type if successful. - public R TryGetResultType() - { - return this.Target.TryGetResultType(); - } - - /// - /// Tries to get the input types. - /// - /// A result containing the input types if successful. - public R> TryGetInputTypes() - { - return this.Target.TryGetInputTypes(); - } - - /// - /// Tries to get the target containing type. - /// - /// A result containing the containing type if successful. - public R TryGetTargetContainingType() - { - return this.Target.TryGetContainingType(); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// A new . - public static AId From(Expression> targetExpression) - { - var target = Target.From(targetExpression); - return new AId(target, null); - } - - /// - /// Gets an from the specified source and expression. - /// - /// The source type. - /// The target expression. - /// A new . - public static AId From(Expression> targetExpression) - { - var target = Target.From(targetExpression); - return new AId(target, null); - } -} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Argument.cs b/Source/Sundew.Base.Identification/Argument.cs deleted file mode 100644 index 90f5638..0000000 --- a/Source/Sundew.Base.Identification/Argument.cs +++ /dev/null @@ -1,128 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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 an argument in a . -/// -/// The name. -/// The value. -public record Argument(string? Name, string Value) : IParsable -{ - /// Key Value separator. - public const char KeyValueSeparator = '='; - - /// - /// 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 Argument 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 Argument 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(Argument)}."); - } - - /// - /// 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 Argument result) - { - if (inputArg.HasValue) - { - string key = string.Empty; - var level = 0; - var index = 0; - while (index < inputArg.Length) - { - var character = inputArg[index++]; - if (character == Arguments.GroupStartSeparator) - { - level++; - } - - if (character == Arguments.GroupEndSeparator) - { - level--; - } - - if (character == KeyValueSeparator && level == 0) - { - result = new Argument(inputArg.Substring(0, index - 1), inputArg.Substring(index)); - return true; - } - } - - result = new Argument(null, inputArg); - return true; - } - - result = null; - return false; - } - - /// - /// Appends this to the specified . - /// - /// The string builder. - /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) - { - if (!string.IsNullOrEmpty(this.Name)) - { - stringBuilder.Append(this.Name).Append(KeyValueSeparator); - } - - stringBuilder.Append(this.Value); - } - - /// - /// Creates a string representation of the . - /// - /// A string. - public override string ToString() - { - var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture); - return stringBuilder.ToString(); - } - - /// - /// Attempts to parse the current value into an instance of the Arguments class. - /// - /// A result containing the parsed Arguments if successful. - public R TryGetValueArguments() - { - return this.TryGetValueArguments(CultureInfo.CurrentCulture); - } - - /// - /// Attempts to parse the current value into an instance of the Arguments class. - /// - /// The format provider. - /// A result containing the parsed Arguments if successful. - public R TryGetValueArguments(IFormatProvider formatProvider) - { - return R.From(Arguments.TryParse(this.Value, formatProvider, out var args), args); - } -} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Arguments.cs b/Source/Sundew.Base.Identification/Arguments.cs deleted file mode 100644 index 038fe89..0000000 --- a/Source/Sundew.Base.Identification/Arguments.cs +++ /dev/null @@ -1,217 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using Sundew.Base.Collections.Immutable; -using Sundew.Base.Text; - -#pragma warning disable CS8907, CS1591 -/// -/// Represents arguments for an . -/// -/// The arguments. -public readonly record struct Arguments(ValueArray Items) : IParsable -{ - /// The argument separator. - public const char ArgumentsSeparator = '&'; - - /// The group start separator. - public const char GroupStartSeparator = '('; - - /// The group end separator. - public const char GroupEndSeparator = ')'; - - /// - /// 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 Argument 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 Arguments Parse(string inputArgs, IFormatProvider? formatProvider) - { - if (TryParse(inputArgs, formatProvider, out var result)) - { - return result; - } - - throw new FormatException($"The string: {inputArgs} is not a valid {nameof(Arguments)}."); - } - - /// - /// 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? inputArguments, IFormatProvider? formatProvider, out Arguments result) - { - var args = ImmutableArray.CreateBuilder(); - if (inputArguments.HasValue) - { - var argStartIndex = 0; - var index = 0; - var level = 0; - while (index < inputArguments.Length) - { - var character = inputArguments[index++]; - if (character == GroupStartSeparator) - { - level++; - } - else if (character == GroupEndSeparator) - { - level--; - } - else if (character == ArgumentsSeparator && level == 0) - { - if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex - 1), formatProvider, out var arg)) - { - args.Add(arg); - argStartIndex = index; - } - else - { - result = default; - return false; - } - } - } - - if (Argument.TryParse(inputArguments.Substring(argStartIndex, index - argStartIndex), formatProvider, out var arg2)) - { - args.Add(arg2); - } - - result = new Arguments(args.ToImmutable()); - return true; - } - - result = default; - return false; - } - - /// - /// Appends this to the specified . - /// - /// The string builder. - /// The format provider. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider) - { - stringBuilder.AppendItems(this.Items, (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider), Arguments.ArgumentsSeparator); - } - - /// - /// 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 from the specified builder func. - /// - /// The value id func. - /// A new . - public static Arguments From(Action valueIdFunc) - { - var valueIdBuilder = new ValueIdBuilder(); - valueIdFunc(valueIdBuilder); - return valueIdBuilder.Build(); - } - - /// - /// 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 Get(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - return TValue.Parse(argument.Value, formatProvider); - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - return TValue.Parse(argument.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 Get2(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - var innerArguments = argument.TryGetValueArguments(); - if (innerArguments.IsSuccess) - { - return TValue.From(defaultValue, innerArguments.Value); - } - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - var innerArguments = argument.TryGetValueArguments(); - if (innerArguments.IsSuccess) - { - return TValue.From(defaultValue, innerArguments.Value); - } - } - - return defaultValue; - } -} \ No newline at end of file From 195492c8c4505cb2e3d2bd68237cb638eaef8a7c Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Tue, 14 Apr 2026 04:50:59 +0200 Subject: [PATCH 06/20] Implemented serialization and deserialization for Ids and ValueIds --- .../ValueArray{TItem}.cs | 5 + .../ValueDictionary{TKey,TValue}.cs | 5 + .../ValueList{TItem}.cs | 5 + .../Collections/ValueArrayTests.cs | 9 + .../Collections/ValueDictionaryTests.cs | 9 + .../Collections/ValueListTests.cs | 9 + .../Identification/IdTests.cs | 68 +++--- .../AppendOptions.cs | 2 +- Source/Sundew.Base.Identification/Argument.cs | 35 +++ .../Sundew.Base.Identification/Arguments.cs | 32 +++ .../Sundew.Base.Identification/ArrayValue.cs | 78 +------ .../ComplexValue.cs | 82 +------- .../ExpressionEvaluator.cs | 28 +-- .../Sundew.Base.Identification/IArguments.cs | 42 ---- .../IParserError.cs | 27 ++- Source/Sundew.Base.Identification/IValue.cs | 32 +-- .../IValueIdentifiable.cs | 7 +- Source/Sundew.Base.Identification/Id.cs | 10 +- .../Parsing/Grammar.cs | 8 +- .../Parsing/IdRouteParser.cs | 199 ++++++++---------- .../Parsing/Tokens.cs | 2 +- Source/Sundew.Base.Identification/Path.cs | 2 +- .../Sundew.Base.Identification/ScalarValue.cs | 30 +-- Source/Sundew.Base.Identification/Segment.cs | 2 +- .../TargetEvaluator.cs | 23 +- .../ValueEscaper.cs | 17 ++ Source/Sundew.Base.Identification/ValueId.cs | 157 +++++++++----- .../ValueIdBuilder.cs | 12 +- Source/Sundew.Base.Parsing/Parser.cs | 112 ++++++---- Source/Sundew.Base.Parsing/ParserDebugView.cs | 24 +++ 30 files changed, 545 insertions(+), 528 deletions(-) create mode 100644 Source/Sundew.Base.Identification/Argument.cs create mode 100644 Source/Sundew.Base.Identification/Arguments.cs delete mode 100644 Source/Sundew.Base.Identification/IArguments.cs create mode 100644 Source/Sundew.Base.Identification/ValueEscaper.cs create mode 100644 Source/Sundew.Base.Parsing/ParserDebugView.cs diff --git a/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs b/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs index f9e3fc4..3110842 100644 --- a/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs +++ b/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs @@ -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..ae70ba3 100644 --- a/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs +++ b/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs @@ -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..8faf200 100644 --- a/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs +++ b/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs @@ -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.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 index 0c9eecb..55f91af 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -9,6 +9,7 @@ namespace Sundew.Base.Development.Tests.Identification; using System; using System.Globalization; +using System.Linq; using AwesomeAssertions; using AwesomeAssertions.Execution; using Sundew.Base.Identification; @@ -49,31 +50,34 @@ public void T() [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("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 (new AssertionScope()) + using (var scope = new AssertionScope()) { + scope.FormattingOptions.MaxDepth = 20; result.Should().NotBeNull(); result.ToString().Should().Be(input); } } [Test] - public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() + 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 (new AssertionScope()) + using (var scope = new AssertionScope()) { - result.Should().Be(Id.Parse(result.ToString(), CultureInfo.InvariantCulture)); + 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)); @@ -82,13 +86,14 @@ public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldNotBeNull() } [Test] - public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() + 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 (new AssertionScope()) + 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)]); @@ -98,13 +103,14 @@ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull() } [Test] - public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldNotBeNull2() + 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 (new AssertionScope()) + 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)]); @@ -119,8 +125,9 @@ 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 (new AssertionScope()) + 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)]); @@ -135,8 +142,9 @@ 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 (new AssertionScope()) + 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)]); @@ -146,7 +154,7 @@ public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() } [Test] - public void AsArguments_Then_ResultShouldBeExpectedResult() + 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); @@ -154,15 +162,16 @@ public void AsArguments_Then_ResultShouldBeExpectedResult() var valueId = position.Id; var result = valueId.ToValue(new Position(0, 0)); - using (new AssertionScope()) + using (var scope = new AssertionScope()) { + scope.FormattingOptions.MaxDepth = 20; valueId.ToString().Should().Be(expectedResult); result.Should().Be(position); } } [Test] - public void AsArguments_Then_ResultShouldBeExpectedResult2() + 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); @@ -170,21 +179,23 @@ public void AsArguments_Then_ResultShouldBeExpectedResult2() var valueId = position.Id; var result = valueId.ToValue(new Position3D(new Position(0, 0), 0)); - using (new AssertionScope()) + using (var scope = new AssertionScope()) { + scope.FormattingOptions.MaxDepth = 20; valueId.ToString().Should().Be(expectedResult); result.Should().Be(position); } } [Test] - public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull2() + 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 (new AssertionScope()) + 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)]); @@ -194,7 +205,7 @@ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull2( } [Test] - public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull3() + 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)); @@ -202,7 +213,8 @@ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull3( using (var scope = new AssertionScope()) { scope.FormattingOptions.MaxDepth = 20; - Id.Parse(result.ToString(), CultureInfo.InvariantCulture).Should().Be(result); + 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)); @@ -232,11 +244,11 @@ 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) + public static Position From(Position position, ValueId valueId, IFormatProvider? formatProvider) { return new Position( - valueId.Value.Get(position.X, CultureInfo.InvariantCulture), - valueId.Value.Get(position.Y, CultureInfo.InvariantCulture)); + valueId.GetScalar(position.X, CultureInfo.InvariantCulture), + valueId.GetScalar(position.Y, CultureInfo.InvariantCulture)); } } @@ -244,11 +256,11 @@ public record Position3D(Position Position, int Z) : IValueIdentifiable ValueId.From(this, (value, builder) => builder.Add(value.Position).Add(value.Z)); - public static Position3D From(Position3D value, ValueId valueId) + public static Position3D From(Position3D value, ValueId valueId, IFormatProvider? formatProvider) { return new Position3D( - valueId.Value.Get2(value.Position, CultureInfo.InvariantCulture), - valueId.Value.Get(value.Z, CultureInfo.InvariantCulture)); + valueId.GetValue(value.Position, CultureInfo.InvariantCulture), + valueId.GetScalar(value.Z, CultureInfo.InvariantCulture)); } } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/AppendOptions.cs b/Source/Sundew.Base.Identification/AppendOptions.cs index 25afe6c..c17b6da 100644 --- a/Source/Sundew.Base.Identification/AppendOptions.cs +++ b/Source/Sundew.Base.Identification/AppendOptions.cs @@ -11,4 +11,4 @@ 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 sealed record AppendOptions(bool IsRoot); \ No newline at end of file +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..f9d2c72 --- /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 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 index ee0ab70..53a4f7e 100644 --- a/Source/Sundew.Base.Identification/ArrayValue.cs +++ b/Source/Sundew.Base.Identification/ArrayValue.cs @@ -9,8 +9,6 @@ namespace Sundew.Base.Identification; using System; using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; using System.Text; using Sundew.Base.Collections.Immutable; using Sundew.Base.Identification.Parsing; @@ -30,10 +28,12 @@ public sealed partial record ArrayValue(ValueArray Items) : IValue /// The append options. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) { + stringBuilder.Append(Grammar.ArrayStart); stringBuilder.AppendItems( this.Items, - (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }), - Grammar.ValueIdsSeparator); + (stringBuilder, valueId) => valueId.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }, false), + Grammar.ArrayElementSeparator); + stringBuilder.Append(Grammar.ArrayEnd); } /// @@ -46,74 +46,4 @@ public override string ToString() this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true)); return stringBuilder.ToString(); } - - /// - /// 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 Get(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - return TValue.Parse(argument.Value.ToString() ?? string.Empty, formatProvider); - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - return TValue.Parse(argument.Value.ToString() ?? string.Empty, 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 Get2(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - return TValue.From(defaultValue, argument); - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - return TValue.From(defaultValue, argument); - } - - return defaultValue; - } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ComplexValue.cs b/Source/Sundew.Base.Identification/ComplexValue.cs index d54396b..7f594cd 100644 --- a/Source/Sundew.Base.Identification/ComplexValue.cs +++ b/Source/Sundew.Base.Identification/ComplexValue.cs @@ -8,11 +8,7 @@ namespace Sundew.Base.Identification; using System; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Runtime.CompilerServices; using System.Text; using Sundew.Base.Collections.Immutable; using Sundew.Base.Identification.Parsing; @@ -22,7 +18,7 @@ namespace Sundew.Base.Identification; /// Represents arguments for an . /// /// The value ids. -public sealed partial record ComplexValue(ValueArray Items) : IValue +public sealed partial record ComplexValue(ValueArray Items) : IValue { /// /// Appends this to the specified . @@ -34,10 +30,10 @@ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvid { stringBuilder.AppendItems( this.Items, - (builder) => builder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupStart)), + (stringBuilder) => stringBuilder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupStart)), (stringBuilder, arg) => arg.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }), - (builder) => builder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupEnd)), - Grammar.ValueIdsSeparator); + (stringBuilder) => stringBuilder.If(!appendOptions.IsRoot, builder => builder.Append(Grammar.GroupEnd)), + Grammar.ArgumentSeparator); } /// @@ -50,74 +46,4 @@ public override string ToString() this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true)); return stringBuilder.ToString(); } - - /// - /// 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 Get(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - return TValue.Parse(argument.Value.ToString() ?? string.Empty, formatProvider); - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - return TValue.Parse(argument.Value.ToString() ?? string.Empty, 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 Get2(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."); - } - - var argument = this.Items.FirstOrDefault(x => x.Name == referenceName); - if (argument.HasValue) - { - return TValue.From(defaultValue, argument); - } - - var firstDotIndex = referenceName.IndexOf('.'); - var fallback = firstDotIndex > -1 - ? referenceName.Substring(firstDotIndex + 1, referenceName.Length - firstDotIndex - 1) - : null; - argument = this.Items.FirstOrDefault(x => x.Name == fallback); - if (argument.HasValue) - { - return TValue.From(defaultValue, argument); - } - - return defaultValue; - } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs index 78b8296..a8f930c 100644 --- a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs +++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs @@ -25,16 +25,16 @@ internal static class ExpressionEvaluator /// The path expression. /// The value. /// A new . - public static (Source Source, Path Path, ValueId? ValueId) From(LambdaExpression pathExpression, IIdentifiable? value = null) + public static (Source Source, Path Path, Arguments? Arguments) From(LambdaExpression pathExpression, IIdentifiable? value = null) { var valueId = value?.Id; var isUsed = false; var segments = ImmutableArray.CreateBuilder(); var source = Source.FromType(pathExpression.Parameters.First().Type); - var valueIds = ImmutableArray.CreateBuilder(); + var valueIds = ImmutableArray.CreateBuilder(); EvaluateToPath(pathExpression); - return (source, new Path(segments.ToImmutable()), isUsed ? null : valueId); + 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) { @@ -49,13 +49,13 @@ void EvaluateToPath(Expression expression) EvaluateToPath(methodCallExpression.Object); } - valueIds = ImmutableArray.CreateBuilder(); + 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(valueId with { Name = argument.Second.Name + valueId.Name }); + valueIds.Add(new Argument(argument.Second.Name, valueId)); isUsed = true; } else @@ -64,7 +64,7 @@ void EvaluateToPath(Expression expression) } } - segments.Add(new Segment(methodCallExpression.Method.Name, new ComplexValue(valueIds.ToValueArray()))); + segments.Add(new Segment(methodCallExpression.Method.Name, new Arguments(valueIds.ToValueArray()))); break; case MemberExpression memberExpression: @@ -82,12 +82,12 @@ void EvaluateToPath(Expression expression) } } - private static void GetArgument(Expression argument, ParameterInfo parameterInfo, ImmutableArray.Builder builder) + private static void GetArgument(Expression argument, ParameterInfo parameterInfo, ImmutableArray.Builder builder) { switch (argument) { case ConstantExpression constantExpression: - builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new ScalarValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default")))); + builder.Add(new Argument(parameterInfo.Name, new ValueId(GetMetadata(argument.Type), new ScalarValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default"))))); break; case MemberExpression memberExpression: if (memberExpression.Expression is ConstantExpression constantExpression2) @@ -96,19 +96,19 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo if (memberExpression.Member is FieldInfo fieldInfo) { var value = fieldInfo.GetValue(container); - builder.Add(new ValueId(null, null, new ScalarValue(value?.ToString() ?? string.Empty))); + builder.Add(new Argument(fieldInfo.Name, new ValueId(GetMetadata(fieldInfo.FieldType), new ScalarValue(value?.ToString() ?? string.Empty)))); } if (memberExpression.Member is PropertyInfo propertyInfo) { var value = propertyInfo.GetValue(container); - builder.Add(new ValueId(null, null, new ScalarValue(value?.ToString() ?? string.Empty))); + builder.Add(new Argument(propertyInfo.Name, new ValueId(GetMetadata(propertyInfo.PropertyType), new ScalarValue(value?.ToString() ?? string.Empty)))); } } break; case NewExpression newExpression: - ImmutableArray.Builder newBuilder = ImmutableArray.CreateBuilder(); + ImmutableArray.Builder newBuilder = ImmutableArray.CreateBuilder(); if (newExpression.Constructor.HasValue) { foreach (var valueTuple in newExpression.Arguments.Zip(newExpression.Constructor.GetParameters())) @@ -116,15 +116,15 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo GetArgument(valueTuple.First, valueTuple.Second, newBuilder); } - builder.Add(new ValueId(parameterInfo.Name, GetMetadata(argument), new ComplexValue(newBuilder.ToImmutable()))); + builder.Add(new Argument(parameterInfo.Name, new ValueId(GetMetadata(argument.Type), new ComplexValue(newBuilder.ToImmutable())))); } break; } } - private static string? GetMetadata(Expression argument) + private static string? GetMetadata(Type argumentType) { - return TargetEvaluator.IsKnownType(argument.Type) ? null : Source.FromType(argument.Type).ToString(); + return TargetEvaluator.IsKnownType(argumentType) ? null : Source.FromType(argumentType).ToString(); } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/IArguments.cs b/Source/Sundew.Base.Identification/IArguments.cs deleted file mode 100644 index 1c0de3e..0000000 --- a/Source/Sundew.Base.Identification/IArguments.cs +++ /dev/null @@ -1,42 +0,0 @@ -// -------------------------------------------------------------------------------------------------------------------- -// -// 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 IArguments -{ - /// - /// Appends this to the specified . - /// - /// The string builder. - /// The format provider. - /// The append options. - void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions); - - /// - /// Converts the current instance to a collection of value identifiers. - /// - /// A object representing the value identifiers, or null if the instance does not correspond - /// to any value identifiers. - ComplexValue ToValueIds() - { - return this switch - { - ArrayValue arrayValue => new ComplexValue([new ValueId(null, null, arrayValue)]), - ScalarValue singleValue => new ComplexValue([new ValueId(null, null, singleValue)]), - ComplexValue valueIds => valueIds, - }; - } -} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/IParserError.cs b/Source/Sundew.Base.Identification/IParserError.cs index 4183c7c..046f652 100644 --- a/Source/Sundew.Base.Identification/IParserError.cs +++ b/Source/Sundew.Base.Identification/IParserError.cs @@ -70,6 +70,8 @@ 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; } @@ -77,11 +79,16 @@ public sealed partial record GroupValueIdError(object Cause, LexerError? Error) /// Union for errors. /// [DiscriminatedUnion] -public partial interface IValueIdError : IParserError +public partial interface IParseValueIdError : IParserError { - public sealed partial record ValueIdError(object Cause, LexerError? Error) : IValueIdError; +} - public sealed partial record ValueIdValueError(IValueError Error) : IValueIdError; +/// +/// Union for errors. +/// +[DiscriminatedUnion] +public partial interface IValueIdError : IParserError +{ } /// @@ -99,14 +106,24 @@ 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; +public sealed partial record NotAtEndError() : IIdError, IIdRouteError, IParseValueIdError; /// /// Represents an error when an Id is empty or null. /// -public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError; +public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError, IParseValueIdError; +/// +/// Represents a lexer error. +/// +/// The cause. +/// The lexer error. +public sealed partial record LexError(object Cause, LexerError LexerError) : IArgumentsError, IValueIdError; #pragma warning restore SA1402 diff --git a/Source/Sundew.Base.Identification/IValue.cs b/Source/Sundew.Base.Identification/IValue.cs index 91d4b6d..f6448ad 100644 --- a/Source/Sundew.Base.Identification/IValue.cs +++ b/Source/Sundew.Base.Identification/IValue.cs @@ -8,40 +8,20 @@ namespace Sundew.Base.Identification; using System; -using System.Runtime.CompilerServices; +using System.Text; using Sundew.DiscriminatedUnions; /// /// Represents a value. /// [DiscriminatedUnion] -public partial interface IValue : IArguments +public partial interface IValue { /// - /// Gets the value from the arguments. + /// Appends this to the specified . /// - /// The type of the value. - /// The default value. + /// The string builder. /// The format provider. - /// The argument name. - /// The retrieved value or the default value. - public TValue Get( - TValue defaultValue, - IFormatProvider formatProvider, - [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) - where TValue : IParsable; - - /// - /// 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 Get2( - TValue defaultValue, - IFormatProvider formatProvider, - [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) - where TValue : IValueIdentifiable; + /// 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 index ca9a44f..0eca3d0 100644 --- a/Source/Sundew.Base.Identification/IValueIdentifiable.cs +++ b/Source/Sundew.Base.Identification/IValueIdentifiable.cs @@ -7,6 +7,8 @@ namespace Sundew.Base.Identification; +using System; + /// /// Interface for implementing a value identifiable. /// @@ -17,7 +19,8 @@ public interface IValueIdentifiable : IIdentifiable /// Creates a value from the value id. /// /// The initial value. - /// The valueId. + /// The value id. + /// The format provider. /// The created value. - static abstract TValue From(TValue value, ValueId valueId); + 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 index cbcae02..9261d1f 100644 --- a/Source/Sundew.Base.Identification/Id.cs +++ b/Source/Sundew.Base.Identification/Id.cs @@ -19,7 +19,7 @@ namespace Sundew.Base.Identification; /// /// Represents any Id. /// -public record Id(Source Source, Path? Path, IArguments? Arguments = null) : IParsable +public record Id(Source Source, Path? Path, Arguments? Arguments = null) : IParsable { /// /// Parses the specified input string into an instance of the type. @@ -126,8 +126,8 @@ public R TryGetTargetContainingType() /// A new . public static Id From(Expression> targetExpression) { - var (source, path, valueId) = ExpressionEvaluator.From(targetExpression); - return new Id(source, path, valueId.HasValue ? new ComplexValue([valueId]) : null); + var (source, path, arguments) = ExpressionEvaluator.From(targetExpression); + return new Id(source, path, arguments); } /// @@ -152,7 +152,7 @@ public static Id From(Expression> targetExpressio public static Id From(Expression> targetExpression, IIdentifiable value) { var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value); - return new Id(source, path, valueId.HasValue ? new ComplexValue(ValueArray.Empty.Add(value.Id)) : null); + return new Id(source, path, valueId); } /// @@ -165,7 +165,7 @@ public static Id From(Expression> targetExpression, IId public static Id From(Expression> targetExpression, IIdentifiable value) { var target = ExpressionEvaluator.From(targetExpression, value); - return new Id(target.Source, target.Path, new ComplexValue(ValueArray.Empty.Add(value.Id))); + return new Id(target.Source, target.Path, target.Arguments); } /// diff --git a/Source/Sundew.Base.Identification/Parsing/Grammar.cs b/Source/Sundew.Base.Identification/Parsing/Grammar.cs index 252acf4..b5c0cec 100644 --- a/Source/Sundew.Base.Identification/Parsing/Grammar.cs +++ b/Source/Sundew.Base.Identification/Parsing/Grammar.cs @@ -27,8 +27,8 @@ internal static class Grammar /// Key Value separator. public const char KeyValueSeparator = '='; - /// The argument separator. - public const char ArgumentSeparator = ','; + /// The array element separator. + public const char ArrayElementSeparator = ','; /// The start of and array. public const char ArrayStart = '['; @@ -36,8 +36,8 @@ internal static class Grammar /// The end of an array. public const char ArrayEnd = ']'; - /// The value id separator. - public const char ValueIdsSeparator = '&'; + /// The argument separator. + public const char ArgumentSeparator = '&'; /// The group start. public const char GroupStart = '('; diff --git a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs index 0bd8fd4..e1a0a32 100644 --- a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -24,15 +24,15 @@ static IdRouteParser() 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 valueIdNameLexerRule = new RegexLexerRule(Tokens.ValueIdName, 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 valueIdValueLexerRule = new RegexLexerRule(Tokens.ValueIdValue, new Regex(@"[^),\]&]+", RegexOptions.Compiled)); IdRouteLexer = new Lexer( [sourceNameLexerRule, sourcePathLexerRule, sourceOriginLexerRule, segmentNameLexerRule, - valueIdNameLexerRule, + argumentNameLexerRule, valueIdMetadataLexerRule, valueIdValueLexerRule]); IdLexer = new Lexer( @@ -40,7 +40,7 @@ static IdRouteParser() sourcePathLexerRule, sourceOriginLexerRule, segmentNameLexerRule, - valueIdNameLexerRule, + argumentNameLexerRule, valueIdMetadataLexerRule, valueIdValueLexerRule]); } @@ -79,6 +79,23 @@ public static R ParseId(string? input, IFormatProvider? formatProv 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(); @@ -120,7 +137,7 @@ private static R Id(Parser parser) } } - IArguments? arguments = null; + Arguments? arguments = null; if (parser.TryAccept(Grammar.ArgumentsSeparator)) { var valueIdsResult = Arguments(parser); @@ -157,14 +174,21 @@ private static R Path(Parser parser) { if (parser.TryAccept(Grammar.GroupStart)) { - var argumentsResult = Arguments(parser).MapError(IPathError._PathValueIdError) - .And(() => parser.Accept(Grammar.GroupEnd).Map(le => IPathError._PathEndError(Grammar.GroupStart, le))); - if (!argumentsResult.IsSuccess) + if (parser.TryAccept(Grammar.GroupEnd)) { - return R.Error(argumentsResult.Error); + 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)); + segments.Add(new Segment(segmentResult.Value, argumentsResult.Value)); + } } else { @@ -190,105 +214,81 @@ private static R Path(Parser parser) return R.Success(new Path(segments.ToValueArray())); } - private static R Arguments(Parser parser) + private static R Arguments(Parser parser) { - var valueIds = ImmutableArray.CreateBuilder(); + var valueIds = ImmutableArray.CreateBuilder(); while (!parser.IsEnd()) { - var value = Value(parser); - if (value.IsSuccess) + var argumentResult = Argument(parser); + if (argumentResult.IsSuccess) { - valueIds.Add(new ValueId(null, null, value.Value)); - if (!parser.Accept(Grammar.ValueIdsSeparator)) + valueIds.Add(argumentResult.Value); + if (!parser.Accept(Grammar.ArgumentSeparator)) { break; } - - continue; - } - - var groupValueIdResult = parser.TryAccept( - Grammar.GroupStart, - r => r.MapError(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupStart, lexerError)) - .And(() => ValueId(parser).MapError(IArgumentsError._ValueIdError)) - .And(() => parser.Accept(Grammar.GroupEnd).Map(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupEnd, lexerError))) - .Map(x => x.Value2)); - - /*var groupValueIdResult = parser.TryAccept( - Grammar.GroupStart, - r => r.MapError(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupStart, lexerError)) - .And(() => ValueId(parser).MapError(IArgumentsError._ValueIdError)) - .And(() => parser.Accept(Grammar.GroupEnd).Map(lexerError => IArgumentsError._GroupValueIdError(Grammar.GroupEnd, lexerError))) - .Map(x => x.Value2));*/ - if (groupValueIdResult.IsSuccess) - { - valueIds.Add(groupValueIdResult.Value); - } - else if (parser.IsNext(Grammar.GroupEnd)) - { } else { - var valueIdResult = ValueId(parser); - if (valueIdResult.IsSuccess) - { - valueIds.Add(valueIdResult.Value); - } - } - - if (groupValueIdResult.Error is IArgumentsError.GroupValueIdError { Cause: Grammar.GroupEnd }) - { - return R.Error(groupValueIdResult.Error); - } - - if (!parser.Accept(Grammar.ValueIdsSeparator)) - { - break; + return R.Error(argumentResult.Error); } } - return R.Success(IArguments.ComplexValue(valueIds.ToValueArray())); + return R.Success(new Arguments(valueIds.ToValueArray())); } - private static R ValueId(Parser parser) + private static R Argument(Parser parser) { - var fullValueIdResult = parser.TryAccept( - Tokens.ValueIdName, - result => result.MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdName, lexerError)) - .And(() => parser.Accept(Grammar.NameMetadataSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.NameMetadataSeparator, lexerError))) - .And(() => parser.Accept(Tokens.ValueIdMetadata).MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdMetadata, lexerError))) - .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) - .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) - .Map(x => new ValueId(x.Value1, x.Value2, x.Value3))); - if (fullValueIdResult.IsSuccess) + var argumentNameOption = parser.TryAccept(Tokens.ArgumentName); + var argumentResult = ValueId(parser).MapError(IArgumentsError._ValueIdError) + .Map(x => new Argument(argumentNameOption, x)); + if (argumentResult.IsSuccess) { - return fullValueIdResult; + return argumentResult; } - fullValueIdResult = parser.TryAccept( - Grammar.NameMetadataSeparator, - result => result.MapError(lexerError => IValueIdError._ValueIdError(Grammar.NameMetadataSeparator, lexerError)) - .And(() => parser.Accept(Tokens.ValueIdMetadata).MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdMetadata, lexerError))) - .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) - .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) - .Map(x => new ValueId(null, x.Value2, x.Value3))); - if (fullValueIdResult.IsSuccess) + argumentResult = parser.TryAccept( + Grammar.KeyValueSeparator, + result => result.MapError(x => IArgumentsError.LexError(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 fullValueIdResult; + return R.Success(new Argument(argumentNameOption, new ValueId(null, valueIdResult.Value))); } - fullValueIdResult = parser.TryAccept( - Tokens.ValueIdName, - result => result.MapError(lexerError => IValueIdError._ValueIdError(Tokens.ValueIdName, lexerError)) - .And(() => parser.Accept(Grammar.KeyValueSeparator).Map(lexerError => IValueIdError._ValueIdError(Grammar.KeyValueSeparator, lexerError))) - .And(() => Value(parser).MapError(IValueIdError._ValueIdValueError)) - .Map(x => new ValueId(x.Value1, null, x.Value2))); - if (fullValueIdResult.IsSuccess) + parser.Undo(); + valueIdResult = Value(parser); + if (valueIdResult.IsSuccess) { - return fullValueIdResult; + return R.Success(new Argument(null, new ValueId(null, valueIdResult.Value))); } - return Value(parser).Map(x => new ValueId(null, null, x), IValueIdError._ValueIdValueError); + return R.Error(IArgumentsError._ValueError(valueIdResult.Error)); + } + + private static R ValueId(Parser parser) + { + var metadataResult = parser.TryAccept( + Grammar.NameMetadataSeparator, + result => result.MapError(lexerError => IValueIdError.LexError(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) @@ -298,32 +298,11 @@ private static R Value(Parser parser) result => { return result.MapError(lexerError => IValueError._GroupError(Grammar.GroupStart, lexerError)) - .And(() => - { - var valueIds = ImmutableArray.CreateBuilder(); - while (!parser.IsNext(Grammar.GroupStart)) - { - var valueIdResult = ValueId(parser); - if (valueIdResult.IsSuccess) - { - valueIds.Add(valueIdResult.Value); - if (!parser.Accept(Grammar.ValueIdsSeparator)) - { - break; - } - } - else - { - return R.Error(IValueError._ValueIdError(valueIdResult.Error)); - } - } - - return R.Success(new ComplexValue(valueIds.ToValueArray())).Omits(); - }) + .And(() => Arguments(parser).MapError(x => IValueError._ValueError(null))) .And(() => parser.Accept(Grammar.GroupEnd) .Map(lexerError => IValueError._GroupError(Grammar.GroupEnd, lexerError))) .Map(x => x.Value2); - }); + }).Map(x => IValue.ComplexValue(x.Items)); if (valueResult.IsSuccess) { return valueResult; @@ -333,7 +312,7 @@ private static R Value(Parser parser) Grammar.ArrayStart, result => { - return result.MapError(lexerError => IValueError._ArrayError(Grammar.GroupStart, lexerError)) + return result.MapError(lexerError => IValueError._ArrayError(Grammar.ArrayStart, lexerError)) .And(() => { var valueIds = ImmutableArray.CreateBuilder(); @@ -343,7 +322,7 @@ private static R Value(Parser parser) if (singleValueIdResult.IsSuccess) { valueIds.Add(singleValueIdResult.Value); - if (!parser.Accept(Grammar.ValueIdsSeparator)) + if (!parser.Accept(Grammar.ArrayElementSeparator)) { break; } @@ -365,6 +344,6 @@ private static R Value(Parser parser) return valueResult; } - return parser.Accept(Tokens.ValueIdValue).Map(x => IValue.ScalarValue(x), IValueError._ValueError); + return parser.Accept(Tokens.ValueIdValue).Map(IValue.ScalarValue, 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 index 6947265..f3ac81a 100644 --- a/Source/Sundew.Base.Identification/Parsing/Tokens.cs +++ b/Source/Sundew.Base.Identification/Parsing/Tokens.cs @@ -17,7 +17,7 @@ internal enum Tokens PathSegmentName, - ValueIdName, + ArgumentName, ValueIdMetadata, diff --git a/Source/Sundew.Base.Identification/Path.cs b/Source/Sundew.Base.Identification/Path.cs index 1a5d71f..ed946d1 100644 --- a/Source/Sundew.Base.Identification/Path.cs +++ b/Source/Sundew.Base.Identification/Path.cs @@ -24,7 +24,7 @@ public sealed record Path(ValueArray Segments) public const char Separator = '/'; /* /// - /// Get the from input path. + /// GetScalar the from input path. /// /// The input path. /// The path. diff --git a/Source/Sundew.Base.Identification/ScalarValue.cs b/Source/Sundew.Base.Identification/ScalarValue.cs index 39dcfd4..01f67e3 100644 --- a/Source/Sundew.Base.Identification/ScalarValue.cs +++ b/Source/Sundew.Base.Identification/ScalarValue.cs @@ -25,35 +25,7 @@ public sealed partial record ScalarValue(string Value) : IValue /// The append options. public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) { - stringBuilder.Append(this.Value); - } - - /// - /// 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 Get(TValue defaultValue, IFormatProvider formatProvider, string? referenceName = null) - where TValue : IParsable - { - 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 Get2(TValue defaultValue, IFormatProvider formatProvider, string? referenceName = null) - where TValue : IValueIdentifiable - { - return defaultValue; + stringBuilder.Append(Uri.EscapeDataString(this.Value)); } /// diff --git a/Source/Sundew.Base.Identification/Segment.cs b/Source/Sundew.Base.Identification/Segment.cs index 95b1af2..083b529 100644 --- a/Source/Sundew.Base.Identification/Segment.cs +++ b/Source/Sundew.Base.Identification/Segment.cs @@ -15,7 +15,7 @@ namespace Sundew.Base.Identification; /// /// The name of the segment, which serves as its identifier. /// A value for the segment. -public sealed record Segment(string Name, IArguments? Arguments = null) +public sealed record Segment(string Name, Arguments? Arguments = null) { /// /// Appends the name of the current instance to the specified StringBuilder, followed by parentheses. diff --git a/Source/Sundew.Base.Identification/TargetEvaluator.cs b/Source/Sundew.Base.Identification/TargetEvaluator.cs index ca6a783..1f2e317 100644 --- a/Source/Sundew.Base.Identification/TargetEvaluator.cs +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -13,6 +13,7 @@ namespace Sundew.Base.Identification; 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; @@ -67,7 +68,7 @@ public static R GetResultType(Source source, Path? path) return R.Error(); } - public static R> GetInputTypes(Source source, Path? path, IArguments? arguments) + public static R> GetInputTypes(Source source, Path? path, Arguments? arguments) { var sourceType = source.TryGetType(); if (sourceType.IsError) @@ -77,7 +78,7 @@ public static R> GetInputTypes(Source source, Path? path, IA if (arguments.HasValue) { - return arguments.ToValueIds().Items.Select(x => x.TryGetType()).AllOrFailed(x => x.ToItem()).Map(x => (IReadOnlyList)x.Items); + return arguments.Items.Select(x => x.ValueId.TryGetType()).AllOrFailed(x => x.ToItem()).Map(x => (IReadOnlyList)x.Items); } if (!path.HasValue) @@ -163,7 +164,7 @@ internal static bool GetTypeName(Type type, StringBuilder stringBuilder) stringBuilder .Append(baseName) .Append(Grammar.ArrayStart) - .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), Grammar.ArgumentSeparator) + .AppendItems(type.GetGenericArguments(), (builder, x) => GetTypeName(x, builder), Grammar.ArrayElementSeparator) .Append(Grammar.ArrayEnd); return false; @@ -209,10 +210,10 @@ private static void BuildNestedName(Type type, StringBuilder stringBuilder) case Empty empty: break; case Multiple multiple: - var valueIds = segment.Arguments?.ToValueIds() ?? new ComplexValue([]); + var valueIds = segment.Arguments?.Items ?? []; var methodInfo = multiple.Items.OfType() .Select(methodInfo => (methodInfo, parameters: methodInfo.GetParameters())) - .Where(x => x.parameters.Length == valueIds.Items.Count) + .Where(x => x.parameters.Length == valueIds.Count) .FirstOrDefault(x => IsMatch(x.parameters, valueIds)).methodInfo; memberInfo = methodInfo; if (methodInfo.HasValue) @@ -240,18 +241,18 @@ private static void BuildNestedName(Type type, StringBuilder stringBuilder) return memberInfo; } - private static bool IsMatch(ParameterInfo[] parameterInfos, ComplexValue complexValue) + private static bool IsMatch(ParameterInfo[] parameterInfos, ValueArray arguments) { - if (!complexValue.HasValue) + if (arguments.IsEmpty) { return parameterInfos.Length == 0; } - return parameterInfos.Zip(complexValue.Items).All(x => + return parameterInfos.Zip(arguments).All(x => { - var argumentType = x.Second.Metadata.HasValue - ? Source.Parse(x.Second.Metadata, CultureInfo.InvariantCulture).TryGetType().Value - : GetTypeFromArgument(x.First.ParameterType, x.Second.Value); + 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); }); } diff --git a/Source/Sundew.Base.Identification/ValueEscaper.cs b/Source/Sundew.Base.Identification/ValueEscaper.cs new file mode 100644 index 0000000..6d61d44 --- /dev/null +++ b/Source/Sundew.Base.Identification/ValueEscaper.cs @@ -0,0 +1,17 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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 ValueEscaper +{ + /* + public static void EncodeInto(string value) + { + return new ValueId(value.Replace("\\", "\\\\").Replace(":", "\\:")); + }*/ +} \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index 471ddbc..694eed8 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -10,16 +10,17 @@ 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 name. /// The metadata. /// The value. -public sealed partial record ValueId(string? Name, string? Metadata, IValue Value) +public sealed partial record ValueId(string? Metadata, IValue Value) { /// /// Gets the type of the source. @@ -42,7 +43,7 @@ public R TryGetType() public override string ToString() { var stringBuilder = new StringBuilder(); - this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true)); + this.AppendInto(stringBuilder, CultureInfo.CurrentCulture, new AppendOptions(true), false); return stringBuilder.ToString(); } @@ -52,7 +53,8 @@ public override string ToString() /// The string builder. /// The format provider. /// The append options. - public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions) + /// Indicates whether the value id has a name. + public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvider, AppendOptions appendOptions, bool requiresKeySeparation) { bool TryAppendMetadata() { @@ -66,18 +68,7 @@ bool TryAppendMetadata() return false; } - if (!string.IsNullOrEmpty(this.Name)) - { - stringBuilder.Append(this.Name); - TryAppendMetadata(); - stringBuilder.Append(Grammar.KeyValueSeparator); - - this.Value.AppendInto(stringBuilder, formatProvider, appendOptions with { IsRoot = false }); - - return; - } - - if (TryAppendMetadata()) + if (TryAppendMetadata() || requiresKeySeparation) { stringBuilder.Append(Grammar.KeyValueSeparator); } @@ -125,44 +116,22 @@ public static ValueId Parse(string inputArg, IFormatProvider? provider) /// true if parsing was successful, otherwise false. public static bool TryParse([NotNullWhen(true)] string? inputArg, IFormatProvider? formatProvider, [MaybeNullWhen(false)] out ValueId result) { - if (inputArg.HasValue) - { - string key = string.Empty; - var level = 0; - var index = 0; - var metadataIndex = -1; - while (index < inputArg.Length) - { - var character = inputArg[index++]; - if (character == Grammar.GroupStart) - { - level++; - } - - if (character == Grammar.GroupEnd) - { - level--; - } - - if (character == Grammar.NameMetadataSeparator && level == 0) - { - metadataIndex = index; - } - - if (character == Grammar.KeyValueSeparator && level == 0) - { - var (nameLength, metadataStart, metadataLength) = metadataIndex > -1 ? (metadataIndex - 1, metadataIndex, index - metadataIndex - 1) : (index - 1, 0, 0); - result = new ValueId(inputArg.Substring(0, nameLength), inputArg.Substring(metadataStart, metadataLength), new ScalarValue(inputArg.Substring(index))); - return true; - } - } - - result = new ValueId(null, null, new ScalarValue(inputArg)); - return true; - } + var valueIdResult = IdRouteParser.ParseValueId(inputArg, formatProvider); + result = valueIdResult.Value; + return valueIdResult.IsSuccess; + } - result = null; - return false; + /// + /// 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); } /// @@ -174,6 +143,86 @@ public static bool TryParse([NotNullWhen(true)] string? inputArg, IFormatProvide public TValue ToValue(TValue defaultValue) where TValue : IValueIdentifiable { - return TValue.From(defaultValue, this); + 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; + } + + var argument = complexValue.Items.FirstOrDefault(x => x.Name == referenceName); + if (argument.HasValue) + { + return TValue.Parse(Uri.UnescapeDataString(argument.ValueId.Value.ToString() ?? string.Empty), 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(Uri.UnescapeDataString(argument.ValueId.Value.ToString() ?? string.Empty), 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) + { + 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) + { + 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 index 362b7df..04f5958 100644 --- a/Source/Sundew.Base.Identification/ValueIdBuilder.cs +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs @@ -19,7 +19,7 @@ namespace Sundew.Base.Identification; /// The . public sealed class ValueIdBuilder(Type type) { - private readonly List values = new(); + private readonly List values = new(); /// /// Adds a value to the builder for dynamic construction of identifiers. @@ -47,14 +47,14 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo if (value != null && value is IValueIdentifiable valueIdentifiable) { var valueId = valueIdentifiable.Id; - this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), valueId.Value)); + this.values.Add(new Argument(name, new ValueId(GetMetadata(value.GetType(), typeof(TValue), false), valueId.Value))); } else if (value != null) { var stringValue = value.ToString(); if (stringValue.HasValue) { - this.values.Add(new ValueId(name, GetMetadata(value.GetType(), typeof(TValue), false), new ScalarValue(stringValue))); + this.values.Add(new Argument(name, new ValueId(GetMetadata(value.GetType(), typeof(TValue), false), new ScalarValue(stringValue)))); } } @@ -71,9 +71,9 @@ public ValueId Build() var metadata = Source.FromType(type).ToString(); return cardinality switch { - Empty empty => new ValueId(null, metadata, new ScalarValue("null")), - Multiple valueIds => new ValueId(null, metadata, new ComplexValue(valueIds.Items.ToValueArray())), - Single single => single.Item, + Empty empty => new ValueId(metadata, new ScalarValue("null")), + Multiple valueIds => new ValueId(metadata, new ComplexValue(valueIds.Items.ToValueArray())), + Single single => single.Item.ValueId, }; } diff --git a/Source/Sundew.Base.Parsing/Parser.cs b/Source/Sundew.Base.Parsing/Parser.cs index 9a4e314..6e69422 100644 --- a/Source/Sundew.Base.Parsing/Parser.cs +++ b/Source/Sundew.Base.Parsing/Parser.cs @@ -9,18 +9,20 @@ 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(); - private State state; /// /// Initializes a new instance of the class. @@ -33,7 +35,7 @@ public Parser(ILexer lexer, string input, IFormatProvider? formatProvide this.lexer = lexer; this.Input = input; this.FormatProvider = formatProvider ?? CultureInfo.CurrentCulture; - this.state = new State(0); + this.stateStack.Push(new State(0)); } /// @@ -46,6 +48,8 @@ public Parser(ILexer lexer, string input, IFormatProvider? formatProvide /// 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. @@ -56,9 +60,10 @@ public Parser(ILexer lexer, string input, IFormatProvider? formatProvide /// true if the token is accepted at the current position; otherwise, false. public bool Accept(TToken token, out string lexeme) { - if (this.lexer.TryGetLexeme(token, this.Input, this.state, out lexeme, out var consumedLength)) + var state = this.stateStack.Peek(); + if (this.lexer.TryGetLexeme(token, this.Input, state, out lexeme, out var consumedLength)) { - this.state = new State(this.state.Position + consumedLength); + this.stateStack.Push(new State(state.Position + consumedLength)); return true; } @@ -74,13 +79,32 @@ public bool Accept(TToken token, out string lexeme) /// true if the token is accepted at the current position; otherwise, false. public R Accept(TToken token) { - if (this.lexer.TryGetLexeme(token, this.Input, this.state, out var lexeme, out var consumedLength)) + var state = this.stateStack.Peek(); + if (this.lexer.TryGetLexeme(token, this.Input, state, out var lexeme, out var consumedLength)) { - this.state = new State(this.state.Position + consumedLength); + this.stateStack.Push(new State(state.Position + consumedLength)); return R.Success(lexeme); } - return R.Error(LexerError._TokenTypeError(token, this.state.Position, 0)); + 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; } /// @@ -94,21 +118,21 @@ public R Accept(TToken token) /// true if the token is accepted at the current position; otherwise, false. public R TryAccept(TToken token, Func, R> matchNextFunc) { - var stateBefore = this.state; - if (this.lexer.TryGetLexeme(token, this.Input, this.state, out var lexeme, out var consumedLength)) + var state = this.stateStack.Peek(); + if (this.lexer.TryGetLexeme(token, this.Input, state, out var lexeme, out var consumedLength)) { - this.state = new State(this.state.Position + consumedLength); + this.stateStack.Push(new State(state.Position + consumedLength)); var result = matchNextFunc(R.Success(lexeme)); if (result.IsSuccess) { return result; } - this.state = stateBefore; + this.stateStack.Pop(); return R.Error(result.Error); } - return matchNextFunc(R.Error(LexerError._TokenTypeError(token, this.state.Position, 0))); + return matchNextFunc(R.Error(LexerError._TokenTypeError(token, state.Position, 0))); } /// @@ -122,21 +146,21 @@ public R TryAccept(TToken token, Functrue if the token is accepted at the current position; otherwise, false. public R TryAccept(char input, Func, R> matchNextFunc) { - var stateBefore = this.state; + var state = this.stateStack.Peek(); if (this.IsNext(input)) { - this.state = new State(this.state.Position + 1); + this.stateStack.Push(new State(state.Position + 1)); var result = matchNextFunc(R.Success(input.ToString())); if (result.IsSuccess) { return result; } - this.state = stateBefore; + this.stateStack.Pop(); return R.Error(result.Error); } - return matchNextFunc(R.Error(LexerError._TokenError(input.ToString(), this.state.Position, 0))); + return matchNextFunc(R.Error(LexerError._TokenError(input.ToString(), state.Position, 0))); } /// @@ -149,13 +173,14 @@ public R TryAccept(char input, Functrue 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.state = new State(this.state.Position + 1); + this.stateStack.Push(new State(state.Position + 1)); return R.Success(); } - return R.Error(LexerError._TokenError(input.ToString(), this.state.Position, 1)); + return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1)); } /// @@ -168,13 +193,14 @@ public RoE TryAccept(char input) /// 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.state = new State(this.state.Position + 1); + this.stateStack.Push(new State(state.Position + 1)); return R.Success(); } - return R.Error(LexerError._TokenError(input.ToString(), this.state.Position, 1)); + return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1)); } /// @@ -183,15 +209,16 @@ public RoE Accept(char input) /// /// 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 bool Accept(string input) + public RoE Accept(string input) { + var state = this.stateStack.Peek(); if (this.IsNext(input)) { - this.state = new State(this.state.Position + input.Length); - return true; + this.stateStack.Push(new State(state.Position + input.Length)); + return R.Success(); } - return false; + return R.Error(LexerError._TokenError(input.ToString(), state.Position, 1)); } /// @@ -201,7 +228,8 @@ public bool Accept(string input) /// true if the specified character matches the current character in the input; otherwise, false. public bool IsNext(char input) { - if (this.Input.Length > this.state.Position && this.Input[this.state.Position] == input) + var state = this.stateStack.Peek(); + if (this.Input.Length > state.Position && this.Input[state.Position] == input) { return true; } @@ -217,8 +245,9 @@ public bool IsNext(char input) /// true if the specified string matches the input sequence at the current position; otherwise, false. public bool IsNext(string input) { - if (this.Input.Length > this.state.Position + input.Length - 1 && - this.Input.AsSpan(this.state.Position, input.Length).SequenceEqual(input.AsSpan())) + 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; } @@ -226,31 +255,42 @@ public bool IsNext(string input) 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() { - return R.FromError(this.state.Position == this.Input.Length, () => LexerError._End(this.state.Position)); + var state = this.stateStack.Peek(); + return R.FromError(state.Position == this.Input.Length, () => LexerError._End(state.Position)); } /// - /// Gets the current state of the parser. + /// Undoes the last accepted change. /// - /// The current state represented as a instance. - public State CurrentState() + /// A value indicating whether the undo was successful. + public bool Undo() { - return this.state; + return this.stateStack.TryPop(out var _); } /// - /// Restores the object's state to the specified value. + /// Gets the current state of the parser. /// - /// The state to restore. This parameter must not be null. - public void RestoreState(State state) + /// The current state represented as a instance. + public State CurrentState() { - this.state = state; + return this.stateStack.Peek(); } /// 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 From 5922df0ab8062a6747b5bed4add3a0b4bf4f7123 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Wed, 15 Apr 2026 01:21:02 +0200 Subject: [PATCH 07/20] Added Uri support to Ids --- .../Identification/IdTests.cs | 83 ++++++++++---- Source/Sundew.Base.Identification/Argument.cs | 2 +- Source/Sundew.Base.Identification/Id.cs | 107 +++++++++++++++++- .../{ => Parsing}/IParserError.cs | 0 .../Parsing/IdRouteParser.cs | 2 +- Source/Sundew.Base.Identification/ValueId.cs | 8 +- 6 files changed, 174 insertions(+), 28 deletions(-) rename Source/Sundew.Base.Identification/{ => Parsing}/IParserError.cs (100%) diff --git a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs index 55f91af..2cc7cd0 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -18,27 +18,40 @@ namespace Sundew.Base.Development.Tests.Identification; public class IdTests { [Test] - [Obsolete("Obsolete")] - public void T() + public void ToUri_Then_ResultShouldBeExpectedResult() { - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?1", UriKind.Absolute, out var uri); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Find?Person=(Address=Home)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri2); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri3); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Nam?espace$Assembly/Path?Name=Kim&LastName=Hugener", UriKind.Absolute, out var uri4); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Namespace$Assembly/Path?Name=Kim?LastName=Hugener", UriKind.Absolute, out var uri5); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,description)?Person=(Address=Home,Number=15)&Description=(Eyes=Blue)", UriKind.Absolute, out var uri6); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,description)?Person={Address=Home,Number=15}&Description={Eyes=Blue}", UriKind.Absolute, out var uri7); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(Person,Description[])?person={Address=Home,Number=15}&description={Eyes=Blue}", UriKind.Absolute, out var uri8); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,IList[Description])?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri9); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(person,descriptions)?person=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri10); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Find(query,descriptions)?Query!Name.Name.Space$Assembly=(Address=Home,Number=15)&descriptions=[Blue,Green]", UriKind.Absolute, out var uri11); - Uri.TryCreate("aid://user:pwd@Host:80/Name+Nested~Name.Space$Assembly/Start(!Name~Name.Space$Assembly=15)/Find?!Name.Name.Space$Assembly=(Address=Home,Number=15)&string[]=[Blue,Green]", UriKind.Absolute, out var uri12); - var t1 = Uri.EscapeUriString(uri6!.OriginalString); - var t2 = Uri.EscapeUriString(uri8!.OriginalString); - var t3 = Uri.EscapeUriString(uri9!.OriginalString); - var t4 = Uri.EscapeUriString(uri10!.OriginalString); - var t5 = Uri.EscapeUriString(uri11!.OriginalString); - var t6 = Uri.EscapeUriString(uri12!.OriginalString); + 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] @@ -222,6 +235,24 @@ public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpe } } + [Test] + public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpected2() + { + const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Search(pointOfInterest!IdTests+PointOfInterest~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Name=City%20with%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))"; + var result = Id.From(x => x.Search(new PointOfInterest("City 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(PointOfInterest)]); + 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 @@ -233,6 +264,8 @@ public interface INavigator void NavigateTo(Position position); void NavigateTo(Position position, bool addToHistory); + + Position Search(PointOfInterest pointOfInterest); } public interface ICommand @@ -263,4 +296,14 @@ public static Position3D From(Position3D value, ValueId valueId, IFormatProvider valueId.GetScalar(value.Z, CultureInfo.InvariantCulture)); } } + + public record PointOfInterest(string Name) : IValueIdentifiable + { + public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.Name)); + + public static PointOfInterest From(PointOfInterest value, ValueId valueId, IFormatProvider? formatProvider) + { + return new PointOfInterest(valueId.GetScalar(value.Name, CultureInfo.InvariantCulture)); + } + } } \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/Argument.cs b/Source/Sundew.Base.Identification/Argument.cs index f9d2c72..1751244 100644 --- a/Source/Sundew.Base.Identification/Argument.cs +++ b/Source/Sundew.Base.Identification/Argument.cs @@ -15,7 +15,7 @@ namespace Sundew.Base.Identification; /// /// The Name. /// The value id. -public record Argument(string? Name, ValueId ValueId) +public sealed record Argument(string? Name, ValueId ValueId) { /// /// Appends this instance into the specified string builder. diff --git a/Source/Sundew.Base.Identification/Id.cs b/Source/Sundew.Base.Identification/Id.cs index 9261d1f..4a98166 100644 --- a/Source/Sundew.Base.Identification/Id.cs +++ b/Source/Sundew.Base.Identification/Id.cs @@ -13,14 +13,100 @@ namespace Sundew.Base.Identification; using System.Globalization; using System.Linq.Expressions; using System.Text; -using Sundew.Base.Collections.Immutable; using Sundew.Base.Identification.Parsing; /// /// Represents any Id. /// -public record Id(Source Source, Path? Path, Arguments? Arguments = null) : IParsable +public sealed record Id(Source Source, Path? Path, Arguments? Arguments = 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. /// @@ -118,6 +204,23 @@ 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. /// diff --git a/Source/Sundew.Base.Identification/IParserError.cs b/Source/Sundew.Base.Identification/Parsing/IParserError.cs similarity index 100% rename from Source/Sundew.Base.Identification/IParserError.cs rename to Source/Sundew.Base.Identification/Parsing/IParserError.cs diff --git a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs index e1a0a32..a994a2b 100644 --- a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -344,6 +344,6 @@ private static R Value(Parser parser) return valueResult; } - return parser.Accept(Tokens.ValueIdValue).Map(IValue.ScalarValue, IValueError._ValueError); + 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/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index 694eed8..48f5dbe 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -154,7 +154,7 @@ public TValue ToValue(TValue defaultValue) /// 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) + public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProvider, [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) where TValue : IParsable { if (!referenceName.HasValue) @@ -170,7 +170,7 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider formatProvi var argument = complexValue.Items.FirstOrDefault(x => x.Name == referenceName); if (argument.HasValue) { - return TValue.Parse(Uri.UnescapeDataString(argument.ValueId.Value.ToString() ?? string.Empty), formatProvider); + return TValue.Parse(argument.ValueId.Value.ToString() ?? string.Empty, formatProvider); } var firstDotIndex = referenceName.IndexOf('.'); @@ -180,7 +180,7 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider formatProvi argument = complexValue.Items.FirstOrDefault(x => x.Name == fallback); if (argument.HasValue) { - return TValue.Parse(Uri.UnescapeDataString(argument.ValueId.Value.ToString() ?? string.Empty), formatProvider); + return TValue.Parse(argument.ValueId.Value.ToString() ?? string.Empty, formatProvider); } return defaultValue; @@ -194,7 +194,7 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider formatProvi /// 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) + public TValue GetValue(TValue defaultValue, IFormatProvider? formatProvider, [CallerArgumentExpression(nameof(defaultValue))] string? referenceName = null) where TValue : IValueIdentifiable { if (!referenceName.HasValue) From 440b7c637f552ab76d85053695e7717bd3cd74fd Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Wed, 15 Apr 2026 06:25:09 +0200 Subject: [PATCH 08/20] Added SequenceId for implementing simple counted ids Partial support for InstanceIds in ValueId --- .../Identification/IdTests.cs | 56 +++++++++++++----- .../Identification/RevisionIdTests.cs | 59 +++++++++++++++++++ .../ExpressionEvaluator.cs | 5 +- .../Sundew.Base.Identification/ISequenceId.cs | 28 +++++++++ Source/Sundew.Base.Identification/Id.cs | 30 +++++++++- .../Sundew.Base.Identification/InstanceId.cs | 34 +++++++++++ .../Properties/AssemblyInfo.cs | 10 ++++ .../Sundew.Base.Identification/RevisionId.cs | 25 ++++++++ .../{ValueEscaper.cs => SequenceId.cs} | 13 ++-- .../SequenceIdExtensions.cs | 44 ++++++++++++++ .../Sundew.Base.Identification.csproj | 4 ++ Source/Sundew.Base.Identification/ValueId.cs | 2 +- 12 files changed, 283 insertions(+), 27 deletions(-) create mode 100644 Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs create mode 100644 Source/Sundew.Base.Identification/ISequenceId.cs create mode 100644 Source/Sundew.Base.Identification/InstanceId.cs create mode 100644 Source/Sundew.Base.Identification/Properties/AssemblyInfo.cs create mode 100644 Source/Sundew.Base.Identification/RevisionId.cs rename Source/Sundew.Base.Identification/{ValueEscaper.cs => SequenceId.cs} (65%) create mode 100644 Source/Sundew.Base.Identification/SequenceIdExtensions.cs diff --git a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs index 2cc7cd0..00eb16e 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -9,11 +9,9 @@ namespace Sundew.Base.Development.Tests.Identification; using System; using System.Globalization; -using System.Linq; using AwesomeAssertions; using AwesomeAssertions.Execution; using Sundew.Base.Identification; -using static Sundew.Base.Development.Tests.Identification.IdTests; public class IdTests { @@ -236,10 +234,10 @@ public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpe } [Test] - public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpected2() + public void From_When_TargetIsPropertyAndPassingArgumentByReferenceId_Then_ResultShouldBeExpected() { - const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Search(pointOfInterest!IdTests+PointOfInterest~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(Name=City%20with%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))"; - var result = Id.From(x => x.Search(new PointOfInterest("City with ( ) { } / , : ! $ ~ = < > & [ ] %"))); + const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Description?%3A1"; + var result = Id.From(x => x.Description, new PointOfInterest("Home")); using (var scope = new AssertionScope()) { @@ -247,7 +245,25 @@ public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpe var expected = Id.Parse(result.ToString(), CultureInfo.InvariantCulture); expected.Should().Be(result); result.ToString().Should().Be(expectedResult); - result.TryGetInputTypes().Value.Should().Equal([typeof(PointOfInterest)]); + 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))"; + 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)); } @@ -259,13 +275,15 @@ public interface INavigator { ICommand Navigate { get; } + ICommand Description { get; } + void GoBack(); void NavigateTo(Position position); void NavigateTo(Position position, bool addToHistory); - Position Search(PointOfInterest pointOfInterest); + Position Search(Query query); } public interface ICommand @@ -273,6 +291,11 @@ 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)); @@ -280,8 +303,8 @@ public record Position(int X, int Y) : IValueIdentifiable public static Position From(Position position, ValueId valueId, IFormatProvider? formatProvider) { return new Position( - valueId.GetScalar(position.X, CultureInfo.InvariantCulture), - valueId.GetScalar(position.Y, CultureInfo.InvariantCulture)); + valueId.GetScalar(position.X, formatProvider), + valueId.GetScalar(position.Y, formatProvider)); } } @@ -292,18 +315,23 @@ public record Position3D(Position Position, int Z) : IValueIdentifiable + public record Query(string Name) : IValueIdentifiable { public ValueId Id => ValueId.From(this, (value, builder) => builder.Add(value.Name)); - public static PointOfInterest From(PointOfInterest value, ValueId valueId, IFormatProvider? formatProvider) + public static Query From(Query value, ValueId valueId, IFormatProvider? formatProvider) { - return new PointOfInterest(valueId.GetScalar(value.Name, CultureInfo.InvariantCulture)); + 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..814ce45 --- /dev/null +++ b/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs @@ -0,0 +1,59 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; + +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.Identification/ExpressionEvaluator.cs b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs index a8f930c..2eaa302 100644 --- a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs +++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs @@ -23,11 +23,10 @@ internal static class ExpressionEvaluator /// Gets a for the specified expression. /// /// The path expression. - /// The value. + /// The value id. /// A new . - public static (Source Source, Path Path, Arguments? Arguments) From(LambdaExpression pathExpression, IIdentifiable? value = null) + public static (Source Source, Path Path, Arguments? Arguments) From(LambdaExpression pathExpression, ValueId? valueId = null) { - var valueId = value?.Id; var isUsed = false; var segments = ImmutableArray.CreateBuilder(); var source = Source.FromType(pathExpression.Parameters.First().Type); 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/Id.cs b/Source/Sundew.Base.Identification/Id.cs index 4a98166..d21f582 100644 --- a/Source/Sundew.Base.Identification/Id.cs +++ b/Source/Sundew.Base.Identification/Id.cs @@ -245,6 +245,32 @@ public static Id From(Expression> targetExpressio 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 ScalarValue(value.Id.ToString()))); + 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, new ValueId(null, new ScalarValue(value.Id.ToString()))); + return new Id(target.Source, target.Path, target.Arguments); + } + /// /// Gets an from the specified source and expression. /// @@ -254,7 +280,7 @@ public static Id From(Expression> targetExpressio /// A new . public static Id From(Expression> targetExpression, IIdentifiable value) { - var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value); + var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, value?.Id); return new Id(source, path, valueId); } @@ -267,7 +293,7 @@ public static Id From(Expression> targetExpression, IId /// A new . public static Id From(Expression> targetExpression, IIdentifiable value) { - var target = ExpressionEvaluator.From(targetExpression, value); + var target = ExpressionEvaluator.From(targetExpression, value?.Id); return new Id(target.Source, target.Path, target.Arguments); } diff --git a/Source/Sundew.Base.Identification/InstanceId.cs b/Source/Sundew.Base.Identification/InstanceId.cs new file mode 100644 index 0000000..babe9bd --- /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/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/ValueEscaper.cs b/Source/Sundew.Base.Identification/SequenceId.cs similarity index 65% rename from Source/Sundew.Base.Identification/ValueEscaper.cs rename to Source/Sundew.Base.Identification/SequenceId.cs index 6d61d44..91f706b 100644 --- a/Source/Sundew.Base.Identification/ValueEscaper.cs +++ b/Source/Sundew.Base.Identification/SequenceId.cs @@ -1,5 +1,5 @@ // -------------------------------------------------------------------------------------------------------------------- -// +// // Copyright (c) Sundews. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // @@ -7,11 +7,10 @@ namespace Sundew.Base.Identification; -internal static class ValueEscaper +internal static class SequenceId + where TId : ISequenceId { - /* - public static void EncodeInto(string value) - { - return new ValueId(value.Replace("\\", "\\\\").Replace(":", "\\:")); - }*/ +#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..81fc6cd --- /dev/null +++ b/Source/Sundew.Base.Identification/SequenceIdExtensions.cs @@ -0,0 +1,44 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// 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; +using System.Runtime.CompilerServices; +using System.Threading; + +/// +/// Extends with easy to use methods. +/// +public static class SequenceIdExtensions +{ + extension(ISequenceId 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/Sundew.Base.Identification.csproj b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj index 3573e16..2d507d5 100644 --- a/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj +++ b/Source/Sundew.Base.Identification/Sundew.Base.Identification.csproj @@ -11,4 +11,8 @@ + + + + \ No newline at end of file diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index 48f5dbe..c8d50b1 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -20,7 +20,7 @@ namespace Sundew.Base.Identification; /// /// The metadata. /// The value. -public sealed partial record ValueId(string? Metadata, IValue Value) +public sealed record ValueId(string? Metadata, IValue Value) { /// /// Gets the type of the source. From b09393b7fb1963b427596b4d99285d29e934fd04 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Fri, 17 Apr 2026 01:24:13 +0200 Subject: [PATCH 09/20] Added literal support for specifying null and instance ids --- .../Identification/IdTests.cs | 62 ++++++++++++++----- .../Identification/RevisionIdTests.cs | 1 + .../ExpressionEvaluator.cs | 15 ++++- Source/Sundew.Base.Identification/Id.cs | 14 +++-- .../Sundew.Base.Identification/InstanceId.cs | 2 +- .../LiteralValue.cs | 37 +++++++++++ .../Parsing/Grammar.cs | 7 ++- .../Parsing/IParserError.cs | 10 ++- .../Parsing/IdRouteParser.cs | 52 ++++++++++++---- .../Parsing/Tokens.cs | 2 + .../SequenceIdExtensions.cs | 4 +- .../ValueIdBuilder.cs | 14 ++++- 12 files changed, 175 insertions(+), 45 deletions(-) create mode 100644 Source/Sundew.Base.Identification/LiteralValue.cs diff --git a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs index 00eb16e..fc66ec2 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -18,7 +18,8 @@ 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))"; + 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); @@ -29,7 +30,8 @@ public void ToUri_Then_ResultShouldBeExpectedResult() [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))"; + 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"); @@ -81,7 +83,8 @@ public void Parse_Then_ResultShouldNotBeNull(string input) [Test] public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldBeExpected() { - const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/GoBack()"; + 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()) @@ -99,7 +102,8 @@ public void From_When_TargetIsMethodWith0Parameters_Then_ResultShouldBeExpected( [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)"; + 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()) @@ -116,7 +120,8 @@ public void From_When_TargetIsMethodWith1ParameterAsNull_Then_ResultShouldBeExpe [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))"; + 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()) @@ -133,7 +138,8 @@ public void From_When_TargetIsMethodWith1Parameter_Then_ResultShouldBeExpected() [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)"; + 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()) @@ -150,7 +156,8 @@ public void From_When_TargetIsMethodWith2Parameters_Then_ResultShouldNotBeNull() [Test] public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() { - const string expectedResult = "IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/X"; + 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()) @@ -167,7 +174,8 @@ public void From_When_TargetIsProperty_Then_ResultShouldNotBeNull() [Test] public void ToValue_Then_ResultShouldBeExpectedResult() { - const string expectedResult = "!IdTests+Position~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests=(X=4&Y=5)"; + 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; @@ -184,7 +192,8 @@ public void ToValue_Then_ResultShouldBeExpectedResult() [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)"; + 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; @@ -198,10 +207,29 @@ public void ToValue_When_UsingNestedType_Then_ResultShouldBeExpectedResult() } } + [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))"; + 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()) @@ -218,7 +246,8 @@ public void From_When_TargetIsMethodWith1Parameters_Then_ResultShouldBeExpected( [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)"; + 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()) @@ -236,7 +265,7 @@ public void From_When_TargetIsPropertyAndPassingArgument_Then_ResultShouldBeExpe [Test] public void From_When_TargetIsPropertyAndPassingArgumentByReferenceId_Then_ResultShouldBeExpected() { - const string expectedResult = "IdTests+INavigator~Sundew.Base.Development.Tests.Identification$Sundew.Base.Development.Tests/Description?%3A1"; + 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()) @@ -254,8 +283,8 @@ public void From_When_TargetIsPropertyAndPassingArgumentByReferenceId_Then_Resul [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))"; - var result = Id.From(x => x.Search(new Query("with ( ) { } / , : ! $ ~ = < > & [ ] %"))); + 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()) { @@ -302,6 +331,11 @@ public record Position(int X, int Y) : IValueIdentifiable public static Position From(Position position, ValueId valueId, IFormatProvider? formatProvider) { + if (!position.HasValue) + { + return position; + } + return new Position( valueId.GetScalar(position.X, formatProvider), valueId.GetScalar(position.Y, formatProvider)); diff --git a/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs index 814ce45..d946e4f 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/RevisionIdTests.cs @@ -10,6 +10,7 @@ namespace Sundew.Base.Development.Tests.Identification; using AwesomeAssertions; using Sundew.Base.Identification; +[NotInParallel] public class RevisionIdTests { [Test] diff --git a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs index 2eaa302..267cdd2 100644 --- a/Source/Sundew.Base.Identification/ExpressionEvaluator.cs +++ b/Source/Sundew.Base.Identification/ExpressionEvaluator.cs @@ -86,7 +86,8 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo switch (argument) { case ConstantExpression constantExpression: - builder.Add(new Argument(parameterInfo.Name, new ValueId(GetMetadata(argument.Type), new ScalarValue(constantExpression.Value?.ToString() ?? (argument.Type.IsClass ? "null" : "default"))))); + 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) @@ -95,13 +96,13 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo if (memberExpression.Member is FieldInfo fieldInfo) { var value = fieldInfo.GetValue(container); - builder.Add(new Argument(fieldInfo.Name, new ValueId(GetMetadata(fieldInfo.FieldType), new ScalarValue(value?.ToString() ?? string.Empty)))); + 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), new ScalarValue(value?.ToString() ?? string.Empty)))); + builder.Add(new Argument(propertyInfo.Name, new ValueId(GetMetadata(propertyInfo.PropertyType), GetValueIdValue(value)))); } } @@ -122,6 +123,14 @@ private static void GetArgument(Expression argument, ParameterInfo parameterInfo } } + 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(); diff --git a/Source/Sundew.Base.Identification/Id.cs b/Source/Sundew.Base.Identification/Id.cs index d21f582..716e19b 100644 --- a/Source/Sundew.Base.Identification/Id.cs +++ b/Source/Sundew.Base.Identification/Id.cs @@ -18,7 +18,7 @@ namespace Sundew.Base.Identification; /// /// Represents any Id. /// -public sealed record Id(Source Source, Path? Path, Arguments? Arguments = null) : IParsable +public sealed record Id(Source Source, Path? Path, Arguments? Arguments = null, string? Fragment = null) : IParsable { /// /// Creates an Uri from this . @@ -155,6 +155,12 @@ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvid stringBuilder.Append(Grammar.ArgumentsSeparator); this.Arguments.AppendInto(stringBuilder, formatProvider, new AppendOptions(true)); } + + if (this.Fragment.HasValue) + { + stringBuilder.Append(Grammar.LiteralSeparator); + stringBuilder.Append(this.Fragment); + } } /// @@ -254,8 +260,8 @@ public static Id From(Expression> targetExpressio /// A new . public static Id From(Expression> targetExpression, IIdentifiable value) { - var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, new ValueId(null, new ScalarValue(value.Id.ToString()))); - return new Id(source, path, valueId); + var (source, path, valueId) = ExpressionEvaluator.From(targetExpression, new ValueId(null, new LiteralValue(value.Id.Number.ToString()))); + return new Id(source, path, valueId, value.Id.Number.ToString()); } /// @@ -267,7 +273,7 @@ public static Id From(Expression> targetExpression, IId /// A new . public static Id From(Expression> targetExpression, IIdentifiable value) { - var target = ExpressionEvaluator.From(targetExpression, new ValueId(null, new ScalarValue(value.Id.ToString()))); + var target = ExpressionEvaluator.From(targetExpression, new ValueId(null, new LiteralValue(value.Id.Number.ToString()))); return new Id(target.Source, target.Path, target.Arguments); } diff --git a/Source/Sundew.Base.Identification/InstanceId.cs b/Source/Sundew.Base.Identification/InstanceId.cs index babe9bd..d75d610 100644 --- a/Source/Sundew.Base.Identification/InstanceId.cs +++ b/Source/Sundew.Base.Identification/InstanceId.cs @@ -29,6 +29,6 @@ public static InstanceId Create(uint number) /// The id as a string. public override string ToString() { - return ':' + this.Number.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 index b5c0cec..7e4ed19 100644 --- a/Source/Sundew.Base.Identification/Parsing/Grammar.cs +++ b/Source/Sundew.Base.Identification/Parsing/Grammar.cs @@ -15,12 +15,15 @@ internal static class Grammar /// The Source path/origin separator. public const char SourcePathOriginSeparator = '$'; - /// The path segment. + /// The path segment separator. public const char PathSegmentSeparator = '/'; - /// The value ids separator. + /// The arguments separator. public const char ArgumentsSeparator = '?'; + /// The literal separator. + public const char LiteralSeparator = '^'; + /// Metadata separator. public const char NameMetadataSeparator = '!'; diff --git a/Source/Sundew.Base.Identification/Parsing/IParserError.cs b/Source/Sundew.Base.Identification/Parsing/IParserError.cs index 046f652..f37bb5a 100644 --- a/Source/Sundew.Base.Identification/Parsing/IParserError.cs +++ b/Source/Sundew.Base.Identification/Parsing/IParserError.cs @@ -97,9 +97,13 @@ public partial interface IValueIdError : IParserError [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 ArrayError(char Expected, LexerError Error) : IValueError;*/ + + public sealed partial record ArgumentsError(IArgumentsError Error) : IValueError; public sealed partial record ValueIdError(IValueIdError Error) : IValueError; @@ -116,7 +120,7 @@ public sealed partial record ValueIdValueError(IValueError Error) : IValueIdErro public sealed partial record NotAtEndError() : IIdError, IIdRouteError, IParseValueIdError; /// -/// Represents an error when an Id is empty or null. +/// Represents an error when the input is empty or null. /// public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError, IParseValueIdError; @@ -125,5 +129,5 @@ public sealed partial record EmptyOrNullError() : IIdError, IIdRouteError, IPars /// /// The cause. /// The lexer error. -public sealed partial record LexError(object Cause, LexerError LexerError) : IArgumentsError, IValueIdError; +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 index a994a2b..5e437f9 100644 --- a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -15,8 +15,8 @@ namespace Sundew.Base.Identification.Parsing; internal static class IdRouteParser { - private static readonly Lexer IdLexer; private static readonly Lexer IdRouteLexer; + private static readonly Lexer IdLexer; static IdRouteParser() { @@ -26,7 +26,8 @@ static IdRouteParser() 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 valueIdValueLexerRule = new RegexLexerRule(Tokens.ValueIdValue, new Regex(@"[^),\]&\#]+", RegexOptions.Compiled)); + var fragmentLexerRule = new RegexLexerRule(Tokens.Fragment, new Regex(@"[^),\]&]+", RegexOptions.Compiled)); IdRouteLexer = new Lexer( [sourceNameLexerRule, sourcePathLexerRule, @@ -34,7 +35,8 @@ static IdRouteParser() segmentNameLexerRule, argumentNameLexerRule, valueIdMetadataLexerRule, - valueIdValueLexerRule]); + valueIdValueLexerRule, + fragmentLexerRule]); IdLexer = new Lexer( [sourceNameLexerRule, sourcePathLexerRule, @@ -42,7 +44,8 @@ static IdRouteParser() segmentNameLexerRule, argumentNameLexerRule, valueIdMetadataLexerRule, - valueIdValueLexerRule]); + valueIdValueLexerRule, + fragmentLexerRule]); } public static R ParseIdRoute(string? input, IFormatProvider? formatProvider) @@ -151,7 +154,21 @@ private static R Id(Parser parser) } } - return R.Success(new Id(sourceResult.Value, path, arguments)); + string? fragment = null; + if (parser.TryAccept(Grammar.LiteralSeparator)) + { + var valueIdsResult = Arguments(parser); + if (valueIdsResult.IsSuccess) + { + arguments = 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) @@ -249,7 +266,7 @@ private static R Argument(Parser parser) argumentResult = parser.TryAccept( Grammar.KeyValueSeparator, - result => result.MapError(x => IArgumentsError.LexError(Grammar.KeyValueSeparator, x))) + 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) @@ -277,7 +294,7 @@ private static R ValueId(Parser parser) { var metadataResult = parser.TryAccept( Grammar.NameMetadataSeparator, - result => result.MapError(lexerError => IValueIdError.LexError(Grammar.NameMetadataSeparator, lexerError)) + 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)) @@ -297,10 +314,9 @@ private static R Value(Parser parser) Grammar.GroupStart, result => { - return result.MapError(lexerError => IValueError._GroupError(Grammar.GroupStart, lexerError)) - .And(() => Arguments(parser).MapError(x => IValueError._ValueError(null))) - .And(() => parser.Accept(Grammar.GroupEnd) - .Map(lexerError => IValueError._GroupError(Grammar.GroupEnd, lexerError))) + 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) @@ -312,7 +328,7 @@ private static R Value(Parser parser) Grammar.ArrayStart, result => { - return result.MapError(lexerError => IValueError._ArrayError(Grammar.ArrayStart, lexerError)) + return result.MapError(lexerError => IValueError.ExpectedCharacterError(Grammar.ArrayStart, lexerError)) .And(() => { var valueIds = ImmutableArray.CreateBuilder(); @@ -336,7 +352,7 @@ private static R Value(Parser parser) return R.Success(new ArrayValue(valueIds.ToValueArray())).Omits(); }) .And(() => parser.Accept(Grammar.ArrayEnd) - .Map(lexerError => IValueError._ArrayError(Grammar.ArrayEnd, lexerError))) + .Map(lexerError => IValueError.ExpectedCharacterError(Grammar.ArrayEnd, lexerError))) .Map(x => x.Value2); }); if (valueResult.IsSuccess) @@ -344,6 +360,16 @@ private static R Value(Parser parser) 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 index f3ac81a..7465c17 100644 --- a/Source/Sundew.Base.Identification/Parsing/Tokens.cs +++ b/Source/Sundew.Base.Identification/Parsing/Tokens.cs @@ -22,4 +22,6 @@ internal enum Tokens ValueIdMetadata, ValueIdValue, + + Fragment, } diff --git a/Source/Sundew.Base.Identification/SequenceIdExtensions.cs b/Source/Sundew.Base.Identification/SequenceIdExtensions.cs index 81fc6cd..6b26e82 100644 --- a/Source/Sundew.Base.Identification/SequenceIdExtensions.cs +++ b/Source/Sundew.Base.Identification/SequenceIdExtensions.cs @@ -7,8 +7,6 @@ namespace Sundew.Base.Identification; -using System; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; @@ -17,7 +15,7 @@ namespace Sundew.Base.Identification; /// public static class SequenceIdExtensions { - extension(ISequenceId id) + extension(TId id) where TId : ISequenceId { /// diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs index 04f5958..2209204 100644 --- a/Source/Sundew.Base.Identification/ValueIdBuilder.cs +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs @@ -47,7 +47,12 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo if (value != null && value is IValueIdentifiable valueIdentifiable) { var valueId = valueIdentifiable.Id; - this.values.Add(new Argument(name, new ValueId(GetMetadata(value.GetType(), typeof(TValue), false), valueId.Value))); + 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) { @@ -57,6 +62,10 @@ public ValueIdBuilder Add(TValue? value, [CallerArgumentExpression(nameo 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; } @@ -69,9 +78,10 @@ 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 ScalarValue("null")), + Empty empty => new ValueId(metadata, new LiteralValue(@null)), Multiple valueIds => new ValueId(metadata, new ComplexValue(valueIds.Items.ToValueArray())), Single single => single.Item.ValueId, }; From b0057c20b599ece202c36fc1a2154f33081d0a61 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Fri, 17 Apr 2026 04:22:15 +0200 Subject: [PATCH 10/20] Null literal improvements --- .../Identification/IdTests.cs | 5 ----- Source/Sundew.Base.Identification/ValueId.cs | 10 ++++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs index fc66ec2..59530c8 100644 --- a/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs +++ b/Source/Sundew.Base.Development.Tests/Identification/IdTests.cs @@ -331,11 +331,6 @@ public record Position(int X, int Y) : IValueIdentifiable public static Position From(Position position, ValueId valueId, IFormatProvider? formatProvider) { - if (!position.HasValue) - { - return position; - } - return new Position( valueId.GetScalar(position.X, formatProvider), valueId.GetScalar(position.Y, formatProvider)); diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index c8d50b1..c308b8c 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -210,6 +210,11 @@ public TValue GetValue(TValue defaultValue, IFormatProvider? formatProvi 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); } @@ -220,6 +225,11 @@ public TValue GetValue(TValue defaultValue, IFormatProvider? formatProvi 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); } From 47a1c63b70d8f311c2d192b15f493b7eae969a1c Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Sat, 2 May 2026 19:23:08 +0200 Subject: [PATCH 11/20] Fix flasky test --- .../Threading/ManualResetEventAsyncTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs b/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs index 8c35407..56bac4f 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(); From d6ac42d7130b00feb2693e62fa3a7503934ff97c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 17:39:02 +0000 Subject: [PATCH 12/20] Fix GetHashCode consistency for ValueList, ValueArray, ValueDictionary when Count == 0 Equal instances (default and Empty) now always return the same hash code (0) when Count == 0, fixing the Equals/GetHashCode contract violation that broke hash-based collections. Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/d77bd4d4-f942-4427-be6b-c50f0d860b48 Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- .../Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs | 6 +++--- .../ValueDictionary{TKey,TValue}.cs | 6 +++--- .../Sundew.Base.Collections.Immutable/ValueList{TItem}.cs | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs b/Source/Sundew.Base.Collections.Immutable/ValueArray{TItem}.cs index 3110842..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!); } /// diff --git a/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs b/Source/Sundew.Base.Collections.Immutable/ValueDictionary{TKey,TValue}.cs index ae70ba3..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 } diff --git a/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs b/Source/Sundew.Base.Collections.Immutable/ValueList{TItem}.cs index 8faf200..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 } From 95b0345eca217adb931e424b03314416c1b22a0a Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Sat, 2 May 2026 19:42:47 +0200 Subject: [PATCH 13/20] Improve Id fragment handling --- .../Threading/ManualResetEventAsyncTests.cs | 4 ++-- Source/Sundew.Base.Identification/Id.cs | 8 ++++---- Source/Sundew.Base.Identification/Parsing/Grammar.cs | 3 +++ .../Sundew.Base.Identification/Parsing/IdRouteParser.cs | 6 +++--- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs b/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs index 56bac4f..de1f79c 100644 --- a/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs +++ b/Source/Sundew.Base.Development.Tests/Threading/ManualResetEventAsyncTests.cs @@ -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/Id.cs b/Source/Sundew.Base.Identification/Id.cs index 716e19b..955d1a8 100644 --- a/Source/Sundew.Base.Identification/Id.cs +++ b/Source/Sundew.Base.Identification/Id.cs @@ -18,7 +18,7 @@ namespace Sundew.Base.Identification; /// /// Represents any Id. /// -public sealed record Id(Source Source, Path? Path, Arguments? Arguments = null, string? Fragment = null) : IParsable +public sealed record Id(Source Source, Path? Path, Arguments? Arguments = null, Arguments? Fragment = null) : IParsable { /// /// Creates an Uri from this . @@ -158,8 +158,8 @@ public void AppendInto(StringBuilder stringBuilder, IFormatProvider formatProvid if (this.Fragment.HasValue) { - stringBuilder.Append(Grammar.LiteralSeparator); - stringBuilder.Append(this.Fragment); + stringBuilder.Append(Grammar.FragmentSeparator); + this.Fragment.AppendInto(stringBuilder, formatProvider, new AppendOptions(true)); } } @@ -261,7 +261,7 @@ public static Id From(Expression> targetExpressio 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, value.Id.Number.ToString()); + return new Id(source, path, valueId, null); } /// diff --git a/Source/Sundew.Base.Identification/Parsing/Grammar.cs b/Source/Sundew.Base.Identification/Parsing/Grammar.cs index 7e4ed19..f701a68 100644 --- a/Source/Sundew.Base.Identification/Parsing/Grammar.cs +++ b/Source/Sundew.Base.Identification/Parsing/Grammar.cs @@ -24,6 +24,9 @@ internal static class Grammar /// The literal separator. public const char LiteralSeparator = '^'; + /// The fragment separator. + public const char FragmentSeparator = '#'; + /// Metadata separator. public const char NameMetadataSeparator = '!'; diff --git a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs index 5e437f9..0f55ce0 100644 --- a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -154,13 +154,13 @@ private static R Id(Parser parser) } } - string? fragment = null; - if (parser.TryAccept(Grammar.LiteralSeparator)) + Arguments? fragment = null; + if (parser.TryAccept(Grammar.FragmentSeparator)) { var valueIdsResult = Arguments(parser); if (valueIdsResult.IsSuccess) { - arguments = valueIdsResult.Value; + fragment = valueIdsResult.Value; } else { From 030e9c94586a6b2fb9b88886101ecaf54d2cb814 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 17:47:59 +0000 Subject: [PATCH 14/20] Fix Parser.Undo() to prevent popping initial state; fix GetInputTypes to fall back to member signature for primitive arguments - Parser.Undo(): now returns false and leaves stack intact when only the initial state remains, preventing InvalidOperationException on subsequent Peek() calls - TargetEvaluator.GetInputTypes(): when argument metadata-based type resolution fails (e.g. for primitives without metadata), falls back to the target method's parameter types from the member signature Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/4ed14592-e3bd-41bb-ad4d-b9f6fcdf2e89 Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- .../TargetEvaluator.cs | 29 ++++++++++++++++++- Source/Sundew.Base.Parsing/Parser.cs | 8 ++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Source/Sundew.Base.Identification/TargetEvaluator.cs b/Source/Sundew.Base.Identification/TargetEvaluator.cs index 1f2e317..eb17635 100644 --- a/Source/Sundew.Base.Identification/TargetEvaluator.cs +++ b/Source/Sundew.Base.Identification/TargetEvaluator.cs @@ -78,7 +78,34 @@ public static R> GetInputTypes(Source source, Path? path, Ar if (arguments.HasValue) { - return arguments.Items.Select(x => x.ValueId.TryGetType()).AllOrFailed(x => x.ToItem()).Map(x => (IReadOnlyList)x.Items); + 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) diff --git a/Source/Sundew.Base.Parsing/Parser.cs b/Source/Sundew.Base.Parsing/Parser.cs index 6e69422..2964e41 100644 --- a/Source/Sundew.Base.Parsing/Parser.cs +++ b/Source/Sundew.Base.Parsing/Parser.cs @@ -281,7 +281,13 @@ public RoE IsEnd() /// A value indicating whether the undo was successful. public bool Undo() { - return this.stateStack.TryPop(out var _); + if (this.stateStack.Count > 1) + { + this.stateStack.Pop(); + return true; + } + + return false; } /// From f0435edd8120274fd9b05a3eaadf5aea2b7c444d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 17:54:58 +0000 Subject: [PATCH 15/20] Fix GetScalar to use raw ScalarValue.Value instead of escaped ToString() ScalarValue.ToString() returns Uri.EscapeDataString output (wire format). Using it in TValue.Parse() caused round-trip failures for strings with reserved characters. Now pattern-matching on ScalarValue to access the raw Value property directly. Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/5cbb2907-948b-43e1-8b28-653475331fdb Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- Source/Sundew.Base.Identification/ValueId.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index c308b8c..9a9d4ca 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -170,7 +170,8 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProv var argument = complexValue.Items.FirstOrDefault(x => x.Name == referenceName); if (argument.HasValue) { - return TValue.Parse(argument.ValueId.Value.ToString() ?? string.Empty, formatProvider); + var rawValue = argument.ValueId.Value is ScalarValue scalarValue ? scalarValue.Value : argument.ValueId.Value.ToString() ?? string.Empty; + return TValue.Parse(rawValue, formatProvider); } var firstDotIndex = referenceName.IndexOf('.'); @@ -180,7 +181,8 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProv argument = complexValue.Items.FirstOrDefault(x => x.Name == fallback); if (argument.HasValue) { - return TValue.Parse(argument.ValueId.Value.ToString() ?? string.Empty, formatProvider); + var rawValue = argument.ValueId.Value is ScalarValue scalarValue ? scalarValue.Value : argument.ValueId.Value.ToString() ?? string.Empty; + return TValue.Parse(rawValue, formatProvider); } return defaultValue; From cee067e2cc1cb252a81b2711a76ac41ca5a2d231 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 17:58:45 +0000 Subject: [PATCH 16/20] Refactor GetScalar: extract raw-value helper to avoid duplication Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/5cbb2907-948b-43e1-8b28-653475331fdb Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- Source/Sundew.Base.Identification/ValueId.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Source/Sundew.Base.Identification/ValueId.cs b/Source/Sundew.Base.Identification/ValueId.cs index 9a9d4ca..35a693e 100644 --- a/Source/Sundew.Base.Identification/ValueId.cs +++ b/Source/Sundew.Base.Identification/ValueId.cs @@ -167,11 +167,13 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProv 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) { - var rawValue = argument.ValueId.Value is ScalarValue scalarValue ? scalarValue.Value : argument.ValueId.Value.ToString() ?? string.Empty; - return TValue.Parse(rawValue, formatProvider); + return TValue.Parse(GetRawString(argument.ValueId.Value), formatProvider); } var firstDotIndex = referenceName.IndexOf('.'); @@ -181,8 +183,7 @@ public TValue GetScalar(TValue defaultValue, IFormatProvider? formatProv argument = complexValue.Items.FirstOrDefault(x => x.Name == fallback); if (argument.HasValue) { - var rawValue = argument.ValueId.Value is ScalarValue scalarValue ? scalarValue.Value : argument.ValueId.Value.ToString() ?? string.Empty; - return TValue.Parse(rawValue, formatProvider); + return TValue.Parse(GetRawString(argument.ValueId.Value), formatProvider); } return defaultValue; From c4f8bc9caa3231434cfb4ba34df51e35f0d430ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:09:13 +0000 Subject: [PATCH 17/20] Fix RegexLexerRule to reject matches not starting exactly at state.Position Regex.Match(input, startat) can return a match starting after startat. Added match.Index == state.Position check to ensure only matches at the exact current position are accepted. Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/28a3fb6e-d0d3-4a43-a06b-da8e4615a660 Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- Source/Sundew.Base.Parsing/RegexLexerRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Sundew.Base.Parsing/RegexLexerRule.cs b/Source/Sundew.Base.Parsing/RegexLexerRule.cs index a6c3f35..af78175 100644 --- a/Source/Sundew.Base.Parsing/RegexLexerRule.cs +++ b/Source/Sundew.Base.Parsing/RegexLexerRule.cs @@ -37,7 +37,7 @@ public class RegexLexerRule(TToken token, Regex regex) : ILexerRule TryGetLexeme(string input, Parser.State state) { var match = regex.Match(input, state.Position); - if (match.Success) + if (match.Success && match.Index == state.Position) { if (match.Groups.TryGetValue(TokenGroupName, out var matchingGroup)) { From a2d7da5f3879b82837d3f8e4d4af84774ce77459 Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Sat, 2 May 2026 20:10:25 +0200 Subject: [PATCH 18/20] Fixed result extension And returned incorrect result --- Source/Sundew.Base.Primitives/ResultExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Sundew.Base.Primitives/ResultExtensions.cs b/Source/Sundew.Base.Primitives/ResultExtensions.cs index a729b3f..8d552d3 100644 --- a/Source/Sundew.Base.Primitives/ResultExtensions.cs +++ b/Source/Sundew.Base.Primitives/ResultExtensions.cs @@ -166,7 +166,7 @@ public static R And(this R return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(false, default, otherResult.Error); } - return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(true, default, result.Error); + return new R<(TSuccess1, TSuccess2, TSuccess3, TSuccess4), TError>(false, default, result.Error); } #pragma warning restore SA1101 } \ No newline at end of file From 3e995cdc61e405e282d7d34f5c9626835248e511 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 18:16:08 +0000 Subject: [PATCH 19/20] Fix IdRouteParser array/route loop conditions and ValueIdBuilder single-item case - Fix array parsing loop: use IsNext(ArrayEnd) instead of IsNext(GroupStart) so [Blue,Green] style arrays correctly terminate at ']' - Fix IdRoute do-while loop: use TryAccept(IdSeparator) without negation so a single-id route with no trailing '>' exits cleanly at end-of-input - Fix ValueIdBuilder.Build() Single case: wrap in ComplexValue with metadata instead of returning single.Item.ValueId bare, preserving round-trip identity Agent-Logs-Url: https://github.com/sundews/Sundew.Base/sessions/605f58c5-a131-42fb-af81-b359b3a1aa79 Co-authored-by: hugener <5023998+hugener@users.noreply.github.com> --- Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs | 4 ++-- Source/Sundew.Base.Identification/ValueIdBuilder.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs index 0f55ce0..febcbb9 100644 --- a/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs +++ b/Source/Sundew.Base.Identification/Parsing/IdRouteParser.cs @@ -114,7 +114,7 @@ private static R IdRoute(Parser parser) return R.Error(IIdRouteError._IdRouteIdError(idResult.Error)); } } - while (!parser.TryAccept(Grammar.IdSeparator)); + while (parser.TryAccept(Grammar.IdSeparator)); return R.Success(new IdRoute(builder.ToValueList())); } @@ -332,7 +332,7 @@ private static R Value(Parser parser) .And(() => { var valueIds = ImmutableArray.CreateBuilder(); - while (!parser.IsNext(Grammar.GroupStart)) + while (!parser.IsNext(Grammar.ArrayEnd)) { var singleValueIdResult = ValueId(parser); if (singleValueIdResult.IsSuccess) diff --git a/Source/Sundew.Base.Identification/ValueIdBuilder.cs b/Source/Sundew.Base.Identification/ValueIdBuilder.cs index 2209204..8577cb8 100644 --- a/Source/Sundew.Base.Identification/ValueIdBuilder.cs +++ b/Source/Sundew.Base.Identification/ValueIdBuilder.cs @@ -83,7 +83,7 @@ public ValueId Build() { Empty empty => new ValueId(metadata, new LiteralValue(@null)), Multiple valueIds => new ValueId(metadata, new ComplexValue(valueIds.Items.ToValueArray())), - Single single => single.Item.ValueId, + Single single => new ValueId(metadata, new ComplexValue(new[] { single.Item }.ToValueArray())), }; } From 24b80be5c987a56afdd05777fe3cd3ab6d39dc8e Mon Sep 17 00:00:00 2001 From: Kim Hugener-Ohlsen Date: Sat, 2 May 2026 20:25:13 +0200 Subject: [PATCH 20/20] Improve test flakyness --- Source/Sundew.Base.Development.Tests/Primitives/QuestTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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);