Skip to content

Commit

Permalink
Add support for named strings as inlined map keys
Browse files Browse the repository at this point in the history
There isn't a good reason why map[~string]T fields cannot be inlined, unlike map[string]T.
This allows map[K]T fields, where K is a ~string type, to be inlined.
  • Loading branch information
nickkhyl committed Oct 14, 2024
1 parent ebd3a89 commit f9a9eeb
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 13 deletions.
11 changes: 7 additions & 4 deletions arshal_inlined.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,12 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j
}
return nil
} else {
m := v // must be a map[string]V
m := v // must be a map[~string]V
n := m.Len()
if n == 0 {
return nil
}
mk := newAddressableValue(stringType)
mk := newAddressableValue(m.Type().Key())
mv := newAddressableValue(m.Type().Elem())
marshalKey := func(mk addressableValue) error {
xe := export.Encoder(enc)
Expand Down Expand Up @@ -202,12 +202,15 @@ func unmarshalInlinedFallbackNext(dec *jsontext.Decoder, va addressableValue, uo
} else {
name := string(unquotedName) // TODO: Intern this?

m := v // must be a map[string]V
m := v // must be a map[~string]V
if m.IsNil() {
m.Set(reflect.MakeMap(m.Type()))
}
mk := reflect.ValueOf(name)
mv := newAddressableValue(v.Type().Elem()) // TODO: Cache across calls?
if mkt := m.Type().Key(); mkt != stringType {
mk = mk.Convert(mkt)
}
mv := newAddressableValue(m.Type().Elem()) // TODO: Cache across calls?
if v2 := m.MapIndex(mk); v2.IsValid() {
mv.Set(v2)
}
Expand Down
183 changes: 183 additions & 0 deletions arshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,14 @@ type (
structInlineMapStringInt struct {
X map[string]int `json:",inline"`
}
structInlineMapNamedStringInt struct {
X map[namedString]int `json:",inline"`
}
structInlineMapNamedStringAny struct {
A int `json:",omitzero"`
X map[namedString]any `json:",inline"`
B int `json:",omitzero"`
}
structNoCaseInlineTextValue struct {
AAA string `json:",omitempty,strictcase"`
AA_b string `json:",omitempty"`
Expand Down Expand Up @@ -2432,6 +2440,72 @@ func TestMarshal(t *testing.T) {
},
want: `{"one":"1","two":"2","zero":"0"}`,
canonicalize: true,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt"),
in: structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 1, "two": 2},
},
want: `{"one":1,"two":2,"zero":0}`,
canonicalize: true,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt/Deterministic"),
opts: []Options{Deterministic(true)},
in: structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 1, "two": 2},
},
want: `{"one":1,"two":2,"zero":0}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/Nil"),
in: structInlineMapNamedStringAny{X: nil},
want: `{}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/Empty"),
in: structInlineMapNamedStringAny{X: make(map[namedString]any)},
want: `{}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/NonEmptyN1"),
in: structInlineMapNamedStringAny{X: map[namedString]any{"fizz": nil}},
want: `{"fizz":null}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/NonEmptyN2"),
in: structInlineMapNamedStringAny{X: map[namedString]any{"fizz": time.Time{}, "buzz": math.Pi}},
want: `{"buzz":3.141592653589793,"fizz":"0001-01-01T00:00:00Z"}`,
canonicalize: true,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/NonEmptyWithOthers"),
in: structInlineMapNamedStringAny{
A: 1,
X: map[namedString]any{"fizz": nil},
B: 2,
},
// NOTE: Inlined fallback fields are always serialized last.
want: `{"A":1,"B":2,"fizz":null}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/RejectInvalidUTF8"),
opts: []Options{jsontext.AllowInvalidUTF8(false)},
in: structInlineMapNamedStringAny{X: map[namedString]any{"\xde\xad\xbe\xef": nil}},
want: `{`,
wantErr: export.NewInvalidUTF8Error(0),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/AllowInvalidUTF8"),
opts: []Options{jsontext.AllowInvalidUTF8(true)},
in: structInlineMapNamedStringAny{X: map[namedString]any{"\xde\xad\xbe\xef": nil}},
want: `{"ޭ��":null}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/InvalidValue"),
opts: []Options{jsontext.AllowInvalidUTF8(true)},
in: structInlineMapNamedStringAny{X: map[namedString]any{"name": make(chan string)}},
want: `{"name"`,
wantErr: &SemanticError{action: "marshal", GoType: chanStringType},
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MarshalFuncV1"),
opts: []Options{
WithMarshalers(MarshalFuncV1(func(v float64) ([]byte, error) {
return []byte(fmt.Sprintf(`"%v"`, v)), nil
})),
},
in: structInlineMapNamedStringAny{X: map[namedString]any{"fizz": 3.14159}},
want: `{"fizz":"3.14159"}`,
}, {
name: jsontest.Name("Structs/InlinedFallback/DiscardUnknownMembers"),
opts: []Options{DiscardUnknownMembers(true)},
Expand Down Expand Up @@ -6340,6 +6414,115 @@ func TestUnmarshal(t *testing.T) {
want: addr(structInlineMapStringInt{
X: map[string]int{"zero": 0, "one": 1, "two": 2},
}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt"),
inBuf: `{"zero": 0, "one": 1, "two": 2}`,
inVal: new(structInlineMapNamedStringInt),
want: addr(structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 1, "two": 2},
}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt/Null"),
inBuf: `{"zero": 0, "one": null, "two": 2}`,
inVal: new(structInlineMapNamedStringInt),
want: addr(structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 0, "two": 2},
}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt/Invalid"),
inBuf: `{"zero": 0, "one": {}, "two": 2}`,
inVal: new(structInlineMapNamedStringInt),
want: addr(structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 0},
}),
wantErr: &SemanticError{action: "unmarshal", JSONKind: '{', GoType: intType},
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt/StringifiedNumbers"),
opts: []Options{StringifyNumbers(true)},
inBuf: `{"zero": 0, "one": "1", "two": 2}`,
inVal: new(structInlineMapNamedStringInt),
want: addr(structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 1, "two": 2},
}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringInt/UnmarshalFuncV1"),
opts: []Options{
WithUnmarshalers(UnmarshalFuncV1(func(b []byte, v *int) error {
i, err := strconv.ParseInt(string(bytes.Trim(b, `"`)), 10, 64)
if err != nil {
return err
}
*v = int(i)
return nil
})),
},
inBuf: `{"zero": "0", "one": "1", "two": "2"}`,
inVal: new(structInlineMapNamedStringInt),
want: addr(structInlineMapNamedStringInt{
X: map[namedString]int{"zero": 0, "one": 1, "two": 2},
}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/Noop"),
inBuf: `{"A":1,"B":2}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: nil, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeN1/Nil"),
inBuf: `{"A":1,"fizz":"buzz","B":2}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": "buzz"}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeN1/Empty"),
inBuf: `{"A":1,"fizz":"buzz","B":2}`,
inVal: addr(structInlineMapNamedStringAny{X: map[namedString]any{}}),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": "buzz"}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeN1/ObjectN1"),
inBuf: `{"A":1,"fizz":{"charlie":"DELTA","echo":"foxtrot"},"B":2}`,
inVal: addr(structInlineMapNamedStringAny{X: map[namedString]any{"fizz": jsonObject{
"alpha": "bravo",
"charlie": "delta",
}}}),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": jsonObject{
"alpha": "bravo",
"charlie": "DELTA",
"echo": "foxtrot",
}}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeN2/ObjectN1"),
inBuf: `{"A":1,"fizz":"buzz","B":2,"foo": [ 1 , 2 , 3 ]}`,
inVal: addr(structInlineMapNamedStringAny{X: map[namedString]any{"fizz": "wuzz"}}),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": "buzz", "foo": jsonArray{1.0, 2.0, 3.0}}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeInvalidValue"),
inBuf: `{"A":1,"fizz":nil,"B":2}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": nil}}),
wantErr: export.NewInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`)),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/MergeInvalidValue/Existing"),
inBuf: `{"A":1,"fizz":nil,"B":2}`,
inVal: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": true}}),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": true}}),
wantErr: export.NewInvalidCharacterError("i", "within literal null (expecting 'u')", len64(`{"A":1,"fizz":n`)),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/CaseSensitive"),
inBuf: `{"A":1,"fizz":"buzz","B":2,"a":3}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": "buzz", "a": 3.0}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/RejectDuplicateNames"),
opts: []Options{jsontext.AllowDuplicateNames(false)},
inBuf: `{"A":1,"fizz":"buzz","B":2,"fizz":"buzz"}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": "buzz"}, B: 2}),
wantErr: export.NewDuplicateNameError([]byte(`"fizz"`), len64(`{"A":1,"fizz":"buzz","B":2,`)),
}, {
name: jsontest.Name("Structs/InlinedFallback/MapNamedStringAny/AllowDuplicateNames"),
opts: []Options{jsontext.AllowDuplicateNames(true)},
inBuf: `{"A":1,"fizz":{"one":1,"two":-2},"B":2,"fizz":{"two":2,"three":3}}`,
inVal: new(structInlineMapNamedStringAny),
want: addr(structInlineMapNamedStringAny{A: 1, X: map[namedString]any{"fizz": map[string]any{"one": 1.0, "two": 2.0, "three": 3.0}}, B: 2}),
}, {
name: jsontest.Name("Structs/InlinedFallback/RejectUnknownMembers"),
opts: []Options{RejectUnknownMembers(true)},
Expand Down
4 changes: 2 additions & 2 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@
// A Go embedded field is implicitly inlined unless an explicit JSON name
// is specified. The inlined field must be a Go struct
// (that does not implement any JSON methods), [jsontext.Value],
// map[string]T, or an unnamed pointer to such types. When marshaling,
// map[~string]T, or an unnamed pointer to such types. When marshaling,
// inlined fields from a pointer type are omitted if it is nil.
// Inlined fields of type [jsontext.Value] and map[string]T are called
// Inlined fields of type [jsontext.Value] and map[~string]T are called
// “inlined fallbacks” as they can represent all possible
// JSON object members not directly handled by the parent struct.
// Only one inlined fallback field may be specified in a struct,
Expand Down
2 changes: 1 addition & 1 deletion fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func makeStructFields(root reflect.Type) (structFields, *SemanticError) {
switch {
case tf == jsontextValueType:
f.fncs = nil // specially handled in arshal_inlined.go
case tf.Kind() == reflect.Map && tf.Key() == stringType:
case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String:
f.fncs = lookupArshaler(tf.Elem())
default:
err := fmt.Errorf("inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf)
Expand Down
6 changes: 0 additions & 6 deletions fields_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,12 +280,6 @@ func TestMakeStructFields(t *testing.T) {
A map[int]any `json:",unknown"`
}{},
wantErr: errors.New(`inlined Go struct field A of type map[int]interface {} must be a Go struct, Go map of string key, or jsontext.Value`),
}, {
name: jsontest.Name("InlineUnsupported/MapNamedStringKey"),
in: struct {
A map[namedString]any `json:",inline"`
}{},
wantErr: errors.New(`inlined Go struct field A of type map[json.namedString]interface {} must be a Go struct, Go map of string key, or jsontext.Value`),
}, {
name: jsontest.Name("InlineUnsupported/DoublePointer"),
in: struct {
Expand Down

0 comments on commit f9a9eeb

Please sign in to comment.