Repository: segmentio/encoding (go)

Issue:
Proposal: add an option to prevent encoder from adding a newline on Encode
Hi there!

Thanks for this amazing lib that allowed me to get close to 10x perf improvements on a project for both Marshaling & Unmarshaling use-cases.

A small suggestion I'd have following my usage of library would be to add an option to prevent adding a newline at the end of `Encode`.
For manual crafting of a JSON without newlines, this behavior requires to add a `Truncate` call on the byte buffer that is both easy to forget & confusing for readers.

```go
val := "foo"

var buf bytes.Buffer
buf.WriteByte('[')
encoder := json.NewEncoder(&buf)
encoder.Encode(val)
buf.Truncate(buf.Len() - 1) // Remove useless newline
buf.WriteByte(']')

// Outputs:
// ["foo"]

// Without the trucate, outputs:
// ["foo"
// ]
```

https://go.dev/play/p/4Jkoe_A6sk0

Your version of `Encoder` already has a few non-standard options (e.g. `SetTrustRawMessage`), so I believe this could be another one, e.g. `SetNoExtraNewline`.
Is this an addition you'd be open to? I'd be happy to open a PR.

Pull request description:
Encoder: add SetAddExtraNewline option to json.Encoder
Closes #118 

This PR introduces a new `Encoder` option, toggled with `SetAddExtraNewline`.
Its default value is `true`, matching `encoding/json`'s behavior.

Setting it to false allows to use the encoder without having to handle this newline with the output.

There are for instance multiple locations in the tests where newlines are explicitly added to the expected value. Using this option would allow not to have to think about it, making the code a little bit easier to follow.

Patch:
diff --git a/json/json.go b/json/json.go
index 9d259cc..47f3ba1 100644
--- a/json/json.go
+++ b/json/json.go
@@ -70,6 +70,11 @@ const (
 	// checking of raw messages. It should only be used if the values are
 	// known to be valid json (e.g., they were created by json.Unmarshal).
 	TrustRawMessage
+
+	// appendNewline is a formatting flag to enable the addition of a newline
+	// in Encode (this matches the behavior of the standard encoding/json
+	// package).
+	appendNewline
 )
 
 // ParseFlags is a type used to represent configuration options that can be
@@ -480,7 +485,9 @@ type Encoder struct {
 }
 
 // NewEncoder is documented at https://golang.org/pkg/encoding/json/#NewEncoder
-func NewEncoder(w io.Writer) *Encoder { return &Encoder{writer: w, flags: EscapeHTML | SortMapKeys} }
+func NewEncoder(w io.Writer) *Encoder {
+	return &Encoder{writer: w, flags: EscapeHTML | SortMapKeys | appendNewline}
+}
 
 // Encode is documented at https://golang.org/pkg/encoding/json/#Encoder.Encode
 func (enc *Encoder) Encode(v interface{}) error {
@@ -498,7 +505,9 @@ func (enc *Encoder) Encode(v interface{}) error {
 		return err
 	}
 
-	buf.data = append(buf.data, '\n')
+	if (enc.flags & appendNewline) != 0 {
+		buf.data = append(buf.data, '\n')
+	}
 	b := buf.data
 
 	if enc.prefix != "" || enc.indent != "" {
@@ -556,6 +565,16 @@ func (enc *Encoder) SetTrustRawMessage(on bool) {
 	}
 }
 
+// SetAppendNewline is an extension to the standard encoding/json package which
+// allows the program to toggle the addition of a newline in Encode on or off.
+func (enc *Encoder) SetAppendNewline(on bool) {
+	if on {
+		enc.flags |= appendNewline
+	} else {
+		enc.flags &= ^appendNewline
+	}
+}
+
 var encoderBufferPool = sync.Pool{
 	New: func() interface{} { return &encoderBuffer{data: make([]byte, 0, 4096)} },
 }


Test patch:
diff --git a/json/json_test.go b/json/json_test.go
index 63ec861..6840b85 100644
--- a/json/json_test.go
+++ b/json/json_test.go
@@ -1669,6 +1669,44 @@ func TestSetTrustRawMessage(t *testing.T) {
 	}
 }
 
+func TestSetAppendNewline(t *testing.T) {
+	buf := &bytes.Buffer{}
+	enc := NewEncoder(buf)
+
+	m := "value"
+
+	// Default encoding adds an extra newline
+	if err := enc.Encode(m); err != nil {
+		t.Error(err)
+	}
+	b := buf.Bytes()
+	exp := []byte(`"value"`)
+	exp = append(exp, '\n')
+	if bytes.Compare(exp, b) != 0 {
+		t.Error(
+			"unexpected encoding:",
+			"expected", exp,
+			"got", b,
+		)
+	}
+
+	// With SetAppendNewline(false), there shouldn't be a newline in the output
+	buf.Reset()
+	enc.SetAppendNewline(false)
+	if err := enc.Encode(m); err != nil {
+		t.Error(err)
+	}
+	b = buf.Bytes()
+	exp = []byte(`"value"`)
+	if bytes.Compare(exp, b) != 0 {
+		t.Error(
+			"unexpected encoding:",
+			"expected", exp,
+			"got", b,
+		)
+	}
+}
+
 func TestEscapeString(t *testing.T) {
 	b := Escape(`value`)
 	x := []byte(`"value"`)