Skip to content

Commit ec6b58f

Browse files
calin-lupas_dsamcapscalin-lupas_dsamcaps
authored andcommitted
#42 Refactoring and Enhance error handling and localization support
- Updated `GitHubClientExtensions` for improved error handling when retrieving file content and added `GetDefaultBranchAsync`. - Introduced `GetLocalizedString` method in `LocalizationExtensions` for `RequestType` enum. - Modified `StringExtensions` to support Unicode characters and added `NormalizeToValidChars` method. - Corrected error constants in `Constants` class for better clarity. - Enhanced `DevExIssueRequestModel`, `RepositoryRequestModel`, and `TeamRequestModel` with fallback logic for localization. - Updated `GitHubIssueFormParser` to handle Unicode in `FormatKey`. - Added new localization keys in resource files for repository and team actions. - Improved user experience in `DevExIssuesEventProcessorService` and `RepositoryService` by using localized strings for issue labels. - Updated `TeamService` to better manage team member lists and maintainers.
1 parent 34b43da commit ec6b58f

14 files changed

Lines changed: 376 additions & 65 deletions

src/DevExcelerateApi/Core/Extensions/GitHubClientExtensions.cs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,20 @@ public static async Task<string> GetFileContentAsync(this IGitHubClient gitHubCl
3535
throw new InvalidOperationException("GitHub client is not configured.");
3636
}
3737

38-
var fileContent = await gitHubClient.Repository.Content.GetRawContent(owner, repo, path);
39-
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
40-
return fileContentString;
38+
try
39+
{
40+
var fileContent = await gitHubClient.Repository.Content.GetRawContent(owner, repo, path);
41+
var fileContentString = System.Text.Encoding.UTF8.GetString(fileContent);
42+
return fileContentString;
43+
}
44+
catch (NotFoundException ex)
45+
{
46+
throw new FileNotFoundException($"File not found at path '{path}' in repository '{owner}/{repo}'.", ex);
47+
}
48+
catch (Exception ex)
49+
{
50+
throw new InvalidOperationException($"Failed to retrieve file content from '{path}' in repository '{owner}/{repo}': {ex.Message}", ex);
51+
}
4152
}
4253

4354
public static async Task<RepositoryContentChangeSet?> SaveFileAsync(
@@ -121,12 +132,26 @@ public static async Task<string> GetFileContentAsync(this IGitHubClient gitHubCl
121132
}
122133
catch (NotFoundException)
123134
{
124-
// File does not exist at 'path' (GetAllContentsByRef threw NotFoundException)
125-
// Create the file at 'pathForCreationOrNonExistent'
135+
// If no file was found, it was probably already moved using the newPath logic above
136+
if (newPath != null && pathForCreationOrNonExistent == newPath)
137+
{
138+
var existingFile = await gitHubClient.Repository.Content.GetAllContentsByRef(owner, repo, pathForCreationOrNonExistent, branch);
139+
140+
if (existingFile != null && existingFile.Count > 0)
141+
{
142+
var updateFileRequest = new UpdateFileRequest(
143+
$"Updating file {pathForCreationOrNonExistent}",
144+
content,
145+
existingFile[0].Sha,
146+
branch);
147+
return await gitHubClient.Repository.Content.UpdateFile(owner, repo, pathForCreationOrNonExistent, updateFileRequest);
148+
}
149+
}
150+
126151
var createFileRequest = new CreateFileRequest(
127-
$"Adding file {pathForCreationOrNonExistent}",
128-
content,
129-
branch);
152+
$"Adding file {pathForCreationOrNonExistent}",
153+
content,
154+
branch);
130155
return await gitHubClient.Repository.Content.CreateFile(owner, repo, pathForCreationOrNonExistent, createFileRequest);
131156
}
132157
}
@@ -206,4 +231,27 @@ public static async Task<Dictionary<string, string>> GetCommitFilesWithContentAs
206231

207232
return fileContents;
208233
}
234+
235+
public static async Task<string> GetDefaultBranchAsync(this IGitHubClient gitHubClient, string owner, string repo)
236+
{
237+
if (gitHubClient == null)
238+
{
239+
throw new InvalidOperationException("GitHub client is not configured.");
240+
}
241+
242+
try
243+
{
244+
// Fetch repository information which includes the default branch
245+
var repository = await gitHubClient.Repository.Get(owner, repo);
246+
return repository.DefaultBranch;
247+
}
248+
catch (NotFoundException)
249+
{
250+
throw new InvalidOperationException($"Repository '{owner}/{repo}' not found or not accessible.");
251+
}
252+
catch (Exception ex)
253+
{
254+
throw new InvalidOperationException($"Failed to retrieve default branch for repository '{owner}/{repo}': {ex.Message}", ex);
255+
}
256+
}
209257
}

