mirror of
https://github.com/zeromicro/go-zero.git
synced 2026-05-10 16:30:01 +08:00
feat: add JSON5 configuration support (#5433)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -21,10 +21,11 @@ const (
|
||||
var (
|
||||
fillDefaultUnmarshaler = mapping.NewUnmarshaler(jsonTagKey, mapping.WithDefault())
|
||||
loaders = map[string]func([]byte, any) error{
|
||||
".json": LoadFromJsonBytes,
|
||||
".toml": LoadFromTomlBytes,
|
||||
".yaml": LoadFromYamlBytes,
|
||||
".yml": LoadFromYamlBytes,
|
||||
".json": LoadFromJsonBytes,
|
||||
".json5": LoadFromJson5Bytes,
|
||||
".toml": LoadFromTomlBytes,
|
||||
".yaml": LoadFromYamlBytes,
|
||||
".yml": LoadFromYamlBytes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ func FillDefault(v any) error {
|
||||
return fillDefaultUnmarshaler.Unmarshal(map[string]any{}, v)
|
||||
}
|
||||
|
||||
// Load loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||
// Load loads config into v from file, .json, .json5, .toml, .yaml and .yml are acceptable.
|
||||
func Load(file string, v any, opts ...Option) error {
|
||||
content, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
@@ -65,7 +66,7 @@ func Load(file string, v any, opts ...Option) error {
|
||||
return loader(content, v)
|
||||
}
|
||||
|
||||
// LoadConfig loads config into v from file, .json, .yaml and .yml are acceptable.
|
||||
// LoadConfig loads config into v from file, .json, .json5, .toml, .yaml and .yml are acceptable.
|
||||
// Deprecated: use Load instead.
|
||||
func LoadConfig(file string, v any, opts ...Option) error {
|
||||
return Load(file, v, opts...)
|
||||
@@ -119,6 +120,16 @@ func LoadFromYamlBytes(content []byte, v any) error {
|
||||
return LoadFromJsonBytes(b, v)
|
||||
}
|
||||
|
||||
// LoadFromJson5Bytes loads config into v from content json5 bytes.
|
||||
func LoadFromJson5Bytes(content []byte, v any) error {
|
||||
b, err := encoding.Json5ToJson(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return LoadFromJsonBytes(b, v)
|
||||
}
|
||||
|
||||
// LoadConfigFromYamlBytes loads config into v from content yaml bytes.
|
||||
// Deprecated: use LoadFromYamlBytes instead.
|
||||
func LoadConfigFromYamlBytes(content []byte, v any) error {
|
||||
|
||||
@@ -75,6 +75,160 @@ func TestLoadFromJsonBytesArray(t *testing.T) {
|
||||
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
||||
}
|
||||
|
||||
func TestConfigJson5(t *testing.T) {
|
||||
// JSON5 with comments, trailing commas, and unquoted keys
|
||||
text := `{
|
||||
// This is a comment
|
||||
a: 'foo', // single quotes
|
||||
b: 1,
|
||||
c: "${FOO}",
|
||||
d: "abcd!@#$112", // trailing comma
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "${FOO}", val.C)
|
||||
assert.Equal(t, "abcd!@#$112", val.D)
|
||||
}
|
||||
|
||||
func TestConfigJsonStandardParser(t *testing.T) {
|
||||
// Standard JSON uses standard JSON parser (not JSON5) for backward compatibility
|
||||
text := `{
|
||||
"a": "foo",
|
||||
"b": 1,
|
||||
"c": "${FOO}",
|
||||
"d": "abcd!@#$112"
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "${FOO}", val.C)
|
||||
assert.Equal(t, "abcd!@#$112", val.D)
|
||||
}
|
||||
|
||||
func TestConfigJsonLargeIntegers(t *testing.T) {
|
||||
// Test that .json files preserve large integer precision (backward compatibility)
|
||||
text := `{
|
||||
"id": 1234567890123456789,
|
||||
"timestamp": 9223372036854775807
|
||||
}`
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
ID int64 `json:"id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
MustLoad(tmpfile, &val)
|
||||
assert.Equal(t, int64(1234567890123456789), val.ID)
|
||||
assert.Equal(t, int64(9223372036854775807), val.Timestamp)
|
||||
}
|
||||
|
||||
func TestConfigJson5Env(t *testing.T) {
|
||||
text := `{
|
||||
// Comment with env variable
|
||||
a: "foo",
|
||||
b: 1,
|
||||
c: "${FOO}",
|
||||
d: "abcd!@#$a12 3",
|
||||
}`
|
||||
t.Setenv("FOO", "2")
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
A string `json:"a"`
|
||||
B int `json:"b"`
|
||||
C string `json:"c"`
|
||||
D string `json:"d"`
|
||||
}
|
||||
MustLoad(tmpfile, &val, UseEnv())
|
||||
assert.Equal(t, "foo", val.A)
|
||||
assert.Equal(t, 1, val.B)
|
||||
assert.Equal(t, "2", val.C)
|
||||
assert.Equal(t, "abcd!@# 3", val.D)
|
||||
}
|
||||
|
||||
func TestLoadFromJson5Bytes(t *testing.T) {
|
||||
// Test JSON5 features: comments, trailing commas, single quotes, unquoted keys
|
||||
input := []byte(`{
|
||||
// This is a comment
|
||||
users: [
|
||||
{name: 'foo'}, // trailing comma
|
||||
{Name: "bar"},
|
||||
],
|
||||
}`)
|
||||
var val struct {
|
||||
Users []struct {
|
||||
Name string
|
||||
}
|
||||
}
|
||||
|
||||
assert.NoError(t, LoadFromJson5Bytes(input, &val))
|
||||
var expect []string
|
||||
for _, user := range val.Users {
|
||||
expect = append(expect, user.Name)
|
||||
}
|
||||
assert.EqualValues(t, []string{"foo", "bar"}, expect)
|
||||
}
|
||||
|
||||
func TestLoadFromJson5BytesError(t *testing.T) {
|
||||
// Invalid JSON5 syntax
|
||||
input := []byte(`{a: foo}`) // unquoted string value (invalid)
|
||||
var val struct {
|
||||
A string
|
||||
}
|
||||
|
||||
assert.Error(t, LoadFromJson5Bytes(input, &val))
|
||||
}
|
||||
|
||||
func TestConfigJson5LargeIntegersLimitation(t *testing.T) {
|
||||
// Document that JSON5 has precision limitations for large integers (>2^53)
|
||||
// due to JavaScript number semantics. Users should use .json for configs with large IDs.
|
||||
text := `{
|
||||
// JSON5 converts numbers to float64, which loses precision for large integers
|
||||
id: 1234567890123456789
|
||||
}`
|
||||
|
||||
tmpfile, err := createTempFile(t, ".json5", text)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var val struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
// This will load; depending on the JSON5 implementation, large integers may lose precision.
|
||||
// This test documents that behavior without requiring loss of precision as an invariant.
|
||||
err = Load(tmpfile, &val)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Logf("loaded JSON5 large integer id=%d (original 1234567890123456789)", val.ID)
|
||||
}
|
||||
|
||||
func TestConfigToml(t *testing.T) {
|
||||
text := `a = "foo"
|
||||
b = 1
|
||||
|
||||
Reference in New Issue
Block a user