Skip to content

Commit

Permalink
[FEATURE] kvlist may now deserialize much more complex structures
Browse files Browse the repository at this point in the history
Prior to this PR, kvlist was restricted to structs in which all fields were `[]string`.
Now, kvlist can be used to deserialize structs in which each fields is either `[]primitive`
(for every trivially-deserializable primitive type) *or* a type that implements `TextUnmarshal`.

For this:

- we get rid of the notion nesting and replace it with an Enter/Exit mechanism
- we use this Enter/Exit to build a startup-time trivial FSM in kvlist, which lets us better control where we're at in the data structure
- we generally try harder to find a solution when we're trying to deserialize an array or slice.
  • Loading branch information
Yoric committed Oct 30, 2024
1 parent 8ef2217 commit c890daa
Show file tree
Hide file tree
Showing 8 changed files with 416 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ linters:
- godox
- gofmt
- goimports
- gomnd
- gomodguard
- gosec
- gosmopolitan
- govet
- mirror
- mnd
- nosprintfhostport
- perfsprint
- prealloc
Expand Down
196 changes: 118 additions & 78 deletions deserialize/deserialize.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions deserialize/deserialize_reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
104 changes: 101 additions & 3 deletions deserialize/deserialize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,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 {
Expand All @@ -103,7 +103,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 {
Expand All @@ -124,7 +124,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
}
Expand Down Expand Up @@ -389,6 +389,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
Expand Down Expand Up @@ -1325,3 +1326,100 @@ 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")
}
22 changes: 17 additions & 5 deletions deserialize/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
Loading

0 comments on commit c890daa

Please sign in to comment.