Skip to content

Commit 9de2b55

Browse files
author
Chris Nantau
authored
feat: add initial filter impl (#137)
Admin override into main (Reviewers all busy) * feat: add initial filter impl * move to using regex filter instead of action filter * separating files and formatting * case insensitive regex * adding initial tests for filters * Add filter separator as ' ', remove debug statements, aggregate instead of for loop
1 parent 8b782e5 commit 9de2b55

19 files changed

Lines changed: 445 additions & 53 deletions

Peer.Domain/Commands/Show.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
using System.Linq;
1+
using System.Collections.Generic;
2+
using System.Linq;
23
using System.Reactive.Linq;
34
using System.Threading;
45
using System.Threading.Tasks;
6+
using Peer.Domain.Filters;
57
using wimm.Secundatives;
68

79
namespace Peer.Domain.Commands
@@ -12,22 +14,27 @@ public class Show
1214
private readonly IListFormatter _formatter;
1315
private readonly IConsoleWriter _writer;
1416
private readonly ISorter<PullRequest>? _sorter;
17+
private readonly List<IFilter> _filters;
1518

1619
public Show(
1720
IPullRequestService prService,
1821
IListFormatter formatter,
1922
IConsoleWriter writer,
20-
ISorter<PullRequest>? sorter = null)
23+
ISorter<PullRequest>? sorter = null,
24+
IEnumerable<IFilter>? filters = null)
2125
{
2226
_pullRequestService = prService;
2327
_formatter = formatter;
2428
_writer = writer;
2529
_sorter = sorter;
30+
_filters = filters?.ToList() ?? new();
2631
}
2732

