Skip to content

Commit e67b298

Browse files
author
schlehlein
committed
feat: add matcher
1 parent bd19ed7 commit e67b298

File tree

3 files changed

+117
-31
lines changed

3 files changed

+117
-31
lines changed

codeowners.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,28 @@
55
// the CODEOWNERS file format into rulesets, which may then be used to determine
66
// the ownership of files.
77
//
8-
// Usage
8+
// # Usage
99
//
1010
// To find the owner of a given file, parse a CODEOWNERS file and call Match()
1111
// on the resulting ruleset.
12-
// ruleset, err := codeowners.ParseFile(file)
13-
// if err != nil {
14-
// log.Fatal(err)
15-
// }
1612
//
17-
// rule, err := ruleset.Match("path/to/file")
18-
// if err != nil {
19-
// log.Fatal(err)
20-
// }
13+
// ruleset, err := codeowners.ParseFile(file)
14+
// if err != nil {
15+
// log.Fatal(err)
16+
// }
2117
//
22-
// Command line interface
18+
// rule, err := ruleset.Match("path/to/file")
19+
// if err != nil {
20+
// log.Fatal(err)
21+
// }
22+
//
23+
// # Command line interface
2324
//
2425
// A command line interface is also available in the cmd/codeowners package.
2526
// When run, it will walk the directory tree showing the code owners for each
2627
// file encountered. The help flag lists available options.
2728
//
28-
// $ codeowners --help
29+
// $ codeowners --help
2930
package codeowners
3031

