package parsekit import ( "fmt" "io" "git.makaay.nl/mauricem/go-parsekit/common" "git.makaay.nl/mauricem/go-parsekit/tokenize" ) // ParseAPI holds the internal state of a parse run and provides an API that // ParseHandler methods can use to communicate with the parser. type ParseAPI struct { tokenAPI *tokenize.TokenAPI // the TokenAPI, used for communicating with TokenHandler functions loopCheck map[string]bool // used for parser loop detection result *tokenize.TokenHandlerResult // Last TokenHandler result as produced by On(...).Accept() err *common.Error // error during parsing, retrieved by Error(), further ParseAPI calls are ignored stopped bool // a boolean set to true by Stop(), further ParseAPI calls are ignored } // Peek checks if the upcoming input data matches the provided TokenHandler. // If it does, then true will be returned, false otherwise. The read cursor // will be kept at the same position, so the next call to Peek() or Accept() // will start from the same cursor position. // // After calling this method, you can retrieve the produced TokenHandlerResult // using the ParseAPI.Result() method. func (p *ParseAPI) Peek(tokenHandler tokenize.TokenHandler) bool { p.result = nil forkedTokenAPI, ok := p.invokeTokenHandler("Peek", tokenHandler) if ok { p.result = forkedTokenAPI.Result() p.tokenAPI.ClearResults() p.tokenAPI.DetachChilds() } return ok } // Accept checks if the upcoming input data matches the provided TokenHandler. // If it does, then true will be returned, false otherwise. The read cursor // will be moved forward to beyond the match that was found. // // After calling this method, you can retrieve the produced TokenHandlerResult // using the ParseAPI.Result() method. func (p *ParseAPI) Accept(tokenHandler tokenize.TokenHandler) bool { p.result = nil forkedTokenAPI, ok := p.invokeTokenHandler("Accept", tokenHandler) if ok { forkedTokenAPI.Merge() p.result = p.tokenAPI.Result() p.tokenAPI.DetachChilds() if p.tokenAPI.FlushReader() { p.initLoopCheck() } } return ok } func (p *ParseAPI) invokeTokenHandler(name string, tokenHandler tokenize.TokenHandler) (*tokenize.TokenAPI, bool) { p.panicWhenStoppedOrInError() p.checkForLoops() if tokenHandler == nil { common.CallerPanic(2, "parsekit.ParseAPI.%s(): %s() called with nil tokenHandler argument at {caller}", name, name) } p.result = nil p.tokenAPI.ClearResults() child := p.tokenAPI.Fork() ok := tokenHandler(child) return child, ok } // panicWhenStoppedOrInError will panic when the parser has produced an error // or when it has been stopped. It is used from the ParseAPI methods, to // prevent further calls to the ParseAPI on these occasions. // // Basically, this guard ensures proper coding of parsers, making sure // that clean routes are followed. You can consider this check a runtime // unit test. func (p *ParseAPI) panicWhenStoppedOrInError() { if !p.isStoppedOrInError() { return } called := common.CallerFunc(1) after := "Error()" if p.stopped { after = "Stop()" } common.CallerPanic(2, "parsekit.ParseAPI.%s(): Illegal call to %s() at {caller}: "+ "no calls allowed after ParseAPI.%s", called, called, after) } func (p *ParseAPI) isStoppedOrInError() bool { return p.stopped || p.err != nil } func (p *ParseAPI) initLoopCheck() { p.loopCheck = map[string]bool{} } func (p *ParseAPI) checkForLoops() { filepos := common.CallerFilePos(3) if _, ok := p.loopCheck[filepos]; ok { common.CallerPanic(3, "parsekit.ParseAPI: Loop detected in parser at {caller}") } p.loopCheck[filepos] = true } // Result returns a TokenHandlerResult struct, containing results as produced by the // last Peek() or Accept() call. // // When Result() is called without first doing a Peek() or Accept(), then no // result will be available and the method will panic. func (p *ParseAPI) Result() *tokenize.TokenHandlerResult { result := p.result if p.result == nil { common.CallerPanic(1, "parsekit.ParseAPI.TokenHandlerResult(): TokenHandlerResult() called "+ "at {caller} without calling ParseAPI.Peek() or ParseAPI.Accept() on beforehand") } return result } // Handle is used to execute other ParseHandler functions from within your // ParseHandler function. // // The boolean return value is true when the parser can still continue. // It will be false when either an error was set (using ParseAPI.Error()), // or the parser was stopped (using ParseAPI.Stop()). func (p *ParseAPI) Handle(parseHandler ParseHandler) bool { p.panicWhenStoppedOrInError() p.panicWhenParseHandlerNil(parseHandler) parseHandler(p) return !p.isStoppedOrInError() } func (p *ParseAPI) panicWhenParseHandlerNil(parseHandler ParseHandler) { if parseHandler == nil { common.CallerPanic(2, "parsekit.ParseAPI.Handle(): Handle() called with nil input at {caller}") } } // Stop is used by the parser impementation to tell the ParseAPI that it has // completed the parsing process successfully. // // When the parser implementation returns without stopping first (and // without running into an error), the Parser.Execute() will call // ParserAPI.ExpectEndOfFile() to check if the end of the file was reached. // If not, then things will end in an unexpected input error. // Even though this fallback mechanism will work in a lot of cases, try to make // your parser explicit about things and call Stop() actively yourself. // // After stopping, no more calls to ParseAPI methods are allowed. // Calling a method in this state will result in a panic. func (p *ParseAPI) Stop() { p.stopped = true } // Error sets the error message in the ParseAPI. // // After setting an error, no more calls to ParseAPI methods are allowed. // Calling a method in this state will result in a panic. func (p *ParseAPI) Error(format string, args ...interface{}) { // No call to p.panicWhenStoppedOrInError(), to allow a parser to // set a different error message when needed. message := fmt.Sprintf(format, args...) p.err = &common.Error{message, *p.tokenAPI.Result().Cursor()} } // ExpectEndOfFile can be used to check if the input is at end of file. // // When it finds that the end of the file was indeed reached, then the // parser will be stopped through ParseAPI.Stop(). Otherwise unexpected // input is reported through ParseAPI.Expected() with "end of file" // as the expectation. func (p *ParseAPI) ExpectEndOfFile() { p.panicWhenStoppedOrInError() if p.Peek(tokenize.A.EndOfFile) { p.Stop() } else { p.Expected("end of file") } } // Expected is used to set an error that tells the user that some // unexpected input was encountered, and what input was expected. // // The 'expected' argument can be an empty string. In that case the error // message will not contain a description of the expected input. // // It automatically produces an error message for a couple of situations: // 1) the input simply didn't match the expectation // 2) the end of the input was reached // 3) there was an error while reading the input. func (p *ParseAPI) Expected(expected string) { p.panicWhenStoppedOrInError() _, err := p.tokenAPI.NextRune() switch { case err == nil: p.Error("unexpected input%s", fmtExpects(expected)) case err == io.EOF: p.Error("unexpected end of file%s", fmtExpects(expected)) default: p.Error("unexpected error '%s'%s", err, fmtExpects(expected)) } } func fmtExpects(expected string) string { if expected == "" { return "" } return fmt.Sprintf(" (expected %s)", expected) }