Skip to content

Commit e7a92f5

Browse files
committed
Fix optional binding
This fixes #570. The code was mis-structured, and would occasionally allocate a destination for an optional query parameter, when that parameter should have been nil.
1 parent 447d2bd commit e7a92f5

2 files changed

Lines changed: 74 additions & 16 deletions

File tree

bindparam.go

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ func bindSplitPartsToDestinationStruct(paramName string, parts []string, explode
263263
return nil
264264
}
265265

266-
// This works much like BindStyledParameter, however it takes a query argument
266+
// BindQueryParameter works much like BindStyledParameter, however it takes a query argument
267267
// input array from the url package, since query arguments come through a
268268
// different path than the styled arguments. They're also exceptionally fussy.
269269
// For example, consider the exploded and unexploded form parameter examples:
@@ -336,10 +336,12 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
336336
case reflect.Slice:
337337
// In the slice case, we simply use the arguments provided by
338338
// http library.
339+
339340
if !found {
340341
if required {
341342
return fmt.Errorf("query parameter '%s' is required", paramName)
342343
} else {
344+
// If an optional parameter is not found, we do nothing,
343345
return nil
344346
}
345347
}
@@ -349,7 +351,13 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
349351
// form style object binding doesn't tell us which arguments
350352
// in the query string correspond to the object's fields. We'll
351353
// try to bind field by field.
352-
err = bindParamsToExplodedObject(paramName, queryParams, output)
354+
var fieldsPresent bool
355+
fieldsPresent, err = bindParamsToExplodedObject(paramName, queryParams, output)
356+
// If no fields were set, and there is no error, we will not fall
357+
// through to assign the destination.
358+
if !fieldsPresent {
359+
return nil
360+
}
353361
default:
354362
// Primitive object case. We expect to have 1 value to
355363
// unmarshal.
@@ -363,6 +371,15 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
363371
if len(values) != 1 {
364372
return fmt.Errorf("multiple values for single value parameter '%s'", paramName)
365373
}
374+
375+
if !found {
376+
if required {
377+
return fmt.Errorf("query parameter '%s' is required", paramName)
378+
} else {
379+
// If an optional parameter is not found, we do nothing,
380+
return nil
381+
}
382+
}
366383
err = BindStringToObject(values[0], output)
367384
}
368385
if err != nil {
@@ -427,21 +444,28 @@ func BindQueryParameter(style string, explode bool, required bool, paramName str
427444
}
428445
}
429446

430-
// This function reflects the destination structure, and pulls the value for
447+
// bindParamsToExplodedObject reflects the destination structure, and pulls the value for
431448
// each settable field from the given parameters map. This is to deal with the
432449
// exploded form styled object which may occupy any number of parameter names.
433450
// We don't try to be smart here, if the field exists as a query argument,
434-
// set its value.
435-
func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) error {
451+
// set its value. This function returns a boolean, telling us whether there was
452+
// anything to bind. There will be nothing to bind if a parameter isn't found by name,
453+
// or none of an exploded object's fields are present.
454+
func bindParamsToExplodedObject(paramName string, values url.Values, dest interface{}) (bool, error) {
436455
// Dereference pointers to their destination values
437456
binder, v, t := indirect(dest)
438457
if binder != nil {
439-
return BindStringToObject(values.Get(paramName), dest)
458+
_, found := values[paramName]
459+
if !found {
460+
return false, nil
461+
}
462+
return true, BindStringToObject(values.Get(paramName), dest)
440463
}
441464
if t.Kind() != reflect.Struct {
442-
return fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName)
465+
return false, fmt.Errorf("unmarshaling query arg '%s' into wrong type", paramName)
443466
}
444467

468+
fieldsPresent := false
445469
for i := 0; i < t.NumField(); i++ {
446470
fieldT := t.Field(i)
447471

@@ -466,15 +490,16 @@ func bindParamsToExplodedObject(paramName string, values url.Values, dest interf
466490
fieldVal, found := values[fieldName]
467491
if found {
468492
if len(fieldVal) != 1 {
469-
return fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName)
493+
return false, fmt.Errorf("field '%s' specified multiple times for param '%s'", fieldName, paramName)
470494
}
471495
err := BindStringToObject(fieldVal[0], v.Field(i).Addr().Interface())
472496
if err != nil {
473-
return fmt.Errorf("could not bind query arg '%s' to request object: %s'", paramName, err)
497+
return false, fmt.Errorf("could not bind query arg '%s' to request object: %s'", paramName, err)
474498
}
499+
fieldsPresent = true
475500
}
476501
}
477-
return nil
502+
return fieldsPresent, nil
478503
}
479504

