Files
gf/encoding/gjson/gjson.go
John Guo f3f2cb3c57 refactor(encoding/gjson): enhance auto type checks when loading data without type specified (#4637)
This pull request improves YAML support for i18n translation files and
refactors content type detection and loading logic in the `gjson`
package. The main changes include more robust detection of YAML, TOML,
INI, and Properties formats, refactoring of content type handling, and
the addition of new tests to ensure correct parsing of YAML-based i18n
resources.

### Improved content type detection and loading

* Refactored content type detection logic in `gjson` to use dedicated
functions for XML, YAML, TOML, INI, and Properties formats, making the
detection more reliable and maintainable.
* Changed the content loading mechanism in `gjson` to use specific
decode functions (`gxml.Decode`, `gyaml.Decode`, etc.) for each format
instead of converting everything to JSON first, improving accuracy and
extensibility.
* Updated type definitions and struct field comments in `gjson.go` for
clarity and consistency, including changing `ContentType` to a type
alias and improving documentation.
[[1]](diffhunk://#diff-0e4432d7e4cf171c0339e01b1842530432b986948d7f839a155543623236a03fL24-R24)
[[2]](diffhunk://#diff-0e4432d7e4cf171c0339e01b1842530432b986948d7f839a155543623236a03fL38-R71)

### i18n YAML support

* Modified i18n manager to use the new `gjson.LoadPath` method for
loading translation files, ensuring correct parsing of YAML files for
i18n.
* Added new test cases and test data for loading and verifying YAML i18n
files, including edge cases and real-world translation strings.
[[1]](diffhunk://#diff-e6eacc5abab33c149f9b39d8ebe300cf4d0abe907434605991984a5969e8707dR262-R283)
[[2]](diffhunk://#diff-1bfd438797c1f9ef18ab3cb00d23ae95202e85e2362c39c3df4f1a29c55733feR421-R430)
[[3]](diffhunk://#diff-a3ee37ff2a67c9e1ba2e1617e0f5fd63eb261ad7760a07423f703538138c2decR1-R16)

### Minor improvements

* Simplified file loading logic in `gjson.LoadPath` by removing caching
and directly reading file bytes, which streamlines the code and avoids
potential cache issues.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-20 19:25:23 +08:00

490 lines
12 KiB
Go

// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gjson provides convenient API for JSON/XML/INI/YAML/TOML data handling.
package gjson
import (
"reflect"
"strconv"
"strings"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/reflection"
"github.com/gogf/gf/v2/internal/rwmutex"
"github.com/gogf/gf/v2/internal/utils"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
)
type ContentType = string
const (
ContentTypeJSON ContentType = `json`
ContentTypeJs ContentType = `js`
ContentTypeXML ContentType = `xml`
ContentTypeIni ContentType = `ini`
ContentTypeYaml ContentType = `yaml`
ContentTypeYml ContentType = `yml`
ContentTypeToml ContentType = `toml`
ContentTypeProperties ContentType = `properties`
)
const (
// Separator char for hierarchical data access.
defaultSplitChar = '.'
)
// Json is the customized JSON struct.
type Json struct {
mu rwmutex.RWMutex
// Pointer for hierarchical data access, it's the root of data in default.
p *any
// Char separator('.' in default).
c byte
// Violence Check(false in default),
// which is used to access data when the hierarchical data key contains separator char.
vc bool
}
// Options for Json object creating/loading.
type Options struct {
// Mark this object is for in concurrent-safe usage. This is especially for Json object creating.
Safe bool
// Custom priority tags for decoding, eg: "json,yaml,MyTag".
// This is specially for struct parsing into Json object.
Tags string
// Type specifies the data content type, eg: json, xml, yaml, toml, ini.
Type ContentType
// StrNumber causes the Decoder to unmarshal a number into an any as a string instead of as a float64.
// This is specially for json content parsing into Json object.
StrNumber bool
}
// iInterfaces is used for type assert api for Interfaces().
type iInterfaces interface {
Interfaces() []any
}
// iMapStrAny is the interface support for converting struct parameter to map.
type iMapStrAny interface {
MapStrAny() map[string]any
}
// iVal is the interface for underlying any retrieving.
type iVal interface {
Val() any
}
// setValue sets `value` to `j` by `pattern`.
// Note:
// 1. If value is nil and removed is true, means deleting this value;
// 2. It's quite complicated in hierarchical data search, node creating and data assignment;
func (j *Json) setValue(pattern string, value any, removed bool) error {
var (
err error
array = strings.Split(pattern, string(j.c))
length = len(array)
)
if value, err = j.convertValue(value); err != nil {
return err
}
// Initialization checks.
if *j.p == nil {
if gstr.IsNumeric(array[0]) {
*j.p = make([]any, 0)
} else {
*j.p = make(map[string]any)
}
}
var (
pparent *any = nil // Parent pointer.
pointer = j.p // Current pointer.
)
j.mu.Lock()
defer j.mu.Unlock()
for i := 0; i < length; i++ {
switch (*pointer).(type) {
case map[string]any:
if i == length-1 {
if removed && value == nil {
// Delete item from map.
delete((*pointer).(map[string]any), array[i])
} else {
if (*pointer).(map[string]any) == nil {
*pointer = map[string]any{}
}
(*pointer).(map[string]any)[array[i]] = value
}
} else {
// If the key does not exit in the map.
if v, ok := (*pointer).(map[string]any)[array[i]]; !ok {
if removed && value == nil {
goto done
}
// Creating new node.
if gstr.IsNumeric(array[i+1]) {
// Creating array node.
n, _ := strconv.Atoi(array[i+1])
var v any = make([]any, n+1)
pparent = j.setPointerWithValue(pointer, array[i], v)
pointer = &v
} else {
// Creating map node.
var v any = make(map[string]any)
pparent = j.setPointerWithValue(pointer, array[i], v)
pointer = &v
}
} else {
pparent = pointer
pointer = &v
}
}
case []any:
// A string key.
if !gstr.IsNumeric(array[i]) {
if i == length-1 {
*pointer = map[string]any{array[i]: value}
} else {
var v any = make(map[string]any)
*pointer = v
pparent = pointer
pointer = &v
}
continue
}
// Numeric index.
valueNum, err := strconv.Atoi(array[i])
if err != nil {
err = gerror.WrapCodef(gcode.CodeInvalidParameter, err, `strconv.Atoi failed for string "%s"`, array[i])
return err
}
if i == length-1 {
// Leaf node.
if len((*pointer).([]any)) > valueNum {
if removed && value == nil {
// Deleting element.
if pparent == nil {
*pointer = append((*pointer).([]any)[:valueNum], (*pointer).([]any)[valueNum+1:]...)
} else {
j.setPointerWithValue(pparent, array[i-1], append((*pointer).([]any)[:valueNum], (*pointer).([]any)[valueNum+1:]...))
}
} else {
(*pointer).([]any)[valueNum] = value
}
} else {
if removed && value == nil {
goto done
}
if pparent == nil {
// It is the root node.
j.setPointerWithValue(pointer, array[i], value)
} else {
// It is not the root node.
s := make([]any, valueNum+1)
copy(s, (*pointer).([]any))
s[valueNum] = value
j.setPointerWithValue(pparent, array[i-1], s)
}
}
} else {
// Branch node.
if gstr.IsNumeric(array[i+1]) {
n, _ := strconv.Atoi(array[i+1])
pSlice := (*pointer).([]any)
if len(pSlice) > valueNum {
item := pSlice[valueNum]
if s, ok := item.([]any); ok {
for i := 0; i < n-len(s); i++ {
s = append(s, nil)
}
pparent = pointer
pointer = &pSlice[valueNum]
} else {
if removed && value == nil {
goto done
}
var v any = make([]any, n+1)
pparent = j.setPointerWithValue(pointer, array[i], v)
pointer = &v
}
} else {
if removed && value == nil {
goto done
}
var v any = make([]any, n+1)
pparent = j.setPointerWithValue(pointer, array[i], v)
pointer = &v
}
} else {
pSlice := (*pointer).([]any)
if len(pSlice) > valueNum {
pparent = pointer
pointer = &(*pointer).([]any)[valueNum]
} else {
s := make([]any, valueNum+1)
copy(s, pSlice)
s[valueNum] = make(map[string]any)
if pparent != nil {
// i > 0
j.setPointerWithValue(pparent, array[i-1], s)
pparent = pointer
pointer = &s[valueNum]
} else {
// i = 0
var v any = s
*pointer = v
pparent = pointer
pointer = &s[valueNum]
}
}
}
}
// If the variable pointed to by the `pointer` is not of a reference type,
// then it modifies the variable via its the parent, ie: pparent.
default:
if removed && value == nil {
goto done
}
if gstr.IsNumeric(array[i]) {
n, _ := strconv.Atoi(array[i])
s := make([]any, n+1)
if i == length-1 {
s[n] = value
}
if pparent != nil {
pparent = j.setPointerWithValue(pparent, array[i-1], s)
} else {
*pointer = s
pparent = pointer
}
} else {
var v1, v2 any
if i == length-1 {
v1 = map[string]any{
array[i]: value,
}
} else {
v1 = map[string]any{
array[i]: nil,
}
}
if pparent != nil {
pparent = j.setPointerWithValue(pparent, array[i-1], v1)
} else {
*pointer = v1
pparent = pointer
}
v2 = v1.(map[string]any)[array[i]]
pointer = &v2
}
}
}
done:
return nil
}
// convertValue converts `value` to map[string]any or []any,
// which can be supported for hierarchical data access.
func (j *Json) convertValue(value any) (convertedValue any, err error) {
if value == nil {
return
}
switch value.(type) {
case map[string]any:
return value, nil
case []any:
return value, nil
default:
var (
reflectInfo = reflection.OriginValueAndKind(value)
)
switch reflectInfo.OriginKind {
case reflect.Array:
return gconv.Interfaces(value), nil
case reflect.Slice:
return gconv.Interfaces(value), nil
case reflect.Map:
return gconv.Map(value), nil
case reflect.Struct:
if v, ok := value.(iMapStrAny); ok {
convertedValue = v.MapStrAny()
}
if utils.IsNil(convertedValue) {
if v, ok := value.(iInterfaces); ok {
convertedValue = v.Interfaces()
}
}
if utils.IsNil(convertedValue) {
convertedValue = gconv.Map(value)
}
if utils.IsNil(convertedValue) {
err = gerror.NewCodef(gcode.CodeInvalidParameter, `unsupported value type "%s"`, reflect.TypeOf(value))
}
return
default:
return value, nil
}
}
}
// setPointerWithValue sets `key`:`value` to `pointer`, the `key` may be a map key or slice index.
// It returns the pointer to the new value set.
func (j *Json) setPointerWithValue(pointer *any, key string, value any) *any {
switch (*pointer).(type) {
case map[string]any:
(*pointer).(map[string]any)[key] = value
return &value
case []any:
n, _ := strconv.Atoi(key)
if len((*pointer).([]any)) > n {
(*pointer).([]any)[n] = value
return &(*pointer).([]any)[n]
} else {
s := make([]any, n+1)
copy(s, (*pointer).([]any))
s[n] = value
*pointer = s
return &s[n]
}
default:
*pointer = value
}
return pointer
}
// getPointerByPattern returns a pointer to the value by specified `pattern`.
func (j *Json) getPointerByPattern(pattern string) *any {
if j.p == nil {
return nil
}
if j.vc {
return j.getPointerByPatternWithViolenceCheck(pattern)
} else {
return j.getPointerByPatternWithoutViolenceCheck(pattern)
}
}
// getPointerByPatternWithViolenceCheck returns a pointer to the value of specified `pattern` with violence check.
func (j *Json) getPointerByPatternWithViolenceCheck(pattern string) *any {
if !j.vc {
return j.getPointerByPatternWithoutViolenceCheck(pattern)
}
// It returns nil if pattern is empty.
if pattern == "" {
return nil
}
// It returns all if pattern is ".".
if pattern == "." {
return j.p
}
var (
index = len(pattern)
start = 0
length = 0
pointer = j.p
)
if index == 0 {
return pointer
}
for {
if r := j.checkPatternByPointer(pattern[start:index], pointer); r != nil {
if length += index - start; start > 0 {
length += 1
}
start = index + 1
index = len(pattern)
if length == len(pattern) {
return r
} else {
pointer = r
}
} else {
// Get the position for next separator char.
index = strings.LastIndexByte(pattern[start:index], j.c)
if index != -1 && length > 0 {
index += length + 1
}
}
if start >= index {
break
}
}
return nil
}
// getPointerByPatternWithoutViolenceCheck returns a pointer to the value of specified `pattern`, with no violence check.
func (j *Json) getPointerByPatternWithoutViolenceCheck(pattern string) *any {
if j.vc {
return j.getPointerByPatternWithViolenceCheck(pattern)
}
// It returns nil if pattern is empty.
if pattern == "" {
return nil
}
// It returns all if pattern is ".".
if pattern == "." {
return j.p
}
pointer := j.p
if len(pattern) == 0 {
return pointer
}
array := strings.Split(pattern, string(j.c))
for k, v := range array {
if r := j.checkPatternByPointer(v, pointer); r != nil {
if k == len(array)-1 {
return r
} else {
pointer = r
}
} else {
break
}
}
return nil
}
// checkPatternByPointer checks whether there's value by `key` in specified `pointer`.
// It returns a pointer to the value.
func (j *Json) checkPatternByPointer(key string, pointer *any) *any {
switch (*pointer).(type) {
case map[string]any:
if v, ok := (*pointer).(map[string]any)[key]; ok {
return &v
}
case []any:
if gstr.IsNumeric(key) {
n, err := strconv.Atoi(key)
if err == nil && len((*pointer).([]any)) > n {
return &(*pointer).([]any)[n]
}
}
}
return nil
}