This commit completes the implementation of nested field support.
- :
- now correctly handles and by recursively flattening them into format.
- Introduced for recursive map marshalling.
- Refactored for cleaner buffer writing.
- : Added comprehensive test cases for nested JSON, nested maps, double-nested maps, and nested keys within JSON to ensure correct marshalling and unmarshalling.
- : Updated tests to reflect the new nil handling and removed redundant JSON object test.
This allows for more flexible and structured data representation within log entries.
385 lines
12 KiB
Go
385 lines
12 KiB
Go
package models
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Type assertions
|
|
var _ encoding.TextMarshaler = Entry{}
|
|
var _ encoding.TextUnmarshaler = new(Entry)
|
|
var _ json.Marshaler = Entry{}
|
|
var _ json.Unmarshaler = new(Entry)
|
|
|
|
func TestEntryMarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(DateFormat)
|
|
simple := []Meta{}
|
|
nolines := []string{}
|
|
tests := []struct {
|
|
name string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
first string
|
|
lines []string
|
|
err error
|
|
}{
|
|
{"no-title", "", when, simple, "", nolines, ErrorMissingTitle},
|
|
{"zero-date", "Empty title", time.Time{}, simple, "", nolines, ErrorMissingDate},
|
|
{"one-line", "A Title", when, simple, "@begin " + whens + " - A Title", []string{"@id .+ @end"}, nil},
|
|
{
|
|
"one-field",
|
|
"Title 2",
|
|
when,
|
|
[]Meta{{"age", 41}},
|
|
"@begin " + whens + " - Title 2",
|
|
[]string{"@age 41", "@id .*"},
|
|
nil,
|
|
},
|
|
{
|
|
"three-fields",
|
|
"Title 3",
|
|
when,
|
|
[]Meta{{"age", 41}, {"cool", true}, {"name", "Jim"}},
|
|
"@begin " + whens + " - Title 3",
|
|
[]string{"@age 41", "@cool true", "@name Jim"},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
"Title J",
|
|
when,
|
|
[]Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}},
|
|
"@begin " + whens + " - Title J",
|
|
[]string{"@age 41", "@cool true", "@name Jim"},
|
|
nil,
|
|
},
|
|
{
|
|
"nested-json",
|
|
"Title N",
|
|
when,
|
|
[]Meta{{"me", json.RawMessage(`{"age": 43, "cool": true}`)}},
|
|
"@begin " + whens + " - Title N",
|
|
[]string{"@me:age 43", "@me:cool true"},
|
|
nil,
|
|
},
|
|
{
|
|
"nested-map",
|
|
"Title M",
|
|
when,
|
|
[]Meta{{"me", map[string]any{"age": 43, "cool": true}}},
|
|
"@begin " + whens + " - Title M",
|
|
[]string{"@me:age 43", "@me:cool true"},
|
|
nil,
|
|
},
|
|
{
|
|
"double-nested-map",
|
|
"Title DM",
|
|
when,
|
|
[]Meta{{"me", map[string]any{"age": 43, "name": map[string]any{"first": "Dan", "last": "Jones"}}}},
|
|
"@begin " + whens + " - Title DM",
|
|
[]string{"@me:age 43", "@me:name:first Dan", "@me:name:last Jones"},
|
|
nil,
|
|
},
|
|
{
|
|
"nested-keys-in-json",
|
|
"Title NKJ",
|
|
when,
|
|
[]Meta{{"me", json.RawMessage(`{"name:first": "Dan", "name:last": "Jones"}`)}},
|
|
"@begin " + whens + " - Title NKJ",
|
|
[]string{"@me:name:first Dan", "@me:name:last Jones"},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryMarshalTestRunner(tt.title, tt.date, tt.fields, tt.first, tt.lines, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryMarshalTestRunner(title string, date time.Time, fields []Meta, first string, lines []string, err error) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
en := Entry{title, date, fields}
|
|
o, er := en.MarshalText()
|
|
assert.Equal(t, err, er)
|
|
if first == "" {
|
|
return
|
|
}
|
|
os := string(o)
|
|
if len(lines) == 0 {
|
|
assert.Equal(t, first, os)
|
|
return
|
|
}
|
|
|
|
assert.Regexp(t, first, os)
|
|
for _, line := range lines {
|
|
assert.Regexp(t, "(?m)^"+line, os)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryUnmarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(DateFormat)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
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, 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",
|
|
"@begin " + whens + " - A Title\n@age 41\n@cool true @end",
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", 41}, {"cool", true}},
|
|
nil,
|
|
},
|
|
{
|
|
"obj-field",
|
|
"@begin " + whens + " - A Title\n" + `@me {"name":"Dan","coder":true} @end`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}},
|
|
nil,
|
|
},
|
|
{
|
|
"nested-field",
|
|
"@begin " + whens + " - A Title\n@me:name Dan\n@me:coder true @end",
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"me:name", "Dan"}, {"me:coder", true}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
"@begin " + whens + " - Some Guy\n" + `@json {"name":"Dan","coder":true} @end`,
|
|
"Some Guy",
|
|
when,
|
|
[]Meta{{"name", "Dan"}, {"coder", true}},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields []Meta, err error) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
e := &Entry{}
|
|
er := e.UnmarshalText([]byte(in))
|
|
if err != nil {
|
|
assert.ErrorIs(t, er, err)
|
|
return
|
|
}
|
|
|
|
assert.Equal(t, title, e.Title)
|
|
assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second))
|
|
for _, f := range fields {
|
|
got := false
|
|
for _, m := range e.Fields {
|
|
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
|
|
}
|
|
}
|
|
assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.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())
|
|
}
|
|
|
|
func TestEntryJsonMarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(time.RFC3339)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
out string
|
|
err error
|
|
}{
|
|
{"simple", "A Title", when, simple, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"one-field", "A Title 2", when, []Meta{{"age", 41}}, `{"title":"A Title 2","date":"` + whens + `","age":41}`, nil},
|
|
{"skip-title-field", "A Title", when, []Meta{{"title", "Different title"}}, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"skip-date-field", "A Title", when, []Meta{{"date", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"skip-dupe-field", "A Title", when, []Meta{{"foo", "bar"}, {"foo", "baz"}}, `{"title":"A Title","date":"` + whens + `","foo": "bar"}`, nil},
|
|
{"two-fields", "A Title", when, []Meta{{"foo", "bar"}, {"baz", 42}}, `{"title":"A Title","date":"` + whens + `","foo": "bar","baz":42}`, nil},
|
|
{"empty-title", "", when, simple, "", ErrorMissingTitle},
|
|
{"empty-date", "A Title", time.Time{}, simple, "", ErrorMissingDate},
|
|
{"obj-field", "A Title", when, []Meta{{"obj", json.RawMessage(`{"foo":"bar","title":"Sub-title"}`)}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil},
|
|
{"date-field", "A Title", when, []Meta{{"when", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `","when":"` + when.Add(time.Hour).Format(time.RFC3339) + `"}`, nil},
|
|
{"json-field", "A Title", when, []Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}}, `{"title":"A Title","date":"` + whens + `","age":41,"cool": true, "name": "Jim"}`, nil},
|
|
{"nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:title", "Sub-title"}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil},
|
|
{"double-nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:name", "Dan"}, {"obj:me:age", 27}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{"name":"Dan","age":27}}}`, nil},
|
|
{"nested-plus-json", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me", json.RawMessage(`{"name":"Dan","age":27}`)}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{"name":"Dan","age":27}}}`, nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryJsonMarshalTestRunner(tt.title, tt.date, tt.fields, tt.out, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryJsonMarshalTestRunner(title string, date time.Time, fields []Meta, out string, err error) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
e := Entry{title, date, fields}
|
|
o, er := json.Marshal(e)
|
|
if err == nil {
|
|
assert.JSONEq(t, out, string(o))
|
|
|
|
} else {
|
|
assert.ErrorIs(t, er, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryJsonUnmarshal(t *testing.T) {
|
|
when := time.Now().Truncate(time.Second)
|
|
whens := when.Format(time.RFC3339)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
err error
|
|
}{
|
|
{"simple", `{"title":"A Title","date":"` + whens + `"}`, "A Title", when, simple, nil},
|
|
{"missing-title", `{"date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle},
|
|
{"missing-date", `{"title":"A Title"}`, "", when, simple, ErrorMissingDate},
|
|
{"empty-title", `{"title":"","date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle},
|
|
{"empty-date", `{"title":"A Title","date":""}`, "", when, simple, ErrorMissingDate},
|
|
{"bad-date", `{"title":"A Title","date":"bad"}`, "", when, simple, ErrorParsing},
|
|
{"bad-json", `{"title":"A Title","date":"`, "", when, simple, ErrorParsing},
|
|
{
|
|
"single-field",
|
|
`{"title":"A Title","date":"` + whens + `","hello":"Hi"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"hello", "Hi"}},
|
|
nil,
|
|
},
|
|
{
|
|
"many-fields",
|
|
`{"title":"A Title","date":"` + whens + `","hello":"Hi","bye":42,"b":true,"fl":42.13}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"hello", "Hi"}, {"bye", int64(42)}, {"b", true}, {"fl", float64(42.13)}},
|
|
nil,
|
|
},
|
|
{
|
|
"date-field",
|
|
`{"title":"A Title","date":"` + whens + `","posted":"` + when.Add(-time.Hour).In(time.UTC).Format(time.RFC3339) + `"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"posted", when.Add(-time.Hour)}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
`{"title":"A Title","date":"` + whens + `","json":{"age": 41, "cool": true, "name": "Jim"}}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field-embed",
|
|
`{"title":"A Title","date":"` + whens + `","json":"{\"age\": 41, \"cool\": true, \"name\": \"Jim\"}"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryJsonUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryJsonUnmarshalTestRunner(in, title string, date time.Time, fields []Meta, err error) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
e := new(Entry)
|
|
er := e.UnmarshalJSON([]byte(in))
|
|
if err != nil {
|
|
assert.ErrorIs(t, er, err)
|
|
return
|
|
}
|
|
|
|
assert.Nil(t, er)
|
|
assert.Equal(t, title, e.Title)
|
|
assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second))
|
|
assert.Len(t, e.Fields, len(fields))
|
|
for _, f := range fields {
|
|
got := false
|
|
fTime, isTime := f.Value.(time.Time)
|
|
for _, m := range e.Fields {
|
|
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
|
|
}
|
|
if isTime && m.Key == f.Key {
|
|
mTime, _ := mVal.(time.Time)
|
|
if assert.WithinRange(t, mTime, fTime.Add(-2*time.Second), fTime.Add(2*time.Second)) {
|
|
got = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.Fields)
|
|
}
|
|
}
|
|
}
|