3132
import (

parse.go

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,23 @@ package codeowners
33
import (
44
"bufio"
55
"bytes"
6+
"errors"
67
"fmt"
78
"io"
89
"regexp"
910
"strings"
1011
)
1112

13+
type ErrInvalidOwnerFormat struct {
14+
Owner string
15+
}
16+
17+
func (err ErrInvalidOwnerFormat) Error() string {
18+
return fmt.Sprintf("invalid owner format '%s'", err.Owner)
19+
}
20+
21+
var ErrNoMatch = errors.New("no match")
22+
1223
var (
1324
emailRegexp = regexp.MustCompile(`\A[A-Z0-9a-z\._%\+\-]+@[A-Za-z0-9\.\-]+\.[A-Za-z]{2,6}\z`)
1425
teamRegexp = regexp.MustCompile(`\A@([a-zA-Z0-9\-]+\/[a-zA-Z0-9_\-]+)\z`)
@@ -20,8 +31,52 @@ const (
2031
stateOwners
2132
)
2233

23-
// ParseFile parses a CODEOWNERS file, returning a set of rules.
24-
func ParseFile(f io.Reader) (Ruleset, error) {
34+
var DefaultMatchers = []Matcher{
35+
MatcherFunc(EmailMatcher),
36+
MatcherFunc(TeamMatcher),
37+
MatcherFunc(UsernameMatcher),
38+
}
39+
40+
type Matcher interface {
41+
Match(s string) (Owner, error)
42+
}
43+
44+
type MatcherFunc func(s string) (Owner, error)
45+
46+
func (f MatcherFunc) Match(s string) (Owner, error) {
47+
return f(s)
48+
}
49+
50+
func EmailMatcher(s string) (Owner, error) {
51+
match := emailRegexp.FindStringSubmatch(s)
52+
if match == nil {
53+
return Owner{}, ErrNoMatch
54+
}
55+
56+
return Owner{Value: match[0], Type: EmailOwner}, nil
57+
}
58+
59+
func TeamMatcher(s string) (Owner, error) {
60+
match := teamRegexp.FindStringSubmatch(s)
61+
if match == nil {
62+
return Owner{}, ErrNoMatch
63+
}
64+
65+
return Owner{Value: match[1], Type: TeamOwner}, nil
66+
}
67+
68+
func UsernameMatcher(s string) (Owner, error) {
69+
match := usernameRegexp.FindStringSubmatch(s)
70+
if match == nil {
71+
return Owner{}, ErrNoMatch
72+
}
73+
74+
return Owner{Value: match[1], Type: UsernameOwner}, nil
75+
}
76+
77+
// ParseFile parses a CODEOWNERS file and Matcher, returning a set of rules.
78+
// If no Matchers are passed explicitly the DefaultMatchers are used.
79+
func ParseFile(f io.Reader, mm ...Matcher) (Ruleset, error) {
2580
rules := Ruleset{}
2681
scanner := bufio.NewScanner(f)
2782
lineNo := 0
@@ -34,7 +89,7 @@ func ParseFile(f io.Reader) (Ruleset, error) {
3489
continue
3590
}
3691

37-
rule, err := parseRule(line)
92+
rule, err := parseRule(line, mm)
3893
if err != nil {
3994
return nil, fmt.Errorf("line %d: %w", lineNo, err)
4095
}
@@ -45,7 +100,7 @@ func ParseFile(f io.Reader) (Ruleset, error) {
45100
}
46101

47102
// parseRule parses a single line of a CODEOWNERS file, returning a Rule struct
48-
func parseRule(ruleStr string) (Rule, error) {
103+
func parseRule(ruleStr string, mm []Matcher) (Rule, error) {
49104
r := Rule{}
50105

51106
state := statePattern
@@ -95,9 +150,9 @@ func parseRule(ruleStr string) (Rule, error) {
95150
// through whitespace before or after owner declarations
96151
if buf.Len() > 0 {
97152
ownerStr := buf.String()
98-
owner, err := newOwner(ownerStr)
153+
owner, err := newOwner(ownerStr, mm)
99154
if err != nil {
100-
return r, fmt.Errorf("%s at position %d", err.Error(), i+1-len(ownerStr))
155+
return r, fmt.Errorf("%w at position %d", err, i+1-len(ownerStr))
101156
}
102157
r.Owners = append(r.Owners, owner)
103158
buf.Reset()
@@ -131,7 +186,7 @@ func parseRule(ruleStr string) (Rule, error) {
131186
// If there's an owner left in the buffer, don't leave it behind
132187
if buf.Len() > 0 {
133188
ownerStr := buf.String()
134-
owner, err := newOwner(ownerStr)
189+
owner, err := newOwner(ownerStr, mm)
135190
if err != nil {
136191
return r, fmt.Errorf("%s at position %d", err.Error(), len(ruleStr)+1-len(ownerStr))
137192
}
@@ -143,23 +198,25 @@ func parseRule(ruleStr string) (Rule, error) {
143198
}
144199

145200
// newOwner figures out which kind of owner this is and returns an Owner struct
146-
func newOwner(s string) (Owner, error) {
147-
match := emailRegexp.FindStringSubmatch(s)
148-
if match != nil {
149-
return Owner{Value: match[0], Type: EmailOwner}, nil
201+
func newOwner(s string, mm []Matcher) (Owner, error) {
202+
if len(mm) == 0 {
203+
mm = DefaultMatchers
150204
}
151205

152-
match = teamRegexp.FindStringSubmatch(s)
153-
if match != nil {
154-
return Owner{Value: match[1], Type: TeamOwner}, nil
155-
}
206+
for _, m := range mm {
207+
o, err := m.Match(s)
208+
if errors.Is(err, ErrNoMatch) {
209+
continue
210+
} else if err != nil {
211+
return Owner{}, err
212+
}
156213

157-
match = usernameRegexp.FindStringSubmatch(s)
158-
if match != nil {
159-
return Owner{Value: match[1], Type: UsernameOwner}, nil
214+
return o, nil
160215
}
161216

162-
return Owner{}, fmt.Errorf("invalid owner format '%s'", s)
217+
return Owner{}, ErrInvalidOwnerFormat{
218+
Owner: s,
219+
}
163220
}
164221

165222
func isWhitespace(ch rune) bool {

parse_test.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ func TestParseRule(t *testing.T) {
1010
examples := []struct {
1111
name string
1212
rule string
13+
matcher []Matcher
1314
expected Rule
1415
err string
1516
}{
@@ -142,11 +143,38 @@ func TestParseRule(t *testing.T) {
142143
rule: "file.txt missing-at-sign",
143144
err: "invalid owner format 'missing-at-sign' at position 10",
144145
},
146+
{
147+
name: "email owners without email matcher",
148+
rule: "file.txt foo@example.com",
149+
matcher: []Matcher{
150+
MatcherFunc(TeamMatcher),
151+
MatcherFunc(UsernameMatcher),
152+
},
153+
err: "invalid owner format 'foo@example.com' at position 10",
154+
},
155+
{
156+
name: "team owners without team matcher",
157+
rule: "file.txt @org/team",
158+
matcher: []Matcher{
159+
MatcherFunc(EmailMatcher),
160+
MatcherFunc(UsernameMatcher),
161+
},
162+
err: "invalid owner format '@org/team' at position 10",
163+
},
164+
{
165+
name: "username owners without username matcher",
166+
rule: "file.txt @user",
167+
matcher: []Matcher{
168+
MatcherFunc(EmailMatcher),
169+
MatcherFunc(TeamMatcher),
170+
},
171+
err: "invalid owner format '@user' at position 10",
172+
},
145173
}
146174

147175
for _, e := range examples {
148176
t.Run("parses "+e.name, func(t *testing.T) {
149-
actual, err := parseRule(e.rule)
177+
actual, err := parseRule(e.rule, e.matcher)
150178
if e.err != "" {
151179
assert.EqualError(t, err, e.err)
152180
} else {

0 commit comments

Comments
 (0)