gomonop/pkg/perfdata/rangeparser.go
Emmanuel BENOîT c46c9d76d9
All checks were successful
Run tests and linters / test (push) Successful in 50s
Run tests and linters / build (push) Successful in 48s
Run tests and linters / lint (push) Successful in 1m27s
feat: add the check_output_matches plugin (#5)
This PR adds the `check_output_matches` plugin, which can be used to count regexp or substring matches from either text files or command outputs and determine the final status based on the amount of matches that were found.

Reviewed-on: #5
Co-authored-by: Emmanuel BENOÎT <tseeker@nocternity.net>
Co-committed-by: Emmanuel BENOÎT <tseeker@nocternity.net>
2024-07-20 22:57:10 +02:00

196 lines
5.3 KiB
Go

package perfdata // import nocternity.net/gomonop/pkg/perfdata
import (
"fmt"
"strings"
)
// A state of the range parser.
type rangeParserState int
const (
rpsInit rangeParserState = iota // Initial state
rpsExpectStart // Expect the start of the range
rpsInStart // Reading the start of the range
rpsExpectColon // Expect the colon that separates the start and end
rpsExpectEnd // Expect the end of the range
rpsInEnd // Reading the end of the range
)
// An error emitted by the range parser.
type rangeParserError struct {
input string
position int
message string
}
// parseError creates a new range parser error.
func parseError(input string, position int, message string) *rangeParserError {
return &rangeParserError{
input: input,
position: position,
message: message,
}
}
// Error implements the error interface for the range parser error type.
func (rpe *rangeParserError) Error() string {
return fmt.Sprintf("in `%s' at position %d: %s", rpe.input, rpe.position, rpe.message)
}
// The full state of the range parser.
type rangeParser struct {
runes []rune // The runes being parsed
index int // The read position
state rangeParserState // The FSM state
startOfValue int // The position of the start of the value being read
strBuilder strings.Builder // An accumulator for values
output Range // The output being generated
}
// initRangeParser initializes the range parser from the specified input.
func initRangeParser(input string) *rangeParser {
return &rangeParser{
runes: []rune(input),
state: rpsInit,
}
}
// handleInit handles the parser's rpsInit state. It can accept a '@' or skip
// to rpsExpectStart.
func (rp *rangeParser) handleInit(current rune) {
if current == '@' {
rp.output.inside = true
rp.index++
}
rp.state = rpsExpectStart
}
// handleExpectStart handles the parser's rpsExpectStart state. It may accept
// a colon, which will move the parser to the rpsExpectEnd state, a '~' which
// will move it to rpsExpectColon, or any other rune which will cause the
// accumulation of the "start" value to begin, and the state to be moved to
// handleInStart.
func (rp *rangeParser) handleExpectStart(current rune) {
switch current {
case ':':
rp.output.start = "0"
rp.state = rpsExpectEnd
rp.index++
case '~':
rp.state = rpsExpectColon
rp.index++
default:
rp.startOfValue = rp.index
rp.state = rpsInStart
}
}
// handleInStart handles the parser's rpsInStart state, which corresponds to the
// reading of the "start" value. If a colon is found, the value will be written
// to the range structure and validated, before switching to the rpsExpectEnd
// state. Otherwise it will simply accumulate runes.
func (rp *rangeParser) handleInStart(current rune) error {
switch current {
case ':':
rp.output.start = rp.strBuilder.String()
if !valueCheck.MatchString(rp.output.start) {
return parseError(string(rp.runes), rp.startOfValue, "invalid start value")
}
rp.strBuilder.Reset()
rp.state = rpsExpectEnd
default:
rp.strBuilder.WriteRune(current)
}
rp.index++
return nil
}
// handleExpectColon handles the parser's rpsExpectColon state. A colon MUST be
// read, otherwise an error will be returned. The parser will then switch to the
// rpsExpectEnd state.
func (rp *rangeParser) handleExpectColon(current rune) error {
switch current {
case ':':
rp.state = rpsExpectEnd
rp.index++
default:
return parseError(string(rp.runes), rp.index, "expected ':'")
}
return nil
}
// handleExpectEnd handles the parser's rpsExpectEnd state, which sets the
// position of the beginning of the "end" value, and jumps to the rpsInEnd
// state.
func (rp *rangeParser) handleExpectEnd() {
rp.startOfValue = rp.index
rp.state = rpsInEnd
}
// handleInEnd handles the parser's rpsInEnd state, which accumulates runes for
// the "end" value.
func (rp *rangeParser) handleInEnd(current rune) {
rp.strBuilder.WriteRune(current)
rp.index++
}
// consumeInput is the parser's state machine.
func (rp *rangeParser) consumeInput() error {
for rp.index < len(rp.runes) {
var err error
curRune := rp.runes[rp.index]
switch rp.state {
case rpsInit:
rp.handleInit(curRune)
case rpsExpectStart:
rp.handleExpectStart(curRune)
case rpsInStart:
err = rp.handleInStart(curRune)
case rpsExpectColon:
err = rp.handleExpectColon(curRune)
case rpsExpectEnd:
rp.handleExpectEnd()
case rpsInEnd:
rp.handleInEnd(curRune)
}
if err != nil {
return err
}
}
return nil
}
// Try to parse a string into a performance data range.
func ParseRange(input string) (*Range, error) {
parser := initRangeParser(input)
if err := parser.consumeInput(); err != nil {
return nil, err
}
if parser.state == rpsInStart {
// The range was a single value, so that's the upper bound.
parser.output.start = "0"
parser.state = rpsInEnd
}
if parser.state != rpsInEnd {
return nil, parseError(input, parser.index, "unexpected end of input")
}
parser.output.end = parser.strBuilder.String()
if !valueCheck.MatchString(parser.output.end) {
return nil, parseError(input, parser.startOfValue, "invalid end value")
}
return &parser.output, nil
}