480505
// indirect

bindparam_test.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,30 @@ func TestBindQueryParameter(t *testing.T) {
335335
assert.NoError(t, err)
336336
assert.Equal(t, expected, birthday)
337337
})
338+
339+
t.Run("optional", func(t *testing.T) {
340+
queryParams := url.Values{
341+
"time": {"2020-12-09T16:09:53+00:00"},
342+
"number": {"100"},
343+
}
344+
// An optional time will be a pointer to a time in a parameter object
345+
var optionalTime *time.Time
346+
err := BindQueryParameter("form", true, false, "notfound", queryParams, &optionalTime)
347+
require.NoError(t, err)
348+
assert.Nil(t, optionalTime)
349+
350+
var optionalNumber *int
351+
err = BindQueryParameter("form", true, false, "notfound", queryParams, &optionalNumber)
352+
require.NoError(t, err)
353+
assert.Nil(t, optionalNumber)
354+
355+
// If we require values, we require errors when they're not present.
356+
err = BindQueryParameter("form", true, true, "notfound", queryParams, &optionalTime)
357+
assert.Error(t, err)
358+
err = BindQueryParameter("form", true, true, "notfound", queryParams, &optionalNumber)
359+
assert.Error(t, err)
360+
361+
})
338362
}
339363

340364
func TestBindParameterViaAlias(t *testing.T) {
@@ -423,37 +447,46 @@ func TestBindParamsToExplodedObject(t *testing.T) {
423447
}
424448

425449
var dstTime time.Time
426-
err := bindParamsToExplodedObject("time", values, &dstTime)
450+
fieldsPresent, err := bindParamsToExplodedObject("time", values, &dstTime)
427451
assert.NoError(t, err)
452+
assert.True(t, fieldsPresent)
428453
assert.EqualValues(t, now, dstTime)
429454

430455
type AliasedTime time.Time
431456
var aDstTime AliasedTime
432-
err = bindParamsToExplodedObject("time", values, &aDstTime)
457+
fieldsPresent, err = bindParamsToExplodedObject("time", values, &aDstTime)
433458
assert.NoError(t, err)
459+
assert.True(t, fieldsPresent)
434460
assert.EqualValues(t, now, aDstTime)
435461

436462
expectedDate := MockBinder{Time: time.Date(2020, 11, 6, 0, 0, 0, 0, time.UTC)}
437463

438464
var dstDate MockBinder
439-
err = bindParamsToExplodedObject("date", values, &dstDate)
465+
fieldsPresent, err = bindParamsToExplodedObject("date", values, &dstDate)
466+
assert.NoError(t, err)
467+
assert.True(t, fieldsPresent)
440468
assert.EqualValues(t, expectedDate, dstDate)
441469

442470
var eDstDate EmbeddedMockBinder
443-
err = bindParamsToExplodedObject("date", values, &eDstDate)
471+
fieldsPresent, err = bindParamsToExplodedObject("date", values, &eDstDate)
472+
assert.NoError(t, err)
473+
assert.True(t, fieldsPresent)
444474
assert.EqualValues(t, expectedDate, dstDate)
445475

446476
var nTDstDate AnotherMockBinder
447-
err = bindParamsToExplodedObject("date", values, &nTDstDate)
477+
fieldsPresent, err = bindParamsToExplodedObject("date", values, &nTDstDate)
478+
assert.NoError(t, err)
479+
assert.True(t, fieldsPresent)
448480
assert.EqualValues(t, expectedDate, nTDstDate)
449481

450482
type ObjectWithOptional struct {
451483
Time *time.Time `json:"time,omitempty"`
452484
}
453485

454486
var optDstTime ObjectWithOptional
455-
err = bindParamsToExplodedObject("explodedObject", values, &optDstTime)
487+
fieldsPresent, err = bindParamsToExplodedObject("explodedObject", values, &optDstTime)
456488
assert.NoError(t, err)
489+
assert.True(t, fieldsPresent)
457490
assert.EqualValues(t, &now, optDstTime.Time)
458491
}
459492

0 commit comments

Comments
 (0)