Compare commits

..

No commits in common. "44ecfd6ac954346c3f9902b4a701a6c3fccaf962" and "26da173dc77aa9f4f7726029760112a2b087733c" have entirely different histories.

10 changed files with 18 additions and 196 deletions

View file

@ -1,13 +1,5 @@
# Changelog # 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 ## [0.0.9] - 2026-02-01
- ✨ Add Set method to Metas type - ✨ Add Set method to Metas type

View file

@ -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 ### 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: 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:

View file

@ -74,9 +74,8 @@ func (s *AppendTestSuite) TestTwoEntries() {
s.Assert().NoError(err) s.Assert().NoError(err)
s.Require().FileExists(s.dir + "/test.log") s.Require().FileExists(s.dir + "/test.log")
by, _ := os.ReadFile(s.dir + "/test.log") by, _ := os.ReadFile(s.dir + "/test.log")
st := string(by) exp := fmt.Sprintf("@begin %s - one @end\n@begin %s - two @end\n", whens, whens)
s.Assert().Contains(st, fmt.Sprintf("@begin %s - one", whens)) s.Assert().Equal(exp, string(by))
s.Assert().Contains(st, fmt.Sprintf("@begin %s - two", whens))
} }
func (s *AppendTestSuite) TestAddNewLine() { func (s *AppendTestSuite) TestAddNewLine() {
@ -85,7 +84,7 @@ func (s *AppendTestSuite) TestAddNewLine() {
when := time.Now().Local() when := time.Now().Local()
whens := when.Format(models.DateFormat) whens := when.Format(models.DateFormat)
e := []models.Entry{ e := []models.Entry{
{Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}}, {Title: "one", Date: when},
} }
l := models.Log{ l := models.Log{
Name: "test", Name: "test",
@ -95,7 +94,7 @@ func (s *AppendTestSuite) TestAddNewLine() {
s.Assert().NoError(err) s.Assert().NoError(err)
s.Require().FileExists(s.dir + "/test.log") s.Require().FileExists(s.dir + "/test.log")
by, _ := os.ReadFile(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)) s.Assert().Equal(exp, string(by))
} }
@ -105,7 +104,7 @@ func (s *AppendTestSuite) TestDontAddNewLine() {
when := time.Now().Local() when := time.Now().Local()
whens := when.Format(models.DateFormat) whens := when.Format(models.DateFormat)
e := []models.Entry{ e := []models.Entry{
{Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}}, {Title: "one", Date: when},
} }
l := models.Log{ l := models.Log{
Name: "test", Name: "test",
@ -115,7 +114,7 @@ func (s *AppendTestSuite) TestDontAddNewLine() {
s.Assert().NoError(err) s.Assert().NoError(err)
s.Require().FileExists(s.dir + "/test.log") s.Require().FileExists(s.dir + "/test.log")
by, _ := os.ReadFile(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)) s.Assert().Equal(exp, string(by))
} }
@ -151,7 +150,7 @@ func (s *AppendTestSuite) TestDotFolder() {
by, err := os.ReadFile(s.dir + "/sub/test.log") by, err := os.ReadFile(s.dir + "/sub/test.log")
st := string(by) st := string(by)
s.Require().NoError(err) 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() { func (s *AppendTestSuite) TestDotFolderNo() {
@ -170,7 +169,7 @@ func (s *AppendTestSuite) TestDotFolderNo() {
by, err := os.ReadFile(s.dir + "/sub.test.log") by, err := os.ReadFile(s.dir + "/sub.test.log")
st := string(by) st := string(by)
s.Require().NoError(err) 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() { func (s *AppendTestSuite) TestNoExt() {
@ -192,7 +191,7 @@ func (s *AppendTestSuite) TestNoExt() {
by, err := os.ReadFile(s.dir + "/foobar") by, err := os.ReadFile(s.dir + "/foobar")
st := string(by) st := string(by)
s.Require().NoError(err) 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() { func (s *AppendTestSuite) TestConfLoadErr() {

1
go.mod
View file

@ -5,7 +5,6 @@ go 1.21.5
require ( require (
github.com/BurntSushi/toml v1.3.2 github.com/BurntSushi/toml v1.3.2
github.com/caarlos0/env/v10 v10.0.0 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/markusmobius/go-dateparser v1.2.3
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0

2
go.sum
View file

@ -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/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 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og= 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 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ= github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0= github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=

View file

@ -5,15 +5,12 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"os"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
"codeberg.org/danjones000/my-log/tools" "codeberg.org/danjones000/my-log/tools"
"github.com/google/uuid"
) )
const DateFormat = tools.DateFormat const DateFormat = tools.DateFormat
@ -67,18 +64,9 @@ func (e Entry) MarshalText() ([]byte, error) {
if e.Title == "" { if e.Title == "" {
return []byte{}, ErrorMissingTitle return []byte{}, ErrorMissingTitle
} }
if e.Date.IsZero() { if e.Date == (time.Time{}) {
return []byte{}, ErrorMissingDate 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() ch := e.getFieldMarshalChan()
buff := &bytes.Buffer{} buff := &bytes.Buffer{}
buff.WriteString("@begin ") buff.WriteString("@begin ")

View file

@ -33,14 +33,14 @@ func TestEntryMarshal(t *testing.T) {
}{ }{
{"no-title", "", when, simple, "", nolines, ErrorMissingTitle}, {"no-title", "", when, simple, "", nolines, ErrorMissingTitle},
{"zero-date", "Empty title", time.Time{}, simple, "", nolines, ErrorMissingDate}, {"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", "one-field",
"Title 2", "Title 2",
when, when,
[]Meta{{"age", 41}}, []Meta{{"age", 41}},
"@begin " + whens + " - Title 2", "@begin " + whens + " - Title 2",
[]string{"@age 41", "@id .*"}, []string{"@age 41 @end"},
nil, nil,
}, },
{ {
@ -61,42 +61,6 @@ func TestEntryMarshal(t *testing.T) {
[]string{"@age 41", "@cool true", "@name Jim"}, []string{"@age 41", "@cool true", "@name Jim"},
nil, 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 { for _, tt := range tests {
@ -161,14 +125,6 @@ func TestEntryUnmarshal(t *testing.T) {
[]Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}}, []Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}},
nil, 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", "json-field",
"@begin " + whens + " - Some Guy\n" + `@json {"name":"Dan","coder":true} @end`, "@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}, {"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}, {"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}, {"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 { for _, tt := range tests {

View file

@ -2,7 +2,6 @@ package models
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
@ -19,63 +18,16 @@ func (m Meta) MarshalText() ([]byte, error) {
if regexp.MustCompile(`\s`).MatchString(m.Key) { if regexp.MustCompile(`\s`).MatchString(m.Key) {
return []byte{}, fmt.Errorf("whitespace is not allowed in key: %s", m.Key) return []byte{}, fmt.Errorf("whitespace is not allowed in key: %s", m.Key)
} }
buff := &bytes.Buffer{} 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.WriteRune('@')
buff.WriteString(m.Key) buff.WriteString(m.Key)
buff.WriteRune(' ') buff.WriteRune(' ')
n, err := tools.WriteValue(buff, m.Value) n, err := tools.WriteValue(buff, m.Value)
if err != nil { if n == 0 || err != nil {
return err return []byte{}, err
} }
if n == 0 {
return ErrorParsing return buff.Bytes(), nil
}
return nil
} }
func (m *Meta) UnmarshalText(in []byte) error { func (m *Meta) UnmarshalText(in []byte) error {

View file

@ -35,11 +35,12 @@ func TestMeta(t *testing.T) {
{"json number", "num", json.Number("42.13"), "@num 42.13", nil, 42.13}, {"json number", "num", json.Number("42.13"), "@num 42.13", nil, 42.13},
{"true", "b", true, "@b true", nil, true}, {"true", "b", true, "@b true", nil, true},
{"false", "b", false, "@b false", nil, false}, {"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}, {"time", "when", when, "@when " + when.Format(time.RFC3339), nil, when},
{"rune", "char", '@', "@char @", nil, "@"}, {"rune", "char", '@', "@char @", nil, "@"},
{"bytes", "byteme", []byte("yo"), "@byteme yo", nil, "yo"}, {"bytes", "byteme", []byte("yo"), "@byteme yo", nil, "yo"},
{"byte", "byteme", byte(67), "@byteme C", nil, "C"}, {"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]`)}, {"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"), ""}, {"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"), ""}, {"whitespace-key", "no space", "hi", "", errors.New("whitespace is not allowed in key: no space"), ""},

View file

@ -2,7 +2,6 @@ package models
import ( import (
"encoding/json" "encoding/json"
"strings"
"time" "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 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 // Implements json.Marshaler
func (ms Metas) MarshalJSON() ([]byte, error) { func (ms Metas) MarshalJSON() ([]byte, error) {
return json.Marshal(ms.Map()) return json.Marshal(ms.Map())