From 6b546dc6c8b1d7534ce4c8780fbb63a8d324b704 Mon Sep 17 00:00:00 2001 From: Daniel Woodward <16451892+DrBarnabus@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:45:50 +0000 Subject: [PATCH] refactor: Add snapshot tests using Verify.SourceGenerators Add Verify.SourceGenerators snapshot tests to capture generated source output and GenerationSpec models as baselines for all 16 CompilationHelper scenarios. These snapshots act as the safety net for subsequent refactoring work, catching any unintended changes to emitted code or parser output. - Add Verify.SourceGenerators and Verify.Xunit to central package management - Add ModuleInitializer with scrubber for non-deterministic GeneratedCodeAttribute version - Add 32 snapshot tests (16 source output + 16 GenerationSpec) in Snapshots/ subfolder - Add .gitignore entry for *.received.* and .gitattributes for *.verified.* files - Update CLAUDE.md with snapshot testing conventions --- .gitattributes | 4 + .gitignore | 3 + CLAUDE.md | 10 +- ...ositeKey.SourceGeneration.UnitTests.csproj | 2 + .../ModuleInitializer.cs | 24 ++ ...sitePrimaryKey_GenerationSpec.verified.txt | 298 ++++++++++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 311 +++++++++++++++++ ...EnumPrimaryKey_GenerationSpec.verified.txt | 298 ++++++++++++++++ ...Tests.BasicNonSequentialEnum.g.verified.cs | 322 ++++++++++++++++++ ...asicPrimaryKey_GenerationSpec.verified.txt | 147 ++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 181 ++++++++++ ...ashingKeyNames_GenerationSpec.verified.txt | 234 +++++++++++++ ...herNamespace.BasicPrimaryKey.g.verified.cs | 218 ++++++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 181 ++++++++++ ...OnlyProperties_GenerationSpec.verified.txt | 149 ++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 125 +++++++ ...iredProperties_GenerationSpec.verified.txt | 170 +++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 125 +++++++ ...kedConstructor_GenerationSpec.verified.txt | 149 ++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 125 +++++++ ...OnlyProperties_GenerationSpec.verified.txt | 150 ++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 181 ++++++++++ ...ultureDisabled_GenerationSpec.verified.txt | 216 ++++++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 190 +++++++++++ ...edConstructors_GenerationSpec.verified.txt | 1 + ...rkedConstructors_SourceOutput.verified.txt | 17 + ...peDeclarations_GenerationSpec.verified.txt | 115 +++++++ ...termostClass.BasicPrimaryKey.g.verified.cs | 176 ++++++++++ ...peDeclarations_GenerationSpec.verified.txt | 148 ++++++++ ...termostClass.BasicPrimaryKey.g.verified.cs | 184 ++++++++++ ...tyCompositeKey_GenerationSpec.verified.txt | 184 ++++++++++ ...eOutput#UnitTests.UserTagKey.g.verified.cs | 270 +++++++++++++++ ...ingPropertyKey_GenerationSpec.verified.txt | 80 +++++ ...ourceOutput#UnitTests.TagKey.g.verified.cs | 191 +++++++++++ ...iredProperties_GenerationSpec.verified.txt | 152 +++++++++ ...ut#UnitTests.BasicPrimaryKey.g.verified.cs | 125 +++++++ ...pertyUsedTwice_GenerationSpec.verified.txt | 95 ++++++ ...KeyWithSamePropertyUsedTwice.g.verified.cs | 159 +++++++++ .../Snapshots/SourceGeneratorSnapshotTests.cs | 282 +++++++++++++++ .../packages.lock.json | 285 ++++++++-------- src/Directory.Packages.props | 2 + 41 files changed, 6128 insertions(+), 151 deletions(-) create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/ModuleInitializer.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicCompositePrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicNonSequentialEnumPrimaryKey_SourceOutput#UnitTests.BasicNonSequentialEnum.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.BasicPrimaryKey_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#AnotherNamespace.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ClashingKeyNames_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructableInitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ConstructorThatSetsRequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.ExplicitlyMarkedConstructor_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InitOnlyProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.InvariantCultureDisabled_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.MultipleExplicitlyMarkedConstructors_SourceOutput.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedPrivateTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.NestedTypeDeclarations_SourceOutput#UnitTests.OutermostClass.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyCompositeKey_SourceOutput#UnitTests.UserTagKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RepeatingPropertyKey_SourceOutput#UnitTests.TagKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.RequiredProperties_SourceOutput#UnitTests.BasicPrimaryKey.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_GenerationSpec.verified.txt create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.SamePropertyUsedTwice_SourceOutput#UnitTests.KeyWithSamePropertyUsedTwice.g.verified.cs create mode 100644 src/CompositeKey.SourceGeneration.UnitTests/Snapshots/SourceGeneratorSnapshotTests.cs 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 @@ + +