Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,43 @@ public static void CompositePrimaryKey_ToPartitionKeyString_ShouldReturnCorrectl
result.ShouldBe($"{guidValue}#{decimalValue}");
}

[Theory]
[InlineAutoData(0, false)]
[InlineAutoData(0, true)]
[InlineAutoData(1, false)]
public static void CompositePrimaryKey_ToPartitionKeyString_WithSpecificPartIndexAndDelimiterRequirements_ShouldReturnCorrectlyFormattedString(
int throughPartIndex, bool includeTrailingDelimiter, CompositePrimaryKey compositeKey)
{
(var guidValue, decimal decimalValue, _, _) = compositeKey;

string result = compositeKey.ToPartitionKeyString(throughPartIndex, includeTrailingDelimiter);

result.ShouldNotBeNullOrEmpty();

switch (throughPartIndex)
{
case 0 when !includeTrailingDelimiter:
result.ShouldBe($"{guidValue}");
break;
case 0 when includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#");
break;
case 1:
result.ShouldBe($"{guidValue}#{decimalValue}");
break;
}
}

[Theory]
[InlineAutoData(1, true)]
[InlineAutoData(2, false)]
public static void CompositePrimaryKey_ToPartitionKeyString_WithInvalidPartIndexOrDelimiterRequirements_ShouldThrowInvalidOperationException(
int throughPartIndex, bool includeTrailingDelimiter, CompositePrimaryKey compositeKey)
{
var act = () => compositeKey.ToPartitionKeyString(throughPartIndex, includeTrailingDelimiter);
act.ShouldThrow<InvalidOperationException>();
}

[Theory, AutoData]
public static void CompositePrimaryKey_ToSortKeyString_ShouldReturnCorrectlyFormattedString(CompositePrimaryKey compositeKey)
{
Expand All @@ -48,6 +85,51 @@ public static void CompositePrimaryKey_ToSortKeyString_ShouldReturnCorrectlyForm
result.ShouldBe($"Constant~{enumValue}@{stringValue}");
}

[Theory]
[InlineAutoData(0, false)]
[InlineAutoData(0, true)]
[InlineAutoData(1, false)]
[InlineAutoData(1, true)]
[InlineAutoData(2, false)]
public static void CompositePrimaryKey_ToSortKeyString_WithSpecificPartIndexAndDelimiterRequirements_ShouldReturnCorrectlyFormattedString(
int throughPartIndex, bool includeTrailingDelimiter, CompositePrimaryKey compositeKey)
{
(_, _, var enumValue, string stringValue) = compositeKey;

string result = compositeKey.ToSortKeyString(throughPartIndex, includeTrailingDelimiter);

result.ShouldNotBeNullOrEmpty();

switch (throughPartIndex)
{
case 0 when !includeTrailingDelimiter:
result.ShouldBe($"Constant");
break;
case 0 when includeTrailingDelimiter:
result.ShouldBe($"Constant~");
break;
case 1 when !includeTrailingDelimiter:
result.ShouldBe($"Constant~{enumValue}");
break;
case 1 when includeTrailingDelimiter:
result.ShouldBe($"Constant~{enumValue}@");
break;
case 2:
result.ShouldBe($"Constant~{enumValue}@{stringValue}");
break;
}
}

[Theory]
[InlineAutoData(2, true)]
[InlineAutoData(3, false)]
public static void CompositePrimaryKey_ToSortKeyString_WithInvalidPartIndexOrDelimiterRequirements_ShouldThrowInvalidOperationException(
int throughPartIndex, bool includeTrailingDelimiter, CompositePrimaryKey compositeKey)
{
var act = () => compositeKey.ToSortKeyString(throughPartIndex, includeTrailingDelimiter);
act.ShouldThrow<InvalidOperationException>();
}

[Theory, AutoData]
public static void CompositePrimaryKey_Parse_WithValidPrimaryKey_ShouldReturnCorrectlyParsedRecord(CompositePrimaryKey compositeKey)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,5 +275,58 @@ public static void PrimaryKeyWithFastPathFormatting_ToPartitionKeyString_ShouldR
result.ShouldBe($"{primaryKey.GuidValue}#Constant#{primaryKey.EnumValue}@{primaryKey.StringValue}");
}

