diff --git a/.gitattributes b/.gitattributes index 21f83a7..cd1f4e4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,5 +12,9 @@ *.fsproj *.dbproj +# Verify snapshot files +*.verified.txt text eol=lf working-tree-encoding=UTF-8 +*.verified.cs text eol=lf working-tree-encoding=UTF-8 + # Don't check for trailing whitespace at end of lines in the doc pages *.md -whitespace=blank-at-eol diff --git a/.gitignore b/.gitignore index 0f0f4e1..c79b456 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ # SourceGenerator Launch Settings src/CompositeKey.SourceGeneration/Properties/launchSettings.json +# Verify snapshot testing +*.received.* + # Claude Code .claude/settings.local.json .claude/todos/ diff --git a/CLAUDE.md b/CLAUDE.md index e7b3551..0fcb78e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,6 +63,14 @@ The solution is split into four main projects plus corresponding test projects: - Commit messages follow Conventional Commits (`feat:`, `fix:`, `perf:`, etc.) — GitVersion and `.versionrc` drive versioning and changelog - EditorConfig: 4-space indent, UTF-8, LF line endings, 120-char max line length -- Test stack: xunit + Shouldly + AutoFixture +- Test stack: xunit + Shouldly + AutoFixture + Verify (snapshot testing) - Unit tests use `CompilationHelper` to create in-memory Roslyn compilations - Functional tests define real key types and test format/parse round-trips +- Snapshot tests (`Snapshots/` in `SourceGeneration.UnitTests`) use + Verify.SourceGenerators to capture generated `.g.cs` output and + `GenerationSpec` models as `.verified.*` baselines — any change to + emitted code or parser output will cause a snapshot diff failure +- `*.received.*` files are gitignored; only `*.verified.*` baselines + are committed +- The `ModuleInitializer` scrubs the non-deterministic + `GeneratedCodeAttribute` version string to keep snapshots stable diff --git a/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj b/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj index 188c1da..89a3a22 100644 --- a/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj +++ b/src/CompositeKey.SourceGeneration.UnitTests/CompositeKey.SourceGeneration.UnitTests.csproj @@ -15,6 +15,8 @@ + + diff --git a/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs b/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs new file mode 100644 index 0000000..36c33c8 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs @@ -0,0 +1,24 @@ +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using VerifyXunit; + +namespace CompositeKey.SourceGeneration.UnitTests; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() + { + VerifySourceGenerators.Initialize(); + + // Scrub the non-deterministic informational version (includes git commit hash) + // from the GeneratedCodeAttribute emitted by the source generator. + // e.g. [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "1.6.0+7675481fa523...")] + // -> [GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + VerifierSettings.ScrubLinesWithReplace(line => + Regex.Replace( + line, + @"GeneratedCodeAttribute\(""CompositeKey\.SourceGeneration"", ""[^""]*""\)", + @"GeneratedCodeAttribute(""CompositeKey.SourceGeneration"", ""VERSION"")")); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt new file mode 100644 index 0000000..b755a27 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt @@ -0,0 +1,298 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 0 + }, + { + Name: Two, + Value: 1 + }, + { + Name: Three, + Value: 2 + } + ], + IsSequentialFromZero: true + } + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + AllParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 0 + }, + { + Name: Two, + Value: 1 + }, + { + Name: Three, + Value: 2 + } + ], + IsSequentialFromZero: true + } + }, + Format: g, + ParseType: Enum, + FormatType: Enum, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PartitionKeyParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PrimaryDelimiterKeyPart: { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + SortKeyParts: [ + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 0 + }, + { + Name: Two, + Value: 1 + }, + { + Name: Three, + Value: 2 + } + ], + IsSequentialFromZero: true + } + }, + Format: g, + ParseType: Enum, + FormatType: Enum, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..a26951a --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,311 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : ICompositePrimaryKey + { + public override string ToString() + { + return string.Create(54 + SecondPart.Length + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + state.SecondPart.CopyTo(destination[position..]); + position += state.SecondPart.Length; + + destination[position] = '|'; + position += 1; + + "ConstantValue".CopyTo(destination[position..]); + position += 13; + + destination[position] = '#'; + position += 1; + + { + if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(39 + SecondPart.Length, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + state.SecondPart.CopyTo(destination[position..]); + position += state.SecondPart.Length; + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#{SecondPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public string ToSortKeyString() + { + return string.Create(14 + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) => + { + int position = 0; + + "ConstantValue".CopyTo(destination[position..]); + position += 13; + + destination[position] = '#'; + position += 1; + + { + if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:g}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result); + } + + public static BasicPrimaryKey Parse(string partitionKey, string sortKey) + { + ArgumentNullException.ThrowIfNull(partitionKey); + ArgumentNullException.ThrowIfNull(sortKey); + + return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey) + { + if (partitionKey.Length < 40) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length < 15) + throw new FormatException("Unrecognized format."); + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + throw new FormatException("Unrecognized format."); + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (partitionKey is null || sortKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result); + } + + public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (partitionKey.Length < 40) + return false; + + if (sortKey.Length < 15) + return false; + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + return false; + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + return false; + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + return false; + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + return false; + + if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } + file class CustomEnumHelper + { + private static readonly int[] Lengths = new[] { 3, 3, 5 }; + private static readonly string[] Names = new[] { "One", "Two", "Three" }; + + public static int GetFormattedLength(global::UnitTests.CustomEnum value) + { + return Lengths[(uint)value]; + } + + public static bool TryFormat(global::UnitTests.CustomEnum value, Span destination, out int charsWritten) + { + charsWritten = 0; + if ((uint)value >= Names.Length) + return false; + + int formattedLength = Lengths[(uint)value]; + if (destination.Length < formattedLength) + return false; + + charsWritten = formattedLength; + Names[(uint)value].CopyTo(destination); + return true; + } + + public static bool TryParse(in ReadOnlySpan value, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out global::UnitTests.CustomEnum result) + { + switch (value) + { + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.One), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.One; + return true; + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Two), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.Two; + return true; + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Three), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.Three; + return true; + default: + result = default; + return false; + } + } + + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt new file mode 100644 index 0000000..40d6385 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt @@ -0,0 +1,298 @@ +[ + { + TargetType: { + Type: { + Name: BasicNonSequentialEnum, + FullyQualifiedName: global::UnitTests.BasicNonSequentialEnum + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicNonSequentialEnum + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 3 + }, + { + Name: Two, + Value: 2 + }, + { + Name: Three, + Value: 1 + } + ], + IsSequentialFromZero: false + } + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicNonSequentialEnum + }, + Key: { + AllParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 3 + }, + { + Name: Two, + Value: 2 + }, + { + Name: Three, + Value: 1 + } + ], + IsSequentialFromZero: false + } + }, + Format: g, + ParseType: Enum, + FormatType: Enum, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PartitionKeyParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PrimaryDelimiterKeyPart: { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + SortKeyParts: [ + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true, + EnumSpec: { + Name: CustomEnum, + FullyQualifiedName: global::UnitTests.CustomEnum, + UnderlyingType: int, + Members: [ + { + Name: One, + Value: 3 + }, + { + Name: Two, + Value: 2 + }, + { + Name: Three, + Value: 1 + } + ], + IsSequentialFromZero: false + } + }, + Format: g, + ParseType: Enum, + FormatType: Enum, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs new file mode 100644 index 0000000..02ae103 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs @@ -0,0 +1,322 @@ +//HintName: UnitTests.BasicNonSequentialEnum.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicNonSequentialEnum : ICompositePrimaryKey + { + public override string ToString() + { + return string.Create(54 + SecondPart.Length + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + state.SecondPart.CopyTo(destination[position..]); + position += state.SecondPart.Length; + + destination[position] = '|'; + position += 1; + + "ConstantValue".CopyTo(destination[position..]); + position += 13; + + destination[position] = '#'; + position += 1; + + { + if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(39 + SecondPart.Length, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "b", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + state.SecondPart.CopyTo(destination[position..]); + position += state.SecondPart.Length; + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:b}#{SecondPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public string ToSortKeyString() + { + return string.Create(14 + CustomEnumHelper.GetFormattedLength(ThirdPart), this, static (destination, state) => + { + int position = 0; + + "ConstantValue".CopyTo(destination[position..]); + position += 13; + + destination[position] = '#'; + position += 1; + + { + if (!CustomEnumHelper.TryFormat(state.ThirdPart, destination[position..], out int thirdPartCharsWritten)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:g}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicNonSequentialEnum Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicNonSequentialEnum Parse(ReadOnlySpan primaryKey) + { + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result) + { + result = null; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result); + } + + public static BasicNonSequentialEnum Parse(string partitionKey, string sortKey) + { + ArgumentNullException.ThrowIfNull(partitionKey); + ArgumentNullException.ThrowIfNull(sortKey); + + return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey); + } + + public static BasicNonSequentialEnum Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey) + { + if (partitionKey.Length < 40) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length < 15) + throw new FormatException("Unrecognized format."); + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + throw new FormatException("Unrecognized format."); + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicNonSequentialEnum(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result) + { + if (partitionKey is null || sortKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result); + } + + public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum? result) + { + result = null; + + if (partitionKey.Length < 40) + return false; + + if (sortKey.Length < 15) + return false; + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + return false; + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + return false; + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + return false; + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + return false; + + if (!CustomEnumHelper.TryParse(sortKey[sortKeyPartRanges[1]], out var thirdPart)) + return false; + + result = new BasicNonSequentialEnum(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicNonSequentialEnum IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum result) => TryParse(s, out result); + + /// + static BasicNonSequentialEnum ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicNonSequentialEnum result) => TryParse(s, out result); + } + file class CustomEnumHelper + { + public static int GetFormattedLength(global::UnitTests.CustomEnum value) + { + return value switch + { + global::UnitTests.CustomEnum.One => 3, + global::UnitTests.CustomEnum.Two => 3, + global::UnitTests.CustomEnum.Three => 5, + _ => throw new ArgumentOutOfRangeException(nameof(value), value, "The value provided is out of range.") + }; + } + + public static bool TryFormat(global::UnitTests.CustomEnum value, Span destination, out int charsWritten) + { + charsWritten = GetFormattedLength(value); + if (destination.Length < charsWritten) + return false; + + switch (value) + { + case global::UnitTests.CustomEnum.One: + "One".CopyTo(destination); + return true; + case global::UnitTests.CustomEnum.Two: + "Two".CopyTo(destination); + return true; + case global::UnitTests.CustomEnum.Three: + "Three".CopyTo(destination); + return true; + default: + charsWritten = 0; + return false; + } + } + + public static bool TryParse(in ReadOnlySpan value, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out global::UnitTests.CustomEnum result) + { + switch (value) + { + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.One), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.One; + return true; + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Two), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.Two; + return true; + case var _ when value.Equals(nameof(global::UnitTests.CustomEnum.Three), StringComparison.Ordinal): + result = global::UnitTests.CustomEnum.Three; + return true; + default: + result = default; + return false; + } + } + + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt new file mode 100644 index 0000000..8bb9792 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt @@ -0,0 +1,147 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: n, + LengthRequired: 32, + ExactLengthRequirement: true + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..64711a1 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(106, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "n", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(106, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "n", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:n}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 106) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[2]].Length != 32 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "n", out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length != 106) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[2]].Length != 32 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "n", out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt new file mode 100644 index 0000000..3095ddb --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt @@ -0,0 +1,234 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::AnotherNamespace.BasicPrimaryKey + }, + Namespace: AnotherNamespace, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + AllParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: x, + LengthRequired: 32, + ExactLengthRequirement: false + } + ], + PartitionKeyParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PrimaryDelimiterKeyPart: { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + SortKeyParts: [ + { + Value: ConstantValue, + LengthRequired: 13, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: x, + LengthRequired: 32, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..d9749f8 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,218 @@ +//HintName: AnotherNamespace.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace AnotherNamespace +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : ICompositePrimaryKey + { + public override string ToString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}|ConstantValue#{ThirdPart:x}"); + } + + public string ToPartitionKeyString() + { + return string.Create(37 + SecondPart.Length, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + state.SecondPart.CopyTo(destination[position..]); + position += state.SecondPart.Length; + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public string ToSortKeyString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:x}"); + } + + public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"ConstantValue#{ThirdPart:x}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result); + } + + public static BasicPrimaryKey Parse(string partitionKey, string sortKey) + { + ArgumentNullException.ThrowIfNull(partitionKey); + ArgumentNullException.ThrowIfNull(sortKey); + + return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey) + { + if (partitionKey.Length < 38) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length < 46) + throw new FormatException("Unrecognized format."); + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + throw new FormatException("Unrecognized format."); + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + if (!Guid.TryParseExact(sortKey[sortKeyPartRanges[1]], "x", out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (partitionKey is null || sortKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result); + } + + public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (partitionKey.Length < 38) + return false; + + if (sortKey.Length < 46) + return false; + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + return false; + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + return false; + + if (partitionKey[partitionKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (partitionKey[partitionKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = partitionKey[partitionKeyPartRanges[1]].ToString(); + + if (!sortKey[sortKeyPartRanges[0]].Equals("ConstantValue", StringComparison.Ordinal)) + return false; + + if (!Guid.TryParseExact(sortKey[sortKeyPartRanges[1]], "x", out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..9a40717 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(112, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "p", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(112, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "p", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:p}#{ThirdPart:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 112) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length != 38 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "p", out var secondPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length != 112) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length != 38 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "p", out var secondPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt new file mode 100644 index 0000000..8285960 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt @@ -0,0 +1,149 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: firstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: secondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: thirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..9328625 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length < 40) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length < 40) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt new file mode 100644 index 0000000..368b866 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt @@ -0,0 +1,170 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: firstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: secondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: thirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + PropertyInitializers: [ + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + MatchesConstructorParameter: true + }, + { + PropertyType: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1, + MatchesConstructorParameter: true + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..c3de9dc --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length < 40) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart) { FirstPart = firstPart, SecondPart = secondPart }; + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length < 40) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart) { FirstPart = firstPart, SecondPart = secondPart }; + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt new file mode 100644 index 0000000..722a76f --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt @@ -0,0 +1,149 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: firstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: secondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: thirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..9328625 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length < 40) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length < 40) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt new file mode 100644 index 0000000..6cc2547 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt @@ -0,0 +1,150 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + PropertyInitializers: [ + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + MatchesConstructorParameter: false + }, + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1, + MatchesConstructorParameter: false + }, + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2, + MatchesConstructorParameter: false + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..18eb8b6 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,181 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(110, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(110, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 110) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart }; + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length != 110) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + return false; + + result = new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart }; + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt new file mode 100644 index 0000000..7564d22 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt @@ -0,0 +1,216 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Double, + FullyQualifiedName: double + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: Double, + FullyQualifiedName: double + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + AllParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Double, + FullyQualifiedName: double + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PartitionKeyParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: b, + LengthRequired: 38, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Double, + FullyQualifiedName: double + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PrimaryDelimiterKeyPart: { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + SortKeyParts: [ + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: false + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..fb9a482 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,190 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : ICompositePrimaryKey + { + public override string ToString() + { + return $"{FirstPart:b}#{SecondPart}|{ThirdPart}"; + } + + public string ToPartitionKeyString() + { + return $"{FirstPart:b}#{SecondPart}"; + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => $"{FirstPart:b}", + (0, true) => $"{FirstPart:b}#", + (1, false) => $"{FirstPart:b}#{SecondPart}", + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public string ToSortKeyString() + { + return string.Create(0 + ThirdPart.Length, this, static (destination, state) => + { + int position = 0; + + state.ThirdPart.CopyTo(destination[position..]); + position += state.ThirdPart.Length; + }); + } + + public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => $"{ThirdPart}", + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result); + } + + public static BasicPrimaryKey Parse(string partitionKey, string sortKey) + { + ArgumentNullException.ThrowIfNull(partitionKey); + ArgumentNullException.ThrowIfNull(sortKey); + + return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey) + { + if (partitionKey.Length < 40) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length < 1) + throw new FormatException("Unrecognized format."); + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + throw new FormatException("Unrecognized format."); + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (!double.TryParse(partitionKey[partitionKeyPartRanges[1]], out var secondPart)) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length == 0) + throw new FormatException("Unrecognized format."); + + string thirdPart = sortKey.ToString(); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (partitionKey is null || sortKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result); + } + + public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (partitionKey.Length < 40) + return false; + + if (sortKey.Length < 1) + return false; + + const int expectedPartitionKeyParts = 2; + Span partitionKeyPartRanges = stackalloc Range[expectedPartitionKeyParts + 1]; + if (partitionKey.Split(partitionKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPartitionKeyParts) + return false; + + if (partitionKey[partitionKeyPartRanges[0]].Length != 38 || !Guid.TryParseExact(partitionKey[partitionKeyPartRanges[0]], "b", out var firstPart)) + return false; + + if (!double.TryParse(partitionKey[partitionKeyPartRanges[1]], out var secondPart)) + return false; + + if (sortKey.Length == 0) + return false; + + string thirdPart = sortKey.ToString(); + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt new file mode 100644 index 0000000..ad47dbb --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt new file mode 100644 index 0000000..fabb714 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt @@ -0,0 +1,17 @@ +{ + Diagnostics: [ + { + Location: : (7,22)-(7,37), + Message: The 'CompositeKey' type 'BasicPrimaryKey' has no obvious constructor, at present, only types with either a single constructor or types with a parameterless constructor are supported., + Severity: Error, + Descriptor: { + Id: COMPOSITE0004, + Title: The type annotated with the 'CompositeKey' attribute has no obvious constructor., + MessageFormat: The 'CompositeKey' type '{0}' has no obvious constructor, at present, only types with either a single constructor or types with a parameterless constructor are supported., + Category: CompositeKey.SourceGeneration, + DefaultSeverity: Error, + IsEnabledByDefault: true + } + } + ] +} \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt new file mode 100644 index 0000000..44d4068 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt @@ -0,0 +1,115 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.OutermostClass.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + private partial record BasicPrimaryKey, + public static partial class OutermostClass + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Value: Constant, + LengthRequired: 8, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..c61e979 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,176 @@ +//HintName: UnitTests.OutermostClass.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + public static partial class OutermostClass + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + private partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(82, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + "Constant".CopyTo(destination[position..]); + position += 8; + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(82, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + "Constant".CopyTo(destination[position..]); + position += 8; + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#Constant#{SecondPart:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 82) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (!primaryKey[primaryKeyPartRanges[1]].Equals("Constant", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var secondPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length != 82) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (!primaryKey[primaryKeyPartRanges[1]].Equals("Constant", StringComparison.Ordinal)) + return false; + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var secondPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt new file mode 100644 index 0000000..c9fc114 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt @@ -0,0 +1,148 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.OutermostClass.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey, + public static partial class OutermostClass + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1 + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2 + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..6d7c901 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,184 @@ +//HintName: UnitTests.OutermostClass.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + public static partial class OutermostClass + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(110, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(110, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.FirstPart).TryFormat(destination[position..], out int firstPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += firstPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.SecondPart).TryFormat(destination[position..], out int secondPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += secondPartCharsWritten; + } + + destination[position] = '#'; + position += 1; + + { + if (!((ISpanFormattable)state.ThirdPart).TryFormat(destination[position..], out int thirdPartCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += thirdPartCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart:d}#{ThirdPart:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 110) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey(firstPart, secondPart, thirdPart); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length != 110) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var secondPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[2]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[2]], "d", out var thirdPart)) + return false; + + result = new BasicPrimaryKey(firstPart, secondPart, thirdPart); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt new file mode 100644 index 0000000..d106e4f --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt @@ -0,0 +1,184 @@ +[ + { + TargetType: { + Type: { + Name: UserTagKey, + FullyQualifiedName: global::UnitTests.UserTagKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record UserTagKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: UserId, + CamelCaseName: userId, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + }, + { + Type: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false, + CollectionType: List + } + ], + PropertyInitializers: [ + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: UserId, + CamelCaseName: userId, + MatchesConstructorParameter: false + }, + { + PropertyType: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + ParameterIndex: 1, + MatchesConstructorParameter: false + } + ], + TypeName: UserTagKey + }, + Key: { + AllParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: UserId, + CamelCaseName: userId, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Value: TAG, + LengthRequired: 3, + ExactLengthRequirement: true + }, + { + Value: _, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false, + CollectionType: List + }, + Separator: #, + InnerParseType: String, + InnerFormatType: String, + InnerType: { + Name: String, + FullyQualifiedName: string + }, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + PartitionKeyParts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: UserId, + CamelCaseName: userId, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + } + ], + PrimaryDelimiterKeyPart: { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + SortKeyParts: [ + { + Value: TAG, + LengthRequired: 3, + ExactLengthRequirement: true + }, + { + Value: _, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false, + CollectionType: List + }, + Separator: #, + InnerParseType: String, + InnerFormatType: String, + InnerType: { + Name: String, + FullyQualifiedName: string + }, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs new file mode 100644 index 0000000..f6db9c3 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs @@ -0,0 +1,270 @@ +//HintName: UnitTests.UserTagKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record UserTagKey : ICompositePrimaryKey + { + public override string ToString() + { + if (Tags.Count == 0) + throw new FormatException("Collection must contain at least one item."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(5, 1, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendFormatted(UserId, "d"); + handler.AppendLiteral("|"); + handler.AppendLiteral("TAG"); + handler.AppendLiteral("_"); + for (int i = 0; i < Tags.Count; i++) + { + if (i > 0) + handler.AppendLiteral("#"); + + handler.AppendFormatted(Tags[i]); + } + + return handler.ToStringAndClear(); + } + + public string ToPartitionKeyString() + { + return string.Create(36, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.UserId).TryFormat(destination[position..], out int userIdCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += userIdCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{UserId:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public string ToSortKeyString() + { + if (Tags.Count == 0) + throw new FormatException("Collection must contain at least one item."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendLiteral("TAG"); + handler.AppendLiteral("_"); + for (int i = 0; i < Tags.Count; i++) + { + if (i > 0) + handler.AppendLiteral("#"); + + handler.AppendFormatted(Tags[i]); + } + + return handler.ToStringAndClear(); + } + + public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + switch (throughPartIndex, includeTrailingDelimiter) + { + case (0, false): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG"); + case (0, true): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_"); + } + + int fixedPartCount = 1; + int repeatIndex = throughPartIndex - fixedPartCount; + int repeatCount = Math.Min(repeatIndex + 1, Tags.Count); + if (repeatCount <= 0) + throw new InvalidOperationException("Invalid throughPartIndex for repeating section."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(0, 0, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendFormatted(string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_")); + + for (int i = 0; i < repeatCount; i++) + { + if (i > 0) + { + handler.AppendLiteral("#"); + } + + handler.AppendFormatted(Tags[i]); + } + + if (includeTrailingDelimiter) + { + handler.AppendLiteral("#"); + } + + return handler.ToStringAndClear(); + } + + public static UserTagKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static UserTagKey Parse(ReadOnlySpan primaryKey) + { + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + return Parse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]]); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result) + { + result = null; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + return TryParse(primaryKey[primaryKeyPartRanges[0]], primaryKey[primaryKeyPartRanges[1]], out result); + } + + public static UserTagKey Parse(string partitionKey, string sortKey) + { + ArgumentNullException.ThrowIfNull(partitionKey); + ArgumentNullException.ThrowIfNull(sortKey); + + return Parse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey); + } + + public static UserTagKey Parse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey) + { + if (partitionKey.Length != 36) + throw new FormatException("Unrecognized format."); + + if (sortKey.Length < 5) + throw new FormatException("Unrecognized format."); + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + throw new FormatException("Unrecognized format."); + + if (partitionKey.Length != 36 || !Guid.TryParseExact(partitionKey, "d", out var userId)) + throw new FormatException("Unrecognized format."); + + if (!sortKey[sortKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + Span tagsRanges = stackalloc Range[128]; + int tagsCount = sortKey[sortKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (tagsCount < 1) + throw new FormatException("Unrecognized format."); + + var tags = new global::System.Collections.Generic.List(); + for (int ri = 0; ri < tagsCount; ri++) + { + if (sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].Length == 0) + throw new FormatException("Unrecognized format."); + tags.Add(sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].ToString()); + } + + if (tags.Count == 0) + throw new FormatException("Unrecognized format."); + + return new UserTagKey() { UserId = userId, Tags = tags }; + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string partitionKey, string sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result) + { + if (partitionKey is null || sortKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)partitionKey, (ReadOnlySpan)sortKey, out result); + } + + public static bool TryParse(ReadOnlySpan partitionKey, ReadOnlySpan sortKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey? result) + { + result = null; + + if (partitionKey.Length != 36) + return false; + + if (sortKey.Length < 5) + return false; + + const int expectedSortKeyParts = 2; + Span sortKeyPartRanges = stackalloc Range[expectedSortKeyParts + 1]; + if (sortKey.Split(sortKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedSortKeyParts) + return false; + + if (partitionKey.Length != 36 || !Guid.TryParseExact(partitionKey, "d", out var userId)) + return false; + + if (!sortKey[sortKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal)) + return false; + + Span tagsRanges = stackalloc Range[128]; + int tagsCount = sortKey[sortKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (tagsCount < 1) + return false; + + var tags = new global::System.Collections.Generic.List(); + for (int ri = 0; ri < tagsCount; ri++) + { + if (sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].Length == 0) + return false; + tags.Add(sortKey[sortKeyPartRanges[1]][tagsRanges[ri]].ToString()); + } + + if (tags.Count == 0) + return false; + + result = new UserTagKey() { UserId = userId, Tags = tags }; + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static UserTagKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey result) => TryParse(s, out result); + + /// + static UserTagKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out UserTagKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt new file mode 100644 index 0000000..c4809a6 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt @@ -0,0 +1,80 @@ +[ + { + TargetType: { + Type: { + Name: TagKey, + FullyQualifiedName: global::UnitTests.TagKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record TagKey + ], + Properties: [ + { + Type: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false, + CollectionType: List + } + ], + PropertyInitializers: [ + { + PropertyType: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + MatchesConstructorParameter: false + } + ], + TypeName: TagKey + }, + Key: { + Parts: [ + { + Value: TAG, + LengthRequired: 3, + ExactLengthRequirement: true + }, + { + Value: _, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: List, + FullyQualifiedName: global::System.Collections.Generic.List + }, + Name: Tags, + CamelCaseName: tags, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: false, + CollectionType: List + }, + Separator: #, + InnerParseType: String, + InnerFormatType: String, + InnerType: { + Name: String, + FullyQualifiedName: string + }, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs new file mode 100644 index 0000000..8c2eea7 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs @@ -0,0 +1,191 @@ +//HintName: UnitTests.TagKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record TagKey : IPrimaryKey + { + public override string ToString() + { + if (Tags.Count == 0) + throw new FormatException("Collection must contain at least one item."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendLiteral("TAG"); + handler.AppendLiteral("_"); + for (int i = 0; i < Tags.Count; i++) + { + if (i > 0) + handler.AppendLiteral("#"); + + handler.AppendFormatted(Tags[i]); + } + + return handler.ToStringAndClear(); + } + + public string ToPartitionKeyString() + { + if (Tags.Count == 0) + throw new FormatException("Collection must contain at least one item."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(4, 0, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendLiteral("TAG"); + handler.AppendLiteral("_"); + for (int i = 0; i < Tags.Count; i++) + { + if (i > 0) + handler.AppendLiteral("#"); + + handler.AppendFormatted(Tags[i]); + } + + return handler.ToStringAndClear(); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + switch (throughPartIndex, includeTrailingDelimiter) + { + case (0, false): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG"); + case (0, true): return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_"); + } + + int fixedPartCount = 1; + int repeatIndex = throughPartIndex - fixedPartCount; + int repeatCount = Math.Min(repeatIndex + 1, Tags.Count); + if (repeatCount <= 0) + throw new InvalidOperationException("Invalid throughPartIndex for repeating section."); + + var handler = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(0, 0, global::System.Globalization.CultureInfo.InvariantCulture); + handler.AppendFormatted(string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"TAG_")); + + for (int i = 0; i < repeatCount; i++) + { + if (i > 0) + { + handler.AppendLiteral("#"); + } + + handler.AppendFormatted(Tags[i]); + } + + if (includeTrailingDelimiter) + { + handler.AppendLiteral("#"); + } + + return handler.ToStringAndClear(); + } + + public static TagKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static TagKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length < 5) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (!primaryKey[primaryKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal)) + throw new FormatException("Unrecognized format."); + + Span tagsRanges = stackalloc Range[128]; + int tagsCount = primaryKey[primaryKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (tagsCount < 1) + throw new FormatException("Unrecognized format."); + + var tags = new global::System.Collections.Generic.List(); + for (int ri = 0; ri < tagsCount; ri++) + { + if (primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].Length == 0) + throw new FormatException("Unrecognized format."); + tags.Add(primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].ToString()); + } + + if (tags.Count == 0) + throw new FormatException("Unrecognized format."); + + return new TagKey() { Tags = tags }; + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey? result) + { + result = null; + + if (primaryKey.Length < 5) + return false; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '_', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (!primaryKey[primaryKeyPartRanges[0]].Equals("TAG", StringComparison.Ordinal)) + return false; + + Span tagsRanges = stackalloc Range[128]; + int tagsCount = primaryKey[primaryKeyPartRanges[1]].Split(tagsRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (tagsCount < 1) + return false; + + var tags = new global::System.Collections.Generic.List(); + for (int ri = 0; ri < tagsCount; ri++) + { + if (primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].Length == 0) + return false; + tags.Add(primaryKey[primaryKeyPartRanges[1]][tagsRanges[ri]].ToString()); + } + + if (tags.Count == 0) + return false; + + result = new TagKey() { Tags = tags }; + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static TagKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey result) => TryParse(s, out result); + + /// + static TagKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out TagKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_GenerationSpec.verified.txt new file mode 100644 index 0000000..4f23855 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_GenerationSpec.verified.txt @@ -0,0 +1,152 @@ +[ + { + TargetType: { + Type: { + Name: BasicPrimaryKey, + FullyQualifiedName: global::UnitTests.BasicPrimaryKey + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record BasicPrimaryKey + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + PropertyInitializers: [ + { + PropertyType: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + MatchesConstructorParameter: false + }, + { + PropertyType: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + ParameterIndex: 1, + MatchesConstructorParameter: false + }, + { + PropertyType: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + ParameterIndex: 2, + MatchesConstructorParameter: false + } + ], + TypeName: BasicPrimaryKey + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: FirstPart, + CamelCaseName: firstPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: String, + FullyQualifiedName: string + }, + Name: SecondPart, + CamelCaseName: secondPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: String, + FormatType: String, + LengthRequired: 1, + ExactLengthRequirement: false + }, + { + Value: #, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Int32, + FullyQualifiedName: int + }, + Name: ThirdPart, + CamelCaseName: thirdPart, + IsRequired: true, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + ParseType: SpanParsable, + FormatType: SpanFormattable, + LengthRequired: 1, + ExactLengthRequirement: false + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs new file mode 100644 index 0000000..91f07e1 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs @@ -0,0 +1,125 @@ +//HintName: UnitTests.BasicPrimaryKey.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record BasicPrimaryKey : IPrimaryKey + { + public override string ToString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString() + { + return string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}"), + (1, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#"), + (2, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{FirstPart:d}#{SecondPart}#{ThirdPart}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static BasicPrimaryKey Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static BasicPrimaryKey Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length < 40) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + throw new FormatException("Unrecognized format."); + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + throw new FormatException("Unrecognized format."); + + return new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart }; + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey? result) + { + result = null; + + if (primaryKey.Length < 40) + return false; + + const int expectedPrimaryKeyParts = 3; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '#', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var firstPart)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length == 0) + return false; + + string secondPart = primaryKey[primaryKeyPartRanges[1]].ToString(); + + if (!int.TryParse(primaryKey[primaryKeyPartRanges[2]], out var thirdPart)) + return false; + + result = new BasicPrimaryKey() { FirstPart = firstPart, SecondPart = secondPart, ThirdPart = thirdPart }; + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static BasicPrimaryKey IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + + /// + static BasicPrimaryKey ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out BasicPrimaryKey result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_GenerationSpec.verified.txt b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_GenerationSpec.verified.txt new file mode 100644 index 0000000..67e6c4f --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_GenerationSpec.verified.txt @@ -0,0 +1,95 @@ +[ + { + TargetType: { + Type: { + Name: KeyWithSamePropertyUsedTwice, + FullyQualifiedName: global::UnitTests.KeyWithSamePropertyUsedTwice + }, + Namespace: UnitTests, + TypeDeclarations: [ + public partial record KeyWithSamePropertyUsedTwice + ], + Properties: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: Id, + CamelCaseName: id, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: Id, + CamelCaseName: id, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + } + ], + ConstructorParameters: [ + { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: Id, + CamelCaseName: id + } + ], + TypeName: KeyWithSamePropertyUsedTwice + }, + Key: { + Parts: [ + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: Id, + CamelCaseName: id, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + }, + { + Value: |, + LengthRequired: 1, + ExactLengthRequirement: true + }, + { + Property: { + Type: { + Name: Guid, + FullyQualifiedName: global::System.Guid + }, + Name: Id, + CamelCaseName: id, + IsRequired: false, + HasGetter: true, + HasSetter: true, + IsInitOnlySetter: true + }, + Format: d, + LengthRequired: 36, + ExactLengthRequirement: true + } + ], + InvariantFormatting: true + } + } +] \ No newline at end of file diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_SourceOutput#UnitTests.KeyWithSamePropertyUsedTwice.g.verified.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_SourceOutput#UnitTests.KeyWithSamePropertyUsedTwice.g.verified.cs new file mode 100644 index 0000000..eb33758 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_SourceOutput#UnitTests.KeyWithSamePropertyUsedTwice.g.verified.cs @@ -0,0 +1,159 @@ +//HintName: UnitTests.KeyWithSamePropertyUsedTwice.g.cs +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +using System; +using CompositeKey; + +namespace UnitTests +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("CompositeKey.SourceGeneration", "VERSION")] + public partial record KeyWithSamePropertyUsedTwice : IPrimaryKey + { + public override string ToString() + { + return string.Create(73, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.Id).TryFormat(destination[position..], out int idCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += idCharsWritten; + } + + destination[position] = '|'; + position += 1; + + { + if (!((ISpanFormattable)state.Id).TryFormat(destination[position..], out int idCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += idCharsWritten; + } + }); + } + + public string ToPartitionKeyString() + { + return string.Create(73, this, static (destination, state) => + { + int position = 0; + + { + if (!((ISpanFormattable)state.Id).TryFormat(destination[position..], out int idCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += idCharsWritten; + } + + destination[position] = '|'; + position += 1; + + { + if (!((ISpanFormattable)state.Id).TryFormat(destination[position..], out int idCharsWritten, "d", global::System.Globalization.CultureInfo.InvariantCulture)) + throw new FormatException(); + + position += idCharsWritten; + } + }); + } + + public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) + { + return (throughPartIndex, includeTrailingDelimiter) switch + { + (0, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{Id:d}"), + (0, true) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{Id:d}|"), + (1, false) => string.Create(global::System.Globalization.CultureInfo.InvariantCulture, $"{Id:d}|{Id:d}"), + _ => throw new InvalidOperationException("Invalid combination of throughPartIndex and includeTrailingDelimiter provided") + }; + } + + public static KeyWithSamePropertyUsedTwice Parse(string primaryKey) + { + ArgumentNullException.ThrowIfNull(primaryKey); + + return Parse((ReadOnlySpan)primaryKey); + } + + public static KeyWithSamePropertyUsedTwice Parse(ReadOnlySpan primaryKey) + { + if (primaryKey.Length != 73) + throw new FormatException("Unrecognized format."); + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var id)) + throw new FormatException("Unrecognized format."); + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var id1)) + throw new FormatException("Unrecognized format."); + + if (!id.Equals(id1)) + throw new FormatException("Unrecognized format."); + + return new KeyWithSamePropertyUsedTwice(id); + } + + public static bool TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out KeyWithSamePropertyUsedTwice? result) + { + if (primaryKey is null) + { + result = null; + return false; + } + + return TryParse((ReadOnlySpan)primaryKey, out result); + } + + public static bool TryParse(ReadOnlySpan primaryKey, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out KeyWithSamePropertyUsedTwice? result) + { + result = null; + + if (primaryKey.Length != 73) + return false; + + const int expectedPrimaryKeyParts = 2; + Span primaryKeyPartRanges = stackalloc Range[expectedPrimaryKeyParts + 1]; + if (primaryKey.Split(primaryKeyPartRanges, '|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) != expectedPrimaryKeyParts) + return false; + + if (primaryKey[primaryKeyPartRanges[0]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[0]], "d", out var id)) + return false; + + if (primaryKey[primaryKeyPartRanges[1]].Length != 36 || !Guid.TryParseExact(primaryKey[primaryKeyPartRanges[1]], "d", out var id1)) + return false; + + if (!id.Equals(id1)) + return false; + + result = new KeyWithSamePropertyUsedTwice(id); + return true; + } + + /// + string IFormattable.ToString(string? format, IFormatProvider? formatProvider) => ToString(); + + /// + static KeyWithSamePropertyUsedTwice IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)] string? s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out KeyWithSamePropertyUsedTwice result) => TryParse(s, out result); + + /// + static KeyWithSamePropertyUsedTwice ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [global::System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out KeyWithSamePropertyUsedTwice result) => TryParse(s, out result); + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.cs b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.cs new file mode 100644 index 0000000..e0b6c35 --- /dev/null +++ b/src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.cs @@ -0,0 +1,282 @@ +using Microsoft.CodeAnalysis; +using VerifyXunit; + +namespace CompositeKey.SourceGeneration.UnitTests; + +public class SourceGeneratorSnapshotTests +{ + [Fact] + public Task BasicPrimaryKey_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithBasicPrimaryKey(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task BasicPrimaryKey_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithBasicPrimaryKey(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task BasicCompositePrimaryKey_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithBasicCompositePrimaryKey(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task BasicCompositePrimaryKey_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithBasicCompositePrimaryKey(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task BasicNonSequentialEnumPrimaryKey_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithBasicNonSequentialEnumPrimaryKey(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task BasicNonSequentialEnumPrimaryKey_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithBasicNonSequentialEnumPrimaryKey(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task InvariantCultureDisabled_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithInvariantCultureDisabled(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task InvariantCultureDisabled_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithInvariantCultureDisabled(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task ClashingKeyNames_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithClashingKeyNames(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task ClashingKeyNames_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithClashingKeyNames(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task InitOnlyProperties_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task InitOnlyProperties_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithInitOnlyProperties(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task ConstructableInitOnlyProperties_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithConstructableInitOnlyProperties(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task ConstructableInitOnlyProperties_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithConstructableInitOnlyProperties(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task RequiredProperties_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithRequiredProperties(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task RequiredProperties_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithRequiredProperties(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task ConstructorThatSetsRequiredProperties_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithConstructorThatSetsRequiredProperties(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task ConstructorThatSetsRequiredProperties_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithConstructorThatSetsRequiredProperties(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task ExplicitlyMarkedConstructor_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithExplicitlyMarkedConstructor(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task ExplicitlyMarkedConstructor_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithExplicitlyMarkedConstructor(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task MultipleExplicitlyMarkedConstructors_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithMultipleExplicitlyMarkedConstructors(); + var driver = RunDriver(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(driver); + } + + [Fact] + public Task MultipleExplicitlyMarkedConstructors_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithMultipleExplicitlyMarkedConstructors(); + var result = CompilationHelper.RunSourceGenerator(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task NestedTypeDeclarations_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithNestedTypeDeclarations(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task NestedTypeDeclarations_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithNestedTypeDeclarations(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task NestedPrivateTypeDeclarations_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithNestedPrivateTypeDeclarations(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task NestedPrivateTypeDeclarations_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithNestedPrivateTypeDeclarations(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task SamePropertyUsedTwice_SourceOutput() + { + var compilation = CompilationHelper.CreateCompilationWithSamePropertyUsedTwice(); + var driver = RunDriver(compilation); + return Verifier.Verify(driver); + } + + [Fact] + public Task SamePropertyUsedTwice_GenerationSpec() + { + var compilation = CompilationHelper.CreateCompilationWithSamePropertyUsedTwice(); + var result = CompilationHelper.RunSourceGenerator(compilation); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task RepeatingPropertyKey_SourceOutput() + { + // disableDiagnosticValidation: test source uses C# 12 collection expressions with C# 11 parse options + var compilation = CompilationHelper.CreateCompilationWithRepeatingPropertyKey(); + var driver = RunDriver(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(driver); + } + + [Fact] + public Task RepeatingPropertyKey_GenerationSpec() + { + // disableDiagnosticValidation: test source uses C# 12 collection expressions with C# 11 parse options + var compilation = CompilationHelper.CreateCompilationWithRepeatingPropertyKey(); + var result = CompilationHelper.RunSourceGenerator(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + [Fact] + public Task RepeatingPropertyCompositeKey_SourceOutput() + { + // disableDiagnosticValidation: test source uses C# 12 collection expressions with C# 11 parse options + var compilation = CompilationHelper.CreateCompilationWithRepeatingPropertyCompositeKey(); + var driver = RunDriver(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(driver); + } + + [Fact] + public Task RepeatingPropertyCompositeKey_GenerationSpec() + { + // disableDiagnosticValidation: test source uses C# 12 collection expressions with C# 11 parse options + var compilation = CompilationHelper.CreateCompilationWithRepeatingPropertyCompositeKey(); + var result = CompilationHelper.RunSourceGenerator(compilation, disableDiagnosticValidation: true); + return Verifier.Verify(result.GenerationSpecs.ToArray()); + } + + private static GeneratorDriver RunDriver(Compilation compilation, bool disableDiagnosticValidation = false) + { + var generator = new SourceGenerator(); + GeneratorDriver driver = CompilationHelper.CreateSourceGeneratorDriver(compilation, generator); + driver = driver.RunGenerators(compilation); + + if (!disableDiagnosticValidation) + { + var result = driver.GetRunResult(); + result.Diagnostics.Where(d => d.Severity > DiagnosticSeverity.Info).ShouldBeEmpty(); + } + + return driver; + } +} diff --git a/src/CompositeKey.SourceGeneration.UnitTests/packages.lock.json b/src/CompositeKey.SourceGeneration.UnitTests/packages.lock.json index 428fd16..f9a6709 100644 --- a/src/CompositeKey.SourceGeneration.UnitTests/packages.lock.json +++ b/src/CompositeKey.SourceGeneration.UnitTests/packages.lock.json @@ -50,6 +50,29 @@ "EmptyFiles": "4.4.0" } }, + "Verify.SourceGenerators": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "XhAg+fJDPXDH7Ajv/J4Hv8ls0zoeK0LqjZIiOT+quwxOqdplcTuqdPx1+4p1qvYzpTdwkLxyGiIA76MzCljyAQ==", + "dependencies": { + "Verify": "26.5.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[31.12.5, )", + "resolved": "31.12.5", + "contentHash": "i1d2bPonW/3ZzzEZYTWgv8mjPyRWpKaPsIxxp/kYK7Nq8ZeSEmkLA5BkGwInDlybHkxsviFu+s8iF20y+yUcZw==", + "dependencies": { + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0", + "Verify": "31.12.5", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -67,19 +90,23 @@ "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, + "Argon": { + "type": "Transitive", + "resolved": "0.33.5", + "contentHash": "J6821zxO+EqMzO9C/V5uiWc2eBGyzN7Z8Z0xq3Q1/e6IxYcHDA32OgiZX5/7/f8rVPQQa7aYtm6f0UfnrgKNBg==" + }, "DiffEngine": { "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "resolved": "18.4.1", + "contentHash": "9/E4N4auQW4iOKPxP6MpGihpuw0uaxfiLLJfraKrqv02cG2LzVx3ocFwIss70mQFwAolrq58zv5NHwMaqT3+3A==", "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" + "EmptyFiles": "8.17.2" } }, "EmptyFiles": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + "resolved": "8.17.2", + "contentHash": "2oyDVmM/DU3g0h2kqcV05zjOUfo9AdwPoduIGh0LZL6nXqSN4qhZna2M/aJoYiQrmIznJ52wxYCmxDnWaRZ1JQ==" }, "Humanizer.Core": { "type": "Transitive", @@ -101,10 +128,7 @@ "resolved": "4.8.0", "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.CodeAnalysis.Analyzers": "3.3.4" } }, "Microsoft.CodeAnalysis.CSharp": { @@ -141,9 +165,7 @@ "Humanizer.Core": "2.14.1", "Microsoft.Bcl.AsyncInterfaces": "7.0.0", "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" + "System.Composition": "7.0.0" } }, "Microsoft.CodeCoverage": { @@ -154,10 +176,7 @@ "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==", - "dependencies": { - "System.Reflection.Metadata": "8.0.0" - } + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", @@ -173,15 +192,10 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "System.CodeDom": { + "SimpleInfoName": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + "resolved": "3.2.0", + "contentHash": "K8ivHRbPWfncijk62Dan/r/z55gwq3aFzqB6yFlD9X0bbpIaacHyHH2cpcIdz0FECUpERUZTwxts0z4gRWpQpA==" }, "System.Composition": { "type": "Transitive", @@ -231,37 +245,16 @@ "System.Composition.Runtime": "7.0.0" } }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" - }, - "System.Management": { + "Verify": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", + "resolved": "31.12.5", + "contentHash": "Luht+42xCM969Scwl7XQ1teZb/7w9XbQg/4eqVQ2WGTWc7mfheENb8PnaX9yJCNROyb1POjQIrQogO+wtf34mg==", "dependencies": { - "System.CodeDom": "6.0.0" + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0" } }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", - "dependencies": { - "System.Collections.Immutable": "8.0.0" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", @@ -370,6 +363,29 @@ "EmptyFiles": "4.4.0" } }, + "Verify.SourceGenerators": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "XhAg+fJDPXDH7Ajv/J4Hv8ls0zoeK0LqjZIiOT+quwxOqdplcTuqdPx1+4p1qvYzpTdwkLxyGiIA76MzCljyAQ==", + "dependencies": { + "Verify": "26.5.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[31.12.5, )", + "resolved": "31.12.5", + "contentHash": "i1d2bPonW/3ZzzEZYTWgv8mjPyRWpKaPsIxxp/kYK7Nq8ZeSEmkLA5BkGwInDlybHkxsviFu+s8iF20y+yUcZw==", + "dependencies": { + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0", + "Verify": "31.12.5", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -387,19 +403,23 @@ "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, + "Argon": { + "type": "Transitive", + "resolved": "0.33.5", + "contentHash": "J6821zxO+EqMzO9C/V5uiWc2eBGyzN7Z8Z0xq3Q1/e6IxYcHDA32OgiZX5/7/f8rVPQQa7aYtm6f0UfnrgKNBg==" + }, "DiffEngine": { "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "resolved": "18.4.1", + "contentHash": "9/E4N4auQW4iOKPxP6MpGihpuw0uaxfiLLJfraKrqv02cG2LzVx3ocFwIss70mQFwAolrq58zv5NHwMaqT3+3A==", "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" + "EmptyFiles": "8.17.2" } }, "EmptyFiles": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + "resolved": "8.17.2", + "contentHash": "2oyDVmM/DU3g0h2kqcV05zjOUfo9AdwPoduIGh0LZL6nXqSN4qhZna2M/aJoYiQrmIznJ52wxYCmxDnWaRZ1JQ==" }, "Humanizer.Core": { "type": "Transitive", @@ -421,10 +441,7 @@ "resolved": "4.8.0", "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.CodeAnalysis.Analyzers": "3.3.4" } }, "Microsoft.CodeAnalysis.CSharp": { @@ -462,8 +479,7 @@ "Microsoft.Bcl.AsyncInterfaces": "7.0.0", "Microsoft.CodeAnalysis.Common": "[4.8.0]", "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" + "System.IO.Pipelines": "7.0.0" } }, "Microsoft.CodeCoverage": { @@ -474,10 +490,7 @@ "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==", - "dependencies": { - "System.Reflection.Metadata": "8.0.0" - } + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", @@ -493,15 +506,10 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "System.CodeDom": { + "SimpleInfoName": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + "resolved": "3.2.0", + "contentHash": "K8ivHRbPWfncijk62Dan/r/z55gwq3aFzqB6yFlD9X0bbpIaacHyHH2cpcIdz0FECUpERUZTwxts0z4gRWpQpA==" }, "System.Composition": { "type": "Transitive", @@ -556,32 +564,16 @@ "resolved": "7.0.0", "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" }, - "System.Management": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "System.Reflection.Metadata": { + "Verify": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "resolved": "31.12.5", + "contentHash": "Luht+42xCM969Scwl7XQ1teZb/7w9XbQg/4eqVQ2WGTWc7mfheENb8PnaX9yJCNROyb1POjQIrQogO+wtf34mg==", "dependencies": { - "System.Collections.Immutable": "8.0.0" + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0" } }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", @@ -690,6 +682,29 @@ "EmptyFiles": "4.4.0" } }, + "Verify.SourceGenerators": { + "type": "Direct", + "requested": "[2.5.0, )", + "resolved": "2.5.0", + "contentHash": "XhAg+fJDPXDH7Ajv/J4Hv8ls0zoeK0LqjZIiOT+quwxOqdplcTuqdPx1+4p1qvYzpTdwkLxyGiIA76MzCljyAQ==", + "dependencies": { + "Verify": "26.5.0" + } + }, + "Verify.Xunit": { + "type": "Direct", + "requested": "[31.12.5, )", + "resolved": "31.12.5", + "contentHash": "i1d2bPonW/3ZzzEZYTWgv8mjPyRWpKaPsIxxp/kYK7Nq8ZeSEmkLA5BkGwInDlybHkxsviFu+s8iF20y+yUcZw==", + "dependencies": { + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0", + "Verify": "31.12.5", + "xunit.abstractions": "2.0.3", + "xunit.extensibility.execution": "2.9.3" + } + }, "xunit": { "type": "Direct", "requested": "[2.9.3, )", @@ -707,19 +722,23 @@ "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, + "Argon": { + "type": "Transitive", + "resolved": "0.33.5", + "contentHash": "J6821zxO+EqMzO9C/V5uiWc2eBGyzN7Z8Z0xq3Q1/e6IxYcHDA32OgiZX5/7/f8rVPQQa7aYtm6f0UfnrgKNBg==" + }, "DiffEngine": { "type": "Transitive", - "resolved": "11.3.0", - "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", + "resolved": "18.4.1", + "contentHash": "9/E4N4auQW4iOKPxP6MpGihpuw0uaxfiLLJfraKrqv02cG2LzVx3ocFwIss70mQFwAolrq58zv5NHwMaqT3+3A==", "dependencies": { - "EmptyFiles": "4.4.0", - "System.Management": "6.0.1" + "EmptyFiles": "8.17.2" } }, "EmptyFiles": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" + "resolved": "8.17.2", + "contentHash": "2oyDVmM/DU3g0h2kqcV05zjOUfo9AdwPoduIGh0LZL6nXqSN4qhZna2M/aJoYiQrmIznJ52wxYCmxDnWaRZ1JQ==" }, "Humanizer.Core": { "type": "Transitive", @@ -741,10 +760,7 @@ "resolved": "4.8.0", "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" + "Microsoft.CodeAnalysis.Analyzers": "3.3.4" } }, "Microsoft.CodeAnalysis.CSharp": { @@ -781,9 +797,7 @@ "Humanizer.Core": "2.14.1", "Microsoft.Bcl.AsyncInterfaces": "7.0.0", "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" + "System.Composition": "7.0.0" } }, "Microsoft.CodeCoverage": { @@ -794,10 +808,7 @@ "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.0.1", - "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==", - "dependencies": { - "System.Reflection.Metadata": "8.0.0" - } + "contentHash": "qT/mwMcLF9BieRkzOBPL2qCopl8hQu6A1P7JWAoj/FMu5i9vds/7cjbJ/LLtaiwWevWLAeD5v5wjQJ/l6jvhWQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", @@ -813,15 +824,10 @@ "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { + "SimpleInfoName": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==" + "resolved": "3.2.0", + "contentHash": "K8ivHRbPWfncijk62Dan/r/z55gwq3aFzqB6yFlD9X0bbpIaacHyHH2cpcIdz0FECUpERUZTwxts0z4gRWpQpA==" }, "System.Composition": { "type": "Transitive", @@ -871,37 +877,16 @@ "System.Composition.Runtime": "7.0.0" } }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" - }, - "System.Management": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "System.Reflection.Metadata": { + "Verify": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ptvgrFh7PvWI8bcVqG5rsA/weWM09EnthFHR5SCnS6IN+P4mj6rE1lBDC4U8HL9/57htKAqy4KQ3bBj84cfYyQ==", + "resolved": "31.12.5", + "contentHash": "Luht+42xCM969Scwl7XQ1teZb/7w9XbQg/4eqVQ2WGTWc7mfheENb8PnaX9yJCNROyb1POjQIrQogO+wtf34mg==", "dependencies": { - "System.Collections.Immutable": "8.0.0" + "Argon": "0.33.5", + "DiffEngine": "18.4.1", + "SimpleInfoName": "3.2.0" } }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, "xunit.abstractions": { "type": "Transitive", "resolved": "2.0.3", diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 9251c56..d50530f 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -26,6 +26,8 @@ + +