summaryrefslogtreecommitdiff
path: root/internal
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 /internal
parent2b9473f5e8816eeea76b2fdada184532be00d3a2 (diff)
Move struct-handling to internal/jsonstruct
Diffstat (limited to 'internal')
-rw-r--r--internal/jsonstruct/borrowed_misc.go30
-rw-r--r--internal/jsonstruct/borrowed_tags.go (renamed from internal/jsontags/borrowed_tags.go)2
-rw-r--r--internal/jsonstruct/struct.go205
-rw-r--r--internal/jsontags/tags.go7
4 files changed, 236 insertions, 8 deletions
diff --git a/internal/jsonstruct/borrowed_misc.go b/internal/jsonstruct/borrowed_misc.go
new file mode 100644
index 0000000..3b4181e
--- /dev/null
+++ b/internal/jsonstruct/borrowed_misc.go
@@ -0,0 +1,30 @@
+// Copyright 2010 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+package jsonstruct
+
+import (
+ "strings"
+ "unicode"
+)
+
+// isValidTag is borrowed from encode.go.
+func isValidTag(s string) bool {
+ if s == "" {
+ return false
+ }
+ for _, c := range s {
+ switch {
+ case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
+ // Backslash and quote chars are reserved, but
+ // otherwise any punctuation chars are allowed
+ // in a tag name.
+ case !unicode.IsLetter(c) && !unicode.IsDigit(c):
+ return false
+ }
+ }
+ return true
+}
diff --git a/internal/jsontags/borrowed_tags.go b/internal/jsonstruct/borrowed_tags.go
index aa94b9b..f2ef71c 100644
--- a/internal/jsontags/borrowed_tags.go
+++ b/internal/jsonstruct/borrowed_tags.go
@@ -4,7 +4,7 @@
//
// SPDX-License-Identifier: BSD-3-Clause
-package jsontags // MODIFIED: changed package name
+package jsonstruct // MODIFIED: changed package name
import (
"strings"
diff --git a/internal/jsonstruct/struct.go b/internal/jsonstruct/struct.go
new file mode 100644
index 0000000..830dc80
--- /dev/null
+++ b/internal/jsonstruct/struct.go
@@ -0,0 +1,205 @@
+// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
+//
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package jsonstruct
+
+import (
+ "reflect"
+
+ "git.lukeshu.com/go/typedsync"
+)
+
+var ParseTag = parseTag
+
+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 := 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
+ }
+}
diff --git a/internal/jsontags/tags.go b/internal/jsontags/tags.go
deleted file mode 100644
index 386824d..0000000
--- a/internal/jsontags/tags.go
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright (C) 2022-2023 Luke Shumaker <lukeshu@lukeshu.com>
-//
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package jsontags
-
-var ParseTag = parseTag