2833
public async Task<Result<None, ShowError>> ShowAsync(ShowArguments args, CancellationToken token = default)
2934
{
3035
var prs = await _pullRequestService.FetchAllPullRequests(token);
36+
prs = _filters.Aggregate(prs, (prs, filter) => filter.Filter(prs));
37+
3138
var sorted = await (_sorter?.Sort(prs) ?? prs).Take(args.Count).ToListAsync(token);
3239
var lines = _formatter.FormatLines(sorted).ToList();
3340
_writer.Display(lines, token);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Peer.Domain.Filters
6+
{
7+
public class ActionFilter : IFilter
8+
{
9+
private readonly Func<PullRequest, bool> _func;
10+
private readonly bool _negated;
11+
12+
public ActionFilter(Func<PullRequest, bool> func, bool negated)
13+
{
14+
_func = func;
15+
_negated = negated;
16+
}
17+
18+
public IAsyncEnumerable<PullRequest> Filter(IAsyncEnumerable<PullRequest> pullRequests)
19+
{
20+
return pullRequests.Where(x => _func(x) ^ _negated);
21+
}
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Peer.Domain.Filters
6+
{
7+
public class EnumMatchingFilter<T> : IFilter
8+
where T : struct, IComparable
9+
{
10+
private readonly T _value;
11+
private readonly bool _negated;
12+
private readonly PropertySelector<T> _selector;
13+
14+
public EnumMatchingFilter(PropertySelector<T> selector, T value, bool negated)
15+
{
16+
_value = value;
17+
_negated = negated;
18+
_selector = selector;
19+
}
20+
21+
public IAsyncEnumerable<PullRequest> Filter(IAsyncEnumerable<PullRequest> pullRequests)
22+
{
23+
return pullRequests.Where(pr =>
24+
{
25+
var prop = _selector.Selector(pr);
26+
return prop.Equals(_value) ^ _negated;
27+
});
28+
}
29+
}
30+
}

Peer.Domain/Filters/IFilter.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Collections.Generic;
2+
3+
namespace Peer.Domain.Filters
4+
{
5+
public interface IFilter
6+
{
7+
IAsyncEnumerable<PullRequest> Filter(IAsyncEnumerable<PullRequest> pullRequests);
8+
}
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace Peer.Domain.Filters
4+
{
5+
public interface IPropertySelector
6+
{
7+
Func<PullRequest, IComparable> Selector { get; }
8+
Type ReturnType { get; }
9+
}
10+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace Peer.Domain.Filters
4+
{
5+
public class PropertySelector<T> : IPropertySelector
6+
where T : IComparable
7+
{
8+
public Func<PullRequest, T> Selector { get; }
9+
public Type ReturnType => typeof(T);
10+
11+
Func<PullRequest, IComparable> IPropertySelector.Selector => pr => Selector(pr);
12+
13+
public PropertySelector(Func<PullRequest, T> func)
14+
{
15+
Selector = pr => func(pr);
16+
}
17+
}
18+
}

Peer.Domain/Filters/RegexFilter.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Text.RegularExpressions;
4+
5+
namespace Peer.Domain.Filters
6+
{
7+
public class RegexFilter : IFilter
8+
{
9+
private readonly PropertySelector<string> _selector;
10+
private readonly Regex _regex;
11+
private readonly bool _negated;
12+
13+
public RegexFilter(PropertySelector<string> selector, Regex regex, bool negated)
14+
{
15+
_selector = selector;
16+
_regex = regex;
17+
_negated = negated;
18+
}
19+
20+
public IAsyncEnumerable<PullRequest> Filter(IAsyncEnumerable<PullRequest> pullRequests)
21+
{
22+
return pullRequests.Where(pr =>
23+
{
24+
var prop = _selector.Selector(pr);
25+
return _regex.IsMatch(prop) ^ _negated;
26+
});
27+
}
28+
}
29+
}

Peer.Domain/Util/Validators.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ public class Validators
1414
public const string NotEmpty = "Cannot be empty";
1515
public const string UndefinedEnum = "Enum value cannot be undefined";
1616

17-
public static void ArgNotLessThanOrEqualToZero(int value, string name)
17+
public static void ArgNotLessThanOrEqualToZero(int value, [CallerArgumentExpression("value")] string? name = null)
1818
{
1919
if (value <= 0)
2020
throw new ArgumentException(NotLessThanOrEqualToZero, name);
2121
}
2222

23-
public static void ArgIsNotNullEmptyOrWhitespace(string value, string name)
23+
public static void ArgIsNotNullEmptyOrWhitespace(string value, [CallerArgumentExpression("value")] string? name = null)
2424
{
2525
if (value == null)
2626
throw new ArgumentNullException(name);
@@ -35,19 +35,19 @@ public static void ArgIsNotNull(object value, [CallerArgumentExpression("value")
3535
throw new ArgumentNullException(name);
3636
}
3737

38-
public static void ArgIsNotEmpty(Guid value, string name)
38+
public static void ArgIsNotEmpty(Guid value, [CallerArgumentExpression("value")] string? name = null)
3939
{
4040
if (value == Guid.Empty)
4141
throw new ArgumentException(NotGuidEmpty, name);
4242
}
4343

44-
public static void ArgIsNotEmpty(ICollection collection, string name)
44+
public static void ArgIsNotEmpty(ICollection collection, [CallerArgumentExpression("collection")] string? name = null)
4545
{
4646
if (collection?.Count == 0)
4747
throw new ArgumentException(NotEmpty, name);
4848
}
4949

50-
public static void ArgIsNotNullOrEmpty(ICollection collection, string name)
50+
public static void ArgIsNotNullOrEmpty(ICollection collection, [CallerArgumentExpression("collection")] string? name = null)
5151
{
5252
ArgIsNotNull(collection, name);
5353
ArgIsNotEmpty(collection, name);
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Peer.Domain;
7+
using Peer.Domain.Filters;
8+
using Peer.Parsing;
9+
using Xunit;
10+
11+
namespace Peer.UnitTests.Parsing
12+
{
13+
public class FilterParserTests
14+
{
15+
public class ParseFilterOption
16+
{
17+
[Fact]
18+
public void RawStringNull_Throws()
19+
{
20+
Assert.Throws<ArgumentNullException>(() => FilterParser.ParseFilterOption(null));
21+
}
22+
23+
[Fact]
24+
public void RawStringEmpty_ReturnsNotEnoughSections()
25+
{
26+
var value = FilterParser.ParseFilterOption(string.Empty);
27+
ResultAsserts.IsError(value, FilterParseError.NotEnoughSections);
28+
}
29+
30+
[Fact]
31+
public void RawStringHasNoDivider_ReturnsNotEnoughSections()
32+
{
33+
var value = FilterParser.ParseFilterOption("authorInsomniak");
34+
ResultAsserts.IsError(value, FilterParseError.NotEnoughSections);
35+
}
36+
37+
[Fact]
38+
public void RawStringHasTooManyDividersWithContent_ReturnsTooManySections()
39+
{
40+
var value = FilterParser.ParseFilterOption("author:Insomnia:k");
41+
ResultAsserts.IsError(value, FilterParseError.TooManySections);
42+
}
43+
44+
[Fact]
45+
public void KeyNotFound_ReturnsUnknownKey()
46+
{
47+
var value = FilterParser.ParseFilterOption("doot:Insomniak");
48+
ResultAsserts.IsError(value, FilterParseError.UnknownFilterKey);
49+
}
50+
51+
[Fact]
52+
public void RegexInvalidForStringKey_ReturnsUnknownMatchValue()
53+
{
54+
var value = FilterParser.ParseFilterOption("author:Insom**");
55+
ResultAsserts.IsError(value, FilterParseError.UnknownMatchValue);
56+
}
57+
58+
[Fact]
59+
public void IntInvalidForIntKey_ReturnsUnknownMatchValue()
60+
{
61+
var value = FilterParser.ParseFilterOption("id:waka");
62+
ResultAsserts.IsError(value, FilterParseError.UnknownMatchValue);
63+
}
64+
65+
[Fact]
66+
public void EnumValueInvalidForEnumKey_ReturnsUnknownMatchValue()
67+
{
68+
var value = FilterParser.ParseFilterOption("status:theBreadOne");
69+
ResultAsserts.IsError(value, FilterParseError.UnknownMatchValue);
70+
}
71+
72+
[Fact]
73+
public void RegexValidForStringKey_ReturnsRegexFilter()
74+
{
75+
var value = FilterParser.ParseFilterOption("author:waka");
76+
ResultAsserts.IsValue(value);
77+
Assert.IsType<RegexFilter>(value.Value);
78+
}
79+
80+
[Fact]
81+
public void RawStringHasTooManyDividersButNoContent_ReturnsRegexFilter()
82+
{
83+
var value = FilterParser.ParseFilterOption("author:Insomniak:::::::::");
84+
ResultAsserts.IsValue(value);
85+
Assert.IsType<RegexFilter>(value.Value);
86+
}
87+
88+
[Fact]
89+
public void RawStringHasLeadingOrTrailingWhitespaceInSections_ReturnsRegexFilter()
90+
{
91+
var value = FilterParser.ParseFilterOption("author :Insomniak\t\t\t");
92+
ResultAsserts.IsValue(value);
93+
Assert.IsType<RegexFilter>(value.Value);
94+
}
95+
96+
[Fact]
97+
public void IntValidForIntKey_ReturnsActionFilter()
98+
{
99+
var value = FilterParser.ParseFilterOption("id:10");
100+
ResultAsserts.IsValue(value);
101+
Assert.IsType<ActionFilter>(value.Value);
102+
}
103+
104+
[Fact]
105+
public void EnumValidForEnumKey_ReturnsEnumFilter()
106+
{
107+
var value = FilterParser.ParseFilterOption("status:Stale");
108+
ResultAsserts.IsValue(value);
109+
Assert.IsType<EnumMatchingFilter<PullRequestStatus>>(value.Value);
110+
}
111+
112+
[Fact]
113+
public void EnumValidCaseDoesntMatch_ReturnsEnumFilter()
114+
{
115+
//CN: Ensuring case insensitive parsing
116+
var value = FilterParser.ParseFilterOption("status:stale");
117+
ResultAsserts.IsValue(value);
118+
Assert.IsType<EnumMatchingFilter<PullRequestStatus>>(value.Value);
119+
}
120+
}
121+
}
122+
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
using Peer.UnitTests.Util;
88
using Xunit;
99

10-
namespace Peer.UnitTests
10+
namespace Peer.UnitTests.Parsing
1111
{
1212
public class SortParserTests
1313
{
@@ -29,21 +29,21 @@ public void SortOptionNull_Throws()
2929
public void SortOptionEmptyOrWhitespace_ReturnsNotEnoughSections(string value)
3030
{
3131
var res = SortParser.ParseSortOption(value);
32-
ResultAsserts.IsError(res, ParseError.NotEnoughSections);
32+
ResultAsserts.IsError(res, SortParseError.NotEnoughSections);
3333
}
3434

3535
[Fact]
3636
public void SortSectionsEmpty_ReturnsNotEnoughSections()
3737
{
3838
var res = SortParser.ParseSortOption(":");
39-
ResultAsserts.IsError(res, ParseError.NotEnoughSections);
39+
ResultAsserts.IsError(res, SortParseError.NotEnoughSections);
4040
}
4141

4242
[Fact]
4343
public void SortOptionHasTooManySections_ReturnsTooManySections()
4444
{
4545
var res = SortParser.ParseSortOption(":");
46-
ResultAsserts.IsError(res, ParseError.NotEnoughSections);
46+
ResultAsserts.IsError(res, SortParseError.NotEnoughSections);
4747
}
4848

4949
[Fact]
@@ -70,14 +70,14 @@ public async Task SortDirectionUnspecified_ReturnsAscendingSorter()
7070
public void PropertyNotAvailableForSorting_ReturnsUnknownSortKey()
7171
{
7272
var res = SortParser.ParseSortOption("floop:asc");
73-
ResultAsserts.IsError(res, ParseError.UnknownSortKey);
73+
ResultAsserts.IsError(res, SortParseError.UnknownSortKey);
7474
}
7575

7676
[Fact]
7777
public void DirectionInvalid_ReturnsInvalidSortDirection()
7878
{
7979
var res = SortParser.ParseSortOption("id:floop");
80-
ResultAsserts.IsError(res, ParseError.InvalidSortDirection);
80+
ResultAsserts.IsError(res, SortParseError.InvalidSortDirection);
8181
}
8282

8383
[Fact]

0 commit comments

Comments
 (0)