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

Single line pretty-printing options #20

Merged
merged 7 commits into from
Apr 11, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions arshal_any.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@ func marshalObjectAny(enc *jsontext.Encoder, obj map[string]any, mo *jsonopts.St
if mo.Flags.Get(jsonflags.FormatNilMapAsNull) && obj == nil {
return enc.WriteToken(jsontext.Null)
}
// Optimize for marshaling an empty map without any preceding whitespace.
// Optimize for marshaling an empty map without any preceding multiline whitespace.
if !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{'), "{}"...)
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), "{}"...)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -211,9 +211,9 @@ func marshalArrayAny(enc *jsontext.Encoder, arr []any, mo *jsonopts.Struct) erro
if mo.Flags.Get(jsonflags.FormatNilSliceAsNull) && arr == nil {
return enc.WriteToken(jsontext.Null)
}
// Optimize for marshaling an empty slice without any preceding whitespace.
// Optimize for marshaling an empty slice without any preceding multiline whitespace.
if !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '['), "[]"...)
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '[', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), "[]"...)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down
34 changes: 19 additions & 15 deletions arshal_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ func makeBoolArshaler(t reflect.Type) *arshaler {
return newInvalidFormatError("marshal", t, mo.Format)
}

// Optimize for marshaling without preceding whitespace.
// Optimize for marshaling without preceding multiline whitespace.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = strconv.AppendBool(xe.Tokens.MayAppendDelim(xe.Buf, 't'), va.Bool())
xe.Buf = strconv.AppendBool(xe.Tokens.MayAppendDelim(xe.Buf, 't', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), va.Bool())
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -168,11 +168,11 @@ func makeStringArshaler(t reflect.Type) *arshaler {
return newInvalidFormatError("marshal", t, mo.Format)
}

// Optimize for marshaling without preceding whitespace or string escaping.
// Optimize for marshaling without preceding multiline whitespace or string escaping.
s := va.String()
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() && !jsonwire.NeedEscape(s) {
b := xe.Buf
b = xe.Tokens.MayAppendDelim(b, '"')
b = xe.Tokens.MayAppendDelim(b, '"', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma))
b = append(b, '"')
b = append(b, s...)
b = append(b, '"')
Expand Down Expand Up @@ -372,9 +372,9 @@ func makeIntArshaler(t reflect.Type) *arshaler {
return newInvalidFormatError("marshal", t, mo.Format)
}

// Optimize for marshaling without preceding whitespace or string escaping.
// Optimize for marshaling without preceding multiline whitespace or string escaping.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = strconv.AppendInt(xe.Tokens.MayAppendDelim(xe.Buf, '0'), va.Int(), 10)
xe.Buf = strconv.AppendInt(xe.Tokens.MayAppendDelim(xe.Buf, '0', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), va.Int(), 10)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -449,9 +449,9 @@ func makeUintArshaler(t reflect.Type) *arshaler {
return newInvalidFormatError("marshal", t, mo.Format)
}

// Optimize for marshaling without preceding whitespace or string escaping.
// Optimize for marshaling without preceding multiline whitespace or string escaping.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = strconv.AppendUint(xe.Tokens.MayAppendDelim(xe.Buf, '0'), va.Uint(), 10)
xe.Buf = strconv.AppendUint(xe.Tokens.MayAppendDelim(xe.Buf, '0', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), va.Uint(), 10)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -531,9 +531,9 @@ func makeFloatArshaler(t reflect.Type) *arshaler {
return enc.WriteToken(jsontext.Float(fv))
}