[Theory]
[InlineAutoData(0, false)]
[InlineAutoData(0, true)]
[InlineAutoData(1, false)]
[InlineAutoData(1, true)]
[InlineAutoData(2, false)]
[InlineAutoData(2, true)]
[InlineAutoData(3, false)]
public static void PrimaryKeyWithFastPathFormatting_ToPartitionKeyString_WithSpecificPartIndexAndDelimiterRequirements_ShouldReturnCorrectlyFormattedString(
int throughPartIndex, bool includeTrailingDelimiter, PrimaryKeyWithFastPathFormatting compositeKey)
{
(var guidValue, var enumValue, string stringValue) = compositeKey;

string result = compositeKey.ToPartitionKeyString(throughPartIndex, includeTrailingDelimiter);

result.ShouldNotBeNullOrEmpty();

switch (throughPartIndex)
{
case 0 when !includeTrailingDelimiter:
result.ShouldBe($"{guidValue}");
break;
case 0 when includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#");
break;
case 1 when !includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#Constant");
break;
case 1 when includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#Constant#");
break;
case 2 when !includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#Constant#{enumValue}");
break;
case 2 when includeTrailingDelimiter:
result.ShouldBe($"{guidValue}#Constant#{enumValue}@");
break;
case 3:
result.ShouldBe($"{guidValue}#Constant#{enumValue}@{stringValue}");
break;
}
}

[Theory]
[InlineAutoData(3, true)]
[InlineAutoData(4, false)]
public static void PrimaryKeyWithFastPathFormatting_ToPartitionKeyString_WithInvalidPartIndexOrDelimiterRequirements_ShouldThrowInvalidOperationException(
int throughPartIndex, bool includeTrailingDelimiter, PrimaryKeyWithFastPathFormatting compositeKey)
{
var act = () => compositeKey.ToPartitionKeyString(throughPartIndex, includeTrailingDelimiter);
act.ShouldThrow<InvalidOperationException>();
}

