From ea7e0accd96ceb511142365e09be77745fc6419e Mon Sep 17 00:00:00 2001 From: David Teller Date: Thu, 21 Dec 2023 10:17:13 +0100 Subject: [PATCH] [REFACTOR] Removing hardcoded calls to json.Unmarshal --- deserialize/deserialize.go | 281 ++++++++++++++++++------------- deserialize/deserialize_test.go | 35 ++-- deserialize/internal/internal.go | 17 ++ deserialize/json/json.go | 67 ++++++++ deserialize/kvlist/kvlist.go | 55 ++++++ 5 files changed, 325 insertions(+), 130 deletions(-) create mode 100644 deserialize/internal/internal.go create mode 100644 deserialize/json/json.go create mode 100644 deserialize/kvlist/kvlist.go diff --git a/deserialize/deserialize.go b/deserialize/deserialize.go index 51d2d83..dd197fb 100644 --- a/deserialize/deserialize.go +++ b/deserialize/deserialize.go @@ -64,29 +64,28 @@ package deserialize import ( - "encoding/json" "fmt" "log/slog" "reflect" "strconv" "strings" + "github.com/pasqal-io/godasse/deserialize/internal" + jsonPkg "github.com/pasqal-io/godasse/deserialize/json" + "github.com/pasqal-io/godasse/deserialize/kvlist" tagsPkg "github.com/pasqal-io/godasse/deserialize/tags" "github.com/pasqal-io/godasse/validation" ) -// Options used while setting up a deserializer. -type staticOptions struct { - // The name of tag used for renamings (e.g. "json"). - renamingTagName string +// -------- Public API -------- - // 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 -} +type Unmarshaler = internal.Driver +type Dict = internal.Dict // Options for building a deserializer. +// +// See also JSONOptions, QueryOptions, etc. for reasonable +// default values. type Options struct { // The name of tags used for renamings (e.g. "json"). // @@ -105,15 +104,44 @@ type Options struct { // Optional. If you leave this blank, no human-readable // information will be added. RootPath string + + // An unmarshaler, used to deserialize values when they + // are provided as []byte or string. + Unmarshaler Unmarshaler } // The de facto JSON type in Go. -type Dict = map[string]any + +// A preset fit for consuming JSON. +// +// Params: +// - root A human-readable root (e.g. the name of the endpoint). Used only +// for error reporting. `""` is a perfectly acceptable root. +func JSONOptions(root string) Options { + return Options{ + MainTagName: "json", + RootPath: root, + Unmarshaler: jsonPkg.Driver{}, + } +} + +// A preset fit for consuming Queries. +// +// Params: +// - root A human-readable root (e.g. the name of the endpoint). Used only +// for error reporting. `""` is a perfectly acceptable root. +func QueryOptions(root string) Options { + return Options{ + MainTagName: "query", + RootPath: root, + Unmarshaler: kvlist.Driver{}, + } +} // A deserializer from strings or buffers. type Deserializer[To any] interface { - DeserializeJSONString(string) (*To, error) - DeserializeJSONBytes([]byte) (*To, error) + DeserializeString(string) (*To, error) + DeserializeBytes([]byte) (*To, error) } // A deserializers from dictionaries @@ -129,34 +157,10 @@ type MapDeserializer[To any] interface { // Use this to deserialize e.g. query strings. type KVListDeserializer[To any] interface { Deserializer[To] - DeserializeKVList(map[string][]string) (*To, error) -} - -// A deserializer from (key, value) maps. -type mapDeserializer[T any] func(value Dict) (*T, error) - -func (me mapDeserializer[T]) DeserializeMap(value Dict) (*T, error) { - return me(value) -} - -func (me mapDeserializer[T]) DeserializeJSONBytes(source []byte) (*T, error) { - dict := new(Dict) - err := json.Unmarshal(source, &dict) - if err != nil { - return nil, err //nolint:wrapcheck - } - return me(*dict) -} - -func (me mapDeserializer[T]) DeserializeJSONString(source string) (*T, error) { - return me.DeserializeJSONBytes([]byte(source)) + DeserializeKVList(kvlist.KVList) (*To, error) } // Create a deserializer from Dict. -// -// - `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. `json`. func MakeMapDeserializer[T any](options Options) (MapDeserializer[T], error) { tagName := options.MainTagName if tagName == "" { @@ -165,54 +169,109 @@ func MakeMapDeserializer[T any](options Options) (MapDeserializer[T], error) { return makeOuterStructDeserializer[T](options.RootPath, staticOptions{ renamingTagName: tagName, allowNested: true, + unmarshaler: options.Unmarshaler, }) } -type kvList = map[string][]string - -// A deserializer from (key, []string) maps. -type kvListDeserializer[T any] func(value kvList) (*T, error) - -func (me kvListDeserializer[T]) DeserializeKVList(value kvList) (*T, error) { - return me(value) -} - -func (me kvListDeserializer[T]) DeserializeJSONBytes(source []byte) (*T, error) { - dict := new(kvList) - err := json.Unmarshal(source, &dict) - if err != nil { - return nil, err //nolint:wrapcheck - } - return me(*dict) -} - -func (me kvListDeserializer[T]) DeserializeJSONString(source string) (*T, error) { - return me.DeserializeJSONBytes([]byte(source)) -} - +// Create a deserializer from (key, value list). func MakeKVListDeserializer[T any](options Options) (KVListDeserializer[T], error) { tagName := options.MainTagName if tagName == "" { - tagName = JSON + tagName = "query" } innerOptions := staticOptions{ renamingTagName: tagName, allowNested: false, + unmarshaler: options.Unmarshaler, } wrapped, err := makeOuterStructDeserializer[T](options.RootPath, innerOptions) if err != nil { return nil, err } - var result kvListDeserializer[T] = func(value map[string][]string) (*T, error) { + deserializer := func(value kvlist.KVList) (*T, error) { // Normalize the map[string][]any into Dict normalized := make(Dict) err := deListMap[T](normalized, value, innerOptions) if err != nil { return nil, fmt.Errorf("error attempting to deserialize from a list of entries:\n\t * %w", err) } - return (*wrapped)(normalized) + return wrapped.deserializer(normalized) } - return &result, nil + return kvListDeserializer[T]{ + deserializer: deserializer, + options: innerOptions, + }, nil +} + +// ----------------- Private + +// Options used while setting up a deserializer. +type staticOptions 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 +} + +// A deserializer from (key, value) maps. +type mapDeserializer[T any] struct { + deserializer func(value Dict) (*T, error) + options staticOptions +} + +func (me mapDeserializer[T]) DeserializeMap(value Dict) (*T, error) { + return me.deserializer(value) +} + +func (me mapDeserializer[T]) DeserializeBytes(source []byte) (*T, error) { + dict := new(Dict) + if !me.options.unmarshaler.ShouldUnmarshal(reflect.TypeOf(*dict)) { + return nil, fmt.Errorf("this deserializer does not support deserializing from bytes") + } + + var dictAny any = dict + err := me.options.unmarshaler.Unmarshal(source, &dictAny) + if err != nil { + return nil, err //nolint:wrapcheck + } + return me.deserializer(*dict) +} + +func (me mapDeserializer[T]) DeserializeString(source string) (*T, error) { + return me.DeserializeBytes([]byte(source)) +} + +// A deserializer from (key, []string) maps. +type kvListDeserializer[T any] struct { + deserializer func(value kvlist.KVList) (*T, error) + options staticOptions +} + +func (me kvListDeserializer[T]) DeserializeKVList(value kvlist.KVList) (*T, error) { + 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` @@ -290,10 +349,6 @@ type reflectDeserializer func(slot *reflect.Value, data *interface{}) error var canInitializeInterface = reflect.TypeOf((*validation.Initializer)(nil)).Elem() var canValidateInterface = reflect.TypeOf((*validation.Validator)(nil)).Elem() -// The interface `json.Unmarshaler`, which we use throughout the code -// to decode data structures that cannot be decoded natively from JSON. -var canUnmarshal = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem() - // The interface `error`. var errorInterface = reflect.TypeOf((*error)(nil)).Elem() @@ -309,6 +364,10 @@ const JSON = "json" func makeOuterStructDeserializer[T any](path string, options staticOptions) (*mapDeserializer[T], error) { container := new(T) // An uninitialized container, used to extract type information and call initializer methods. + if options.unmarshaler == nil { + panic("Please specify an unmarshaler") + } + // Pre-check if we're going to perform initialization. typ := reflect.TypeOf(*container) shouldPreinitialize, err := canInterface(typ, canInitializeInterface) @@ -328,31 +387,34 @@ func makeOuterStructDeserializer[T any](path string, options staticOptions) (*ma return nil, err } - var result mapDeserializer[T] = func(value Dict) (*T, error) { - result := new(T) - if shouldPreinitialize { - var resultAny any = result - initializer, ok := resultAny.(validation.Initializer) - var err error - if !ok { - err = fmt.Errorf("we have already checked that the result can be converted to `Initializer` but conversion has failed") - } else { - err = initializer.Initialize() + var result = mapDeserializer[T]{ + deserializer: func(value Dict) (*T, error) { + result := new(T) + if shouldPreinitialize { + var resultAny any = result + initializer, ok := resultAny.(validation.Initializer) + var err error + if !ok { + err = fmt.Errorf("we have already checked that the result can be converted to `Initializer` but conversion has failed") + } else { + err = initializer.Initialize() + } + 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 nil, err + + } } + resultSlot := reflect.ValueOf(result).Elem() + var input any = value + err := reflectDeserializer(&resultSlot, &input) 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 nil, err - } - } - resultSlot := reflect.ValueOf(result).Elem() - var input any = value - err := reflectDeserializer(&resultSlot, &input) - if err != nil { - return nil, err - } - return result, nil + return result, nil + }, + options: options, } return &result, nil } @@ -375,9 +437,10 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st if err != nil { return nil, err } - canUnmarshalSelf := options.renamingTagName == JSON && reflect.PointerTo(typ).Implements(canUnmarshal) + canUnmarshalSelf := options.unmarshaler.ShouldUnmarshal(typ) if canInitializeSelf && canUnmarshalSelf { slog.Warn("At %s, type %s supports both Initializer and Unmarshaler, defaulting to Unmarshaler") + canInitializeSelf = false } willPreinitialize := canInitializeSelf || canUnmarshalSelf @@ -444,7 +507,7 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st contentPtr = nil } } else { - // If the field is private, so we should ignore any data provided by + // If the field is private, we should ignore any data provided by // a client. contentPtr = nil } @@ -538,13 +601,8 @@ func makeStructDeserializerFromReflect(path string, typ reflect.Type, options st err = fmt.Errorf("invalid value at %s, expected a string holding a %s, got %s", path, typeName(typ), inValue) return err } - unmarshaler, ok := resultPtr.Interface().(json.Unmarshaler) - if !ok { - err = fmt.Errorf("result should support json.Unmarshaler but conversion failed") - slog.Error("Internal error during deserialization", "error", err) - } else { - err = unmarshaler.UnmarshalJSON([]byte(inString)) - } + resultPtrAny := resultPtr.Interface() + err = options.unmarshaler.Unmarshal([]byte(inString), &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 @@ -770,11 +828,11 @@ 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, _ staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { +func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, options staticOptions, tags *tagsPkg.Tags, container reflect.Value, wasPreinitialized bool) (reflectDeserializer, error) { typeName := typeName(fieldType) // A parser in case we receive our data as a string. - parser, err := makeParser(fieldType) + parser, err := makeParser(fieldType, options) if err != nil { return nil, fmt.Errorf("at %s, failed to build a parser:\n\t * %w", fieldPath, err) } @@ -837,12 +895,12 @@ func makeFlatFieldDeserializer(fieldPath string, fieldType reflect.Type, _ stati constructed, err := (*orMethod)() if err != nil { err = fmt.Errorf("error in optional value at %s\n\t * %w", fieldPath, err) - slog.Error("Internal error during deserialization", err) + slog.Error("Internal error during deserialization", "error", err) return err } input = constructed default: - return fmt.Errorf("missing primitive value at %s, expected %s", fieldPath, typeName) + return fmt.Errorf("missing value at %s, expected %s", fieldPath, typeName) } // Type check: can our value convert to the expected type? @@ -929,7 +987,7 @@ func typeName(typ reflect.Type) string { // A parser for strings into primitive values. type parser func(source string) (any, error) -func makeParser(fieldType reflect.Type) (*parser, error) { +func makeParser(fieldType reflect.Type, options staticOptions) (*parser, error) { var result *parser switch fieldType.Kind() { case reflect.Bool: @@ -1004,27 +1062,20 @@ func makeParser(fieldType reflect.Type) (*parser, error) { } result = &p default: - if fieldType.Implements(canUnmarshal) { + if options.unmarshaler.ShouldUnmarshal(fieldType) { var p parser = func(source string) (any, error) { - reflectedContent := reflect.New(fieldType) bytes := []byte(source) - unmarshaler, ok := reflectedContent.Interface().(json.Unmarshaler) - var err error - if !ok { - err = fmt.Errorf("result should support json.Unmarshaler but conversion failed") - slog.Error("internal error during deserialization", "error", err) - } else { - err = unmarshaler.UnmarshalJSON(bytes) - } + result := reflect.New(fieldType).Interface() + err := options.unmarshaler.Unmarshal(bytes, &result) if err != nil { err = fmt.Errorf("invalid string at, expected to be able to parse a %s:\n\t * %w", typeName(fieldType), err) return nil, err } - return unmarshaler, nil + return result, nil } result = &p } else { - return nil, fmt.Errorf("this type cannot be deserialized") + return nil, fmt.Errorf("type %s cannot be deserialized", fieldType) } } diff --git a/deserialize/deserialize_test.go b/deserialize/deserialize_test.go index 5dc9189..7af4840 100644 --- a/deserialize/deserialize_test.go +++ b/deserialize/deserialize_test.go @@ -10,6 +10,7 @@ import ( "github.com/pasqal-io/godasse/assertions/testutils" "github.com/pasqal-io/godasse/deserialize" + jsonPkg "github.com/pasqal-io/godasse/deserialize/json" "github.com/pasqal-io/godasse/validation" ) @@ -67,7 +68,9 @@ func (s *ValidatedStruct) Validate() error { 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{}) + deserializer, err := deserialize.MakeMapDeserializer[Output](deserialize.Options{ + Unmarshaler: jsonPkg.Driver{}, + }) if err != nil { t.Error(err) return nil, err @@ -91,7 +94,7 @@ func twoWays[T any](t *testing.T, sample T) (*T, error) { } func TestDeserializeMapBufAndString(t *testing.T) { - deserializer, err := deserialize.MakeMapDeserializer[PrimitiveTypesStruct](deserialize.Options{}) + deserializer, err := deserialize.MakeMapDeserializer[PrimitiveTypesStruct](deserialize.JSONOptions("")) if err != nil { t.Error(err) return @@ -119,14 +122,14 @@ func TestDeserializeMapBufAndString(t *testing.T) { } var result *PrimitiveTypesStruct - result, err = deserializer.DeserializeJSONBytes(buf) + result, err = deserializer.DeserializeBytes(buf) if err != nil { t.Error(err) return } testutils.AssertEqual(t, *result, sample, "We should have succeeded when deserializing from bytes") - result, err = deserializer.DeserializeJSONString(string(buf)) + result, err = deserializer.DeserializeString(string(buf)) if err != nil { t.Error(err) return @@ -176,7 +179,7 @@ func TestDeserializeMissingField(t *testing.T) { before := SimpleStruct{ SomeString: "text", } - re := regexp.MustCompile("missing primitive value at PrimitiveTypesStruct.*") + re := regexp.MustCompile("missing value at PrimitiveTypesStruct.*") _, err := twoWaysGeneric[SimpleStruct, PrimitiveTypesStruct](t, before) testutils.AssertRegexp(t, err.Error(), *re, "We should have recovered the same struct") } @@ -217,7 +220,7 @@ func TestDeserializeDeepMissingField(t *testing.T) { SomeString: "text", }, } - re := regexp.MustCompile(`missing primitive value at Pair\[int,PrimitiveTypesStruct\]\.right\..*`) + re := regexp.MustCompile(`missing value at Pair\[int,PrimitiveTypesStruct\]\.right\..*`) _, err := twoWaysGeneric[Pair[int, SimpleStruct], Pair[int, PrimitiveTypesStruct]](t, before) testutils.AssertRegexp(t, err.Error(), *re, "We should have recovered the same struct") } @@ -302,7 +305,7 @@ func TestValidationFailureArray(t *testing.T) { } func TestKVListDoesNotSupportNesting(t *testing.T) { - options := deserialize.Options{} //nolint:exhaustruct + options := deserialize.QueryOptions("") //nolint:exhaustruct _, err := deserialize.MakeKVListDeserializer[PrimitiveTypesStruct](options) testutils.AssertEqual(t, err, nil, "KVList parsing supports simple structurs") @@ -385,6 +388,7 @@ func TestConversionsFailure(t *testing.T) { SomeUint16 string SomeUint32 string SomeUint64 string + SomeString string } sample := StringAsPrimitiveTypesStruct{ SomeBool: "blue", @@ -399,6 +403,7 @@ func TestConversionsFailure(t *testing.T) { SomeUint16: "blue", SomeUint32: "blue", SomeUint64: "blue", + SomeString: "string", } _, err := twoWaysGeneric[StringAsPrimitiveTypesStruct, PrimitiveTypesStruct](t, sample) re := regexp.MustCompile("invalid value at PrimitiveTypesStruct.*, expected .*, got string") @@ -470,7 +475,7 @@ func TestStructDefaultValuesInvalidSyntax(t *testing.T) { Left T `default:"{}"` Right U `default:"{}"` } - _, err := deserialize.MakeMapDeserializer[PairWithDefaults[PairWithDefaults[EmptyStruct, int], int]](deserialize.Options{}) + _, err := deserialize.MakeMapDeserializer[PairWithDefaults[PairWithDefaults[EmptyStruct, int], int]](deserialize.JSONOptions("")) testutils.AssertEqual(t, err.Error(), "could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left with type PairWithDefaults[EmptyStruct,int]:\n\t * could not generate a deserializer for PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left.Right with type int:\n\t * cannot parse default value at PairWithDefaults[PairWithDefaults[EmptyStruct,int]·5,int].Left.Right\n\t * strconv.Atoi: parsing \"{}\": invalid syntax", "MakeMapDeserializer should have detected an error") } @@ -536,7 +541,7 @@ func TestBadDefaultValues(t *testing.T) { SomeUint32 uint32 `default:"blue"` SomeUint64 uint64 `default:"blue"` } - _, err := deserialize.MakeMapDeserializer[PrimitiveTypesStructWithDefault](deserialize.Options{}) //nolint:exhaustruct + _, err := deserialize.MakeMapDeserializer[PrimitiveTypesStructWithDefault](deserialize.JSONOptions("")) //nolint:exhaustruct re := regexp.MustCompile("cannot parse default value at PrimitiveTypesStruct.*") testutils.AssertRegexp(t, err.Error(), *re, "Deserialization should have parsed strings") } @@ -605,16 +610,16 @@ func (SimpleStructWithOrMethodBadOut1) BadOut2() (string, string) { // Test error cases for `onMethod` setup. func TestOrMethodBadSetup(t *testing.T) { - _, err := deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadName](deserialize.Options{}) //nolint:exhaustruct + _, err := deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadName](deserialize.JSONOptions("")) //nolint:exhaustruct testutils.AssertEqual(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") - _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadArgs](deserialize.Options{}) //nolint:exhaustruct + _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadArgs](deserialize.JSONOptions("")) testutils.AssertEqual(t, err.Error(), "could not generate a deserializer for SimpleStructWithOrMethodBadArgs.SomeString with type string:\n\t * at SimpleStructWithOrMethodBadArgs.SomeString, failed to setup `orMethod`\n\t * the method provided with `orMethod` MUST take no argument but takes 1 arguments", "We should fail early if orMethod args are incorrect") - _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadOut1](deserialize.Options{}) //nolint:exhaustruct + _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadOut1](deserialize.JSONOptions("")) testutils.AssertEqual(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.Options{}) //nolint:exhaustruct + _, err = deserialize.MakeMapDeserializer[SimpleStructWithOrMethodBadOut2](deserialize.JSONOptions("")) testutils.AssertEqual(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") } @@ -884,9 +889,9 @@ func (BadInitializeStruct) Initialize() error { // Should be `func (BadInitializ var _ validation.Initializer = BadInitializeStruct{} func TestBadValidate(t *testing.T) { - _, err := deserialize.MakeMapDeserializer[BadValidateStruct](deserialize.Options{}) //nolint:exhaustruct + _, err := deserialize.MakeMapDeserializer[BadValidateStruct](deserialize.JSONOptions("")) //nolint:exhaustruct testutils.AssertEqual(t, err.Error(), "type deserialize_test.BadValidateStruct implements validation.Validator - it should be implemented by pointer type *deserialize_test.BadValidateStruct instead", "We should have detected that this struct does not implement Validator correctly") - _, err = deserialize.MakeMapDeserializer[BadInitializeStruct](deserialize.Options{}) //nolint:exhaustruct + _, err = deserialize.MakeMapDeserializer[BadInitializeStruct](deserialize.JSONOptions("")) //nolint:exhaustruct testutils.AssertEqual(t, err.Error(), "type deserialize_test.BadInitializeStruct implements validation.Initializer - it should be implemented by pointer type *deserialize_test.BadInitializeStruct instead", "We should have detected that this struct does not implement Initializer correctly") } diff --git a/deserialize/internal/internal.go b/deserialize/internal/internal.go new file mode 100644 index 0000000..4da1069 --- /dev/null +++ b/deserialize/internal/internal.go @@ -0,0 +1,17 @@ +package internal + +import "reflect" + +// A dictionary. +type Dict = map[string]any + +// A driver for a specific type of deserialization. +type Driver interface { + // Return true if we have a specific implementation of deserialization + // for a given type, for instance, if that type implements a specific + // deserialization interface. + ShouldUnmarshal(reflect.Type) bool + + // Perform unmarshaling for a value. + Unmarshal([]byte, *any) error +} diff --git a/deserialize/json/json.go b/deserialize/json/json.go new file mode 100644 index 0000000..25e99c5 --- /dev/null +++ b/deserialize/json/json.go @@ -0,0 +1,67 @@ +// Code specific to deserializing JSON. +package json + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/pasqal-io/godasse/deserialize/internal" +) + +// The deserialization driver for JSON. +type Driver struct{} + +// The type of a JSON/Dictionary. +var dictionary = reflect.TypeOf(make(internal.Dict, 0)) + +// The interface for `json.Unmarshaler`. +var unmarshaler = reflect.TypeOf(new(json.Unmarshaler)).Elem() + +// Determine whether we should call the driver to unmarshal values +// of this type from []byte. +// +// For JSON, this is the case if: +// - `typ` represents a dictionary; and/or +// - `typ` implements `json.Unmarshaler`. +// +// You probably won't ever need to call this method. +func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { + if typ.ConvertibleTo(dictionary) { + return true + } + if reflect.PointerTo(typ).ConvertibleTo(unmarshaler) { + return true + } + return false +} + +// Perform unmarshaling. +// +// You probably won't ever need to call this method. +func (u Driver) Unmarshal(buf []byte, out *any) (err error) { + defer func() { + // Attempt to intercept errors that leak implementation details. + if err != nil { + unmarshalErr := json.UnmarshalTypeError{} //nolint:exhaustruct + if errors.Is(err, &unmarshalErr) { + // Go error will mention `map[string] interface{}`, which is an implementation detail. + err = fmt.Errorf("at %s, invalid json value", unmarshalErr.Field) + } + } + }() + + // Attempt de deserialize as a dictionary. + if dict, ok := (*out).(*internal.Dict); ok { + return json.Unmarshal(buf, &dict) //nolint:wrapcheck + } + + // Attempt to deserialize as a `json.Unmarshaler`. + if unmarshal, ok := (*out).(json.Unmarshaler); ok { + return unmarshal.UnmarshalJSON(buf) //nolint:wrapcheck + } + return fmt.Errorf("this type cannot be deserialized") +} + +var _ internal.Driver = Driver{} // Type assertion. diff --git a/deserialize/kvlist/kvlist.go b/deserialize/kvlist/kvlist.go new file mode 100644 index 0000000..3b19b16 --- /dev/null +++ b/deserialize/kvlist/kvlist.go @@ -0,0 +1,55 @@ +package kvlist + +import ( + "encoding/json" + "fmt" + "reflect" + + "github.com/pasqal-io/godasse/deserialize/internal" +) + +// The deserialization driver for (k, value list). +type Driver struct{} + +// The type of a (key, value list) store. +type KVList map[string][]string + +// A type that supports deserialization from bytes. +type Unmarshaler interface { + Unmarshal([]byte) error +} + +// The type KVList. +var kvList = reflect.TypeOf(make(KVList, 0)) + +// The interface `Unmarshaler`. +var unmarshaler = reflect.TypeOf(new(json.Unmarshaler)).Elem() + +// Determine whether we should call the driver to unmarshal values +// of this type from []byte. +// +// For KVList, this is the case if: +// - `typ` represents a KVList; and/or +// - `typ` implements `Unmarshaler`. +func (u Driver) ShouldUnmarshal(typ reflect.Type) bool { + if typ.ConvertibleTo(kvList) { + return true + } + if typ.ConvertibleTo(unmarshaler) { + return true + } + return false +} + +// Perform unmarshaling. +func (u Driver) Unmarshal(buf []byte, out *any) (err error) { + if dict, ok := (*out).(*internal.Dict); ok { + return json.Unmarshal(buf, &dict) //nolint:wrapcheck + } + if unmarshal, ok := (*out).(Unmarshaler); ok { + return unmarshal.Unmarshal(buf) //nolint:wrapcheck + } + return fmt.Errorf("this type cannot be deserialized") +} + +var _ internal.Driver = Driver{} // Type assertion.