// Optimize for marshaling without preceding whitespace or string escaping.
// Optimize for marshaling without preceding multiline whitespace or string escaping.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = jsonwire.AppendFloat(xe.Tokens.MayAppendDelim(xe.Buf, '0'), fv, bits)
xe.Buf = jsonwire.AppendFloat(xe.Tokens.MayAppendDelim(xe.Buf, '0', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), fv, bits)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -650,9 +650,9 @@ func makeMapArshaler(t reflect.Type) *arshaler {
if emitNull && va.IsNil() {
return enc.WriteToken(jsontext.Null)
}
// Optimize for marshaling an empty map without any preceding whitespace.
// Optimize for marshaling an empty map without any preceding multiline whitespace.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{'), "{}"...)
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), "{}"...)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down Expand Up @@ -975,7 +975,11 @@ func makeStructArshaler(t reflect.Type) *arshaler {
// Append any delimiters or optional whitespace.
b := xe.Buf
if xe.Tokens.Last.Length() > 0 {
b = append(b, ',')
if xe.Flags.Get(jsonflags.SpaceAfterComma) {
b = append(b, ',', ' ')
} else {
b = append(b, ',')
}
}
if xe.Flags.Get(jsonflags.Expand) {
b = xe.AppendIndent(b, xe.Tokens.NeedIndent('"'))
Expand Down Expand Up @@ -1251,9 +1255,9 @@ func makeSliceArshaler(t reflect.Type) *arshaler {
if emitNull && va.IsNil() {
return enc.WriteToken(jsontext.Null)
}
// Optimize for marshaling an empty slice without any preceding whitespace.
// Optimize for marshaling an empty slice without any preceding multiline whitespace.
if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() {
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '['), "[]"...)
xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '[', xe.Flags.Get(jsonflags.SpaceAfterColon), xe.Flags.Get(jsonflags.SpaceAfterComma)), "[]"...)
xe.Tokens.Last.Increment()
if xe.NeedFlush() {
return xe.Flush()
Expand Down
85 changes: 85 additions & 0 deletions arshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,91 @@ func TestMarshal(t *testing.T) {
},
"Interface": null
}`,
}, {
name: jsontest.Name("Structs/SpaceAfterColonAndComma"),
opts: []Options{
jsontext.SpaceAfterColon(true),
jsontext.SpaceAfterComma(true),
},
in: structAll{
Bool: true,
String: "hello",
Bytes: []byte{1, 2, 3},
Int: -64,
Uint: +64,
Float: 3.14159,
Map: map[string]string{"key": "value"},
StructScalars: structScalars{
Bool: true,
String: "hello",
Bytes: []byte{1, 2, 3},
Int: -64,
Uint: +64,
Float: 3.14159,
},
StructMaps: structMaps{
MapBool: map[string]bool{"": true},
MapString: map[string]string{"": "hello"},
MapBytes: map[string][]byte{"": {1, 2, 3}},
MapInt: map[string]int64{"": -64},
MapUint: map[string]uint64{"": +64},
MapFloat: map[string]float64{"": 3.14159},
},
StructSlices: structSlices{
SliceBool: []bool{true},
SliceString: []string{"hello"},
SliceBytes: [][]byte{{1, 2, 3}},
SliceInt: []int64{-64},
SliceUint: []uint64{+64},
SliceFloat: []float64{3.14159},
},
Slice: []string{"fizz", "buzz"},
Array: [1]string{"goodbye"},
Pointer: new(structAll),
Interface: (*structAll)(nil),
},
want: `{"Bool": true, "String": "hello", "Bytes": "AQID", "Int": -64, "Uint": 64, "Float": 3.14159, "Map": {"key": "value"}, "StructScalars": {"Bool": true, "String": "hello", "Bytes": "AQID", "Int": -64, "Uint": 64, "Float": 3.14159}, "StructMaps": {"MapBool": {"": true}, "MapString": {"": "hello"}, "MapBytes": {"": "AQID"}, "MapInt": {"": -64}, "MapUint": {"": 64}, "MapFloat": {"": 3.14159}}, "StructSlices": {"SliceBool": [true], "SliceString": ["hello"], "SliceBytes": ["AQID"], "SliceInt": [-64], "SliceUint": [64], "SliceFloat": [3.14159]}, "Slice": ["fizz", "buzz"], "Array": ["goodbye"], "Pointer": {"Bool": false, "String": "", "Bytes": "", "Int": 0, "Uint": 0, "Float": 0, "Map": {}, "StructScalars": {"Bool": false, "String": "", "Bytes": "", "Int": 0, "Uint": 0, "Float": 0}, "StructMaps": {"MapBool": {}, "MapString": {}, "MapBytes": {}, "MapInt": {}, "MapUint": {}, "MapFloat": {}}, "StructSlices": {"SliceBool": [], "SliceString": [], "SliceBytes": [], "SliceInt": [], "SliceUint": [], "SliceFloat": []}, "Slice": [], "Array": [""], "Pointer": null, "Interface": null}, "Interface": null}`,
}, {
name: jsontest.Name("Structs/SpaceAfterComma"),
opts: []Options{jsontext.SpaceAfterComma(true)},
in: structAll{
Bool: true,
String: "hello",
Bytes: []byte{1, 2, 3},
Int: -64,
Uint: +64,
Float: 3.14159,
Map: map[string]string{"key": "value"},
StructScalars: structScalars{
Bool: true,
String: "hello",
Bytes: []byte{1, 2, 3},
Int: -64,
Uint: +64,
Float: 3.14159,
},
StructMaps: structMaps{
MapBool: map[string]bool{"": true},
MapString: map[string]string{"": "hello"},
MapBytes: map[string][]byte{"": {1, 2, 3}},
MapInt: map[string]int64{"": -64},
MapUint: map[string]uint64{"": +64},
MapFloat: map[string]float64{"": 3.14159},
},
StructSlices: structSlices{
SliceBool: []bool{true},
SliceString: []string{"hello"},
SliceBytes: [][]byte{{1, 2, 3}},
SliceInt: []int64{-64},
SliceUint: []uint64{+64},
SliceFloat: []float64{3.14159},
},
Slice: []string{"fizz", "buzz"},
Array: [1]string{"goodbye"},
Pointer: new(structAll),
Interface: (*structAll)(nil),
},
want: `{"Bool":true, "String":"hello", "Bytes":"AQID", "Int":-64, "Uint":64, "Float":3.14159, "Map":{"key":"value"}, "StructScalars":{"Bool":true, "String":"hello", "Bytes":"AQID", "Int":-64, "Uint":64, "Float":3.14159}, "StructMaps":{"MapBool":{"":true}, "MapString":{"":"hello"}, "MapBytes":{"":"AQID"}, "MapInt":{"":-64}, "MapUint":{"":64}, "MapFloat":{"":3.14159}}, "StructSlices":{"SliceBool":[true], "SliceString":["hello"], "SliceBytes":["AQID"], "SliceInt":[-64], "SliceUint":[64], "SliceFloat":[3.14159]}, "Slice":["fizz", "buzz"], "Array":["goodbye"], "Pointer":{"Bool":false, "String":"", "Bytes":"", "Int":0, "Uint":0, "Float":0, "Map":{}, "StructScalars":{"Bool":false, "String":"", "Bytes":"", "Int":0, "Uint":0, "Float":0}, "StructMaps":{"MapBool":{}, "MapString":{}, "MapBytes":{}, "MapInt":{}, "MapUint":{}, "MapFloat":{}}, "StructSlices":{"SliceBool":[], "SliceString":[], "SliceBytes":[], "SliceInt":[], "SliceUint":[], "SliceFloat":[]}, "Slice":[], "Array":[""], "Pointer":null, "Interface":null}, "Interface":null}`,
}, {
name: jsontest.Name("Structs/Stringified"),
opts: []Options{jsontext.Expand(true)},
Expand Down
2 changes: 2 additions & 0 deletions internal/jsonflags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ const (
EscapeForHTML // encode only
EscapeForJS // encode only
Expand // encode only
SpaceAfterColon // encode only
SpaceAfterComma // encode only
Indent // encode only; non-boolean flag
IndentPrefix // encode only; non-boolean flag
ByteLimit // encode or decode; non-boolean flag
Expand Down
10 changes: 10 additions & 0 deletions internal/jsonopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,22 @@ func (dst *Struct) Join(srcs ...Options) {
case nil:
continue
case jsonflags.Bools:
switch src {
case jsonflags.Expand:
dst.Flags.Clear(jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon)
case jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon:
if dst.Flags.Has(jsonflags.Expand) {
continue
}
}
dst.Flags.Set(src)
case Indent:
dst.Flags.Set(jsonflags.Expand | jsonflags.Indent | 1)
dst.Flags.Clear(jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon)
dst.Indent = string(src)
case IndentPrefix:
dst.Flags.Set(jsonflags.Expand | jsonflags.IndentPrefix | 1)
dst.Flags.Clear(jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon)
dst.IndentPrefix = string(src)
case ByteLimit:
dst.Flags.Set(jsonflags.ByteLimit | 1)
Expand Down
25 changes: 19 additions & 6 deletions jsontext/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ func (e *encoderState) UnwriteEmptyObjectMember(prevName *string) bool {
b = b[:len(b)-n]
b = jsonwire.TrimSuffixWhitespace(b)
b = jsonwire.TrimSuffixByte(b, ':')
b = jsonwire.TrimSuffixWhitespace(b)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is needed, but am not really sure about this section of code or why it worked previously without it even on multiline/expanded json.

b = jsonwire.TrimSuffixString(b)
b = jsonwire.TrimSuffixWhitespace(b)
b = jsonwire.TrimSuffixByte(b, ',')
Expand Down Expand Up @@ -336,7 +337,7 @@ func (e *encoderState) WriteToken(t Token) error {
b := e.Buf // use local variable to avoid mutating e in case of error

// Append any delimiters or optional whitespace.
b = e.Tokens.MayAppendDelim(b, k)
b = e.Tokens.MayAppendDelim(b, k, e.Flags.Get(jsonflags.SpaceAfterColon), e.Flags.Get(jsonflags.SpaceAfterComma))
if e.Flags.Get(jsonflags.Expand) {
b = e.appendWhitespace(b, k)
}
Expand Down Expand Up @@ -427,7 +428,7 @@ func (e *encoderState) AppendRaw(k Kind, safeASCII bool, appendFn func([]byte) (
b := e.Buf // use local variable to avoid mutating e in case of error

// Append any delimiters or optional whitespace.
b = e.Tokens.MayAppendDelim(b, k)
b = e.Tokens.MayAppendDelim(b, k, e.Flags.Get(jsonflags.SpaceAfterColon), e.Flags.Get(jsonflags.SpaceAfterComma))
if e.Flags.Get(jsonflags.Expand) {
b = e.appendWhitespace(b, k)
}
Expand Down Expand Up @@ -512,7 +513,7 @@ func (e *encoderState) WriteValue(v Value) error {
b := e.Buf // use local variable to avoid mutating e in case of error

// Append any delimiters or optional whitespace.
b = e.Tokens.MayAppendDelim(b, k)
b = e.Tokens.MayAppendDelim(b, k, e.Flags.Get(jsonflags.SpaceAfterColon), e.Flags.Get(jsonflags.SpaceAfterComma))
if e.Flags.Get(jsonflags.Expand) {
b = e.appendWhitespace(b, k)
}
Expand Down Expand Up @@ -715,7 +716,11 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte,
if src[n] != ':' {
return dst, n, newInvalidCharacterError(src[n:], "after object name (expecting ':')")
}
dst = append(dst, ':')
if e.Flags.Get(jsonflags.SpaceAfterColon) {
dst = append(dst, ':', ' ')
} else {
dst = append(dst, ':')
}
n += len(":")
if e.Flags.Get(jsonflags.Expand) {
dst = append(dst, ' ')
Expand All @@ -739,7 +744,11 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte,
}
switch src[n] {
case ',':
dst = append(dst, ',')
if e.Flags.Get(jsonflags.SpaceAfterComma) {
dst = append(dst, ',', ' ')
} else {
dst = append(dst, ',')
}
n += len(",")
continue
case '}':
Expand Down Expand Up @@ -806,7 +815,11 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte,
}
switch src[n] {
case ',':
dst = append(dst, ',')
if e.Flags.Get(jsonflags.SpaceAfterComma) {
dst = append(dst, ',', ' ')
} else {
dst = append(dst, ',')
}
n += len(",")
continue
case ']':
Expand Down
32 changes: 32 additions & 0 deletions jsontext/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,38 @@ func Expand(v bool) Options {
}
}

// SpaceAfterColon specifies that the JSON output should emit single-line output
// where each key has a space after the colon.
//
// If set to false, then the output is compact with no white space after the key and colon.
//
// This option is overriden by Expand, WithIndent, and WithIndentPrefix.
//
// This only affects encoding and is ignored when decoding.
func SpaceAfterColon(v bool) Options {
if v {
return jsonflags.SpaceAfterColon | 1
} else {
return jsonflags.SpaceAfterColon | 0
}
}

// SpaceAfterComma specifies that the JSON output should emit single-line output
// where each non-final element has a space after the comma.
//
// If set to false, then the output is compact with no white space after the element and comma.
//
// This option is overriden by Expand, WithIndent, and WithIndentPrefix.
//
// This only affects encoding and is ignored when decoding.
func SpaceAfterComma(v bool) Options {
if v {
return jsonflags.SpaceAfterComma | 1
} else {
return jsonflags.SpaceAfterComma | 0
}
}

// WithIndent specifies that the encoder should emit multiline output
// where each element in a JSON object or array begins on a new, indented line
// beginning with the indent prefix (see [WithIndentPrefix])
Expand Down
8 changes: 7 additions & 1 deletion jsontext/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,17 @@ func (m stateMachine) NeedIndent(next Kind) (n int) {
}

// MayAppendDelim appends a colon or comma that may precede the next token.
func (m stateMachine) MayAppendDelim(b []byte, next Kind) []byte {
func (m stateMachine) MayAppendDelim(b []byte, next Kind, spaceAfterColon, spaceAfterComma bool) []byte {
switch {
case m.Last.needImplicitColon():
if spaceAfterColon {
return append(b, ':', ' ')
}
return append(b, ':')
case m.Last.needImplicitComma(next) && len(m.Stack) != 0: // comma not needed for top-level values
if spaceAfterComma {
return append(b, ',', ' ')
}
return append(b, ',')
default:
return b
Expand Down
2 changes: 2 additions & 0 deletions jsontext/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ func (v *Value) reformat(canonical, multiline bool, prefix, indent string) error
e := getBufferedEncoder()
defer putBufferedEncoder(e)
eo := &e.s.Struct
eo.Flags.Set(jsonflags.SpaceAfterColon | 0)
eo.Flags.Set(jsonflags.SpaceAfterComma | 0)
if canonical {
eo.Flags.Set(jsonflags.AllowInvalidUTF8 | 0) // per RFC 8785, section 3.2.4
eo.Flags.Set(jsonflags.AllowDuplicateNames | 0) // per RFC 8785, section 3.1
Expand Down