src/DevExcelerateApi/Core/Extensions/LocalizationExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ public static string GetLocalizedString(this TicketStatus status)
1717
return GetLocalizedString(status.ToString());
1818
}
1919

20+
public static string GetLocalizedString(this RequestType type)
21+
{
22+
return GetLocalizedString(type.ToString());
23+
}
24+
2025
public static string GetLocalizedString(string resourceKey)
2126
{
2227
if (_localizationService == null)

src/DevExcelerateApi/Core/Extensions/StringExtensions.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using DevExcelerateApi.Models;
2+
using System.Globalization;
3+
using System.Text;
24
using System.Text.RegularExpressions;
35

46
namespace DevExcelerateApi.Core.Extensions
@@ -36,7 +38,7 @@ public static bool IsIssueRejected(this string data)
3638
return IsIssueLabel(data, TicketStatus.REJECTED);
3739
}
3840

39-
private static readonly Regex InvalidCharsRegex = new(@"[^a-zA-Z0-9\-_.]", RegexOptions.Compiled);
41+
private static readonly Regex InvalidCharsRegex = new(@"[^\p{L}0-9\-_.]", RegexOptions.Compiled);
4042

4143
public static string SanitizeResourceName(this string name)
4244
{
@@ -45,7 +47,8 @@ public static string SanitizeResourceName(this string name)
4547
throw new ArgumentException("Repository name cannot be null or empty.", nameof(name));
4648
}
4749

48-
return InvalidCharsRegex.Replace(name, string.Empty);
50+
return InvalidCharsRegex.Replace(name, string.Empty)
51+
.NormalizeToValidChars();
4952
}
5053

5154
public static List<string>? ConvertCsvToList(this string? csvString)
@@ -58,5 +61,34 @@ public static string SanitizeResourceName(this string name)
5861
.Where(i => !string.IsNullOrEmpty(i))
5962
.Distinct()];
6063
}
64+
65+
public static string NormalizeToValidChars(this string input)
66+
{
67+
if (string.IsNullOrEmpty(input))
68+
{
69+
return string.Empty;
70+
}
71+
72+
// Normalize the string to decompose accented characters
73+
var normalizedString = input.Normalize(NormalizationForm.FormD);
74+
var stringBuilder = new StringBuilder();
75+
76+
foreach (char c in normalizedString)
77+
{
78+
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
79+
80+
// Keep only letters, digits, hyphens, underscores, and dots
81+
// Skip combining marks (accents) that were decomposed
82+
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
83+
{
84+
if (char.IsLetter(c) || char.IsDigit(c) || c == '-' || c == '_' || c == '.')
85+
{
86+
stringBuilder.Append(char.ToLower(c));
87+
}
88+
}
89+
}
90+
91+
return stringBuilder.ToString();
92+
}
6193
}
6294
}

src/DevExcelerateApi/Models/Constants.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public static class Constants
2222
public const string ERROR_INVAILD_REPO_NAME = "ERROR_INVAILD_REPO_NAME";
2323

2424
// Repository PR related constants
25-
public const string ERROR_PR_CREATION = "ERROR_REPO_PR_CREATION";
26-
public const string ERROR_PR_NO_CHANGES = "ERROR_REPO_PR_NO_CHANGES";
25+
public const string ERROR_PR_CREATION = "ERROR_PR_CREATION";
26+
public const string ERROR_PR_NO_CHANGES = "ERROR_PR_NO_CHANGES";
2727

2828
// Team related constants
2929
public const string ERROR_TEAM_ISSUE = "ERROR_TEAM_ISSUE";
@@ -35,6 +35,7 @@ public static class Constants
3535
public const string ERROR_TEAM_DOES_NOT_EXIST = "ERROR_TEAM_DOES_NOT_EXIST";
3636

3737
public const string ERROR_UPDATE_NO_CHANGES = "ERROR_UPDATE_NO_CHANGES";
38+
public const string ERROR_INVALID_DATA = "ERROR_INVALID_DATA";
3839
public const string NONE = "NONE";
3940

4041
public const string VALIDATION_PASSED = "VALIDATION_PASSED";

src/DevExcelerateApi/Models/DevExIssueRequestModel.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using YamlDotNet.Serialization;
2+
using DevExcelerateApi.Core.Extensions;
23

