git.lukeshu.com/go/lowmemjson@v0.3.9-0.20230723050957-72f6d13f6fb2/internal/jsonstruct/struct.go (about) 1 // Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com> 2 // 3 // SPDX-License-Identifier: GPL-2.0-or-later 4 5 package jsonstruct 6 7 import ( 8 "reflect" 9 10 "git.lukeshu.com/go/typedsync" 11 ) 12 13 var ParseTag = parseTag 14 15 type StructField struct { 16 Name string 17 Path []int 18 Tagged bool 19 OmitEmpty bool 20 Quote bool 21 } 22 23 // A StructIndex is used by Decoder.Decode() and Encoder.Encode() when 24 // decoding-to or encoding-from a struct. 25 type StructIndex struct { 26 ByPos []StructField 27 ByName map[string]int 28 } 29 30 var structIndexCache typedsync.CacheMap[reflect.Type, StructIndex] 31 32 // IndexStruct takes a struct Type, and indexes its fields for use by 33 // Decoder.Decode() and Encoder.Encode(). indexStruct caches its 34 // results. 35 func IndexStruct(typ reflect.Type) StructIndex { 36 ret, _ := structIndexCache.LoadOrCompute(typ, indexStructReal) 37 return ret 38 } 39 40 func ClearCache() { 41 structIndexCache = typedsync.CacheMap[reflect.Type, StructIndex]{} 42 } 43 44 // indexStructReal is like indexStruct, but is the real indexer, 45 // bypassing the cache. 46 func indexStructReal(typ reflect.Type) StructIndex { 47 var byPos []StructField 48 byName := make(map[string][]int) 49 50 indexStructInner(typ, &byPos, byName, nil, map[reflect.Type]struct{}{}) 51 52 ret := StructIndex{ 53 ByName: make(map[string]int), 54 } 55 56 for curPos, _field := range byPos { 57 name := _field.Name 58 fieldPoss := byName[name] 59 switch len(fieldPoss) { 60 case 0: 61 // do nothing 62 case 1: 63 ret.ByName[name] = len(ret.ByPos) 64 ret.ByPos = append(ret.ByPos, _field) 65 default: 66 // To quote the encoding/json docs (version 1.20): 67 // 68 // If there are multiple fields at the same level, and that level is the 69 // least nested (and would therefore be the nesting level selected by the 70 // usual Go rules), the following extra rules apply: 71 // 72 // 1) Of those fields, if any are JSON-tagged, only tagged fields are 73 // considered, even if there are multiple untagged fields that would 74 // otherwise conflict. 75 // 76 // 2) If there is exactly one field (tagged or not according to the first 77 // rule), that is selected. 78 // 79 // 3) Otherwise there are multiple fields, and all are ignored; no error 80 // occurs. 81 leastLevel := len(byPos[fieldPoss[0]].Path) 82 for _, fieldPos := range fieldPoss[1:] { 83 field := byPos[fieldPos] 84 if len(field.Path) < leastLevel { 85 leastLevel = len(field.Path) 86 } 87 } 88 var numUntagged, numTagged int 89 var untaggedPos, taggedPos int 90 for _, fieldPos := range fieldPoss { 91 field := byPos[fieldPos] 92 if len(field.Path) != leastLevel { 93 continue 94 } 95 if field.Tagged { 96 numTagged++ 97 taggedPos = fieldPos 98 if numTagged > 1 { 99 break // optimization 100 } 101 } else { 102 numUntagged++ 103 untaggedPos = fieldPos 104 } 105 } 106 switch numTagged { 107 case 0: 108 switch numUntagged { 109 case 0: 110 // do nothing 111 case 1: 112 if curPos == untaggedPos { 113 ret.ByName[name] = len(ret.ByPos) 114 ret.ByPos = append(ret.ByPos, byPos[curPos]) 115 } 116 } 117 case 1: 118 if curPos == taggedPos { 119 ret.ByName[name] = len(ret.ByPos) 120 ret.ByPos = append(ret.ByPos, byPos[curPos]) 121 } 122 } 123 } 124 } 125 126 return ret 127 } 128 129 // indexStructInner crawls the struct `typ`, storing information on 130 // all struct fields found in to `byPos` and `byName`. If `typ` 131 // contains other structs as fields, indexStructInner will recurse and 132 // call itself; keeping track of stack information with `stackPath` 133 // (which identifies where we are in the parent struct) and 134 // `stackSeen` (which is used for detecting loops). 135 func indexStructInner(typ reflect.Type, byPos *[]StructField, byName map[string][]int, stackPath []int, stackSeen map[reflect.Type]struct{}) { 136 if _, ok := stackSeen[typ]; ok { 137 return 138 } 139 stackSeen[typ] = struct{}{} 140 defer delete(stackSeen, typ) 141 142 n := typ.NumField() 143 for i := 0; i < n; i++ { 144 stackPath := append(stackPath, i) 145 146 fTyp := typ.Field(i) 147 var embed bool 148 if fTyp.Anonymous { 149 t := fTyp.Type 150 if t.Kind() == reflect.Pointer { 151 t = t.Elem() 152 } 153 if !fTyp.IsExported() && t.Kind() != reflect.Struct { 154 continue 155 } 156 embed = t.Kind() == reflect.Struct 157 } else if !fTyp.IsExported() { 158 continue 159 } 160 tag := fTyp.Tag.Get("json") 161 if tag == "-" { 162 continue 163 } 164 tagName, opts := parseTag(tag) 165 name := tagName 166 if !isValidTag(name) { 167 name = "" 168 } 169 if name == "" { 170 name = fTyp.Name 171 } 172 173 if embed && tagName == "" { 174 t := fTyp.Type 175 if t.Kind() == reflect.Pointer { 176 t = t.Elem() 177 } 178 indexStructInner(t, byPos, byName, stackPath, stackSeen) 179 } else { 180 byName[name] = append(byName[name], len(*byPos)) 181 *byPos = append(*byPos, StructField{ 182 Name: name, 183 Path: append([]int(nil), stackPath...), 184 Tagged: tagName != "", 185 OmitEmpty: opts.Contains("omitempty"), 186 Quote: opts.Contains("string") && isQuotable(fTyp.Type), 187 }) 188 } 189 } 190 } 191 192 // isQuotable returns whether a type is eligible for `json:,string` 193 // quoting. 194 func isQuotable(typ reflect.Type) bool { 195 for typ.Kind() == reflect.Pointer { 196 typ = typ.Elem() 197 } 198 switch typ.Kind() { 199 case reflect.Bool, 200 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, 201 reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, 202 reflect.Uintptr, 203 reflect.Float32, reflect.Float64, 204 reflect.String: 205 return true 206 default: 207 return false 208 } 209 }