Skip to content

Commit 8d76382

Browse files
committed
Initial commit
0 parents  commit 8d76382

5 files changed

Lines changed: 206 additions & 0 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# CODEOWNERS Decoder
2+
3+
CodeOwners decoder provides funcionality to evaluate CODEOWNERS file in Go.
4+
5+
## Documentation
6+
7+
To find documentation follow https://godoc.org/github.com/fmenezes/codeowners

codeowners.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Package codeowners provides funcionality to evaluate CODEOWNERS file.
2+
package codeowners // import "github.com/fmenezes/codeowners"
3+
4+
import (
5+
"bufio"
6+
"io"
7+
"strings"
8+
)
9+
10+
// DefaultLocations provides default locations for the CODEOWNERS file
11+
var DefaultLocations = [...]string{"CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"}
12+
13+
// Decoder providers functionality to read CODEOWNERS data
14+
type Decoder struct {
15+
scanner *bufio.Scanner
16+
line *string
17+
done bool
18+
}
19+
20+
// New generates a new CodeOwnersScanner instance. The reader should be a reader containing the contents of the CODEOWNERS file
21+
func New(r io.Reader) *Decoder {
22+
return &Decoder{
23+
scanner: bufio.NewScanner(r),
24+
line: nil,
25+
done: false,
26+
}
27+
}
28+
29+
func (s *Decoder) peek() {
30+
if !s.scanner.Scan() {
31+
s.done = true
32+
}
33+
line := sanitiseLine(s.scanner.Text())
34+
s.line = &line
35+
if len(*s.line) == 0 && !s.done {
36+
s.peek()
37+
}
38+
}
39+
40+
func sanitiseLine(line string) string {
41+
i := strings.Index(line, "#")
42+
if i >= 0 {
43+
line = line[:i]
44+
}
45+
return strings.Trim(line, " ")
46+
}
47+
48+
// More returns if there are available CODEOWNERS lines to be scanned
49+
func (s *Decoder) More() bool {
50+
s.peek()
51+
return !s.done
52+
}
53+
54+
// Token parses the next available line in the CODEOWNERS file
55+
func (s *Decoder) Token() Token {
56+
line := strings.ReplaceAll(*s.line, "\\ ", "\\s")
57+
58+
data := strings.Split(line, " ")
59+
60+
for i := range data {
61+
data[i] = strings.ReplaceAll(data[i], "\\s", " ")
62+
}
63+
64+
return Token{
65+
path: data[0],
66+
owners: data[1:],
67+
}
68+
}
69+
70+
// Token providers reading capabilities for every CODEOWNERS line
71+
type Token struct {
72+
path string
73+
owners []string
74+
}
75+
76+
// Path returns the file path pattern
77+
func (t Token) Path() string {
78+
return t.path
79+
}
80+
81+
// Owners returns the owners
82+
func (t Token) Owners() []string {
83+
return t.owners
84+
}

codeowners_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package codeowners_test
2+
3+
import (
4+
"reflect"
5+
"strings"
6+
"testing"
7+
8+
"github.com/fmenezes/codeowners"
9+
)
10+
11+
func exec(input string) ([][]string, int) {
12+
scanner := codeowners.New(strings.NewReader(input))
13+
got := [][]string{}
14+
c := 0
15+
for scanner.More() {
16+
c++
17+
token := scanner.Token()
18+
got = append(got, append([]string{token.Path()}, token.Owners()...))
19+
}
20+
return got, c
21+
}
22+
23+
func assert(t *testing.T, input string, want [][]string) {
24+
got, gotCount := exec(input)
25+
if !reflect.DeepEqual(got, want) {
26+
t.Errorf("Input: %v, Want: %v, Got: %v", input, want, got)
27+
}
28+
if gotCount != len(want) {
29+
t.Errorf("Input: %v, Want: %v scans, Got: %v scans", input, len(want), gotCount)
30+
}
31+
}
32+
33+
func TestSimple(t *testing.T) {
34+
assert(t, `* test@example.org`, [][]string{
35+
{"*", "test@example.org"},
36+
})
37+
}
38+
39+
func TestMultipleOwners(t *testing.T) {
40+
assert(t, `* test@example.org @owner @company/team`, [][]string{
41+
{"*", "test@example.org", "@owner", "@company/team"},
42+
})
43+
}
44+
45+
func TestFilesWithSpaces(t *testing.T) {
46+
assert(t, `file\ with\ spaces @owner`, [][]string{
47+
{"file with spaces", "@owner"},
48+
})
49+
}
50+
51+
func TestMultipleLines(t *testing.T) {
52+
assert(t, `* test@example.org
53+
file @owner`, [][]string{
54+
{"*", "test@example.org"},
55+
{"file", "@owner"},
56+
})
57+
}
58+
59+
func TestEmptyFile(t *testing.T) {
60+
assert(t, ``, [][]string{})
61+
}
62+
63+
func TestEmptyLines(t *testing.T) {
64+
assert(t, `* test@example.org
65+
66+
67+
68+
file @owner
69+
70+
71+
72+
73+
`, [][]string{
74+
{"*", "test@example.org"},
75+
{"file", "@owner"},
76+
})
77+
}
78+
79+
func TestIgnoreComments(t *testing.T) {
80+
assert(t, `* test@example.org # comment
81+
# comment
82+
file @owner`, [][]string{
83+
{"*", "test@example.org"},
84+
{"file", "@owner"},
85+
})
86+
}
87+
88+
func TestNoOwners(t *testing.T) {
89+
assert(t, `*`, [][]string{
90+
{"*"},
91+
})
92+
}

example_codeowners_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package codeowners_test
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/fmenezes/codeowners"
8+
)
9+
10+
func Example() {
11+
codeownerDecoder := codeowners.New(strings.NewReader(`* test@example.org`))
12+
for codeownerDecoder.More() {
13+
token := codeownerDecoder.Token()
14+
fmt.Printf("File Pattern: %s\n", token.Path())
15+
fmt.Printf("Owners: %v\n", token.Owners())
16+
}
17+
// Output:
18+
// File Pattern: *
19+
// Owners: [test@example.org]
20+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/fmenezes/codeowners
2+
3+
go 1.14

0 commit comments

Comments
 (0)