Skip to content

Commit efe844c

Browse files
committed
ParseFile accepts parse options
1 parent 489d99e commit efe844c

File tree

3 files changed

+102
-47
lines changed

3 files changed

+102
-47
lines changed

example_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package codeowners_test
33
import (
44
"bytes"
55
"fmt"
6+
"regexp"
67

78
"github.com/hmarr/codeowners"
89
)
@@ -41,6 +42,43 @@ func ExampleParseFile() {
4142
// Go code
4243
}
4344

45+
func ExampleParseFile_customOwnerMatchers() {
46+
validUsernames := []string{"the-a-team", "the-b-team"}
47+
usernameRegexp := regexp.MustCompile(`\A@([a-zA-Z0-9\-]+)\z`)
48+
49+
f := bytes.NewBufferString("src/**/*.go @the-a-team # Go code")
50+
ownerMatchers := []codeowners.OwnerMatcher{
51+
codeowners.OwnerMatchFunc(codeowners.MatchEmailOwner),
52+
codeowners.OwnerMatchFunc(func(s string) (codeowners.Owner, error) {
53+
// Custom owner matcher that only matches valid usernames
54+
match := usernameRegexp.FindStringSubmatch(s)
55+
if match == nil {
56+
return codeowners.Owner{}, codeowners.ErrNoMatch
57+
}
58+
59+
for _, t := range validUsernames {
60+
if t == match[1] {
61+
return codeowners.Owner{Value: match[1], Type: codeowners.TeamOwner}, nil
62+
}
63+
}
64+
return codeowners.Owner{}, codeowners.ErrNoMatch
65+
}),
66+
}
67+
ruleset, err := codeowners.ParseFile(f, codeowners.WithOwnerMatchers(ownerMatchers))
68+
if err != nil {
69+
panic(err)
70+
}
71+
fmt.Println(len(ruleset))
72+
fmt.Println(ruleset[0].RawPattern())
73+
fmt.Println(ruleset[0].Owners[0].String())
74+
fmt.Println(ruleset[0].Comment)
75+
// Output:
76+
// 1
77+
// src/**/*.go
78+
// @the-a-team
79+
// Go code
80+
}
81+
4482
func ExampleRuleset_Match() {
4583
f := bytes.NewBufferString("src/**/*.go @acme/go-developers # Go code")
4684
ruleset, _ := codeowners.ParseFile(f)

parse.go

+45-32
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ import (
1010
"strings"
1111
)
1212

13+
type parseOption func(*parseOptions)
14+
15+
type parseOptions struct {
16+
ownerMatchers []OwnerMatcher
17+
}
18+
19+
func WithOwnerMatchers(mm []OwnerMatcher) parseOption {
20+
return func(opts *parseOptions) {
21+
opts.ownerMatchers = mm
22+
}
23+
}
24+
25+
type OwnerMatcher interface {
26+
// Matches give string agains a pattern e.g. a regexp.
27+
// Should return ErrNoMatch if the pattern doesn't match.
28+
Match(s string) (Owner, error)
29+
}
30+
1331
type ErrInvalidOwnerFormat struct {
1432
Owner string
1533
}
@@ -26,30 +44,19 @@ var (
2644
usernameRegexp = regexp.MustCompile(`\A@([a-zA-Z0-9\-]+)\z`)
2745
)
2846

29-
const (
30-
statePattern = iota + 1
31-
stateOwners
32-
)
33-
34-
var DefaultMatchers = []Matcher{
35-
MatcherFunc(MatchEmail),
36-
MatcherFunc(MatchTeam),
37-
MatcherFunc(MatchUsername),
38-
}
39-
40-
type Matcher interface {
41-
// Matches give string agains a pattern e.g. a regexp.
42-
// Should return ErrNoMatch if the pattern doesn't match.
43-
Match(s string) (Owner, error)
47+
var DefaultOwnerMatchers = []OwnerMatcher{
48+
OwnerMatchFunc(MatchEmailOwner),
49+
OwnerMatchFunc(MatchTeamOwner),
50+
OwnerMatchFunc(MatchUsernameOwner),
4451
}
4552

46-
type MatcherFunc func(s string) (Owner, error)
53+
type OwnerMatchFunc func(s string) (Owner, error)
4754

48-
func (f MatcherFunc) Match(s string) (Owner, error) {
55+
func (f OwnerMatchFunc) Match(s string) (Owner, error) {
4956
return f(s)
5057
}
5158

52-
func MatchEmail(s string) (Owner, error) {
59+
func MatchEmailOwner(s string) (Owner, error) {
5360
match := emailRegexp.FindStringSubmatch(s)
5461
if match == nil {
5562
return Owner{}, ErrNoMatch
@@ -58,7 +65,7 @@ func MatchEmail(s string) (Owner, error) {
5865
return Owner{Value: match[0], Type: EmailOwner}, nil
5966
}
6067

61-
func MatchTeam(s string) (Owner, error) {
68+
func MatchTeamOwner(s string) (Owner, error) {
6269
match := teamRegexp.FindStringSubmatch(s)
6370
if match == nil {
6471
return Owner{}, ErrNoMatch
@@ -67,7 +74,7 @@ func MatchTeam(s string) (Owner, error) {
6774
return Owner{Value: match[1], Type: TeamOwner}, nil
6875
}
6976

70-
func MatchUsername(s string) (Owner, error) {
77+
func MatchUsernameOwner(s string) (Owner, error) {
7178
match := usernameRegexp.FindStringSubmatch(s)
7279
if match == nil {
7380
return Owner{}, ErrNoMatch
@@ -76,9 +83,14 @@ func MatchUsername(s string) (Owner, error) {
7683
return Owner{Value: match[1], Type: UsernameOwner}, nil
7784
}
7885

79-
// ParseFile parses a CODEOWNERS file and Matcher, returning a set of rules.
80-
// If no Matchers are passed explicitly the DefaultMatchers are used.
81-
func ParseFile(f io.Reader, mm ...Matcher) (Ruleset, error) {
86+
// ParseFile parses a CODEOWNERS file, returning a set of rules.
87+
// To override the default owner matchers, pass WithOwnerMatchers() as an option.
88+
func ParseFile(f io.Reader, options ...parseOption) (Ruleset, error) {
89+
opts := parseOptions{ownerMatchers: DefaultOwnerMatchers}
90+
for _, opt := range options {
91+
opt(&opts)
92+
}
93+
8294
rules := Ruleset{}
8395
scanner := bufio.NewScanner(f)
8496
lineNo := 0
@@ -91,7 +103,7 @@ func ParseFile(f io.Reader, mm ...Matcher) (Ruleset, error) {
91103
continue
92104
}
93105

94-
rule, err := parseRule(line, mm)
106+
rule, err := parseRule(line, opts)
95107
if err != nil {
96108
return nil, fmt.Errorf("line %d: %w", lineNo, err)
97109
}
@@ -101,8 +113,13 @@ func ParseFile(f io.Reader, mm ...Matcher) (Ruleset, error) {
101113
return rules, nil
102114
}
103115

116+
const (
117+
statePattern = iota + 1
118+
stateOwners
119+
)
120+
104121
// parseRule parses a single line of a CODEOWNERS file, returning a Rule struct
105-
func parseRule(ruleStr string, mm []Matcher) (Rule, error) {
122+
func parseRule(ruleStr string, opts parseOptions) (Rule, error) {
106123
r := Rule{}
107124

108125
state := statePattern
@@ -152,7 +169,7 @@ func parseRule(ruleStr string, mm []Matcher) (Rule, error) {
152169
// through whitespace before or after owner declarations
153170
if buf.Len() > 0 {
154171
ownerStr := buf.String()
155-
owner, err := newOwner(ownerStr, mm)
172+
owner, err := newOwner(ownerStr, opts.ownerMatchers)
156173
if err != nil {
157174
return r, fmt.Errorf("%w at position %d", err, i+1-len(ownerStr))
158175
}
@@ -188,7 +205,7 @@ func parseRule(ruleStr string, mm []Matcher) (Rule, error) {
188205
// If there's an owner left in the buffer, don't leave it behind
189206
if buf.Len() > 0 {
190207
ownerStr := buf.String()
191-
owner, err := newOwner(ownerStr, mm)
208+
owner, err := newOwner(ownerStr, opts.ownerMatchers)
192209
if err != nil {
193210
return r, fmt.Errorf("%s at position %d", err.Error(), len(ruleStr)+1-len(ownerStr))
194211
}
@@ -200,11 +217,7 @@ func parseRule(ruleStr string, mm []Matcher) (Rule, error) {
200217
}
201218

202219
// newOwner figures out which kind of owner this is and returns an Owner struct
203-
func newOwner(s string, mm []Matcher) (Owner, error) {
204-
if len(mm) == 0 {
205-
mm = DefaultMatchers
206-
}
207-
220+
func newOwner(s string, mm []OwnerMatcher) (Owner, error) {
208221
for _, m := range mm {
209222
o, err := m.Match(s)
210223
if errors.Is(err, ErrNoMatch) {

parse_test.go

+19-15
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88

99
func TestParseRule(t *testing.T) {
1010
examples := []struct {
11-
name string
12-
rule string
13-
matcher []Matcher
14-
expected Rule
15-
err string
11+
name string
12+
rule string
13+
ownerMatchers []OwnerMatcher
14+
expected Rule
15+
err string
1616
}{
1717
// Success cases
1818
{
@@ -146,35 +146,39 @@ func TestParseRule(t *testing.T) {
146146
{
147147
name: "email owners without email matcher",
148148
rule: "file.txt [email protected]",
149-
matcher: []Matcher{
150-
MatcherFunc(MatchTeam),
151-
MatcherFunc(MatchUsername),
149+
ownerMatchers: []OwnerMatcher{
150+
OwnerMatchFunc(MatchTeamOwner),
151+
OwnerMatchFunc(MatchUsernameOwner),
152152
},
153153
err: "invalid owner format '[email protected]' at position 10",
154154
},
155155
{
156156
name: "team owners without team matcher",
157157
rule: "file.txt @org/team",
158-
matcher: []Matcher{
159-
MatcherFunc(MatchEmail),
160-
MatcherFunc(MatchUsername),
158+
ownerMatchers: []OwnerMatcher{
159+
OwnerMatchFunc(MatchEmailOwner),
160+
OwnerMatchFunc(MatchUsernameOwner),
161161
},
162162
err: "invalid owner format '@org/team' at position 10",
163163
},
164164
{
165165
name: "username owners without username matcher",
166166
rule: "file.txt @user",
167-
matcher: []Matcher{
168-
MatcherFunc(MatchEmail),
169-
MatcherFunc(MatchTeam),
167+
ownerMatchers: []OwnerMatcher{
168+
OwnerMatchFunc(MatchEmailOwner),
169+
OwnerMatchFunc(MatchTeamOwner),
170170
},
171171
err: "invalid owner format '@user' at position 10",
172172
},
173173
}
174174

175175
for _, e := range examples {
176176
t.Run("parses "+e.name, func(t *testing.T) {
177-
actual, err := parseRule(e.rule, e.matcher)
177+
opts := parseOptions{ownerMatchers: DefaultOwnerMatchers}
178+
if e.ownerMatchers != nil {
179+
opts.ownerMatchers = e.ownerMatchers
180+
}
181+
actual, err := parseRule(e.rule, opts)
178182
if e.err != "" {
179183
assert.EqualError(t, err, e.err)
180184
} else {

0 commit comments

Comments
 (0)