34
namespace DevExcelerateApi.Models
45
{
@@ -29,11 +30,36 @@ public static DevExIssueRequestModel Parse(IDictionary<string, object?> values)
2930
{
3031
ArgumentNullException.ThrowIfNull(values);
3132

32-
values.TryGetValue("request_type", out var requestType);
33+
values.TryGetValue("request_type", out var objRequestType);
34+
35+
if (objRequestType == null)
36+
{
37+
// Fallback to the localized string if the default key is not found
38+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("request_type"), out objRequestType);
39+
}
40+
41+
RequestType requestType = default;
42+
43+
if (Enum.TryParse<RequestType>(objRequestType?.ToString(), out var parsedRequestType))
44+
{
45+
requestType = parsedRequestType;
46+
}
47+
else
48+
{
49+
// Fallback to matching the localized string if parsing fails
50+
foreach (var type in Enum.GetValues<RequestType>())
51+
{
52+
if (LocalizationExtensions.GetLocalizedString(type).Equals(objRequestType?.ToString(), StringComparison.OrdinalIgnoreCase))
53+
{
54+
requestType = type;
55+
break;
56+
}
57+
}
58+
}
3359

3460
return new DevExIssueRequestModel
3561
{
36-
RequestType = Enum.TryParse<RequestType>(requestType?.ToString(), out var parsedRequestType) ? parsedRequestType : default,
62+
RequestType = requestType
3763
};
3864
}
3965
}

