diff --git a/models/entry.go b/models/entry.go index efd58b6..46190e5 100644 --- a/models/entry.go +++ b/models/entry.go @@ -20,18 +20,47 @@ import ( const DateFormat = tools.DateFormat type Entry struct { + // ID is an optional, but recommended field. + // When marshaling/unmarshaling, if ID is empty, one is generated. + ID string Title string Date time.Time Fields Metas } +func GenerateID() string { + host, hostErr := os.Hostname() + if hostErr != nil { + host = "localhost" + } + return fmt.Sprintf( + "tag:%s,%s:my-log/%s", + host, + time.Now().Local().Format("2006"), + uuid.NewString(), + ) +} + +func EnsureID(id *string, fields Metas) { + if *id != "" { + return + } + + metaID, hasMetaID := fields.Get("id") + var isStringID bool + *id, isStringID = metaID.(string) + if !hasMetaID || !isStringID { + *id = GenerateID() + } +} + type metaRes struct { out []byte err error } -func (e Entry) getFieldMarshalChan() chan metaRes { - size := len(e.Fields) +func getFieldMarshalChan(fields Metas) chan metaRes { + size := len(fields) ch := make(chan metaRes, size) var wg sync.WaitGroup @@ -53,7 +82,7 @@ func (e Entry) getFieldMarshalChan() chan metaRes { ch <- metaRes{o, er} } - }(e.Fields[i]) + }(fields[i]) } go func() { @@ -71,16 +100,10 @@ func (e Entry) MarshalText() ([]byte, error) { if e.Date.IsZero() { return []byte{}, ErrorMissingDate } + EnsureID(&e.ID, e.Fields) + fields := e.Fields.Set("id", e.ID) - if _, hasId := e.Fields.Get("id"); !hasId { - host, hostErr := os.Hostname() - if hostErr != nil { - host = "localhost" - } - e.Fields = e.Fields.Set("id", fmt.Sprintf("tag:%s,%s:my-log/%s", host, time.Now().Format("2006"), uuid.NewString())) - } - - ch := e.getFieldMarshalChan() + ch := getFieldMarshalChan(fields) buff := &bytes.Buffer{} buff.WriteString("@begin ") buff.WriteString(e.Date.Format(DateFormat)) @@ -124,8 +147,14 @@ func (m *Entry) UnmarshalText(in []byte) error { m.Date = d for meta := range ch { + if meta.Key == "id" { + if id, idIsString := meta.Value.(string); idIsString { + m.ID = id + } + } m.Fields = append(m.Fields, meta) } + EnsureID(&m.ID, m.Fields) return nil } @@ -195,11 +224,13 @@ func (e Entry) MarshalJSON() ([]byte, error) { if e.Date == (time.Time{}) { return []byte{}, ErrorMissingDate } + EnsureID(&e.ID, e.Fields) + fields := e.Fields.Set("id", e.ID) out := map[string]any{} out["title"] = e.Title out["date"] = e.Date.Format(time.RFC3339) - maps.Copy(out, e.Fields.Map()) + maps.Copy(out, fields.Map()) return json.Marshal(out) } @@ -281,5 +312,6 @@ func (e *Entry) UnmarshalJSON(in []byte) error { } e.Fields = append(e.Fields, m) } + EnsureID(&e.ID, e.Fields) return nil } diff --git a/models/entry_test.go b/models/entry_test.go index 6372897..4baabbe 100644 --- a/models/entry_test.go +++ b/models/entry_test.go @@ -9,7 +9,6 @@ import ( "testing" "time" - "codeberg.org/danjones000/my-log/internal/testutil/bep" "github.com/nalgeon/be" ) @@ -126,7 +125,7 @@ func TestEntryMarshal(t *testing.T) { 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} + en := Entry{Title: title, Date: date, Fields: fields} o, er := en.MarshalText() be.Err(t, er, err) if first == "" { @@ -154,22 +153,24 @@ func TestEntryUnmarshal(t *testing.T) { in string title string date time.Time + id string 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}, + {"one-line", "@begin " + whens + " - A Title @end", "A Title", when, "", simple, nil}, + {"with-id", "@begin " + whens + " - A Title\n@id jimmy-john @end", "A Title", when, "jimmy-john", 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, + when, "", []Meta{{"age", 41}, {"cool", true}}, nil, }, @@ -177,7 +178,7 @@ func TestEntryUnmarshal(t *testing.T) { "obj-field", "@begin " + whens + " - A Title\n" + `@me {"name":"Dan","coder":true} @end`, "A Title", - when, + when, "", []Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}}, nil, }, @@ -185,7 +186,7 @@ func TestEntryUnmarshal(t *testing.T) { "nested-field", "@begin " + whens + " - A Title\n@me:name Dan\n@me:coder true @end", "A Title", - when, + when, "", []Meta{{"me:name", "Dan"}, {"me:coder", true}}, nil, }, @@ -193,7 +194,7 @@ func TestEntryUnmarshal(t *testing.T) { "nested-field-dot", "@begin " + whens + " - A Title\n@me:name Dan Jones\n@me:name:nick Danny\n@me:coder true @end", "A Title", - when, + when, "", []Meta{{"me:name", "Dan Jones"}, {"me:name:nick", "Danny"}, {"me:coder", true}}, nil, }, @@ -201,18 +202,18 @@ func TestEntryUnmarshal(t *testing.T) { "json-field", "@begin " + whens + " - Some Guy\n" + `@json {"name":"Dan","coder":true} @end`, "Some Guy", - when, + 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)) + t.Run(tt.name, getEntryUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.id, tt.fields, tt.err)) } } -func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields []Meta, err error) func(*testing.T) { +func getEntryUnmarshalTestRunner(in string, title string, date time.Time, id string, fields []Meta, err error) func(*testing.T) { return func(t *testing.T) { e := &Entry{} er := e.UnmarshalText([]byte(in)) @@ -223,6 +224,9 @@ func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields be.Equal(t, e.Title, title) be.True(t, e.Date.After(date.Add(-time.Second)) && e.Date.Before(date.Add(time.Second))) + if id != "" { + be.Equal(t, e.ID, id) + } for _, f := range fields { got := false for _, m := range e.Fields { @@ -263,46 +267,282 @@ func TestEntryJsonMarshal(t *testing.T) { whens := when.Format(time.RFC3339) simple := []Meta{} tests := []struct { - name string - title string - date time.Time - fields []Meta - out string - err error + name string + title string + date time.Time + fields []Meta + check func(*testing.T, []byte) + wantErr 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}, - {"nested-part", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me", "Dan"}, {"obj:me:age", 27}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27}}}`, nil}, - {"nested-part-order", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27}}}`, nil}, - {"nested-part-order-two", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}, {"obj:me:cool", true}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{".":"Dan","age":27,"cool":true}}}`, nil}, + { + "simple", + "A Title", + when, + simple, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["title"].(string), "A Title") + be.Equal(t, m["date"].(string), whens) + _, hasID := m["id"].(string) + be.True(t, hasID) + }, + nil, + }, + { + "one-field", + "A Title 2", + when, + []Meta{{"age", 41}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["title"].(string), "A Title 2") + be.Equal(t, m["date"].(string), whens) + be.Equal(t, m["age"].(float64), float64(41)) + }, + nil, + }, + { + "skip-title-field", + "A Title", + when, + []Meta{{"title", "Different title"}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["title"].(string), "A Title") + }, + nil, + }, + { + "skip-date-field", + "A Title", + when, + []Meta{{"date", when.Add(time.Hour)}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["date"].(string), whens) + }, + nil, + }, + { + "skip-dupe-field", + "A Title", + when, + []Meta{{"foo", "bar"}, {"foo", "baz"}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["foo"].(string), "bar") + }, + nil, + }, + { + "two-fields", + "A Title", + when, + []Meta{{"foo", "bar"}, {"baz", 42}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["foo"].(string), "bar") + be.Equal(t, m["baz"].(float64), float64(42)) + }, + nil, + }, + { + "empty-title", + "", + when, + simple, + nil, + ErrorMissingTitle, + }, + { + "empty-date", + "A Title", + time.Time{}, + simple, + nil, + ErrorMissingDate, + }, + { + "obj-field", + "A Title", + when, + []Meta{{"obj", json.RawMessage(`{"foo":"bar","title":"Sub-title"}`)}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + be.Equal(t, obj["title"].(string), "Sub-title") + }, + nil, + }, + { + "date-field", + "A Title", + when, + []Meta{{"when", when.Add(time.Hour)}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["when"].(string), when.Add(time.Hour).Format(time.RFC3339)) + }, + nil, + }, + { + "json-field", + "A Title", + when, + []Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + be.Equal(t, m["age"].(float64), float64(41)) + be.Equal(t, m["cool"].(bool), true) + be.Equal(t, m["name"].(string), "Jim") + }, + nil, + }, + { + "nested-field", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:title", "Sub-title"}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + be.Equal(t, obj["title"].(string), "Sub-title") + }, + nil, + }, + { + "double-nested-field", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:me:name", "Dan"}, {"obj:me:age", 27}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + me, ok := obj["me"].(map[string]any) + be.True(t, ok) + be.Equal(t, me["name"].(string), "Dan") + be.Equal(t, me["age"].(float64), float64(27)) + }, + nil, + }, + { + "nested-plus-json", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:me", json.RawMessage(`{"name":"Dan","age":27}`)}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + me, ok := obj["me"].(map[string]any) + be.True(t, ok) + be.Equal(t, me["name"].(string), "Dan") + be.Equal(t, me["age"].(float64), float64(27)) + }, + nil, + }, + { + "nested-part", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:me", "Dan"}, {"obj:me:age", 27}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + me, ok := obj["me"].(map[string]any) + be.True(t, ok) + be.Equal(t, me["."].(string), "Dan") + be.Equal(t, me["age"].(float64), float64(27)) + }, + nil, + }, + { + "nested-part-order", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + me, ok := obj["me"].(map[string]any) + be.True(t, ok) + be.Equal(t, me["."].(string), "Dan") + be.Equal(t, me["age"].(float64), float64(27)) + }, + nil, + }, + { + "nested-part-order-two", + "A Title", + when, + []Meta{{"obj:foo", "bar"}, {"obj:me:age", 27}, {"obj:me", "Dan"}, {"obj:me:cool", true}}, + func(t *testing.T, o []byte) { + var m map[string]any + err := json.Unmarshal(o, &m) + be.Err(t, err, nil) + obj, ok := m["obj"].(map[string]any) + be.True(t, ok) + be.Equal(t, obj["foo"].(string), "bar") + me, ok := obj["me"].(map[string]any) + be.True(t, ok) + be.Equal(t, me["."].(string), "Dan") + be.Equal(t, me["age"].(float64), float64(27)) + be.Equal(t, me["cool"].(bool), true) + }, + 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 { - bep.JSON(t, o, []byte(out)) - } else { - be.Err(t, er, err) - } + t.Run(tt.name, func(t *testing.T) { + e := Entry{Title: tt.title, Date: tt.date, Fields: tt.fields} + o, er := json.Marshal(e) + if tt.wantErr == nil { + be.Err(t, er, nil) + tt.check(t, o) + } else { + be.Err(t, er, tt.wantErr) + } + }) } }