mirror of
https://gitee.com/johng/gf
synced 2026-06-06 16:21:40 +08:00
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>
490 lines
12 KiB
Go
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
|
|
}
|