Skip to content

Commit

Permalink
Add WithOption for user-specified options
Browse files Browse the repository at this point in the history
  • Loading branch information
dsnet committed Jan 19, 2025
1 parent 0640c11 commit ffbbe55
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 1 deletion.
13 changes: 13 additions & 0 deletions internal/jsonopts/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package jsonopts

import (
"maps"

"github.com/go-json-experiment/json/internal"
"github.com/go-json-experiment/json/internal/jsonflags"
)
Expand All @@ -22,6 +24,11 @@ type Struct struct {

CoderValues
ArshalValues

// UserValues is a map of user-defined option values
// keyed by [reflect.Type] to the concrete user-defined value.
// The key type is not [reflect.Type] to avoid a dependency on reflect.
UserValues map[any]any
}

type CoderValues struct {
Expand Down Expand Up @@ -167,6 +174,12 @@ func (dst *Struct) Join(srcs ...Options) {
dst.Format = src.Format
dst.FormatDepth = src.FormatDepth
}
if len(src.UserValues) > 0 {
if dst.UserValues == nil {
dst.UserValues = make(map[any]any)
}
maps.Copy(dst.UserValues, src.UserValues)
}
default:
JoinUnknownOption(dst, src)
}
Expand Down
46 changes: 45 additions & 1 deletion options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package json

import (
"fmt"
"reflect"

"github.com/go-json-experiment/json/internal"
"github.com/go-json-experiment/json/internal/jsonflags"
Expand Down Expand Up @@ -245,20 +246,47 @@ func WithUnmarshalers(v *Unmarshalers) Options {
return (*unmarshalersOption)(v)
}

// WithOption constructs a user-defined option value.
// Later occurrences of an option of a particular type overrides
// prior occurrences of an option of the exact same type.
// The type T must be a declared type in a package or
// a pointer to such a type.
//
// A user-defined option can be constructed using:
//
// var v mypkg.MyType = ...
// opts := json.WithOption(v)
//
// The option value can be retrieved using:
//
// v, ok := json.GetOption(opts, json.WithOption[mypkg.MyType])
func WithOption[T any](v T) Options {
t := reflect.TypeFor[T]()
if t.PkgPath() == "" && (t.Kind() != reflect.Pointer || t.Elem().PkgPath() == "") {
panic(fmt.Sprintf("%T must a declared type or a pointer to such a type", v))
}
// TODO: Limit this to non-interface types?
return userOption[T]{v}
}

// These option types are declared here instead of "jsonopts"
// to avoid a dependency on "reflect" from "jsonopts".
type (
marshalersOption Marshalers
unmarshalersOption Unmarshalers
userOption[T any] struct{ v T }
)

func (*marshalersOption) JSONOptions(internal.NotForPublicUse) {}
func (*unmarshalersOption) JSONOptions(internal.NotForPublicUse) {}
func (userOption[T]) JSONOptions(internal.NotForPublicUse) {}
func (userOption[T]) key() any { return reflect.TypeFor[T]() }
func (v userOption[T]) val() any { return v.v }

// Inject support into "jsonopts" to handle these types.
func init() {
jsonopts.GetUnknownOption = func(src *jsonopts.Struct, zero jsonopts.Options) (any, bool) {
switch zero.(type) {
switch zero := zero.(type) {
case *marshalersOption:
if !src.Flags.Has(jsonflags.Marshalers) {
return (*Marshalers)(nil), false
Expand All @@ -269,6 +297,14 @@ func init() {
return (*Unmarshalers)(nil), false
}
return src.Unmarshalers.(*Unmarshalers), true
case interface {
key() any
val() any
}: // implemented by [userOption]
if v, ok := src.UserValues[zero.key()]; ok {
return v, true
}
return zero.val(), false
default:
panic(fmt.Sprintf("unknown option %T", zero))
}
Expand All @@ -281,6 +317,14 @@ func init() {
case *unmarshalersOption:
dst.Flags.Set(jsonflags.Unmarshalers | 1)
dst.Unmarshalers = (*Unmarshalers)(src)
case interface {
key() any
val() any
}: // implemented by [userOption]
if dst.UserValues == nil {
dst.UserValues = make(map[any]any)
}
dst.UserValues[src.key()] = src.val()
default:
panic(fmt.Sprintf("unknown option %T", src))
}
Expand Down

0 comments on commit ffbbe55

Please sign in to comment.