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  }