Compare commits
9 commits
26da173dc7
...
44ecfd6ac9
| Author | SHA1 | Date | |
|---|---|---|---|
| 44ecfd6ac9 | |||
| 04e49a9af4 | |||
| 4fc1c623a0 | |||
| c4e864afa5 | |||
| 91c5b3ad15 | |||
| fbcec20221 | |||
| 136546d655 | |||
| 3b36055b1b | |||
| 21534bb9c9 |
10 changed files with 196 additions and 18 deletions
|
|
@ -1,5 +1,13 @@
|
|||
# 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
|
||||
|
|
|
|||
36
README.md
36
README.md
|
|
@ -73,6 +73,42 @@ 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:
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ func (s *AppendTestSuite) TestTwoEntries() {
|
|||
s.Assert().NoError(err)
|
||||
s.Require().FileExists(s.dir + "/test.log")
|
||||
by, _ := os.ReadFile(s.dir + "/test.log")
|
||||
exp := fmt.Sprintf("@begin %s - one @end\n@begin %s - two @end\n", whens, whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
st := string(by)
|
||||
s.Assert().Contains(st, fmt.Sprintf("@begin %s - one", whens))
|
||||
s.Assert().Contains(st, fmt.Sprintf("@begin %s - two", whens))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestAddNewLine() {
|
||||
|
|
@ -84,7 +85,7 @@ func (s *AppendTestSuite) TestAddNewLine() {
|
|||
when := time.Now().Local()
|
||||
whens := when.Format(models.DateFormat)
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: when},
|
||||
{Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}},
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
|
|
@ -94,7 +95,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 @end\n", whens)
|
||||
exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
}
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ func (s *AppendTestSuite) TestDontAddNewLine() {
|
|||
when := time.Now().Local()
|
||||
whens := when.Format(models.DateFormat)
|
||||
e := []models.Entry{
|
||||
{Title: "one", Date: when},
|
||||
{Title: "one", Date: when, Fields: models.Metas{{"id", "jimmy"}}},
|
||||
}
|
||||
l := models.Log{
|
||||
Name: "test",
|
||||
|
|
@ -114,7 +115,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 @end\n", whens)
|
||||
exp := fmt.Sprintf("foo\n@begin %s - one\n@id jimmy @end\n", whens)
|
||||
s.Assert().Equal(exp, string(by))
|
||||
}
|
||||
|
||||
|
|
@ -150,7 +151,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, "something @end")
|
||||
s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestDotFolderNo() {
|
||||
|
|
@ -169,7 +170,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, "another @end")
|
||||
s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestNoExt() {
|
||||
|
|
@ -191,7 +192,7 @@ func (s *AppendTestSuite) TestNoExt() {
|
|||
by, err := os.ReadFile(s.dir + "/foobar")
|
||||
st := string(by)
|
||||
s.Require().NoError(err)
|
||||
s.Assert().Contains(st, "baz @end")
|
||||
s.Assert().Contains(st, fmt.Sprintf("@begin %s - %s", e.Date.Format(models.DateFormat), e.Title))
|
||||
}
|
||||
|
||||
func (s *AppendTestSuite) TestConfLoadErr() {
|
||||
|
|
|
|||
1
go.mod
1
go.mod
|
|
@ -5,6 +5,7 @@ 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
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -8,6 +8,8 @@ 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=
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ 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
|
||||
|
|
@ -64,9 +67,18 @@ func (e Entry) MarshalText() ([]byte, error) {
|
|||
if e.Title == "" {
|
||||
return []byte{}, ErrorMissingTitle
|
||||
}
|
||||
if e.Date == (time.Time{}) {
|
||||
if e.Date.IsZero() {
|
||||
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 ")
|
||||
|
|
|
|||
|
|
@ -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 @end", nolines, nil},
|
||||
{"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 @end"},
|
||||
[]string{"@age 41", "@id .*"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
|
|
@ -61,6 +61,42 @@ 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 {
|
||||
|
|
@ -125,6 +161,14 @@ 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`,
|
||||
|
|
@ -209,6 +253,9 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
|
@ -18,16 +19,63 @@ 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 n == 0 || err != nil {
|
||||
return []byte{}, err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return buff.Bytes(), nil
|
||||
if n == 0 {
|
||||
return ErrorParsing
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Meta) UnmarshalText(in []byte) error {
|
||||
|
|
|
|||
|
|
@ -35,12 +35,11 @@ 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, "", nil, ErrorParsing},
|
||||
{"nil", "n", nil, "", ErrorParsing, 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"), ""},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package models
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -33,9 +34,32 @@ 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())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue