From e77e31a26b5bcf201d9852f9c20aaee3047e4f35 Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Thu, 19 Oct 2023 20:31:24 -0400 Subject: [PATCH 1/6] Proof-of-concept for Structs + Variadic Funcs for options See https://github.com/golang/go/discussions/63397#discussioncomment-7333534 --- internal/jsonopts/interfaces.go | 13 ++++ internal/jsonopts/options.go | 57 ++++++++------- marshal_test.go | 62 ++++++++++++++++ struct_options.go | 124 ++++++++++++++++++++++++++++++++ 4 files changed, 232 insertions(+), 24 deletions(-) create mode 100644 internal/jsonopts/interfaces.go create mode 100644 marshal_test.go create mode 100644 struct_options.go diff --git a/internal/jsonopts/interfaces.go b/internal/jsonopts/interfaces.go new file mode 100644 index 0000000..077177c --- /dev/null +++ b/internal/jsonopts/interfaces.go @@ -0,0 +1,13 @@ +package jsonopts + +import ( + "github.com/go-json-experiment/json/internal" +) + +type OptionsArshaler interface { + OptionsArshal(dst *Struct, _ internal.NotForPublicUse) +} + +type OptionsCoder interface { + OptionsCode(dst *Struct, _ internal.NotForPublicUse) +} diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index 6b63d5c..0a2fc4f 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -126,51 +126,60 @@ var JoinUnknownOption = func(*Struct, Options) { panic("unknown option") } func (dst *Struct) Join(srcs ...Options) { for _, src := range srcs { - switch src := src.(type) { + switch typedSrc := src.(type) { case nil: continue + case OptionsArshaler: + typedSrc.OptionsArshal(dst, internal.NotForPublicUse{}) + case OptionsCoder: + typedSrc.OptionsCode(dst, internal.NotForPublicUse{}) + // Alternate to avoid exporting named interface, if we prefer that? + //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: + // typedSrc.OptionsArshal(dst, internal.NotForPublicUse{}) + //case interface{OptionsCode(*Struct,internal.NotForPublicUse)}: + // typedSrc.OptionsCode(dst, internal.NotForPublicUse{}) case jsonflags.Bools: - dst.Flags.Set(src) + dst.Flags.Set(typedSrc) case Indent: dst.Flags.Set(jsonflags.Expand | jsonflags.Indent | 1) - dst.Indent = string(src) + dst.Indent = string(typedSrc) case IndentPrefix: dst.Flags.Set(jsonflags.Expand | jsonflags.IndentPrefix | 1) - dst.IndentPrefix = string(src) + dst.IndentPrefix = string(typedSrc) case ByteLimit: dst.Flags.Set(jsonflags.ByteLimit | 1) - dst.ByteLimit = int64(src) + dst.ByteLimit = int64(typedSrc) case DepthLimit: dst.Flags.Set(jsonflags.DepthLimit | 1) - dst.DepthLimit = int(src) + dst.DepthLimit = int(typedSrc) case *Struct: - dst.Flags.Join(src.Flags) - if src.Flags.Has(jsonflags.NonBooleanFlags) { - if src.Flags.Has(jsonflags.Indent) { - dst.Indent = src.Indent + dst.Flags.Join(typedSrc.Flags) + if typedSrc.Flags.Has(jsonflags.NonBooleanFlags) { + if typedSrc.Flags.Has(jsonflags.Indent) { + dst.Indent = typedSrc.Indent } - if src.Flags.Has(jsonflags.IndentPrefix) { - dst.IndentPrefix = src.IndentPrefix + if typedSrc.Flags.Has(jsonflags.IndentPrefix) { + dst.IndentPrefix = typedSrc.IndentPrefix } - if src.Flags.Has(jsonflags.ByteLimit) { - dst.ByteLimit = src.ByteLimit + if typedSrc.Flags.Has(jsonflags.ByteLimit) { + dst.ByteLimit = typedSrc.ByteLimit } - if src.Flags.Has(jsonflags.DepthLimit) { - dst.DepthLimit = src.DepthLimit + if typedSrc.Flags.Has(jsonflags.DepthLimit) { + dst.DepthLimit = typedSrc.DepthLimit } - if src.Flags.Has(jsonflags.Marshalers) { - dst.Marshalers = src.Marshalers + if typedSrc.Flags.Has(jsonflags.Marshalers) { + dst.Marshalers = typedSrc.Marshalers } - if src.Flags.Has(jsonflags.Unmarshalers) { - dst.Unmarshalers = src.Unmarshalers + if typedSrc.Flags.Has(jsonflags.Unmarshalers) { + dst.Unmarshalers = typedSrc.Unmarshalers } } - if src.Format != "" { - dst.Format = src.Format - dst.FormatDepth = src.FormatDepth + if typedSrc.Format != "" { + dst.Format = typedSrc.Format + dst.FormatDepth = typedSrc.FormatDepth } default: - JoinUnknownOption(dst, src) + JoinUnknownOption(dst, typedSrc) } } } diff --git a/marshal_test.go b/marshal_test.go new file mode 100644 index 0000000..65afd9f --- /dev/null +++ b/marshal_test.go @@ -0,0 +1,62 @@ +package json_test + +import ( + "reflect" + "testing" + + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/internal/jsonopts" +) + +type Foo struct { + Name string + Age int + Citizen bool + Map map[string]int +} + +func TestMarshalViaStruct(t *testing.T) { + tests := []struct { + name string + in any + wantOut []byte + wantErr bool + opts []jsonopts.Options + }{ + { + name: "No options", + in: Foo{Name: "John", Age: 41, Citizen: true}, + wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":{}}`), + wantErr: false, + opts: nil, + }, + { + name: "Format Nil Map as Null — Variadic", + in: Foo{Name: "John", Age: 41, Citizen: true}, + wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), + wantErr: false, + opts: []jsonopts.Options{json.FormatNilMapAsNull(true)}, + }, + { + name: "Format Nil Map as Null — Struct", + in: Foo{Name: "John", Age: 41, Citizen: true}, + wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), + wantErr: false, + opts: []jsonopts.Options{json.MarshalOptions{ + FormatNilMapAsNull: true, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotOut, err := json.Marshal(tt.in, tt.opts...) + if (err != nil) != tt.wantErr { + t.Errorf("Marshal() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(gotOut, tt.wantOut) { + t.Errorf("Marshal() gotOut = %s, want %v", string(gotOut), string(tt.wantOut)) + } + }) + } +} diff --git a/struct_options.go b/struct_options.go new file mode 100644 index 0000000..4f5b2d9 --- /dev/null +++ b/struct_options.go @@ -0,0 +1,124 @@ +package json + +import ( + "github.com/go-json-experiment/json/internal" + "github.com/go-json-experiment/json/internal/jsonflags" + "github.com/go-json-experiment/json/internal/jsonopts" +) + +// OptionsArshaler defines a method for allowing `json` to set options from +// `jsonopts` without creating an import cycle. +type OptionsArshaler interface { + OptionsArshal(*jsonopts.Struct, internal.NotForPublicUse) +} + +// OptionsCoder defines a method for allowing `json` to set options from +// `jsonopts` without creating an import cycle. +type OptionsCoder interface { + OptionsCode(*jsonopts.Struct, internal.NotForPublicUse) +} + +var _ OptionsArshaler = (*MarshalOptions)(nil) +var _ jsonopts.Options = (*MarshalOptions)(nil) + +type MarshalOptions struct { + StringifyNumbers bool + Deterministic bool + FormatNilSliceAsNull bool + FormatNilMapAsNull bool + MatchCaseInsensitiveNames bool + DiscardUnknownMembers bool + Marshalers *Marshalers + // TODO: add other options + Options // TODO: Use for v1 options +} + +func (mo MarshalOptions) OptionsArshal(dst *jsonopts.Struct, _ internal.NotForPublicUse) { + var bits Options + bits = StringifyNumbers(mo.StringifyNumbers) + dst.Flags.Set(bits.(jsonflags.Bools)) + bits = FormatNilMapAsNull(mo.FormatNilMapAsNull) + dst.Flags.Set(bits.(jsonflags.Bools)) + dst.Marshalers = mo.Marshalers + // TODO: Get all the other values from public struct + + // TODO: ALTERNATELY, this is what it would look like if we still need to + // decouple from the struct. + //stringifiers, ok := src.(interface{ getStringifyNumbers() bool }) + //if ok { + // bits := StringifyNumbers(stringifiers.getStringifyNumbers()) + // dst.Flags.Set(bits.(jsonflags.Bools)) + //} + //stringifiers, ok := src.(interface{ getFormatNilMapAsNull() bool }) + //if ok { + // bits := FormatNilMapAsNull(stringifiers.getFormatNilMapAsNull()) + // dst.Flags.Set(bits.(jsonflags.Bools)) + //} + //marshalers, ok := src.(interface{ getMarshalers() *Marshalers }) + //if ok { + // dst.Flags.Set(marshalers.getMarshalers()) + //} + // +} + +// TODO: ALTERNATELY, these are the get() methods to support the above +// commented out code. +//func (opts MarshalOptions) getFormatNilMapAsNull() bool { +// return opts.FormatNilMapAsNull +//} +//func (opts MarshalOptions) getStringifyNumbers() bool { +// return opts.StringifyNumbers +//} +//func (opts MarshalOptions) getMarshalers() *Marshalers { +// return opts.Marshalers +//} + +func (MarshalOptions) JSONOptions(internal.NotForPublicUse) {} + +var _ OptionsArshaler = (*UnmarshalOptions)(nil) +var _ jsonopts.Options = (*UnmarshalOptions)(nil) + +type UnmarshalOptions struct { + StringifyNumbers bool + MatchCaseInsensitiveNames bool + RejectUnknownMembers bool + Unmarshalers *Unmarshalers + // other options +} + +func (UnmarshalOptions) JSONOptions(internal.NotForPublicUse) {} +func (mo UnmarshalOptions) OptionsArshal(dst *jsonopts.Struct, _ internal.NotForPublicUse) { + // TODO: Get all the other values from public struct +} + +var _ OptionsCoder = (*EncodeOptions)(nil) +var _ jsonopts.Options = (*EncodeOptions)(nil) + +type EncodeOptions struct { + AllowDuplicateNames bool + AllowInvalidUTF8 bool + EscapeForHTML bool + EscapeForJS bool + Indent string + IndentPrefix string + // other options +} + +func (mo EncodeOptions) OptionsCode(dst *jsonopts.Struct, _ internal.NotForPublicUse) { + // TODO: Get all the other values from public struct +} +func (EncodeOptions) JSONOptions(internal.NotForPublicUse) {} + +var _ OptionsCoder = (*DecodeOptions)(nil) +var _ jsonopts.Options = (*DecodeOptions)(nil) + +type DecodeOptions struct { + AllowDuplicateNames bool + AllowInvalidUTF8 bool + // other options +} + +func (DecodeOptions) OptionsCode(dst *jsonopts.Struct, _ internal.NotForPublicUse) { + // TODO: Get all the other values from public struct +} +func (DecodeOptions) JSONOptions(internal.NotForPublicUse) {} From 4a1617dc23a8607360eb18b717be7e00cd962ad2 Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Thu, 19 Oct 2023 20:41:31 -0400 Subject: [PATCH 2/6] Rename `typedSrc` back to `src` The change was not needed after-all. --- internal/jsonopts/options.go | 56 ++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index 0a2fc4f..02e6a4b 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -126,60 +126,60 @@ var JoinUnknownOption = func(*Struct, Options) { panic("unknown option") } func (dst *Struct) Join(srcs ...Options) { for _, src := range srcs { - switch typedSrc := src.(type) { + switch src := src.(type) { case nil: continue case OptionsArshaler: - typedSrc.OptionsArshal(dst, internal.NotForPublicUse{}) + src.OptionsArshal(dst, internal.NotForPublicUse{}) case OptionsCoder: - typedSrc.OptionsCode(dst, internal.NotForPublicUse{}) + src.OptionsCode(dst, internal.NotForPublicUse{}) // Alternate to avoid exporting named interface, if we prefer that? //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: - // typedSrc.OptionsArshal(dst, internal.NotForPublicUse{}) + // src.OptionsArshal(dst, internal.NotForPublicUse{}) //case interface{OptionsCode(*Struct,internal.NotForPublicUse)}: - // typedSrc.OptionsCode(dst, internal.NotForPublicUse{}) + // src.OptionsCode(dst, internal.NotForPublicUse{}) case jsonflags.Bools: - dst.Flags.Set(typedSrc) + dst.Flags.Set(src) case Indent: dst.Flags.Set(jsonflags.Expand | jsonflags.Indent | 1) - dst.Indent = string(typedSrc) + dst.Indent = string(src) case IndentPrefix: dst.Flags.Set(jsonflags.Expand | jsonflags.IndentPrefix | 1) - dst.IndentPrefix = string(typedSrc) + dst.IndentPrefix = string(src) case ByteLimit: dst.Flags.Set(jsonflags.ByteLimit | 1) - dst.ByteLimit = int64(typedSrc) + dst.ByteLimit = int64(src) case DepthLimit: dst.Flags.Set(jsonflags.DepthLimit | 1) - dst.DepthLimit = int(typedSrc) + dst.DepthLimit = int(src) case *Struct: - dst.Flags.Join(typedSrc.Flags) - if typedSrc.Flags.Has(jsonflags.NonBooleanFlags) { - if typedSrc.Flags.Has(jsonflags.Indent) { - dst.Indent = typedSrc.Indent + dst.Flags.Join(src.Flags) + if src.Flags.Has(jsonflags.NonBooleanFlags) { + if src.Flags.Has(jsonflags.Indent) { + dst.Indent = src.Indent } - if typedSrc.Flags.Has(jsonflags.IndentPrefix) { - dst.IndentPrefix = typedSrc.IndentPrefix + if src.Flags.Has(jsonflags.IndentPrefix) { + dst.IndentPrefix = src.IndentPrefix } - if typedSrc.Flags.Has(jsonflags.ByteLimit) { - dst.ByteLimit = typedSrc.ByteLimit + if src.Flags.Has(jsonflags.ByteLimit) { + dst.ByteLimit = src.ByteLimit } - if typedSrc.Flags.Has(jsonflags.DepthLimit) { - dst.DepthLimit = typedSrc.DepthLimit + if src.Flags.Has(jsonflags.DepthLimit) { + dst.DepthLimit = src.DepthLimit } - if typedSrc.Flags.Has(jsonflags.Marshalers) { - dst.Marshalers = typedSrc.Marshalers + if src.Flags.Has(jsonflags.Marshalers) { + dst.Marshalers = src.Marshalers } - if typedSrc.Flags.Has(jsonflags.Unmarshalers) { - dst.Unmarshalers = typedSrc.Unmarshalers + if src.Flags.Has(jsonflags.Unmarshalers) { + dst.Unmarshalers = src.Unmarshalers } } - if typedSrc.Format != "" { - dst.Format = typedSrc.Format - dst.FormatDepth = typedSrc.FormatDepth + if src.Format != "" { + dst.Format = src.Format + dst.FormatDepth = src.FormatDepth } default: - JoinUnknownOption(dst, typedSrc) + JoinUnknownOption(dst, src) } } } From 66a5e48af47d6312a2b45d6966c93ac9059be4be Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Fri, 20 Oct 2023 16:57:49 -0400 Subject: [PATCH 3/6] Made `optionsArshaler` and optionsCoder` private No need for them to be public it seems. --- internal/jsonopts/interfaces.go | 4 ++-- internal/jsonopts/options.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/jsonopts/interfaces.go b/internal/jsonopts/interfaces.go index 077177c..7ac4fc3 100644 --- a/internal/jsonopts/interfaces.go +++ b/internal/jsonopts/interfaces.go @@ -4,10 +4,10 @@ import ( "github.com/go-json-experiment/json/internal" ) -type OptionsArshaler interface { +type optionsArshaler interface { OptionsArshal(dst *Struct, _ internal.NotForPublicUse) } -type OptionsCoder interface { +type optionsCoder interface { OptionsCode(dst *Struct, _ internal.NotForPublicUse) } diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index 02e6a4b..e50dddc 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -129,9 +129,9 @@ func (dst *Struct) Join(srcs ...Options) { switch src := src.(type) { case nil: continue - case OptionsArshaler: + case optionsArshaler: src.OptionsArshal(dst, internal.NotForPublicUse{}) - case OptionsCoder: + case optionsCoder: src.OptionsCode(dst, internal.NotForPublicUse{}) // Alternate to avoid exporting named interface, if we prefer that? //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: From c2abdc33e4153a5cc420172ea184ad80009d6997 Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Fri, 20 Oct 2023 17:00:30 -0400 Subject: [PATCH 4/6] Moved additions to the end of the type switch --- internal/jsonopts/options.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index e50dddc..dbd3a18 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -129,15 +129,6 @@ func (dst *Struct) Join(srcs ...Options) { switch src := src.(type) { case nil: continue - case optionsArshaler: - src.OptionsArshal(dst, internal.NotForPublicUse{}) - case optionsCoder: - src.OptionsCode(dst, internal.NotForPublicUse{}) - // Alternate to avoid exporting named interface, if we prefer that? - //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: - // src.OptionsArshal(dst, internal.NotForPublicUse{}) - //case interface{OptionsCode(*Struct,internal.NotForPublicUse)}: - // src.OptionsCode(dst, internal.NotForPublicUse{}) case jsonflags.Bools: dst.Flags.Set(src) case Indent: @@ -178,6 +169,15 @@ func (dst *Struct) Join(srcs ...Options) { dst.Format = src.Format dst.FormatDepth = src.FormatDepth } + case optionsArshaler: + src.OptionsArshal(dst, internal.NotForPublicUse{}) + case optionsCoder: + src.OptionsCode(dst, internal.NotForPublicUse{}) + // Alternate to avoid exporting named interface, if we prefer that? + //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: + // src.OptionsArshal(dst, internal.NotForPublicUse{}) + //case interface{OptionsCode(*Struct,internal.NotForPublicUse)}: + // src.OptionsCode(dst, internal.NotForPublicUse{}) default: JoinUnknownOption(dst, src) } From 062ffa6e77ec0cea5be73e748caaa4f21d9c4d16 Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Wed, 25 Oct 2023 20:45:05 -0400 Subject: [PATCH 5/6] Update to allow 3rd parties to implement option structs --- flags.go | 9 ++ internal/jsonflags/flags.go | 4 +- internal/jsonopts/interfaces.go | 13 --- internal/jsonopts/options.go | 30 +++--- myjson/arshal.go | 103 ++++++++++++++++++ myjson/go.mod | 7 ++ myjson/go.sum | 0 myjson/interfaces.go | 13 +++ marshal_test.go => myjson/marshal_test.go | 10 +- options.go | 6 +- struct_options.go | 124 ---------------------- 11 files changed, 154 insertions(+), 165 deletions(-) create mode 100644 flags.go delete mode 100644 internal/jsonopts/interfaces.go create mode 100644 myjson/arshal.go create mode 100644 myjson/go.mod create mode 100644 myjson/go.sum create mode 100644 myjson/interfaces.go rename marshal_test.go => myjson/marshal_test.go (86%) delete mode 100644 struct_options.go diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..2a6d5c4 --- /dev/null +++ b/flags.go @@ -0,0 +1,9 @@ +package json + +import ( + "github.com/go-json-experiment/json/internal/jsonflags" +) + +type ( + BoolFlags = jsonflags.Bools +) diff --git a/internal/jsonflags/flags.go b/internal/jsonflags/flags.go index c2473cd..9860f17 100644 --- a/internal/jsonflags/flags.go +++ b/internal/jsonflags/flags.go @@ -6,8 +6,6 @@ // These flags are shared across both "json", "jsontext", and "jsonopts". package jsonflags -import "github.com/go-json-experiment/json/internal" - // Bools represents zero or more boolean flags, all set to true or false. // The least-significant bit is the boolean value of all flags in the set. // The remaining bits identify which particular flags. @@ -17,7 +15,7 @@ import "github.com/go-json-experiment/json/internal" // - (Expand | Indent | 1) means "Expand and Indent are true" type Bools uint64 -func (Bools) JSONOptions(internal.NotForPublicUse) {} +func (Bools) JSONOptions() {} const ( // AllFlags is the set of all flags. diff --git a/internal/jsonopts/interfaces.go b/internal/jsonopts/interfaces.go deleted file mode 100644 index 7ac4fc3..0000000 --- a/internal/jsonopts/interfaces.go +++ /dev/null @@ -1,13 +0,0 @@ -package jsonopts - -import ( - "github.com/go-json-experiment/json/internal" -) - -type optionsArshaler interface { - OptionsArshal(dst *Struct, _ internal.NotForPublicUse) -} - -type optionsCoder interface { - OptionsCode(dst *Struct, _ internal.NotForPublicUse) -} diff --git a/internal/jsonopts/options.go b/internal/jsonopts/options.go index dbd3a18..376780c 100644 --- a/internal/jsonopts/options.go +++ b/internal/jsonopts/options.go @@ -5,14 +5,13 @@ package jsonopts import ( - "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" ) // Options is the common options type shared across json packages. type Options interface { // JSONOptions is exported so related json packages can implement Options. - JSONOptions(internal.NotForPublicUse) + JSONOptions() } // Struct is the combination of all options in struct form. @@ -72,7 +71,7 @@ func (dst *Struct) CopyCoderOptions(src *Struct) { dst.CoderValues = src.CoderValues } -func (*Struct) JSONOptions(internal.NotForPublicUse) {} +func (*Struct) JSONOptions() {} // GetUnknownOption is injected by the "json" package to handle Options // declared in that package so that "jsonopts" can handle them. @@ -169,17 +168,14 @@ func (dst *Struct) Join(srcs ...Options) { dst.Format = src.Format dst.FormatDepth = src.FormatDepth } - case optionsArshaler: - src.OptionsArshal(dst, internal.NotForPublicUse{}) - case optionsCoder: - src.OptionsCode(dst, internal.NotForPublicUse{}) - // Alternate to avoid exporting named interface, if we prefer that? - //case interface{OptionsArshal(*Struct,internal.NotForPublicUse)}: - // src.OptionsArshal(dst, internal.NotForPublicUse{}) - //case interface{OptionsCode(*Struct,internal.NotForPublicUse)}: - // src.OptionsCode(dst, internal.NotForPublicUse{}) default: - JoinUnknownOption(dst, src) + unknown := true + if joiner, ok := src.(interface{ Join(*Struct, Options) bool }); ok { + unknown = joiner.Join(dst, src) + } + if unknown { + JoinUnknownOption(dst, src) + } } } } @@ -193,7 +189,7 @@ type ( // type for jsonflags.Unmarshalers declared in "json" package ) -func (Indent) JSONOptions(internal.NotForPublicUse) {} -func (IndentPrefix) JSONOptions(internal.NotForPublicUse) {} -func (ByteLimit) JSONOptions(internal.NotForPublicUse) {} -func (DepthLimit) JSONOptions(internal.NotForPublicUse) {} +func (Indent) JSONOptions() {} +func (IndentPrefix) JSONOptions() {} +func (ByteLimit) JSONOptions() {} +func (DepthLimit) JSONOptions() {} diff --git a/myjson/arshal.go b/myjson/arshal.go new file mode 100644 index 0000000..171a7c1 --- /dev/null +++ b/myjson/arshal.go @@ -0,0 +1,103 @@ +package myjson + +import ( + "github.com/go-json-experiment/json" +) + +// OptionsArshaler defines a method for allowing `json` to set options from +// `jsonopts` without creating an import cycle. +type OptionsArshaler interface { + OptionsArshal(*json.OptionsStruct) +} + +// OptionsCoder defines a method for allowing `json` to set options from +// `jsonopts` without creating an import cycle. +type OptionsCoder interface { + OptionsCode(*json.OptionsStruct) +} + +var _ OptionsArshaler = (*MarshalOptions)(nil) +var _ json.Options = (*MarshalOptions)(nil) + +type MarshalOptions struct { + StringifyNumbers bool + Deterministic bool + FormatNilSliceAsNull bool + FormatNilMapAsNull bool + MatchCaseInsensitiveNames bool + DiscardUnknownMembers bool + Marshalers *json.Marshalers + // TODO: add other options + json.Options // TODO: Use for v1 options +} + +func (mo *MarshalOptions) Join(s *json.OptionsStruct, options json.Options) (unknown bool) { + switch option := options.(type) { + case optionsArshaler: + option.OptionsArshal(s) + case optionsCoder: + option.OptionsCode(s) + default: + unknown = true + } + return unknown +} + +func (mo *MarshalOptions) OptionsArshal(dst *json.OptionsStruct) { + var bits json.Options + bits = json.StringifyNumbers(mo.StringifyNumbers) + dst.Flags.Set(bits.(json.BoolFlags)) + bits = json.FormatNilMapAsNull(mo.FormatNilMapAsNull) + dst.Flags.Set(bits.(json.BoolFlags)) + dst.Marshalers = mo.Marshalers +} + +func (MarshalOptions) JSONOptions() {} + +var _ OptionsArshaler = (*UnmarshalOptions)(nil) +var _ json.Options = (*UnmarshalOptions)(nil) + +type UnmarshalOptions struct { + StringifyNumbers bool + MatchCaseInsensitiveNames bool + RejectUnknownMembers bool + Unmarshalers *json.Unmarshalers + // other options +} + +func (UnmarshalOptions) JSONOptions() {} +func (mo UnmarshalOptions) OptionsArshal(dst *json.OptionsStruct) { + // TODO: Get all the other values from public struct +} + +var _ OptionsCoder = (*EncodeOptions)(nil) +var _ json.Options = (*EncodeOptions)(nil) + +type EncodeOptions struct { + AllowDuplicateNames bool + AllowInvalidUTF8 bool + EscapeForHTML bool + EscapeForJS bool + Indent string + IndentPrefix string + // other options +} + +func (mo EncodeOptions) OptionsCode(dst *json.OptionsStruct) { + // TODO: Get all the other values from public struct +} +func (EncodeOptions) JSONOptions() {} + +var _ OptionsCoder = (*DecodeOptions)(nil) +var _ json.Options = (*DecodeOptions)(nil) + +type DecodeOptions struct { + AllowDuplicateNames bool + AllowInvalidUTF8 bool + // other options +} + +func (DecodeOptions) OptionsCode(dst *json.OptionsStruct) { + // TODO: Get all the other values from public struct +} +func (DecodeOptions) JSONOptions() {} diff --git a/myjson/go.mod b/myjson/go.mod new file mode 100644 index 0000000..ef45313 --- /dev/null +++ b/myjson/go.mod @@ -0,0 +1,7 @@ +module myjson + +go 1.21 + +replace github.com/go-json-experiment/json => ../. + +require github.com/go-json-experiment/json v0.0.0-20231013223334-54c864be5b8d diff --git a/myjson/go.sum b/myjson/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/myjson/interfaces.go b/myjson/interfaces.go new file mode 100644 index 0000000..c67679d --- /dev/null +++ b/myjson/interfaces.go @@ -0,0 +1,13 @@ +package myjson + +import ( + "github.com/go-json-experiment/json" +) + +type optionsArshaler interface { + OptionsArshal(dst *json.OptionsStruct) +} + +type optionsCoder interface { + OptionsCode(dst *json.OptionsStruct) +} diff --git a/marshal_test.go b/myjson/marshal_test.go similarity index 86% rename from marshal_test.go rename to myjson/marshal_test.go index 65afd9f..6353542 100644 --- a/marshal_test.go +++ b/myjson/marshal_test.go @@ -1,11 +1,11 @@ -package json_test +package myjson_test import ( "reflect" "testing" "github.com/go-json-experiment/json" - "github.com/go-json-experiment/json/internal/jsonopts" + "myjson" ) type Foo struct { @@ -21,7 +21,7 @@ func TestMarshalViaStruct(t *testing.T) { in any wantOut []byte wantErr bool - opts []jsonopts.Options + opts []json.Options }{ { name: "No options", @@ -35,14 +35,14 @@ func TestMarshalViaStruct(t *testing.T) { in: Foo{Name: "John", Age: 41, Citizen: true}, wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), wantErr: false, - opts: []jsonopts.Options{json.FormatNilMapAsNull(true)}, + opts: []json.Options{json.FormatNilMapAsNull(true)}, }, { name: "Format Nil Map as Null — Struct", in: Foo{Name: "John", Age: 41, Citizen: true}, wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), wantErr: false, - opts: []jsonopts.Options{json.MarshalOptions{ + opts: []json.Options{&myjson.MarshalOptions{ FormatNilMapAsNull: true, }}, }, diff --git a/options.go b/options.go index a43e31e..3bbd391 100644 --- a/options.go +++ b/options.go @@ -7,7 +7,6 @@ package json import ( "fmt" - "github.com/go-json-experiment/json/internal" "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" ) @@ -69,6 +68,7 @@ import ( // // Options that do not affect a particular operation are ignored. type Options = jsonopts.Options +type OptionsStruct = jsonopts.Struct // JoinOptions coalesces the provided list of options into a single Options. // Properties set in later options override the value of previously set properties. @@ -237,8 +237,8 @@ type ( unmarshalersOption Unmarshalers ) -func (*marshalersOption) JSONOptions(internal.NotForPublicUse) {} -func (*unmarshalersOption) JSONOptions(internal.NotForPublicUse) {} +func (*marshalersOption) JSONOptions() {} +func (*unmarshalersOption) JSONOptions() {} // Inject support into "jsonopts" to handle these types. func init() { diff --git a/struct_options.go b/struct_options.go deleted file mode 100644 index 4f5b2d9..0000000 --- a/struct_options.go +++ /dev/null @@ -1,124 +0,0 @@ -package json - -import ( - "github.com/go-json-experiment/json/internal" - "github.com/go-json-experiment/json/internal/jsonflags" - "github.com/go-json-experiment/json/internal/jsonopts" -) - -// OptionsArshaler defines a method for allowing `json` to set options from -// `jsonopts` without creating an import cycle. -type OptionsArshaler interface { - OptionsArshal(*jsonopts.Struct, internal.NotForPublicUse) -} - -// OptionsCoder defines a method for allowing `json` to set options from -// `jsonopts` without creating an import cycle. -type OptionsCoder interface { - OptionsCode(*jsonopts.Struct, internal.NotForPublicUse) -} - -var _ OptionsArshaler = (*MarshalOptions)(nil) -var _ jsonopts.Options = (*MarshalOptions)(nil) - -type MarshalOptions struct { - StringifyNumbers bool - Deterministic bool - FormatNilSliceAsNull bool - FormatNilMapAsNull bool - MatchCaseInsensitiveNames bool - DiscardUnknownMembers bool - Marshalers *Marshalers - // TODO: add other options - Options // TODO: Use for v1 options -} - -func (mo MarshalOptions) OptionsArshal(dst *jsonopts.Struct, _ internal.NotForPublicUse) { - var bits Options - bits = StringifyNumbers(mo.StringifyNumbers) - dst.Flags.Set(bits.(jsonflags.Bools)) - bits = FormatNilMapAsNull(mo.FormatNilMapAsNull) - dst.Flags.Set(bits.(jsonflags.Bools)) - dst.Marshalers = mo.Marshalers - // TODO: Get all the other values from public struct - - // TODO: ALTERNATELY, this is what it would look like if we still need to - // decouple from the struct. - //stringifiers, ok := src.(interface{ getStringifyNumbers() bool }) - //if ok { - // bits := StringifyNumbers(stringifiers.getStringifyNumbers()) - // dst.Flags.Set(bits.(jsonflags.Bools)) - //} - //stringifiers, ok := src.(interface{ getFormatNilMapAsNull() bool }) - //if ok { - // bits := FormatNilMapAsNull(stringifiers.getFormatNilMapAsNull()) - // dst.Flags.Set(bits.(jsonflags.Bools)) - //} - //marshalers, ok := src.(interface{ getMarshalers() *Marshalers }) - //if ok { - // dst.Flags.Set(marshalers.getMarshalers()) - //} - // -} - -// TODO: ALTERNATELY, these are the get() methods to support the above -// commented out code. -//func (opts MarshalOptions) getFormatNilMapAsNull() bool { -// return opts.FormatNilMapAsNull -//} -//func (opts MarshalOptions) getStringifyNumbers() bool { -// return opts.StringifyNumbers -//} -//func (opts MarshalOptions) getMarshalers() *Marshalers { -// return opts.Marshalers -//} - -func (MarshalOptions) JSONOptions(internal.NotForPublicUse) {} - -var _ OptionsArshaler = (*UnmarshalOptions)(nil) -var _ jsonopts.Options = (*UnmarshalOptions)(nil) - -type UnmarshalOptions struct { - StringifyNumbers bool - MatchCaseInsensitiveNames bool - RejectUnknownMembers bool - Unmarshalers *Unmarshalers - // other options -} - -func (UnmarshalOptions) JSONOptions(internal.NotForPublicUse) {} -func (mo UnmarshalOptions) OptionsArshal(dst *jsonopts.Struct, _ internal.NotForPublicUse) { - // TODO: Get all the other values from public struct -} - -var _ OptionsCoder = (*EncodeOptions)(nil) -var _ jsonopts.Options = (*EncodeOptions)(nil) - -type EncodeOptions struct { - AllowDuplicateNames bool - AllowInvalidUTF8 bool - EscapeForHTML bool - EscapeForJS bool - Indent string - IndentPrefix string - // other options -} - -func (mo EncodeOptions) OptionsCode(dst *jsonopts.Struct, _ internal.NotForPublicUse) { - // TODO: Get all the other values from public struct -} -func (EncodeOptions) JSONOptions(internal.NotForPublicUse) {} - -var _ OptionsCoder = (*DecodeOptions)(nil) -var _ jsonopts.Options = (*DecodeOptions)(nil) - -type DecodeOptions struct { - AllowDuplicateNames bool - AllowInvalidUTF8 bool - // other options -} - -func (DecodeOptions) OptionsCode(dst *jsonopts.Struct, _ internal.NotForPublicUse) { - // TODO: Get all the other values from public struct -} -func (DecodeOptions) JSONOptions(internal.NotForPublicUse) {} From 23139b421c9e07802250a94217c0e285951d92a2 Mon Sep 17 00:00:00 2001 From: Mike Schinkel Date: Wed, 25 Oct 2023 21:02:42 -0400 Subject: [PATCH 6/6] Remove 'myjson' module used to test PR #17 --- myjson/arshal.go | 103 ----------------------------------------- myjson/go.mod | 7 --- myjson/go.sum | 0 myjson/interfaces.go | 13 ------ myjson/marshal_test.go | 62 ------------------------- 5 files changed, 185 deletions(-) delete mode 100644 myjson/arshal.go delete mode 100644 myjson/go.mod delete mode 100644 myjson/go.sum delete mode 100644 myjson/interfaces.go delete mode 100644 myjson/marshal_test.go diff --git a/myjson/arshal.go b/myjson/arshal.go deleted file mode 100644 index 171a7c1..0000000 --- a/myjson/arshal.go +++ /dev/null @@ -1,103 +0,0 @@ -package myjson - -import ( - "github.com/go-json-experiment/json" -) - -// OptionsArshaler defines a method for allowing `json` to set options from -// `jsonopts` without creating an import cycle. -type OptionsArshaler interface { - OptionsArshal(*json.OptionsStruct) -} - -// OptionsCoder defines a method for allowing `json` to set options from -// `jsonopts` without creating an import cycle. -type OptionsCoder interface { - OptionsCode(*json.OptionsStruct) -} - -var _ OptionsArshaler = (*MarshalOptions)(nil) -var _ json.Options = (*MarshalOptions)(nil) - -type MarshalOptions struct { - StringifyNumbers bool - Deterministic bool - FormatNilSliceAsNull bool - FormatNilMapAsNull bool - MatchCaseInsensitiveNames bool - DiscardUnknownMembers bool - Marshalers *json.Marshalers - // TODO: add other options - json.Options // TODO: Use for v1 options -} - -func (mo *MarshalOptions) Join(s *json.OptionsStruct, options json.Options) (unknown bool) { - switch option := options.(type) { - case optionsArshaler: - option.OptionsArshal(s) - case optionsCoder: - option.OptionsCode(s) - default: - unknown = true - } - return unknown -} - -func (mo *MarshalOptions) OptionsArshal(dst *json.OptionsStruct) { - var bits json.Options - bits = json.StringifyNumbers(mo.StringifyNumbers) - dst.Flags.Set(bits.(json.BoolFlags)) - bits = json.FormatNilMapAsNull(mo.FormatNilMapAsNull) - dst.Flags.Set(bits.(json.BoolFlags)) - dst.Marshalers = mo.Marshalers -} - -func (MarshalOptions) JSONOptions() {} - -var _ OptionsArshaler = (*UnmarshalOptions)(nil) -var _ json.Options = (*UnmarshalOptions)(nil) - -type UnmarshalOptions struct { - StringifyNumbers bool - MatchCaseInsensitiveNames bool - RejectUnknownMembers bool - Unmarshalers *json.Unmarshalers - // other options -} - -func (UnmarshalOptions) JSONOptions() {} -func (mo UnmarshalOptions) OptionsArshal(dst *json.OptionsStruct) { - // TODO: Get all the other values from public struct -} - -var _ OptionsCoder = (*EncodeOptions)(nil) -var _ json.Options = (*EncodeOptions)(nil) - -type EncodeOptions struct { - AllowDuplicateNames bool - AllowInvalidUTF8 bool - EscapeForHTML bool - EscapeForJS bool - Indent string - IndentPrefix string - // other options -} - -func (mo EncodeOptions) OptionsCode(dst *json.OptionsStruct) { - // TODO: Get all the other values from public struct -} -func (EncodeOptions) JSONOptions() {} - -var _ OptionsCoder = (*DecodeOptions)(nil) -var _ json.Options = (*DecodeOptions)(nil) - -type DecodeOptions struct { - AllowDuplicateNames bool - AllowInvalidUTF8 bool - // other options -} - -func (DecodeOptions) OptionsCode(dst *json.OptionsStruct) { - // TODO: Get all the other values from public struct -} -func (DecodeOptions) JSONOptions() {} diff --git a/myjson/go.mod b/myjson/go.mod deleted file mode 100644 index ef45313..0000000 --- a/myjson/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module myjson - -go 1.21 - -replace github.com/go-json-experiment/json => ../. - -require github.com/go-json-experiment/json v0.0.0-20231013223334-54c864be5b8d diff --git a/myjson/go.sum b/myjson/go.sum deleted file mode 100644 index e69de29..0000000 diff --git a/myjson/interfaces.go b/myjson/interfaces.go deleted file mode 100644 index c67679d..0000000 --- a/myjson/interfaces.go +++ /dev/null @@ -1,13 +0,0 @@ -package myjson - -import ( - "github.com/go-json-experiment/json" -) - -type optionsArshaler interface { - OptionsArshal(dst *json.OptionsStruct) -} - -type optionsCoder interface { - OptionsCode(dst *json.OptionsStruct) -} diff --git a/myjson/marshal_test.go b/myjson/marshal_test.go deleted file mode 100644 index 6353542..0000000 --- a/myjson/marshal_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package myjson_test - -import ( - "reflect" - "testing" - - "github.com/go-json-experiment/json" - "myjson" -) - -type Foo struct { - Name string - Age int - Citizen bool - Map map[string]int -} - -func TestMarshalViaStruct(t *testing.T) { - tests := []struct { - name string - in any - wantOut []byte - wantErr bool - opts []json.Options - }{ - { - name: "No options", - in: Foo{Name: "John", Age: 41, Citizen: true}, - wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":{}}`), - wantErr: false, - opts: nil, - }, - { - name: "Format Nil Map as Null — Variadic", - in: Foo{Name: "John", Age: 41, Citizen: true}, - wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), - wantErr: false, - opts: []json.Options{json.FormatNilMapAsNull(true)}, - }, - { - name: "Format Nil Map as Null — Struct", - in: Foo{Name: "John", Age: 41, Citizen: true}, - wantOut: []byte(`{"Name":"John","Age":41,"Citizen":true,"Map":null}`), - wantErr: false, - opts: []json.Options{&myjson.MarshalOptions{ - FormatNilMapAsNull: true, - }}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gotOut, err := json.Marshal(tt.in, tt.opts...) - if (err != nil) != tt.wantErr { - t.Errorf("Marshal() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(gotOut, tt.wantOut) { - t.Errorf("Marshal() gotOut = %s, want %v", string(gotOut), string(tt.wantOut)) - } - }) - } -}