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..37d8a96 100644 --- a/formatters/new.go +++ b/formatters/new.go @@ -10,6 +10,7 @@ type formatMaker func(config.Formatters) (Formatter, error) var formatterMap = map[string]formatMaker{ "plain": newPlain, + "json": newJson, } func Preferred() (f Formatter, err error) { diff --git a/formatters/new_test.go b/formatters/new_test.go index 04b40fb..c6d5e56 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"}, Kinds()) } func TestNewUnsupported(t *testing.T) { 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"})