From c45acd57c48a98b90e424f1c22302f66e074038f Mon Sep 17 00:00:00 2001 From: Dan Jones Date: Sun, 28 Jan 2024 17:39:42 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Entry=20implements=20TextUnmarshale?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/entry.go | 81 ++++++++++++++++++++++++++++++++++++++++++++ models/entry_test.go | 36 +++++++++++++++++--- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/models/entry.go b/models/entry.go index a836f50..7516628 100644 --- a/models/entry.go +++ b/models/entry.go @@ -1,8 +1,10 @@ package models import ( + "bufio" "bytes" "errors" + "regexp" "strings" "sync" "time" @@ -72,5 +74,84 @@ func (e Entry) MarshalText() ([]byte, error) { } func (m *Entry) UnmarshalText(in []byte) error { + re := regexp.MustCompile("(?s)^@begin (.+) - (.+?)[ \n]@") + match := re.FindSubmatch(in) + if len(match) == 0 { + return newParsingError(errors.New("Failed to find title and date")) + } + + ch := m.getFieldUnarshalChan(in) + + title := bytes.TrimSpace(match[2]) + if len(title) == 0 { + return ErrorMissingTitle + } + m.Title = string(title) + date := string(bytes.TrimSpace(match[1])) + if date == "" { + return ErrorMissingDate + } + d, e := time.Parse(time.RFC3339, date) + if e != nil { + d, e = time.Parse(DateFormat, date) + if e != nil { + return newParsingError(e) + } + } + m.Date = d + + for meta := range ch { + m.Fields = append(m.Fields, meta) + } + return nil } + +func scanEntry(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.Index(data, []byte{10, 64}); i > 0 { + return i + 1, data[0:i], nil + } + if atEOF { + end := []byte{32, 64, 101, 110, 100} + token = data + if i := bytes.Index(data, end); i >= 0 { + token = data[0:i] + } + return len(data), token, nil + } + // Request more data. + return 0, nil, nil +} + +func (e *Entry) getFieldUnarshalChan(in []byte) chan Meta { + size := len(in) / 3 // rough estimation + ch := make(chan Meta, size) + var wg sync.WaitGroup + + read := bytes.NewReader(in) + scan := bufio.NewScanner(read) + scan.Split(scanEntry) + scan.Scan() // throw out first line + + // @todo figure out a way to handle json field + for scan.Scan() { + wg.Add(1) + go func(field []byte) { + defer wg.Done() + m := new(Meta) + err := m.UnmarshalText(field) + if err == nil { + ch <- *m + } + }(scan.Bytes()) + } + + go func() { + wg.Wait() + close(ch) + }() + return ch +} diff --git a/models/entry_test.go b/models/entry_test.go index 7620c86..0e1e4ec 100644 --- a/models/entry_test.go +++ b/models/entry_test.go @@ -1,9 +1,10 @@ package models import ( + "bufio" "encoding" "encoding/json" - "errors" + "strings" "testing" "time" @@ -101,9 +102,12 @@ func TestEntryUnmarshal(t *testing.T) { err error }{ {"one-line", "@begin " + whens + " - A Title @end", "A Title", when, simple, nil}, + {"rfc3999-date", "@begin " + when.Format(time.RFC3339) + " - A Title @end", "A Title", when, simple, nil}, {"multi-title", "@begin " + whens + " - A Title\nwith break @end", "A Title\nwith break", when, simple, nil}, - {"no-title", "@begin " + whens + " - @end", "", when, simple, errors.New("Missing title")}, - {"no-date", "@begin - A Title @end", "A Title", when, simple, errors.New("Missing date")}, + {"no-title", "@begin " + whens + " - @end", "", when, simple, ErrorMissingTitle}, + {"parse-error", "this is no good", "", when, simple, ErrorParsing}, + {"no-date", "@begin - A Title @end", "A Title", when, simple, ErrorMissingDate}, + {"bad-date", "@begin not-a-real date - A Title @end", "A Title", when, simple, ErrorParsing}, {"one-field", "@begin " + whens + " - A Title\n@age 41 @end", "A Title", when, []Meta{{"age", 41}}, nil}, { "two-fields", @@ -142,8 +146,8 @@ func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields return func(t *testing.T) { e := &Entry{} er := e.UnmarshalText([]byte(in)) - assert.Equal(t, err, er) if err != nil { + assert.ErrorIs(t, er, err) return } @@ -152,7 +156,15 @@ func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields for _, f := range fields { got := false for _, m := range e.Fields { - if m == f { + var mVal any = m.Value + var fVal any = f.Value + if mJ, ok := m.Value.(json.RawMessage); ok { + mVal = string(mJ) + } + if fJ, ok := f.Value.(json.RawMessage); ok { + fVal = string(fJ) + } + if m.Key == f.Key && mVal == fVal { got = true break } @@ -161,3 +173,17 @@ func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields } } } + +func TestScan(t *testing.T) { + in := "@begin date - Title\nlong\n@foo john\njones\n@bar 42@nobody @end" + read := strings.NewReader(in) + scan := bufio.NewScanner(read) + scan.Split(scanEntry) + assert.True(t, scan.Scan()) + assert.Equal(t, "@begin date - Title\nlong", scan.Text()) + assert.True(t, scan.Scan()) + assert.Equal(t, "@foo john\njones", scan.Text()) + assert.True(t, scan.Scan()) + assert.Equal(t, "@bar 42@nobody", scan.Text()) + assert.False(t, scan.Scan()) +}