// Copyright (C) 2022 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package lowmemjson import ( "reflect" ) type structField struct { Name string Path []int Tagged bool OmitEmpty bool Quote bool } type structIndex struct { byPos []structField byName map[string]int } func indexStruct(typ reflect.Type) structIndex { byName := make(map[string][]structField) var byPos []string indexStructInner(typ, nil, byName, &byPos) ret := structIndex{ byName: make(map[string]int), } for _, name := range byPos { fields := byName[name] delete(byName, name) switch len(fields) { case 0: // do nothing case 1: ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, fields[0]) default: // To quote the encoding/json docs (version 1.18.4): // // If there are multiple fields at the same level, and that level is the // least nested (and would therefore be the nesting level selected by the // usual Go rules), the following extra rules apply: // // 1) Of those fields, if any are JSON-tagged, only tagged fields are // considered, even if there are multiple untagged fields that would // otherwise conflict. // // 2) If there is exactly one field (tagged or not according to the first // rule), that is selected. // // 3) Otherwise there are multiple fields, and all are ignored; no error // occurs. leastLevel := len(fields[0].Path) for _, field := range fields[1:] { if len(field.Path) < leastLevel { leastLevel = len(field.Path) } } var numUntagged, numTagged int var untaggedIdx, taggedIdx int for i, field := range fields { if len(field.Path) != leastLevel { continue } if field.Tagged { numTagged++ taggedIdx = i if numTagged > 1 { break // optimization } } else { numUntagged++ untaggedIdx = i } } switch numTagged { case 0: switch numUntagged { case 0: // do nothing case 1: ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, fields[untaggedIdx]) } case 1: ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, fields[taggedIdx]) } } } return ret } func indexStructInner(typ reflect.Type, prefix []int, byName map[string][]structField, byPos *[]string) { n := typ.NumField() for i := 0; i < n; i++ { path := append(append([]int(nil), prefix...), i) fTyp := typ.Field(i) var embed bool if fTyp.Anonymous { t := fTyp.Type if t.Kind() == reflect.Pointer { t = t.Elem() } if !fTyp.IsExported() && t.Kind() != reflect.Struct { continue } embed = t.Kind() == reflect.Struct } else if !fTyp.IsExported() { continue } tag := fTyp.Tag.Get("json") if tag == "-" { continue } tagName, opts := parseTag(tag) name := tagName if name == "" { name = fTyp.Name } if embed { t := fTyp.Type if t.Kind() == reflect.Pointer { t = t.Elem() } indexStructInner(t, path, byName, byPos) } else { byName[name] = append(byName[name], structField{ Name: name, Path: path, Tagged: tagName != "", OmitEmpty: opts.Contains("omitempty"), Quote: opts.Contains("string") && isQuotable(fTyp.Type), }) *byPos = append(*byPos, name) } } } func isQuotable(typ reflect.Type) bool { for typ.Kind() == reflect.Pointer { typ = typ.Elem() } switch typ.Kind() { case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.String: return true default: return false } }