Skip to content
Closed
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
5 changes: 3 additions & 2 deletions foreign/csharp/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ Microsoft.NET.Test.Sdk: "18.0.0", "MIT",
Microsoft.Testing.Extensions.CodeCoverage: "18.0.4", "MIT",
Microsoft.Testing.Extensions.TrxReport: "1.9.0", "MIT",
Moq: "4.20.72", "BSD-3-Clause",
Reqnroll.xUnit: "3.2.1", "BSD-3-Clause",
Reqnroll.xUnit: "3.3.0", "BSD-3-Clause",
Shouldly: "4.3.0", "BSD-3-Clause",
System.CommandLine: "2.0.1", "MIT",
System.IO.Hashing: "8.0.0", "MIT",
Testcontainers: "4.8.1", "MIT",
Testcontainers: "4.9.0", "MIT",
TUnit: "0.70.0", "MIT",
xunit: "2.9.3", "Apache-2.0",
xunit.runner.visualstudio: "3.1.5", "Apache-2.0",
5 changes: 3 additions & 2 deletions foreign/csharp/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.0.4" />
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="1.9.0" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Reqnroll.xUnit" Version="3.2.1" />
<PackageVersion Include="Reqnroll.xUnit" Version="3.3.0" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.1" />
<PackageVersion Include="System.IO.Hashing" Version="8.0.0" />
<PackageVersion Include="Testcontainers" Version="4.8.1" />
<PackageVersion Include="Testcontainers" Version="4.9.0" />
<PackageVersion Include="TUnit" Version="0.70.0" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
<PackageReference Include="Microsoft.Testing.Extensions.TrxReport" />
<PackageReference Include="Shouldly" />
Expand Down
12 changes: 10 additions & 2 deletions foreign/csharp/Iggy_SDK.Tests.Integration/StreamsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ public async Task CreateStream_HappyPath_Should_CreateStream_Successfully(Protoc
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task CreateStream_Duplicate_Should_Throw_InvalidResponse(Protocol protocol)
{
await Should.ThrowAsync<IggyInvalidStatusCodeException>(Fixture.Clients[protocol]
var exception = await Should.ThrowAsync<IggyInvalidStatusCodeException>(Fixture.Clients[protocol]
.CreateStreamAsync(Name.GetWithProtocol(protocol)));

exception.ShouldNotBeNull();
exception.ErrorCode.ShouldBe(IggyErrorCode.StreamNameAlreadyExists);
exception.Message.ShouldBe("Invalid status code: 1012 (StreamNameAlreadyExists)");
}

[Test]
Expand Down Expand Up @@ -229,8 +233,12 @@ await Should.NotThrowAsync(() =>
[MethodDataSource<IggyServerFixture>(nameof(IggyServerFixture.ProtocolData))]
public async Task DeleteStream_NotExists_Should_Throw_InvalidResponse(Protocol protocol)
{
await Should.ThrowAsync<IggyInvalidStatusCodeException>(() =>
var exception = await Should.ThrowAsync<IggyInvalidStatusCodeException>(() =>
Fixture.Clients[protocol].DeleteStreamAsync(Identifier.String("stream-to-delete".GetWithProtocol(protocol))));

exception.ShouldNotBeNull();
exception.ErrorCode.ShouldBe(IggyErrorCode.StreamIdNotFound);
exception.Message.ShouldBe("Invalid status code: 1009 (StreamIdNotFound)");
}

[Test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

using System.Text;

namespace Apache.Iggy.Tools.ErrorCodeGenerator;

/// <summary>
/// Generates C# code for IggyErrorCode enum and extensions.
/// </summary>
public static class CSharpCodeGenerator
{
private const string GeneratedHeader = """
// <auto-generated>
// This code was generated by iggy-error-codegen tool.
// Do not modify this file manually.
// To regenerate, run: dotnet iggy-error-codegen generate
// </auto-generated>

""";

/// <summary>
/// Converts license file content to C# comment format.
/// </summary>
public static string ConvertLicenseToComments(string licenseContent)
{
var sb = new StringBuilder();
var lines = licenseContent.Split('\n');

foreach (var line in lines)
{
var trimmedLine = line.TrimEnd('\r');
if (string.IsNullOrWhiteSpace(trimmedLine))
{
sb.AppendLine("//");
}
else
{
sb.AppendLine($"// {trimmedLine}");
}
}

sb.AppendLine();
return sb.ToString();
}

/// <summary>
/// Generates the IggyErrorCode.cs enum file content.
/// </summary>
/// <param name="errors">List of error codes parsed from Rust.</param>
/// <param name="license">License header to include in the generated file.</param>
public static string GenerateEnum(List<RustErrorCode> errors, string license)
{
var sb = new StringBuilder();
sb.Append(license);
sb.Append(GeneratedHeader);
sb.AppendLine();
sb.AppendLine("namespace Apache.Iggy.Enums;");
sb.AppendLine();
sb.AppendLine("/// <summary>");
sb.AppendLine("/// Error codes returned by the Iggy server.");
sb.AppendLine("/// These codes match the error codes defined in the Rust server implementation.");
sb.AppendLine("/// </summary>");
sb.AppendLine("public enum IggyErrorCode");
sb.AppendLine("{");

foreach (var error in errors.OrderBy(e => e.Code))
{
// Clean up message for XML comment
var xmlSafeMessage = error.Message
.Replace("&", "&amp;")
.Replace("<", "&lt;")
.Replace(">", "&gt;")
.Replace(" ", " ")
.Trim();

sb.AppendLine($" /// <summary>{xmlSafeMessage}</summary>");
sb.AppendLine($" {error.Name} = {error.Code},");
sb.AppendLine();
}

sb.AppendLine("}");

return sb.ToString();
}

/// <summary>
/// Generates the IggyErrorCodeExtensions.cs file content.
/// </summary>
/// <param name="errors">List of error codes parsed from Rust.</param>
/// <param name="license">License header to include in the generated file.</param>
public static string GenerateExtensions(List<RustErrorCode> errors, string license)
{
var sb = new StringBuilder();
sb.Append(license);
sb.Append(GeneratedHeader);
sb.AppendLine();
sb.AppendLine("namespace Apache.Iggy.Enums;");
sb.AppendLine();
sb.AppendLine("/// <summary>");
sb.AppendLine(
"/// Extension methods for <see cref=\"IggyErrorCode\" /> to categorize and check error types.");
sb.AppendLine("/// </summary>");
sb.AppendLine("public static class IggyErrorCodeExtensions");
sb.AppendLine("{");

// Generate category methods
foreach (KeyValuePair<string, Func<string, bool>> category in ErrorCategories.Categories.OrderBy(c => c.Key))
{
List<RustErrorCode> matchingErrors = errors.Where(e => category.Value(e.Name)).ToList();
if (matchingErrors.Count == 0)
{
continue;
}

var methodName = category.Key;
var description = GetCategoryDescription(methodName);

sb.AppendLine(" /// <summary>");
sb.AppendLine($" /// {description}");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public static bool {methodName}(this IggyErrorCode code) => code switch");
sb.AppendLine(" {");

foreach (var error in matchingErrors.OrderBy(e => e.Code))
{
sb.AppendLine($" IggyErrorCode.{error.Name} => true,");
}

sb.AppendLine(" _ => false");
sb.AppendLine(" };");
sb.AppendLine();
}

// Generate TryFromStatusCode method
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Tries to convert an integer status code to <see cref=\"IggyErrorCode\" />.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"statusCode\">The integer status code.</param>");
sb.AppendLine(" /// <param name=\"errorCode\">The resulting error code if conversion succeeds.</param>");
sb.AppendLine(" /// <returns>True if the conversion succeeded, false otherwise.</returns>");
sb.AppendLine(" public static bool TryFromStatusCode(int statusCode, out IggyErrorCode errorCode)");
sb.AppendLine(" {");
sb.AppendLine(" if (Enum.IsDefined(typeof(IggyErrorCode), statusCode))");
sb.AppendLine(" {");
sb.AppendLine(" errorCode = (IggyErrorCode)statusCode;");
sb.AppendLine(" return true;");
sb.AppendLine(" }");
sb.AppendLine();
sb.AppendLine(" errorCode = IggyErrorCode.Error;");
sb.AppendLine(" return false;");
sb.AppendLine(" }");
sb.AppendLine();

// Generate FromStatusCode method
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// Converts an integer status code to <see cref=\"IggyErrorCode\" />.");
sb.AppendLine(" /// Returns <see cref=\"IggyErrorCode.Error\" /> if the code is not recognized.");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"statusCode\">The integer status code.</param>");
sb.AppendLine(
" /// <returns>The corresponding error code or <see cref=\"IggyErrorCode.Error\" /> if not found.</returns>");
sb.AppendLine(" public static IggyErrorCode FromStatusCode(int statusCode)");
sb.AppendLine(" {");
sb.AppendLine(
" return TryFromStatusCode(statusCode, out var errorCode) ? errorCode : IggyErrorCode.Error;");
sb.AppendLine(" }");

sb.AppendLine("}");

return sb.ToString();
}

private static string GetCategoryDescription(string methodName)
{
return methodName switch
{
"IsNotFound" => "Checks if the error code indicates that a resource was not found.",
"IsAlreadyExists" => "Checks if the error code indicates that a resource already exists.",
"IsAuthenticationError" => "Checks if the error code is related to authentication.",
"IsConnectionError" => "Checks if the error code is related to connection issues.",
"IsStreamError" => "Checks if the error code is related to streams.",
"IsTopicError" => "Checks if the error code is related to topics.",
"IsConsumerGroupError" => "Checks if the error code is related to consumer groups.",
"IsMessageError" => "Checks if the error code is related to messages.",
"IsUserError" => "Checks if the error code is related to user management.",
"IsPersonalAccessTokenError" => "Checks if the error code is related to personal access tokens.",
"IsTlsError" => "Checks if the error code is related to TLS/certificates.",
"IsValidationError" => "Checks if the error code indicates a validation error.",
"IsLimitReached" => "Checks if the error code indicates a limit has been reached.",
_ => $"Checks if the error code matches the {methodName} category."
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

using System.Text.RegularExpressions;

namespace Apache.Iggy.Tools.ErrorCodeGenerator;

/// <summary>
/// Represents a parsed error code from C#.
/// </summary>
public sealed record CSharpErrorCode(string Name, int Code);

/// <summary>
/// Parses C# IggyErrorCode enum.
/// </summary>
public static partial class CSharpEnumParser
{
// Matches: EnumValue = 123, or EnumValue = 123
[GeneratedRegex(@"^\s*([A-Z][a-zA-Z0-9_]*)\s*=\s*(\d+)\s*,?\s*$")]
private static partial Regex EnumValueRegex();

/// <summary>
/// Parses the C# IggyErrorCode.cs file and extracts error codes.
/// </summary>
public static List<CSharpErrorCode> Parse(string csharpFileContent)
{
var errors = new List<CSharpErrorCode>();
var lines = csharpFileContent.Split('\n');

var inEnum = false;

foreach (var line in lines)
{
// Detect start of enum
if (line.Contains("public enum IggyErrorCode"))
{
inEnum = true;
continue;
}

// Detect end of enum
if (inEnum && line.Trim() == "}")
{
break;
}

if (!inEnum)
{
continue;
}

// Check for enum value
var match = EnumValueRegex().Match(line);
if (match.Success)
{
var name = match.Groups[1].Value;
var code = int.Parse(match.Groups[2].Value);
errors.Add(new CSharpErrorCode(name, code));
}
}

return errors;
}

/// <summary>
/// Parses the C# file from the given path.
/// </summary>
public static async Task<List<CSharpErrorCode>> ParseFileAsync(string filePath)
{
var content = await File.ReadAllTextAsync(filePath);
return Parse(content);
}
}
Loading
Loading