// Copyright (C) 2022-2023 Luke Shumaker // // SPDX-License-Identifier: GPL-2.0-or-later package lowmemjson import ( "reflect" "git.lukeshu.com/go/typedsync" "git.lukeshu.com/go/lowmemjson/internal" ) type structField struct { Name string Path []int Tagged bool OmitEmpty bool Quote bool } // A structIndex is used by Decoder.Decode() and Encoder.Encode() when // decoding-to or encoding-from a struct. type structIndex struct { byPos []structField byName map[string]int } var structIndexCache typedsync.CacheMap[reflect.Type, structIndex] // indexStruct takes a struct Type, and indexes its fields for use by // Decoder.Decode() and Encoder.Encode(). indexStruct caches its // results. func indexStruct(typ reflect.Type) structIndex { ret, _ := structIndexCache.LoadOrCompute(typ, indexStructReal) return ret } // indexStructReal is like indexStruct, but is the real indexer, // bypassing the cache. func indexStructReal(typ reflect.Type) structIndex { var byPos []structField byName := make(map[string][]int) indexStructInner(typ, &byPos, byName, nil, map[reflect.Type]struct{}{}) ret := structIndex{ byName: make(map[string]int), } for curPos, _field := range byPos { name := _field.Name fieldPoss := byName[name] switch len(fieldPoss) { case 0: // do nothing case 1: ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, _field) 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(byPos[fieldPoss[0]].Path) for _, fieldPos := range fieldPoss[1:] { field := byPos[fieldPos] if len(field.Path) < leastLevel { leastLevel = len(field.Path) } } var numUntagged, numTagged int var untaggedPos, taggedPos int for _, fieldPos := range fieldPoss { field := byPos[fieldPos] if len(field.Path) != leastLevel { continue } if field.Tagged { numTagged++ taggedPos = fieldPos if numTagged > 1 { break // optimization } } else { numUntagged++ untaggedPos = fieldPos } } switch numTagged { case 0: switch numUntagged { case 0: // do nothing case 1: if curPos == untaggedPos { ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, byPos[curPos]) } } case 1: if curPos == taggedPos { ret.byName[name] = len(ret.byPos) ret.byPos = append(ret.byPos, byPos[curPos]) } } } } return ret } // indexStructInner crawls the struct `typ`, storing information on // all struct fields foun in to `byPos` and `byName`. If `typ` // contains other structs as fields, indexStructInner will recurse and // call itself; keeping track of stack information with `stackPath` // (which identifies where we are in the parent struct) and // `stackSeen` (which is used for detecting loops). func indexStructInner(typ reflect.Type, byPos *[]structField, byName map[string][]int, stackPath []int, stackSeen map[reflect.Type]struct{}) { if _, ok := stackSeen[typ]; ok { return } stackSeen[typ] = struct{}{} defer delete(stackSeen, typ) n := typ.NumField() for i := 0; i < n; i++ { stackPath := append(stackPath, 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 := internal.ParseTag(tag) name := tagName if !isValidTag(name) { name = "" } if name == "" { name = fTyp.Name } if embed && tagName == "" { t := fTyp.Type if t.Kind() == reflect.Pointer { t = t.Elem() } indexStructInner(t, byPos, byName, stackPath, stackSeen) } else { byName[name] = append(byName[name], len(*byPos)) *byPos = append(*byPos, structField{ Name: name, Path: append([]int(nil), stackPath...), Tagged: tagName != "", OmitEmpty: opts.Contains("omitempty"), Quote: opts.Contains("string") && isQuotable(fTyp.Type), }) } } } // isQuotable returns whether a type is eligible for `json:,string` // quoting. 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 } }