src/DevExcelerateApi/Models/RepositoryRequestModel.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,31 @@ public class RepositoryRequestModel : DevExIssueRequestModel
5252
{
5353
ArgumentNullException.ThrowIfNull(values);
5454

55-
values.TryGetValue("repository_owner", out var repositoryOwner);
5655
values.TryGetValue("repository_name", out var repositoryName);
5756
values.TryGetValue("repository_visibility", out var repositoryVisibility);
5857
values.TryGetValue("repository_classification", out var repositoryClassification);
5958
values.TryGetValue("repository_description", out var repositoryDescription);
6059
values.TryGetValue("repository_maintainers", out var repositoryMaintainers);
61-
values.TryGetValue("repository_readers", out var repositoryReaders);
60+
values.TryGetValue("repository_readers", out var repositoryReaders);
6261
values.TryGetValue("repository_contributors", out var repositoryContributors);
6362
values.TryGetValue("repository_rulesets", out var repositoryRulesets);
6463

64+
if (repositoryName == null)
65+
{
66+
// Fallback to the localized string if the default key is not found
67+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_name"), out repositoryName);
68+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_visibility"), out repositoryVisibility);
69+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_classification"), out repositoryClassification);
70+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_description"), out repositoryDescription);
71+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_maintainers"), out repositoryMaintainers);
72+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_readers"), out repositoryReaders);
73+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_contributors"), out repositoryContributors);
74+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("repository_rulesets"), out repositoryRulesets);
75+
}
76+
6577
return new RepositoryRequestModel
6678
{
6779
RequestType = DevExIssueRequestModel.Parse(values).RequestType,
68-
RepositoryOwner = repositoryOwner?.ToString(),
6980
RepositoryName = repositoryName?.ToString()?.SanitizeResourceName(),
7081
RepositoryVisibility = repositoryVisibility?.ToString(),
7182
RepositoryClassification = repositoryClassification?.ToString(),

src/DevExcelerateApi/Models/TeamRequestModel.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ public class TeamRequestModel : DevExIssueRequestModel
5353
{
5454
ArgumentNullException.ThrowIfNull(values);
5555

56-
values.TryGetValue("team_owner", out var teamOwner);
5756
values.TryGetValue("team_name", out var teamName);
5857
values.TryGetValue("new_team_name", out var newTeamName);
5958
values.TryGetValue("team_description", out var teamDescription);
@@ -64,10 +63,23 @@ public class TeamRequestModel : DevExIssueRequestModel
6463
values.TryGetValue("team_members", out var teamMembers);
6564
values.TryGetValue("remove_team_members", out var removeTeamMembers);
6665

66+
if (teamName == null)
67+
{
68+
// Fallback to the localized string if the default key is not found
69+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_name"), out teamName);
70+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("new_team_name"), out newTeamName);
71+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_description"), out teamDescription);
72+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("parent_team_name"), out parentTeamName);
73+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_identity_provider_group"), out teamIdentityProviderGroup);
74+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_visibility"), out teamVisibility);
75+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_maintainers"), out teamMaintainers);
76+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("team_members"), out teamMembers);
77+
values.TryGetValue(LocalizationExtensions.GetLocalizedString("remove_team_members"), out removeTeamMembers);
78+
}
79+
6780
return new TeamRequestModel
6881
{
6982
RequestType = DevExIssueRequestModel.Parse(values).RequestType,
70-
TeamOwner = teamOwner?.ToString(),
7183
TeamName = teamName?.ToString()?.SanitizeResourceName(),
7284
NewTeamName = newTeamName?.ToString()?.SanitizeResourceName(),
7385
TeamDescription = teamDescription?.ToString(),

src/DevExcelerateApi/Parsers/GitHubIssueFormParser.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace DevExcelerateApi.Parsers
44
{
55
public static class GitHubIssueFormParser
66
{
7-
private static Regex ParseIssueRegEx = new Regex(@"### *(?<key>.*?)\s*[\r\n]+(?<value>[\s\S]*?)(?=\n?###|\n?$)", RegexOptions.Compiled);
7+
private static readonly Regex ParseIssueRegEx = new(@"### *(?<key>.*?)\s*[\r\n]+(?<value>[\s\S]*?)(?=\n?###|\n?$)", RegexOptions.Compiled);
88

99
public class ParsedBody
1010
{
@@ -67,7 +67,8 @@ public static bool IsEmptyResponse(string value)
6767
/// <returns>A slugified string with non-alphanumeric characters replaced by underscores, and leading/trailing underscores removed.</returns>
6868
public static string FormatKey(string name)
6969
{
70-
return Regex.Replace(name.Trim().ToLower(), @"[^a-z0-9]", "_").Trim('_').Replace("__", "_");
70+
// Allow Unicode letters (including French accents), digits, and replace other chars with underscores
71+
return Regex.Replace(name.Trim().ToLower(), @"[^\p{L}0-9]", "_", RegexOptions.None).Trim('_').Replace("__", "_");
7172
}
7273

7374
/// <summary>

src/DevExcelerateApi/Resources/Services.LocalizationService.fr.resx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,4 +207,79 @@
207207
<data name="NONE" xml:space="preserve">
208208
<value>ACUNE</value>
209209
</data>
210+
<data name="CREATE_REPOSITORY" xml:space="preserve">
211+
<value>CRÉER_DÉPÔT</value>
212+
</data>
213+
<data name="UPDATE_REPOSITORY" xml:space="preserve">
214+
<value>METTRE_À_JOUR_DÉPÔT</value>
215+
</data>
216+
<data name="CREATE_TEAM" xml:space="preserve">
217+
<value>CRÉER_ÉQUIPE</value>
218+
</data>
219+
<data name="UPDATE_TEAM" xml:space="preserve">
220+
<value>METTRE_À_JOUR_ÉQUIPE</value>
221+
</data>
222+
<data name="request_type" xml:space="preserve">
223+
<value>type_de_demande</value>
224+
</data>
225+
<data name="team_name" xml:space="preserve">
226+
<value>nom_de_l_équipe</value>
227+
</data>
228+
<data name="new_team_name" xml:space="preserve">
229+
<value>nouveau_nom_de_l_équipe</value>
230+
</data>
231+
<data name="team_description" xml:space="preserve">
232+
<value>description_de_l_équipe</value>
233+
</data>
234+
<data name="parent_team_name" xml:space="preserve">
235+
<value>nom_de_l_équipe_parente</value>
236+
</data>
237+
<data name="team_identity_provider_group" xml:space="preserve">
238+
<value>groupe_fournisseur_d_identité_de_l_équipe</value>
239+
</data>
240+
<data name="team_visibility" xml:space="preserve">
241+
<value>visibilité_de_l_équipe</value>
242+
</data>
243+
<data name="team_maintainers" xml:space="preserve">
244+
<value>responsables_de_l_équipe</value>
245+
</data>
246+
<data name="team_members" xml:space="preserve">
247+
<value>membres_de_l_équipe</value>
248+
</data>
249+
<data name="remove_team_members" xml:space="preserve">
250+
<value>retirer_des_membres_de_l_équipe</value>
251+
</data>
252+
<data name="repository_name" xml:space="preserve">
253+
<value>nom_du_dépôt</value>
254+
</data>
255+
<data name="repository_visibility" xml:space="preserve">
256+
<value>visibilité_du_dépôt</value>
257+
</data>
258+
<data name="repository_classification" xml:space="preserve">
259+
<value>classification_du_dépôt</value>
260+
</data>
261+
<data name="repository_description" xml:space="preserve">
262+
<value>description_du_dépôt</value>
263+
</data>
264+
<data name="repository_maintainers" xml:space="preserve">
265+
<value>responsables_du_dépôt</value>
266+
</data>
267+
<data name="repository_readers" xml:space="preserve">
268+
<value>lecteurs_du_dépôt</value>
269+
</data>
270+
<data name="repository_contributors" xml:space="preserve">
271+
<value>contributeurs_du_dépôt</value>
272+
</data>
273+
<data name="repository_rulesets" xml:space="preserve">
274+
<value>règles_du_dépôt</value>
275+
</data>
276+
<data name="ERROR_INVALID_DATA" xml:space="preserve">
277+
<value>Les données de la requête sont invalides.</value>
278+
</data>
279+
<data name="Internal" xml:space="preserve">
280+
<value>Interne</value>
281+
</data>
282+
<data name="Private" xml:space="preserve">
283+
<value>Privé</value>
284+
</data>
210285
</root>

0 commit comments

Comments
 (0)