diff --git a/deserialize/deserialize.go b/deserialize/deserialize.go index e1c0efc..47467f0 100644 --- a/deserialize/deserialize.go +++ b/deserialize/deserialize.go @@ -293,24 +293,6 @@ func (me kvListDeserializer[T]) DeserializeKVList(value kvlist.KVList) (*T, erro return me.deserializer(value) } -func (me kvListDeserializer[T]) DeserializeBytes(source []byte) (*T, error) { - kvList := new(kvlist.KVList) - if !me.options.unmarshaler.ShouldUnmarshal(reflect.TypeOf(*kvList)) { - return nil, fmt.Errorf("this deserializer does not support deserializing from bytes") - } - - var kvListAny any = kvList - err := me.options.unmarshaler.Unmarshal(source, &kvListAny) - if err != nil { - return nil, err //nolint:wrapcheck - } - return me.deserializer(*kvList) -} - -func (me kvListDeserializer[T]) DeserializeString(source string) (*T, error) { - return me.DeserializeBytes([]byte(source)) -} - // Convert a `map[string][]string` (as provided e.g. by the query parser) into a `Dict` // (as consumed by this parsing mechanism). func deListMap[T any](outMap map[string]any, inMap map[string][]string, options staticOptions) error { @@ -324,6 +306,7 @@ func deListMap[T any](outMap map[string]any, inMap map[string][]string, options field := reflectedT.Field(i) tags, err := tagsPkg.Parse(field.Tag) if err != nil { + // This probably cannot happen as we have already failed in makeStructDeserializerFromReflect. return fmt.Errorf("invalid tags\n\t * %w", err) } @@ -385,6 +368,7 @@ type reflectDeserializer func(slot *reflect.Value, data shared.Value) error // to pre-initialize structs. var initializerInterface = reflect.TypeOf((*validation.Initializer)(nil)).Elem() var validatorInterface = reflect.TypeOf((*validation.Validator)(nil)).Elem() +var unmarshalDictInterface = reflect.TypeOf((*shared.UnmarshalDict)(nil)).Elem() // The interface `error`. var errorInterface = reflect.TypeOf((*error)(nil)).Elem() @@ -407,10 +391,11 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma // Pre-check if we're going to perform initialization. typ := reflect.TypeOf(*container) - shouldPreinitialize, err := canInterface(typ, initializerInterface) + initializationMetadata, err := initializationData(path, typ, options) if err != nil { return nil, err } + if path == "" { path = typeName(typ) } else { @@ -419,7 +404,7 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma // The outer struct can't have any tags attached. tags := tagsPkg.Empty() - reflectDeserializer, err := makeStructDeserializerFromReflect(path, typ, options, &tags, reflect.ValueOf(container), shouldPreinitialize) + reflectDeserializer, err := makeStructDeserializerFromReflect(path, typ, options, &tags, reflect.ValueOf(container), initializationMetadata.canInitializeSelf) if err != nil { return nil, err } @@ -427,7 +412,7 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma var result = mapDeserializer[T]{ deserializer: func(value shared.Dict) (*T, error) { result := new(T) - if shouldPreinitialize { + if initializationMetadata.canInitializeSelf { var resultAny any = result initializer, ok := resultAny.(validation.Initializer) var err error @@ -468,7 +453,7 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma // - `wasPreinitialized` if this value was preinitialized, typically through `Initializer` func makeStructDeserializerFromReflect(path string, typ reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreInitialized bool) (reflectDeserializer, error) { if typ.Kind() != reflect.Struct { - panic(fmt.Sprintf("invalid call to StructDeserializer: %s is not a struct", path)) + return nil, fmt.Errorf("invalid call to StructDeserializer: %s is not a struct", path) } selfContainer := reflect.New(typ) deserializers := make(map[string]func(outPtr *reflect.Value, inMap shared.Dict) error) @@ -564,16 +549,18 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st result := resultPtr.Elem() // If possible, perform pre-initialization with default values. - if initializer, ok := resultPtr.Interface().(validation.Initializer); ok { - err = initializer.Initialize() - wasPreInitialized = true - if err != nil { - err = fmt.Errorf("at %s, encountered an error while initializing optional fields:\n\t * %w", path, err) - slog.Error("Internal error during deserialization", "error", err) - return CustomDeserializerError{ - Wrapped: err, - Operation: "initializer", - Structure: "struct", + if initializationData.canInitializeSelf { + if initializer, ok := resultPtr.Interface().(validation.Initializer); ok { + err = initializer.Initialize() + wasPreInitialized = true + if err != nil { + err = fmt.Errorf("at %s, encountered an error while initializing optional fields:\n\t * %w", path, err) + slog.Error("Internal error during deserialization", "error", err) + return CustomDeserializerError{ + Wrapped: err, + Operation: "initializer", + Structure: "struct", + } } } } @@ -618,14 +605,30 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st return err } - if initializationData.canUnmarshalSelf { + switch { + case initializationData.canDriverUnmarshal: resultPtrAny := resultPtr.Interface() err = options.unmarshaler.Unmarshal(inValue, &resultPtrAny) if err != nil { err = fmt.Errorf("at %s, expected to be able to parse a %s:\n\t * %w", path, typeName(typ), err) return err } - } else { + case initializationData.canUnmarshalFromDict: + unmarshalDict, ok := resultPtr.Interface().(shared.UnmarshalDict) + if !ok { + panic("at this stage, we should have an UnmarshalDict") // We have checked this already when setting canUnmarshalFromDict. + } + inDict, ok := inValue.AsDict() + if !ok { + err = fmt.Errorf("invalid value at %s, expected an object of type %s, got %s", path, typeName(typ), result.Type().Name()) + return err + } + err = unmarshalDict.UnmarshalDict(inDict) + if err != nil { + err = fmt.Errorf("at %s, expected to be able to parse a %s:\n\t * %w", path, typeName(typ), err) + return err + } + default: inMap, ok := inValue.AsDict() if !ok { err = fmt.Errorf("invalid value at %s, expected an object of type %s, got %s", path, typeName(typ), result.Type().Name()) @@ -640,7 +643,6 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st } } } - outPtr.Set(result) return nil } @@ -668,6 +670,9 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati if err != nil { return nil, err } + if initializationMetadata.canInitializeSelf { + panic(fmt.Errorf("at %s, we see a map type that looks like it can be initialized, that's currently impossible in go", path)) + } subPath := fmt.Sprintf("%s[]", path) subTags := tagsPkg.Empty() @@ -694,38 +699,7 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati result := func(outPtr *reflect.Value, inValue shared.Value) (err error) { result := reflect.MakeMap(typ) - // If possible, perform pre-initialization with default values. - if initializer, ok := result.Interface().(validation.Initializer); ok { - err = initializer.Initialize() - wasPreInitialized = true - if err != nil { - err = fmt.Errorf("at %s, encountered an error while initializing optional object:\n\t * %w", path, err) - slog.Error("Internal error during deserialization", "error", err) - return CustomDeserializerError{ - Wrapped: err, - Operation: "initialize", - Structure: "map", - } - } - } - - // Don't forget to perform validation (unless we're returning an error). - defer func() { - if err != nil { - // We're already returning an error, no need to insist. - return - } - mightValidate := result.Interface() - if validator, ok := mightValidate.(validation.Validator); ok { - err = validator.Validate() - if err != nil { - // Validation error, abort struct construction. - err = fmt.Errorf("deserialized value %s did not pass validation\n\t * %w", path, err) - result = reflect.Zero(typ) - } - } - }() - print("TEST") + // No deferred validation, as we can't implement Validator on a map. switch { case inValue != nil: // We have all the data we need, proceed. @@ -734,7 +708,7 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati case orMethod != nil: constructed, err := (*orMethod)() if err != nil { - err = fmt.Errorf("error in optional object at %s\n\t * %w", path, err) + err = fmt.Errorf("error in optional value at %s\n\t * %w", path, err) slog.Error("Internal error during deserialization", "error", err) return CustomDeserializerError{ Wrapped: err, @@ -750,41 +724,28 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati return err } - if initializationMetadata.canUnmarshalSelf { - // Our struct supports `Unmarshaler`. This means that: - // - // - it enforces its own invariants (we do not perform pre-initialization or in-depth validation); - // - from the point of view of the outside world, this MUST be a string. - resultPtrAny := result.Interface() - err = options.unmarshaler.Unmarshal(inValue.Interface(), &resultPtrAny) - if err != nil { - err = fmt.Errorf("invalid string at %s, expected to be able to parse a %s:\n\t * %w", path, typeName(typ), err) - return err - } - } else { - inMap, ok := inValue.AsDict() + inMap, ok := inValue.AsDict() + if !ok { + err = fmt.Errorf("invalid value at %s, expected an object of type %s, got %v", path, typeName(typ), inValue.Interface()) + return err + } + + // We may now deserialize keys and values. + keys := inMap.Keys() + for _, k := range keys { + subInValue, ok := inMap.Lookup(k) if !ok { - err = fmt.Errorf("invalid value at %s, expected an object of type %s, got %v", path, typeName(typ), inValue.Interface()) - return err + slog.Error("Internal error while ranging over map: missing value", "path", path, "key", k) + // Hobble on. + continue } - // We may now deserialize keys and values. - keys := inMap.Keys() - for _, k := range keys { - subInValue, ok := inMap.Lookup(k) - if !ok { - slog.Error("Internal error while ranging over map: missing value", "path", path, "key", k) - // Hobble on. - continue - } - - reflectedContent := reflect.New(subTyp).Elem() - err = contentDeserializer(&reflectedContent, subInValue) - if err != nil { - return err - } - result.SetMapIndex(reflect.ValueOf(k), reflectedContent) + reflectedContent := reflect.New(subTyp).Elem() + err = contentDeserializer(&reflectedContent, subInValue) + if err != nil { + return err } + result.SetMapIndex(reflect.ValueOf(k), reflectedContent) } outPtr.Set(result) @@ -832,24 +793,8 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options sta result := func(outPtr *reflect.Value, inValue shared.Value) (err error) { var reflectedResult reflect.Value - // Don't forget to perform validation (unless we're returning an error). - defer func() { - if err != nil { - // We're already returning an error, no need to insist. - return - } - mightValidate := outPtr.Interface() - if validator, ok := mightValidate.(validation.Validator); ok { - err = validator.Validate() - if err != nil { - // Validation error, abort struct construction. - err = fmt.Errorf("deserialized value %s did not pass validation\n\t * %w", fieldPath, err) - outPtr.Set(reflect.Zero(fieldType)) - } - } - }() - - // Move into array + // Note: no defer() to call validation, as Validate cannot be implemented on slices. + // Move into slice. var input []shared.Value switch { case inValue != nil: @@ -878,16 +823,34 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options sta return fmt.Errorf("missing value at %s, expected an array of %s", arrayPath, fieldPath) } - reflectedResult = reflect.MakeSlice(fieldType, len(input), len(input)) + switch fieldType.Kind() { + case reflect.Slice: + reflectedResult = reflect.MakeSlice(fieldType, len(input), len(input)) - // Recurse into entries. - for i, inAtIndex := range input { - outAtIndex := reflectedResult.Index(i) - err := elementDeserializer(&outAtIndex, inAtIndex) - if err != nil { - return fmt.Errorf("error while deserializing %s[%d]:\n\t * %w", fieldPath, i, err) + // Recurse into entries. + for i, inAtIndex := range input { + outAtIndex := reflectedResult.Index(i) + err := elementDeserializer(&outAtIndex, inAtIndex) + if err != nil { + return fmt.Errorf("error while deserializing %s[%d]:\n\t * %w", fieldPath, i, err) + } + reflect.Append(reflectedResult, outAtIndex) + } + case reflect.Array: + if fieldType.Len() != len(input) { + return fmt.Errorf("invalid array length at %s, expecting %d, got %d", fieldPath, fieldType.Len(), len(input)) } - reflect.Append(reflectedResult, outAtIndex) + reflectedResult = reflect.New(fieldType).Elem() + // Recurse into entries. + for i, inAtIndex := range input { + outAtIndex := reflectedResult.Index(i) + err := elementDeserializer(&outAtIndex, inAtIndex) + if err != nil { + return fmt.Errorf("error while deserializing %s[%d]:\n\t * %w", fieldPath, i, err) + } + } + default: + panic("at this stage, we should have either an array or a slice") } outPtr.Set(reflectedResult) return nil @@ -897,9 +860,8 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options sta // Construct a dynamically-typed deserializer for pointers. // -// - `path` the human-readable path into the data structure, used for error-reporting; -// - `typ` the dynamic type for the pointer being compiled; -// - `tagName` the name of tags to use for field renamings, e.g. `query`; +// - `fieldPath` the human-readable path into the data structure, used for error-reporting; +// - `fieldType` the dynamic type for the pointer being compiled; // - `tags` the table of tags for this field. func makePointerDeserializer(fieldPath string, fieldType reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { ptrPath := fmt.Sprint(fieldPath, "*") @@ -1022,22 +984,7 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options result := func(outPtr *reflect.Value, inValue shared.Value) (err error) { var reflectedInput reflect.Value - // Don't forget to perform validation (unless we're returning an error). - defer func() { - if err != nil { - // We're already returning an error, no need to insist. - return - } - mightValidate := outPtr.Interface() - if validator, ok := mightValidate.(validation.Validator); ok { - err = validator.Validate() - if err != nil { - // Validation error, abort struct construction. - err = fmt.Errorf("deserialized value %s did not pass validation\n\t * %w", fieldPath, err) - outPtr.Set(reflect.Zero(fieldType)) - } - } - }() + // No defer-time validation here, as a flat value cannot implement `Validator`. var input any switch { @@ -1185,7 +1132,7 @@ func makeOrMethodConstructor(tags *tagsPkg.Tags, fieldType reflect.Type, contain var err error err, ok := out[1].Interface().(error) // We have just checked that it MUST be convertible to `error`. if !ok { - // Conversion fails if `error` is `nil`. + // Conversion failure? This means that `out[1]` is `nil`. return result, nil } return result, err @@ -1212,9 +1159,10 @@ func canInterface(typ reflect.Type, interfaceType reflect.Type) (bool, error) { // Some metadata on initialization for a type. type initializationMetadata struct { - canInitializeSelf bool - canUnmarshalSelf bool - willPreinitialize bool + canInitializeSelf bool + canDriverUnmarshal bool + canUnmarshalFromDict bool + willPreinitialize bool } func initializationData(path string, typ reflect.Type, options staticOptions) (initializationMetadata, error) { @@ -1225,12 +1173,20 @@ func initializationData(path string, typ reflect.Type, options staticOptions) (i return initializationMetadata{}, err } - canUnmarshalSelf := options.unmarshaler.ShouldUnmarshal(typ) - if canInitializeSelf && canUnmarshalSelf { + canDriverUnmarshal := options.unmarshaler.ShouldUnmarshal(typ) + canUnmarshalFromDict, err := canInterface(typ, unmarshalDictInterface) + if err != nil { + return initializationMetadata{}, err + } + if canInitializeSelf && canDriverUnmarshal { slog.Warn("Type supports both Initializer and Unmarshaler, defaulting to Unmarshaler", "path", path, "type", typ) canInitializeSelf = false } - willPreinitialize := canInitializeSelf || canUnmarshalSelf + if canDriverUnmarshal && canUnmarshalFromDict { + slog.Warn("Type supports both Unmarshaler and UnmarshalDict, defaulting to UnmarshalDict", "path", path, "type", typ) + canDriverUnmarshal = false + } + willPreinitialize := canInitializeSelf || canDriverUnmarshal || canUnmarshalFromDict // Early check that we're not mis-using `Validator`. _, err = canInterface(typ, validatorInterface) @@ -1239,8 +1195,9 @@ func initializationData(path string, typ reflect.Type, options staticOptions) (i } return initializationMetadata{ - canInitializeSelf: canInitializeSelf, - canUnmarshalSelf: canUnmarshalSelf, - willPreinitialize: willPreinitialize, + canInitializeSelf: canInitializeSelf, + canDriverUnmarshal: canDriverUnmarshal, + willPreinitialize: willPreinitialize, + canUnmarshalFromDict: canUnmarshalFromDict, }, nil } diff --git a/deserialize/deserialize_test.go b/deserialize/deserialize_test.go index dca76db..b87ec06 100644 --- a/deserialize/deserialize_test.go +++ b/deserialize/deserialize_test.go @@ -39,7 +39,8 @@ type PrimitiveTypesStruct struct { } type SimpleArrayStruct struct { - SomeArray []string + SomeSlice []string + SomeArray [3]string } type ArrayOfStructs struct { @@ -92,8 +93,8 @@ func twoWaysGeneric[Input any, Output any](t *testing.T, sample Input) (*Output, } return deserializer.DeserializeDict(dict) //nolint:wrapcheck } -func twoWaysList[T any](t *testing.T, samples []T) ([]T, error) { - return twoWaysListGeneric[T, T](t, samples) +func twoWays[T any](t *testing.T, sample T) (*T, error) { + return twoWaysGeneric[T, T](t, sample) } func twoWaysListGeneric[Input any, Output any](t *testing.T, samples []Input) ([]Output, error) { @@ -123,8 +124,8 @@ func twoWaysListGeneric[Input any, Output any](t *testing.T, samples []Input) ([ } return deserializer.DeserializeList(list) //nolint:wrapcheck } -func twoWays[T any](t *testing.T, sample T) (*T, error) { - return twoWaysGeneric[T, T](t, sample) +func twoWaysList[T any](t *testing.T, samples []T) ([]T, error) { + return twoWaysListGeneric[T, T](t, samples) } func TestMissingOptions(t *testing.T) { @@ -278,18 +279,17 @@ func TestDeserializeDeepMissingField(t *testing.T) { } func TestDeserializeSimpleArray(t *testing.T) { - array := []string{"one", "ttwo", "three"} + array := [3]string{"one", "ttwo", "three"} + slice := []string{"four", "fife", "six"} before := SimpleArrayStruct{ + SomeSlice: slice, SomeArray: array, } after, err := twoWays(t, before) if err != nil { t.Error(err) } - assert.Equal(t, len(after.SomeArray), len(before.SomeArray), "We should have recovered the same struct") - for i := 0; i < len(before.SomeArray); i++ { - assert.Equal(t, after.SomeArray[i], before.SomeArray[i], "We should have recovered the same struct") - } + assert.DeepEqual(t, after, &before) } func TestDeserializeArrayOfStruct(t *testing.T) { @@ -396,7 +396,6 @@ func TestKVListSimple(t *testing.T) { deserialized, err := deserializer.DeserializeKVList(entry) assert.NilError(t, err) - assert.Equal(t, *deserialized, sample, "We should have extracted the expected value") } @@ -586,14 +585,16 @@ func TestStructDefaultValuesNestedStruct(t *testing.T) { // Test that default values are parsed. func TestSliceDefaultValues(t *testing.T) { type ArrayWithDefault struct { - Data []EmptyStruct `default:"[]"` + Data []EmptyStruct `default:"[]"` + Data2 [0]EmptyStruct `default:"[]"` } result, err := twoWaysGeneric[EmptyStruct, ArrayWithDefault](t, EmptyStruct{}) assert.NilError(t, err) assert.Equal(t, len(result.Data), 0, "Deserialization should have inserted default values") } -// Test that default values are parsed. +// ---- Test that a bad default value will cause an error while generating the deserializer. + func TestBadDefaultValues(t *testing.T) { type PrimitiveTypesStructWithDefault struct { SomeBool bool `default:"blue"` @@ -613,38 +614,105 @@ func TestBadDefaultValues(t *testing.T) { assert.ErrorContains(t, err, "cannot parse default value at PrimitiveTypesStruct", "Deserialization should have parsed strings") } +// ------ + +// ---- Test that a good default value will succeed. + type SimpleStructWithOrMethodSuccess struct { - SomeString string `orMethod:"MakeString"` + SomeString string `orMethod:"MakeString"` + SomeSlice []string `orMethod:"MakeStringSlice"` + SomeMap map[string]string `orMethod:"MakeMap"` } func (SimpleStructWithOrMethodSuccess) MakeString() (string, error) { - return "Test value constructed with a method", nil + return "This is a string", nil +} + +func (SimpleStructWithOrMethodSuccess) MakeStringSlice() ([]string, error) { + return []string{"This is a slice"}, nil +} + +func (SimpleStructWithOrMethodSuccess) MakeMap() (map[string]string, error) { + return map[string]string{"zero": "This is a map"}, nil } // Test that an `orMethod` will be called if no value is provided (success case). func TestOrMethodSuccess(t *testing.T) { expected := SimpleStructWithOrMethodSuccess{ - SomeString: "Test value constructed with a method", + SomeString: "This is a string", + SomeSlice: []string{"This is a slice"}, + SomeMap: map[string]string{"zero": "This is a map"}, } result, err := twoWaysGeneric[EmptyStruct, SimpleStructWithOrMethodSuccess](t, EmptyStruct{}) assert.NilError(t, err) - assert.Equal(t, *result, expected, "The method should have been called to inject a value") + assert.DeepEqual(t, *result, expected) } -type SimpleStructWithOrMethodError struct { +// ---- + +// ---- Test that an `orMethod` will be called if no value is provided (error case). --- + +type SimpleStructWithFlatOrMethodError struct { SomeString string `orMethod:"MakeString"` } -func (SimpleStructWithOrMethodError) MakeString() (string, error) { - return "Test value constructed with a method", fmt.Errorf("This is an error from SimpleStructWithOrMethodError") +func (SimpleStructWithFlatOrMethodError) MakeString() (string, error) { + return "Test value constructed with a method", fmt.Errorf("This is an error from SimpleStructWithFlatOrMethodError") +} + +type SimpleStructWithPtrOrMethodError struct { + SomeString *string `orMethod:"MakeString"` +} + +func (SimpleStructWithPtrOrMethodError) MakeString() (*string, error) { + return nil, fmt.Errorf("This is an error from SimpleStructWithPtrOrMethodError") +} + +type SimpleStructWithSliceOrMethodError struct { + SomeString []string `orMethod:"MakeStringSlice"` +} + +func (SimpleStructWithSliceOrMethodError) MakeStringSlice() ([]string, error) { + return []string{"Test value constructed with a method"}, fmt.Errorf("This is an error from SimpleStructWithSliceOrMethodError") +} + +type SimpleStructWithMapOrMethodError struct { + SomeString map[string]string `orMethod:"MakeStringMap"` +} + +func (SimpleStructWithMapOrMethodError) MakeStringMap() (map[string]string, error) { + return map[string]string{"zero": "Test value constructed with a method"}, fmt.Errorf("This is an error from SimpleStructWithMapOrMethodError") +} + +type SimpleStructWithStructOrMethodError struct { + SomeString SimpleStruct `orMethod:"MakeSimpleStruct"` +} + +func (SimpleStructWithStructOrMethodError) MakeSimpleStruct() (SimpleStruct, error) { + return SimpleStruct{}, fmt.Errorf("This is an error from SimpleStructWithStructOrMethodError") } -// Test that an `orMethod` will be called if no value is provided (error case). func TestOrMethodError(t *testing.T) { - _, err := twoWaysGeneric[EmptyStruct, SimpleStructWithOrMethodError](t, EmptyStruct{}) - assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithOrMethodError.SomeString\n\t * This is an error from SimpleStructWithOrMethodError", "The method should have been called to inject a value") + _, err := twoWaysGeneric[EmptyStruct, SimpleStructWithFlatOrMethodError](t, EmptyStruct{}) + assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithFlatOrMethodError.SomeString\n\t * This is an error from SimpleStructWithFlatOrMethodError", "The method should have been called to inject a value") + + _, err = twoWaysGeneric[EmptyStruct, SimpleStructWithPtrOrMethodError](t, EmptyStruct{}) + assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithPtrOrMethodError.SomeString\n\t * This is an error from SimpleStructWithPtrOrMethodError", "The method should have been called to inject a value") + + _, err = twoWaysGeneric[EmptyStruct, SimpleStructWithSliceOrMethodError](t, EmptyStruct{}) + assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithSliceOrMethodError.SomeString\n\t * This is an error from SimpleStructWithSliceOrMethodError", "The method should have been called to inject a value") + + _, err = twoWaysGeneric[EmptyStruct, SimpleStructWithStructOrMethodError](t, EmptyStruct{}) + assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithStructOrMethodError.SomeString\n\t * This is an error from SimpleStructWithStructOrMethodError", "The method should have been called to inject a value") + + _, err = twoWaysGeneric[EmptyStruct, SimpleStructWithMapOrMethodError](t, EmptyStruct{}) + assert.Equal(t, err.Error(), "error in optional value at SimpleStructWithMapOrMethodError.SomeString\n\t * This is an error from SimpleStructWithMapOrMethodError", "The method should have been called to inject a value") } +// ---------- + +// Test error cases for `onMethod` setup. + type SimpleStructWithOrMethodBadName struct { SomeString string `orMethod:"IDoNotExist"` } @@ -653,6 +721,7 @@ type SimpleStructWithOrMethodBadArgs struct { } func (SimpleStructWithOrMethodBadArgs) BadArgs(string) (string, error) { + // This method should not take any arguments. return "", nil } @@ -668,11 +737,22 @@ type SimpleStructWithOrMethodBadOut2 struct { SomeString string `orMethod:"BadOut2"` } -func (SimpleStructWithOrMethodBadOut1) BadOut2() (string, string) { +func (SimpleStructWithOrMethodBadOut2) BadOut2() (string, string) { return "", "" } -// Test error cases for `onMethod` setup. +type SimpleStructWithOrMethodBadOut3 struct { + SomeString string `orMethod:"BadOut3"` +} + +func (SimpleStructWithOrMethodBadOut3) BadOut3() (string, error, error) { + return "", nil, nil +} + +type SimpleStructWithOrMethodMissingMethod struct { + SomeString string `orMethod:"IDoNotExist"` +} + func TestOrMethodBadSetup(t *testing.T) { _, err := deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadName](deserialize.JSONOptions("")) //nolint:exhaustruct assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadName.SomeString with type string:\n\t * at SimpleStructWithOrMethodBadName.SomeString, failed to setup `orMethod`\n\t * method IDoNotExist provided with `orMethod` doesn't seem to exist - note that the method must be public", "We should fail early if the orMethod doesn't exist") @@ -684,9 +764,17 @@ func TestOrMethodBadSetup(t *testing.T) { assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadOut1.SomeInt with type int:\n\t * at SimpleStructWithOrMethodBadOut1.SomeInt, failed to setup `orMethod`\n\t * the method provided with `orMethod` MUST return (int, error) but it returns (string, _) which is not convertible to `int`", "We should fail early if first result is incorrect") _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadOut2](deserialize.JSONOptions("")) - assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadOut2.SomeString with type string:\n\t * at SimpleStructWithOrMethodBadOut2.SomeString, failed to setup `orMethod`\n\t * method BadOut2 provided with `orMethod` doesn't seem to exist - note that the method must be public", "We should fail early if second result is incorrect") + assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadOut2.SomeString with type string:\n\t * at SimpleStructWithOrMethodBadOut2.SomeString, failed to setup `orMethod`\n\t * the method provided with `orMethod` MUST return (string, error) but it returns (_, string) which is not convertible to `error`", "We should fail early if second result is incorrect") + + _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadOut3](deserialize.JSONOptions("")) + assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadOut3.SomeString with type string:\n\t * at SimpleStructWithOrMethodBadOut3.SomeString, failed to setup `orMethod`\n\t * the method provided with `orMethod` MUST return (string, error) but it returns 3 value(s)", "We should fail early if second result is incorrect") + + _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodMissingMethod](deserialize.JSONOptions("")) + assert.Equal(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodMissingMethod.SomeString with type string:\n\t * at SimpleStructWithOrMethodMissingMethod.SomeString, failed to setup `orMethod`\n\t * method IDoNotExist provided with `orMethod` doesn't seem to exist - note that the method must be public", "We should fail early if second result is incorrect") } +// ------- + type NestedStructWithOrMethod struct { SomeStruct SimpleStruct `orMethod:"MakeSimpleStruct"` } @@ -835,6 +923,8 @@ func TestInitializerNested(t *testing.T) { assert.Equal(t, *result, expected, "Initializer should have initialized our structure") } +// ----- Test that we're correctly running pre-initialization. + type StructInitializerFaulty struct { SomeBool bool SomeString string @@ -870,7 +960,6 @@ func (s *StructInitializerFaulty) Initialize() error { var _ validation.Initializer = &StructInitializerFaulty{} // Type assertion. -// Test that we're correctly running pre-initialization at toplevel. func TestInitializerFaulty(t *testing.T) { sample := EmptyStruct{} _, err := twoWaysGeneric[EmptyStruct, StructInitializerFaulty](t, sample) @@ -879,8 +968,19 @@ func TestInitializerFaulty(t *testing.T) { asCustom := deserialize.CustomDeserializerError{} ok := errors.As(err, &asCustom) assert.Equal(t, ok, true, "the error should be a CustomDeserializerError") + + type ContainerStructFaulty struct { + Data StructInitializerFaulty + } + _, err = twoWaysGeneric[EmptyStruct, ContainerStructFaulty](t, sample) + assert.ErrorContains(t, err, "at ContainerStructFaulty.Data, encountered an error while initializing optional fields:\n\t * Test error", "Initializer should have initialized our structure") + + ok = errors.As(err, &asCustom) + assert.Equal(t, ok, true, "the error should be a CustomDeserializerError") } +// ----- + type StructUnmarshal struct { hidden string } @@ -985,3 +1085,117 @@ func TestBadMapMap(t *testing.T) { _, err := deserialize.MakeMapDeserializer[BadMap](deserialize.JSONOptions("")) assert.ErrorContains(t, err, "invalid map type at BadMap.Field, only map[string]T can be converted into a deserializer", "We should have detected that we cannot convert maps with such keys") } +func TestInvalidDefaultValues(t *testing.T) { + type RandomStruct struct { + } + type InvalidStruct struct { + Field RandomStruct `default:"abc"` + } + type InvalidPointer struct { + Field *RandomStruct `default:"abc"` + } + type InvalidSlice struct { + Field []RandomStruct `default:"abc"` + } + type InvalidArray struct { + Field [4]RandomStruct `default:"abc"` + } + _, err := deserialize.MakeMapDeserializer[InvalidStruct](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid `default` value", "We should have detected that we cannot convert maps with such keys") + + _, err = deserialize.MakeMapDeserializer[InvalidPointer](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid `default` value", "We should have detected that we cannot convert maps with such keys") + + _, err = deserialize.MakeMapDeserializer[InvalidSlice](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid `default` value", "We should have detected that we cannot convert maps with such keys") + + _, err = deserialize.MakeMapDeserializer[InvalidArray](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid `default` value", "We should have detected that we cannot convert maps with such keys") + +} + +func TestInvalidStruct(t *testing.T) { + _, err := deserialize.MakeKVListDeserializer[int](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid call to StructDeserializer: int is not a struct") + + _, err = deserialize.MakeMapDeserializer[int](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid call to StructDeserializer: int is not a struct") +} + +func TestInvalidTags(t *testing.T) { + type BadTag struct { + Field string `json:"abc" json:"abc"` + } + _, err := deserialize.MakeKVListDeserializer[BadTag](deserialize.JSONOptions("")) + assert.ErrorContains(t, err, "invalid tag, name json should only be defined once") +} + +// ------ Test that when a struct implements both `Unmarshaler` and `Initializer`, we +// default to `Unmarshaler`. + +type StructSupportBothUnmarshalerAndInitializer struct { + Field string +} + +func (s *StructSupportBothUnmarshalerAndInitializer) Initialize() error { + panic("this method shouldn't have been called") +} +func (s *StructSupportBothUnmarshalerAndInitializer) UnmarshalJSON(buf []byte) error { + container := map[string]string{} + err := json.Unmarshal(buf, &container) + if err != nil { + panic(err) + } + + s.Field = "test has succeeded with " + container["Field"] + return nil +} + +func TestSupportBothUnmarshalerAndInitializer(t *testing.T) { + sample := StructSupportBothUnmarshalerAndInitializer{ + Field: "some content", + } + result, err := twoWays[StructSupportBothUnmarshalerAndInitializer](t, sample) + assert.NilError(t, err) + assert.DeepEqual(t, result, &StructSupportBothUnmarshalerAndInitializer{Field: "test has succeeded with some content"}) +} + +// ------- + +// ------ Test that when a struct implements both `Unmarshaler` and `DictUnmarshaler`, we +// default to `DictUnmarshaler`. + +type StructSupportBothUnmarshalerAndDictUnmarshaler struct { + Field string +} + +func (s *StructSupportBothUnmarshalerAndDictUnmarshaler) UnmarshalJSON([]byte) error { + panic("this method shouldn't have been called") +} + +func (s *StructSupportBothUnmarshalerAndDictUnmarshaler) UnmarshalDict(in shared.Dict) error { + value, ok := in.Lookup("Field") + if !ok { + panic("missing key") + } + str, ok := value.Interface().(string) + if !ok { + panic("invalid value") + } + + s.Field = "test has succeeded with " + str + return nil +} + +var _ shared.UnmarshalDict = new(StructSupportBothUnmarshalerAndDictUnmarshaler) + +func TestSupportBothUnmarshalerAndDictInitializer(t *testing.T) { + sample := StructSupportBothUnmarshalerAndDictUnmarshaler{ + Field: "some content", + } + result, err := twoWays[StructSupportBothUnmarshalerAndDictUnmarshaler](t, sample) + assert.NilError(t, err) + assert.DeepEqual(t, result, &StructSupportBothUnmarshalerAndDictUnmarshaler{Field: "test has succeeded with some content"}) +} + +// ----- diff --git a/deserialize/json/json.go b/deserialize/json/json.go index 8f99f0e..ef45561 100644 --- a/deserialize/json/json.go +++ b/deserialize/json/json.go @@ -116,12 +116,25 @@ func (u Driver) Unmarshal(in any, out *any) (err error) { var buf []byte switch typed := in.(type) { + // Normalize string, []byte into []byte. case string: buf = []byte(typed) case []byte: buf = typed + // Unwrap Value. case Value: return u.Unmarshal(typed.wrapped, out) + case JSON: + if reflect.TypeOf(out).Elem() == dictionary { + *out = typed + return nil + } + + // Sadly, at this stage, we need to reserialize. + buf, err = json.Marshal(typed) + if err != nil { + return fmt.Errorf("internal error while deserializing: \n\t * %w", err) + } default: return fmt.Errorf("expected a string, got %s", in) } diff --git a/deserialize/kvlist/kvlist.go b/deserialize/kvlist/kvlist.go index ca1fff2..cb20aaa 100644 --- a/deserialize/kvlist/kvlist.go +++ b/deserialize/kvlist/kvlist.go @@ -98,13 +98,20 @@ func (u Driver) Unmarshal(in any, out *any) (err error) { buf = typed case Value: return u.Unmarshal(typed.wrapped, out) + case KVList: + if reflect.TypeOf(out).Elem() == kvList { + *out = typed + return nil + } + // Sadly, at this stage, we need to reserialize. + buf, err = json.Marshal(typed) + if err != nil { + return fmt.Errorf("internal error while deserializing: \n\t * %w", err) + } default: return fmt.Errorf("expected a string, got %s", in) } - if dict, ok := (*out).(*shared.Dict); ok { - return json.Unmarshal(buf, &dict) //nolint:wrapcheck - } if unmarshal, ok := (*out).(Unmarshaler); ok { return unmarshal.Unmarshal(buf) //nolint:wrapcheck } diff --git a/deserialize/shared/shared.go b/deserialize/shared/shared.go index a9d15a4..b62d214 100644 --- a/deserialize/shared/shared.go +++ b/deserialize/shared/shared.go @@ -123,3 +123,8 @@ func LookupParser(fieldType reflect.Type) *Parser { } return result } + +// A type that can be deserialized from a shared.Dict. +type UnmarshalDict interface { + UnmarshalDict(Dict) error +}