Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support jsonl encoder option #44

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 43 additions & 7 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"syscall"
"unicode"

runewidth "github.com/mattn/go-runewidth"
"github.com/mattn/go-runewidth"
)

// TableEncoder is a buffered, lookahead table encoder for result sets.
Expand Down Expand Up @@ -825,6 +825,8 @@ type JSONEncoder struct {
lowerColumnNames bool
// useColumnTypes indicates using the result's column types.
useColumnTypes bool
// jsonl indicates whether json-line format
jsonl bool
}

// NewJSONEncoder creates a new JSON encoder using the provided options.
Expand All @@ -838,6 +840,28 @@ func NewJSONEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) {
Tabs: make([][][2]int, 1),
Raw: true,
},
jsonl: false,
}
for _, o := range opts {
if err := o.apply(enc); err != nil {
return nil, err
}
}
return enc, nil
}

// NewJSONLEncoder creates a new JSON encoder using the provided options.
func NewJSONLEncoder(resultSet ResultSet, opts ...Option) (Encoder, error) {
enc := &JSONEncoder{
resultSet: resultSet,
newline: newline,
formatter: NewEscapeFormatter(WithIsJSONL(true)),
empty: &Value{
Buf: []byte("null"),
Tabs: make([][][2]int, 0),
Raw: true,
},
jsonl: true,
}
for _, o := range opts {
if err := o.apply(enc); err != nil {
Expand All @@ -862,6 +886,11 @@ func (enc *JSONEncoder) Encode(w io.Writer) error {
q = []byte{'"'}
cma = []byte{','}
)
sep := cma
if enc.jsonl {
sep = enc.newline
}

// get and check columns
clen, cols, err := buildColNames(enc.resultSet, enc.lowerColumnNames)
switch {
Expand All @@ -883,16 +912,19 @@ func (enc *JSONEncoder) Encode(w io.Writer) error {
return err
}
// start
if _, err = w.Write(start); err != nil {
return err
if !enc.jsonl {
if _, err = w.Write(start); err != nil {
return err
}
}

// process
var v *Value
var vals []*Value
var count int64
for enc.resultSet.Next() {
if count != 0 {
if _, err = w.Write(cma); err != nil {
if _, err = w.Write(sep); err != nil {
return err
}
}
Expand Down Expand Up @@ -943,7 +975,9 @@ func (enc *JSONEncoder) Encode(w io.Writer) error {
return err
}
// end
_, err = w.Write(end)
if !enc.jsonl {
_, err = w.Write(end)
}
return err
}

Expand All @@ -953,8 +987,10 @@ func (enc *JSONEncoder) EncodeAll(w io.Writer) error {
return err
}
for enc.resultSet.NextResultSet() {
if _, err := w.Write([]byte{','}); err != nil {
return err
if !enc.jsonl {
if _, err := w.Write([]byte{','}); err != nil {
return err
}
}
if _, err := w.Write(enc.newline); err != nil {
return err
Expand Down
12 changes: 11 additions & 1 deletion fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"unicode"
"unicode/utf8"

runewidth "github.com/mattn/go-runewidth"
"github.com/mattn/go-runewidth"
"golang.org/x/text/language"
"golang.org/x/text/message"
"golang.org/x/text/number"
Expand Down Expand Up @@ -522,6 +522,16 @@ func WithIsJSON(isJSON bool) EscapeFormatterOption {
}
}

// WithIsJSONL is an escape formatter option to enable special escaping for JSONL
// characters in non-complex values.
func WithIsJSONL(isJSONL bool) EscapeFormatterOption {
return func(f *EscapeFormatter) {
f.isJSON = isJSONL
f.prefix = ""
f.indent = ""
}
}

// WithJSONConfig is an escape formatter option to set the JSON encoding
// prefix, indent value, and whether or not to escape HTML. Passed to the
// standard encoding/json.Encoder when a marshaler has not been set on the
Expand Down
7 changes: 7 additions & 0 deletions opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/nathan-fiscaletti/consolesize-go"

"github.com/xo/tblfmt/templates"
)

Expand Down Expand Up @@ -99,6 +100,12 @@ func FromMap(opts map[string]string) (Builder, []Option) {
WithUseColumnTypes(opts["use_column_types"] == "true"),
WithFormatterOptions(WithTimeFormat(timeFormat)),
}
case "jsonl":
return NewJSONLEncoder, []Option{
WithLowerColumnNames(opts["lower_column_names"] == "true"),
WithUseColumnTypes(opts["use_column_types"] == "true"),
WithFormatterOptions(WithTimeFormat(timeFormat)),
}
case "csv", "unaligned":
// determine separator, quote
sep, quote, field := '|', rune(0), "fieldsep"
Expand Down
20 changes: 20 additions & 0 deletions tblfmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ func EncodeJSONAll(w io.Writer, resultSet ResultSet, opts ...Option) error {
return enc.EncodeAll(w)
}

// EncodeJSONL encodes the result set to the writer as JSON-line using the supplied
// encoding options.
func EncodeJSONL(w io.Writer, resultSet ResultSet, opts ...Option) error {
enc, err := NewJSONLEncoder(resultSet, opts...)
if err != nil {
return err
}
return enc.Encode(w)
}

// EncodeJSONLAll encodes all result sets to the writer as JSON-line using the
// supplied encoding options.
func EncodeJSONLAll(w io.Writer, resultSet ResultSet, opts ...Option) error {
enc, err := NewJSONLEncoder(resultSet, opts...)
if err != nil {
return err
}
return enc.EncodeAll(w)
}

// EncodeUnaligned encodes the result set to the writer unaligned using the
// supplied encoding options.
func EncodeUnaligned(w io.Writer, resultSet ResultSet, opts ...Option) error {
Expand Down
29 changes: 29 additions & 0 deletions tblfmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,35 @@ func TestEncodeJSONAll(t *testing.T) {
}
}

func TestEncodeJSONLAll(t *testing.T) {
t.Parallel()
exp := `{"author_id":14,"name":"a\tb\tc\td","z":"x"}
{"author_id":15,"name":"aoeu\ntest\n","z":null}
{"author_id":16,"name":"foo\bbar","z":null}
{"author_id":17,"name":"袈\t袈\t\t袈","z":null}
{"author_id":18,"name":"a\tb\t\r\n\ta","z":"a\n"}
{"author_id":19,"name":"袈\t袈\t\t袈\n","z":null}
{"author_id":20,"name":"javascript","z":{"test21":"a value","test22":"foo\u0008bar"}}
{"author_id":23,"name":"slice","z":["a","b"]}
{"author_id":38,"name":"a\tb\tc\td","z":"x"}
{"author_id":39,"name":"aoeu\ntest\n","z":null}
{"author_id":40,"name":"foo\bbar","z":null}
{"author_id":41,"name":"袈\t袈\t\t袈","z":null}
{"author_id":42,"name":"a\tb\t\r\n\ta","z":"a\n"}
{"author_id":43,"name":"袈\t袈\t\t袈\n","z":null}
{"author_id":44,"name":"javascript","z":{"test45":"a value","test46":"foo\u0008bar"}}
{"author_id":47,"name":"slice","z":["a","b"]}
`
buf := new(bytes.Buffer)
if err := EncodeJSONLAll(buf, internal.NewRsetMulti()); err != nil {
t.Fatalf("expected no error, got: %v", err)
}
actual := buf.String()
if actual != exp {
t.Errorf("expected:\n%q\n---\ngot:\n%q", exp, actual)
}
}

func TestEncodeTemplateAll(t *testing.T) {
t.Parallel()
exp := `Row 0:
Expand Down