🔀 Merge branch 'feat/nested-data' into develop
This commit is contained in:
commit
04e49a9af4
5 changed files with 160 additions and 6 deletions
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
|
### 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:
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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"), ""},
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue