diff --git a/internal/serialization/serialization.go b/internal/serialization/serialization.go index 5dd777a8..f2881fde 100644 --- a/internal/serialization/serialization.go +++ b/internal/serialization/serialization.go @@ -449,51 +449,58 @@ func Convert(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr. } return mapUnmarshalValidate(obj, dst.Addr(), checkValidateTag) case srcKind == reflect.Slice: // slice to slice - srcLen := src.Len() - if srcLen == 0 { - dst.SetZero() - return nil - } - if dstT.Kind() != reflect.Slice { - return ErrUnsupportedConversion.Subject(dstT.String() + " to " + srcT.String()) - } - var sliceErrs gperr.Builder - i := 0 - gi.ReflectInitSlice(dst, srcLen, srcLen) - for j, v := range src.Seq2() { - err := Convert(v, dst.Index(i), checkValidateTag) - if err != nil { - sliceErrs.Add(err.Subjectf("[%d]", j)) - continue - } - i++ - } - if err := sliceErrs.Error(); err != nil { - dst.SetLen(i) // shrink to number of elements that were successfully converted - return err - } - return nil + return ConvertSlice(src, dst, checkValidateTag) } - return ErrUnsupportedConversion.Subjectf("%s to %s", srcT.String(), dstT.String()) + return ErrUnsupportedConversion.Subjectf("%s to %s", srcT, dstT) } -var parserType = reflect.TypeFor[strutils.Parser]() +func ConvertSlice(src reflect.Value, dst reflect.Value, checkValidateTag bool) gperr.Error { + if src.Kind() != reflect.Slice { + return Convert(src, dst, checkValidateTag) + } + + srcLen := src.Len() + if srcLen == 0 { + dst.SetZero() + return nil + } + if dst.Kind() != reflect.Slice { + return ErrUnsupportedConversion.Subjectf("%s to %s", dst.Type(), src.Type()) + } + + var sliceErrs gperr.Builder + numValid := 0 + gi.ReflectInitSlice(dst, srcLen, srcLen) + for j := range srcLen { + err := Convert(src.Index(j), dst.Index(numValid), checkValidateTag) + if err != nil { + sliceErrs.Add(err.Subjectf("[%d]", j)) + continue + } + numValid++ + } + if err := sliceErrs.Error(); err != nil { + dst.SetLen(numValid) // shrink to number of elements that were successfully converted + return err + } + return nil +} func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gperr.Error) { convertible = true dstT := dst.Type() if dst.Kind() == reflect.Pointer { if dst.IsNil() { + // Early return for empty string + if src == "" { + return true, nil + } initPtr(dst) } dst = dst.Elem() dstT = dst.Type() } - if dst.Kind() == reflect.String { - dst.SetString(src) - return true, nil - } // Early return for empty string if src == "" { @@ -501,6 +508,17 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe return true, nil } + if dst.Kind() == reflect.String { + dst.SetString(src) + return true, nil + } + + // 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)) + } + switch dstT { case reflect.TypeFor[time.Duration](): d, err := time.ParseDuration(src) @@ -512,12 +530,6 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe default: } - // check if (*T).Convertor is implemented - if dst.Addr().Type().Implements(parserType) { - parser := dst.Addr().Interface().(strutils.Parser) - return true, gperr.Wrap(parser.Parse(src)) - } - if gi.ReflectIsNumeric(dst) || dst.Kind() == reflect.Bool { err := gi.ReflectStrToNumBool(dst, src) if err != nil { @@ -527,29 +539,25 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe } // yaml like - var tmp any switch dst.Kind() { case reflect.Slice: - // Avoid unnecessary TrimSpace if we can detect the format early - srcLen := len(src) - if srcLen == 0 { - return true, nil - } - // one liner is comma separated list - isMultiline := strings.ContainsRune(src, '\n') - if !isMultiline && src[0] != '-' { + isMultiline := strings.IndexByte(src, '\n') != -1 + if !isMultiline && src[0] != '-' && src[0] != '[' { values := strutils.CommaSeperatedList(src) - gi.ReflectInitSlice(dst, len(values), len(values)) + size := len(values) + gi.ReflectInitSlice(dst, size, size) var errs gperr.Builder for i, v := range values { _, err := ConvertString(v, dst.Index(i)) if err != nil { - errs.Add(err.Subjectf("[%d]", i)) + errs.AddSubjectf(err, "[%d]", i) } } - err := errs.Error() - return true, err + if errs.HasError() { + return true, errs.Error() + } + return true, nil } sl := []any{} @@ -557,18 +565,17 @@ func ConvertString(src string, dst reflect.Value) (convertible bool, convErr gpe if err != nil { return true, gperr.Wrap(err) } - tmp = sl + 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) } - tmp = rawMap + return true, mapUnmarshalValidate(rawMap, dst, true) default: return false, nil } - return true, Convert(reflect.ValueOf(tmp), dst, true) } var envRegex = regexp.MustCompile(`\$\{([^}]+)\}`) // e.g. ${CLOUDFLARE_API_KEY} diff --git a/internal/serialization/serialization_benchmark_test.go b/internal/serialization/serialization_benchmark_test.go index 56531046..4fb881df 100644 --- a/internal/serialization/serialization_benchmark_test.go +++ b/internal/serialization/serialization_benchmark_test.go @@ -32,9 +32,9 @@ func BenchmarkDeserialize(b *testing.B) { "c": "1,2,3", "d": "a: a\nb: b\nc: c", "e": "- a: a\n b: b\n c: c", - "f": map[string]any{"a": "a", "b": "456", "c": []string{"1", "2", "3"}}, + "f": map[string]any{"a": "a", "b": "456", "c": `1,2,3`}, "g": map[string]any{"g1": "1.23", "g2": 123}, - "h": []map[string]any{{"a": 123, "b": "456", "c": []string{"1", "2", "3"}}}, + "h": []map[string]any{{"a": 123, "b": "456", "c": `["1","2","3"]`}}, "j": "1.23", "k": 123, }