diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 31ddaae..ad787dd 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -27,4 +27,4 @@ jobs: # Require: The version of golangci-lint to use. # When `install-mode` is `binary` (default) the value can be v1.2 or v1.2.3 or `latest` to use the latest version. # When `install-mode` is `goinstall` the value can be v1.2.3, `latest`, or the hash of a commit. - version: v1.55 + version: v1.61.0 diff --git a/.golangci.yml b/.golangci.yml index f6bae36..16f9adf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -18,12 +18,12 @@ linters: - godox - gofmt - goimports - - gomnd - gomodguard - gosec - gosmopolitan - govet - mirror + - mnd - nosprintfhostport - perfsprint - prealloc diff --git a/deserialize/deserialize.go b/deserialize/deserialize.go index 0c936af..167f9de 100644 --- a/deserialize/deserialize.go +++ b/deserialize/deserialize.go @@ -81,7 +81,7 @@ import ( // -------- Public API -------- -type Unmarshaler shared.Driver +type Unmarshaler func() shared.Driver // Options for building a deserializer. // @@ -122,7 +122,7 @@ func JSONOptions(root string) Options { return Options{ MainTagName: "json", RootPath: root, - Unmarshaler: jsonPkg.Driver{}, + Unmarshaler: jsonPkg.Driver, } } @@ -137,7 +137,7 @@ func QueryOptions(root string) Options { return Options{ MainTagName: "query", RootPath: root, - Unmarshaler: kvlist.Driver{}, + Unmarshaler: kvlist.Driver, } } @@ -152,7 +152,7 @@ func PathOptions(root string) Options { return Options{ MainTagName: "path", RootPath: root, - Unmarshaler: kvlist.Driver{}, + Unmarshaler: kvlist.Driver, } } @@ -193,10 +193,12 @@ func MakeMapDeserializer[T any](options Options) (MapDeserializer[T], error) { if tagName == "" { return nil, errors.New("missing option MainTagName") } - return makeOuterStructDeserializer[T](options.RootPath, staticOptions{ + if options.Unmarshaler == nil { + return nil, errors.New("please specify an unmarshaler") + } + return makeOuterStructDeserializer[T](options.RootPath, innerOptions{ renamingTagName: tagName, - allowNested: true, - unmarshaler: options.Unmarshaler, + unmarshaler: options.Unmarshaler(), }) } func MakeMapDeserializerFromReflect(options Options, typ reflect.Type) (MapReflectDeserializer, error) { @@ -204,15 +206,17 @@ func MakeMapDeserializerFromReflect(options Options, typ reflect.Type) (MapRefle if tagName == "" { return nil, errors.New("missing option MainTagName") } + if options.Unmarshaler == nil { + return nil, errors.New("please specify an unmarshaler") + } var placeholder = reflect.New(typ).Elem() - staticOptions := staticOptions{ + innerOptions := innerOptions{ renamingTagName: tagName, - allowNested: true, - unmarshaler: options.Unmarshaler, + unmarshaler: options.Unmarshaler(), } noTags := tags.Empty() - reflectDeserializer, err := makeFieldDeserializerFromReflect(options.RootPath, typ, staticOptions, &noTags, placeholder, false, false) + reflectDeserializer, err := makeFieldDeserializerFromReflect(options.RootPath, typ, innerOptions, &noTags, placeholder, false) if err != nil { return nil, err @@ -237,27 +241,41 @@ func (mrd mapReflectDeserializer) DeserializeDictTo(dict shared.Dict, reflectOut } // Create a deserializer from (key, value list). +// +// `T` MUST have the following shape: +// +// struct { +// Field1 []Type1 // Optionally `query:"field1"` +// Field2 []Type2 // Optionally `query:"field2"` +// Field3 []Type3 // Optionally `query:"field3"` +// } +// +// Where each `TypeX` is either +// - int, intX, uintX, float, string, bool +// - a type that supports `UnmarshalText`. func MakeKVListDeserializer[T any](options Options) (KVListDeserializer[T], error) { tagName := options.MainTagName if tagName == "" { return nil, errors.New("missing option MainTagName") } - innerOptions := staticOptions{ + if options.Unmarshaler == nil { + return nil, errors.New("please specify an unmarshaler") + } + innerOptions := innerOptions{ renamingTagName: tagName, - allowNested: false, - unmarshaler: options.Unmarshaler, + unmarshaler: options.Unmarshaler(), } wrapped, err := makeOuterStructDeserializer[T](options.RootPath, innerOptions) if err != nil { return nil, err } deserializer := func(value kvlist.KVList, out *T) error { - normalized := make(jsonPkg.JSON) + normalized := make(map[string]any) err := deListMap[T](normalized, value, innerOptions) if err != nil { return fmt.Errorf("error attempting to deserialize from a list of entries:\n\t * %w", err) } - return wrapped.deserializer(normalized, out) + return wrapped.deserializer(kvlist.MakeRootDict(normalized), out) } return kvListDeserializer[T]{ deserializer: deserializer, @@ -269,14 +287,16 @@ func MakeKVDeserializerFromReflect(options Options, typ reflect.Type) (KVListRef if tagName == "" { return nil, errors.New("missing option MainTagName") } - innerOptions := staticOptions{ + if options.Unmarshaler == nil { + return nil, errors.New("please specify an unmarshaler") + } + innerOptions := innerOptions{ renamingTagName: tagName, - allowNested: false, - unmarshaler: options.Unmarshaler, + unmarshaler: options.Unmarshaler(), } var placeholder = reflect.New(typ).Elem() noTags := tags.Empty() - wrapped, err := makeFieldDeserializerFromReflect(".", typ, innerOptions, &noTags, placeholder, false, false) + wrapped, err := makeFieldDeserializerFromReflect(".", typ, innerOptions, &noTags, placeholder, false) if err != nil { return nil, err } @@ -290,7 +310,7 @@ func MakeKVDeserializerFromReflect(options Options, typ reflect.Type) (KVListRef type kvReflectDeserializer struct { reflectDeserializer reflectDeserializer - options staticOptions + options innerOptions typ reflect.Type } @@ -334,31 +354,27 @@ var _ error = CustomDeserializerError{} //nolint:exhaustruct // ----------------- Private -// Options used while setting up a deserializer. -type staticOptions struct { +type innerOptions struct { // The name of tag used for renamings (e.g. "json"). renamingTagName string - // If true, allow the outer struct to contain arrays, slices and inner structs. - // - // Otherwise, the outer struct is only allowed to contain flat types. - allowNested bool - - unmarshaler Unmarshaler + // The instance of the unmarshaling driver. + unmarshaler shared.Driver } // A deserializer from (key, value) maps. type mapDeserializer[T any] struct { deserializer func(value shared.Dict, out *T) error - options staticOptions + options innerOptions } func (me mapDeserializer[T]) DeserializeBytes(source []byte) (*T, error) { + unmarshaler := me.options.unmarshaler dict := new(any) - if err := me.options.unmarshaler.Unmarshal(source, dict); err != nil { + if err := unmarshaler.Unmarshal(source, dict); err != nil { return nil, fmt.Errorf("failed to deserialize source: \n\t * %w", err) } - asDict, ok := me.options.unmarshaler.WrapValue(*dict).AsDict() + asDict, ok := unmarshaler.WrapValue(*dict).AsDict() if !ok { return nil, errors.New("failed to deserialize as a dictionary") } @@ -396,7 +412,7 @@ func (me mapDeserializer[T]) DeserializeList(list []shared.Value) ([]T, error) { // A deserializer from (key, []string) maps. type kvListDeserializer[T any] struct { deserializer func(value kvlist.KVList, out *T) error - options staticOptions + options innerOptions } func (me kvListDeserializer[T]) DeserializeKVList(value kvlist.KVList) (*T, error) { @@ -410,7 +426,7 @@ func (me kvListDeserializer[T]) DeserializeKVList(value kvlist.KVList) (*T, erro // Convert a `map[string] []string` (as provided e.g. by the query parser) into a `Dict` // (as consumed by this parsing mechanism). -func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string][]string, options staticOptions) error { +func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string][]string, options innerOptions) error { if typ.Kind() != reflect.Struct { return fmt.Errorf("cannot implement a MapListDeserializer without a struct, got %s", typ.Name()) } @@ -434,33 +450,7 @@ func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string] fallthrough case reflect.Slice: outMap[*publicFieldName] = inMap[*publicFieldName] - case reflect.Bool: - fallthrough - case reflect.String: - fallthrough - case reflect.Float32: - fallthrough - case reflect.Float64: - fallthrough - case reflect.Int: - fallthrough - case reflect.Int8: - fallthrough - case reflect.Int16: - fallthrough - case reflect.Int32: - fallthrough - case reflect.Int64: - fallthrough - case reflect.Uint: - fallthrough - case reflect.Uint8: - fallthrough - case reflect.Uint16: - fallthrough - case reflect.Uint32: - fallthrough - case reflect.Uint64: + default: length := len(inMap[*publicFieldName]) switch length { case 0: // No value. @@ -469,13 +459,11 @@ func deListMapReflect(typ reflect.Type, outMap map[string]any, inMap map[string] default: return fmt.Errorf("cannot fit %d elements into a single entry of field %s.%s", length, typ.Name(), field.Name) } - default: - panic("This should not happen") } } return nil } -func deListMap[T any](outMap map[string]any, inMap map[string][]string, options staticOptions) error { +func deListMap[T any](outMap map[string]any, inMap map[string][]string, options innerOptions) error { var placeholder T reflectedT := reflect.TypeOf(placeholder) return deListMapReflect(reflectedT, outMap, inMap, options) @@ -495,10 +483,14 @@ var errorInterface = reflect.TypeOf((*error)(nil)).Elem() const JSON = "json" -func makeOuterStructDeserializerFromReflect(path string, options staticOptions, container reflect.Value, typ reflect.Type) (*mapDeserializer[any], error) { - if options.unmarshaler == nil { - return nil, errors.New("please specify an unmarshaler") +func makeOuterStructDeserializerFromReflect(path string, options innerOptions, container reflect.Value, typ reflect.Type) (*mapDeserializer[any], error) { + err := options.unmarshaler.Enter(path, typ) + if err != nil { + return nil, err //nolint:wrapcheck } + defer func() { + options.unmarshaler.Exit(typ) + }() initializationMetadata, err := initializationData(path, typ, options) if err != nil { @@ -563,7 +555,7 @@ func makeOuterStructDeserializerFromReflect(path string, options staticOptions, // - `path` a human-readable path (e.g. the name of the endpoint) or "" if you have nothing // useful for human beings; // - `tagName` the name of tags to use for field renamings, e.g. `query`. -func makeOuterStructDeserializer[T any](path string, options staticOptions) (*mapDeserializer[T], error) { +func makeOuterStructDeserializer[T any](path string, options innerOptions) (*mapDeserializer[T], error) { container := new(T) // An uninitialized container, used to extract type information and call initializer methods. // Pre-check if we're going to perform initialization. @@ -592,7 +584,7 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma // - `typ` the dynamic type for the struct being compiled; // - `tags` the table of tags for this field. // - `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) { +func makeStructDeserializerFromReflect(path string, typ reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreInitialized bool) (reflectDeserializer, error) { if typ.Kind() != reflect.Struct { return nil, fmt.Errorf("invalid call to StructDeserializer: %s is not a struct", path) } @@ -641,7 +633,7 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st fieldPath := fmt.Sprint(path, ".", *publicFieldName) var fieldContentDeserializer reflectDeserializer - fieldContentDeserializer, err = makeFieldDeserializerFromReflect(fieldPath, fieldType, options, &tags, selfContainer, willPreinitialize, true) + fieldContentDeserializer, err = makeFieldDeserializerFromReflect(fieldPath, fieldType, options, &tags, selfContainer, willPreinitialize) if err != nil { return nil, err } @@ -718,8 +710,8 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st 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) + // Validation error, abort struct construction, wrap the error so that we can catch it. + err = validation.WrapError(path, err) result = reflect.Zero(typ) } } @@ -787,7 +779,7 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st } } outPtr.Set(result) - return nil + return err } return result, nil } @@ -798,7 +790,15 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st // - `typ` the dynamic type for the struct being compiled; // - `tags` the table of tags for this field. // - `wasPreinitialized` if this value was preinitialized, typically through `Initializer` -func makeMapDeserializerFromReflect(path string, typ reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreInitialized bool) (reflectDeserializer, error) { +func makeMapDeserializerFromReflect(path string, typ reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreInitialized bool) (reflectDeserializer, error) { + err := options.unmarshaler.Enter(path, typ) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer func() { + options.unmarshaler.Exit(typ) + }() + if typ.Kind() != reflect.Map { panic(fmt.Sprintf("invalid call: %s is not a map", path)) } @@ -820,7 +820,7 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati subPath := path + "[]" subTags := tagsPkg.Empty() subTyp := typ.Elem() - contentDeserializer, err := makeFieldDeserializerFromReflect(subPath, subTyp, options, &subTags, selfContainer, initializationMetadata.willPreinitialize, true) + contentDeserializer, err := makeFieldDeserializerFromReflect(subPath, subTyp, options, &subTags, selfContainer, initializationMetadata.willPreinitialize) if err != nil { return nil, err } @@ -903,7 +903,7 @@ func makeMapDeserializerFromReflect(path string, typ reflect.Type, options stati // - `typ` the dynamic type for the slice being compiled; // - `tagName` the name of tags to use for field renamings, e.g. `query`; // - `tags` the table of tags for this field. -func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { +func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { arrayPath := fmt.Sprint(fieldPath, "[]") isEmptyDefault := false if defaultSource := tags.Default(); defaultSource != nil { @@ -929,7 +929,7 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options sta // Prepare a deserializer for elements in this slice. childPreinitialized := wasPreinitialized || tags.IsPreinitialized() - elementDeserializer, err := makeFieldDeserializerFromReflect(arrayPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized, true) + elementDeserializer, err := makeFieldDeserializerFromReflect(arrayPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized) if err != nil { return nil, fmt.Errorf("failed to generate a deserializer for %s\n\t * %w", fieldPath, err) } @@ -1006,13 +1006,21 @@ func makeSliceDeserializer(fieldPath string, fieldType reflect.Type, options sta // - `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, wasNested bool) (reflectDeserializer, error) { +func makePointerDeserializer(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { + err := options.unmarshaler.Enter(fieldPath, fieldType) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer func() { + options.unmarshaler.Exit(fieldType) + }() + ptrPath := fmt.Sprint(fieldPath, "*") elemType := fieldType.Elem() subTags := tagsPkg.Empty() subContainer := reflect.New(fieldType).Elem() childPreinitialized := wasPreinitialized || tags.IsPreinitialized() - elementDeserializer, err := makeFieldDeserializerFromReflect(ptrPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized, wasNested) + elementDeserializer, err := makeFieldDeserializerFromReflect(ptrPath, fieldType.Elem(), options, &subTags, subContainer, childPreinitialized) if err != nil { return nil, fmt.Errorf("failed to generate a deserializer for %s\n\t * %w", fieldPath, err) } @@ -1079,7 +1087,15 @@ func makePointerDeserializer(fieldPath string, fieldType reflect.Type, options s // - `typ` the dynamic type for the field being compiled; // - `tagName` the name of tags to use for field renamings, e.g. `query`; // - `tags` the table of tags for this field. -func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { +func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { + err := options.unmarshaler.Enter(fieldPath, fieldType) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer func() { + options.unmarshaler.Exit(fieldType) + }() + typeName := typeName(fieldType) if typeName == "" { typeName = fieldPath @@ -1104,7 +1120,7 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options unmarshaler = &u } // Early check that we're not misusing Validator. - _, err := canInterface(fieldType, validatorInterface) + _, err = canInterface(fieldType, validatorInterface) if err != nil { return nil, err } @@ -1180,9 +1196,14 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options return fmt.Errorf("invalid value at %s, expected %s, got ", fieldPath, typeName) } } else { - // Case 2: we're not dealing with `nil`. In such a case, we'll gently ask Go to pretty - // please `Convert` the input. - ok := reflectedInput.CanConvert(fieldType) + // Case 2: we're not dealing with `nil`. In such a case, let's first unwrap any `shared.Value`. + // Then we'll gently ask Go to pretty please `Convert` the input. + unwrapped, ok := input.(shared.Value) + if ok { + input = unwrapped.Interface() + reflectedInput = reflect.ValueOf(input) + } + ok = reflectedInput.CanConvert(fieldType) if !ok { // The input cannot be converted? // @@ -1192,7 +1213,7 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options if parser != nil { if inputString, ok := input.(string); ok { // The input is represented as a string, but we're not looking for a - // string. This can happen e.g. for queries or paths, for which + // string. This can happen e.g. for queries, for which // everything is a string, or for json bodies, in case of client error. // // Regardless, let's try and convert. @@ -1230,33 +1251,28 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options // - `typ` the dynamic type for the field being compiled; // - `tagName` the name of tags to use for field renamings, e.g. `query`; // - `tags` the table of tags for this field. -func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool, wasNested bool) (reflectDeserializer, error) { +func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, options innerOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { + err := options.unmarshaler.Enter(fieldPath, fieldType) + if err != nil { + return nil, err //nolint:wrapcheck + } + defer func() { + options.unmarshaler.Exit(fieldType) + }() + var structured reflectDeserializer - var err error - var nestError error + switch fieldType.Kind() { case reflect.Pointer: - structured, err = makePointerDeserializer(fieldPath, fieldType, options, tags, container, wasPreinitialized, wasNested) + structured, err = makePointerDeserializer(fieldPath, fieldType, options, tags, container, wasPreinitialized) case reflect.Array: fallthrough case reflect.Slice: - if options.allowNested || !wasNested { - structured, err = makeSliceDeserializer(fieldPath, fieldType, options, tags, container, wasPreinitialized) - } else { - nestError = errors.New("this type of extractor does not support arrays/slices") - } + structured, err = makeSliceDeserializer(fieldPath, fieldType, options, tags, container, wasPreinitialized) case reflect.Struct: - if options.allowNested || !wasNested { - structured, err = makeStructDeserializerFromReflect(fieldPath, fieldType, options, tags, container, wasPreinitialized) - } else { - nestError = errors.New("this type of extractor does not support nested structs") - } + structured, err = makeStructDeserializerFromReflect(fieldPath, fieldType, options, tags, container, wasPreinitialized) case reflect.Map: - if options.allowNested || !wasNested { - structured, err = makeMapDeserializerFromReflect(fieldPath, fieldType, options, tags, container, wasPreinitialized) - } else { - nestError = errors.New("this type of extractor does not support nested maps") - } + structured, err = makeMapDeserializerFromReflect(fieldPath, fieldType, options, tags, container, wasPreinitialized) default: // We'll have to try with a flat field deserializer (see below). } @@ -1276,10 +1292,6 @@ func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, // Alright, we have a flat field deserializer and that's the only way we can deserialize this structure. return flat, nil } - if nestError != nil { - // We have no flat field deserializer and we had a pending nest error, time to unleash it. - return nil, nestError - } // Neither structured deserializer nor flat field deserializer, we can't deserialize at all. return nil, fmt.Errorf("could not generate a deserializer for %s with type %s:\n\t * %w", fieldPath, typeName(fieldType), flatError) } @@ -1290,8 +1302,9 @@ func makeFieldDeserializerFromReflect(fieldPath string, fieldType reflect.Type, // We have both a flat and a structured deserializer. Need to try both! var combined reflectDeserializer = func(slot *reflect.Value, data shared.Value) error { err := structured(slot, data) - if err == nil { - return nil + if err == nil || errors.As(err, &validation.Error{}) { //nolint:exhaustruct + // Don't try to recover from a validation error by switching to the next deserializer! + return err } err2 := flat(slot, data) if err2 == nil { @@ -1323,7 +1336,7 @@ func makeOrMethodConstructor(tags *tagsPkg.Tags, fieldType reflect.Type, contain switch { case typ.NumIn() != 0: return nil, fmt.Errorf("the method provided with `orMethod` MUST take no argument but takes %d arguments", typ.NumIn()) - case typ.NumOut() != 2: //nolint:gomnd + case typ.NumOut() != 2: //nolint:mnd return nil, fmt.Errorf("the method provided with `orMethod` MUST return (%s, error) but it returns %d value(s)", fieldType.Name(), typ.NumOut()) case !typ.Out(0).ConvertibleTo(fieldType): return nil, fmt.Errorf("the method provided with `orMethod` MUST return (%s, error) but it returns (%s, _) which is not convertible to `%s`", fieldType.Name(), typ.Out(0).Name(), fieldType.Name()) @@ -1370,7 +1383,7 @@ type initializationMetadata struct { willPreinitialize bool } -func initializationData(path string, typ reflect.Type, options staticOptions) (initializationMetadata, error) { +func initializationData(path string, typ reflect.Type, options innerOptions) (initializationMetadata, error) { // If this structure supports self-initialization or custom unmarshaling, we don't need (or use) // default fields and `orMethod` constructors. canInitializeSelf, err := canInterface(typ, initializerInterface) diff --git a/deserialize/deserialize_reflect_test.go b/deserialize/deserialize_reflect_test.go index 7f4f7d3..f1557be 100644 --- a/deserialize/deserialize_reflect_test.go +++ b/deserialize/deserialize_reflect_test.go @@ -15,7 +15,7 @@ func twoWaysReflect[Input any, Output any](t *testing.T, sample Input) (*Output, var placeholderOutput Output typeOutput := reflect.TypeOf(placeholderOutput) deserializer, err := deserialize.MakeMapDeserializerFromReflect(deserialize.Options{ - Unmarshaler: jsonPkg.Driver{}, + Unmarshaler: jsonPkg.Driver, MainTagName: "json", RootPath: "", }, typeOutput) @@ -70,7 +70,7 @@ func TestReflectKVDeserializer(t *testing.T) { Int: 123, } deserializer, err := deserialize.MakeKVDeserializerFromReflect(deserialize.Options{ - Unmarshaler: jsonPkg.Driver{}, + Unmarshaler: jsonPkg.Driver, MainTagName: "json", RootPath: "", }, reflect.TypeOf(sample)) diff --git a/deserialize/deserialize_test.go b/deserialize/deserialize_test.go index aac7ea7..1f3b898 100644 --- a/deserialize/deserialize_test.go +++ b/deserialize/deserialize_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" "testing" "time" @@ -76,7 +77,7 @@ var _ validation.Validator = &ValidatedStruct{} // Type assertion. func twoWaysGeneric[Input any, Output any](t *testing.T, sample Input) (*Output, error) { deserializer, err := deserialize.MakeMapDeserializer[Output](deserialize.Options{ - Unmarshaler: jsonPkg.Driver{}, + Unmarshaler: jsonPkg.Driver, MainTagName: "json", }) if err != nil { @@ -103,7 +104,7 @@ func twoWays[T any](t *testing.T, sample T) (*T, error) { func twoWaysListGeneric[Input any, Output any](t *testing.T, samples []Input) ([]Output, error) { deserializer, err := deserialize.MakeMapDeserializer[Output](deserialize.Options{ - Unmarshaler: jsonPkg.Driver{}, + Unmarshaler: jsonPkg.Driver, MainTagName: "json", }) if err != nil { @@ -124,7 +125,7 @@ func twoWaysListGeneric[Input any, Output any](t *testing.T, samples []Input) ([ } list := []shared.Value{} for _, entry := range unmarshalList { - list = append(list, jsonPkg.Driver{}.WrapValue(entry)) + list = append(list, jsonPkg.Driver().WrapValue(entry)) } return deserializer.DeserializeList(list) //nolint:wrapcheck } @@ -325,7 +326,8 @@ func TestValidationFailureField(t *testing.T) { SomeEmail: "someone+example.com", } _, err := twoWays(t, before) - assert.Equal(t, err.Error(), "deserialized value ValidatedStruct did not pass validation\n\t * Invalid email", "Validation should have caught the error") + assert.ErrorContains(t, err, "ValidatedStruct") + assert.ErrorContains(t, err, "Invalid email") } func TestValidationFailureFieldField(t *testing.T) { @@ -336,7 +338,8 @@ func TestValidationFailureFieldField(t *testing.T) { }, } _, err := twoWays(t, before) - assert.Equal(t, err.Error(), "deserialized value Pair[int,ValidatedStruct].right did not pass validation\n\t * Invalid email", "Validation should have caught the error") + assert.ErrorContains(t, err, ".right") + assert.ErrorContains(t, err, "Invalid email") } func TestValidationFailureArray(t *testing.T) { @@ -347,7 +350,8 @@ func TestValidationFailureArray(t *testing.T) { Data: array, } _, err := twoWays(t, before) - assert.Equal(t, err.Error(), "error while deserializing Array[ValidatedStruct].Data[0]:\n\t * deserialized value Array[ValidatedStruct].Data[] did not pass validation\n\t * Invalid email", "Validation should have caught the error") + assert.ErrorContains(t, err, "Data[0]") + assert.ErrorContains(t, err, "Invalid email") } func TestKVListSimple(t *testing.T) { @@ -389,6 +393,7 @@ 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") + } // Test that if we place a string instead of a primitive type, this string @@ -1325,3 +1330,154 @@ func TestKVDeserializeWithPrivate(t *testing.T) { assert.NilError(t, err) assert.Equal(t, *deserialized, sample) } + +// ------ Test that we can deserialize things more complicated than just `[]string` with KVList + +type StructWithPrimitiveSlices struct { + SomeStrings []string + SomeInts []int + SomeInt8 []int8 + SomeInt16 []int16 + SomeInt32 []int32 + SomeInt64 []int64 + SomeUints []uint + SomeUint8 []uint8 + SomeUint16 []uint16 + SomeUint32 []uint32 + SomeUint64 []uint64 + SomeBools []bool + SomeFloat32 []float32 + SomeFloat64 []float64 +} + +func TestKVDeserializePrimitiveSlices(t *testing.T) { + deserializer, err := deserialize.MakeKVListDeserializer[StructWithPrimitiveSlices](deserialize.QueryOptions("")) + assert.NilError(t, err) + + sample := StructWithPrimitiveSlices{ + SomeStrings: []string{"abc", "def"}, + SomeInts: []int{15, 0, -15}, + SomeInt8: []int8{0, -2, 4, 8}, + SomeInt16: []int16{16, -32, 64}, + SomeInt32: []int32{128, -256, 512}, + SomeInt64: []int64{1024, -2048, 4096}, + SomeUints: []uint{0, 2, 4, 8}, + SomeUint8: []uint8{16, 32, 64, 128}, + SomeUint16: []uint16{256, 512, 1024, 2048}, + SomeUint32: []uint32{4096, 8192, 16364}, + SomeUint64: []uint64{32768, 65536}, + SomeBools: []bool{true, true, false, true}, + SomeFloat32: []float32{3.1415, 1.2}, + SomeFloat64: []float64{42.0}, + } + + kvlist := make(map[string][]string, 0) + + kvlist["SomeStrings"] = []string{"abc", "def"} + kvlist["SomeInts"] = []string{"15", "0", "-15"} + kvlist["SomeInt8"] = []string{"0", "-2", "4", "8"} + kvlist["SomeInt16"] = []string{"16", "-32", "64"} + kvlist["SomeInt32"] = []string{"128", "-256", "512"} + kvlist["SomeInt64"] = []string{"1024", "-2048", "4096"} + kvlist["SomeUints"] = []string{"0", "2", "4", "8"} + kvlist["SomeUint8"] = []string{"16", "32", "64", "128"} + kvlist["SomeUint16"] = []string{"256", "512", "1024", "2048"} + kvlist["SomeUint32"] = []string{"4096", "8192", "16364"} + kvlist["SomeUint64"] = []string{"32768", "65536"} + kvlist["SomeBools"] = []string{"true", "true", "false", "true"} + kvlist["SomeFloat32"] = []string{"3.1415", "1.2"} + kvlist["SomeFloat64"] = []string{"42.0"} + + deserialized, err := deserializer.DeserializeKVList(kvlist) + assert.NilError(t, err) + assert.DeepEqual(t, *deserialized, sample) + +} + +func TestDeserializeUUIDKVList(t *testing.T) { + deserializer, err := deserialize.MakeKVListDeserializer[StructWithUUID](deserialize.QueryOptions("")) + assert.NilError(t, err) + + // This is deserializable because the field supports `TextUnmarshal` + sample := StructWithUUID{ + Field: TextUnmarshalerUUID(uuid.New()), + } + + marshaledField, err := uuid.UUID(sample.Field).MarshalText() + assert.NilError(t, err) + kvlist := make(map[string][]string, 0) + kvlist["Field"] = []string{string(marshaledField)} + + deserialized, err := deserializer.DeserializeKVList(kvlist) + assert.NilError(t, err) + assert.DeepEqual(t, *deserialized, sample) +} + +// ------ Test that KVList detects structures that it cannot deserialize + +// A struct that just can't be deserialized. +type StructWithChan struct { + Chan chan int +} + +func TestKVCannotDeserializeChan(t *testing.T) { + _, err := deserialize.MakeKVListDeserializer[StructWithChan](deserialize.QueryOptions("")) + if err == nil { + t.Fatal("this should have failed") + } + assert.ErrorContains(t, err, "chan int") +} + +// ------ Test that KVList calls validation + +type CustomStructWithValidation struct { + Field int +} + +func (c *CustomStructWithValidation) Validate() error { + if c.Field < 0 { + return errors.New("custom validation error") + } + return nil +} + +func (c *CustomStructWithValidation) UnmarshalText(source []byte) error { + result, err := strconv.Atoi(string(source)) + if err != nil { + return err //nolint:wrapcheck + } + c.Field = result + return nil +} + +func TestKVCallsInnerValidation(t *testing.T) { + type Struct struct { + Inner CustomStructWithValidation + } + deserializer, err := deserialize.MakeKVListDeserializer[Struct](deserialize.QueryOptions("")) + assert.NilError(t, err) + + goodSample := Struct{ + Inner: CustomStructWithValidation{ + Field: 123, + }, + } + + kvlist := make(map[string][]string, 0) + kvlist["Inner"] = []string{strconv.Itoa(goodSample.Inner.Field)} + + deserialized, err := deserializer.DeserializeKVList(kvlist) + assert.NilError(t, err) + assert.DeepEqual(t, *deserialized, goodSample) + + badSample := Struct{ + Inner: CustomStructWithValidation{ + Field: -123, + }, + } + + kvlist["Inner"] = []string{strconv.Itoa(badSample.Inner.Field)} + + _, err = deserializer.DeserializeKVList(kvlist) + assert.ErrorContains(t, err, "custom validation error") +} diff --git a/deserialize/json/json.go b/deserialize/json/json.go index aea2797..6a5f879 100644 --- a/deserialize/json/json.go +++ b/deserialize/json/json.go @@ -12,7 +12,11 @@ import ( ) // The deserialization driver for JSON. -type Driver struct{} +type driver struct{} + +func Driver() shared.Driver { + return driver{} +} // A JSON value. type Value struct { @@ -91,7 +95,7 @@ var textUnmarshaler = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() // - `typ` implements `json.Unmarshaler`. // // You probably won't ever need to call this method. -func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { +func (driver) ShouldUnmarshal(typ reflect.Type) bool { if typ.ConvertibleTo(dictionary) { return true } @@ -102,7 +106,7 @@ func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { // Perform unmarshaling. // // You probably won't ever need to call this method. -func (u Driver) Unmarshal(in any, out *any) (err error) { +func (u driver) Unmarshal(in any, out *any) (err error) { defer func() { // Attempt to intercept errors that leak implementation details. if err != nil { @@ -163,10 +167,18 @@ func (u Driver) Unmarshal(in any, out *any) (err error) { return fmt.Errorf("failed to unmarshal '%s': \n\t * %w", buf, err) } -func (u Driver) WrapValue(wrapped any) shared.Value { +func (driver) WrapValue(wrapped any) shared.Value { return Value{ wrapped: wrapped, } } -var _ shared.Driver = Driver{} // Type assertion. +func (driver) Enter(string, reflect.Type) error { + // No particular protocol to follow. + return nil +} +func (driver) Exit(reflect.Type) { + // No particular protocol to follow. +} + +var _ shared.Driver = driver{} // Type assertion. diff --git a/deserialize/kvlist/kvlist.go b/deserialize/kvlist/kvlist.go index cbf07d4..fe7f7b5 100644 --- a/deserialize/kvlist/kvlist.go +++ b/deserialize/kvlist/kvlist.go @@ -10,21 +10,80 @@ import ( ) // The deserialization driver for (k, value list). -type Driver struct{} +// +// The fields of this value are used only while building the deserializer. +type driver struct { + // If non-nil, we have entered the root struct while building the deserializer + // and this points to the type of the root struct. + enteredStructAt *reflect.Type + + // If non-nil, we have entered a slice or array field within the root struct + // while building the deserializer and this points to the type of the slice or + // array field. + enteredSliceAt *reflect.Type + + // If non-nil, we have entered a leaf, i.e. either the contents of the slice or + // array field within the root struct or a data structure that supports `TextUnmarshaler`, + // while building the deserializer and this points to the type of the slice or array field. + enteredLeafAt *reflect.Type +} + +func Driver() shared.Driver { + return &driver{ + enteredStructAt: nil, + enteredSliceAt: nil, + enteredLeafAt: nil, + } +} // The type of a (key, value list) store. type KVList map[string][]string +type dict struct { + wrapped map[string]any +} + +func MakeRootDict(wrapped map[string]any) shared.Dict { + return dict{wrapped} +} + +func (d dict) Lookup(key string) (shared.Value, bool) { + v, ok := d.wrapped[key] + if !ok { + return Value{nil}, false + } + return Value{v}, true + +} + +func (d dict) AsValue() shared.Value { + return Value{ + wrapped: d.wrapped, + } +} + +func (d dict) Keys() []string { + keys := []string{} + for k := range d.wrapped { + keys = append(keys, k) + } + return keys +} + type Value struct { wrapped any } -// A KVValue may never be converted into a string. func (v Value) AsDict() (shared.Dict, bool) { + if asDict, ok := v.wrapped.(map[string]any); ok { + return dict{ + wrapped: asDict, + }, true + } return nil, false } func (v Value) Interface() any { - return v + return v.wrapped } func (v Value) AsSlice() ([]shared.Value, bool) { if wrapped, ok := v.wrapped.([]any); ok { @@ -34,6 +93,13 @@ func (v Value) AsSlice() ([]shared.Value, bool) { } return result, true } + if wrapped, ok := v.wrapped.([]string); ok { + result := make([]shared.Value, len(wrapped)) + for i, value := range wrapped { + result[i] = Value{wrapped: value} + } + return result, true + } return nil, false } @@ -79,7 +145,7 @@ var textUnmarshaler = reflect.TypeOf(new(encoding.TextUnmarshaler)).Elem() // For KVList, this is the case if: // - `typ` represents a KVList; and/or // - `typ` implements `Unmarshaler`. -func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { +func (u *driver) ShouldUnmarshal(typ reflect.Type) bool { if typ.ConvertibleTo(kvList) { return true } @@ -93,7 +159,7 @@ func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { } // Perform unmarshaling. -func (u Driver) Unmarshal(in any, out *any) (err error) { +func (u *driver) Unmarshal(in any, out *any) (err error) { var buf []byte switch typed := in.(type) { case string: @@ -124,10 +190,107 @@ func (u Driver) Unmarshal(in any, out *any) (err error) { return errors.New("this type cannot be deserialized") } -func (u Driver) WrapValue(wrapped any) shared.Value { +func (u *driver) WrapValue(wrapped any) shared.Value { return Value{ wrapped: wrapped, } } -var _ shared.Driver = Driver{} //nolint:exhaustruct +func canBeALeaf(typ reflect.Type) bool { + switch typ.Kind() { + // Primitive-ish types that can be trivially parsed. + case reflect.Float32: + fallthrough + case reflect.Float64: + fallthrough + case reflect.Bool: + fallthrough + case reflect.Int: + fallthrough + case reflect.Int8: + fallthrough + case reflect.Int16: + fallthrough + case reflect.Int32: + fallthrough + case reflect.Int64: + fallthrough + case reflect.Uint: + fallthrough + case reflect.Uint8: + fallthrough + case reflect.Uint16: + fallthrough + case reflect.Uint32: + fallthrough + case reflect.Uint64: + fallthrough + case reflect.String: + return true + default: + // Types that can be unmarshaled. + return typ.Implements(textUnmarshaler) || typ.ConvertibleTo(textUnmarshaler) || reflect.PointerTo(typ).Implements(textUnmarshaler) || reflect.PointerTo(typ).ConvertibleTo(textUnmarshaler) + } +} + +func (u *driver) Enter(at string, typ reflect.Type) error { + kind := typ.Kind() + if kind == reflect.Pointer { + // ignore pointers entirely + return nil + } + switch { + // Initial state. + case u.enteredStructAt == nil: + if kind != reflect.Struct { + return fmt.Errorf("KVList deserialization expects a struct, got %s", typ.String()) + } + u.enteredStructAt = &typ + case u.enteredSliceAt == nil && u.enteredLeafAt == nil: + if u.enteredStructAt == nil { + panic("internal error: inconsistent state") + } + switch { + case canBeALeaf(typ): + u.enteredLeafAt = &typ + case kind == reflect.Array || kind == reflect.Slice: + u.enteredSliceAt = &typ + default: + return fmt.Errorf("KVList deserialization expects a struct of slices of trivially deserializable types, but at %s, got %s", at, typ.String()) + } + case u.enteredLeafAt == nil && u.enteredSliceAt != nil: + if u.enteredStructAt == nil { + panic("internal error: inconsistent state") + } + if canBeALeaf(typ) { + u.enteredSliceAt = &typ + } else { + return fmt.Errorf("KVList deserialization expects a struct of slices of trivially deserializable types, but at %s, got %s", at, typ.String()) + } + default: + if u.enteredStructAt == nil || u.enteredLeafAt == nil { + panic("internal error: inconsistent state") + } + // We're in a leaf, there isn't anything we can check. + } + + return nil +} + +func (u *driver) Exit(typ reflect.Type) { + kind := typ.Kind() + if kind == reflect.Pointer { + // ignore pointers entirely + return + } + switch { + case u.enteredLeafAt != nil && *u.enteredLeafAt == typ: + u.enteredLeafAt = nil + case u.enteredSliceAt != nil && *u.enteredSliceAt == typ: + u.enteredSliceAt = nil + case u.enteredStructAt != nil && *u.enteredStructAt == typ: + u.enteredStructAt = nil + } +} + +var _ shared.Driver = &driver{} //nolint:exhaustruct diff --git a/deserialize/shared/shared.go b/deserialize/shared/shared.go index b62d214..3ff7995 100644 --- a/deserialize/shared/shared.go +++ b/deserialize/shared/shared.go @@ -29,6 +29,12 @@ type Dict interface { // A driver for a specific type of deserialization. type Driver interface { + // A method called during deserializer construction whenever we enter a field. + Enter(string, reflect.Type) error + + // A method called during deserializer construction whenever we leave a field. + Exit(reflect.Type) + // Return true if we have a specific implementation of deserialization // for a given type, for instance, if that type implements a specific // deserialization interface. diff --git a/validation/validation.go b/validation/validation.go index a394d5c..852dc50 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -49,16 +49,34 @@ type Validator interface { // Use errors.As() or Unwrap() to expose the error returned by Validate(). type Error struct { // Where the validation error happened. - path *path + // + // As of this writing, the structured path is only constructed when + // we create the error by calling `validation.Validate`. + structuredPath *path + + // An unstructured path that may be provided when creating an `Error` + // manually. + unstructedPath string // The error returned by `Validate()`. wrapped error } +// Wrap an error as a validation error. +func WrapError(at string, wrapped error) Error { + return Error{ + structuredPath: nil, + unstructedPath: at, + wrapped: wrapped, + } +} + // Extract a human-readable string. func (v Error) Error() string { + serialized := v.unstructedPath + buf := []string{} - cursor := v.path + cursor := v.structuredPath for cursor != nil { switch cursor.kind { case kindField: @@ -78,11 +96,10 @@ func (v Error) Error() string { } cursor = cursor.prev } - serialized := "" for i := len(buf) - 1; i >= 0; i-- { serialized += buf[i] } - return fmt.Sprintf("validation error at %s:\n\t * %s", buf, v.wrapped.Error()) + return fmt.Sprintf("validation error at %s:\n\t * %s", serialized, v.wrapped.Error()) } // Unwrap the underlying validation error. @@ -213,8 +230,9 @@ func validateReflect(path *path, value reflect.Value) error { if validator, ok := asAny.(Validator); ok { if err := validator.Validate(); err != nil { return Error{ - wrapped: err, - path: path, + wrapped: err, + structuredPath: path, + unstructedPath: "", } } }