diff --git a/PanoramicData.IssueAssessment/Execution/CategoryExecutor.cs b/PanoramicData.IssueAssessment/Execution/CategoryExecutor.cs index 26adab7..c1a9742 100644 --- a/PanoramicData.IssueAssessment/Execution/CategoryExecutor.cs +++ b/PanoramicData.IssueAssessment/Execution/CategoryExecutor.cs @@ -157,12 +157,7 @@ private static CategoryResult BuildCategoryResult(AssessmentCategory category, I .Zip(category.Assessments) .Where(pair => pair.First.Outcome != AssessmentOutcome.NotApplicable)]; - double? rawScore = null; - if (applicable.Count > 0 && applicable.TrueForAll(pair => pair.Result.Score.RawScore.HasValue)) - { - rawScore = applicable.Sum(pair => pair.Result.Score.RawScore!.Value * pair.Assessment.Weight); - } - + double? rawScore = WeightedRawScore(applicable); double? normalizedScore = rawScore.HasValue ? ScoreCalculator.Normalize(rawScore.Value, minPossibleRaw, maxPossibleRaw) : null; @@ -176,4 +171,12 @@ private static CategoryResult BuildCategoryResult(AssessmentCategory category, I string reason = $"Executed {resultList.Count} assessment(s) in category '{category.Name}'."; return new CategoryResult(category.Name, score, reason, resultList) { Weight = category.Weight }; } + + /// + /// The weighted sum of the applicable assessments' raw scores; null when none applies or any of them has no score. + /// + private static double? WeightedRawScore(List<(AssessmentResult Result, Assessment Assessment)> applicable) + => applicable.Count > 0 && applicable.TrueForAll(pair => pair.Result.Score.RawScore.HasValue) + ? applicable.Sum(pair => pair.Result.Score.RawScore!.Value * pair.Assessment.Weight) + : null; } diff --git a/PanoramicData.IssueAssessment/Mapping/GitHubIssueMapper.cs b/PanoramicData.IssueAssessment/Mapping/GitHubIssueMapper.cs index 10156b3..9cd54e8 100644 --- a/PanoramicData.IssueAssessment/Mapping/GitHubIssueMapper.cs +++ b/PanoramicData.IssueAssessment/Mapping/GitHubIssueMapper.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using static PanoramicData.IssueAssessment.Mapping.JsonReading; namespace PanoramicData.IssueAssessment.Mapping; @@ -29,30 +30,30 @@ public static NormalizedIssue FromRestJson(JsonElement issue) } ImmutableList labels = Labels(issue); - string body = Text(issue, "body") ?? string.Empty; + string body = StringAt(issue, "body") ?? string.Empty; return new NormalizedIssue { Key = Key(issue), - Title = Text(issue, "title"), + Title = StringAt(issue, "title"), Description = body, IssueType = IssueType(labels), Priority = Priority(labels), - Status = Text(issue, "state"), + Status = StringAt(issue, "state"), Assignee = FirstAssignee(issue), Components = Components(labels), Labels = labels, AttachmentsCount = MarkdownImage().Count(body) + ImageUrl().Count(body), SupportsFixVersion = false, - DueDate = issue.TryGetProperty("milestone", out JsonElement milestone) ? Text(milestone, "due_on") : null, - Url = Text(issue, "html_url"), + DueDate = issue.TryGetProperty("milestone", out JsonElement milestone) ? StringAt(milestone, "due_on") : null, + Url = StringAt(issue, "html_url"), Platform = "GitHub", }; } private static ImmutableList Labels(JsonElement issue) => issue.TryGetProperty("labels", out JsonElement labels) && labels.ValueKind == JsonValueKind.Array - ? [.. labels.EnumerateArray().Select(label => label.ValueKind == JsonValueKind.String ? label.GetString() : Text(label, "name")).OfType()] + ? [.. labels.EnumerateArray().Select(label => label.ValueKind == JsonValueKind.String ? label.GetString() : StringAt(label, "name")).OfType()] : []; private static string? Key(JsonElement issue) @@ -60,7 +61,7 @@ private static ImmutableList Labels(JsonElement issue) private static string? FirstAssignee(JsonElement issue) => issue.TryGetProperty("assignees", out JsonElement assignees) && assignees.ValueKind == JsonValueKind.Array && assignees.GetArrayLength() > 0 - ? Text(assignees[0], "login") + ? StringAt(assignees[0], "login") : null; private static string IssueType(ImmutableList labels) @@ -92,10 +93,6 @@ private static ImmutableList Components(ImmutableList labels) .Select(match => match.Groups[2].Value.Trim()) .Distinct(StringComparer.Ordinal)]; - private static string? Text(JsonElement element, string property) - => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.String - ? value.GetString() - : null; [GeneratedRegex("enhancement|feature|improvement|story", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, 1000)] private static partial Regex FeatureLabel(); diff --git a/PanoramicData.IssueAssessment/Mapping/JiraIssueMapper.cs b/PanoramicData.IssueAssessment/Mapping/JiraIssueMapper.cs index abba341..aa928fd 100644 --- a/PanoramicData.IssueAssessment/Mapping/JiraIssueMapper.cs +++ b/PanoramicData.IssueAssessment/Mapping/JiraIssueMapper.cs @@ -1,5 +1,6 @@ using System.Collections.Frozen; using System.Text; +using static PanoramicData.IssueAssessment.Mapping.JsonReading; namespace PanoramicData.IssueAssessment.Mapping; @@ -45,22 +46,22 @@ public static NormalizedIssue FromRestJson(JsonElement issue) public static NormalizedIssue FromRestJson(JsonElement issue, Uri? baseUrl) { JsonElement fields = Fields(issue); - string? key = Text(issue, "key"); + string? key = StringAt(issue, "key"); return new NormalizedIssue { Key = key, - Project = Text(fields, "project", "key"), - Title = Text(fields, "summary"), + Project = StringAt(fields, "project", "key"), + Title = StringAt(fields, "summary"), Description = Description(fields), - IssueType = Text(fields, "issuetype", "name"), - Priority = Text(fields, "priority", "name"), - Status = Text(fields, "status", "name"), - Assignee = Text(fields, "assignee", "displayName"), + IssueType = StringAt(fields, "issuetype", "name"), + Priority = StringAt(fields, "priority", "name"), + Status = StringAt(fields, "status", "name"), + Assignee = StringAt(fields, "assignee", "displayName"), Components = Names(fields, "components"), Labels = Strings(fields, "labels"), FixVersions = Names(fields, "fixVersions"), AttachmentsCount = ArrayLength(fields, "attachment"), - DueDate = Text(fields, "duedate"), + DueDate = StringAt(fields, "duedate"), Url = BrowseUrl(baseUrl, key), Platform = "Jira", }; @@ -106,7 +107,7 @@ public static string FlattenAdf(JsonElement document) private static void AppendBlock(JsonElement node, StringBuilder text, string listPrefix) { - string type = Text(node, "type") ?? string.Empty; + string type = StringAt(node, "type") ?? string.Empty; if (ListMarkers.TryGetValue(type, out string? listMarker)) { AppendList(node, text, listPrefix + listMarker); @@ -150,7 +151,7 @@ private static void AppendListItem(JsonElement item, StringBuilder text, string bool first = true; foreach (JsonElement child in Children(item)) { - if (Text(child, "type") is "bulletList" or "orderedList") + if (StringAt(child, "type") is "bulletList" or "orderedList") { AppendBlock(child, text, marker); } @@ -162,12 +163,12 @@ private static void AppendListItem(JsonElement item, StringBuilder text, string } } - private static string Inline(JsonElement node) => Text(node, "type") switch + private static string Inline(JsonElement node) => StringAt(node, "type") switch { - "text" => Text(node, "text") ?? string.Empty, + "text" => StringAt(node, "text") ?? string.Empty, "hardBreak" => "\n", - "mention" or "emoji" => node.TryGetProperty("attrs", out JsonElement attrs) ? Text(attrs, "text") ?? string.Empty : string.Empty, - "inlineCard" => node.TryGetProperty("attrs", out JsonElement attrs) ? Text(attrs, "url") ?? string.Empty : string.Empty, + "mention" or "emoji" => node.TryGetProperty("attrs", out JsonElement attrs) ? StringAt(attrs, "text") ?? string.Empty : string.Empty, + "inlineCard" => node.TryGetProperty("attrs", out JsonElement attrs) ? StringAt(attrs, "url") ?? string.Empty : string.Empty, _ => string.Concat(Children(node).Select(Inline)), }; @@ -176,17 +177,10 @@ private static JsonElement[] Children(JsonElement node) ? [.. content.EnumerateArray()] : []; - private static string? Text(JsonElement element, string property) - => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.String - ? value.GetString() - : null; - - private static string? Text(JsonElement element, string property, string nested) - => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out JsonElement value) ? Text(value, nested) : null; private static ImmutableList Names(JsonElement fields, string property) => fields.TryGetProperty(property, out JsonElement values) && values.ValueKind == JsonValueKind.Array - ? [.. values.EnumerateArray().Select(value => Text(value, "name")).OfType()] + ? [.. values.EnumerateArray().Select(value => StringAt(value, "name")).OfType()] : []; private static ImmutableList Strings(JsonElement fields, string property) diff --git a/PanoramicData.IssueAssessment/Mapping/JsonReading.cs b/PanoramicData.IssueAssessment/Mapping/JsonReading.cs new file mode 100644 index 0000000..a7309cb --- /dev/null +++ b/PanoramicData.IssueAssessment/Mapping/JsonReading.cs @@ -0,0 +1,22 @@ +namespace PanoramicData.IssueAssessment.Mapping; + +/// +/// Reads values out of the JSON a platform returns, tolerating anything of the wrong shape as absent. +/// +internal static class JsonReading +{ + /// + /// The string at of an object; null when it is not an object, the property is missing or + /// the value is not a string. + /// + public static string? StringAt(JsonElement element, string property) + => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out JsonElement value) && value.ValueKind == JsonValueKind.String + ? value.GetString() + : null; + + /// + /// The string at of the object at . + /// + public static string? StringAt(JsonElement element, string property, string nested) + => element.ValueKind == JsonValueKind.Object && element.TryGetProperty(property, out JsonElement value) ? StringAt(value, nested) : null; +} diff --git a/PanoramicData.IssueAssessment/Text/IssueText.cs b/PanoramicData.IssueAssessment/Text/IssueText.cs index f1caffb..b22dfed 100644 --- a/PanoramicData.IssueAssessment/Text/IssueText.cs +++ b/PanoramicData.IssueAssessment/Text/IssueText.cs @@ -186,23 +186,29 @@ private static void Classify(string? heading, List body, List pa string? first = body.Find(line => line.Trim().Length > 0); Match panel = first is null ? Match.Empty : PanelOpening.Match(first); - bool hasContent = first is not null && !IsPlaceholder(string.Join('\n', body)); - bool isPlain = !body.Exists(line => PanelBlocker.IsMatch(line)); if (panel.Success) { panelable.Add(heading); - if (panel.Groups[1].Value.Contains("title=", StringComparison.OrdinalIgnoreCase)) - { - titled.Add(heading); - } + AddIf(titled, heading, panel.Groups[1].Value.Contains("title=", StringComparison.OrdinalIgnoreCase)); + return; } - else if (hasContent && isPlain) + + bool hasContent = first is not null && !IsPlaceholder(string.Join('\n', body)); + if (hasContent && !body.Exists(line => PanelBlocker.IsMatch(line))) { panelable.Add(heading); unpanelled.Add(heading); } } + private static void AddIf(List list, string item, bool condition) + { + if (condition) + { + list.Add(item); + } + } + /// /// True when a section body is a placeholder (TBD, N/A, none, to do, pending, ...) or blank, rather than content. ///