diff --git a/arshal_any.go b/arshal_any.go index 334ce94..317d572 100644 --- a/arshal_any.go +++ b/arshal_any.go @@ -99,7 +99,7 @@ func marshalObjectAny(enc *jsontext.Encoder, obj map[string]any, mo *jsonopts.St return enc.WriteToken(jsontext.Null) } // Optimize for marshaling an empty map without any preceding whitespace. - if !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() { + if !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{'), "{}"...) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -212,7 +212,7 @@ func marshalArrayAny(enc *jsontext.Encoder, arr []any, mo *jsonopts.Struct) erro return enc.WriteToken(jsontext.Null) } // Optimize for marshaling an empty slice without any preceding whitespace. - if !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() { + if !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '['), "[]"...) xe.Tokens.Last.Increment() if xe.NeedFlush() { diff --git a/arshal_default.go b/arshal_default.go index b353bc5..4bbcd05 100644 --- a/arshal_default.go +++ b/arshal_default.go @@ -126,7 +126,7 @@ func makeBoolArshaler(t reflect.Type) *arshaler { } // Optimize for marshaling without preceding whitespace. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = strconv.AppendBool(xe.Tokens.MayAppendDelim(xe.Buf, 't'), va.Bool()) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -170,7 +170,7 @@ func makeStringArshaler(t reflect.Type) *arshaler { // Optimize for marshaling without preceding whitespace or string escaping. s := va.String() - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() && !jsonwire.NeedEscape(s) { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() && !jsonwire.NeedEscape(s) { b := xe.Buf b = xe.Tokens.MayAppendDelim(b, '"') b = append(b, '"') @@ -373,7 +373,7 @@ func makeIntArshaler(t reflect.Type) *arshaler { } // Optimize for marshaling without preceding whitespace or string escaping. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = strconv.AppendInt(xe.Tokens.MayAppendDelim(xe.Buf, '0'), va.Int(), 10) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -450,7 +450,7 @@ func makeUintArshaler(t reflect.Type) *arshaler { } // Optimize for marshaling without preceding whitespace or string escaping. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = strconv.AppendUint(xe.Tokens.MayAppendDelim(xe.Buf, '0'), va.Uint(), 10) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -532,7 +532,7 @@ func makeFloatArshaler(t reflect.Type) *arshaler { } // Optimize for marshaling without preceding whitespace or string escaping. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !mo.Flags.Get(jsonflags.StringifyNumbers) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = jsonwire.AppendFloat(xe.Tokens.MayAppendDelim(xe.Buf, '0'), fv, bits) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -651,7 +651,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { return enc.WriteToken(jsontext.Null) } // Optimize for marshaling an empty map without any preceding whitespace. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '{'), "{}"...) xe.Tokens.Last.Increment() if xe.NeedFlush() { @@ -975,9 +975,13 @@ 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) { + if xe.Flags.Get(jsonflags.Multiline) { b = xe.AppendIndent(b, xe.Tokens.NeedIndent('"')) } @@ -1252,7 +1256,7 @@ func makeSliceArshaler(t reflect.Type) *arshaler { return enc.WriteToken(jsontext.Null) } // Optimize for marshaling an empty slice without any preceding whitespace. - if optimizeCommon && !xe.Flags.Get(jsonflags.Expand) && !xe.Tokens.Last.NeedObjectName() { + if optimizeCommon && !xe.Flags.Get(jsonflags.AnyWhitespace) && !xe.Tokens.Last.NeedObjectName() { xe.Buf = append(xe.Tokens.MayAppendDelim(xe.Buf, '['), "[]"...) xe.Tokens.Last.Increment() if xe.NeedFlush() { diff --git a/arshal_test.go b/arshal_test.go index 6a6ef14..f189e61 100644 --- a/arshal_test.go +++ b/arshal_test.go @@ -1087,7 +1087,7 @@ func TestMarshal(t *testing.T) { want: `{"Aaa":"Aaa","AA_A":"AA_A","AaA":"AaA","AAa":"AAa","AAA":"AAA"}`, }, { name: jsontest.Name("Structs/Normal"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structAll{ Bool: true, String: "hello", @@ -1231,9 +1231,94 @@ 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)}, + opts: []Options{jsontext.Multiline(true)}, in: structStringifiedAll{ Bool: true, String: "hello", @@ -1383,7 +1468,7 @@ func TestMarshal(t *testing.T) { want: `{}`, }, { name: jsontest.Name("Structs/OmitZero/NonZero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitZeroAll{ Bool: true, // not omitted since true is non-zero String: " ", // not omitted since non-empty string is non-zero @@ -1445,7 +1530,7 @@ func TestMarshal(t *testing.T) { want: `{"ValueNeverZero":"","PointerNeverZero":""}`, }, { name: jsontest.Name("Structs/OmitZeroMethod/NonZero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitZeroMethodAll{ ValueAlwaysZero: valueAlwaysZero("nonzero"), ValueNeverZero: valueNeverZero("nonzero"), @@ -1472,12 +1557,12 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitZeroMethod/Interface/Zero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitZeroMethodInterfaceAll{}, want: `{}`, }, { name: jsontest.Name("Structs/OmitZeroMethod/Interface/PartialZero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitZeroMethodInterfaceAll{ ValueAlwaysZero: valueAlwaysZero(""), ValueNeverZero: valueNeverZero(""), @@ -1491,7 +1576,7 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitZeroMethod/Interface/NonZero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitZeroMethodInterfaceAll{ ValueAlwaysZero: valueAlwaysZero("nonzero"), ValueNeverZero: valueNeverZero("nonzero"), @@ -1507,7 +1592,7 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitEmpty/Zero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitEmptyAll{}, want: `{ "Bool": false, @@ -1525,7 +1610,7 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitEmpty/EmptyNonZero"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitEmptyAll{ String: string(""), StringEmpty: stringMarshalEmpty(""), @@ -1580,7 +1665,7 @@ func TestMarshal(t *testing.T) { }`, }, { name: jsontest.Name("Structs/OmitEmpty/NonEmpty"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structOmitEmptyAll{ Bool: true, PointerBool: addr(true), @@ -1668,7 +1753,7 @@ func TestMarshal(t *testing.T) { want: `{}`, }, { name: jsontest.Name("Structs/OmitEmpty/Legacy/NonEmpty"), - opts: []Options{jsontext.Expand(true), jsonflags.OmitEmptyWithLegacyDefinition | 1}, + opts: []Options{jsontext.Multiline(true), jsonflags.OmitEmptyWithLegacyDefinition | 1}, in: structOmitEmptyAll{ Bool: true, PointerBool: addr(true), @@ -1835,7 +1920,7 @@ func TestMarshal(t *testing.T) { want: `{"Bytes":"dmFsdWU=","Map":{"":""},"Slice":[""],"Pointer":{"Bool":true},"Interface":[""]}`, }, { name: jsontest.Name("Structs/Format/Bytes"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structFormatBytes{ Base16: []byte("\x01\x23\x45\x67\x89\xab\xcd\xef"), Base32: []byte("\x00D2\x14\xc7BT\xb65τe:V\xd7\xc6u\xbew\xdf"), @@ -1858,7 +1943,7 @@ func TestMarshal(t *testing.T) { ] }`}, { name: jsontest.Name("Structs/Format/ArrayBytes"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structFormatArrayBytes{ Base16: [4]byte{1, 2, 3, 4}, Base32: [4]byte{1, 2, 3, 4}, @@ -1883,7 +1968,7 @@ func TestMarshal(t *testing.T) { "Default": "AQIDBA==" }`}, { name: jsontest.Name("Structs/Format/ArrayBytes/Legacy"), - opts: []Options{jsontext.Expand(true), jsonflags.FormatByteArrayAsArray | 1}, + opts: []Options{jsontext.Multiline(true), jsonflags.FormatByteArrayAsArray | 1}, in: structFormatArrayBytes{ Base16: [4]byte{1, 2, 3, 4}, Base32: [4]byte{1, 2, 3, 4}, @@ -1930,7 +2015,7 @@ func TestMarshal(t *testing.T) { want: `{"Array":[false,true,false,true,false,true]}`, }, { name: jsontest.Name("Structs/Format/Floats"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: []structFormatFloats{ {NonFinite: math.Pi, PointerNonFinite: addr(math.Pi)}, {NonFinite: math.NaN(), PointerNonFinite: addr(math.NaN())}, @@ -1957,7 +2042,7 @@ func TestMarshal(t *testing.T) { ]`, }, { name: jsontest.Name("Structs/Format/Maps"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: []structFormatMaps{{ EmitNull: map[string]string(nil), PointerEmitNull: addr(map[string]string(nil)), EmitEmpty: map[string]string(nil), PointerEmitEmpty: addr(map[string]string(nil)), @@ -2013,7 +2098,7 @@ func TestMarshal(t *testing.T) { name: jsontest.Name("Structs/Format/Maps/FormatNilMapAsNull"), opts: []Options{ FormatNilMapAsNull(true), - jsontext.Expand(true), + jsontext.Multiline(true), }, in: []structFormatMaps{{ EmitNull: map[string]string(nil), PointerEmitNull: addr(map[string]string(nil)), @@ -2068,7 +2153,7 @@ func TestMarshal(t *testing.T) { ]`, }, { name: jsontest.Name("Structs/Format/Slices"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: []structFormatSlices{{ EmitNull: []string(nil), PointerEmitNull: addr([]string(nil)), EmitEmpty: []string(nil), PointerEmitEmpty: addr([]string(nil)), @@ -2851,7 +2936,7 @@ func TestMarshal(t *testing.T) { want: `{"X":{}}`, }, { name: jsontest.Name("Interfaces/Any/Maps/Empty/Multiline"), - opts: []Options{jsontext.Expand(true), jsontext.WithIndent("")}, + opts: []Options{jsontext.Multiline(true), jsontext.WithIndent("")}, in: struct{ X any }{map[string]any{}}, want: "{\n\"X\": {}\n}", }, { @@ -2914,7 +2999,7 @@ func TestMarshal(t *testing.T) { want: `{"X":[]}`, }, { name: jsontest.Name("Interfaces/Any/Slices/Empty/Multiline"), - opts: []Options{jsontext.Expand(true), jsontext.WithIndent("")}, + opts: []Options{jsontext.Multiline(true), jsontext.WithIndent("")}, in: struct{ X any }{[]any{}}, want: "{\n\"X\": []\n}", }, { @@ -3979,7 +4064,7 @@ func TestMarshal(t *testing.T) { want: `"0s"`, }, { name: jsontest.Name("Duration/Format"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structDurationFormat{ 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, @@ -4034,7 +4119,7 @@ func TestMarshal(t *testing.T) { want: `{"T1":"0001-01-01T00:00:00Z","T2":"01 Jan 01 00:00 UTC","T3":"0001-01-01","T5":"0001-01-01T00:00:00Z"}`, }, { name: jsontest.Name("Time/Format"), - opts: []Options{jsontext.Expand(true)}, + opts: []Options{jsontext.Multiline(true)}, in: structTimeFormat{ time.Date(1234, 1, 2, 3, 4, 5, 6, time.UTC), time.Date(1234, 1, 2, 3, 4, 5, 6, time.UTC), diff --git a/example_test.go b/example_test.go index d8fb612..d6380ec 100644 --- a/example_test.go +++ b/example_test.go @@ -573,7 +573,7 @@ func ExampleWithMarshalers_errors() { return []byte(`"internal server error"`), nil }), )), - jsontext.Expand(true)) // expand for readability + jsontext.Multiline(true)) // expand for readability if err != nil { log.Fatal(err) } diff --git a/internal/jsonflags/flags.go b/internal/jsonflags/flags.go index c2473cd..8ed0bfa 100644 --- a/internal/jsonflags/flags.go +++ b/internal/jsonflags/flags.go @@ -14,7 +14,7 @@ import "github.com/go-json-experiment/json/internal" // // In common usage, this is OR'd with 0 or 1. For example: // - (AllowInvalidUTF8 | 0) means "AllowInvalidUTF8 is false" -// - (Expand | Indent | 1) means "Expand and Indent are true" +// - (Multiline | Indent | 1) means "Multiline and Indent are true" type Bools uint64 func (Bools) JSONOptions(internal.NotForPublicUse) {} @@ -65,6 +65,9 @@ const ( SkipUnaddressableMethods | StringifyWithLegacySemantics | UnmarshalArrayFromAnyLength + + // AnyWhitespace reports whether the encoded output might have any whitespace. + AnyWhitespace = Multiline | SpaceAfterColon | SpaceAfterComma ) // Encoder and decoder flags. @@ -79,7 +82,9 @@ const ( CanonicalizeNumbers // encode only; for internal use by jsontext.Value.Canonicalize EscapeForHTML // encode only EscapeForJS // encode only - Expand // encode only + Multiline // 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 diff --git a/internal/jsonflags/flags_test.go b/internal/jsonflags/flags_test.go index a916bfd..1673f71 100644 --- a/internal/jsonflags/flags_test.go +++ b/internal/jsonflags/flags_test.go @@ -39,15 +39,15 @@ func TestFlags(t *testing.T) { Check{want: Flags{Presence: uint64(AllowDuplicateNames | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames | AllowInvalidUTF8)}}, Join{in: Flags{Presence: 0, Values: 0}}, Check{want: Flags{Presence: uint64(AllowDuplicateNames | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames | AllowInvalidUTF8)}}, - Join{in: Flags{Presence: uint64(Expand | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames)}}, - Check{want: Flags{Presence: uint64(Expand | AllowDuplicateNames | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames)}}, + Join{in: Flags{Presence: uint64(Multiline | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames)}}, + Check{want: Flags{Presence: uint64(Multiline | AllowDuplicateNames | AllowInvalidUTF8), Values: uint64(AllowDuplicateNames)}}, Clear{in: AllowDuplicateNames | AllowInvalidUTF8}, - Check{want: Flags{Presence: uint64(Expand), Values: uint64(0)}}, + Check{want: Flags{Presence: uint64(Multiline), Values: uint64(0)}}, Set{in: AllowInvalidUTF8 | Deterministic | IgnoreStructErrors | 1}, - Set{in: Expand | StringifyNumbers | RejectFloatOverflow | 0}, - Check{want: Flags{Presence: uint64(AllowInvalidUTF8 | Deterministic | IgnoreStructErrors | Expand | StringifyNumbers | RejectFloatOverflow), Values: uint64(AllowInvalidUTF8 | Deterministic | IgnoreStructErrors)}}, + Set{in: Multiline | StringifyNumbers | RejectFloatOverflow | 0}, + Check{want: Flags{Presence: uint64(AllowInvalidUTF8 | Deterministic | IgnoreStructErrors | Multiline | StringifyNumbers | RejectFloatOverflow), Values: uint64(AllowInvalidUTF8 | Deterministic | IgnoreStructErrors)}}, Clear{in: ^AllCoderFlags}, - Check{want: Flags{Presence: uint64(AllowInvalidUTF8 | Expand), Values: uint64(AllowInvalidUTF8)}}, + Check{want: Flags{Presence: uint64(AllowInvalidUTF8 | Multiline), Values: uint64(AllowInvalidUTF8)}}, } var fs Flags for i, call := range calls { diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index 6b63d5c..aee297f 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -49,7 +49,7 @@ var DefaultOptionsV2 = Struct{ Presence: uint64(jsonflags.AllFlags), Values: uint64(0), }, - CoderValues: CoderValues{Indent: "\t"}, // Indent is set, but Expand is set to false + CoderValues: CoderValues{Indent: "\t"}, // Indent is set, but Multiline is set to false } // DefaultOptionsV1 is the set of all options that define default v1 behavior. @@ -58,7 +58,7 @@ var DefaultOptionsV1 = Struct{ Presence: uint64(jsonflags.AllFlags), Values: uint64(jsonflags.DefaultV1Flags), }, - CoderValues: CoderValues{Indent: "\t"}, // Indent is set, but Expand is set to false + CoderValues: CoderValues{Indent: "\t"}, // Indent is set, but Multiline is set to false } // CopyCoderOptions copies coder-specific options from src to dst. @@ -130,12 +130,22 @@ func (dst *Struct) Join(srcs ...Options) { case nil: continue case jsonflags.Bools: + switch src { + case jsonflags.Multiline | 1: + dst.Flags.Clear(jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon) + case jsonflags.SpaceAfterComma | 1, jsonflags.SpaceAfterColon | 1: + if dst.Flags.Get(jsonflags.Multiline) { + continue + } + } dst.Flags.Set(src) case Indent: - dst.Flags.Set(jsonflags.Expand | jsonflags.Indent | 1) + dst.Flags.Set(jsonflags.Multiline | 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.Set(jsonflags.Multiline | jsonflags.IndentPrefix | 1) + dst.Flags.Clear(jsonflags.SpaceAfterComma | jsonflags.SpaceAfterColon) dst.IndentPrefix = string(src) case ByteLimit: dst.Flags.Set(jsonflags.ByteLimit | 1) diff --git a/internal/jsonopts/options_test.go b/internal/jsonopts/options_test.go index 2e5281b..acebb90 100644 --- a/internal/jsonopts/options_test.go +++ b/internal/jsonopts/options_test.go @@ -23,17 +23,17 @@ func makeFlags(f ...jsonflags.Bools) (fs jsonflags.Flags) { func TestCopyCoderOptions(t *testing.T) { got := &Struct{ - Flags: makeFlags(jsonflags.Indent|jsonflags.AllowInvalidUTF8|0, jsonflags.Expand|jsonflags.AllowDuplicateNames|jsonflags.Unmarshalers|1), + Flags: makeFlags(jsonflags.Indent|jsonflags.AllowInvalidUTF8|0, jsonflags.Multiline|jsonflags.AllowDuplicateNames|jsonflags.Unmarshalers|1), CoderValues: CoderValues{Indent: " "}, ArshalValues: ArshalValues{Unmarshalers: "something"}, } src := &Struct{ - Flags: makeFlags(jsonflags.Indent|jsonflags.Deterministic|jsonflags.Marshalers|1, jsonflags.Expand|0), + Flags: makeFlags(jsonflags.Indent|jsonflags.Deterministic|jsonflags.Marshalers|1, jsonflags.Multiline|0), CoderValues: CoderValues{Indent: "\t"}, ArshalValues: ArshalValues{Marshalers: "something"}, } want := &Struct{ - Flags: makeFlags(jsonflags.AllowInvalidUTF8|jsonflags.Expand|0, jsonflags.Indent|jsonflags.AllowDuplicateNames|jsonflags.Unmarshalers|1), + Flags: makeFlags(jsonflags.AllowInvalidUTF8|jsonflags.Multiline|0, jsonflags.Indent|jsonflags.AllowDuplicateNames|jsonflags.Unmarshalers|1), CoderValues: CoderValues{Indent: "\t"}, ArshalValues: ArshalValues{Unmarshalers: "something"}, } @@ -51,21 +51,21 @@ func TestJoin(t *testing.T) { in: jsonflags.AllowInvalidUTF8 | 1, want: &Struct{Flags: makeFlags(jsonflags.AllowInvalidUTF8 | 1)}, }, { - in: jsonflags.Expand | 0, + in: jsonflags.Multiline | 0, want: &Struct{ - Flags: makeFlags(jsonflags.AllowInvalidUTF8|1, jsonflags.Expand|0)}, + Flags: makeFlags(jsonflags.AllowInvalidUTF8|1, jsonflags.Multiline|0)}, }, { - in: Indent("\t"), // implicitly sets Expand=true + in: Indent("\t"), // implicitly sets Multiline=true want: &Struct{ - Flags: makeFlags(jsonflags.AllowInvalidUTF8 | jsonflags.Expand | jsonflags.Indent | 1), + Flags: makeFlags(jsonflags.AllowInvalidUTF8 | jsonflags.Multiline | jsonflags.Indent | 1), CoderValues: CoderValues{Indent: "\t"}, }, }, { in: &Struct{ - Flags: makeFlags(jsonflags.Expand|jsonflags.EscapeForJS|0, jsonflags.AllowInvalidUTF8|1), + Flags: makeFlags(jsonflags.Multiline|jsonflags.EscapeForJS|0, jsonflags.AllowInvalidUTF8|1), }, want: &Struct{ - Flags: makeFlags(jsonflags.AllowInvalidUTF8|jsonflags.Indent|1, jsonflags.Expand|jsonflags.EscapeForJS|0), + Flags: makeFlags(jsonflags.AllowInvalidUTF8|jsonflags.Indent|1, jsonflags.Multiline|jsonflags.EscapeForJS|0), CoderValues: CoderValues{Indent: "\t"}, }, }, { @@ -84,7 +84,7 @@ func TestJoin(t *testing.T) { func TestGet(t *testing.T) { opts := &Struct{ - Flags: makeFlags(jsonflags.Indent|jsonflags.Deterministic|jsonflags.Marshalers|1, jsonflags.Expand|0), + Flags: makeFlags(jsonflags.Indent|jsonflags.Deterministic|jsonflags.Marshalers|1, jsonflags.Multiline|0), CoderValues: CoderValues{Indent: "\t"}, ArshalValues: ArshalValues{Marshalers: new(json.Marshalers)}, } @@ -115,8 +115,8 @@ func TestGet(t *testing.T) { if v, ok := json.GetOption(opts, json.Deterministic); !v || !ok { t.Errorf("GetOption(..., Deterministic) = (%v, %v), want (true, true)", v, ok) } - if v, ok := json.GetOption(opts, jsontext.Expand); v || !ok { - t.Errorf("GetOption(..., Expand) = (%v, %v), want (false, true)", v, ok) + if v, ok := json.GetOption(opts, jsontext.Multiline); v || !ok { + t.Errorf("GetOption(..., Multiline) = (%v, %v), want (false, true)", v, ok) } if v, ok := json.GetOption(opts, jsontext.AllowInvalidUTF8); v || ok { t.Errorf("GetOption(..., AllowInvalidUTF8) = (%v, %v), want (false, false)", v, ok) diff --git a/jsontext/encode.go b/jsontext/encode.go index c45b325..b339d55 100644 --- a/jsontext/encode.go +++ b/jsontext/encode.go @@ -116,7 +116,7 @@ func (e *encoderState) reset(b []byte, w io.Writer, opts ...Options) { } e.Struct = jsonopts.Struct{} e.Struct.Join(opts...) - if e.Flags.Get(jsonflags.Expand) && !e.Flags.Has(jsonflags.Indent) { + if e.Flags.Get(jsonflags.Multiline) && !e.Flags.Has(jsonflags.Indent) { e.Indent = "\t" } } @@ -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) b = jsonwire.TrimSuffixString(b) b = jsonwire.TrimSuffixWhitespace(b) b = jsonwire.TrimSuffixByte(b, ',') @@ -337,7 +338,7 @@ func (e *encoderState) WriteToken(t Token) error { // Append any delimiters or optional whitespace. b = e.Tokens.MayAppendDelim(b, k) - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.AnyWhitespace) { b = e.appendWhitespace(b, k) } pos := len(b) // offset before the token @@ -428,7 +429,7 @@ func (e *encoderState) AppendRaw(k Kind, safeASCII bool, appendFn func([]byte) ( // Append any delimiters or optional whitespace. b = e.Tokens.MayAppendDelim(b, k) - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.AnyWhitespace) { b = e.appendWhitespace(b, k) } pos := len(b) // offset before the token @@ -513,7 +514,7 @@ func (e *encoderState) WriteValue(v Value) error { // Append any delimiters or optional whitespace. b = e.Tokens.MayAppendDelim(b, k) - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.AnyWhitespace) { b = e.appendWhitespace(b, k) } pos := len(b) // offset before the value @@ -580,11 +581,19 @@ func (e *encoderState) WriteValue(v Value) error { // appendWhitespace appends whitespace that immediately precedes the next token. func (e *encoderState) appendWhitespace(b []byte, next Kind) []byte { - if e.Tokens.needDelim(next) == ':' { - return append(b, ' ') + if delim := e.Tokens.needDelim(next); delim == ':' { + if e.Flags.Get(jsonflags.Multiline | jsonflags.SpaceAfterColon) { + return append(b, ' ') + } } else { - return e.AppendIndent(b, e.Tokens.NeedIndent(next)) + if e.Flags.Get(jsonflags.Multiline) { + return e.AppendIndent(b, e.Tokens.NeedIndent(next)) + } + if delim == ',' && e.Flags.Get(jsonflags.SpaceAfterComma) { + return append(b, ' ') + } } + return b } // AppendIndent appends the appropriate number of indentation characters @@ -683,7 +692,7 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, depth++ for { // Append optional newline and indentation. - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.Multiline) { dst = e.AppendIndent(dst, depth) } @@ -717,7 +726,7 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, } dst = append(dst, ':') n += len(":") - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.Multiline | jsonflags.SpaceAfterColon) { dst = append(dst, ' ') } @@ -739,11 +748,15 @@ 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 '}': - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.Multiline) { dst = e.AppendIndent(dst, depth-1) } dst = append(dst, '}') @@ -783,7 +796,7 @@ func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, depth++ for { // Append optional newline and indentation. - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.Multiline) { dst = e.AppendIndent(dst, depth) } @@ -806,11 +819,15 @@ 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 ']': - if e.Flags.Get(jsonflags.Expand) { + if e.Flags.Get(jsonflags.Multiline) { dst = e.AppendIndent(dst, depth-1) } dst = append(dst, ']') diff --git a/jsontext/encode_test.go b/jsontext/encode_test.go index fd20dcb..73f1391 100644 --- a/jsontext/encode_test.go +++ b/jsontext/encode_test.go @@ -37,7 +37,7 @@ func testEncoder(t *testing.T, where jsontest.CasePos, formatName, typeName stri want = td.outCompacted switch formatName { case "Indented": - opts = append(opts, Expand(true)) + opts = append(opts, Multiline(true)) opts = append(opts, WithIndentPrefix("\t")) opts = append(opts, WithIndent(" ")) if td.outIndented != "" { diff --git a/jsontext/example_test.go b/jsontext/example_test.go index 06fd098..c694b4c 100644 --- a/jsontext/example_test.go +++ b/jsontext/example_test.go @@ -38,7 +38,7 @@ func Example_stringReplace() { in := strings.NewReader(input) dec := jsontext.NewDecoder(in) out := new(bytes.Buffer) - enc := jsontext.NewEncoder(out, jsontext.Expand(true)) // expand for readability + enc := jsontext.NewEncoder(out, jsontext.Multiline(true)) // expand for readability for { // Read a token from the input. tok, err := dec.ReadToken() @@ -114,7 +114,7 @@ func ExampleEscapeForHTML() { // JSON will be safe to directly embed inside HTML. jsontext.EscapeForHTML(true), jsontext.EscapeForJS(true), - jsontext.Expand(true)) // expand for readability + jsontext.Multiline(true)) // expand for readability if err != nil { log.Fatal(err) } diff --git a/jsontext/options.go b/jsontext/options.go index 242c50a..78ac6d5 100644 --- a/jsontext/options.go +++ b/jsontext/options.go @@ -78,20 +78,54 @@ func EscapeForJS(v bool) Options { } } -// Expand specifies that the JSON output should be expanded, +// Multiline specifies that the JSON output should expand to multiple lines, // where every JSON object member or JSON array element // appears on a new, indented line according to the nesting depth. // If an indent is not already specified, then it defaults to using "\t". // +// Multiline will override SpaceAfterColon and SpaceAfterComma. +// // If set to false, then the output is compact, // where no whitespace is emitted between JSON values. // // This only affects encoding and is ignored when decoding. -func Expand(v bool) Options { +func Multiline(v bool) Options { if v { - return jsonflags.Expand | 1 + return jsonflags.Multiline | 1 } else { - return jsonflags.Expand | 0 + return jsonflags.Multiline | 0 + } +} + +// 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 Multiline, 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 Multiline, 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 } } @@ -101,11 +135,13 @@ func Expand(v bool) Options { // followed by one or more copies of indent according to the nesting depth. // The indent must only be composed of space or tab characters. // +// WithIndent will override SpaceAfterColon and SpaceAfterComma. +// // If the intent to emit indented output without a preference for -// the particular indent string, then use [Expand] instead. +// the particular indent string, then use [Multiline] instead. // // This only affects encoding and is ignored when decoding. -// Use of this option implies [Expand] being set to true. +// Use of this option implies [Multiline] being set to true. func WithIndent(indent string) Options { // Fast-path: Return a constant for common indents, which avoids allocating. // These are derived from analyzing the Go module proxy on 2023-07-01. @@ -137,8 +173,10 @@ func WithIndent(indent string) Options { // (see [WithIndent]) according to the nesting depth. // The prefix must only be composed of space or tab characters. // +// WithIndentPrefix will override SpaceAfterColon and SpaceAfterComma. +// // This only affects encoding and is ignored when decoding. -// Use of this option implies [Expand] being set to true. +// Use of this option implies [Multiline] being set to true. func WithIndentPrefix(prefix string) Options { if s := strings.Trim(prefix, " \t"); len(s) > 0 { panic("json: invalid character " + jsonwire.QuoteRune(s) + " in indent prefix") diff --git a/jsontext/value.go b/jsontext/value.go index 361c1fc..d9b5c34 100644 --- a/jsontext/value.go +++ b/jsontext/value.go @@ -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 @@ -150,7 +152,7 @@ func (v *Value) reformat(canonical, multiline bool, prefix, indent string) error eo.Flags.Set(jsonflags.PreserveRawStrings | 0) // per RFC 8785, section 3.2.2.2 eo.Flags.Set(jsonflags.EscapeForHTML | 0) // per RFC 8785, section 3.2.2.2 eo.Flags.Set(jsonflags.EscapeForJS | 0) // per RFC 8785, section 3.2.2.2 - eo.Flags.Set(jsonflags.Expand | 0) // per RFC 8785, section 3.2.1 + eo.Flags.Set(jsonflags.Multiline | 0) // per RFC 8785, section 3.2.1 } else { if s := strings.TrimLeft(prefix, " \t"); len(s) > 0 { panic("json: invalid character " + jsonwire.QuoteRune(s) + " in indent prefix") @@ -162,13 +164,13 @@ func (v *Value) reformat(canonical, multiline bool, prefix, indent string) error eo.Flags.Set(jsonflags.AllowDuplicateNames | 1) eo.Flags.Set(jsonflags.PreserveRawStrings | 1) if multiline { - eo.Flags.Set(jsonflags.Expand | 1) + eo.Flags.Set(jsonflags.Multiline | 1) eo.Flags.Set(jsonflags.Indent | 1) eo.Flags.Set(jsonflags.IndentPrefix | 1) eo.IndentPrefix = prefix eo.Indent = indent } else { - eo.Flags.Set(jsonflags.Expand | 0) + eo.Flags.Set(jsonflags.Multiline | 0) } } eo.Flags.Set(jsonflags.OmitTopLevelNewline | 1)