summaryrefslogtreecommitdiff
path: root/struct.go
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2023-02-07 12:45:46 -0700
committerLuke Shumaker <lukeshu@lukeshu.com>2023-02-07 14:05:37 -0700
commit643cbc4d6e37d07619bec05039da1abb411d28d4 (patch)
tree68f771d5103d0243ed49b21ff896f01e49a81a72 /struct.go
parent2b9473f5e8816eeea76b2fdada184532be00d3a2 (diff)
Move struct-handling to internal/jsonstruct
Diffstat (limited to 'struct.go')
-rw-r--r--struct.go205
1 files changed, 0 insertions, 205 deletions
diff --git a/struct.go b/struct.go
deleted file mode 100644
index 5ccb62f..0000000
--- a/struct.go
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
-//
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package lowmemjson
-
-import (
- "reflect"
-
- "git.lukeshu.com/go/typedsync"
-
- "git.lukeshu.com/go/lowmemjson/internal/jsontags"
-)
-
-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 found 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 := jsontags.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
- }
-}