Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+83
-81
@@ -29,10 +29,12 @@ type isZeroer interface {
|
||||
var isZeroerType = reflect.TypeFor[isZeroer]()
|
||||
|
||||
type structFields struct {
|
||||
flattened []structField // listed in depth-first ordering
|
||||
byActualName map[string]*structField
|
||||
byFoldedName map[string][]*structField
|
||||
inlinedFallback *structField
|
||||
flattened []structField // listed in depth-first ordering
|
||||
byActualName map[string]*structField
|
||||
byFoldedName map[string][]*structField
|
||||
embeddedFallback *structField
|
||||
|
||||
errUnsupportedFormat *SemanticError
|
||||
}
|
||||
|
||||
// reindex recomputes index to avoid bounds check during runtime.
|
||||
@@ -51,8 +53,8 @@ func (sf *structFields) reindex() {
|
||||
for i := range sf.flattened {
|
||||
reindex(&sf.flattened[i])
|
||||
}
|
||||
if sf.inlinedFallback != nil {
|
||||
reindex(sf.inlinedFallback)
|
||||
if sf.embeddedFallback != nil {
|
||||
reindex(sf.embeddedFallback)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,25 +82,25 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
return cmp.Or(serr, &SemanticError{GoType: t, Err: fmt.Errorf(f, a...)})
|
||||
}
|
||||
|
||||
// Setup a queue for a breath-first search.
|
||||
// Setup a queue for a breadth-first search.
|
||||
var queueIndex int
|
||||
type queueEntry struct {
|
||||
typ reflect.Type
|
||||
index []int
|
||||
visitChildren bool // whether to recursively visit inlined field in this struct
|
||||
visitChildren bool // whether to recursively visit embedded field in this struct
|
||||
}
|
||||
queue := []queueEntry{{root, nil, true}}
|
||||
seen := map[reflect.Type]bool{root: true}
|
||||
|
||||
// Perform a breadth-first search over all reachable fields.
|
||||
// This ensures that len(f.index) will be monotonically increasing.
|
||||
var allFields, inlinedFallbacks []structField
|
||||
var allFields, embeddedFallbacks []structField
|
||||
for queueIndex < len(queue) {
|
||||
qe := queue[queueIndex]
|
||||
queueIndex++
|
||||
|
||||
t := qe.typ
|
||||
inlinedFallbackIndex := -1 // index of last inlined fallback field in current struct
|
||||
embeddedFallbackIndex := -1 // index of last embedded fallback field in current struct
|
||||
namesIndex := make(map[string]int) // index of each field with a given JSON object name in current struct
|
||||
var hasAnyJSONTag bool // whether any Go struct field has a `json` tag
|
||||
var hasAnyJSONField bool // whether any JSON serializable fields exist in current struct
|
||||
@@ -126,80 +128,73 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
if indirectType(f.typ).Kind() != reflect.Struct {
|
||||
serr = orErrorf(serr, t, "embedded Go struct field %s of non-struct type must be explicitly given a JSON name", sf.Name)
|
||||
} else {
|
||||
f.inline = true // implied by use of Go embedding without an explicit name
|
||||
f.embed = true // implied by use of Go embedding without an explicit name
|
||||
}
|
||||
}
|
||||
if f.inline || f.unknown {
|
||||
// Handle an inlined field that serializes to/from
|
||||
|
||||
var handleEmbed, handleField func()
|
||||
handleEmbed = func() {
|
||||
// Handle an embedded field that serializes to/from
|
||||
// zero or more JSON object members.
|
||||
|
||||
switch f.fieldOptions {
|
||||
case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true}:
|
||||
case fieldOptions{name: f.name, quotedName: f.quotedName, unknown: true}:
|
||||
case fieldOptions{name: f.name, quotedName: f.quotedName, inline: true, unknown: true}:
|
||||
serr = orErrorf(serr, t, "Go struct field %s cannot have both `inline` and `unknown` specified", sf.Name)
|
||||
f.inline = false // let `unknown` take precedence
|
||||
default:
|
||||
serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `inline` or `unknown` specified", sf.Name)
|
||||
if f.fieldOptions != (fieldOptions{name: f.name, quotedName: f.quotedName, embed: true}) {
|
||||
serr = orErrorf(serr, t, "Go struct field %s cannot have any options other than `embed` specified", sf.Name)
|
||||
if f.hasName {
|
||||
continue // invalid inlined field; treat as ignored
|
||||
}
|
||||
f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, inline: f.inline, unknown: f.unknown}
|
||||
if f.inline && f.unknown {
|
||||
f.inline = false // let `unknown` take precedence
|
||||
handleField()
|
||||
return // invalid embedded field; treat as regular field
|
||||
}
|
||||
f.fieldOptions = fieldOptions{name: f.name, quotedName: f.quotedName, embed: f.embed}
|
||||
}
|
||||
|
||||
// Reject any types with custom serialization otherwise
|
||||
// it becomes impossible to know what sub-fields to inline.
|
||||
// it becomes impossible to know what sub-fields to embed.
|
||||
tf := indirectType(f.typ)
|
||||
if implementsAny(tf, allMethodTypes...) && tf != jsontextValueType {
|
||||
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf)
|
||||
serr = orErrorf(serr, t, "embedded Go struct field %s of type %s must not implement marshal or unmarshal methods", sf.Name, tf)
|
||||
}
|
||||
|
||||
// Handle an inlined field that serializes to/from
|
||||
// Handle an embedded field that serializes to/from
|
||||
// a finite number of JSON object members backed by a Go struct.
|
||||
if tf.Kind() == reflect.Struct {
|
||||
if f.unknown {
|
||||
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value", sf.Name, tf)
|
||||
continue // invalid inlined field; treat as ignored
|
||||
}
|
||||
if qe.visitChildren {
|
||||
queue = append(queue, queueEntry{tf, f.index, !seen[tf]})
|
||||
}
|
||||
seen[tf] = true
|
||||
continue
|
||||
return
|
||||
} else if !sf.IsExported() {
|
||||
serr = orErrorf(serr, t, "inlined Go struct field %s is not exported", sf.Name)
|
||||
continue // invalid inlined field; treat as ignored
|
||||
serr = orErrorf(serr, t, "embedded Go struct field %s is not exported", sf.Name)
|
||||
return // invalid embedded field; treat as ignored
|
||||
}
|
||||
|
||||
// Handle an inlined field that serializes to/from any number of
|
||||
// Handle an embedded field that serializes to/from any number of
|
||||
// JSON object members back by a Go map or jsontext.Value.
|
||||
switch {
|
||||
case tf == jsontextValueType:
|
||||
f.fncs = nil // specially handled in arshal_inlined.go
|
||||
f.fncs = nil // specially handled in arshal_embedded.go
|
||||
case tf.Kind() == reflect.Map && tf.Key().Kind() == reflect.String:
|
||||
if implementsAny(tf.Key(), allMethodTypes...) {
|
||||
serr = orErrorf(serr, t, "inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf)
|
||||
continue // invalid inlined field; treat as ignored
|
||||
serr = orErrorf(serr, t, "embedded map field %s of type %s must have a string key that does not implement marshal or unmarshal methods", sf.Name, tf)
|
||||
handleField()
|
||||
return // invalid embedded field; treat as regular field
|
||||
}
|
||||
f.fncs = lookupArshaler(tf.Elem())
|
||||
default:
|
||||
serr = orErrorf(serr, t, "inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf)
|
||||
continue // invalid inlined field; treat as ignored
|
||||
serr = orErrorf(serr, t, "embedded Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value", sf.Name, tf)
|
||||
handleField()
|
||||
return // invalid embedded field; treat as regular field
|
||||
}
|
||||
|
||||
// Reject multiple inlined fallback fields within the same struct.
|
||||
if inlinedFallbackIndex >= 0 {
|
||||
serr = orErrorf(serr, t, "inlined Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(inlinedFallbackIndex).Name, sf.Name)
|
||||
// Still append f to inlinedFallbacks as there is still a
|
||||
// check for a dominant inlined fallback before returning.
|
||||
// Reject multiple embedded fallback fields within the same struct.
|
||||
if embeddedFallbackIndex >= 0 {
|
||||
serr = orErrorf(serr, t, "embedded Go struct fields %s and %s cannot both be a Go map or jsontext.Value", t.Field(embeddedFallbackIndex).Name, sf.Name)
|
||||
// Still append f to embeddedFallbacks as there is still a
|
||||
// check for a dominant embedded fallback before returning.
|
||||
}
|
||||
inlinedFallbackIndex = i
|
||||
embeddedFallbackIndex = i
|
||||
|
||||
inlinedFallbacks = append(inlinedFallbacks, f)
|
||||
} else {
|
||||
embeddedFallbacks = append(embeddedFallbacks, f)
|
||||
}
|
||||
handleField = func() {
|
||||
// Handle normal Go struct field that serializes to/from
|
||||
// a single JSON object member.
|
||||
|
||||
@@ -210,14 +205,14 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
tf := indirectType(f.typ)
|
||||
if !(sf.Anonymous && tf.Kind() == reflect.Struct) {
|
||||
serr = orErrorf(serr, t, "Go struct field %s is not exported", sf.Name)
|
||||
continue
|
||||
return
|
||||
}
|
||||
// Unfortunately, methods on the unexported field
|
||||
// still cannot be called.
|
||||
if implementsAny(tf, allMethodTypes...) ||
|
||||
(f.omitzero && implementsAny(tf, isZeroerType)) {
|
||||
serr = orErrorf(serr, t, "Go struct field %s is not exported for method calls", sf.Name)
|
||||
continue
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +255,15 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
f.id = len(allFields)
|
||||
f.fncs = lookupArshaler(sf.Type)
|
||||
allFields = append(allFields, f)
|
||||
if f.format != "" && fs.errUnsupportedFormat == nil {
|
||||
fs.errUnsupportedFormat = &SemanticError{GoType: t, Err: fmt.Errorf("Go struct field %s has unsupported `format` tag option", sf.Name)}
|
||||
}
|
||||
}
|
||||
|
||||
if f.embed {
|
||||
handleEmbed()
|
||||
} else {
|
||||
handleField()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,6 +329,8 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
flattened: flattened,
|
||||
byActualName: make(map[string]*structField, len(flattened)),
|
||||
byFoldedName: make(map[string][]*structField, len(flattened)),
|
||||
|
||||
errUnsupportedFormat: fs.errUnsupportedFormat,
|
||||
}
|
||||
for i, f := range fs.flattened {
|
||||
foldedName := string(foldName([]byte(f.name)))
|
||||
@@ -333,7 +339,7 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
}
|
||||
for foldedName, fields := range fs.byFoldedName {
|
||||
if len(fields) > 1 {
|
||||
// The precedence order for conflicting ignoreCase names
|
||||
// The precedence order for conflicting case-insensitive names
|
||||
// is by breadth-first order, rather than depth-first order.
|
||||
slices.SortFunc(fields, func(x, y *structField) int {
|
||||
return cmp.Compare(x.id, y.id)
|
||||
@@ -341,8 +347,8 @@ func makeStructFields(root reflect.Type) (fs structFields, serr *SemanticError)
|
||||
fs.byFoldedName[foldedName] = fields
|
||||
}
|
||||
}
|
||||
if n := len(inlinedFallbacks); n == 1 || (n > 1 && len(inlinedFallbacks[0].index) != len(inlinedFallbacks[1].index)) {
|
||||
fs.inlinedFallback = &inlinedFallbacks[0] // dominant inlined fallback field
|
||||
if n := len(embeddedFallbacks); n == 1 || (n > 1 && len(embeddedFallbacks[0].index) != len(embeddedFallbacks[1].index)) {
|
||||
fs.embeddedFallback = &embeddedFallbacks[0] // dominant embedded fallback field
|
||||
}
|
||||
fs.reindex()
|
||||
return fs, serr
|
||||
@@ -391,8 +397,7 @@ type fieldOptions struct {
|
||||
hasName bool
|
||||
nameNeedEscape bool
|
||||
casing int8 // either 0, caseIgnore, or caseStrict
|
||||
inline bool
|
||||
unknown bool
|
||||
embed bool
|
||||
omitzero bool
|
||||
omitempty bool
|
||||
string bool
|
||||
@@ -404,7 +409,6 @@ type fieldOptions struct {
|
||||
// the JSON member name and other features.
|
||||
func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) {
|
||||
tag, hasTag := sf.Tag.Lookup("json")
|
||||
tagOrig := tag
|
||||
|
||||
// Check whether this field is explicitly ignored.
|
||||
if tag == "-" {
|
||||
@@ -428,10 +432,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
return fieldOptions{}, true, err
|
||||
}
|
||||
|
||||
// Determine the JSON member name for this Go field. A user-specified name
|
||||
// may be provided as either an identifier or a single-quoted string.
|
||||
// The single-quoted string allows arbitrary characters in the name.
|
||||
// See https://go.dev/issue/2718 and https://go.dev/issue/3546.
|
||||
// Determine the JSON member name for this Go field.
|
||||
out.name = sf.Name // always starts with an uppercase character
|
||||
if len(tag) > 0 && !strings.HasPrefix(tag, ",") {
|
||||
// For better compatibility with v1, accept almost any unescaped name.
|
||||
@@ -445,7 +446,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
// In either case, call consumeTagOption to handle it further.
|
||||
var err2 error
|
||||
if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) {
|
||||
name, n, err2 = consumeTagOption(tag)
|
||||
name, n, err2 = consumeTagOption(tag, false)
|
||||
if err2 != nil {
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
|
||||
}
|
||||
@@ -454,22 +455,15 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name))
|
||||
name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError
|
||||
}
|
||||
if name == "-" && tag[0] == '-' {
|
||||
defer func() { // defer to let other errors take precedence
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q; either "+
|
||||
"use `json:\"-\"` to ignore the field or "+
|
||||
"use `json:\"'-'%s` to specify %q as the name", sf.Name, out.name, strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name))
|
||||
}()
|
||||
}
|
||||
if err2 == nil {
|
||||
out.hasName = true
|
||||
out.name = name
|
||||
}
|
||||
tag = tag[n:]
|
||||
}
|
||||
b, _ := jsonwire.AppendQuote(nil, out.name, &jsonflags.Flags{})
|
||||
b, _ := jsonwire.AppendQuote(nil, []byte(out.name), &jsonflags.Flags{})
|
||||
out.quotedName = string(b)
|
||||
out.nameNeedEscape = jsonwire.NeedEscape(out.name)
|
||||
out.nameNeedEscape = jsonwire.NeedEscape([]byte(out.name))
|
||||
|
||||
// Handle any additional tag options (if any).
|
||||
var wasFormat bool
|
||||
@@ -487,7 +481,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
}
|
||||
|
||||
// Consume and process the tag option.
|
||||
opt, n, err2 := consumeTagOption(tag)
|
||||
opt, n, err2 := consumeTagOption(tag, false)
|
||||
if err2 != nil {
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed `json` tag: %v", sf.Name, err2))
|
||||
}
|
||||
@@ -506,7 +500,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
break
|
||||
}
|
||||
tag = tag[len(":"):]
|
||||
opt, n, err2 := consumeTagOption(tag)
|
||||
opt, n, err2 := consumeTagOption(tag, false)
|
||||
if err2 != nil {
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `case` tag option: %v", sf.Name, err2))
|
||||
break
|
||||
@@ -524,10 +518,8 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
default:
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has unknown `case:%s` tag value", sf.Name, rawOpt))
|
||||
}
|
||||
case "inline":
|
||||
out.inline = true
|
||||
case "unknown":
|
||||
out.unknown = true
|
||||
case "embed":
|
||||
out.embed = true
|
||||
case "omitzero":
|
||||
out.omitzero = true
|
||||
case "omitempty":
|
||||
@@ -540,10 +532,13 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
break
|
||||
}
|
||||
tag = tag[len(":"):]
|
||||
opt, n, err2 := consumeTagOption(tag)
|
||||
opt, n, err2 := consumeTagOption(tag, true)
|
||||
if err2 != nil {
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has malformed value for `format` tag option: %v", sf.Name, err2))
|
||||
break
|
||||
} else if opt == "" {
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s cannot have empty value for `format` tag option", sf.Name))
|
||||
break
|
||||
}
|
||||
tag = tag[n:]
|
||||
out.format = opt
|
||||
@@ -553,7 +548,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
// This catches invalid mutants such as "omitEmpty" or "omit_empty".
|
||||
normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "")
|
||||
switch normOpt {
|
||||
case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format":
|
||||
case "case", "embed", "omitzero", "omitempty", "string", "format":
|
||||
err = cmp.Or(err, fmt.Errorf("Go struct field %s has invalid appearance of `%s` tag option; specify `%s` instead", sf.Name, opt, normOpt))
|
||||
}
|
||||
|
||||
@@ -578,7 +573,7 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
|
||||
// which is either a Go identifier or a single-quoted string.
|
||||
// If the next option is invalid, it returns all of in until the next comma,
|
||||
// and reports an error.
|
||||
func consumeTagOption(in string) (string, int, error) {
|
||||
func consumeTagOption(in string, allowQuoted bool) (string, int, error) {
|
||||
// For legacy compatibility with v1, assume options are comma-separated.
|
||||
i := strings.IndexByte(in, ',')
|
||||
if i < 0 {
|
||||
@@ -592,6 +587,10 @@ func consumeTagOption(in string) (string, int, error) {
|
||||
return in[:n], n, nil
|
||||
// Option as a single-quoted string.
|
||||
case r == '\'':
|
||||
if !allowQuoted {
|
||||
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter)", r)
|
||||
}
|
||||
|
||||
// The grammar is nearly identical to a double-quoted Go string literal,
|
||||
// but uses single quotes as the terminators. The reason for a custom
|
||||
// grammar is because both backtick and double quotes cannot be used
|
||||
@@ -633,6 +632,9 @@ func consumeTagOption(in string) (string, int, error) {
|
||||
case len(in) == 0:
|
||||
return in[:i], i, io.ErrUnexpectedEOF
|
||||
default:
|
||||
if !allowQuoted {
|
||||
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter)", r)
|
||||
}
|
||||
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user