my-log/models/metas.go
Dan Jones c4e864afa5 Add support for nested fields in log entries and JSON marshalling
This commit introduces the ability to handle nested fields within log entries.
The  file has been updated with a  function
that transforms flat keys with a : delimiter (e.g., obj:foo) into nested
JSON objects (e.g., ).

The  file includes new test cases to verify that:
- Nested fields are correctly unmarshalled from string representations.
- Nested fields are correctly marshalled into JSON objects.

This enhancement allows for more structured and organized metadata within log entries.
2026-02-10 16:13:39 -06:00

123 lines
2.6 KiB
Go

package models
import (
"encoding/json"
"strings"
"time"
)
// A slice of Meta
type Metas []Meta
// Returns a single map containing all the Meta. Is useful when encoding to JSON
func (ms Metas) Map() map[string]any {
out := map[string]any{}
for _, f := range ms {
if _, found := out[f.Key]; found || f.Key == "title" || f.Key == "date" {
continue
}
if f.Key == "json" {
ob := map[string]any{}
if j, ok := f.Value.(json.RawMessage); ok {
json.Unmarshal(j, &ob)
}
// If we couldn't get valid data from there, this will just be empty
for k, v := range ob {
if k != "title" && k != "date" {
out[k] = v
}
}
} else {
out[f.Key] = f.Value
if vt, ok := f.Value.(time.Time); ok {
out[f.Key] = vt.Format(time.RFC3339)
}
}
}
// 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())
}
// Implements json.Unmarshaler
func (ms *Metas) UnmarshalJSON(in []byte) error {
old := (*ms).Map()
out := map[string]any{}
err := json.Unmarshal(in, &out)
if err != nil {
return err
}
ret := *ms
for k, v := range out {
if _, found := old[k]; k != "title" && k != "date" && !found {
ret = append(ret, Meta{k, v})
}
}
*ms = ret
return nil
}
// Returns a new Metas with a new Meta appended
func (ms Metas) Append(k string, v any) Metas {
return append(ms, Meta{k, v})
}
// Appends a new Meta to this Metas
func (ms *Metas) AppendTo(k string, v any) {
n := (*ms).Append(k, v)
*ms = n
}
// Returns the value of the Meta with the given key, and a bool indicating if it was found
func (ms Metas) Get(key string) (any, bool) {
_, m, ok := ms.findVal(key)
return m.Value, ok
}
// Set sets the key to the given value.
// If key is already in the ms, Set updates the value of the found Meta.
// If key is not in ms, then we append a new Meta and return the modified Metas.
func (ms Metas) Set(key string, value any) Metas {
idx, m, found := ms.findVal(key)
if !found {
return ms.Append(key, value)
}
m.Value = value
ms[idx] = m
return ms
}
func (ms Metas) findVal(key string) (idx int, m Meta, found bool) {
for idx, m = range ms {
if m.Key == key {
return idx, m, true
}
}
return 0, Meta{}, false
}