diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e3b58..1dadceb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.0.3] - 2024-03-11 + +- ✨ Add JSON formatter +- 💥 Breaking change: renamed output.stdout.config value formatter to format + ## [0.0.2] - 2024-03-09 - ✨ Use plain formatter to output entry from drop diff --git a/README.md b/README.md index 24eae91..8b4aa0c 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ Each separate output has its own set of configuration. So, replace `which-one` w *This section may change in the near future. We're considering supporting multiple formats.* -- `formatter`: Which formatter to use when outputting data. This value is used by `my-log drop` to output the new entry. +- `format`: Which formatter to use when outputting data. This value is also used by `my-log drop` to output the new entry. ### `[formatters]` @@ -144,7 +144,7 @@ Some formatters may have custom configuration. + [ ] filter to specific logs + [ ] stdout - [x] plain text - - [ ] JSON + - [x] JSON - [ ] YAML - [ ] Other formats? Submit an issue! + [ ] file output diff --git a/cmd/drop.go b/cmd/drop.go index 658cb7c..d74917e 100644 --- a/cmd/drop.go +++ b/cmd/drop.go @@ -42,18 +42,17 @@ var dropCmd = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { log := args[0] title := args[1] - e := models.PartialEntry() + ms := &models.Metas{} if len(j.RawMessage) > 8 { - err := json.Unmarshal([]byte(j.RawMessage), &e) + err := json.Unmarshal([]byte(j.RawMessage), ms) if err != nil { return err } } for k, v := range fields { - e.Fields = append(e.Fields, models.Meta{k, tools.ParseString(v)}) + ms.AppendTo(k, tools.ParseString(v)) } - e.Title = title - e.Date = d.t + e := models.Entry{title, d.t, *ms} l := models.Log{log, []models.Entry{e}} err := files.Append(l) if err != nil { @@ -68,7 +67,10 @@ var dropCmd = &cobra.Command{ if err != nil { return err } - fmt.Fprintf(cmd.OutOrStdout(), "%s\n", out) + if len(out) > 0 && out[len(out)-1] != 10 { + out = append(out, 10) + } + fmt.Fprintf(cmd.OutOrStdout(), "%s", out) return nil }, diff --git a/cmd/root.go b/cmd/root.go index 7e7bbbd..44de5f0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -50,7 +50,7 @@ func init() { // will be global for your application. rootCmd.PersistentFlags().StringVarP(&config.ConfigPath, "config", "c", config.ConfigPath, "config file") - rootCmd.PersistentFlags().StringToStringVarP(&config.Overrides, "config-value", "v", config.Overrides, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.formatter=json") + rootCmd.PersistentFlags().StringToStringVarP(&config.Overrides, "config-value", "v", config.Overrides, "Override config values. Use dot syntax to specify key. E.g. -v output.stdout.config.format=json") // Cobra also supports local flags, which will only run // when this action is called directly. diff --git a/config/default.go b/config/default.go index 76ef8d2..8f93873 100644 --- a/config/default.go +++ b/config/default.go @@ -28,7 +28,7 @@ dotFolder = true enabled = true [output.stdout.config] # Formatter to use when outputting to stdout -formatter = "plain" +format = "plain" [formatters] diff --git a/config/load_test.go b/config/load_test.go index 51e024e..eab7c94 100644 --- a/config/load_test.go +++ b/config/load_test.go @@ -70,14 +70,14 @@ func TestStdoutMissing(t *testing.T) { } func TestStdoutLoad(t *testing.T) { - os.Setenv("LOG_STDOUT_FORMATTER", "json") - defer os.Unsetenv("LOG_STDOUT_FORMATTER") + os.Setenv("LOG_STDOUT_FORMAT", "json") + defer os.Unsetenv("LOG_STDOUT_FORMAT") os.Setenv("LOG_STDOUT_ENABLED", "true") defer os.Unsetenv("LOG_STDOUT_ENABLED") c, _ := Load() std, en := c.Outputs.Stdout() assert.True(t, en) - assert.Equal(t, "json", std.Formatter) + assert.Equal(t, "json", std.Format) } func TestFormatJson(t *testing.T) { diff --git a/config/types.go b/config/types.go index a950a80..58fb2c4 100644 --- a/config/types.go +++ b/config/types.go @@ -21,7 +21,7 @@ type Output struct { } type Stdout struct { - Formatter string `env:"LOG_STDOUT_FORMATTER" mapstructure:"formatter"` + Format string `env:"LOG_STDOUT_FORMAT" mapstructure:"format"` } type stdoutEnabled struct { diff --git a/formatters/interface.go b/formatters/interface.go index 4276aed..5478572 100644 --- a/formatters/interface.go +++ b/formatters/interface.go @@ -4,6 +4,7 @@ import "codeberg.org/danjones000/my-log/models" type Formatter interface { Name() string + Logs([]models.Log) (out []byte, err error) Log(models.Log) (out []byte, err error) Entry(models.Entry) (out []byte, err error) Meta(models.Meta) (out []byte, err error) diff --git a/formatters/json.go b/formatters/json.go new file mode 100644 index 0000000..d56854d --- /dev/null +++ b/formatters/json.go @@ -0,0 +1,77 @@ +package formatters + +import ( + "bytes" + "encoding/json" + "time" + + "codeberg.org/danjones000/my-log/config" + "codeberg.org/danjones000/my-log/models" +) + +func newJson(ff config.Formatters) (Formatter, error) { + return &Json{ff.Json().PrettyPrint}, nil +} + +type Json struct { + prettPrint bool +} + +func (js *Json) Name() string { + return "json" +} + +func (js *Json) marshal(v any) (o []byte, err error) { + o, err = json.Marshal(v) + if err != nil { + return + } + if js.prettPrint { + buff := &bytes.Buffer{} + err = json.Indent(buff, o, "", "\t") + if err == nil { + o = buff.Bytes() + } + } + return +} + +func (js *Json) Meta(m models.Meta) ([]byte, error) { + o := map[string]any{m.Key: m.Value} + return js.marshal(o) +} + +func (js *Json) entryMap(e models.Entry) map[string]any { + o := map[string]any{ + "title": e.Title, + "date": e.Date.Format(time.RFC3339), + } + for _, m := range e.Fields { + o[m.Key] = m.Value + } + return o +} + +func (js *Json) Entry(e models.Entry) ([]byte, error) { + return js.marshal(js.entryMap(e)) +} + +func (js *Json) Log(l models.Log) ([]byte, error) { + return js.Logs([]models.Log{l}) +} + +func (js *Json) Logs(logs []models.Log) (out []byte, err error) { + if len(logs) == 0 { + return + } + + o := map[string][]map[string]any{} + for _, l := range logs { + es := []map[string]any{} + for _, e := range l.Entries { + es = append(es, js.entryMap(e)) + } + o[l.Name] = es + } + return js.marshal(o) +} diff --git a/formatters/json_test.go b/formatters/json_test.go new file mode 100644 index 0000000..da6f111 --- /dev/null +++ b/formatters/json_test.go @@ -0,0 +1,81 @@ +package formatters + +import ( + "fmt" + "testing" + "time" + + "codeberg.org/danjones000/my-log/models" + "github.com/stretchr/testify/assert" +) + +func TestJsonName(t *testing.T) { + f, _ := New("json") + assert.Equal(t, "json", f.Name()) +} + +func TestJsonMeta(t *testing.T) { + f, _ := New("json") + m := models.Meta{"foo", 42} + exp := `{"foo":42}` + o, err := f.Meta(m) + assert.NoError(t, err) + assert.JSONEq(t, exp, string(o)) +} + +func TestJsonEntry(t *testing.T) { + when := time.Now() + f, _ := New("json") + m := models.Meta{"foo", 42} + e := models.Entry{ + Title: "Homer", + Date: when, + Fields: []models.Meta{m}, + } + exp := fmt.Sprintf(`{"title":"%s","date":"%s","foo":42}`, e.Title, when.Format(time.RFC3339)) + o, err := f.Entry(e) + assert.NoError(t, err) + assert.JSONEq(t, exp, string(o)) +} + +func TestJsonLog(t *testing.T) { + when := time.Now() + f, _ := New("json") + m := models.Meta{"foo", 42} + e := models.Entry{ + Title: "Homer", + Date: when, + Fields: []models.Meta{m}, + } + l := models.Log{"stuff", []models.Entry{e}} + exp := fmt.Sprintf(`{"%s":[{"title":"%s","date":"%s","foo":42}]}`, l.Name, e.Title, when.Format(time.RFC3339)) + o, err := f.Log(l) + assert.NoError(t, err) + assert.JSONEq(t, exp, string(o)) +} + +func TestJsonNoLogs(t *testing.T) { + f, _ := New("json") + o, err := f.Logs([]models.Log{}) + var exp []byte + assert.NoError(t, err) + assert.Equal(t, exp, o) +} + +func TestJsonErr(t *testing.T) { + f, _ := New("json") + o, err := f.Meta(models.Meta{"foo", make(chan bool)}) + var exp []byte + assert.Error(t, err) + assert.Equal(t, exp, o) +} + +func TestJsonPretty(t *testing.T) { + f := Json{true} + o, err := f.Meta(models.Meta{"foo", 42}) + exp := `{ + "foo": 42 +}` + assert.NoError(t, err) + assert.Equal(t, exp, string(o)) +} diff --git a/formatters/new.go b/formatters/new.go index d88a9d0..bade49b 100644 --- a/formatters/new.go +++ b/formatters/new.go @@ -10,6 +10,8 @@ type formatMaker func(config.Formatters) (Formatter, error) var formatterMap = map[string]formatMaker{ "plain": newPlain, + "json": newJson, + "zero": newNull, } func Preferred() (f Formatter, err error) { @@ -18,8 +20,7 @@ func Preferred() (f Formatter, err error) { return } std, _ := conf.Outputs.Stdout() - kind := std.Formatter - return New(kind) + return New(std.Format) } func New(kind string) (f Formatter, err error) { diff --git a/formatters/new_test.go b/formatters/new_test.go index 04b40fb..ed56be3 100644 --- a/formatters/new_test.go +++ b/formatters/new_test.go @@ -10,7 +10,7 @@ import ( ) func TestKinds(t *testing.T) { - assert.Equal(t, []string{"plain"}, Kinds()) + assert.ElementsMatch(t, []string{"plain", "json", "zero"}, Kinds()) } func TestNewUnsupported(t *testing.T) { diff --git a/formatters/null.go b/formatters/null.go new file mode 100644 index 0000000..4dd1775 --- /dev/null +++ b/formatters/null.go @@ -0,0 +1,32 @@ +package formatters + +import ( + "codeberg.org/danjones000/my-log/config" + "codeberg.org/danjones000/my-log/models" +) + +func newNull(ff config.Formatters) (Formatter, error) { + return &Null{}, nil +} + +type Null struct{} + +func (n *Null) Name() string { + return "zero" +} + +func (n *Null) Meta(m models.Meta) (o []byte, err error) { + return +} + +func (n *Null) Entry(e models.Entry) (o []byte, err error) { + return +} + +func (n *Null) Log(l models.Log) (o []byte, err error) { + return +} + +func (n *Null) Logs(logs []models.Log) (out []byte, err error) { + return +} diff --git a/formatters/null_test.go b/formatters/null_test.go new file mode 100644 index 0000000..f7c26c1 --- /dev/null +++ b/formatters/null_test.go @@ -0,0 +1,45 @@ +package formatters + +import ( + "testing" + "time" + + "codeberg.org/danjones000/my-log/models" + "github.com/stretchr/testify/assert" +) + +var empty []byte + +func TestNullName(t *testing.T) { + f, err := New("zero") + assert.NoError(t, err) + assert.Equal(t, "zero", f.Name()) +} + +func TestNullMeta(t *testing.T) { + f, _ := New("zero") + o, err := f.Meta(models.Meta{"foo", 42}) + assert.NoError(t, err) + assert.Equal(t, empty, o) +} + +func TestNullEntry(t *testing.T) { + f, _ := New("zero") + o, err := f.Entry(models.Entry{"title", time.Now(), models.Metas{}}) + assert.NoError(t, err) + assert.Equal(t, empty, o) +} + +func TestNullLog(t *testing.T) { + f, _ := New("zero") + o, err := f.Log(models.Log{"jim", []models.Entry{{"title", time.Now(), models.Metas{}}}}) + assert.NoError(t, err) + assert.Equal(t, empty, o) +} + +func TestNullLogs(t *testing.T) { + f, _ := New("zero") + o, err := f.Logs([]models.Log{{"jim", []models.Entry{{"title", time.Now(), models.Metas{}}}}}) + assert.NoError(t, err) + assert.Equal(t, empty, o) +} diff --git a/formatters/plain.go b/formatters/plain.go index ed8ee82..2b79f00 100644 --- a/formatters/plain.go +++ b/formatters/plain.go @@ -20,6 +20,26 @@ func (pt *PlainText) Name() string { return "plain" } +func (pt *PlainText) Logs(logs []models.Log) (out []byte, err error) { + if len(logs) == 0 { + return + } + + buff := &bytes.Buffer{} + first := true + for _, log := range logs { + o, _ := pt.Log(log) + if !first { + buff.WriteByte(10) + buff.WriteByte(10) + } + first = false + buff.Write(o) + } + out = buff.Bytes() + return +} + func (pt *PlainText) Log(log models.Log) (out []byte, err error) { if len(log.Entries) == 0 { return diff --git a/formatters/plain_test.go b/formatters/plain_test.go index e384101..487550f 100644 --- a/formatters/plain_test.go +++ b/formatters/plain_test.go @@ -13,7 +13,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestPlainLog(t *testing.T) { +func TestPlainLogs(t *testing.T) { m := []models.Meta{ {"foo", "bar"}, {"baz", 42}, @@ -23,11 +23,17 @@ func TestPlainLog(t *testing.T) { {Title: "small", Date: time.Now()}, } l := models.Log{"stuff", e} + e2 := models.Entry{ + Title: "three", + Date: time.Now(), + } + l2 := models.Log{"more-stuff", []models.Entry{e2}} + logs := []models.Log{l, l2} f, err := New("plain") require.NoError(t, err) - out, err := f.Log(l) + out, err := f.Logs(logs) require.NoError(t, err) read := bytes.NewReader(out) @@ -67,6 +73,24 @@ func TestPlainLog(t *testing.T) { line = scan.Text() assert.Equal(t, "Date: "+e[1].Date.Format(tools.DateFormat), line) + scan.Scan() + scan.Scan() + line = scan.Text() + assert.Equal(t, l2.Name, line) + + scan.Scan() + line = scan.Text() + assert.Equal(t, "#######", line) + + scan.Scan() + scan.Scan() + line = scan.Text() + assert.Equal(t, "Title: "+e2.Title, line) + + scan.Scan() + line = scan.Text() + assert.Equal(t, "Date: "+e2.Date.Format(tools.DateFormat), line) + more := scan.Scan() assert.False(t, more) } @@ -76,6 +100,13 @@ func TestPlainName(t *testing.T) { assert.Equal(t, "plain", f.Name()) } +func TestPlainLogNone(t *testing.T) { + f, _ := New("plain") + out, err := f.Logs([]models.Log{}) + assert.NoError(t, err) + assert.Len(t, out, 0) +} + func TestPlainLogNoEntries(t *testing.T) { f, _ := New("plain") out, err := f.Log(models.Log{Name: "foo"}) diff --git a/models/entry.go b/models/entry.go index 5a19933..2a11fd2 100644 --- a/models/entry.go +++ b/models/entry.go @@ -16,14 +16,9 @@ import ( const DateFormat = tools.DateFormat type Entry struct { - Title string - Date time.Time - Fields []Meta - skipMissing bool -} - -func PartialEntry() Entry { - return Entry{skipMissing: true} + Title string + Date time.Time + Fields Metas } type metaRes struct { @@ -42,9 +37,9 @@ func (e Entry) getFieldMarshalChan() chan metaRes { defer wg.Done() if m.Key == "json" { if j, ok := m.Value.(json.RawMessage); ok { - sub := Entry{skipMissing: true} + sub := Metas{} json.Unmarshal(j, &sub) - for _, subM := range sub.Fields { + for _, subM := range sub { o, er := subM.MarshalText() ch <- metaRes{o, er} } @@ -160,9 +155,9 @@ func (e *Entry) getFieldUnarshalChan(in []byte) chan Meta { if err == nil { if m.Key == "json" { if j, ok := m.Value.(json.RawMessage); ok { - sub := Entry{skipMissing: true} - json.Unmarshal(j, &sub) - for _, subM := range sub.Fields { + ms := Metas{} + json.Unmarshal(j, &ms) + for _, subM := range ms { ch <- subM } } @@ -191,26 +186,8 @@ func (e Entry) MarshalJSON() ([]byte, error) { out := map[string]any{} out["title"] = e.Title out["date"] = e.Date.Format(time.RFC3339) - for _, f := range e.Fields { - if _, ok := out[f.Key]; !ok { - 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) - } - } - } + for k, v := range e.Fields.Map() { + out[k] = v } return json.Marshal(out) } @@ -259,16 +236,16 @@ func (e *Entry) UnmarshalJSON(in []byte) error { return newParsingError(err) } title, ok := out["title"].(string) - if (!ok || title == "") && !e.skipMissing { + if !ok || title == "" { return ErrorMissingTitle } e.Title = title dates, ok := out["date"].(string) - if (!ok || dates == "") && !e.skipMissing { + if !ok || dates == "" { return ErrorMissingDate } date, err := tools.ParseDate(dates) - if err != nil && !e.skipMissing { + if err != nil { return newParsingError(err) } e.Date = date diff --git a/models/entry_test.go b/models/entry_test.go index aefecc9..01c65a7 100644 --- a/models/entry_test.go +++ b/models/entry_test.go @@ -9,7 +9,6 @@ import ( "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) // Type assertions @@ -18,19 +17,6 @@ var _ encoding.TextUnmarshaler = new(Entry) var _ json.Marshaler = Entry{} var _ json.Unmarshaler = new(Entry) -func TestPartialEntry(t *testing.T) { - e := PartialEntry() - assert.True(t, e.skipMissing) - err := json.Unmarshal([]byte(`{"a":42}`), &e) - assert.NoError(t, err) - assert.Equal(t, "", e.Title) - assert.Equal(t, time.Time{}, e.Date) - require.Len(t, e.Fields, 1) - f := e.Fields[0] - assert.Equal(t, "a", f.Key) - assert.Equal(t, int64(42), f.Value) -} - func TestEntryMarshal(t *testing.T) { when := time.Now() whens := when.Format(DateFormat) @@ -84,7 +70,7 @@ func TestEntryMarshal(t *testing.T) { func getEntryMarshalTestRunner(title string, date time.Time, fields []Meta, first string, lines []string, err error) func(*testing.T) { return func(t *testing.T) { - en := Entry{title, date, fields, false} + en := Entry{title, date, fields} o, er := en.MarshalText() assert.Equal(t, err, er) if first == "" { @@ -232,7 +218,7 @@ func TestEntryJsonMarshal(t *testing.T) { func getEntryJsonMarshalTestRunner(title string, date time.Time, fields []Meta, out string, err error) func(t *testing.T) { return func(t *testing.T) { - e := Entry{title, date, fields, false} + e := Entry{title, date, fields} o, er := json.Marshal(e) if err == nil { assert.JSONEq(t, out, string(o)) diff --git a/models/meta_test.go b/models/meta_test.go index 1e0888b..2d4cea8 100644 --- a/models/meta_test.go +++ b/models/meta_test.go @@ -13,6 +13,8 @@ import ( // Type assertions var _ encoding.TextMarshaler = Meta{} var _ encoding.TextUnmarshaler = new(Meta) +var _ json.Marshaler = Metas{} +var _ json.Unmarshaler = new(Metas) var skipMarshalTest = errors.New("skip marshal") @@ -97,3 +99,45 @@ func getMetaTestRunner(key string, value any, out string, err error, newVal any) } } } + +func TestMetasJson(t *testing.T) { + ms := Metas{{"me", 41}, {"you", false}} + exp := `{"me":41,"you":false}` + o, err := json.Marshal(ms) + assert.NoError(t, err) + assert.JSONEq(t, exp, string(o)) +} + +func TestMetasJsonUnmarshal(t *testing.T) { + ms := Metas{} + in := `{"me":"cool","you":false}` + err := json.Unmarshal([]byte(in), &ms) + assert.NoError(t, err) + assert.Len(t, ms, 2) + assert.ElementsMatch(t, Metas{ + {"me", "cool"}, + {"you", false}, + }, ms) +} + +func TestMetasJsonError(t *testing.T) { + ms := Metas{} + in := "not json" + err := (&ms).UnmarshalJSON([]byte(in)) + assert.Error(t, err) + assert.Len(t, ms, 0) +} + +func TestMetasAppend(t *testing.T) { + ms := Metas{} + ms = ms.Append("foo", 42) + assert.Len(t, ms, 1) + assert.Equal(t, Meta{"foo", 42}, ms[0]) +} + +func TestMetasAppendTo(t *testing.T) { + ms := &Metas{} + ms.AppendTo("foo", 42) + assert.Len(t, *ms, 1) + assert.Equal(t, Meta{"foo", 42}, (*ms)[0]) +} diff --git a/models/metas.go b/models/metas.go new file mode 100644 index 0000000..4db9d46 --- /dev/null +++ b/models/metas.go @@ -0,0 +1,71 @@ +package models + +import ( + "encoding/json" + "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) + } + } + } + return out +} + +// 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 +} diff --git a/tools/parse_date_test.go b/tools/parse_date_test.go index cfaadd1..7aa30b2 100644 --- a/tools/parse_date_test.go +++ b/tools/parse_date_test.go @@ -16,8 +16,8 @@ func TestParseDate(t *testing.T) { y, mon, d, h, loc := now.Year(), now.Month(), now.Day(), now.Hour(), now.Location() sec := now.Truncate(time.Second) today := time.Date(y, mon, d, 0, 0, 0, 0, loc) - tomorrow := today.Add(day) - yesterday := today.Add(-day) + tomorrow := time.Date(y, mon, d+1, 0, 0, 0, 0, loc) + yesterday := time.Date(y, mon, d-1, 0, 0, 0, 0, loc) twoMin := now.Add(2 * time.Minute).Truncate(time.Minute) twoHour := time.Date(y, mon, d, h+2, 0, 0, 0, loc) firstMonth := time.Date(y, mon, 1, 0, 0, 0, 0, loc) diff --git a/tools/write_buffer.go b/tools/write_buffer.go index e67d219..8ae9386 100644 --- a/tools/write_buffer.go +++ b/tools/write_buffer.go @@ -14,6 +14,18 @@ func WriteValue(buff *bytes.Buffer, val any) (n int, err error) { err = fmt.Errorf("Unsupported type %T", v) case nil: return + case []any: + var o []byte + o, err = json.Marshal(v) + if err == nil { + return buff.Write(o) + } + case map[string]any: + var o []byte + o, err = json.Marshal(v) + if err == nil { + return buff.Write(o) + } case string: return buff.WriteString(v) case int: diff --git a/tools/write_buffer_test.go b/tools/write_buffer_test.go index 1a4a103..aa7e203 100644 --- a/tools/write_buffer_test.go +++ b/tools/write_buffer_test.go @@ -30,6 +30,8 @@ func TestWriteBuffer(t *testing.T) { {"json.Number", json.Number("42.13"), "42.13", nil}, {"json.RawMessage", json.RawMessage("{}"), "{}", nil}, {"time", when, when.Format(time.RFC3339), nil}, + {"slice", []any{1, 2, "foo"}, `[1,2,"foo"]`, nil}, + {"map", map[string]any{"baz": 42, "foo": "bar"}, `{"baz":42,"foo":"bar"}`, nil}, {"struct", struct{}{}, "", errors.New("Unsupported type struct {}")}, }