diff --git a/bindparam.go b/bindparam.go index e3dc053..5abe5c7 100644 --- a/bindparam.go +++ b/bindparam.go @@ -565,7 +565,7 @@ func BindQueryParameterWithOptions(style string, explode bool, required bool, pa if !explode { return errors.New("deepObjects must be exploded") } - return UnmarshalDeepObject(dest, paramName, queryParams) + return unmarshalDeepObject(dest, paramName, queryParams, required) case "spaceDelimited", "pipeDelimited": return fmt.Errorf("query arguments of style '%s' aren't yet supported", style) default: diff --git a/bindparam_test.go b/bindparam_test.go index ea51058..ef6b76e 100644 --- a/bindparam_test.go +++ b/bindparam_test.go @@ -428,6 +428,76 @@ func TestBindQueryParameter(t *testing.T) { err := BindQueryParameter("deepObject", true, false, paramName, queryParams, &actual) assert.NoError(t, err) assert.Equal(t, expectedDeepObject, actual) + + // If we require values, we require errors when they're not present. + err = BindQueryParameter("deepObject", true, true, "notfound", queryParams, &actual) + assert.Error(t, err) + }) + + t.Run("deepObject/required-and-optional", func(t *testing.T) { + type SimpleObj struct { + Name string `json:"name"` + Age int `json:"age"` + } + + queryWithParam := url.Values{ + "obj[name]": {"Alice"}, + "obj[age]": {"30"}, + } + emptyQuery := url.Values{} + unrelatedQuery := url.Values{ + "other[name]": {"Bob"}, + } + + t.Run("optional/present binds successfully", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, false, "obj", queryWithParam, &dest) + require.NoError(t, err) + assert.Equal(t, SimpleObj{Name: "Alice", Age: 30}, dest) + }) + + t.Run("optional/missing returns no error and does not modify dest", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, false, "obj", emptyQuery, &dest) + require.NoError(t, err) + assert.Equal(t, SimpleObj{}, dest, "destination should remain zero-valued") + }) + + t.Run("optional/missing with unrelated params returns no error", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, false, "obj", unrelatedQuery, &dest) + require.NoError(t, err) + assert.Equal(t, SimpleObj{}, dest, "destination should remain zero-valued") + }) + + t.Run("optional/missing preserves pre-populated dest", func(t *testing.T) { + dest := SimpleObj{Name: "PreExisting", Age: 99} + err := BindQueryParameter("deepObject", true, false, "obj", emptyQuery, &dest) + require.NoError(t, err) + assert.Equal(t, SimpleObj{Name: "PreExisting", Age: 99}, dest, + "destination should not be zeroed out when optional param is absent") + }) + + t.Run("required/present binds successfully", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, true, "obj", queryWithParam, &dest) + require.NoError(t, err) + assert.Equal(t, SimpleObj{Name: "Alice", Age: 30}, dest) + }) + + t.Run("required/missing returns error", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, true, "obj", emptyQuery, &dest) + require.Error(t, err) + assert.Contains(t, err.Error(), "required") + }) + + t.Run("required/missing with unrelated params returns error", func(t *testing.T) { + var dest SimpleObj + err := BindQueryParameter("deepObject", true, true, "obj", unrelatedQuery, &dest) + require.Error(t, err) + assert.Contains(t, err.Error(), "required") + }) }) t.Run("form", func(t *testing.T) { diff --git a/deepobject.go b/deepobject.go index d1b4830..5fb4ae0 100644 --- a/deepobject.go +++ b/deepobject.go @@ -127,6 +127,10 @@ func makeFieldOrValue(paths [][]string, values []string) fieldOrValue { } func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) error { + return unmarshalDeepObject(dst, paramName, params, false) +} + +func unmarshalDeepObject(dst interface{}, paramName string, params url.Values, required bool) error { // Params are all the query args, so we need those that look like // "paramName["... var fieldNames []string @@ -149,6 +153,14 @@ func UnmarshalDeepObject(dst interface{}, paramName string, params url.Values) e } } + if len(fieldNames) == 0 { + if required { + return fmt.Errorf("query parameter '%s' is required", paramName) + } else { + return nil + } + } + // Now, for each field, reconstruct its subscript path and value paths := make([][]string, len(fieldNames)) for i, path := range fieldNames {