summaryrefslogtreecommitdiff
path: root/struct.go
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@lukeshu.com>2022-08-13 15:11:17 -0600
committerLuke Shumaker <lukeshu@lukeshu.com>2022-08-13 15:18:11 -0600
commit234e0836f1040f7724251b4120a2351bcbf64131 (patch)
tree201b30fcc99eed470ae345a9bbe594f7ee7a1178 /struct.go
parentf2769bd863521cf316ec9237a498bfa4ecaa115f (diff)
set up as a separate repo
Diffstat (limited to 'struct.go')
-rw-r--r--struct.go164
1 files changed, 164 insertions, 0 deletions
diff --git a/struct.go b/struct.go
new file mode 100644
index 0000000..ad142d6
--- /dev/null
+++ b/struct.go
@@ -0,0 +1,164 @@
+// Copyright (C) 2022 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// 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
+ }
+}