diff --git a/pkg/perfdata/range.go b/pkg/perfdata/range.go index 5c621d2..7defac8 100644 --- a/pkg/perfdata/range.go +++ b/pkg/perfdata/range.go @@ -3,7 +3,6 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( "fmt" "strconv" - "strings" ) // Performance data range. @@ -92,121 +91,3 @@ func (r *Range) Contains(value float64) bool { return (inStart || inEnd) != r.inside } - -// 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) -} - -// Try to parse a string into a performance data range. -func ParseRange(input string) (*Range, error) { - runes := []rune(input) - index := 0 - state := rpsInit - parsed := Range{} - startOfStart := 0 - startOfEnd := 0 - strBuilder := strings.Builder{} - - for index < len(runes) { - curRune := runes[index] - - switch state { - case rpsInit: - if curRune == '@' { - parsed.inside = true - index++ - } - state = rpsExpectStart - - case rpsExpectStart: - switch curRune { - case ':': - parsed.start = "0" - state = rpsExpectEnd - case '~': - state = rpsExpectColon - default: - strBuilder.WriteRune(curRune) - startOfStart = index - state = rpsInStart - } - index++ - - case rpsInStart: - switch curRune { - case ':': - parsed.start = strBuilder.String() - if !valueCheck.MatchString(parsed.start) { - return nil, parseError(input, startOfStart, "invalid start value") - } - strBuilder.Reset() - state = rpsExpectEnd - default: - strBuilder.WriteRune(curRune) - } - index++ - - case rpsExpectColon: - switch curRune { - case ':': - state = rpsExpectEnd - index++ - default: - return nil, parseError(input, index, "expected ':'") - } - - case rpsExpectEnd: - startOfEnd = index - state = rpsInEnd - - case rpsInEnd: - strBuilder.WriteRune(curRune) - index++ - } - } - - if state == rpsInStart { - // The range was a single value, so that's the upper bound. - parsed.start = "0" - state = rpsInEnd - } - if state != rpsInEnd { - return nil, parseError(input, index, "unexpected end of input") - } - - parsed.end = strBuilder.String() - if !valueCheck.MatchString(parsed.end) { - return nil, parseError(input, startOfEnd, "invalid end value") - } - - return &parsed, nil -} diff --git a/pkg/perfdata/range_test.go b/pkg/perfdata/range_test.go index a7d616a..9873395 100644 --- a/pkg/perfdata/range_test.go +++ b/pkg/perfdata/range_test.go @@ -2,11 +2,9 @@ package perfdata // import nocternity.net/gomonop/pkg/perfdata import ( "fmt" - "strings" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestRangeMaxInvalid(t *testing.T) { @@ -126,59 +124,3 @@ func TestRangeContains(t *testing.T) { }) } } - -func TestRangeParserOk(t *testing.T) { - type Test struct { - in string - out Range - } - tests := []Test{ - {in: ":0", out: Range{start: "0", end: "0"}}, - {in: "0:0", out: Range{start: "0", end: "0"}}, - {in: "~:0", out: Range{start: "", end: "0"}}, - {in: ":123", out: Range{start: "0", end: "123"}}, - {in: "0:123", out: Range{start: "0", end: "123"}}, - {in: "~:123", out: Range{start: "", end: "123"}}, - {in: "1", out: Range{start: "0", end: "1"}}, - {in: "@:0", out: Range{start: "0", end: "0", inside: true}}, - {in: "@0:0", out: Range{start: "0", end: "0", inside: true}}, - {in: "@~:0", out: Range{start: "", end: "0", inside: true}}, - {in: "@:123", out: Range{start: "0", end: "123", inside: true}}, - {in: "@0:123", out: Range{start: "0", end: "123", inside: true}}, - {in: "@~:123", out: Range{start: "", end: "123", inside: true}}, - {in: "@1", out: Range{start: "0", end: "1", inside: true}}, - } - - for _, test := range tests { - result, err := ParseRange(test.in) - require.NoError(t, err, "Expected no error, got '%v'", err) - assert.Equal(t, test.out, *result, "Expected '%v', got '%v'", test.out, *result) - } -} - -func TestRangeParserError(t *testing.T) { - type Test struct { - in string - errPos int - } - tests := []Test{ - {in: "", errPos: 0}, - {in: ":", errPos: 1}, - {in: "x:1", errPos: 0}, - {in: ":~", errPos: 1}, - {in: "@", errPos: 1}, - {in: "@:", errPos: 2}, - {in: "@x:1", errPos: 1}, - {in: "@:~", errPos: 2}, - } - - for _, test := range tests { - result, err := ParseRange(test.in) - require.Error(t, err, "Expected error, got '%v'", err) - assert.Nil(t, result, "Expected nil result, got '%v'", result) - assert.True( - t, strings.Contains(err.Error(), fmt.Sprintf("at position %d", test.errPos)), - "Expected error to contain '%s', got '%s'", fmt.Sprintf("at position %d", test.errPos), err, - ) - } -} diff --git a/pkg/perfdata/rangeparser.go b/pkg/perfdata/rangeparser.go new file mode 100644 index 0000000..c9ea0c1 --- /dev/null +++ b/pkg/perfdata/rangeparser.go @@ -0,0 +1,196 @@ +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 +} diff --git a/pkg/perfdata/rangeparser_test.go b/pkg/perfdata/rangeparser_test.go new file mode 100644 index 0000000..8ff5271 --- /dev/null +++ b/pkg/perfdata/rangeparser_test.go @@ -0,0 +1,66 @@ +package perfdata // import nocternity.net/gomonop/pkg/perfdata + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRangeParserOk(t *testing.T) { + type Test struct { + in string + out Range + } + tests := []Test{ + {in: ":0", out: Range{start: "0", end: "0"}}, + {in: "0:0", out: Range{start: "0", end: "0"}}, + {in: "~:0", out: Range{start: "", end: "0"}}, + {in: ":123", out: Range{start: "0", end: "123"}}, + {in: "0:123", out: Range{start: "0", end: "123"}}, + {in: "~:123", out: Range{start: "", end: "123"}}, + {in: "1", out: Range{start: "0", end: "1"}}, + {in: "@:0", out: Range{start: "0", end: "0", inside: true}}, + {in: "@0:0", out: Range{start: "0", end: "0", inside: true}}, + {in: "@~:0", out: Range{start: "", end: "0", inside: true}}, + {in: "@:123", out: Range{start: "0", end: "123", inside: true}}, + {in: "@0:123", out: Range{start: "0", end: "123", inside: true}}, + {in: "@~:123", out: Range{start: "", end: "123", inside: true}}, + {in: "@1", out: Range{start: "0", end: "1", inside: true}}, + } + + for _, test := range tests { + result, err := ParseRange(test.in) + require.NoError(t, err, "Expected no error, got '%v'", err) + assert.Equal(t, test.out, *result, "Expected '%v', got '%v'", test.out, *result) + } +} + +func TestRangeParserError(t *testing.T) { + type Test struct { + in string + errPos int + } + tests := []Test{ + {in: "", errPos: 0}, + {in: ":", errPos: 1}, + {in: "x:1", errPos: 0}, + {in: ":~", errPos: 1}, + {in: "@", errPos: 1}, + {in: "@:", errPos: 2}, + {in: "@x:1", errPos: 1}, + {in: "@:~", errPos: 2}, + } + + for _, test := range tests { + result, err := ParseRange(test.in) + require.Error(t, err, "Expected error, got '%v'", err) + assert.Nil(t, result, "Expected nil result, got '%v'", result) + assert.True( + t, strings.Contains(err.Error(), fmt.Sprintf("at position %d", test.errPos)), + "Expected error to contain '%s', got '%s'", fmt.Sprintf("at position %d", test.errPos), err, + ) + } +}