From 643cbc4d6e37d07619bec05039da1abb411d28d4 Mon Sep 17 00:00:00 2001 From: Luke Shumaker Date: Tue, 7 Feb 2023 12:45:46 -0700 Subject: Move struct-handling to internal/jsonstruct --- internal/jsonstruct/borrowed_misc.go | 30 +++++ internal/jsonstruct/borrowed_tags.go | 40 +++++++ internal/jsonstruct/struct.go | 205 +++++++++++++++++++++++++++++++++++ internal/jsontags/borrowed_tags.go | 40 ------- internal/jsontags/tags.go | 7 -- 5 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 internal/jsonstruct/borrowed_misc.go create mode 100644 internal/jsonstruct/borrowed_tags.go create mode 100644 internal/jsonstruct/struct.go delete mode 100644 internal/jsontags/borrowed_tags.go delete mode 100644 internal/jsontags/tags.go (limited to 'internal') 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/jsonstruct/borrowed_tags.go b/internal/jsonstruct/borrowed_tags.go new file mode 100644 index 0000000..f2ef71c --- /dev/null +++ b/internal/jsonstruct/borrowed_tags.go @@ -0,0 +1,40 @@ +// Copyright 2011 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 // MODIFIED: changed package name + +import ( + "strings" +) + +// tagOptions is the string following a comma in a struct field's "json" +// tag, or the empty string. It does not include the leading comma. +type tagOptions string + +// parseTag splits a struct field's json tag into its name and +// comma-separated options. +func parseTag(tag string) (string, tagOptions) { + tag, opt, _ := strings.Cut(tag, ",") + return tag, tagOptions(opt) +} + +// Contains reports whether a comma-separated list of options +// contains a particular substr flag. substr must be surrounded by a +// string boundary or commas. +func (o tagOptions) Contains(optionName string) bool { + if len(o) == 0 { + return false + } + s := string(o) + for s != "" { + var name string + name, s, _ = strings.Cut(s, ",") + if name == optionName { + return true + } + } + return false +} 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 +// +// 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/borrowed_tags.go b/internal/jsontags/borrowed_tags.go deleted file mode 100644 index aa94b9b..0000000 --- a/internal/jsontags/borrowed_tags.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2011 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 jsontags // MODIFIED: changed package name - -import ( - "strings" -) - -// tagOptions is the string following a comma in a struct field's "json" -// tag, or the empty string. It does not include the leading comma. -type tagOptions string - -// parseTag splits a struct field's json tag into its name and -// comma-separated options. -func parseTag(tag string) (string, tagOptions) { - tag, opt, _ := strings.Cut(tag, ",") - return tag, tagOptions(opt) -} - -// Contains reports whether a comma-separated list of options -// contains a particular substr flag. substr must be surrounded by a -// string boundary or commas. -func (o tagOptions) Contains(optionName string) bool { - if len(o) == 0 { - return false - } - s := string(o) - for s != "" { - var name string - name, s, _ = strings.Cut(s, ",") - if name == optionName { - return true - } - } - 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 -// -// SPDX-License-Identifier: GPL-2.0-or-later - -package jsontags - -var ParseTag = parseTag -- cgit v1.2.3-2-g168b