This commit is contained in:
yusing
2026-02-16 08:59:01 +08:00
parent 15b9635ee1
commit e4e6f6b3e8
242 changed files with 3953 additions and 3502 deletions

View File

@@ -43,12 +43,12 @@ type SerializedObject = map[string]any
```go
// For custom map unmarshaling logic
type MapUnmarshaller interface {
UnmarshalMap(m map[string]any) gperr.Error
UnmarshalMap(m map[string]any) error
}
// For custom validation logic
type CustomValidator interface {
Validate() gperr.Error
Validate() error
}
```
@@ -56,16 +56,16 @@ type CustomValidator interface {
```go
// Generic unmarshal with pluggable format handler
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) error
// Read from io.Reader with format decoder
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) error
// Direct map deserialization
func MapUnmarshalValidate(src SerializedObject, dst any) gperr.Error
func MapUnmarshalValidate(src SerializedObject, dst any) error
// To xsync.Map with pluggable format handler
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error)
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], error)
```
### File I/O Functions
@@ -82,23 +82,23 @@ func LoadFileIfExist[T any](path string, dst *T, unmarshaler unmarshalFunc) erro
```go
// Convert any value to target reflect.Value
func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error
func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) error
// String to target type conversion
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error)
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr error)
```
### Validation Functions
```go
// Validate using struct tags
func ValidateWithFieldTags(s any) gperr.Error
func ValidateWithFieldTags(s any) error
// Register custom validator
func MustRegisterValidation(tag string, fn validator.Func)
// Validate using CustomValidator interface
func ValidateWithCustomValidator(v reflect.Value) gperr.Error
func ValidateWithCustomValidator(v reflect.Value) error
// Get underlying validator
func Validator() *validator.Validate
@@ -301,9 +301,9 @@ type Config struct {
URL string `json:"url" validate:"required"`
}
func (c *Config) Validate() gperr.Error {
func (c *Config) Validate() error {
if !strings.HasPrefix(c.URL, "https://") {
return gperr.New("url must use https").Subject("url")
return errors.New("url must use https")
}
return nil
}

View File

@@ -2,11 +2,12 @@ package serialization_test
import (
"bytes"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/yusing/godoxy/internal/serialization"
gperr "github.com/yusing/goutils/errs"
)
type TestStruct struct {
@@ -14,18 +15,17 @@ type TestStruct struct {
Value2 int `json:"value2"`
}
func (t *TestStruct) Validate() gperr.Error {
func (t *TestStruct) Validate() error {
if t.Value == "" {
return gperr.New("value is required")
return errors.New("value is required")
}
if t.Value2 != 0 && (t.Value2 < 5 || t.Value2 > 10) {
return gperr.New("value2 must be between 5 and 10")
return errors.New("value2 must be between 5 and 10")
}
return nil
}
func TestGinBinding(t *testing.T) {
tests := []struct {
name string
input string
@@ -40,7 +40,7 @@ func TestGinBinding(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
var dst TestStruct
body := bytes.NewBufferString(tt.input)
req := httptest.NewRequest("POST", "/", body)
req := httptest.NewRequest(http.MethodPost, "/", body)
err := serialization.GinJSONBinding{}.Bind(req, &dst)
if (err != nil) != tt.wantErr {
t.Errorf("%s: Bind() error = %v, wantErr %v", tt.name, err, tt.wantErr)

View File

@@ -15,8 +15,10 @@ func NewSubstituteEnvReader(reader io.Reader) *SubstituteEnvReader {
return &SubstituteEnvReader{reader: reader}
}
const peekSize = 4096
const maxVarNameLength = 256
const (
peekSize = 4096
maxVarNameLength = 256
)
func (r *SubstituteEnvReader) Read(p []byte) (n int, err error) {
// Return buffered data first
@@ -66,6 +68,7 @@ func (r *SubstituteEnvReader) Read(p []byte) (n int, err error) {
if nMore > 0 {
incomplete = append(incomplete, more[:nMore]...)
// Check if pattern is now complete
//nolint:modernize
if idx := bytes.IndexByte(incomplete, '}'); idx >= 0 {
// Pattern complete, append the rest back to chunk
chunk = append(chunk, incomplete...)

View File

@@ -2,8 +2,8 @@ package serialization
import (
"bytes"
"errors"
"io"
"os"
"strings"
"testing"
)
@@ -11,17 +11,9 @@ import (
// setupEnv sets up environment variables for benchmarks
func setupEnv(b *testing.B) {
b.Helper()
os.Setenv("BENCH_VAR", "benchmark_value")
os.Setenv("BENCH_VAR_2", "second_value")
os.Setenv("BENCH_VAR_3", "third_value")
}
// cleanupEnv cleans up environment variables after benchmarks
func cleanupEnv(b *testing.B) {
b.Helper()
os.Unsetenv("BENCH_VAR")
os.Unsetenv("BENCH_VAR_2")
os.Unsetenv("BENCH_VAR_3")
b.Setenv("BENCH_VAR", "benchmark_value")
b.Setenv("BENCH_VAR_2", "second_value")
b.Setenv("BENCH_VAR_3", "third_value")
}
// BenchmarkSubstituteEnvReader_NoSubstitution benchmarks reading without any env substitutions
@@ -44,7 +36,6 @@ data: some content here
// BenchmarkSubstituteEnvReader_SingleSubstitution benchmarks reading with a single env substitution
func BenchmarkSubstituteEnvReader_SingleSubstitution(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
r := strings.NewReader(`key: ${BENCH_VAR}
`)
@@ -62,7 +53,6 @@ func BenchmarkSubstituteEnvReader_SingleSubstitution(b *testing.B) {
// BenchmarkSubstituteEnvReader_MultipleSubstitutions benchmarks reading with multiple env substitutions
func BenchmarkSubstituteEnvReader_MultipleSubstitutions(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
r := strings.NewReader(`key1: ${BENCH_VAR}
key2: ${BENCH_VAR_2}
@@ -96,7 +86,6 @@ func BenchmarkSubstituteEnvReader_LargeInput_NoSubstitution(b *testing.B) {
// BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions benchmarks large input with scattered substitutions
func BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
var builder bytes.Buffer
for range 100 {
@@ -118,7 +107,6 @@ func BenchmarkSubstituteEnvReader_LargeInput_WithSubstitutions(b *testing.B) {
// BenchmarkSubstituteEnvReader_SmallBuffer benchmarks reading with a small buffer size
func BenchmarkSubstituteEnvReader_SmallBuffer(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
r := strings.NewReader(`key: ${BENCH_VAR} and some more content here`)
buf := make([]byte, 16)
@@ -127,7 +115,7 @@ func BenchmarkSubstituteEnvReader_SmallBuffer(b *testing.B) {
reader := NewSubstituteEnvReader(r)
for {
_, err := reader.Read(buf)
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@@ -141,7 +129,6 @@ func BenchmarkSubstituteEnvReader_SmallBuffer(b *testing.B) {
// BenchmarkSubstituteEnvReader_YAMLConfig benchmarks a realistic YAML config scenario
func BenchmarkSubstituteEnvReader_YAMLConfig(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
r := strings.NewReader(`database:
host: ${BENCH_VAR}
@@ -170,7 +157,6 @@ server:
// BenchmarkSubstituteEnvReader_BoundaryPattern benchmarks patterns at buffer boundaries (4096 bytes)
func BenchmarkSubstituteEnvReader_BoundaryPattern(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
// Pattern exactly at 4090 bytes, with ${VAR} crossing the 4096 boundary
prefix := strings.Repeat("x", 4090)
@@ -189,7 +175,6 @@ func BenchmarkSubstituteEnvReader_BoundaryPattern(b *testing.B) {
// BenchmarkSubstituteEnvReader_MultipleBoundaries benchmarks multiple patterns crossing boundaries
func BenchmarkSubstituteEnvReader_MultipleBoundaries(b *testing.B) {
setupEnv(b)
defer cleanupEnv(b)
var builder bytes.Buffer
for range 10 {
@@ -210,8 +195,7 @@ func BenchmarkSubstituteEnvReader_MultipleBoundaries(b *testing.B) {
// BenchmarkSubstituteEnvReader_SpecialChars benchmarks substitution with special characters
func BenchmarkSubstituteEnvReader_SpecialChars(b *testing.B) {
os.Setenv("SPECIAL_BENCH_VAR", `value with "quotes" and \backslash\`)
defer os.Unsetenv("SPECIAL_BENCH_VAR")
b.Setenv("SPECIAL_BENCH_VAR", `value with "quotes" and \backslash\`)
r := strings.NewReader(`key: ${SPECIAL_BENCH_VAR}
`)
@@ -228,8 +212,7 @@ func BenchmarkSubstituteEnvReader_SpecialChars(b *testing.B) {
// BenchmarkSubstituteEnvReader_EmptyValue benchmarks substitution with empty value
func BenchmarkSubstituteEnvReader_EmptyValue(b *testing.B) {
os.Setenv("EMPTY_BENCH_VAR", "")
defer os.Unsetenv("EMPTY_BENCH_VAR")
b.Setenv("EMPTY_BENCH_VAR", "")
r := strings.NewReader(`key: ${EMPTY_BENCH_VAR}
`)
@@ -246,8 +229,7 @@ func BenchmarkSubstituteEnvReader_EmptyValue(b *testing.B) {
// BenchmarkSubstituteEnvReader_DollarWithoutBrace benchmarks $ without following {
func BenchmarkSubstituteEnvReader_DollarWithoutBrace(b *testing.B) {
os.Setenv("BENCH_VAR", "benchmark_value")
defer os.Unsetenv("BENCH_VAR")
b.Setenv("BENCH_VAR", "benchmark_value")
r := strings.NewReader(`price: $100 and $200 for ${BENCH_VAR}`)

View File

@@ -2,8 +2,8 @@ package serialization
import (
"bytes"
"errors"
"io"
"os"
"strings"
"testing"
@@ -11,8 +11,7 @@ import (
)
func TestSubstituteEnvReader_Basic(t *testing.T) {
os.Setenv("TEST_VAR", "hello")
defer os.Unsetenv("TEST_VAR")
t.Setenv("TEST_VAR", "hello")
input := []byte(`key: ${TEST_VAR}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -23,10 +22,8 @@ func TestSubstituteEnvReader_Basic(t *testing.T) {
}
func TestSubstituteEnvReader_Multiple(t *testing.T) {
os.Setenv("VAR1", "first")
os.Setenv("VAR2", "second")
defer os.Unsetenv("VAR1")
defer os.Unsetenv("VAR2")
t.Setenv("VAR1", "first")
t.Setenv("VAR2", "second")
input := []byte(`a: ${VAR1}, b: ${VAR2}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -46,8 +43,6 @@ func TestSubstituteEnvReader_NoSubstitution(t *testing.T) {
}
func TestSubstituteEnvReader_UnsetEnvError(t *testing.T) {
os.Unsetenv("UNSET_VAR_FOR_TEST")
input := []byte(`key: ${UNSET_VAR_FOR_TEST}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -57,8 +52,7 @@ func TestSubstituteEnvReader_UnsetEnvError(t *testing.T) {
}
func TestSubstituteEnvReader_SmallBuffer(t *testing.T) {
os.Setenv("SMALL_BUF_VAR", "value")
defer os.Unsetenv("SMALL_BUF_VAR")
t.Setenv("SMALL_BUF_VAR", "value")
input := []byte(`key: ${SMALL_BUF_VAR}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -70,7 +64,7 @@ func TestSubstituteEnvReader_SmallBuffer(t *testing.T) {
if n > 0 {
result = append(result, buf[:n]...)
}
if err == io.EOF {
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
@@ -79,8 +73,7 @@ func TestSubstituteEnvReader_SmallBuffer(t *testing.T) {
}
func TestSubstituteEnvReader_SpecialChars(t *testing.T) {
os.Setenv("SPECIAL_VAR", `hello "world" \n`)
defer os.Unsetenv("SPECIAL_VAR")
t.Setenv("SPECIAL_VAR", `hello "world" \n`)
input := []byte(`key: ${SPECIAL_VAR}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -91,8 +84,7 @@ func TestSubstituteEnvReader_SpecialChars(t *testing.T) {
}
func TestSubstituteEnvReader_EmptyValue(t *testing.T) {
os.Setenv("EMPTY_VAR", "")
defer os.Unsetenv("EMPTY_VAR")
t.Setenv("EMPTY_VAR", "")
input := []byte(`key: ${EMPTY_VAR}`)
reader := NewSubstituteEnvReader(bytes.NewReader(input))
@@ -103,8 +95,7 @@ func TestSubstituteEnvReader_EmptyValue(t *testing.T) {
}
func TestSubstituteEnvReader_LargeInput(t *testing.T) {
os.Setenv("LARGE_VAR", "replaced")
defer os.Unsetenv("LARGE_VAR")
t.Setenv("LARGE_VAR", "replaced")
prefix := strings.Repeat("x", 5000)
suffix := strings.Repeat("y", 5000)
@@ -119,8 +110,7 @@ func TestSubstituteEnvReader_LargeInput(t *testing.T) {
}
func TestSubstituteEnvReader_PatternAtBoundary(t *testing.T) {
os.Setenv("BOUNDARY_VAR", "boundary_value")
defer os.Unsetenv("BOUNDARY_VAR")
t.Setenv("BOUNDARY_VAR", "boundary_value")
prefix := strings.Repeat("a", 4090)
input := []byte(prefix + "${BOUNDARY_VAR}")
@@ -134,10 +124,8 @@ func TestSubstituteEnvReader_PatternAtBoundary(t *testing.T) {
}
func TestSubstituteEnvReader_MultiplePatternsBoundary(t *testing.T) {
os.Setenv("VAR_A", "aaa")
os.Setenv("VAR_B", "bbb")
defer os.Unsetenv("VAR_A")
defer os.Unsetenv("VAR_B")
t.Setenv("VAR_A", "aaa")
t.Setenv("VAR_B", "bbb")
prefix := strings.Repeat("x", 4090)
input := []byte(prefix + "${VAR_A} middle ${VAR_B}")
@@ -151,12 +139,9 @@ func TestSubstituteEnvReader_MultiplePatternsBoundary(t *testing.T) {
}
func TestSubstituteEnvReader_YAMLConfig(t *testing.T) {
os.Setenv("DB_HOST", "localhost")
os.Setenv("DB_PORT", "5432")
os.Setenv("DB_PASSWORD", "secret123")
defer os.Unsetenv("DB_HOST")
defer os.Unsetenv("DB_PORT")
defer os.Unsetenv("DB_PASSWORD")
t.Setenv("DB_HOST", "localhost")
t.Setenv("DB_PORT", "5432")
t.Setenv("DB_PASSWORD", "secret123")
input := []byte(`database:
host: ${DB_HOST}

View File

@@ -1,7 +1,9 @@
package serialization
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"reflect"
@@ -11,7 +13,6 @@ import (
"time"
"unsafe"
"github.com/bytedance/sonic"
"github.com/go-playground/validator/v10"
"github.com/goccy/go-yaml"
"github.com/puzpuzpuz/xsync/v4"
@@ -33,22 +34,22 @@ func ToSerializedObject[VT any](m map[string]VT) SerializedObject {
}
func init() {
strutils.SetJSONMarshaler(sonic.Marshal)
strutils.SetJSONUnmarshaler(sonic.Unmarshal)
strutils.SetJSONMarshaler(json.Marshal)
strutils.SetJSONUnmarshaler(json.Unmarshal)
strutils.SetYAMLMarshaler(yaml.Marshal)
strutils.SetYAMLUnmarshaler(yaml.Unmarshal)
}
type MapUnmarshaller interface {
UnmarshalMap(m map[string]any) gperr.Error
UnmarshalMap(m map[string]any) error
}
var (
ErrInvalidType = gperr.New("invalid type")
ErrNilValue = gperr.New("nil")
ErrUnsettable = gperr.New("unsettable")
ErrUnsupportedConversion = gperr.New("unsupported conversion")
ErrUnknownField = gperr.New("unknown field")
ErrInvalidType = errors.New("invalid type")
ErrNilValue = errors.New("nil")
ErrUnsettable = errors.New("unsettable")
ErrUnsupportedConversion = errors.New("unsupported conversion")
ErrUnknownField = errors.New("unknown field")
)
var (
@@ -86,11 +87,11 @@ func initPtr(dst reflect.Value) {
}
}
// Validate performs struct validation using go-playground/validator tags.
// ValidateWithFieldTags performs struct validation using go-playground/validator tags.
//
// It collects all validation errors and returns them as a single error.
// Field names in errors are prefixed with their namespace (e.g., "User.Email").
func ValidateWithFieldTags(s any) gperr.Error {
func ValidateWithFieldTags(s any) error {
var errs gperr.Builder
err := validate.Struct(s)
var valErrs validator.ValidationErrors
@@ -103,15 +104,16 @@ func ValidateWithFieldTags(s any) gperr.Error {
if detail != "required" {
detail = "require " + strconv.Quote(detail)
}
errs.Add(ErrValidationError.
Subject(e.Namespace()).
Withf(detail))
errs.Add(gperr.PrependSubject(ErrValidationError, e.Namespace()).
Withf("%s", detail))
}
}
return errs.Error()
}
func dive(dst reflect.Value) (v reflect.Value, t reflect.Type, err gperr.Error) {
// dive recursively dives into the nested pointers of the dst.
// dst value pointer must be valid (satisfies reflect.Value.IsValid()).
func dive(dst reflect.Value) (v reflect.Value, t reflect.Type) {
dstT := dst.Type()
for {
switch dstT.Kind() {
@@ -119,7 +121,7 @@ func dive(dst reflect.Value) (v reflect.Value, t reflect.Type, err gperr.Error)
dst = dst.Elem()
dstT = dstT.Elem()
default:
return dst, dstT, nil
return dst, dstT
}
}
}
@@ -276,32 +278,26 @@ func initTypeKeyFieldIndexesMap(t reflect.Type) typeInfo {
// If the target value is a map[string]any the SerializedObject will be deserialized into the map.
//
// The function returns an error if the target value is not a struct or a map[string]any, or if there is an error during deserialization.
func MapUnmarshalValidate(src SerializedObject, dst any) (err gperr.Error) {
func MapUnmarshalValidate(src SerializedObject, dst any) error {
return mapUnmarshalValidate(src, reflect.ValueOf(dst), true)
}
func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidateTag bool) (err gperr.Error) {
func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidateTag bool) (err error) {
dstT := dstV.Type()
if src != nil && dstT.Implements(mapUnmarshalerType) {
dstV, _, err = dive(dstV)
if err != nil {
return err
}
dstV, _ = dive(dstV)
return dstV.Addr().Interface().(MapUnmarshaller).UnmarshalMap(src)
}
dstV, dstT, err = dive(dstV)
if err != nil {
return err
}
dstV, dstT = dive(dstV)
if src == nil {
if dstV.CanSet() {
dstV.SetZero()
return nil
}
return gperr.Errorf("deserialize: src is %w and dst is not settable", ErrNilValue)
return fmt.Errorf("deserialize: src is %w and dst is not settable", ErrNilValue)
}
// convert data fields to lower no-snake
@@ -317,10 +313,10 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
if field, ok := info.getField(dstV, k); ok {
err := Convert(reflect.ValueOf(v), field, checkValidateTag)
if err != nil {
errs.Add(err.Subject(k))
errs.AddSubject(err, k)
}
} else {
errs.Add(ErrUnknownField.Subject(k).With(gperr.DoYouMeanField(k, info.fieldNames)))
errs.Add(gperr.PrependSubject(ErrUnknownField, k).With(gperr.DoYouMeanField(k, info.fieldNames)))
}
}
if info.hasValidateTag && checkValidateTag {
@@ -333,23 +329,23 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
case reflect.Map:
if dstV.IsNil() {
if !dstV.CanSet() {
return gperr.Errorf("dive: dst is %w and is not settable", ErrNilValue)
return fmt.Errorf("dive: dst is %w and is not settable", ErrNilValue)
}
gi.ReflectInitMap(dstV, len(src))
}
if dstT.Key().Kind() != reflect.String {
return gperr.Errorf("deserialize: %w for map of non string keys (map of %s)", ErrUnsupportedConversion, dstT.Elem().String())
return fmt.Errorf("deserialize: %w for map of non string keys (map of %s)", ErrUnsupportedConversion, dstT.Elem().String())
}
// ?: should we clear the map?
for k, v := range src {
elem := gi.ReflectStrMapAssign(dstV, k)
err := Convert(reflect.ValueOf(v), elem, true)
if err != nil {
errs.Add(err.Subject(k))
errs.AddSubject(err, k)
continue
}
if err := ValidateWithCustomValidator(elem); err != nil {
errs.Add(err.Subject(k))
errs.AddSubject(err, k)
}
}
if err := ValidateWithCustomValidator(dstV); err != nil {
@@ -357,7 +353,7 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
}
return errs.Error()
default:
return ErrUnsupportedConversion.Subject("mapping to " + dstT.String() + " ")
return fmt.Errorf("deserialize: %w for mapping to %s", ErrUnsupportedConversion, dstT)
}
}
@@ -373,14 +369,14 @@ func mapUnmarshalValidate(src SerializedObject, dstV reflect.Value, checkValidat
//
// Returns:
// - error: the error occurred during conversion, or nil if no error occurred.
func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) error {
if !dst.IsValid() {
return gperr.Errorf("convert: dst is %w", ErrNilValue)
return fmt.Errorf("convert: dst is %w", ErrNilValue)
}
if (src.Kind() == reflect.Pointer && src.IsNil()) || !src.IsValid() {
if !dst.CanSet() {
return gperr.Errorf("convert: src is %w", ErrNilValue)
return fmt.Errorf("convert: src is %w", ErrNilValue)
}
dst.SetZero()
return nil
@@ -388,7 +384,7 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
if src.IsZero() {
if !dst.CanSet() {
return gperr.Errorf("convert: src is %w", ErrNilValue)
return fmt.Errorf("convert: src is %w", ErrNilValue)
}
switch dst.Kind() {
case reflect.Pointer:
@@ -410,7 +406,7 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
if dst.Kind() == reflect.Pointer {
if dst.IsNil() {
if !dst.CanSet() {
return ErrUnsettable.Subject(dstT.String())
return fmt.Errorf("convert: dst is %w", ErrUnsettable)
}
initPtr(dst)
}
@@ -423,13 +419,13 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
switch {
case srcT == dstT, srcT.AssignableTo(dstT):
if !dst.CanSet() {
return ErrUnsettable.Subject(dstT.String())
return fmt.Errorf("convert: dst is %w", ErrUnsettable)
}
dst.Set(src)
return nil
case srcKind == reflect.String:
if !dst.CanSet() {
return ErrUnsettable.Subject(dstT.String())
return fmt.Errorf("convert: dst is %w", ErrUnsettable)
}
if convertible, err := ConvertString(src.String(), dst); convertible {
return err
@@ -451,14 +447,14 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
}
obj, ok := src.Interface().(SerializedObject)
if !ok {
return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String())
return fmt.Errorf("convert: %w from %s to %s", ErrUnsupportedConversion, srcT, dstT)
}
return mapUnmarshalValidate(obj, dst.Addr(), checkValidateTag)
case srcKind == reflect.Slice: // slice to slice
return ConvertSlice(src, dst, checkValidateTag)
}
return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT)
return fmt.Errorf("convert: %w for %s to %s", ErrUnsupportedConversion, srcT, dstT)
}
// ConvertSlice converts a source slice to a destination slice.
@@ -468,17 +464,17 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.
// - The destination slice is initialized with the source length.
// - On error, the destination slice is truncated to the number of
// successfully converted elements.
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error {
func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) error {
if dst.Kind() == reflect.Pointer {
if dst.IsNil() && !dst.CanSet() {
return ErrNilValue
return fmt.Errorf("convert: dst is %w", ErrNilValue)
}
initPtr(dst)
dst = dst.Elem()
}
if !dst.CanSet() {
return ErrUnsettable.Subject(dst.Type().String())
return fmt.Errorf("convert: dst is %w", ErrUnsettable)
}
if src.Kind() != reflect.Slice {
@@ -491,7 +487,7 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
return nil
}
if dst.Kind() != reflect.Slice {
return ErrUnsupportedConversion.Subjectf("%s to %s", dst.Type(), src.Type())
return fmt.Errorf("convert: %w for %s to %s", ErrUnsupportedConversion, dst.Type(), src.Type())
}
var sliceErrs gperr.Builder
@@ -500,7 +496,7 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
for j := range srcLen {
err := Convert(src.Index(j), dst.Index(numValid), checkValidateTag)
if err != nil {
sliceErrs.Add(err.Subjectf("[%d]", j))
sliceErrs.AddSubjectf(err, "[%d]", j)
continue
}
numValid++
@@ -526,8 +522,7 @@ func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) g
// - If the destination implements the Parser interface, it is used for conversion.
// - Returns true if conversion was handled (even with error), false if
// conversion is unsupported.
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) {
convertible = true
func ConvertString(src string, dst reflect.Value) (convertible bool, convErr error) {
dstT := dst.Type()
if dst.Kind() == reflect.Pointer {
if dst.IsNil() {
@@ -555,14 +550,14 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
// check if (*T).Convertor is implemented
if addr := dst.Addr(); addr.Type().Implements(reflect.TypeFor[strutils.Parser]()) {
parser := addr.Interface().(strutils.Parser)
return true, gperr.Wrap(parser.Parse(src))
return true, parser.Parse(src)
}
switch dstT {
case reflect.TypeFor[time.Duration]():
d, err := time.ParseDuration(src)
if err != nil {
return true, gperr.Wrap(err)
return true, err
}
gi.ReflectValueSet(dst, d)
return true, nil
@@ -572,7 +567,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
if gi.ReflectIsNumeric(dst) || dst.Kind() == reflect.Bool {
err := gi.ReflectStrToNumBool(dst, src)
if err != nil {
return true, gperr.Wrap(err)
return true, err
}
return true, nil
}
@@ -602,14 +597,14 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
sl := []any{}
err := yaml.Unmarshal(unsafe.Slice(unsafe.StringData(src), len(src)), &sl)
if err != nil {
return true, gperr.Wrap(err)
return true, err
}
return true, ConvertSlice(reflect.ValueOf(sl), dst, true)
case reflect.Map, reflect.Struct:
rawMap := SerializedObject{}
err := yaml.Unmarshal(unsafe.Slice(unsafe.StringData(src), len(src)), &rawMap)
if err != nil {
return true, gperr.Wrap(err)
return true, err
}
return true, mapUnmarshalValidate(rawMap, dst, true)
default:
@@ -619,7 +614,7 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe
var envRegex = regexp.MustCompile(`\$\{([^}]+)\}`) // e.g. ${CLOUDFLARE_API_KEY}
func substituteEnv(data []byte) ([]byte, gperr.Error) {
func substituteEnv(data []byte) ([]byte, error) {
envError := gperr.NewBuilder("env substitution error")
data = envRegex.ReplaceAllFunc(data, func(match []byte) []byte {
varName := string(match[2 : len(match)-1])
@@ -643,7 +638,7 @@ type (
newDecoderFunc func(r io.Reader) interface {
Decode(v any) error
}
interceptFunc func(m map[string]any) gperr.Error
interceptFunc func(m map[string]any) error
)
// UnmarshalValidate unmarshals data into a map, applies optional intercept
@@ -651,7 +646,7 @@ type (
// - Environment variables in the data are substituted using ${VAR} syntax.
// - The unmarshaler function converts data to a map[string]any.
// - Intercept functions can modify or validate the map before unmarshaling.
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) gperr.Error {
func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) error {
data, err := substituteEnv(data)
if err != nil {
return err
@@ -659,7 +654,7 @@ func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc,
m := make(map[string]any)
if err := unmarshaler(data, &m); err != nil {
return gperr.Wrap(err)
return err
}
for _, intercept := range interceptFns {
if err := intercept(m); err != nil {
@@ -674,10 +669,10 @@ func UnmarshalValidate[T any](data []byte, target *T, unmarshaler unmarshalFunc,
// - Environment variables are substituted during reading using ${VAR} syntax.
// - The newDecoder function creates a decoder for the reader (e.g.,
// json.NewDecoder).
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) gperr.Error {
func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newDecoderFunc, interceptFns ...interceptFunc) error {
m := make(map[string]any)
if err := newDecoder(NewSubstituteEnvReader(reader)).Decode(&m); err != nil {
return gperr.Wrap(err)
return err
}
for _, intercept := range interceptFns {
if err := intercept(m); err != nil {
@@ -692,7 +687,7 @@ func UnmarshalValidateReader[T any](reader io.Reader, target *T, newDecoder newD
// - The unmarshaler function converts data to a map[string]any.
// - Intercept functions can modify or validate the map before unmarshaling.
// - Returns a thread-safe concurrent map with the unmarshaled values.
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], gperr.Error) {
func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, interceptFns ...interceptFunc) (*xsync.Map[string, V], error) {
data, err := substituteEnv(data)
if err != nil {
return nil, err
@@ -700,7 +695,7 @@ func UnmarshalValidateXSync[V any](data []byte, unmarshaler unmarshalFunc, inter
m := make(map[string]any)
if err := unmarshaler(data, &m); err != nil {
return nil, gperr.Wrap(err)
return nil, err
}
for _, intercept := range interceptFns {
if err := intercept(m); err != nil {

View File

@@ -42,7 +42,7 @@ func BenchmarkDeserialize(b *testing.B) {
dst := complexStruct{}
err := MapUnmarshalValidate(src, &dst)
if err != nil {
b.Fatal(string(err.Plain()))
b.Fatal(err.Error())
}
}
}

View File

@@ -1,15 +1,15 @@
package serialization
import (
"errors"
"reflect"
"github.com/go-playground/validator/v10"
gperr "github.com/yusing/goutils/errs"
)
var validate = validator.New()
var ErrValidationError = gperr.New("validation error")
var ErrValidationError = errors.New("validation error")
func Validator() *validator.Validate {
return validate
@@ -23,12 +23,12 @@ func MustRegisterValidation(tag string, fn validator.Func) {
}
type CustomValidator interface {
Validate() gperr.Error
Validate() error
}
var validatorType = reflect.TypeFor[CustomValidator]()
func ValidateWithCustomValidator(v reflect.Value) gperr.Error {
func ValidateWithCustomValidator(v reflect.Value) error {
vt := v.Type()
if v.Kind() == reflect.Pointer {
elemType := vt.Elem()

View File

@@ -1,24 +1,23 @@
package serialization
import (
"errors"
"reflect"
"testing"
gperr "github.com/yusing/goutils/errs"
)
// Test cases for when *T implements CustomValidator but T is passed in
type CustomValidatingInt int
func (c *CustomValidatingInt) Validate() gperr.Error {
func (c *CustomValidatingInt) Validate() error {
if c == nil {
return gperr.New("pointer int cannot be nil")
return errors.New("pointer int cannot be nil")
}
if *c <= 0 {
return gperr.New("int must be positive")
return errors.New("int must be positive")
}
if *c > 100 {
return gperr.New("int must be <= 100")
return errors.New("int must be <= 100")
}
return nil
}
@@ -26,12 +25,12 @@ func (c *CustomValidatingInt) Validate() gperr.Error {
// Test cases for when T implements CustomValidator but *T is passed in
type CustomValidatingFloat float64
func (c CustomValidatingFloat) Validate() gperr.Error {
func (c CustomValidatingFloat) Validate() error {
if c < 0 {
return gperr.New("float must be non-negative")
return errors.New("float must be non-negative")
}
if c > 1000 {
return gperr.New("float must be <= 1000")
return errors.New("float must be <= 1000")
}
return nil
}

View File

@@ -1,23 +1,22 @@
package serialization
import (
"errors"
"reflect"
"testing"
gperr "github.com/yusing/goutils/errs"
)
type CustomValidatingPointerString string
func (c *CustomValidatingPointerString) Validate() gperr.Error {
func (c *CustomValidatingPointerString) Validate() error {
if c == nil {
return gperr.New("pointer string cannot be nil")
return errors.New("pointer string cannot be nil")
}
if *c == "" {
return gperr.New("string cannot be empty")
return errors.New("string cannot be empty")
}
if len(*c) < 2 {
return gperr.New("string must be at least 2 characters")
return errors.New("string must be at least 2 characters")
}
return nil
}

View File

@@ -1,20 +1,19 @@
package serialization
import (
"errors"
"reflect"
"testing"
gperr "github.com/yusing/goutils/errs"
)
type CustomValidatingString string
func (c CustomValidatingString) Validate() gperr.Error {
func (c CustomValidatingString) Validate() error {
if c == "" {
return gperr.New("string cannot be empty")
return errors.New("string cannot be empty")
}
if len(c) < 2 {
return gperr.New("string must be at least 2 characters")
return errors.New("string must be at least 2 characters")
}
return nil
}

View File

@@ -1,25 +1,24 @@
package serialization
import (
"errors"
"reflect"
"testing"
gperr "github.com/yusing/goutils/errs"
)
type CustomValidatingPointerStruct struct {
Value string
}
func (c *CustomValidatingPointerStruct) Validate() gperr.Error {
func (c *CustomValidatingPointerStruct) Validate() error {
if c == nil {
return gperr.New("pointer struct cannot be nil")
return errors.New("pointer struct cannot be nil")
}
if c.Value == "" {
return gperr.New("value cannot be empty")
return errors.New("value cannot be empty")
}
if len(c.Value) < 3 {
return gperr.New("value must be at least 3 characters")
return errors.New("value must be at least 3 characters")
}
return nil
}

View File

@@ -1,22 +1,21 @@
package serialization
import (
"errors"
"reflect"
"testing"
gperr "github.com/yusing/goutils/errs"
)
type CustomValidatingStruct struct {
Value string
}
func (c CustomValidatingStruct) Validate() gperr.Error {
func (c CustomValidatingStruct) Validate() error {
if c.Value == "" {
return gperr.New("value cannot be empty")
return errors.New("value cannot be empty")
}
if len(c.Value) < 3 {
return gperr.New("value must be at least 3 characters")
return errors.New("value must be at least 3 characters")
}
return nil
}