-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(parsing): build a minimal commit-type parser
Using parser combinators and names similar to https://github.com/Geal/nom
- Loading branch information
Showing
3 changed files
with
252 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
module github.com/skalt/git-cc | ||
|
||
go 1.14 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
func TestParsingCommitTypes(t *testing.T) { | ||
var testExpectedMatch = func( | ||
input string, | ||
expected string, | ||
remainder string, | ||
) func(t *testing.T) { | ||
result, err := CommitType([]rune(input)) | ||
return func(t *testing.T) { | ||
if err != nil { | ||
fmt.Printf("%v", err) | ||
t.Fail() | ||
return | ||
} | ||
if result.Output != expected { | ||
fmt.Printf( | ||
"expected output %v, not %v\n", expected, result.Output, | ||
) | ||
t.Fail() | ||
} | ||
if string(result.Remaining) != remainder { | ||
fmt.Printf( | ||
"expected remainder %v, not %v\n", expected, result.Output, | ||
) | ||
t.Fail() | ||
} | ||
} | ||
} | ||
|
||
t.Run( | ||
"accepts valid commit types [fix]", | ||
testExpectedMatch("fix", "fix", ""), | ||
) | ||
|
||
t.Run( | ||
"accepts valid commit types [feat]", | ||
testExpectedMatch("feat", "feat", ""), | ||
) | ||
t.Run( | ||
"watch out: only matches the first runes", | ||
testExpectedMatch("fixing", "fix", "ing"), | ||
) | ||
t.Run("rejects invalid commit types", func(t *testing.T) { | ||
_, err := CommitType([]rune("foo")) | ||
if err == nil { | ||
t.Fail() | ||
} | ||
}) | ||
} | ||
|
||
func TestTakeUntil(t *testing.T) { | ||
var callback = func(chars []rune) interface{} { | ||
return string(chars) | ||
} | ||
var test = func(input string, until rune, output string, remaining string) func(t *testing.T) { | ||
return func(t *testing.T) { | ||
result, err := TakeUntil(until, callback)([]rune(input)) | ||
if err != nil { | ||
fmt.Println("err != nil") | ||
t.Fail() | ||
} | ||
if result.Output != output { | ||
fmt.Printf("unexpected output %v (should be %v)\n", result.Output, output) | ||
t.Fail() | ||
} | ||
if string(result.Remaining) != remaining { | ||
fmt.Printf("unexpected remaining \"%v\" (should be %v)\n", string(result.Remaining), remaining) | ||
t.Fail() | ||
} | ||
} | ||
} | ||
t.Run( | ||
"matching in the middle of the input works", | ||
test("abcdef", 'c' /* output: */, "ab" /* remaining: */, "cdef"), | ||
) | ||
t.Run( | ||
"matching at the start of the input works", | ||
test("abcdef", 'a' /* output: */, "" /* remaining: */, "abcdef"), | ||
) | ||
t.Run( | ||
"matching at the end of the input works", | ||
test("abcdef", 'f' /* output: */, "abcde" /* remaining: */, "f"), | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
) | ||
|
||
// see https://medium.com/@armin.heller/using-parser-combinators-in-go-e63b3ad69c94 | ||
// and https://github.com/Geal/nom | ||
|
||
type ParserResult struct { | ||
Output interface{} | ||
Remaining []rune | ||
} | ||
type ParserCallback func([]rune) interface{} | ||
type Parser func([]rune) (ParserResult, error) | ||
|
||
func ident(input []rune) interface{} { | ||
return []rune(input) | ||
} | ||
func TakeUntil(character rune, callback ParserCallback) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
for i, nextChar := range input { | ||
if nextChar == character { | ||
output := callback(input[:i]) | ||
return ParserResult{output, input[i:]}, nil | ||
} | ||
} | ||
return ParserResult{nil, input}, fmt.Errorf("didn't match '%v'", character) | ||
} | ||
} | ||
|
||
func Opt(parser Parser) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
result, _ := parser(input) | ||
return result, nil | ||
} | ||
} | ||
|
||
func LiteralRune(match rune, callback ParserCallback) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
if len(input) > 0 { | ||
if input[0] == match { | ||
return ParserResult{callback([]rune{match}), input[1:]}, nil | ||
} else { | ||
return ParserResult{nil, input}, fmt.Errorf("%v not matched", match) | ||
} | ||
} else { | ||
return ParserResult{nil, input}, fmt.Errorf("no input") | ||
} | ||
} | ||
} | ||
|
||
func NotMatching(parser Parser) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
result, err := parser(input) | ||
if err == nil { | ||
return result, nil | ||
} else { | ||
return ParserResult{nil, input}, fmt.Errorf("wasn't expecting to match %v", parser) | ||
} | ||
} | ||
} | ||
|
||
func toRunes(i interface{}) []rune { | ||
switch i.(type) { | ||
case string: | ||
str, _ := i.(string) | ||
return []rune(str) | ||
case rune: | ||
r, _ := i.(rune) | ||
return []rune{r} | ||
case []rune: | ||
runes, _ := i.([]rune) | ||
return runes | ||
default: | ||
panic(fmt.Errorf("%v is not string or []rune", i)) | ||
} | ||
} | ||
|
||
func Tag(tag interface{}) Parser { | ||
toMatch := toRunes(tag) | ||
return func(input []rune) (ParserResult, error) { | ||
if len(toMatch) > len(input) { | ||
return ParserResult{nil, input}, fmt.Errorf("input longer than tag") | ||
} | ||
for i, matching := range toMatch { | ||
if input[i] != matching { | ||
err := fmt.Errorf( | ||
"\"%v\" does not match \"%v\"", | ||
string(input[:i+1]), | ||
string(toMatch), | ||
) | ||
return ParserResult{nil, input}, err | ||
} | ||
} | ||
return ParserResult{string(toMatch), input[len(toMatch):]}, nil | ||
} | ||
} | ||
|
||
// func MatchingString(str string, callback ParserCallback) Parser { | ||
// var toMatch = []rune(str) | ||
|
||
// } | ||
|
||
func AnyOf(parsers ...Parser) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
for _, parser := range parsers { | ||
result, err := parser(input) | ||
if err == nil { | ||
return result, err | ||
} | ||
} | ||
return ParserResult{nil, input}, fmt.Errorf("expected a parser to match") | ||
} | ||
} | ||
|
||
func asString(result []rune) interface{} { | ||
return string(result) | ||
} | ||
|
||
func Sequence(parsers ...Parser) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
var currentInput = []rune{} | ||
results := make([]interface{}, len(currentInput)) | ||
copy(currentInput, input) | ||
for _, parser := range parsers { | ||
result, err := parser(currentInput) | ||
if err != nil { | ||
return ParserResult{nil, input}, err | ||
} else { | ||
currentInput = result.Remaining | ||
results = append(results, result.Output) | ||
} | ||
} | ||
return ParserResult{results, currentInput}, nil | ||
} | ||
} | ||
|
||
func Delimeted(start Parser, middle Parser, end Parser) Parser { | ||
return func(input []rune) (ParserResult, error) { | ||
result, err := Sequence(start, middle, end)(input) | ||
if err != nil { | ||
return ParserResult{nil, input}, err | ||
} | ||
results, _ := result.Output.([]string) | ||
return ParserResult{results[1], result.Remaining}, nil | ||
} | ||
} | ||
|
||
var CommitType = AnyOf(Tag("feat"), Tag("fix")) | ||
var Scope = Delimeted( | ||
Tag('('), | ||
NotMatching(Tag(')')), | ||
Tag(')'), | ||
) | ||
var BreakingChangeBang = Tag("!") | ||
var CommitTypeContext = Sequence(CommitType, Opt(Scope), Opt(BreakingChangeBang)) | ||
|
||
func main() {} |