diff --git a/go.mod b/go.mod index 7644db9e..a32688d2 100644 --- a/go.mod +++ b/go.mod @@ -31,13 +31,18 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/miekg/dns v1.1.62 // indirect diff --git a/go.sum b/go.sum index eb16cff3..65ea5d62 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-acme/lego/v4 v4.20.4 h1:yCQGBX9jOfMbriEQUocdYm7EBapdTp8nLXYG8k6SqSU= github.com/go-acme/lego/v4 v4.20.4/go.mod h1:foauPlhnhoq8WUphaWx5U04uDc+JGhk4ZZtPz/Vqsjg= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= @@ -42,6 +44,12 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -72,6 +80,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/internal/utils/nearest_field.go b/internal/utils/nearest_field.go index 9919dd41..238b3842 100644 --- a/internal/utils/nearest_field.go +++ b/internal/utils/nearest_field.go @@ -36,7 +36,7 @@ func NearestField(input string, s any) string { fields[i] = key.String() } default: - panic("unsupported type: " + t.String()) + panic("NearestField unsupported type: " + t.String()) } } for _, field := range fields { diff --git a/internal/utils/serialization.go b/internal/utils/serialization.go index 6e6ae47c..2bc3368d 100644 --- a/internal/utils/serialization.go +++ b/internal/utils/serialization.go @@ -10,6 +10,7 @@ import ( "time" "unicode" + "github.com/go-playground/validator/v10" "github.com/santhosh-tekuri/jsonschema" E "github.com/yusing/go-proxy/internal/error" "github.com/yusing/go-proxy/internal/utils/strutils" @@ -33,6 +34,8 @@ var ( ErrUnknownField = E.New("unknown field") ) +var validate = validator.New() + func ValidateYaml(schema *jsonschema.Schema, data []byte) E.Error { var i any @@ -143,7 +146,7 @@ func Serialize(data any) (SerializedObject, error) { // Deserialize ignores case differences between the field names in the SerializedObject and the target. // // The target value must be a struct or a map[string]any. -// If the target value is a struct, the SerializedObject will be deserialized into the struct fields. +// If the target value is a struct, the SerializedObject will be deserialized into the struct fields and validate if needed. // 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. @@ -158,9 +161,13 @@ func Deserialize(src SerializedObject, dst any) E.Error { dstV := reflect.ValueOf(dst) dstT := dstV.Type() - if dstV.Kind() == reflect.Ptr { + for dstT.Kind() == reflect.Ptr { if dstV.IsNil() { - return E.Errorf("deserialize: dst is %w", ErrNilValue) + if dstV.CanSet() { + dstV.Set(reflect.New(dstT.Elem())) + } else { + return E.Errorf("deserialize: dst is %w", ErrNilValue) + } } dstV = dstV.Elem() dstT = dstV.Type() @@ -174,9 +181,20 @@ func Deserialize(src SerializedObject, dst any) E.Error { switch dstV.Kind() { case reflect.Struct: + needValidate := false mapping := make(map[string]reflect.Value) for _, field := range reflect.VisibleFields(dstT) { - mapping[strutils.ToLowerNoSnake(field.Name)] = dstV.FieldByName(field.Name) + var key string + if jsonTag, ok := field.Tag.Lookup("json"); ok { + key = strings.Split(jsonTag, ",")[0] + } else { + key = field.Name + } + mapping[strutils.ToLowerNoSnake(key)] = dstV.FieldByName(field.Name) + _, ok := field.Tag.Lookup("validate") + if ok { + needValidate = true + } } for k, v := range src { if field, ok := mapping[strutils.ToLowerNoSnake(k)]; ok { @@ -185,9 +203,12 @@ func Deserialize(src SerializedObject, dst any) E.Error { errs.Add(err.Subject(k)) } } else { - errs.Add(ErrUnknownField.Subject(k).Withf(strutils.DoYouMean(NearestField(k, dst)))) + errs.Add(ErrUnknownField.Subject(k).Withf(strutils.DoYouMean(NearestField(k, dstV.Interface())))) } } + if needValidate { + errs.Add(validate.Struct(dstV.Interface())) + } return errs.Error() case reflect.Map: if dstV.IsNil() { diff --git a/internal/utils/testing/testing.go b/internal/utils/testing/testing.go index d8a9b922..8b3217a6 100644 --- a/internal/utils/testing/testing.go +++ b/internal/utils/testing/testing.go @@ -44,6 +44,15 @@ func ExpectError2(t *testing.T, input any, expected error, err error) { } } +func ExpectErrorT[T error](t *testing.T, err error) { + t.Helper() + var errAs T + if !errors.As(err, &errAs) { + t.Errorf("expected err %T, got %v", errAs, err) + t.FailNow() + } +} + func ExpectEqual[T comparable](t *testing.T, got T, want T) { t.Helper() if got != want { @@ -98,10 +107,9 @@ func ExpectFalse(t *testing.T, got bool) { func ExpectType[T any](t *testing.T, got any) (_ T) { t.Helper() - tExpect := reflect.TypeFor[T]() _, ok := got.(T) if !ok { - t.Fatalf("expected type %s, got %s", tExpect, reflect.TypeOf(got).Elem()) + t.Fatalf("expected type %s, got %s", reflect.TypeFor[T](), reflect.TypeOf(got).Elem()) return } return got.(T)