diff --git a/CHANGELOG.md b/CHANGELOG.md index 22bb4c0..aa2abe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,5 @@ # Changelog -## [0.0.10] - 2026-02-10 - -- ✨ Implement full support for nested fields in Meta and Entry marshalling -- ✨ Add support for nested fields in log entries and JSON marshalling -- ✨ Ensure an id is included when serializing a log entry -- 📝 Add documentation for nested fields -- 🐛 Fix test assertions for added id field and entry serialization - ## [0.0.9] - 2026-02-01 - ✨ Add Set method to Metas type diff --git a/README.md b/README.md index d10730c..8b4aa0c 100644 --- a/README.md +++ b/README.md @@ -73,42 +73,6 @@ As JSON, that would be: }] ``` -#### Nested fields - -This format also supports deeper structures. Of course, you can use raw JSON, like this: - -``` -@begin February 3, 2015 at 01:33PM - Check-in at Piggly Wiggly -@metadata {"lat":"33.6798911","lng":"-84.3959460","shop":"supermarket","osm_id":11617197123,"osm_type":"node","copyright":"The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."} -@url https://www.openstreetmap.org/node/11617197123 @end -``` - -This becomes exactly what you expect: - -```json -{ - "title":"Check-in at Piggly Wiggly", - "date":"2015-02-03T13:33:00Z", - "metadata":{"lat":"33.6798911","lng":"-84.3959460","shop":"supermarket","osm_id":11617197123,"osm_type":"node","copyright":"The data included in this document is from www.openstreetmap.org. The data is made available under ODbL."}, - "url":"https://www.openstreetmap.org/node/11617197123" -} -``` - -This `metadata` field is a bit clunky, though. We can expand it into multiple fields for better readability: - -``` -@begin February 3, 2015 at 01:33PM - Check-in at Piggly Wiggly -@metadata:lat 33.6798911 -@metadata:lng -84.3959460 -@metadata:shop supermarket -@metadata:osm_id 11617197123 -@metadata:osm_type node -@metadata:copyright The data included in this document is from www.openstreetmap.org. The data is made available under ODbL. -@url https://www.openstreetmap.org/node/11617197123 @end -``` - -In addition to improving readability, it may be more suitable for generation by other tools. - ### Adding log entries As was previously noted, the idea is that you can figure out the best way for you to add to the log file. But, `my-log` also comes with a command to add them from the command line. Run `my-log drop --help` for instructions on how to use it. But, here's a few examples: diff --git a/files/append_test.go b/files/append_test.go index 1d6d16f..03f0253 100644 --- a/files/append_test.go +++ b/files/append_test.go @@ -74,9 +74,8 @@ func (s *AppendTestSuite) TestTwoEntries() { s.Assert().NoError(err) s.Require().FileExists(s.dir + "/test.log") by, _ := os.ReadFile(s.dir + "/test.log") - st := string(by) - s.Assert().Contains(st, fmt.Sprintf("@begin %s - one", whens)) - s.Assert().Contains(st, fmt.Sprintf("@begin %s - two", whens)) + exp := fmt.Sprintf("@begin %s - one @end\n@begin %s - two @end\n", whens, whens) + s.Assert().Equal(exp, string(by)) } func (s *AppendTestSuite) TestAddNewLine() { @@ -85,7 +84,7 @@ func (s *AppendTestSuite) TestAddNewLine() { when := time.Now().Local() whens := when.Format(models.DateFormat) e := []models.Entry{ - {Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}}, + {Title: "one", Date: when}, } l := models.Log{ Name: "test", @@ -95,7 +94,7 @@ func (s *AppendTestSuite) TestAddNewLine() { s.Assert().NoError(err) s.Require().FileExists(s.dir + "/test.log") by, _ := os.ReadFile(s.dir + "/test.log") - exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens) + exp := fmt.Sprintf("foo\n@begin %s - one @end\n", whens) s.Assert().Equal(exp, string(by)) } @@ -105,7 +104,7 @@ func (s *AppendTestSuite) TestDontAddNewLine() { when := time.Now().Local() whens := when.Format(models.DateFormat) e := []models.Entry{ - {Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}}, + {Title: "one", Date: when}, } l := models.Log{ Name: "test", @@ -115,7 +114,7 @@ func (s *AppendTestSuite) TestDontAddNewLine() { s.Assert().NoError(err) s.Require().FileExists(s.dir + "/test.log") by, _ := os.ReadFile(s.dir + "/test.log") - exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens) + exp := fmt.Sprintf("foo\n@begin %s - one @end\n", whens) s.Assert().Equal(exp, string(by)) } @@ -151,7 +150,7 @@ func (s *AppendTestSuite) TestDotFolder() { by, err := os.ReadFile(s.dir + "/sub/test.log") st := string(by) s.Require().NoError(err) - s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title)) + s.Assert().Contains(st, "something @end") } func (s *AppendTestSuite) TestDotFolderNo() { @@ -170,7 +169,7 @@ func (s *AppendTestSuite) TestDotFolderNo() { by, err := os.ReadFile(s.dir + "/sub.test.log") st := string(by) s.Require().NoError(err) - s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title)) + s.Assert().Contains(st, "another @end") } func (s *AppendTestSuite) TestNoExt() { @@ -192,7 +191,7 @@ func (s *AppendTestSuite) TestNoExt() { by, err := os.ReadFile(s.dir + "/foobar") st := string(by) s.Require().NoError(err) - s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title)) + s.Assert().Contains(st, "baz @end") } func (s *AppendTestSuite) TestConfLoadErr() { diff --git a/go.mod b/go.mod index aa282b4..b67cb17 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.21.5 require ( github.com/BurntSushi/toml v1.3.2 github.com/caarlos0/env/v10 v10.0.0 - github.com/google/uuid v1.6.0 github.com/markusmobius/go-dateparser v1.2.3 github.com/mitchellh/mapstructure v1.5.0 github.com/spf13/cobra v1.8.0 diff --git a/go.sum b/go.sum index a9fbca0..f782cde 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg= github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= diff --git a/models/entry.go b/models/entry.go index 502db78..2a11fd2 100644 --- a/models/entry.go +++ b/models/entry.go @@ -5,15 +5,12 @@ import ( "bytes" "encoding/json" "errors" - "fmt" - "os" "regexp" "strings" "sync" "time" "codeberg.org/danjones000/my-log/tools" - "github.com/google/uuid" ) const DateFormat = tools.DateFormat @@ -67,18 +64,9 @@ func (e Entry) MarshalText() ([]byte, error) { if e.Title == "" { return []byte{}, ErrorMissingTitle } - if e.Date.IsZero() { + if e.Date == (time.Time{}) { return []byte{}, ErrorMissingDate } - - 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() buff := &bytes.Buffer{} buff.WriteString("@begin ") diff --git a/models/entry_test.go b/models/entry_test.go index a4fd2b7..01c65a7 100644 --- a/models/entry_test.go +++ b/models/entry_test.go @@ -33,14 +33,14 @@ func TestEntryMarshal(t *testing.T) { }{ {"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-line", "A Title", when, simple, "@begin " + whens + " - A Title @end", nolines, nil}, { "one-field", "Title 2", when, []Meta{{"age", 41}}, "@begin " + whens + " - Title 2", - []string{"@age 41", "@id .*"}, + []string{"@age 41 @end"}, nil, }, { @@ -61,42 +61,6 @@ func TestEntryMarshal(t *testing.T) { []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 { @@ -161,14 +125,6 @@ func TestEntryUnmarshal(t *testing.T) { []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`, @@ -253,9 +209,6 @@ func TestEntryJsonMarshal(t *testing.T) { {"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 { diff --git a/models/meta.go b/models/meta.go index c35f302..dba99b2 100644 --- a/models/meta.go +++ b/models/meta.go @@ -2,7 +2,6 @@ package models import ( "bytes" - "encoding/json" "errors" "fmt" "regexp" @@ -19,63 +18,16 @@ func (m Meta) MarshalText() ([]byte, error) { if regexp.MustCompile(`\s`).MatchString(m.Key) { return []byte{}, fmt.Errorf("whitespace is not allowed in key: %s", m.Key) } - buff := &bytes.Buffer{} - - if jv, ok := m.Value.(map[string]any); ok { - err := marshalMap(m.Key, jv, buff) - return buff.Bytes(), err - } - - if jj, ok := m.Value.(json.RawMessage); ok { - mp := map[string]any{} - err := json.Unmarshal(jj, &mp) - if err == nil { - err := marshalMap(m.Key, mp, buff) - return buff.Bytes(), err - } - } - - if err := m.marshalToBuff(buff); err != nil { - return nil, err - } - - return buff.Bytes(), nil -} - -func marshalMap(pre string, mp map[string]any, buff *bytes.Buffer) error { - var idx uint - for k, v := range mp { - if idx > 0 { - buff.WriteRune('\n') - } - idx++ - if subM, ok := v.(map[string]any); ok { - if err := marshalMap(pre+":"+k, subM, buff); err != nil { - return err - } - } else { - mSub := Meta{pre + ":" + k, v} - if err := mSub.marshalToBuff(buff); err != nil { - return err - } - } - } - return nil -} - -func (m Meta) marshalToBuff(buff *bytes.Buffer) error { buff.WriteRune('@') buff.WriteString(m.Key) buff.WriteRune(' ') n, err := tools.WriteValue(buff, m.Value) - if err != nil { - return err + if n == 0 || err != nil { + return []byte{}, err } - if n == 0 { - return ErrorParsing - } - return nil + + return buff.Bytes(), nil } func (m *Meta) UnmarshalText(in []byte) error { diff --git a/models/meta_test.go b/models/meta_test.go index c6dd0e4..29e32a1 100644 --- a/models/meta_test.go +++ b/models/meta_test.go @@ -35,11 +35,12 @@ func TestMeta(t *testing.T) { {"json number", "num", json.Number("42.13"), "@num 42.13", nil, 42.13}, {"true", "b", true, "@b true", nil, true}, {"false", "b", false, "@b false", nil, false}, - {"nil", "n", nil, "", ErrorParsing, ErrorParsing}, + {"nil", "n", nil, "", nil, ErrorParsing}, {"time", "when", when, "@when " + when.Format(time.RFC3339), nil, when}, {"rune", "char", '@', "@char @", nil, "@"}, {"bytes", "byteme", []byte("yo"), "@byteme yo", nil, "yo"}, {"byte", "byteme", byte(67), "@byteme C", nil, "C"}, + {"json-obj", "obj", json.RawMessage(`{"foo":"bar","baz":"quux"}`), `@obj {"foo":"bar","baz":"quux"}`, nil, json.RawMessage(`{"foo":"bar","baz":"quux"}`)}, {"json-arr", "arr", json.RawMessage(`["foo",42,"bar", null,"quux", true]`), `@arr ["foo",42,"bar", null,"quux", true]`, nil, json.RawMessage(`["foo",42,"bar", null,"quux", true]`)}, {"chan", "nope", make(chan bool), "", errors.New("Unsupported type chan bool"), ""}, {"whitespace-key", "no space", "hi", "", errors.New("whitespace is not allowed in key: no space"), ""}, diff --git a/models/metas.go b/models/metas.go index 5b68455..cf4c23e 100644 --- a/models/metas.go +++ b/models/metas.go @@ -2,7 +2,6 @@ package models import ( "encoding/json" - "strings" "time" ) @@ -34,32 +33,9 @@ func (ms Metas) Map() map[string]any { } } } - - // Next we have to go through them again to find nested fields - parseNestedFields(out) - return out } -func parseNestedFields(f map[string]any) { - for k, v := range f { - if strings.Contains(k, ":") { - idx := strings.Index(k, ":") - top := k[:idx] - bottom := k[(idx + 1):] - - nest, ok := f[top].(map[string]any) - if !ok { - nest = map[string]any{} - } - nest[bottom] = v - parseNestedFields(nest) - f[top] = nest - delete(f, k) - } - } -} - // Implements json.Marshaler func (ms Metas) MarshalJSON() ([]byte, error) { return json.Marshal(ms.Map())