Repository: algolia/algoliasearch-client-go
Pull request #577 (2020-01-24 14:04:10)

fix: implement correct type for `Settings.UserData`
So far, the inner value of `opt.UserDataOption` was incorrectly typed as a
`map[string]interface{}` while the engine can actually accepts both JSON
objects or JSON literals.

This commit ensures that any literal can now be accepted as a valid input of
`opt.UserData(...)` (by accepting an `interface{}` instead of a map).

To also be able to correctly decode the inner value of the
`opt.UserDataOption`, the method `opt.UserData.Decode(..)` was also
added to let the users decode the inner value into any Go variable.

aseure: More user-oriented point of view, this lets us do:

```go
var yourVariable string // or really any type
err := settings.UserData.Decode(&yourVariable)
```

where before we could only do

```go
var m map[string]interface{}
m = settings.UserData.Get() // panics if the `userData` is not a JSON object
```

Patch:
diff --git a/algolia/internal/gen/options.go b/algolia/internal/gen/options.go
index 060fc4f2..78cc945b 100644
--- a/algolia/internal/gen/options.go
+++ b/algolia/internal/gen/options.go
@@ -137,7 +137,7 @@ var options = []Option{
 	{"explain", Search, []string{}, ""},
 	{"advanced", Settings, 0, ""},
 	{"attributeCriteriaComputedByMinProximity", Settings | Search, false, ""},
-	{"userData", Settings, map[string]interface{}{}, ""},
+	{"userData", Settings, nil, ""},
 
 	// Secured API key
 	{"restrictIndices", Other, []string{}, ""},
diff --git a/algolia/internal/opt/user_data.go b/algolia/internal/opt/user_data.go
index 39fd5dfe..d6687a97 100644
--- a/algolia/internal/opt/user_data.go
+++ b/algolia/internal/opt/user_data.go
@@ -7,22 +7,12 @@ import (
 )
 
 // ExtractUserData returns the first found UserDataOption from the
-// given variadic arguments or nil otherwise. If multiple options are found, the
-// inner maps are merged.
+// given variadic arguments or nil otherwise.
 func ExtractUserData(opts ...interface{}) *opt.UserDataOption {
-	merged := make(map[string]interface{})
-
 	for _, o := range opts {
 		if v, ok := o.(*opt.UserDataOption); ok {
-			for key, value := range v.Get() {
-				merged[key] = value
-			}
+			return v
 		}
 	}
-
-	if len(merged) == 0 {
-		return nil
-	}
-
-	return opt.UserData(merged)
+	return nil
 }
diff --git a/algolia/opt/user_data.go b/algolia/opt/user_data.go
index 7c7a05ce..9ed0fa8d 100644
--- a/algolia/opt/user_data.go
+++ b/algolia/opt/user_data.go
@@ -1,45 +1,69 @@
-// Code generated by go generate. DO NOT EDIT.
-
 package opt
 
 import (
+	"bytes"
 	"encoding/json"
 	"reflect"
+
+	"github.com/algolia/algoliasearch-client-go/v3/algolia/errs"
 )
 
 // UserDataOption is a wrapper for an UserData option parameter. It holds
 // the actual value of the option that can be accessed by calling Get.
 type UserDataOption struct {
-	value map[string]interface{}
+	itf  interface{}
+	data []byte
 }
 
 // UserData wraps the given value into a UserDataOption.
-func UserData(v map[string]interface{}) *UserDataOption {
-	return &UserDataOption{v}
+func UserData(v interface{}) *UserDataOption {
+	return &UserDataOption{itf: v}
 }
 
-// Get retrieves the actual value of the option parameter.
+// Get retrieves the actual value of the option parameter as a map. If the value
+// is not a map, nil is returned instead. In that case, use the Decode() method
+// to appropriately decode the underlying userData into a different instance
+// type.
 func (o *UserDataOption) Get() map[string]interface{} {
 	if o == nil {
 		return map[string]interface{}{}
 	}
-	return o.value
+	m, ok := o.itf.(map[string]interface{})
+	if !ok {
+		return map[string]interface{}{}
+	}
+	return m
+}
+
+func (o *UserDataOption) Decode(itf interface{}) error {
+	if o == nil {
+		return errs.ErrJSONDecode(nil, "UserDataOption")
+	}
+	if o.data == nil {
+		data, err := json.Marshal(o.itf)
+		if err != nil {
+			return err
+		}
+		o.data = data
+	}
+	return json.Unmarshal(o.data, &itf)
 }
 
 // MarshalJSON implements the json.Marshaler interface for
 // UserDataOption.
 func (o UserDataOption) MarshalJSON() ([]byte, error) {
-	return json.Marshal(o.value)
+	return json.Marshal(o.itf)
 }
 
 // UnmarshalJSON implements the json.Unmarshaler interface for
 // UserDataOption.
 func (o *UserDataOption) UnmarshalJSON(data []byte) error {
+	o.data = data
 	if string(data) == "null" {
-		o.value = map[string]interface{}{}
+		o.itf = map[string]interface{}{}
 		return nil
 	}
-	return json.Unmarshal(data, &o.value)
+	return json.Unmarshal(data, &o.itf)
 }
 
 // Equal returns true if the given option is equal to the instance one. In case
@@ -47,12 +71,14 @@ func (o *UserDataOption) UnmarshalJSON(data []byte) error {
 // value of the option.
 func (o *UserDataOption) Equal(o2 *UserDataOption) bool {
 	if o == nil {
-		return o2 == nil || reflect.DeepEqual(o2.value, map[string]interface{}{})
+		return o2 == nil || reflect.DeepEqual(o2.itf, map[string]interface{}{})
 	}
 	if o2 == nil {
-		return o == nil || reflect.DeepEqual(o.value, map[string]interface{}{})
+		return o == nil || reflect.DeepEqual(o.itf, map[string]interface{}{})
 	}
-	return reflect.DeepEqual(o.value, o2.value)
+	data, err := json.Marshal(o.itf)
+	data2, err2 := json.Marshal(o2.itf)
+	return err == nil && err2 == nil && bytes.Equal(data, data2)
 }
 
 // UserDataEqual returns true if the two options are equal.


Test patch:
diff --git a/algolia/internal/opt/user_data_test.go b/algolia/internal/opt/user_data_test.go
index 1aab37e0..6ebdbc41 100644
--- a/algolia/internal/opt/user_data_test.go
+++ b/algolia/internal/opt/user_data_test.go
@@ -1,5 +1,3 @@
-// Code generated by go generate. DO NOT EDIT.
-
 package opt
 
 import (
@@ -27,6 +25,14 @@ func TestExtractUserData(t *testing.T) {
 			opts:     []interface{}{opt.UserData(map[string]interface{}{"k1": 1, "k2": []string{"2", "3"}})},
 			expected: opt.UserData(map[string]interface{}{"k1": 1.0, "k2": []interface{}{"2", "3"}}),
 		},
+		{
+			opts:     []interface{}{opt.UserData(42)},
+			expected: opt.UserData(42),
+		},
+		{
+			opts:     []interface{}{opt.UserData("random string")},
+			expected: opt.UserData("random string"),
+		},
 	} {
 		var (
 			in  = ExtractUserData(c.opts...)
@@ -34,8 +40,11 @@ func TestExtractUserData(t *testing.T) {
 		)
 		data, err := json.Marshal(&in)
 		require.NoError(t, err)
+
 		err = json.Unmarshal(data, &out)
 		require.NoError(t, err)
-		require.Equal(t, *c.expected, out)
+
+		require.True(t, c.expected.Equal(&out), "expected %#v but got %#v", *c.expected, out)
+		require.True(t, out.Equal(c.expected), "expected %#v but got %#v", *c.expected, out)
 	}
 }
diff --git a/algolia/opt/user_data_test.go b/algolia/opt/user_data_test.go
new file mode 100644
index 00000000..c9d81894
--- /dev/null
+++ b/algolia/opt/user_data_test.go
@@ -0,0 +1,39 @@
+package opt
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestUserData_DecodeIntger(t *testing.T) {
+	var i int
+	userData := UserData(42)
+	require.Equal(t, map[string]interface{}{}, userData.Get())
+	require.NoError(t, userData.Decode(&i))
+	require.Equal(t, 42, i)
+}
+
+func TestUserData_DecodeString(t *testing.T) {
+	var s string
+	userData := UserData("algolia")
+	require.Equal(t, map[string]interface{}{}, userData.Get())
+	require.NoError(t, userData.Decode(&s))
+	require.Equal(t, "algolia", s)
+}
+
+func TestUserData_DecodeStringSlice(t *testing.T) {
+	var s []string
+	userData := UserData([]string{"algolia", "Algolia"})
+	require.Equal(t, map[string]interface{}{}, userData.Get())
+	require.NoError(t, userData.Decode(&s))
+	require.Equal(t, []string{"algolia", "Algolia"}, s)
+}
+
+func TestUserData_DecodeMap(t *testing.T) {
+	var m map[string]interface{}
+	userData := UserData(map[string]interface{}{"k1": "v1", "k2": 2})
+	require.Equal(t, map[string]interface{}{"k1": "v1", "k2": 2}, userData.Get())
+	require.NoError(t, userData.Decode(&m))
+	require.Equal(t, map[string]interface{}{"k1": "v1", "k2": float64(2)}, m)
+}