diff --git a/parser.go b/parser.go index dcdbbe9..b83e035 100644 --- a/parser.go +++ b/parser.go @@ -45,6 +45,8 @@ var ( ErrInvalidLabel = xerrors.New("invalid label") // ErrInvalidValue is an error to describe value contains invalid char (ex. 'my_label:my_value\n') ErrInvalidValue = xerrors.New("invalid value") + // Break is an error for break loop + Break = xerrors.New("break") ) // ParseField parse LTSV-encoded field and return the label and value. @@ -75,7 +77,7 @@ func (p Parser) ParseField(field []byte) (label []byte, value []byte, err error) // ParseLine parse one line of LTSV-encoded data and call callback. // The callback function will be called for each field. -func (p Parser) ParseLine(line []byte, callback func(label []byte, value []byte)) error { +func (p Parser) ParseLine(line []byte, callback func(label []byte, value []byte) error) error { oriLine := line for len(line) > 0 { idx := bytes.IndexByte(line, p.FieldDelimiter) @@ -95,7 +97,12 @@ func (p Parser) ParseLine(line []byte, callback func(label []byte, value []byte) return xerrors.Errorf("bad line syntax %q: %w", string(oriLine), err) } - callback(label, value) + if err = callback(label, value); err != nil { + if err == Break { + break + } + return xerrors.Errorf("ParseLine callback error: %w", err) + } } return nil } @@ -106,8 +113,9 @@ func (p Parser) ParseLineAsMap(line []byte, record map[string]string) (map[strin if record == nil { record = map[string]string{} } - err := p.ParseLine(line, func(label []byte, value []byte) { + err := p.ParseLine(line, func(label []byte, value []byte) error { record[string(label)] = string(value) + return nil }) if err != nil { return nil, xerrors.Errorf(": %w", err) @@ -119,8 +127,9 @@ func (p Parser) ParseLineAsMap(line []byte, record map[string]string) (map[strin // For reducing memory allocation, you can pass a slice to record to reuse the given slice. func (p Parser) ParseLineAsSlice(line []byte, record []Field) ([]Field, error) { record = record[:0] - err := p.ParseLine(line, func(label []byte, value []byte) { + err := p.ParseLine(line, func(label []byte, value []byte) error { record = append(record, Field{string(label), string(value)}) + return nil }) if err != nil { return nil, xerrors.Errorf(": %w", err) diff --git a/parser_test.go b/parser_test.go index fcef655..00eb512 100644 --- a/parser_test.go +++ b/parser_test.go @@ -12,7 +12,7 @@ import ( func TestParseLine(t *testing.T) { line := []byte("foo:123\tbar:456") - ParseLine(line, func(label []byte, value []byte) { + ParseLine(line, func(label []byte, value []byte) error { val := string(value) switch string(label) { case "foo": @@ -22,9 +22,34 @@ func TestParseLine(t *testing.T) { default: t.Errorf("unknown label: %s", string(label)) } + return nil }) } +func TestParseLine_break(t *testing.T) { + line := []byte("foo:123\tbar:456") + counter := 0 + err := ParseLine(line, func(label []byte, value []byte) error { + counter++ + return Break + }) + assert.NoError(t, err) + assert.Equal(t, 1, counter) +} + +func TestParseLine_error(t *testing.T) { + line := []byte("foo:123\tbar:456") + counter := 0 + customErr := xerrors.New("custom error") + err := ParseLine(line, func(label []byte, value []byte) error { + counter++ + return customErr + }) + assert.Error(t, err) + assert.True(t, xerrors.Is(err, customErr)) + assert.Equal(t, 1, counter) +} + func TestParseLineAsMap(t *testing.T) { tests := []struct { name string @@ -136,7 +161,7 @@ func TestParseField(t *testing.T) { } } -func BenchmarkParseLine(b *testing.B) { +func BenchmarkParseLineAsMap(b *testing.B) { parser := DefaultParser parser.StrictMode = false line := []byte("host:127.0.0.1\tident:-\tuser:frank\ttime:[10/Oct/2000:13:55:36 -0700]\treq:GET /apache_pb.gif HTTP/1.0\tstatus:200\tsize:2326\treferer:http://www.example.com/start.html\tua:Mozilla/4.08 [en] (Win98; I ;Nav)") @@ -145,3 +170,14 @@ func BenchmarkParseLine(b *testing.B) { parser.ParseLineAsMap(line, m) } } + +func BenchmarkParseLine(b *testing.B) { + parser := DefaultParser + parser.StrictMode = false + line := []byte("host:127.0.0.1\tident:-\tuser:frank\ttime:[10/Oct/2000:13:55:36 -0700]\treq:GET /apache_pb.gif HTTP/1.0\tstatus:200\tsize:2326\treferer:http://www.example.com/start.html\tua:Mozilla/4.08 [en] (Win98; I ;Nav)") + for i := 0; i < b.N; i++ { + parser.ParseLine(line, func(label []byte, value []byte) error { + return nil + }) + } +} diff --git a/static.go b/static.go index 42e8d11..3b5d6d5 100644 --- a/static.go +++ b/static.go @@ -8,7 +8,7 @@ func ParseField(field []byte) (label []byte, value []byte, err error) { // ParseLine parse one line of LTSV-encoded data and call callback. // The callback function will be called for each field. -func ParseLine(line []byte, callback func(label []byte, value []byte)) error { +func ParseLine(line []byte, callback func(label []byte, value []byte) error) error { return DefaultParser.ParseLine(line, callback) }