Normalizing error handling, to always include the caller location in errors. This makes debugging a lot easier for users of the package, because it doesn't say stuff like 'Method() was called incorrectly', but instead something like 'Method() was called incorrectlty at /path/to/file.go:1234'.

This commit is contained in:
Maurice Makaay 2019-06-05 10:07:50 +00:00
parent 75373e5ed5
commit 05585db341
16 changed files with 301 additions and 212 deletions

View File

@ -2,6 +2,8 @@ package parsekit
import ( import (
"fmt" "fmt"
"runtime"
"strings"
) )
// Error is used as the error type when parsing errors occur. // Error is used as the error type when parsing errors occur.
@ -14,8 +16,7 @@ type Error struct {
func (err *Error) Error() string { func (err *Error) Error() string {
if err == nil { if err == nil {
_, linepos := getCaller(1) callerPanic(1, "parsekit.Error.Error(): method called with nil error at {caller}")
panic(fmt.Sprintf("parsekit.Error.Error(): method called with nil error at %s", linepos))
} }
return err.Message return err.Message
} }
@ -24,8 +25,29 @@ func (err *Error) Error() string {
// the position in the input where the error occurred. // the position in the input where the error occurred.
func (err *Error) Full() string { func (err *Error) Full() string {
if err == nil { if err == nil {
_, linepos := getCaller(1) callerPanic(1, "parsekit.Error.Full(): method called with nil error at {caller}")
panic(fmt.Sprintf("parsekit.Error.Full(): method called with nil error at %s", linepos))
} }
return fmt.Sprintf("%s at %s", err, err.Cursor) return fmt.Sprintf("%s at %s", err, err.Cursor)
} }
func callerFunc(depth int) string {
// No error handling, because we call this method ourselves with safe depth values.
pc, _, _, _ := runtime.Caller(depth + 1)
caller := runtime.FuncForPC(pc)
parts := strings.Split(caller.Name(), ".")
funcName := parts[len(parts)-1]
return funcName
}
func callerFilepos(depth int) string {
// No error handling, because we call this method ourselves with safe depth values.
_, file, line, _ := runtime.Caller(depth + 1)
return fmt.Sprintf("%s:%d", file, line)
}
func callerPanic(depth int, f string, args ...interface{}) {
filepos := callerFilepos(depth + 1)
m := fmt.Sprintf(f, args...)
m = strings.Replace(m, "{caller}", filepos, 1)
panic(m)
}

View File

@ -41,10 +41,10 @@ func Example_basicCalculator1() {
// Input: "1+2+3", got outcome: 6, correct = true // Input: "1+2+3", got outcome: 6, correct = true
// Input: " 10 + \t20 - 3 + 7 -10 ", got outcome: 24, correct = true // Input: " 10 + \t20 - 3 + 7 -10 ", got outcome: 24, correct = true
// Input: "", got error: unexpected end of file (expected integer number) // Input: "", got error: unexpected end of file (expected integer number)
// Input: " \t ", got error: unexpected character ' ' (expected integer number) // Input: " \t ", got error: unexpected input (expected integer number)
// Input: "+", got error: unexpected character '+' (expected integer number) // Input: "+", got error: unexpected input (expected integer number)
// Input: "10.8 + 12", got error: unexpected character '.' (expected operator, '+' or '-') // Input: "10.8 + 12", got error: unexpected input (expected operator, '+' or '-')
// Input: "42+ ", got error: unexpected character ' ' (expected integer number) // Input: "42+ ", got error: unexpected input (expected integer number)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -56,7 +56,7 @@ func Example_basicCalculator2() {
// Input: "", got error: unexpected end of file at start of file // Input: "", got error: unexpected end of file at start of file
// Input: "(", got error: unexpected end of file at line 1, column 2 // Input: "(", got error: unexpected end of file at line 1, column 2
// Input: "10+20-", got error: unexpected end of file at line 1, column 7 // Input: "10+20-", got error: unexpected end of file at line 1, column 7
// Input: "10+20-(4*10))", got error: unexpected character ')' (expected end of file) at line 1, column 13 // Input: "10+20-(4*10))", got error: unexpected input (expected end of file) at line 1, column 13
// Input: "10+20-((4*10) + 17", got error: unexpected end of file (expected ')') at line 1, column 19 // Input: "10+20-((4*10) + 17", got error: unexpected end of file (expected ')') at line 1, column 19
} }

View File

@ -40,11 +40,11 @@ func Example_dutchPostcodeUsingTokenizer() {
// [1] Input: "2233Ab" Output: 2233 AB Tokens: PCD(2233) PCL(AB) // [1] Input: "2233Ab" Output: 2233 AB Tokens: PCD(2233) PCL(AB)
// [2] Input: "1001\t\tab" Output: 1001 AB Tokens: PCD(1001) PCL(AB) // [2] Input: "1001\t\tab" Output: 1001 AB Tokens: PCD(1001) PCL(AB)
// [3] Input: "1818ab" Output: 1818 AB Tokens: PCD(1818) PCL(AB) // [3] Input: "1818ab" Output: 1818 AB Tokens: PCD(1818) PCL(AB)
// [4] Input: "1212abc" Error: unexpected character '1' (expected a Dutch postcode) at start of file // [4] Input: "1212abc" Error: unexpected input (expected a Dutch postcode) at start of file
// [5] Input: "1234" Error: unexpected character '1' (expected a Dutch postcode) at start of file // [5] Input: "1234" Error: unexpected input (expected a Dutch postcode) at start of file
// [6] Input: "huh" Error: unexpected character 'h' (expected a Dutch postcode) at start of file // [6] Input: "huh" Error: unexpected input (expected a Dutch postcode) at start of file
// [7] Input: "" Error: unexpected end of file (expected a Dutch postcode) at start of file // [7] Input: "" Error: unexpected end of file (expected a Dutch postcode) at start of file
// [8] Input: "\xcd2222AB" Error: unexpected character '<27>' (expected a Dutch postcode) at start of file // [8] Input: "\xcd2222AB" Error: unexpected input (expected a Dutch postcode) at start of file
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -24,6 +24,7 @@ import (
func Example_helloWorldUsingParser1() { func Example_helloWorldUsingParser1() {
for i, input := range []string{ for i, input := range []string{
"Oh!",
"Hello, world!", "Hello, world!",
"HELLO ,Johnny!", "HELLO ,Johnny!",
"hello , Bob123!", "hello , Bob123!",
@ -50,17 +51,17 @@ func Example_helloWorldUsingParser1() {
// [0] Input: "Hello, world!" Output: world // [0] Input: "Hello, world!" Output: world
// [1] Input: "HELLO ,Johnny!" Output: Johnny // [1] Input: "HELLO ,Johnny!" Output: Johnny
// [2] Input: "hello , Bob123!" Output: Bob123 // [2] Input: "hello , Bob123!" Output: Bob123
// [3] Input: "hello Pizza!" Error: unexpected character 'P' (expected comma) // [3] Input: "hello Pizza!" Error: unexpected input (expected comma)
// [4] Input: "" Error: unexpected end of file (expected hello) // [4] Input: "" Error: unexpected end of file (expected hello)
// [5] Input: " " Error: unexpected character ' ' (expected hello) // [5] Input: " " Error: unexpected input (expected hello)
// [6] Input: "hello" Error: unexpected end of file (expected comma) // [6] Input: "hello" Error: unexpected end of file (expected comma)
// [7] Input: "hello," Error: unexpected end of file (expected name) // [7] Input: "hello," Error: unexpected end of file (expected name)
// [8] Input: "hello , " Error: unexpected end of file (expected name) // [8] Input: "hello , " Error: unexpected end of file (expected name)
// [9] Input: "hello , Droopy" Error: unexpected end of file (expected exclamation) // [9] Input: "hello , Droopy" Error: unexpected end of file (expected exclamation)
// [10] Input: "hello , Droopy!" Output: Droopy // [10] Input: "hello , Droopy!" Output: Droopy
// [11] Input: "hello , \t \t Droopy \t !" Output: Droopy // [11] Input: "hello , \t \t Droopy \t !" Output: Droopy
// [12] Input: "Oh no!" Error: unexpected character 'O' (expected hello) // [12] Input: "Oh no!" Error: unexpected input (expected hello)
// [13] Input: "hello,!" Error: unexpected character '!' (expected name) // [13] Input: "hello,!" Error: unexpected input (expected name)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -37,9 +37,9 @@ func Example_helloWorldUsingTokenizer() {
// [1] Input: "HELLO ,Johnny!" Output: Johnny // [1] Input: "HELLO ,Johnny!" Output: Johnny
// [2] Input: "hello , Bob123!" Output: Bob123 // [2] Input: "hello , Bob123!" Output: Bob123
// [3] Input: "hello Pizza!" Output: Pizza // [3] Input: "hello Pizza!" Output: Pizza
// [4] Input: "Oh no!" Error: unexpected character 'O' (expected a friendly greeting) at start of file // [4] Input: "Oh no!" Error: unexpected input (expected a friendly greeting) at start of file
// [5] Input: "Hello, world" Error: unexpected character 'H' (expected a friendly greeting) at start of file // [5] Input: "Hello, world" Error: unexpected input (expected a friendly greeting) at start of file
// [6] Input: "Hello,!" Error: unexpected character 'H' (expected a friendly greeting) at start of file // [6] Input: "Hello,!" Error: unexpected input (expected a friendly greeting) at start of file
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -3,7 +3,6 @@ package parsekit
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
) )
// ParseAPI holds the internal state of a parse run and provides an API to // ParseAPI holds the internal state of a parse run and provides an API to
@ -29,20 +28,15 @@ func (p *ParseAPI) panicWhenStoppedOrInError() {
return return
} }
called, _ := getCaller(1) called := callerFunc(1)
parts := strings.Split(called, ".")
calledShort := parts[len(parts)-1]
_, filepos := getCaller(2)
after := "Error()" after := "Error()"
if p.stopped { if p.stopped {
after = "Stop()" after = "Stop()"
} }
panic(fmt.Sprintf( callerPanic(2, "parsekit.ParseAPI.%s(): Illegal call to %s() at {caller}: "+
"parsekit.ParseAPI.%s(): Illegal call to %s() at %s: "+ "no calls allowed after ParseAPI.%s", called, called, after)
"no calls allowed after ParseAPI.%s",
calledShort, calledShort, filepos, after))
} }
func (p *ParseAPI) isStoppedOrInError() bool { func (p *ParseAPI) isStoppedOrInError() bool {
@ -54,9 +48,9 @@ func (p *ParseAPI) initLoopCheck() {
} }
func (p *ParseAPI) checkForLoops() { func (p *ParseAPI) checkForLoops() {
_, filepos := getCaller(2) filepos := callerFilepos(2)
if _, ok := p.loopCheck[filepos]; ok { if _, ok := p.loopCheck[filepos]; ok {
panic(fmt.Sprintf("parsekit.ParseAPI: Loop detected in parser at %s", filepos)) callerPanic(2, "parsekit.ParseAPI: Loop detected in parser at {caller}")
} }
p.loopCheck[filepos] = true p.loopCheck[filepos] = true
} }
@ -65,9 +59,9 @@ func (p *ParseAPI) checkForLoops() {
// TokenHandler. On must be chained with another method that tells the parser // TokenHandler. On must be chained with another method that tells the parser
// what action to perform when a match was found: // what action to perform when a match was found:
// //
// 1) On(...).Skip() - Only move cursor forward, ignore the matched runes. // 1) On(...).Skip() - Move read cursor forward, ignoring the match results.
// //
// 2) On(...).Accept() - Move cursor forward, add runes to parsers's string buffer. // 2) On(...).Accept() - Move cursor, making results available through Result()
// //
// 3) On(...).Stay() - Do nothing, the cursor stays at the same position. // 3) On(...).Stay() - Do nothing, the cursor stays at the same position.
// //
@ -93,18 +87,15 @@ func (p *ParseAPI) checkForLoops() {
// p.RouteTo(stateHandlerC) // p.RouteTo(stateHandlerC)
// } // }
// //
// // When there's a "hi" on input, then say hello. // // Echo back a sequence of digits on the input.
// if p.On(parsekit.C.Str("hi")).Accept() { // if p.On(parsekit.A.Digits).Accept() {
// fmt.Println("Hello!") // fmt.Println(p.Result().String())
// } // }
func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction { func (p *ParseAPI) On(tokenHandler TokenHandler) *ParseAPIOnAction {
p.panicWhenStoppedOrInError() p.panicWhenStoppedOrInError()
p.checkForLoops() p.checkForLoops()
if tokenHandler == nil { if tokenHandler == nil {
_, filepos := getCaller(1) callerPanic(1, "parsekit.ParseAPI.On(): On() called with nil tokenHandler argument at {caller}")
panic(fmt.Sprintf(
"parsekit.ParseAPI.On(): On() called with nil "+
"tokenHandler argument at %s", filepos))
} }
p.result = nil p.result = nil
@ -127,9 +118,9 @@ type ParseAPIOnAction struct {
ok bool ok bool
} }
// Accept tells the parser to move the cursor past a match that was found, // Accept tells the parser to move the read cursor past a match that was
// and to make the TokenResult from the TokenAPI available in the ParseAPI // found, and to make the TokenResult from the TokenAPI available in the
// through the Result() method. // ParseAPI through the ParseAPI.Result() method.
// //
// Returns true in case a match was found. // Returns true in case a match was found.
// When no match was found, then no action is taken and false is returned. // When no match was found, then no action is taken and false is returned.
@ -198,10 +189,8 @@ func (a *ParseAPIOnAction) flushReader() {
func (p *ParseAPI) Result() *TokenResult { func (p *ParseAPI) Result() *TokenResult {
result := p.result result := p.result
if p.result == nil { if p.result == nil {
_, filepos := getCaller(1) callerPanic(1, "parsekit.ParseAPI.TokenResult(): TokenResult() called "+
panic(fmt.Sprintf( "at {caller} without calling ParseAPI.Accept() on beforehand")
"parsekit.ParseAPI.TokenResult(): TokenResult() called at %s without "+
"calling ParseAPI.Accept() on beforehand", filepos))
} }
return result return result
} }
@ -221,8 +210,7 @@ func (p *ParseAPI) Handle(parseHandler ParseHandler) bool {
func (p *ParseAPI) panicWhenParseHandlerNil(parseHandler ParseHandler) { func (p *ParseAPI) panicWhenParseHandlerNil(parseHandler ParseHandler) {
if parseHandler == nil { if parseHandler == nil {
_, filepos := getCaller(2) callerPanic(2, "parsekit.ParseAPI.Handle(): Handle() called with nil input at {caller}")
panic(fmt.Sprintf("parsekit.ParseAPI.Handle(): Handle() called with nil input at %s", filepos))
} }
} }
@ -286,19 +274,19 @@ func (p *ParseAPI) ExpectEndOfFile() {
// unexpected input was encountered. // unexpected input was encountered.
// //
// It can automatically produce an error message for a couple of situations: // It can automatically produce an error message for a couple of situations:
// 1) input simply didn't match the expectation // 1) the input simply didn't match the expectation
// 2) the end of the input was reached // 2) the end of the input was reached
// 3) there was an invalid UTF8 character on the input. // 3) there was an error while reading the input.
// //
// The parser implementation can provide some feedback for this error by // The parser implementation can provide some feedback for this error by
// calling ParseAPI.Expects() to set the expectation. When set, the // calling ParseAPI.Expects() to set the expectation. When set, the
// expectation is included in the error message. // expectation is included in the error message.
func (p *ParseAPI) UnexpectedInput() { func (p *ParseAPI) UnexpectedInput() {
p.panicWhenStoppedOrInError() p.panicWhenStoppedOrInError()
r, err := p.tokenAPI.NextRune() _, err := p.tokenAPI.NextRune()
switch { switch {
case err == nil: case err == nil:
p.Error("unexpected character %q%s", r, fmtExpects(p)) p.Error("unexpected input%s", fmtExpects(p))
case err == io.EOF: case err == io.EOF:
p.Error("unexpected end of file%s", fmtExpects(p)) p.Error("unexpected end of file%s", fmtExpects(p))
default: default:

View File

@ -1,8 +1,6 @@
package parsekit package parsekit
import ( import (
"fmt"
"runtime"
"strings" "strings"
) )
@ -30,8 +28,7 @@ type ParseHandler func(*ParseAPI)
// To parse input data, use the method Parser.Execute(). // To parse input data, use the method Parser.Execute().
func NewParser(startHandler ParseHandler) *Parser { func NewParser(startHandler ParseHandler) *Parser {
if startHandler == nil { if startHandler == nil {
_, filepos := getCaller(1) callerPanic(1, "parsekit.NewParser(): NewParser() called with nil input at {caller}")
panic(fmt.Sprintf("parsekit.NewParser(): NewParser() called with nil input at %s", filepos))
} }
return &Parser{startHandler: startHandler} return &Parser{startHandler: startHandler}
} }
@ -44,21 +41,14 @@ func (p *Parser) Execute(input string) *Error {
loopCheck: map[string]bool{}, loopCheck: map[string]bool{},
} }
if api.Handle(p.startHandler) { if api.Handle(p.startHandler) {
// Handle indicated that parsing could still continue, meaning that there // Handle returned true, indicating that parsing could still continue.
// was no error and that the parsing has not actively been Stop()-ed. // There was no error and that the parsing has not actively been Stop()-ed.
// However, at this point, the parsing really should have stopped. // Let's try to make the best of it.
// We'll see what happens when we tell the parser that EOF was expected. if api.expecting != "" {
// This might work if we're indeed at EOF. Otherwise, an error will be api.UnexpectedInput()
// generated. } else {
api.ExpectEndOfFile() api.ExpectEndOfFile()
}
} }
return api.err return api.err
} }
func getCaller(depth int) (string, string) {
// No error handling, because we call this method ourselves with safe depth values.
pc, file, line, _ := runtime.Caller(depth + 1)
filepos := fmt.Sprintf("%s:%d", file, line)
caller := runtime.FuncForPC(pc)
return caller.Name(), filepos
}

View File

@ -30,26 +30,18 @@ func ExampleParser_usingTokens() {
// Easy access to the parsekit definitions. // Easy access to the parsekit definitions.
c, a, tok := parsekit.C, parsekit.A, parsekit.T c, a, tok := parsekit.C, parsekit.A, parsekit.T
var tokens []*parsekit.Token
var accepted string
parser := parsekit.NewParser(func(p *parsekit.ParseAPI) { parser := parsekit.NewParser(func(p *parsekit.ParseAPI) {
if p.On(c.OneOrMore(tok.Rune("a rune", a.AnyRune))).Accept() { if p.On(c.OneOrMore(tok.Rune("RUNE", a.AnyRune))).Accept() {
tokens = p.Result().Tokens() fmt.Printf("Runes accepted: %q\n", p.Result().String())
accepted = p.Result().String() fmt.Printf("Token values: %s\n", p.Result().Tokens())
} }
p.ExpectEndOfFile() p.ExpectEndOfFile()
}) })
parser.Execute(Any will dö!") parser.Execute(ök!")
fmt.Printf("Runes accepted: %q\n", accepted)
fmt.Printf("Token values: ")
for _, t := range tokens {
fmt.Printf("%c ", t.Value)
}
// Output: // Output:
// Runes accepted: "¡Any will dö!" // Runes accepted: "¡ök!"
// Token values: ¡ A n y w i l l d ö ! // Token values: RUNE(int32:161) RUNE(int32:246) RUNE(int32:107) RUNE(int32:33)
} }
func ExampleParseAPI_UnexpectedInput() { func ExampleParseAPI_UnexpectedInput() {
@ -61,7 +53,7 @@ func ExampleParseAPI_UnexpectedInput() {
fmt.Println(err.Full()) fmt.Println(err.Full())
// Output: // Output:
// unexpected character 'W' (expected a thing) at start of file // unexpected input (expected a thing) at start of file
} }
func ExampleParseAPIOnAction_Accept() { func ExampleParseAPIOnAction_Accept() {
@ -151,7 +143,7 @@ func ExampleParseAPI_Stop_notCalledButInputPending() {
// Output: // Output:
// First word: Input // First word: Input
// Error: unexpected character ' ' (expected end of file) at line 1, column 6 // Error: unexpected input (expected end of file) at line 1, column 6
} }
func ExampleParseAPIOnAction_Stay() { func ExampleParseAPIOnAction_Stay() {
@ -265,7 +257,7 @@ func TestGivenParserWhichIsNotStopped_WithNoMoreInput_FallbackExpectEndOfFileKic
func TestGivenParserWhichIsNotStopped_WithMoreInput_ProducesError(t *testing.T) { func TestGivenParserWhichIsNotStopped_WithMoreInput_ProducesError(t *testing.T) {
p := parsekit.NewParser(func(p *parsekit.ParseAPI) {}) p := parsekit.NewParser(func(p *parsekit.ParseAPI) {})
err := p.Execute("x") err := p.Execute("x")
parsekit.AssertEqual(t, "unexpected character 'x' (expected end of file) at start of file", err.Full(), "err") parsekit.AssertEqual(t, "unexpected input (expected end of file) at start of file", err.Full(), "err")
} }
type parserWithLoop struct { type parserWithLoop struct {

View File

@ -88,10 +88,8 @@ func NewTokenAPI(r io.Reader) *TokenAPI {
// without explicitly accepting, this method will panic. // without explicitly accepting, this method will panic.
func (i *TokenAPI) NextRune() (rune, error) { func (i *TokenAPI) NextRune() (rune, error) {
if i.result.lastRune != nil { if i.result.lastRune != nil {
_, linepos := getCaller(1) callerPanic(1, "parsekit.TokenAPI.NextRune(): NextRune() called at {caller} "+
panic(fmt.Sprintf( "without a prior call to Accept()")
"parsekit.TokenAPI.NextRune(): NextRune() called at %s without a "+
"prior call to Accept()", linepos))
} }
i.detachChilds() i.detachChilds()
@ -107,15 +105,9 @@ func (i *TokenAPI) NextRune() (rune, error) {
// returned an error. Calling Accept() in such case will result in a panic. // returned an error. Calling Accept() in such case will result in a panic.
func (i *TokenAPI) Accept() { func (i *TokenAPI) Accept() {
if i.result.lastRune == nil { if i.result.lastRune == nil {
_, linepos := getCaller(1) callerPanic(1, "parsekit.TokenAPI.Accept(): Accept() called at {caller} without first calling NextRune()")
panic(fmt.Sprintf(
"parsekit.TokenAPI.Accept(): Accept() called at %s without "+
"first calling NextRune()", linepos))
} else if i.result.lastRune.err != nil { } else if i.result.lastRune.err != nil {
_, linepos := getCaller(1) callerPanic(1, "parsekit.TokenAPI.Accept(): Accept() called at {caller}, but the prior call to NextRune() failed")
panic(fmt.Sprintf(
"parsekit.TokenAPI.Accept(): Accept() called at %s, but the "+
"prior call to NextRune() failed", linepos))
} }
i.result.runes = append(i.result.runes, i.result.lastRune.r) i.result.runes = append(i.result.runes, i.result.lastRune.r)
i.cursor.Move(fmt.Sprintf("%c", i.result.lastRune.r)) i.cursor.Move(fmt.Sprintf("%c", i.result.lastRune.r))
@ -167,10 +159,7 @@ func (i *TokenAPI) Fork() *TokenAPI {
// This allows a child to feed results in chunks to its parent. // This allows a child to feed results in chunks to its parent.
func (i *TokenAPI) Merge() { func (i *TokenAPI) Merge() {
if i.parent == nil { if i.parent == nil {
_, filepos := getCaller(1) callerPanic(1, "parsekit.TokenAPI.Merge(): Merge() called at {caller} on a non-forked TokenAPI")
panic(fmt.Sprintf(
"parsekit.TokenAPI.Merge(): Merge() called at %s "+
"on a non-forked TokenAPI", filepos))
} }
i.parent.result.runes = append(i.parent.result.runes, i.result.runes...) i.parent.result.runes = append(i.parent.result.runes, i.result.runes...)

View File

@ -40,8 +40,8 @@ func ExampleTokenAPI_Fork() {
// Output: // Output:
// abcd <nil> // abcd <nil>
// abcd <nil> // abcd <nil>
// <nil> unexpected character 'a' (expected abcd) // <nil> unexpected input (expected abcd)
// <nil> unexpected character 'x' (expected abcd) // <nil> unexpected input (expected abcd)
} }
func ExampleTokenAPI_Merge() { func ExampleTokenAPI_Merge() {

View File

@ -184,9 +184,9 @@ var A = struct {
Pipe: MatchRune('|'), Pipe: MatchRune('|'),
CurlyClose: MatchRune('}'), CurlyClose: MatchRune('}'),
Tilde: MatchRune('~'), Tilde: MatchRune('~'),
Whitespace: MatchOneOrMore(MatchAny(MatchRune(' '), MatchRune('\t'))), Whitespace: MatchWhitespace(),
WhitespaceAndNewlines: MatchOneOrMore(MatchAny(MatchRune(' '), MatchRune('\t'), MatchStr("\r\n"), MatchRune('\n'))), WhitespaceAndNewlines: MatchWhitespaceAndNewlines(),
EndOfLine: MatchAny(MatchStr("\r\n"), MatchRune('\n'), MatchEndOfFile()), EndOfLine: MatchEndOfLine(),
Digit: MatchDigit(), Digit: MatchDigit(),
DigitNotZero: MatchDigitNotZero(), DigitNotZero: MatchDigitNotZero(),
Digits: MatchDigits(), Digits: MatchDigits(),
@ -195,15 +195,50 @@ var A = struct {
IntegerBetween: MatchIntegerBetween, IntegerBetween: MatchIntegerBetween,
Float: MatchFloat(), Float: MatchFloat(),
Boolean: MatchBoolean(), Boolean: MatchBoolean(),
ASCII: MatchRuneRange('\x00', '\x7F'), ASCII: MatchASCII(),
ASCIILower: MatchRuneRange('a', 'z'), ASCIILower: MatchASCIILower(),
ASCIIUpper: MatchRuneRange('A', 'Z'), ASCIIUpper: MatchASCIIUpper(),
HexDigit: MatchAny(MatchRuneRange('0', '9'), MatchRuneRange('a', 'f'), MatchRuneRange('A', 'F')), HexDigit: MatchHexDigit(),
Octet: MatchOctet(false), Octet: MatchOctet(false),
IPv4: MatchIPv4(), IPv4: MatchIPv4(),
IPv4MaskBits: MatchIntegerBetween(0, 32), IPv4MaskBits: MatchIntegerBetween(0, 32),
} }
// M provides convenient access to a range of modifiers (which in their nature are
// parser/combinators) that can be used when creating TokenHandler functions.
//
// In parsekit, a modifier is defined as a TokenHandler function that modifies the
// resulting output of another TokenHandler in some way. It does not do any matching
// against input of its own.
//
// When using M in your own parser, then it is advised to create a variable
// to reference it:
//
// var m = parsekit.M
//
// Doing so saves you a lot of typing, and it makes your code a lot cleaner.
var M = struct {
Drop func(TokenHandler) TokenHandler
Trim func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimLeft func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimRight func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimSpace func(handler TokenHandler) TokenHandler
ToLower func(TokenHandler) TokenHandler
ToUpper func(TokenHandler) TokenHandler
Replace func(handler TokenHandler, replaceWith string) TokenHandler // TODO reverse arguments?
ByCallback func(TokenHandler, func(string) string) TokenHandler
}{
Drop: ModifyDrop,
Trim: ModifyTrim,
TrimLeft: ModifyTrimLeft,
TrimRight: ModifyTrimRight,
TrimSpace: ModifyTrimSpace,
ToLower: ModifyToLower,
ToUpper: ModifyToUpper,
Replace: ModifyReplace,
ByCallback: ModifyByCallback,
}
// T provides convenient access to a range of Token producers (which in their // T provides convenient access to a range of Token producers (which in their
// nature are parser/combinators) that can be used when creating TokenHandler // nature are parser/combinators) that can be used when creating TokenHandler
// functions. // functions.
@ -254,8 +289,7 @@ var T = struct {
ByCallback: MakeTokenByCallback, ByCallback: MakeTokenByCallback,
} }
// MatchRune creates a TokenHandler function that checks if the next rune from // MatchRune creates a TokenHandler function that matches against the provided rune.
// the input matches the provided rune.
func MatchRune(expected rune) TokenHandler { func MatchRune(expected rune) TokenHandler {
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
input, err := t.NextRune() input, err := t.NextRune()
@ -267,8 +301,8 @@ func MatchRune(expected rune) TokenHandler {
} }
} }
// MatchRunes creates a TokenHandler function that that checks if the next rune // MatchRunes creates a TokenHandler function that checks if the input matches
// from the input is one of the provided runes. // one of the provided runes.
func MatchRunes(expected ...rune) TokenHandler { func MatchRunes(expected ...rune) TokenHandler {
s := string(expected) s := string(expected)
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
@ -283,17 +317,16 @@ func MatchRunes(expected ...rune) TokenHandler {
} }
} }
// MatchRuneRange creates a TokenHandler function that that checks if the next rune // MatchRuneRange creates a TokenHandler function that checks if the input
// from the input is contained by the provided rune range. // matches the provided rune range. The rune range is defined by a start and
// // an end rune, inclusive, so:
// The rune range is defined by a start and an end rune, inclusive, so:
// //
// MatchRuneRange('g', 'k') // MatchRuneRange('g', 'k')
// //
// creates a TokenHandler that will match any of 'g', 'h', 'i', 'j' or 'k'. // creates a TokenHandler that will match any of 'g', 'h', 'i', 'j' or 'k'.
func MatchRuneRange(start rune, end rune) TokenHandler { func MatchRuneRange(start rune, end rune) TokenHandler {
if end < start { if end < start {
panic(fmt.Sprintf("TokenHandler bug: MatchRuneRange definition error: start %q must not be < end %q", start, end)) callerPanic(1, "TokenHandler: MatchRuneRange definition error at {caller}: start %q must not be < end %q", start, end)
} }
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
input, err := t.NextRune() input, err := t.NextRune()
@ -305,8 +338,28 @@ func MatchRuneRange(start rune, end rune) TokenHandler {
} }
} }
// MatchStr creates a TokenHandler that will check if the upcoming runes on the // MatchWhitespace creates a TokenHandler that matches the input against one
// input match the provided string. // or more whitespace characters, meansing tabs and spaces.
//
// When you need whitespace matching to also include newlines, then make use
// of MatchWhitespaceAndNewlines().
func MatchWhitespace() TokenHandler {
return MatchOneOrMore(MatchAny(MatchRune(' '), MatchRune('\t')))
}
// MatchWhitespaceAndNewlines creates a TokenHandler that matches the input
// against one or more whitespace and/or newline characters, meaning tabs,
// spaces and newlines ("\r\n" and "\n").
func MatchWhitespaceAndNewlines() TokenHandler {
return MatchOneOrMore(MatchAny(MatchRune(' '), MatchRune('\t'), MatchStr("\r\n"), MatchRune('\n')))
}
// MatchEndOfLine creates a TokenHandler that matches a newline ("\r\n" or "\n") or EOF.
func MatchEndOfLine() TokenHandler {
return MatchAny(MatchStr("\r\n"), MatchRune('\n'), MatchEndOfFile())
}
// MatchStr creates a TokenHandler that matches the input against the provided string.
// TODO make this a more efficient string-level match? // TODO make this a more efficient string-level match?
func MatchStr(expected string) TokenHandler { func MatchStr(expected string) TokenHandler {
var handlers = []TokenHandler{} var handlers = []TokenHandler{}
@ -316,8 +369,8 @@ func MatchStr(expected string) TokenHandler {
return MatchSeq(handlers...) return MatchSeq(handlers...)
} }
// MatchStrNoCase creates a TokenHandler that will check if the upcoming runes // MatchStrNoCase creates a TokenHandler that matches the input against the
// on the input match the provided string in a case-insensitive manner. // provided string in a case-insensitive manner.
// TODO make this a more efficient string-level match? // TODO make this a more efficient string-level match?
func MatchStrNoCase(expected string) TokenHandler { func MatchStrNoCase(expected string) TokenHandler {
var handlers = []TokenHandler{} var handlers = []TokenHandler{}
@ -331,7 +384,8 @@ func MatchStrNoCase(expected string) TokenHandler {
// MatchOpt creates a TokenHandler that makes the provided TokenHandler optional. // MatchOpt creates a TokenHandler that makes the provided TokenHandler optional.
// When the provided TokenHandler applies, then its output is used, otherwise // When the provided TokenHandler applies, then its output is used, otherwise
// no output is generated but still a successful match is reported. // no output is generated but still a successful match is reported (but the
// result will be empty).
func MatchOpt(handler TokenHandler) TokenHandler { func MatchOpt(handler TokenHandler) TokenHandler {
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
child := t.Fork() child := t.Fork()
@ -410,7 +464,7 @@ func MatchRep(times int, handler TokenHandler) TokenHandler {
// When more matches are possible, these will be included in the output. // When more matches are possible, these will be included in the output.
func MatchMin(min int, handler TokenHandler) TokenHandler { func MatchMin(min int, handler TokenHandler) TokenHandler {
if min < 0 { if min < 0 {
panic("TokenHandler bug: MatchMin definition error: min must be >= 0") callerPanic(1, "TokenHandler: MatchMin definition error at {caller}: min must be >= 0")
} }
return matchMinMax(min, -1, handler, "MatchMin") return matchMinMax(min, -1, handler, "MatchMin")
} }
@ -421,7 +475,7 @@ func MatchMin(min int, handler TokenHandler) TokenHandler {
// Zero matches are considered a successful match. // Zero matches are considered a successful match.
func MatchMax(max int, handler TokenHandler) TokenHandler { func MatchMax(max int, handler TokenHandler) TokenHandler {
if max < 0 { if max < 0 {
panic("TokenHandler bug: MatchMax definition error: max must be >= 0") callerPanic(1, "TokenHandler: MatchMax definition error at {caller}: max must be >= 0")
} }
return matchMinMax(0, max, handler, "MatchMax") return matchMinMax(0, max, handler, "MatchMax")
} }
@ -444,17 +498,17 @@ func MatchOneOrMore(handler TokenHandler) TokenHandler {
// inclusive. All matches will be included in the output. // inclusive. All matches will be included in the output.
func MatchMinMax(min int, max int, handler TokenHandler) TokenHandler { func MatchMinMax(min int, max int, handler TokenHandler) TokenHandler {
if max < 0 { if max < 0 {
panic("TokenHandler bug: MatchMinMax definition error: max must be >= 0") callerPanic(1, "TokenHandler: MatchMinMax definition error at {caller}: max must be >= 0")
} }
if min < 0 { if min < 0 {
panic("TokenHandler bug: MatchMinMax definition error: min must be >= 0") callerPanic(1, "TokenHandler: MatchMinMax definition error at {caller}: min must be >= 0")
} }
return matchMinMax(min, max, handler, "MatchMinMax") return matchMinMax(min, max, handler, "MatchMinMax")
} }
func matchMinMax(min int, max int, handler TokenHandler, name string) TokenHandler { func matchMinMax(min int, max int, handler TokenHandler, name string) TokenHandler {
if max >= 0 && min > max { if max >= 0 && min > max {
panic(fmt.Sprintf("TokenHandler bug: %s definition error: max %d must not be < min %d", name, max, min)) callerPanic(2, "TokenHandler: %s definition error at {caller}: max %d must not be < min %d", name, max, min)
} }
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
child := t.Fork() child := t.Fork()
@ -592,15 +646,43 @@ func MatchFloat() TokenHandler {
return MatchSeq(digits, MatchOpt(MatchSeq(MatchRune('.'), digits))) return MatchSeq(digits, MatchOpt(MatchSeq(MatchRune('.'), digits)))
} }
// MatchBoolean creates a TokenHandler function that checks if a valid boolean // MatchBoolean creates a TokenHandler function that checks if a boolean
// value can be read from the input. It supports the boolean values as understood // value can be read from the input. It supports the boolean values as understood
// by Go's strconv.ParseBool() function. // by Go's strconv.ParseBool() function.
//
// True values: true, TRUE, True, 1, t, T
//
// False falues: false, FALSE, False, 0, f, F
func MatchBoolean() TokenHandler { func MatchBoolean() TokenHandler {
trues := MatchAny(MatchStr("true"), MatchStr("TRUE"), MatchStr("True"), MatchRune('1'), MatchRune('t'), MatchRune('T')) trues := MatchAny(MatchStr("true"), MatchStr("TRUE"), MatchStr("True"), MatchRune('1'), MatchRune('t'), MatchRune('T'))
falses := MatchAny(MatchStr("false"), MatchStr("FALSE"), MatchStr("False"), MatchRune('0'), MatchRune('f'), MatchRune('F')) falses := MatchAny(MatchStr("false"), MatchStr("FALSE"), MatchStr("False"), MatchRune('0'), MatchRune('f'), MatchRune('F'))
return MatchAny(trues, falses) return MatchAny(trues, falses)
} }
// MatchASCII creates a TokenHandler function that matches against any
// ASCII value on the input.
func MatchASCII() TokenHandler {
return MatchRuneRange('\x00', '\x7F')
}
// MatchASCIILower creates a TokenHandler function that matches against any
// lower case ASCII letter on the input (a - z).
func MatchASCIILower() TokenHandler {
return MatchRuneRange('a', 'z')
}
// MatchASCIIUpper creates a TokenHandler function that matches against any
// upper case ASCII letter on the input (a - z).
func MatchASCIIUpper() TokenHandler {
return MatchRuneRange('A', 'Z')
}
// MatchHexDigit creates a TokenHandler function that check if a single hexadecimal
// digit can be read from the input.
func MatchHexDigit() TokenHandler {
return MatchAny(MatchRuneRange('0', '9'), MatchRuneRange('a', 'f'), MatchRuneRange('A', 'F'))
}
// MatchOctet creates a TokenHandler function that checks if a valid octet value // MatchOctet creates a TokenHandler function that checks if a valid octet value
// can be read from the input (octet = byte value representation, with a value // can be read from the input (octet = byte value representation, with a value
// between 0 and 255 inclusive). It only looks at the first 1 to 3 upcoming // between 0 and 255 inclusive). It only looks at the first 1 to 3 upcoming
@ -610,25 +692,25 @@ func MatchBoolean() TokenHandler {
// When the normalize parameter is set to true, then leading zeroes will be // When the normalize parameter is set to true, then leading zeroes will be
// stripped from the octet. // stripped from the octet.
func MatchOctet(normalize bool) TokenHandler { func MatchOctet(normalize bool) TokenHandler {
digits := MatchMinMax(1, 3, MatchDigit()) max3Digits := MatchMinMax(1, 3, MatchDigit())
return func(t *TokenAPI) bool { return func(t *TokenAPI) bool {
fork := t.Fork() fork := t.Fork()
if !digits(fork) { if !max3Digits(fork) {
return false return false
} }
value, _ := strconv.ParseInt(fork.Result().String(), 10, 16) value, _ := strconv.ParseInt(fork.Result().String(), 10, 16)
if value <= 255 { if value > 255 {
if normalize { return false
runes := fork.Result().Runes()
for len(runes) > 1 && runes[0] == '0' {
runes = runes[1:]
}
fork.Result().SetRunes(runes)
}
fork.Merge()
return true
} }
return false if normalize {
runes := fork.Result().Runes()
for len(runes) > 1 && runes[0] == '0' {
runes = runes[1:]
}
fork.Result().SetRunes(runes)
}
fork.Merge()
return true
} }
} }
@ -642,41 +724,6 @@ func MatchIPv4() TokenHandler {
return MatchSeq(octet, dot, octet, dot, octet, dot, octet) return MatchSeq(octet, dot, octet, dot, octet, dot, octet)
} }
// M provides convenient access to a range of modifiers (which in their nature are
// parser/combinators) that can be used when creating TokenHandler functions.
//
// In parsekit, a modifier is defined as a TokenHandler function that modifies the
// resulting output of another TokenHandler in some way. It does not do any matching
// against input of its own.
//
// When using M in your own parser, then it is advised to create a variable
// to reference it:
//
// var m = parsekit.M
//
// Doing so saves you a lot of typing, and it makes your code a lot cleaner.
var M = struct {
Drop func(TokenHandler) TokenHandler
Trim func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimLeft func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimRight func(handler TokenHandler, cutset string) TokenHandler // TODO reverse arguments?
TrimSpace func(handler TokenHandler) TokenHandler
ToLower func(TokenHandler) TokenHandler
ToUpper func(TokenHandler) TokenHandler
Replace func(handler TokenHandler, replaceWith string) TokenHandler // TODO reverse arguments?
ByCallback func(TokenHandler, func(string) string) TokenHandler
}{
Drop: ModifyDrop,
Trim: ModifyTrim,
TrimLeft: ModifyTrimLeft,
TrimRight: ModifyTrimRight,
TrimSpace: ModifyTrimSpace,
ToLower: ModifyToLower,
ToUpper: ModifyToUpper,
Replace: ModifyReplace,
ByCallback: ModifyByCallback,
}
// ModifyDrop creates a TokenHandler that checks if the provided TokenHandler applies. // ModifyDrop creates a TokenHandler that checks if the provided TokenHandler applies.
// If it does, then its output is discarded completely. // If it does, then its output is discarded completely.
// //
@ -970,6 +1017,7 @@ func makeStrconvToken(toktype interface{}, handler TokenHandler, convert func(s
return MakeTokenByCallback(handler, func(t *TokenAPI) *Token { return MakeTokenByCallback(handler, func(t *TokenAPI) *Token {
value, err := convert(t.Result().String()) value, err := convert(t.Result().String())
if err != nil { if err != nil {
// TODO meh, panic feels so bad here. Maybe just turn this case into "no match"?
panic(fmt.Sprintf( panic(fmt.Sprintf(
"TokenHandler error: %s cannot handle input %q: %s "+ "TokenHandler error: %s cannot handle input %q: %s "+
"(only use a type conversion token maker, when the input has been "+ "(only use a type conversion token maker, when the input has been "+

View File

@ -70,18 +70,18 @@ func TestCombinators(t *testing.T) {
func TestCombinatorPanics(t *testing.T) { func TestCombinatorPanics(t *testing.T) {
var c, a = parsekit.C, parsekit.A var c, a = parsekit.C, parsekit.A
parsekit.AssertPanics(t, []parsekit.PanicT{ parsekit.AssertPanics(t, []parsekit.PanicT{
{func() { a.RuneRange('z', 'a') }, false, {func() { a.RuneRange('z', 'a') }, true,
"TokenHandler bug: MatchRuneRange definition error: start 'z' must not be < end 'a'"}, `TokenHandler: MatchRuneRange definition error at /.*/tokenhandlers_builtin_test\.go:\d+: start 'z' must not be < end 'a'`},
{func() { c.MinMax(-1, 1, parsekit.A.Space) }, false, {func() { c.MinMax(-1, 1, parsekit.A.Space) }, true,
"TokenHandler bug: MatchMinMax definition error: min must be >= 0"}, `TokenHandler: MatchMinMax definition error at /.*/tokenhandlers_builtin_test\.go:\d+: min must be >= 0`},
{func() { c.MinMax(1, -1, parsekit.A.Space) }, false, {func() { c.MinMax(1, -1, parsekit.A.Space) }, true,
"TokenHandler bug: MatchMinMax definition error: max must be >= 0"}, `TokenHandler: MatchMinMax definition error at /.*/tokenhandlers_builtin_test\.go:\d+: max must be >= 0`},
{func() { c.MinMax(10, 5, parsekit.A.Space) }, false, {func() { c.MinMax(10, 5, parsekit.A.Space) }, true,
"TokenHandler bug: MatchMinMax definition error: max 5 must not be < min 10"}, `TokenHandler: MatchMinMax definition error at /.*/tokenhandlers_builtin_test\.go:\d+: max 5 must not be < min 10`},
{func() { c.Min(-10, parsekit.A.Space) }, false, {func() { c.Min(-10, parsekit.A.Space) }, true,
"TokenHandler bug: MatchMin definition error: min must be >= 0"}, `TokenHandler: MatchMin definition error at /.*/tokenhandlers_builtin_test\.go:\d+: min must be >= 0`},
{func() { c.Max(-42, parsekit.A.Space) }, false, {func() { c.Max(-42, parsekit.A.Space) }, true,
"TokenHandler bug: MatchMax definition error: max must be >= 0"}, `TokenHandler: MatchMax definition error at /.*/tokenhandlers_builtin_test\.go:\d+: max must be >= 0`},
}) })
} }

View File

@ -1,12 +1,55 @@
package parsekit package parsekit
import ( import (
"fmt"
"io" "io"
"strings" "strings"
"testing" "testing"
"unicode/utf8" "unicode/utf8"
) )
// TODO For error handling, it would be really cool if for example the
// 10.0.300.1/24 case would return an actual error stating that
// 300 is not a valid octet for an IPv4 address.
// Biggest thing to take care of here, is that errors should not stop
// a Parser flow (since we might be trying to match different cases in
// sequence), but a Parser flow should optionally be able to make use
// of the actual error.
// The same goes for a Tokenizer, since those can also make use of
// optional matching using parsekit.C.Any(...) for example. If matching
// for Any(IPv4, Digits), the example case should simply end up with 10
// after the IPv4 mismatch.
func ExampleTokenizer_Execute() {
// Build the tokenizer for ip/mask.
ip := T.Str("ip", A.IPv4)
mask := T.Int8("mask", A.IPv4MaskBits)
cidr := C.Seq(ip, A.Slash, mask)
tokenizer := NewTokenizer(cidr, "cidr")
for _, input := range []string{
"000.000.000.000/000",
"192.168.0.1/24",
"255.255.255.255/32",
"10.0.300.1/24",
"not an IPv4 CIDR",
} {
// Execute returns a TokenResult and an error, which is nil on success.
result, err := tokenizer.Execute(input)
if err == nil {
fmt.Printf("Result: %s\n", result.Tokens())
} else {
fmt.Printf("Error: %s\n", err)
}
}
// Output:
// Result: ip(string:0.0.0.0) mask(int8:0)
// Result: ip(string:192.168.0.1) mask(int8:24)
// Result: ip(string:255.255.255.255) mask(int8:32)
// Error: unexpected input (expected cidr)
// Error: unexpected input (expected cidr)
}
func TestCallingNextRune_ReturnsNextRune(t *testing.T) { func TestCallingNextRune_ReturnsNextRune(t *testing.T) {
r, _ := mkInput().NextRune() r, _ := mkInput().NextRune()
AssertEqual(t, 'T', r, "first rune") AssertEqual(t, 'T', r, "first rune")
@ -31,8 +74,7 @@ func TestCallingNextRuneTwice_Panics(t *testing.T) {
i.NextRune() i.NextRune()
}, },
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.NextRune\(\): NextRune\(\) called at ` + Expect: `parsekit\.TokenAPI\.NextRune\(\): NextRune\(\) called at /.*/tokenizer_test\.go:\d+ without a prior call to Accept\(\)`,
`/.*/tokenizer_test\.go:\d+ without a prior call to Accept\(\)`,
}) })
} }
@ -40,8 +82,7 @@ func TestCallingAcceptWithoutCallingNextRune_Panics(t *testing.T) {
AssertPanic(t, PanicT{ AssertPanic(t, PanicT{
Function: mkInput().Accept, Function: mkInput().Accept,
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.Accept\(\): Accept\(\) called ` + Expect: `parsekit\.TokenAPI\.Accept\(\): Accept\(\) called at /.*/assertions_test\.go:\d+ without first calling NextRune()`,
`at /.*/assertions_test\.go:\d+ without first calling NextRune()`,
}) })
} }
@ -52,8 +93,7 @@ func TestCallingMergeOnNonForkedChild_Panics(t *testing.T) {
i.Merge() i.Merge()
}, },
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at ` + Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at /.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
`/.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
} }
func TestCallingNextRuneOnForkedParent_DetachesForkedChild(t *testing.T) { func TestCallingNextRuneOnForkedParent_DetachesForkedChild(t *testing.T) {
@ -65,8 +105,7 @@ func TestCallingNextRuneOnForkedParent_DetachesForkedChild(t *testing.T) {
f.Merge() f.Merge()
}, },
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at ` + Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at /.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
`/.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
} }
func TestCallingForkOnForkedParent_DetachesForkedChild(t *testing.T) { func TestCallingForkOnForkedParent_DetachesForkedChild(t *testing.T) {
@ -78,8 +117,7 @@ func TestCallingForkOnForkedParent_DetachesForkedChild(t *testing.T) {
f.Merge() f.Merge()
}, },
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at ` + Expect: `parsekit\.TokenAPI\.Merge\(\): Merge\(\) called at /.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
`/.*/tokenizer_test\.go:\d+ on a non-forked TokenAPI`})
} }
func TestGivenMultipleLevelsOfForks_WhenReturningToRootInput_ForksAreDetached(t *testing.T) { func TestGivenMultipleLevelsOfForks_WhenReturningToRootInput_ForksAreDetached(t *testing.T) {
@ -127,8 +165,7 @@ func TestForkingInput_ClearsLastRune(t *testing.T) {
i.Accept() i.Accept()
}, },
Regexp: true, Regexp: true,
Expect: `parsekit\.TokenAPI\.Accept\(\): Accept\(\) called ` + Expect: `parsekit\.TokenAPI\.Accept\(\): Accept\(\) called at /hom.*/tokenizer_test\.go:\d+ without first calling NextRune\(\)`,
`at /hom.*/tokenizer_test\.go:\d+ without first calling NextRune\(\)`,
}) })
} }

View File

@ -2,6 +2,7 @@ package parsekit
import ( import (
"fmt" "fmt"
"strings"
) )
// Result holds results as produced by a TokenHandler. // Result holds results as produced by a TokenHandler.
@ -39,11 +40,16 @@ func (r *TokenResult) ClearRunes() {
// SetRunes replaces the Runes from the TokenResult with the provided input. // SetRunes replaces the Runes from the TokenResult with the provided input.
func (r *TokenResult) SetRunes(s interface{}) { func (r *TokenResult) SetRunes(s interface{}) {
r.ClearRunes() r.ClearRunes()
r.AddRunes(s) r.addRunes(s)
} }
// AddRunes is used to add runes to the TokenResult. // AddRunes is used to add runes to the TokenResult.
func (r *TokenResult) AddRunes(set ...interface{}) { func (r *TokenResult) AddRunes(set ...interface{}) {
r.addRunes(set...)
}
// AddRunes is used to add runes to the TokenResult.
func (r *TokenResult) addRunes(set ...interface{}) {
for _, s := range set { for _, s := range set {
switch s := s.(type) { switch s := s.(type) {
case string: case string:
@ -53,7 +59,7 @@ func (r *TokenResult) AddRunes(set ...interface{}) {
case rune: case rune:
r.runes = append(r.runes, s) r.runes = append(r.runes, s)
default: default:
panic(fmt.Sprintf("parsekit.TokenResult.SetRunes(): unsupported type '%T' used", s)) callerPanic(2, "parsekit.TokenResult.AddRunes(): unsupported type '%T' used at {caller}", s)
} }
} }
} }
@ -91,8 +97,22 @@ func (r *TokenResult) AddToken(t *Token) {
r.tokens = append(r.tokens, t) r.tokens = append(r.tokens, t)
} }
// SliceOfTokens is an alias for []*Token type. The method Tokens() returns
// this type. A String() method is defined for it, to make it easy to
// format the tokens as a string for testing / debugging purposes.
type SliceOfTokens []*Token
func (ts SliceOfTokens) String() string {
parts := make([]string, len(ts))
for i, t := range ts {
str := fmt.Sprintf("%v(%T:%v)", t.Type, t.Value, t.Value)
parts[i] = str
}
return strings.Join(parts, " ")
}
// Tokens retrieves the Tokens from the TokenResult. // Tokens retrieves the Tokens from the TokenResult.
func (r *TokenResult) Tokens() []*Token { func (r *TokenResult) Tokens() SliceOfTokens {
return r.tokens return r.tokens
} }

View File

@ -1,11 +1,12 @@
package parsekit package parsekit
import ( import (
"strings"
"testing" "testing"
) )
func TestSetResult_AcceptsVariousTypesAsInput(t *testing.T) { func TestSetResult_AcceptsVariousTypesAsInput(t *testing.T) {
i := mkInput() i := NewTokenAPI(strings.NewReader("Testing"))
i.Result().SetRunes("string") i.Result().SetRunes("string")
AssertEqual(t, "string", string(i.Result().String()), "i.Result() with string input") AssertEqual(t, "string", string(i.Result().String()), "i.Result() with string input")
i.Result().SetRunes([]rune("rune slice")) i.Result().SetRunes([]rune("rune slice"))
@ -17,9 +18,10 @@ func TestSetResult_AcceptsVariousTypesAsInput(t *testing.T) {
func TestSetResult_PanicsOnUnhandledInput(t *testing.T) { func TestSetResult_PanicsOnUnhandledInput(t *testing.T) {
AssertPanic(t, PanicT{ AssertPanic(t, PanicT{
Function: func() { Function: func() {
i := mkInput() i := NewTokenAPI(strings.NewReader("Testing"))
i.Result().SetRunes(1234567) i.Result().SetRunes(1234567)
}, },
Expect: "parsekit.TokenResult.SetRunes(): unsupported type 'int' used", Regexp: true,
Expect: `parsekit\.TokenResult\.AddRunes\(\): unsupported type 'int' used at /.*/tokenresult_test.go:\d+`,
}) })
} }