mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-03 23:36:29 +00:00
use experimental go json v2 library (#35392)
details: https://pkg.go.dev/encoding/json/v2 --------- Co-authored-by: techknowlogick <matti@mdranta.net> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -365,11 +365,11 @@ func GenerateEmbedBindata(fsRootPath, outputFile string) error {
|
||||
if err = embedFiles(meta.Root, fsRootPath, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonBuf, err := json.Marshal(meta) // can't use json.NewEncoder here because it writes extra EOL
|
||||
jsonBuf, err := json.Marshal(meta)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = output.Write([]byte{'\n'})
|
||||
_, err = output.Write(jsonBuf)
|
||||
_, err = output.Write(bytes.TrimSpace(jsonBuf))
|
||||
return err
|
||||
}
|
||||
|
@@ -32,8 +32,7 @@ type Interface interface {
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultJSONHandler default json handler
|
||||
DefaultJSONHandler Interface = JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
|
||||
DefaultJSONHandler = getDefaultJSONHandler()
|
||||
|
||||
_ Interface = StdJSON{}
|
||||
_ Interface = JSONiter{}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -16,3 +17,12 @@ func TestGiteaDBJSONUnmarshal(t *testing.T) {
|
||||
err = UnmarshalHandleDoubleEncode([]byte(""), &m)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestIndent(t *testing.T) {
|
||||
buf := &bytes.Buffer{}
|
||||
err := Indent(buf, []byte(`{"a":1}`), ">", " ")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `{
|
||||
> "a": 1
|
||||
>}`, buf.String())
|
||||
}
|
||||
|
24
modules/json/jsonlegacy.go
Normal file
24
modules/json/jsonlegacy.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !goexperiment.jsonv2
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func getDefaultJSONHandler() Interface {
|
||||
return JSONiter{jsoniter.ConfigCompatibleWithStandardLibrary}
|
||||
}
|
||||
|
||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||
return DefaultJSONHandler.Marshal(v)
|
||||
}
|
||||
|
||||
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
|
||||
return DefaultJSONHandler.NewDecoder(reader)
|
||||
}
|
92
modules/json/jsonv2.go
Normal file
92
modules/json/jsonv2.go
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build goexperiment.jsonv2
|
||||
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
jsonv1 "encoding/json" //nolint:depguard // this package wraps it
|
||||
jsonv2 "encoding/json/v2" //nolint:depguard // this package wraps it
|
||||
"io"
|
||||
)
|
||||
|
||||
// JSONv2 implements Interface via encoding/json/v2
|
||||
// Requires GOEXPERIMENT=jsonv2 to be set at build time
|
||||
type JSONv2 struct {
|
||||
marshalOptions jsonv2.Options
|
||||
marshalKeepOptionalEmptyOptions jsonv2.Options
|
||||
unmarshalOptions jsonv2.Options
|
||||
unmarshalCaseInsensitiveOptions jsonv2.Options
|
||||
}
|
||||
|
||||
var jsonV2 JSONv2
|
||||
|
||||
func init() {
|
||||
commonMarshalOptions := []jsonv2.Options{
|
||||
jsonv2.FormatNilSliceAsNull(true),
|
||||
jsonv2.FormatNilMapAsNull(true),
|
||||
}
|
||||
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
|
||||
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
|
||||
|
||||
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
|
||||
// v1 has a different behavior: if the `""` is from a null pointer, or a Marshal function, it is kept.
|
||||
// Golang issue: https://github.com/golang/go/issues/75623 encoding/json/v2: unable to make omitempty work with pointer or Optional type with goexperiment.jsonv2
|
||||
jsonV2.marshalKeepOptionalEmptyOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsonv1.OmitEmptyWithLegacySemantics(true))...)
|
||||
|
||||
// Some legacy code uses case-insensitive matching (for example: parsing oci.ImageConfig)
|
||||
jsonV2.unmarshalCaseInsensitiveOptions = jsonv2.JoinOptions(jsonv2.MatchCaseInsensitiveNames(true))
|
||||
}
|
||||
|
||||
func getDefaultJSONHandler() Interface {
|
||||
return &jsonV2
|
||||
}
|
||||
|
||||
func MarshalKeepOptionalEmpty(v any) ([]byte, error) {
|
||||
return jsonv2.Marshal(v, jsonV2.marshalKeepOptionalEmptyOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) Marshal(v any) ([]byte, error) {
|
||||
return jsonv2.Marshal(v, j.marshalOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) Unmarshal(data []byte, v any) error {
|
||||
return jsonv2.Unmarshal(data, v, j.unmarshalOptions)
|
||||
}
|
||||
|
||||
func (j *JSONv2) NewEncoder(writer io.Writer) Encoder {
|
||||
return &jsonV2Encoder{writer: writer, opts: j.marshalOptions}
|
||||
}
|
||||
|
||||
func (j *JSONv2) NewDecoder(reader io.Reader) Decoder {
|
||||
return &jsonV2Decoder{reader: reader, opts: j.unmarshalOptions}
|
||||
}
|
||||
|
||||
// Indent implements Interface using standard library (JSON v2 doesn't have Indent yet)
|
||||
func (*JSONv2) Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
|
||||
return jsonv1.Indent(dst, src, prefix, indent)
|
||||
}
|
||||
|
||||
type jsonV2Encoder struct {
|
||||
writer io.Writer
|
||||
opts jsonv2.Options
|
||||
}
|
||||
|
||||
func (e *jsonV2Encoder) Encode(v any) error {
|
||||
return jsonv2.MarshalWrite(e.writer, v, e.opts)
|
||||
}
|
||||
|
||||
type jsonV2Decoder struct {
|
||||
reader io.Reader
|
||||
opts jsonv2.Options
|
||||
}
|
||||
|
||||
func (d *jsonV2Decoder) Decode(v any) error {
|
||||
return jsonv2.UnmarshalRead(d.reader, v, d.opts)
|
||||
}
|
||||
|
||||
func NewDecoderCaseInsensitive(reader io.Reader) Decoder {
|
||||
return &jsonV2Decoder{reader: reader, opts: jsonV2.unmarshalCaseInsensitiveOptions}
|
||||
}
|
@@ -193,7 +193,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
||||
},
|
||||
{
|
||||
endpoint: "https://invalid-json-response.io",
|
||||
expectedError: "invalid json",
|
||||
expectedError: "/(invalid json|jsontext: invalid character)/",
|
||||
},
|
||||
{
|
||||
endpoint: "https://valid-batch-request-download.io",
|
||||
@@ -258,7 +258,11 @@ func TestHTTPClientDownload(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
if c.expectedError != "" {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
|
||||
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
|
||||
} else {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
@@ -297,7 +301,7 @@ func TestHTTPClientUpload(t *testing.T) {
|
||||
},
|
||||
{
|
||||
endpoint: "https://invalid-json-response.io",
|
||||
expectedError: "invalid json",
|
||||
expectedError: "/(invalid json|jsontext: invalid character)/",
|
||||
},
|
||||
{
|
||||
endpoint: "https://valid-batch-request-upload.io",
|
||||
@@ -352,7 +356,11 @@ func TestHTTPClientUpload(t *testing.T) {
|
||||
return io.NopCloser(new(bytes.Buffer)), objectError
|
||||
})
|
||||
if c.expectedError != "" {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
if strings.HasPrefix(c.expectedError, "/") && strings.HasSuffix(c.expectedError, "/") {
|
||||
assert.Regexp(t, strings.Trim(c.expectedError, "/"), err.Error())
|
||||
} else {
|
||||
assert.ErrorContains(t, err, c.expectedError)
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@@ -15,12 +15,17 @@ import (
|
||||
)
|
||||
|
||||
type testSerializationStruct struct {
|
||||
NormalString string `json:"normal_string" yaml:"normal_string"`
|
||||
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
|
||||
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
|
||||
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
|
||||
NormalString string `json:"normal_string" yaml:"normal_string"`
|
||||
NormalBool bool `json:"normal_bool" yaml:"normal_bool"`
|
||||
OptBool optional.Option[bool] `json:"optional_bool,omitempty" yaml:"optional_bool,omitempty"`
|
||||
|
||||
// It causes an undefined behavior: should the "omitempty" tag only omit "null", or also the empty string?
|
||||
// The behavior is inconsistent between json and v2 packages, and there is no such use case in Gitea.
|
||||
// If anyone really needs it, they can use json.MarshalKeepOptionalEmpty to revert the v1 behavior
|
||||
OptString optional.Option[string] `json:"optional_string,omitempty" yaml:"optional_string,omitempty"`
|
||||
|
||||
OptTwoBool optional.Option[bool] `json:"optional_two_bool" yaml:"optional_two_bool"`
|
||||
OptTwoString optional.Option[string] `json:"optional_twostring" yaml:"optional_two_string"`
|
||||
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
|
||||
}
|
||||
|
||||
func TestOptionalToJson(t *testing.T) {
|
||||
@@ -32,7 +37,7 @@ func TestOptionalToJson(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
obj: new(testSerializationStruct),
|
||||
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_twostring":null}`,
|
||||
want: `{"normal_string":"","normal_bool":false,"optional_two_bool":null,"optional_two_string":null}`,
|
||||
},
|
||||
{
|
||||
name: "some",
|
||||
@@ -44,12 +49,12 @@ func TestOptionalToJson(t *testing.T) {
|
||||
OptTwoBool: optional.None[bool](),
|
||||
OptTwoString: optional.None[string](),
|
||||
},
|
||||
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
|
||||
want: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
b, err := json.Marshal(tc.obj)
|
||||
b, err := json.MarshalKeepOptionalEmpty(tc.obj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tc.want, string(b), "gitea json module returned unexpected")
|
||||
|
||||
@@ -75,7 +80,7 @@ func TestOptionalFromJson(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "some",
|
||||
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_twostring":null}`,
|
||||
data: `{"normal_string":"a string","normal_bool":true,"optional_bool":false,"optional_string":"","optional_two_bool":null,"optional_two_string":null}`,
|
||||
want: testSerializationStruct{
|
||||
NormalString: "a string",
|
||||
NormalBool: true,
|
||||
@@ -169,7 +174,7 @@ normal_bool: true
|
||||
optional_bool: false
|
||||
optional_string: ""
|
||||
optional_two_bool: null
|
||||
optional_twostring: null
|
||||
optional_two_string: null
|
||||
`,
|
||||
want: testSerializationStruct{
|
||||
NormalString: "a string",
|
||||
|
@@ -103,7 +103,9 @@ func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
|
||||
|
||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
||||
var image oci.Image
|
||||
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
||||
// FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive
|
||||
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
|
||||
if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@@ -22,6 +22,8 @@ func TestParseImageConfig(t *testing.T) {
|
||||
repositoryURL := "https://gitea.com/gitea"
|
||||
documentationURL := "https://docs.gitea.com"
|
||||
|
||||
// FIXME: JSON-KEY-CASE: the test case is not right, the config fields are capitalized in the spec
|
||||
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
|
||||
configOCI := `{"config": {"labels": {"` + labelAuthors + `": "` + author + `", "` + labelLicenses + `": "` + license + `", "` + labelURL + `": "` + projectURL + `", "` + labelSource + `": "` + repositoryURL + `", "` + labelDocumentation + `": "` + documentationURL + `", "` + labelDescription + `": "` + description + `"}}, "history": [{"created_by": "do it 1"}, {"created_by": "dummy #(nop) do it 2"}]}`
|
||||
|
||||
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
|
||||
|
Reference in New Issue
Block a user