#endregion
}
68 changes: 54 additions & 14 deletions src/CompositeKey.SourceGeneration/SourceGenerator.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ private static void EmitForPrimaryKey(SourceWriter writer, TargetTypeSpec target

WriteFormatMethodBodyForKeyParts(writer, "public override string ToString()", keyParts, keySpec.InvariantFormatting);
WriteFormatMethodBodyForKeyParts(writer, "public string ToPartitionKeyString()", keyParts, keySpec.InvariantFormatting);
WriteDynamicFormatMethodBodyForKeyParts(writer, "public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)", keyParts, keySpec.InvariantFormatting);

WriteParseMethodImplementation();
WriteTryParseMethodImplementation();
Expand Down Expand Up @@ -138,7 +139,9 @@ private static void EmitForCompositePrimaryKey(SourceWriter writer, TargetTypeSp

WriteFormatMethodBodyForKeyParts(writer, "public override string ToString()", keySpec.AllParts, keySpec.InvariantFormatting);
WriteFormatMethodBodyForKeyParts(writer, "public string ToPartitionKeyString()", partitionKeyParts, keySpec.InvariantFormatting);
WriteDynamicFormatMethodBodyForKeyParts(writer, "public string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)", partitionKeyParts, keySpec.InvariantFormatting);
WriteFormatMethodBodyForKeyParts(writer, "public string ToSortKeyString()", sortKeyParts, keySpec.InvariantFormatting);
WriteDynamicFormatMethodBodyForKeyParts(writer, "public string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true)", sortKeyParts, keySpec.InvariantFormatting);

WriteParseMethodImplementation();
WriteTryParseMethodImplementation();
Expand Down Expand Up @@ -401,7 +404,7 @@ private static void WriteParsePropertiesImplementation(
""");
break;

case PropertyKeyPart { ParseType: ParseType.String } part:
case PropertyKeyPart { ParseType: ParseType.String }:
writer.WriteLines($"""
if ({partInputVariable}.Length == 0)
{(shouldThrow ? "throw new FormatException(\"Unrecognized format.\")" : "return false")};
Expand Down Expand Up @@ -570,18 +573,7 @@ or ConstantKeyPart
}
else
{
string formatString = string.Empty;
foreach (var keyPart in keyParts)
{
formatString += keyPart switch
{
DelimiterKeyPart d => d.Value,
ConstantKeyPart c => c.Value,
PropertyKeyPart p => $"{{{p.Property.Name}{(p.Format is not null ? $":{p.Format}" : string.Empty)}}}",
_ => throw new InvalidOperationException()
};
}

string formatString = BuildFormatStringForKeyParts(keyParts);
writer.WriteLine(invariantFormatting
? $"return string.Create({InvariantCulture}, $\"{formatString}\");"
: $"return $\"{formatString}\";");
Expand All @@ -593,6 +585,54 @@ or ConstantKeyPart
static string GetCharsWritten(PropertySpec p) => $"{p.CamelCaseName}CharsWritten";
}

private static void WriteDynamicFormatMethodBodyForKeyParts(
SourceWriter writer, string methodDeclaration, IReadOnlyList<KeyPart> keyParts, bool invariantFormatting)
{
writer.StartBlock(methodDeclaration);

writer.StartBlock("return (throughPartIndex, includeTrailingDelimiter) switch");

for (int i = 0, keyPartIndex = -1; i < keyParts.Count; i++)
{
var keyPart = keyParts[i];

bool isDelimiter = keyPart is DelimiterKeyPart;
if (!isDelimiter)
keyPartIndex++;

string switchCase = $"({keyPartIndex}, {(isDelimiter ? "true" : "false")}) =>";
string formatString = BuildFormatStringForKeyParts(keyParts.Take(i + 1));

writer.WriteLine(invariantFormatting
? $"{switchCase} string.Create({InvariantCulture}, $\"{formatString}\"),"
: $"{switchCase} $\"{formatString}\",");
}

writer.WriteLine("_ => throw new InvalidOperationException(\"Invalid combination of throughPartIndex and includeTrailingDelimiter provided\")");

writer.EndBlock(withSemicolon: true);

writer.EndBlock();
writer.WriteLine();
}

private static string BuildFormatStringForKeyParts(IEnumerable<KeyPart> keyParts)
{
var builder = new StringBuilder();
foreach (var keyPart in keyParts)
{
builder.Append(keyPart switch
{
DelimiterKeyPart d => d.Value,
ConstantKeyPart c => c.Value,
PropertyKeyPart p => $"{{{p.Property.Name}{(p.Format is not null ? $":{p.Format}" : string.Empty)}}}",
_ => throw new InvalidOperationException()
});
}

return builder.ToString();
}

private void AddSource(string hintName, SourceText sourceText) => _context.AddSource(hintName, sourceText);

private static SourceWriter CreateSourceWriterWithHeader(GenerationSpec generationSpec)
Expand Down Expand Up @@ -622,7 +662,7 @@ private static SourceWriter CreateSourceWriterWithHeader(GenerationSpec generati
for (int i = nestedTypeDeclarations.Count - 1; i > 0; i--)
writer.StartBlock(nestedTypeDeclarations[i]);

// Annotate context class with the GeneratedCodeAttribute
// Annotate the context class with the GeneratedCodeAttribute
writer.WriteLine($"""[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{AssemblyName}", "{AssemblyVersion}")]""");

// Emit the main class declaration
Expand Down
16 changes: 16 additions & 0 deletions src/CompositeKey/ICompositePrimaryKey`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ public interface ICompositePrimaryKey<TSelf> : IPrimaryKey<TSelf> where TSelf :
/// <returns>The sort key portion of the current instance formatted to a string.</returns>
string ToSortKeyString();

/// <summary>
/// Formats the sort key portion of the current instance to a string through the specified index.
/// </summary>
/// <param name="throughPartIndex">The zero-based index of the key part to format through (inclusive). This counts only properties and constants, not delimiters.</param>
/// <param name="includeTrailingDelimiter">Whether to include the following delimiter character in the formatted string, defaults to true.</param>
/// <returns>The sort key portion of the current instance up to the specified index formatted to a string.</returns>
/// <example>
/// For a key with template <c>"{Country}#{County}#{Locality}"</c> and values "UK", "Derbyshire" and "Matlock":
/// <code>
/// key.ToSortKeyString(0); // Returns "UK#"
/// key.ToSortKeyString(1, false); // Returns "UK#Derbyshire"
/// key.ToSortKeyString(1, true); // Returns "UK#Derbyshire#"
/// </code>
/// </example>
string ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true);

/// <summary>
/// Parses both partition key and sort key together as strings into a <see cref="TSelf"/> instance.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/CompositeKey/IPrimaryKey`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ public interface IPrimaryKey<TSelf> : IFormattable, ISpanParsable<TSelf> where T
/// <returns>The partition key portion of the current instance formatted to a string.</returns>
string ToPartitionKeyString();

/// <summary>
/// Formats the partition key portion of the current instance to a string through the specified index.
/// </summary>
/// <param name="throughPartIndex">The zero-based index of the key part to format through (inclusive). This counts only properties and constants, not delimiters.</param>
/// <param name="includeTrailingDelimiter">Whether to include the following delimiter character in the formatted string, defaults to true.</param>
/// <returns>The partition key portion of the current instance up to the specified index formatted to a string.</returns>
/// <example>
/// For a key with template <c>"{Country}#{County}#{Locality}"</c> and values "UK", "Derbyshire" and "Matlock":
/// <code>
/// key.ToPartitionKeyString(0); // Returns "UK#"
/// key.ToPartitionKeyString(1, false); // Returns "UK#Derbyshire"
/// key.ToPartitionKeyString(1, true); // Returns "UK#Derbyshire#"
/// </code>
/// </example>
string ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true);

/// <summary>
/// Parses a string into a <see cref="TSelf"/> instance.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/CompositeKey/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
#nullable enable
CompositeKey.ICompositePrimaryKey<TSelf>.ToSortKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) -> string!
CompositeKey.IPrimaryKey<TSelf>.ToPartitionKeyString(int throughPartIndex, bool includeTrailingDelimiter = true) -> string!