🔀 Merge branch 'feat/nested-data' into develop

This commit is contained in:
Dan Jones 2026-02-10 18:23:19 -06:00
commit 04e49a9af4
5 changed files with 160 additions and 6 deletions

View file

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

@ -61,6 +61,42 @@ 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 {
@ -125,6 +161,14 @@ 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`,
@ -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}, {"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,6 +2,7 @@ package models
import ( import (
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"regexp" "regexp"
@ -18,16 +19,63 @@ 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 n == 0 || err != nil { if err != nil {
return []byte{}, err return err
} }
if n == 0 {
return buff.Bytes(), nil return ErrorParsing
}
return nil
} }
func (m *Meta) UnmarshalText(in []byte) error { func (m *Meta) UnmarshalText(in []byte) error {

View file

@ -35,12 +35,11 @@ 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, "", nil, ErrorParsing}, {"nil", "n", nil, "", ErrorParsing, 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,6 +2,7 @@ package models
import ( import (
"encoding/json" "encoding/json"
"strings"
"time" "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 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())